Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion ts/packages/agentServer/server/test/viewModelOptions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ test("the host enables derived Views without an environment opt-in", async () =>
expect((await service.getCapabilities()).derivedViews).toMatchObject({
builds: true,
informationalWiki: true,
kinds: ["troubleshootingGuide", "projectBrief", "timeline", "wiki"],
kinds: [
"troubleshootingGuide",
"projectBrief",
"timeline",
"wiki",
"schemaView",
],
});
} finally {
await service.close();
Expand Down
30 changes: 30 additions & 0 deletions ts/packages/agents/browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,36 @@ puppeteer helpers).

## Architecture

### Schema-defined questions

The Memory hub's Questions workspace is a complete projection of versioned
schema-view slots, including proposed and rejected slots. The inspector reviews
candidates and exact source revisions, records schema-validated user assertions,
and edits versioned rubrics without rewriting source evidence. Question and
answer state, review, freshness and coverage are displayed separately.

Question decisions are ranked, grouped by view and capped at five groups. Dismissal
and one-day snooze use the existing fingerprint preferences; changed evidence
resurfaces decisions, and Questions can restore dismissed decisions. Source
mentions are not independent votes.

Assisted authoring requires explicit approval to generate a proposal and an
explicit maintenance scope; generated slots still require approval. Discovery is
opt-in, bounded and explicitly invoked. Neither operation authorizes tools.
`memoryGetViewPage` returns deterministic Markdown and a `memory-view:` URI;
Markdown links preserve exact revisions, slots and collection item keys when
opened in the browser. Optional service capabilities and operation errors are
reported explicitly, not interpreted as empty inventories. Question lists use
bounded summary pages; selecting a question separately loads its full evidence.
There is no corpus-wide discovery policy or knowledge-status control.

Questions also shows acquisition progress and explicit run-once/cancel controls
for existing host-authorized requests. Its RPC projection excludes staged
content, authorized arguments and raw request errors. The workspace cannot admit
acquisition requests, enable periodic work or authorize tools; unsupported and
disabled hosts are reported explicitly. Ingestion receipts link to ordinary
activity and retained-source pages.

### PDF Understanding Worktree

The PDF viewer supports local open/drop, selected-page semantic inspection,
Expand Down
2 changes: 1 addition & 1 deletion ts/packages/agents/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"test:local": "jest && npm run test:crossword && npm run test:views:gateway && npm run test:views:client",
"test:pdf": "node --test test/pdf*.spec.mjs test/pdfExtraction/*.spec.mjs test/memoryImportHandlers.spec.mjs test/agentConnectionBoundary.spec.mjs",
"test:views:client": "jest --runInBand --testMatch \"**/views/client/**/*.test.ts\"",
"test:views:gateway": "node --test test/viewGateway.test.mjs test/viewDraftGateway.test.mjs test/viewLifecycle.test.mjs test/memoryHub.test.mjs test/memoryHubQuery.test.mjs test/memoryHubSourceDependencies.test.mjs test/memoryHubCapture.test.mjs test/browserCaptureRpc.test.mjs test/memoryHubRunbookImports.test.mjs test/memoryHubWebAnalytics.test.mjs",
"test:views:gateway": "node --test test/viewGateway.test.mjs test/viewDraftGateway.test.mjs test/viewLifecycle.test.mjs test/memoryHub.test.mjs test/memoryHubQuery.test.mjs test/memoryHubSourceDependencies.test.mjs test/memoryHubCapture.test.mjs test/browserCaptureRpc.test.mjs test/memoryHubRunbookImports.test.mjs test/memoryHubWebAnalytics.test.mjs test/schemaQuestionRpc.test.mjs",
"tsc:agent": "tsc -p src/agent/tsconfig.json",
"tsc:puppeteer": "tsc -p src/puppeteer/tsconfig.json",
"tsc:views:client:typecheck": "tsc -p src/views/client/pdf/tsconfig.json && tsc -p src/views/client/library/tsconfig.json",
Expand Down
56 changes: 56 additions & 0 deletions ts/packages/agents/browser/src/agent/memoryAcquisitionProgress.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import type { AcquisitionRequest } from "@typeagent/memory-service";
import type { MemoryAcquisitionProgress } from "@typeagent/browser-control-rpc/serviceTypes";

export function acquisitionProgress(
request: AcquisitionRequest,
): MemoryAcquisitionProgress {
return {
corpusId: request.corpusId,
requestId: request.requestId,
sourceId: request.sourceId,
state: request.state,
createdAt: request.createdAt,
updatedAt: request.updatedAt,
calls: request.calls,
retries: request.retries,
consumers: request.consumers.map(
({ viewId, slotId, definitionRevision }) => ({
viewId,
slotId,
definitionRevision,
}),
),
hasError: Boolean(request.error),
...(request.receipt
? {
receipt: {
jobId: request.receipt.jobId,
revisionId: request.receipt.revisionId,
state: request.receipt.state,
},
}
: {}),
};
}

export async function acquisitionOperation<T>(
operation: () => Promise<T>,
): Promise<T> {
try {
return await operation();
} catch (error) {
const message = error instanceof Error ? error.message : "";
if (/unavailable|unsupported|not configured/i.test(message))
throw new Error(
"Acquisition capability is unavailable on this host.",
);
if (/disabled/i.test(message))
throw new Error("Acquisition is disabled by host policy.");
throw new Error(
"Acquisition operation failed. Request details are restricted to trusted host diagnostics.",
);
}
}
67 changes: 67 additions & 0 deletions ts/packages/agents/browser/src/agent/memoryViews.mts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,28 @@ import type {
MemoryViewService,
} from "@typeagent/memory-service";
import { createMemoryServiceRpcFacade } from "@typeagent/memory-service/rpc";
import type { MemoryAcquisitionProgressService } from "@typeagent/memory-client";
import {
acquisitionProgress,
acquisitionOperation,
} from "./memoryAcquisitionProgress.mjs";

type ViewMethods = Pick<
MemoryCenterInvokeFunctions,
| "memoryViewCapabilities"
| "memoryQueryTopics"
| "memoryRebuildTopics"
| "memoryListViews"
| "memoryListQuestions"
| "memoryListQuestionSummaries"
| "memoryGetQuestion"
| "memoryListAcquisitions"
| "memoryRunAcquisitions"
| "memoryCancelAcquisition"
| "memoryEditQuestion"
| "memoryGetViewPage"
| "memoryProposeSchemaView"
| "memoryDiscoverSchemaQuestions"
| "memoryGetView"
| "memorySaveViewDraft"
| "memoryArchiveView"
Expand Down Expand Up @@ -41,11 +56,63 @@ export function createMemoryViewFunctions(
service: () => MemoryService & Partial<MemoryViewService>,
): ViewMethods {
const views = () => createMemoryServiceRpcFacade(service());
const progressClient = () =>
service() as MemoryService &
Partial<MemoryViewService & MemoryAcquisitionProgressService>;
function required<K extends keyof MemoryViewService>(
name: K,
): NonNullable<MemoryViewService[K]> {
const value = service();
const method = value[name];
if (typeof method !== "function")
throw new Error(
`Schema-defined questions capability unavailable: ${String(name)}`,
);
return method.bind(value) as NonNullable<MemoryViewService[K]>;
}
return {
memoryViewCapabilities: () => service().getCapabilities(),
memoryQueryTopics: (request) => views().queryTopics(request),
memoryRebuildTopics: ({ corpusId }) => views().rebuildTopics(corpusId),
memoryListViews: ({ corpusId }) => views().listViews(corpusId),
memoryListQuestions: ({ corpusId }) =>
required("listQuestions")(corpusId),
memoryListQuestionSummaries: (request) =>
required("listQuestionSummaries")(request),
memoryGetQuestion: (request) => required("getQuestion")(request),
memoryListAcquisitions: ({ corpusId }) =>
acquisitionOperation(async () => {
const client = progressClient();
if (client.listAcquisitionProgress)
return client.listAcquisitionProgress(corpusId);
return (await required("listAcquisitions")(corpusId)).map(
acquisitionProgress,
);
}),
memoryRunAcquisitions: ({ corpusId }) =>
acquisitionOperation(async () => {
const client = progressClient();
if (client.runAcquisitionProgress)
return client.runAcquisitionProgress(corpusId);
return (await required("runAcquisitions")(corpusId)).map(
acquisitionProgress,
);
}),
memoryCancelAcquisition: (request) =>
acquisitionOperation(async () => {
const client = progressClient();
if (client.cancelAcquisitionProgress)
return client.cancelAcquisitionProgress(request);
return acquisitionProgress(
await required("cancelAcquisition")(request),
);
}),
memoryEditQuestion: (request) => required("editQuestion")(request),
memoryGetViewPage: (request) => required("getViewPage")(request),
memoryProposeSchemaView: (request) =>
required("proposeSchemaView")(request),
memoryDiscoverSchemaQuestions: (request) =>
required("discoverSchemaQuestions")(request),
memoryGetView: (request) => views().getView(request),
memorySaveViewDraft: (request) => views().saveViewDraft(request),
memoryArchiveView: (request) => views().archiveView(request),
Expand Down
140 changes: 140 additions & 0 deletions ts/packages/agents/browser/src/views/client/library/memoryHub.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,146 @@ body[data-memory-hub] {
--accent-soft-hover: color-mix(in srgb, var(--primary) 16%, var(--surface));
}

#page-questions .hub-qhead,
#page-questions .hub-qfilters,
#page-questions .hub-row-actions,
#page-questions .hub-qmeta,
#page-questions .hub-qdetail-head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
#page-questions .hub-qfilters {
margin-bottom: 12px;
}
#page-questions .hub-qfilters input[type="search"] {
flex: 1 1 180px;
max-width: 320px;
min-height: 30px;
}
#page-questions .hub-rows {
display: grid;
gap: 2px;
margin-top: 6px;
}
#page-questions .hub-grow {
flex: 1 1 auto;
min-width: 0;
}
#page-questions .hub-muted,
#page-questions .hub-qmeta {
color: var(--muted);
font-size: 13px;
}
#page-questions .hub-notice {
margin-bottom: 12px;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface-alt);
}
#page-questions .hub-split {
display: grid;
grid-template-columns: minmax(280px, 2fr) minmax(0, 3fr);
gap: 16px;
align-items: start;
}
#page-questions .hub-qlist {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
#page-questions .hub-qrow {
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
cursor: pointer;
}
#page-questions .hub-qrow:hover {
background: var(--surface-alt);
}
#page-questions .hub-qrow.selected {
border-color: var(--primary);
background: var(--accent-soft-hover);
}
#page-questions .hub-qrow > button {
display: block;
width: 100%;
padding: 0;
border: 0;
background: none;
color: inherit;
font: inherit;
font-weight: 600;
text-align: left;
cursor: pointer;
}
#page-questions .hub-qmeta {
margin-top: 4px;
}
#page-questions .hub-qmeta small {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#page-questions .hub-qdetail {
position: sticky;
top: 8px;
max-height: calc(100vh - 24px);
min-width: 0;
overflow: auto;
padding: 0 4px 16px;
}
#page-questions .hub-qdetail:empty::before {
content: "Select a question to see its answers.";
color: var(--muted);
}
#page-questions .hub-qdetail-head h3 {
flex: 1 1 100%;
margin: 0;
font-size: 16px;
}
#page-questions .hub-qdetail details {
margin: 10px 0;
}
#page-questions summary {
cursor: pointer;
font-weight: 600;
}
#page-questions .hub-fields {
display: grid;
grid-template-columns: max-content 1fr;
gap: 2px 12px;
margin: 0 0 6px;
}
#page-questions .hub-fields dt {
color: var(--muted);
font-size: 13px;
}
#page-questions .hub-fields dd {
margin: 0;
}
#page-questions .hub-panel {
margin-top: 20px;
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 8px;
}
@media (max-width: 900px) {
#page-questions .hub-split {
grid-template-columns: minmax(0, 1fr);
}
#page-questions .hub-qdetail {
position: static;
max-height: none;
}
}

[hidden] {
display: none !important;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ <h1><i class="fa-solid fa-brain" aria-hidden="true"></i>Memory</h1>
><i class="fa-solid fa-list-check" aria-hidden="true"></i
><span class="hub-nav-label">Views</span></a
>
<a href="#/questions" title="Questions"
><i class="fa-solid fa-circle-question" aria-hidden="true"></i
><span class="hub-nav-label">Questions</span></a
>
<a href="#/explore" title="Explore (Alt+4)"
><i class="fa-solid fa-compass" aria-hidden="true"></i
><span class="hub-nav-label">Explore</span></a
Expand Down Expand Up @@ -157,6 +161,10 @@ <h2>Search and Ask</h2>
<div class="hub-page-head"><h2>Views</h2></div>
<div id="hubDraftViews"></div>
</section>
<section id="page-questions" class="hub-page hidden">
<div id="hubQuestions"></div>
<div id="hubQuestionAttention"></div>
</section>
<section id="page-explore" class="hub-page hidden">
<div class="hub-page-head">
<h2>Explore</h2>
Expand Down
Loading
Loading