From 520d5b1b11eed53544660ebc16fe5251f777114b Mon Sep 17 00:00:00 2001 From: AstroHan Date: Mon, 21 Sep 2026 11:17:47 +0800 Subject: [PATCH 1/4] refactor: retire the Deep Research workflow Remove the specialized research runtime, protocol, tools, storage writer, desktop entry points and progress UI. Preserve legacy chats and report artifacts under their existing permission boundaries, and advance the Runtime Host compatibility epoch. Generated-by: Codex --- apps/desktop/e2e/fixtures.ts | 6 +- apps/desktop/renderer-architecture.json | 59 +- .../__tests__/create-session-input.test.ts | 34 +- .../__tests__/runtime-host-client-uds.test.ts | 5 - .../runtime-host-desktop-candidate.test.ts | 8 +- ...time-host-session-catalog-ipc-main.test.ts | 6 +- ...time-host-session-domains-ipc-main.test.ts | 146 +-- apps/desktop/src/main/create-session-input.ts | 10 +- .../main/deep-research-desktop-projection.ts | 64 - apps/desktop/src/main/runtime-host-client.ts | 6 - .../runtime-host-session-domains-ipc-main.ts | 14 - apps/desktop/src/preload/bridge-contract.d.ts | 5 - apps/desktop/src/preload/preload.ts | 14 - .../src/renderer/app-shell-command-actions.ts | 6 - .../app-shell-session-start-actions.ts | 187 --- apps/desktop/src/renderer/app-shell.tsx | 28 - .../contracts/session-start-mode.ts | 20 - .../src/renderer/chat-message-surface.tsx | 9 - .../src/renderer/command-palette-commands.ts | 14 - .../src/renderer/locales/shell-copy.ts | 12 - apps/desktop/src/renderer/maka-tokens.css | 3 +- apps/desktop/src/renderer/styles.css | 1 - .../src/renderer/styles/deep-research.css | 254 ---- apps/desktop/src/renderer/styles/hero.css | 2 +- .../src/renderer/use-deep-research-run.ts | 59 - apps/desktop/stories/app-shell.stories.tsx | 3 +- .../stories/command-search.stories.tsx | 12 +- docs/README.md | 1 - docs/astryx-surface-file-inventory.md | 5 +- docs/astryx-surface-file-inventory.paths | 1 - docs/deep-research-durable-workspace.md | 341 ------ packages/core/package.json | 3 - .../src/__tests__/deep-research-run.test.ts | 233 ---- .../__tests__/deep-research-session.test.ts | 36 - .../core/src/__tests__/deep-research.test.ts | 31 - .../src/__tests__/tool-quiet-preview.test.ts | 16 - packages/core/src/artifacts.ts | 3 +- .../core/src/deep-research-client-progress.ts | 153 --- packages/core/src/deep-research-run.ts | 1055 ----------------- packages/core/src/deep-research.ts | 228 ---- packages/core/src/session-start-mode.ts | 7 +- packages/core/src/session.ts | 2 - packages/core/src/settings.ts | 2 +- packages/core/src/tool-quiet-preview.ts | 21 +- .../__tests__/deep-research-protocol.test.ts | 270 ----- .../deep-research-two-client-uds.test.ts | 149 --- .../interactive-run-composer.test.ts | 25 +- .../session-catalog-coordinator.test.ts | 44 +- .../session-catalog-protocol.test.ts | 6 +- .../session-catalog-two-client-uds.test.ts | 15 - .../session-revision-two-client-uds.test.ts | 34 +- .../src/protocol/deep-research.ts | 385 ------ packages/runtime-host/src/protocol/index.ts | 2 +- .../runtime-host/src/protocol/operations.ts | 4 - .../src/protocol/session-continuity.ts | 8 +- .../src/server/deep-research-coordinator.ts | 164 --- .../src/server/execution-composition.ts | 35 - .../src/server/interactive-run-composer.ts | 31 +- .../src/server/operation-dispatcher.ts | 3 - .../server/session-revision-coordinator.ts | 7 - packages/runtime/package.json | 1 - .../src/__tests__/deep-research-tools.test.ts | 582 --------- .../src/__tests__/session-manager.test.ts | 29 +- packages/runtime/src/deep-research-tools.ts | 1004 ---------------- packages/runtime/src/session-manager.ts | 16 +- packages/storage/package.json | 2 - .../__tests__/artifact-copy-replay.test.ts | 1 - .../src/__tests__/artifact-store.test.ts | 15 +- .../src/__tests__/public-entrypoints.test.ts | 2 - .../__tests__/sqlite-workflow-store.test.ts | 33 - .../storage/src/artifact-metadata-codec.ts | 7 +- packages/storage/src/artifact-store.ts | 18 +- .../storage/src/deep-research-authority.ts | 199 ---- packages/storage/src/deep-research-store.ts | 511 -------- .../storage/src/sqlite-workflow-schema.ts | 9 - .../storage/src/storage-writer-composition.ts | 7 - packages/ui/src/chat-empty-hero.tsx | 115 +- packages/ui/src/chat-view.tsx | 124 +- packages/ui/src/conversation-copy.ts | 185 +-- packages/ui/src/session-context-layer.tsx | 23 - 80 files changed, 133 insertions(+), 7057 deletions(-) delete mode 100644 apps/desktop/src/main/deep-research-desktop-projection.ts delete mode 100644 apps/desktop/src/renderer/app-shell-session-start-actions.ts delete mode 100644 apps/desktop/src/renderer/application/contracts/session-start-mode.ts delete mode 100644 apps/desktop/src/renderer/styles/deep-research.css delete mode 100644 apps/desktop/src/renderer/use-deep-research-run.ts delete mode 100644 docs/deep-research-durable-workspace.md delete mode 100644 packages/core/src/__tests__/deep-research-run.test.ts delete mode 100644 packages/core/src/__tests__/deep-research-session.test.ts delete mode 100644 packages/core/src/__tests__/deep-research.test.ts delete mode 100644 packages/core/src/deep-research-client-progress.ts delete mode 100644 packages/core/src/deep-research-run.ts delete mode 100644 packages/core/src/deep-research.ts delete mode 100644 packages/runtime-host/src/__tests__/deep-research-protocol.test.ts delete mode 100644 packages/runtime-host/src/__tests__/deep-research-two-client-uds.test.ts delete mode 100644 packages/runtime-host/src/protocol/deep-research.ts delete mode 100644 packages/runtime-host/src/server/deep-research-coordinator.ts delete mode 100644 packages/runtime/src/__tests__/deep-research-tools.test.ts delete mode 100644 packages/runtime/src/deep-research-tools.ts delete mode 100644 packages/storage/src/deep-research-authority.ts delete mode 100644 packages/storage/src/deep-research-store.ts diff --git a/apps/desktop/e2e/fixtures.ts b/apps/desktop/e2e/fixtures.ts index 31c14b6c9e..45e05ec985 100644 --- a/apps/desktop/e2e/fixtures.ts +++ b/apps/desktop/e2e/fixtures.ts @@ -277,7 +277,7 @@ async function seedE2eInvocableSkills(userDataDir: string): Promise { mkdir(path.join(projectSkillRoot, 'project-only'), { recursive: true }), mkdir(path.join(projectSkillRoot, 'host-incompatible'), { recursive: true }), mkdir(path.join(projectSkillRoot, 'agent-write'), { recursive: true }), - mkdir(path.join(projectSkillRoot, 'deep-research-only'), { recursive: true }), + mkdir(path.join(projectSkillRoot, 'unavailable-tool'), { recursive: true }), mkdir(path.join(workspaceSkillRoot, 'workspace-only'), { recursive: true }), mkdir(path.join(userSkillRoot, 'user-only'), { recursive: true }), ]); @@ -303,8 +303,8 @@ async function seedE2eInvocableSkills(userDataDir: string): Promise { 'utf8', ), writeFile( - path.join(projectSkillRoot, 'deep-research-only', 'SKILL.md'), - `---\nname: Deep Research Only\ndescription: Requires a tool available only in Deep Research mode.\nrequired-tools: [deep_research_status]\n---\n# Deep Research Only`, + path.join(projectSkillRoot, 'unavailable-tool', 'SKILL.md'), + `---\nname: Unavailable Tool\ndescription: Requires a tool unavailable on this Host.\nrequired-tools: [unavailable_fixture_tool]\n---\n# Unavailable Tool`, 'utf8', ), writeFile( diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json index 5802fac9b9..6bbde631df 100644 --- a/apps/desktop/renderer-architecture.json +++ b/apps/desktop/renderer-architecture.json @@ -16,7 +16,6 @@ "src/renderer/app-shell-project-actions.ts", "src/renderer/app-shell-revision-actions.ts", "src/renderer/app-shell-session-events.ts", - "src/renderer/app-shell-session-start-actions.ts", "src/renderer/app-shell-session-ui-state.ts", "src/renderer/app-shell-stop-action.ts", "src/renderer/app-shell-turn-actions.ts", @@ -208,7 +207,6 @@ "src/renderer/use-app-shell-session-list.ts", "src/renderer/use-app-shell-session-ui-reads.ts", "src/renderer/use-app-shell-session-workspace.ts", - "src/renderer/use-deep-research-run.ts", "src/renderer/use-new-task-choice.ts", "src/renderer/use-onboarding-snapshot.ts", "src/renderer/use-project-context.ts", @@ -381,7 +379,7 @@ "react": 1 }, "importSpecifiers": 8, - "nonTriviaTokens": 2234 + "nonTriviaTokens": 2189 }, "src/renderer/app-shell-context-compaction.ts": { "importDeclarations": 0, @@ -605,27 +603,6 @@ "importSpecifiers": 7, "nonTriviaTokens": 2687 }, - "src/renderer/app-shell-session-start-actions.ts": { - "importDeclarations": 2, - "bridgePaths": { - "window.maka.newTasks.create": 1, - "window.maka.onboarding.setMilestone": 1 - }, - "environmentCapabilities": {}, - "hookCalls": {}, - "lifecycleMethods": {}, - "unresolvedDependencies": 0, - "actionFactories": [ - "createAppShellSessionStartActions" - ], - "dependencyPaths": { - "./locales/shell-copy.js": 1, - "./model-connection-errors.js": 1, - "./session-workspace-errors.js": 1 - }, - "importSpecifiers": 5, - "nonTriviaTokens": 650 - }, "src/renderer/app-shell-session-ui-state.ts": { "importDeclarations": 0, "bridgePaths": {}, @@ -704,7 +681,7 @@ "nonTriviaTokens": 1245 }, "src/renderer/app-shell.tsx": { - "importDeclarations": 63, + "importDeclarations": 62, "bridgePaths": { "window.maka.attachments": 1, "window.maka.attachments.readBytes": 1, @@ -727,7 +704,7 @@ }, "environmentCapabilities": { "window.clearTimeout": 1, - "window.requestAnimationFrame": 4, + "window.requestAnimationFrame": 3, "window.setTimeout": 1 }, "hookCalls": { @@ -748,7 +725,7 @@ "useNewTaskChoice": 1, "useOnboardingSnapshot": 1, "usePlanModeState": 1, - "useRef": 16, + "useRef": 15, "useSessionCollaborationDialog": 1, "useSessionEventHealthPolling": 1, "useSessionNavigationReads": 1, @@ -760,7 +737,7 @@ "useShellMemoryPill": 1, "useShellResume": 1, "useShellRunUpdates": 1, - "useStableActions": 6, + "useStableActions": 5, "useState": 13, "useSystemUiLocale": 1, "useTaskSubmissionReadiness": 1, @@ -782,7 +759,6 @@ "./app-shell-overlays": 1, "./app-shell-revision-actions": 1, "./app-shell-session-events": 1, - "./app-shell-session-start-actions": 1, "./app-shell-stop-action": 1, "./app-shell-turn-actions": 1, "./app-shell-turn-view-model": 1, @@ -853,8 +829,8 @@ "@maka/ui": 1, "react": 1 }, - "importSpecifiers": 99, - "nonTriviaTokens": 12970 + "importSpecifiers": 98, + "nonTriviaTokens": 12840 }, "src/renderer/use-app-shell-composer-quotes.ts": { "importDeclarations": 0, @@ -1095,7 +1071,6 @@ "bridgePaths": {}, "environmentCapabilities": {}, "hookCalls": { - "useDeepResearchRun": 1, "useExternalStoreSelector": 2, "useState": 1, "useUiLocale": 1 @@ -1109,9 +1084,7 @@ "./features/conversation/index.js": 1, "./locales/shell-copy": 1, "./onboarding-hero": 1, - "./use-deep-research-run": 1, "@astryxdesign/core": 1, - "@maka/core/deep-research": 1, "@maka/ui": 1, "react": 1 } @@ -3894,23 +3867,6 @@ "react": 1 } }, - "src/renderer/use-deep-research-run.ts": { - "bridgePaths": { - "window.maka.deepResearch.get": 1, - "window.maka.deepResearch.subscribeChanges": 1 - }, - "environmentCapabilities": {}, - "hookCalls": { - "useEffect": 1, - "useState": 1 - }, - "lifecycleMethods": {}, - "unresolvedDependencies": 0, - "actionFactories": [], - "dependencyPaths": { - "react": 1 - } - }, "src/renderer/use-new-task-choice.ts": { "bridgePaths": {}, "environmentCapabilities": {}, @@ -4421,7 +4377,6 @@ "targetZone": "features/conversation", "legacyPaths": [ "src/renderer/app-shell-chat-actions.ts", - "src/renderer/app-shell-session-start-actions.ts", "src/renderer/use-app-shell-composer-quotes.ts" ] }, diff --git a/apps/desktop/src/main/__tests__/create-session-input.test.ts b/apps/desktop/src/main/__tests__/create-session-input.test.ts index fb20348747..2ecc5daefa 100644 --- a/apps/desktop/src/main/__tests__/create-session-input.test.ts +++ b/apps/desktop/src/main/__tests__/create-session-input.test.ts @@ -30,7 +30,6 @@ import { strict as assert } from 'node:assert'; import { describe, it } from 'node:test'; -import { DEEP_RESEARCH_SESSION_LABEL } from '@maka/core/deep-research'; import { DEFAULT_SESSION_NAME } from '@maka/core/session-name'; @@ -59,8 +58,8 @@ describe('resolveCreateSessionRequest', () => { }); it('passes a product mode through verbatim for the Host to expand', () => { - assert.deepEqual(resolve({ mode: 'deep_research' }), { - mode: 'deep_research', + assert.deepEqual(resolve({ mode: 'bot' }), { + mode: 'bot', collaborationMode: 'agent', orchestrationMode: 'default', name: DEFAULT_SESSION_NAME, @@ -68,15 +67,6 @@ describe('resolveCreateSessionRequest', () => { }); }); - /** - * `explore` is a boundary a mode confers, never one a caller may open a - * session at — core names the pickable set `ChatDefaultPermissionMode`. - * Without this refusal the seed is only a default: a renderer could ask for - * `explore` outright and get it without the Deep Research label, tools or - * system prompt that define the mode. `sessions:setPermissionMode` stays the - * separate, deliberate path for moving an EXISTING session (the quote - * companion relies on it), so the guard belongs on creation only. - */ it('refuses a directly-requested explore boundary', () => { assert.throws(() => resolve({ permissionMode: 'explore' }), TypeError); assert.throws(() => resolve({ permissionMode: 'nonsense' }), TypeError); @@ -87,23 +77,19 @@ describe('resolveCreateSessionRequest', () => { assert.throws(() => resolve({ orchestrationMode: 'nonsense' }), TypeError); }); - /** - * The mode is a closed set, exercised with the raw values a renderer can - * actually put on the wire. An unrecognized mode must not reach the Host as - * one — it simply is not a mode. - */ - it('drops an unrecognized mode from the renderer', () => { + it('rejects the retired research workflow', () => { + assert.throws(() => resolve({ mode: 'deep_research' }), /Invalid session start mode/); + }); + + it('drops other unrecognized modes from the renderer', () => { for (const mode of ['explore', 'deep-reseach', 'chat', 'admin', '', null, 42, {}]) { - const resolved = resolve({ mode }); - assert.equal(resolved.mode, undefined, `mode ${JSON.stringify(mode)} reached the wire`); - assert.equal(resolved.name, DEFAULT_SESSION_NAME); - assert.equal(resolved.labels, undefined); + assert.equal(resolve({ mode }).mode, undefined); } }); it("carries the caller's name and labels when no mode overrides them", () => { - const resolved = resolve({ name: 'Release notes', labels: ['pinned', DEEP_RESEARCH_SESSION_LABEL] }); + const resolved = resolve({ name: 'Release notes', labels: ['pinned', 'mode:bot'] }); assert.equal(resolved.name, 'Release notes'); - assert.deepEqual(resolved.labels, ['pinned', DEEP_RESEARCH_SESSION_LABEL]); + assert.deepEqual(resolved.labels, ['pinned', 'mode:bot']); }); }); diff --git a/apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts b/apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts index d0b67b69b2..8af0e2e3ae 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts @@ -493,10 +493,6 @@ test('drives bounded Session domain projections through real UDS framing', async ok: true, result: { sessionId: input.sessionId, goal: null }, }), - 'deep-research.query': async (input) => ({ - ok: true, - result: { kind: 'not_started', sessionId: input.sessionId, revision: 0 }, - }), 'runtime.resource.query': async (input) => ({ ok: true, result: { @@ -542,7 +538,6 @@ test('drives bounded Session domain projections through real UDS framing', async executions: [], }); assert.equal(await ipc.invoke('goal:get', 'session-1'), null); - assert.equal(await ipc.invoke('deepResearch:get', 'session-1'), undefined); assert.deepEqual(await ipc.invoke('shell-runs:list', 'session-1'), []); await client.close(); diff --git a/apps/desktop/src/main/__tests__/runtime-host-desktop-candidate.test.ts b/apps/desktop/src/main/__tests__/runtime-host-desktop-candidate.test.ts index 7509f10fd9..a3fae5a713 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-desktop-candidate.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-desktop-candidate.test.ts @@ -714,16 +714,16 @@ test('drains an accepted Host-backed Bot turn before closing its generation', as test('rolls back only candidate-owned IPC after a registration collision', async () => { const ipc = ipcHarness(); - ipc.handle('deepResearch:get', async () => 'embedded'); + ipc.handle('todo:read', async () => 'embedded'); const host = connectionHarness('collision'); await assert.rejects( () => createDesktopRuntimeHostCandidate(host.connection, deps(ipc)), - /duplicate handler: deepResearch:get/, + /duplicate handler: todo:read/, ); - assert.equal(await ipc.invoke('deepResearch:get'), 'embedded'); - assert.deepEqual(ipc.channels, ['deepResearch:get']); + assert.equal(await ipc.invoke('todo:read'), 'embedded'); + assert.deepEqual(ipc.channels, ['todo:read']); assert.equal(host.closeCalls, 1); }); diff --git a/apps/desktop/src/main/__tests__/runtime-host-session-catalog-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-session-catalog-ipc-main.test.ts index 0f7cff7040..9735c56686 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-session-catalog-ipc-main.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-session-catalog-ipc-main.test.ts @@ -51,13 +51,15 @@ test('session creation forwards the caller name for a mode that carries none', a registerRuntimeHostSessionCatalogIpc(createDeps(creates), ipc as unknown as IpcMain); await ipc.invoke('sessions:create', { mode: 'bot', name: '飞书 任务' }); - await ipc.invoke('sessions:create', { mode: 'deep_research', name: '飞书 任务' }); + await assert.rejects( + () => ipc.invoke('sessions:create', { mode: 'deep_research', name: '飞书 任务' }), + /Invalid session start mode/, + ); assert.deepEqual( creates.map((input) => [input.mode, input.name]), [ ['bot', '飞书 任务'], - ['deep_research', '飞书 任务'], ], ); }); diff --git a/apps/desktop/src/main/__tests__/runtime-host-session-domains-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-session-domains-ipc-main.test.ts index 921b8c55b9..79de0255b9 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-session-domains-ipc-main.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-session-domains-ipc-main.test.ts @@ -23,12 +23,9 @@ import { deferred } from '@maka/core/test-only/async-primitives'; import { TerminalCloseIntents } from '../terminal-close-intents.js'; import type { TerminalCloseChange, TerminalRecovery } from '../../shared/runtime-host-identity.js'; import type { IpcMain } from 'electron'; -import { projectDeepResearchClientProgress } from '@maka/core/deep-research-client-progress'; -import { type DeepResearchRun } from '@maka/core/deep-research-run'; import { emptyPlanSessionState, type PlanSessionState } from '@maka/core/plan'; import { type ShellRunUpdate } from '@maka/core/events'; import { - encodeDeepResearchSnapshot, type GoalProjection, } from '@maka/runtime-host/protocol'; import { @@ -39,10 +36,6 @@ import type { ReconciledControlHandlers, ReconnectableReadIpcMain, } from '../ipc-reconnect-policy.js'; -import { - projectEmbeddedDeepResearch, - projectHostedDeepResearch, -} from '../deep-research-desktop-projection.js'; import { registerRuntimeHostSessionDomainsIpc, type RuntimeHostSessionDomainsIpcDeps, @@ -452,7 +445,7 @@ test('goal:arm takes the Session from the scoped channel and refuses any other k assert.equal(armed.length, 2); }); -test('adapts Host Goal, Task, Deep Research, and Resource projections', async () => { +test('adapts Host Goal, Task, and Resource projections', async () => { const controls: unknown[] = []; const client = domainClient({ querySessionTodo: async () => [{ content: 'todo-1', status: 'pending' }] as never, @@ -467,7 +460,6 @@ test('adapts Host Goal, Task, Deep Research, and Resource projections', async () controlGoalWithRetry: async (sessionId, action) => { controls.push({ sessionId, action }); }, - queryDeepResearch: async () => hostedResearch(), }); const ipc = ipcHarness(); registerDomainsIpc({ client, emitModeChanged() {} }, ipc); @@ -502,29 +494,6 @@ test('adapts Host Goal, Task, Deep Research, and Resource projections', async () { sessionId: 'session-1', action: 'pause' }, { sessionId: 'session-1', action: 'resume' }, ]); - assert.deepEqual(await ipc.invoke('deepResearch:get', 'session-1'), { - sessionId: 'session-1', - objective: 'Inspect the adapter', - scopeLevel: 'standard', - status: 'completed', - stage: 'completed', - round: 2, - createdAt: 1, - updatedAt: 2, - artifactsCount: 4, - stepsCount: 3, - checklist: [ - { itemId: 'entrypoints', title: 'Map entrypoints', status: 'completed' }, - ], - reportSections: [{ key: 'conclusion', status: 'completed' }], - recentInspectedRefs: [ - { kind: 'file', locator: 'apps/desktop/src/main/runtime-host-boot.ts' }, - ], - workerRunIds: ['run-1'], - blockers: [], - reportArtifactId: 'artifact-1', - implementationPrompt: 'Implement the result.', - }); }); test('adapts bounded Agent Graph epoch reads without changing graph identity', async () => { @@ -1047,7 +1016,6 @@ test('publishes typed invalidations and refreshes only changed Runtime Resources ); handle.sessionDomainChanged({ sessionId: 'session-1', domain: 'todo' }); - handle.sessionDomainChanged({ sessionId: 'session-1', domain: 'deep_research' }); handle.sessionDomainChanged({ sessionId: 'session-1', domain: 'plan' }); handle.sessionDomainChanged({ sessionId: 'session-1', domain: 'usage' }); handle.sessionDomainChanged({ @@ -1075,10 +1043,6 @@ test('publishes typed invalidations and refreshes only changed Runtime Resources channel: 'todo:changed', payload: { sessionId: 'session-1', at: 12 }, }, - { - channel: 'deepResearch:changed', - payload: { sessionId: 'session-1', ts: 12 }, - }, { channel: 'plan-mode:changed', payload: { sessionId: 'session-1' }, @@ -1118,10 +1082,6 @@ test('publishes typed invalidations and refreshes only changed Runtime Resources channel: 'todo:changed', payload: { sessionId: 'session-1', at: 12 }, }, - { - channel: 'deepResearch:changed', - payload: { sessionId: 'session-1', ts: 12 }, - }, { channel: 'plan-mode:changed', payload: { sessionId: 'session-1' }, @@ -1141,72 +1101,6 @@ test('publishes typed invalidations and refreshes only changed Runtime Resources ]); }); -test('projects embedded and hosted Deep Research into one renderer contract', () => { - const run: DeepResearchRun = { - schemaVersion: 1, - sessionId: 'session-1', - objective: 'Inspect the adapter', - scopeLevel: 'standard', - status: 'blocked', - stage: 'knowledge_base', - round: 1, - createdAt: 1, - updatedAt: 2, - artifacts: [], - checklist: [], - steps: [{ - stepId: 'step-1', - kind: 'local_exploration', - status: 'blocked', - objective: 'Inspect the adapter', - summary: 'The adapter cannot continue.', - roots: [], - keywords: [], - ignoredPaths: [], - stoppingCondition: 'The boundary is known.', - expectedEvidence: 'A stable projection.', - evidenceArtifactIds: [], - inspectedRefs: [], - workerRunIds: [], - blockedReason: 'Runtime credentials are unavailable.', - createdAt: 2, - }], - reportSections: [], - checkpoints: [], - }; - const embedded = projectEmbeddedDeepResearch(run); - const hosted = projectHostedDeepResearch( - encodeDeepResearchSnapshot(projectDeepResearchClientProgress(run), 1), - ); - - assert.deepEqual(hosted, embedded); - assert.deepEqual(hosted?.blockers, [ - 'Inspect the adapter: Runtime credentials are unavailable.', - ]); - - const checklistBlocked = structuredClone(run); - checklistBlocked.checklist = [{ - itemId: 'blocked-item', - title: '🙂'.repeat(240), - status: 'blocked', - evidenceArtifactIds: [], - blockedReason: 'Credentials are unavailable.', - updatedAt: 2, - }]; - assert.match( - projectDeepResearchClientProgress(checklistBlocked).blockers[0] ?? '', - /Credentials are unavailable\./, - ); - - const stepBlocked = structuredClone(run); - if (!stepBlocked.steps[0]) throw new Error('Missing Deep Research step fixture'); - stepBlocked.steps[0].objective = '🙂'.repeat(240); - assert.match( - projectDeepResearchClientProgress(stepBlocked).blockers.at(-1) ?? '', - /Runtime credentials are unavailable\./, - ); -}); - function domainClient(overrides: Partial): DomainClient { const unavailable = async () => { throw new Error('Unexpected domain operation'); @@ -1226,7 +1120,6 @@ function domainClient(overrides: Partial): DomainClient { querySessionTodo: unavailable, queryAgentGraph: unavailable, queryAgentGraphOperator: unavailable, - queryDeepResearch: unavailable, queryGoal: unavailable, releaseRuntimeResourceController: unavailable, startRuntimeResource: unavailable, @@ -1295,43 +1188,6 @@ function graphSnapshot(rootSessionId: string, graphId: string) { }; } -function hostedResearch() { - return { - kind: 'snapshot' as const, - sessionId: 'session-1', - revision: 4, - objective: 'Inspect the adapter', - scopeLevel: 'standard' as const, - status: 'completed' as const, - stage: 'completed' as const, - round: 2, - createdAt: 1, - updatedAt: 2, - artifactsCount: 4, - stepsCount: 3, - checklist: [ - { - itemId: 'entrypoints', - title: 'Map entrypoints', - status: 'completed' as const, - blockedReason: null, - }, - ], - reportSections: [{ key: 'conclusion' as const, status: 'completed' as const }], - recentInspectedRefs: [ - { - kind: 'file' as const, - locator: 'apps/desktop/src/main/runtime-host-boot.ts', - label: null, - }, - ], - workerRunIds: ['run-1'], - blockers: [], - reportArtifactId: 'artifact-1', - implementationPrompt: 'Implement the result.', - }; -} - function shellRunUpdate(overrides: Partial = {}): ShellRunUpdate { return { sessionId: 'session-1', diff --git a/apps/desktop/src/main/create-session-input.ts b/apps/desktop/src/main/create-session-input.ts index d098cf9614..18b7b72009 100644 --- a/apps/desktop/src/main/create-session-input.ts +++ b/apps/desktop/src/main/create-session-input.ts @@ -54,12 +54,7 @@ import { isOrchestrationMode } from '@maka/core/orchestration'; import { isSessionStartMode } from '@maka/core/session-start-mode'; -/** - * `unknown`, because this is an IPC boundary and the renderer's type is a - * promise, not a guarantee. An unrecognized value confers nothing — it is not - * a mode — and the caller falls through to an ordinary session, which is the - * same session it would have got by not naming one. - */ +/** Reject the retired workflow explicitly; other unknown modes still fall back to ordinary chat. */ export interface CreateSessionRequest { mode?: SessionStartMode; permissionMode?: PermissionMode; @@ -81,6 +76,9 @@ export interface ResolvedCreateSessionRequest { export function resolveCreateSessionRequest( input: CreateSessionRequest | undefined, ): ResolvedCreateSessionRequest { + if ((input?.mode as unknown) === 'deep_research') { + throw new TypeError('Invalid session start mode.'); + } const collaborationMode = input?.collaborationMode ?? 'agent'; if (!isCollaborationMode(collaborationMode)) { throw new TypeError('Invalid collaboration mode.'); diff --git a/apps/desktop/src/main/deep-research-desktop-projection.ts b/apps/desktop/src/main/deep-research-desktop-projection.ts deleted file mode 100644 index 6e497a1f35..0000000000 --- a/apps/desktop/src/main/deep-research-desktop-projection.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { projectDeepResearchClientProgress } from '@maka/core/deep-research-client-progress'; -import { type DeepResearchClientProgress, type DeepResearchRun } from '@maka/core/deep-research-run'; -import type { DeepResearchQueryResult } from '@maka/runtime-host/protocol'; - -export function projectEmbeddedDeepResearch( - run: DeepResearchRun | undefined, -): DeepResearchClientProgress | undefined { - return run ? projectDeepResearchClientProgress(run) : undefined; -} - -export function projectHostedDeepResearch( - result: DeepResearchQueryResult, -): DeepResearchClientProgress | undefined { - if (result.kind === 'not_started') return undefined; - return { - sessionId: result.sessionId, - objective: result.objective, - scopeLevel: result.scopeLevel, - status: result.status, - stage: result.stage, - round: result.round, - createdAt: result.createdAt, - updatedAt: result.updatedAt, - artifactsCount: result.artifactsCount, - stepsCount: result.stepsCount, - checklist: result.checklist.map(({ itemId, title, status, blockedReason }) => ({ - itemId, - title, - status, - ...(blockedReason === null ? {} : { blockedReason }), - })), - reportSections: result.reportSections.map(({ key, status }) => ({ key, status })), - recentInspectedRefs: result.recentInspectedRefs.map(({ kind, locator, label }) => ({ - kind, - locator, - ...(label === null ? {} : { label }), - })), - workerRunIds: [...result.workerRunIds], - blockers: [...result.blockers], - ...(result.reportArtifactId === null ? {} : { reportArtifactId: result.reportArtifactId }), - ...(result.implementationPrompt === null - ? {} - : { implementationPrompt: result.implementationPrompt }), - }; -} diff --git a/apps/desktop/src/main/runtime-host-client.ts b/apps/desktop/src/main/runtime-host-client.ts index 97f66bb483..91287fcd4d 100644 --- a/apps/desktop/src/main/runtime-host-client.ts +++ b/apps/desktop/src/main/runtime-host-client.ts @@ -1603,12 +1603,6 @@ export class DesktopRuntimeHostClient { return this.request("agent.graph.stop", input); } - queryDeepResearch( - sessionId: string, - ): Promise> { - return this.request("deep-research.query", { sessionId }); - } - async listRuntimeResources(sessionId: string): Promise { this.#assertOpen(); try { diff --git a/apps/desktop/src/main/runtime-host-session-domains-ipc-main.ts b/apps/desktop/src/main/runtime-host-session-domains-ipc-main.ts index 8452baa944..f43fbe5252 100644 --- a/apps/desktop/src/main/runtime-host-session-domains-ipc-main.ts +++ b/apps/desktop/src/main/runtime-host-session-domains-ipc-main.ts @@ -41,7 +41,6 @@ import { GOAL_ARM_REQUEST_KEYS, type GoalArmOutcome, } from '../shared/goal-arm.js'; -import { projectHostedDeepResearch } from './deep-research-desktop-projection.js'; import { handleReconciledControl, handleReconnectableRead, @@ -70,7 +69,6 @@ type RuntimeHostSessionDomainClient = RuntimeHostShellRunsClient & | 'querySessionTodo' | 'queryAgentGraph' | 'queryAgentGraphOperator' - | 'queryDeepResearch' | 'queryGoal' | 'startPlanTurn' | 'stopAgentGraph' @@ -123,11 +121,6 @@ export function registerRuntimeHostSessionDomainsIpc( handleReconnectableRead(ipcMain, 'todo:read', (_event, sessionId: unknown) => deps.client.querySessionTodo(requiredId(sessionId, 'Session')), ); - handleReconnectableRead(ipcMain, 'deepResearch:get', async (_event, sessionId: unknown) => - projectHostedDeepResearch( - await deps.client.queryDeepResearch(requiredId(sessionId, 'Session')), - ), - ); handleReconnectableRead(ipcMain, 'goal:get', async (_event, sessionId: unknown) => { const result = await deps.client.queryGoal(requiredId(sessionId, 'Session')); @@ -374,12 +367,6 @@ export function registerRuntimeHostSessionDomainsIpc( at: now(), }); break; - case 'deep_research': - deps.sendToRenderer?.('deepResearch:changed', { - sessionId: change.sessionId, - ts: now(), - }); - break; case 'plan': deps.sendToRenderer?.('plan-mode:changed', { sessionId: change.sessionId }); break; @@ -402,7 +389,6 @@ export function registerRuntimeHostSessionDomainsIpc( }, sessionSubscriptionRecovered(sessionId) { sessionDomainChanged({ sessionId, domain: 'todo' }); - sessionDomainChanged({ sessionId, domain: 'deep_research' }); sessionDomainChanged({ sessionId, domain: 'plan' }); sessionDomainChanged({ sessionId, domain: 'usage' }); deps.sendToRenderer?.('graphs:resync', { rootSessionId: sessionId }); diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts index d8055a5160..91cb3ae407 100644 --- a/apps/desktop/src/preload/bridge-contract.d.ts +++ b/apps/desktop/src/preload/bridge-contract.d.ts @@ -111,7 +111,6 @@ import type { import type { WebSearchProvider, WebSearchResponse } from '@maka/core/web-search'; import type { BrowserState, BrowserViewRect } from '@maka/core/browser'; import type { SessionTodoItem } from '@maka/core/session-todo'; -import type { DeepResearchChangedEvent, DeepResearchClientProgress } from '@maka/core/deep-research-run'; import type { DesktopTranscriptBatch, DesktopTranscriptHandle, @@ -1135,10 +1134,6 @@ export interface MakaBridge { read(sessionId: string): Promise; subscribeChanges(handler: (event: { sessionId: string; at: number }) => void): () => void; }; - deepResearch: { - get(sessionId: string): Promise; - subscribeChanges(handler: (event: DeepResearchChangedEvent) => void): () => void; - }; graphs: { listEpochs(rootSessionId: string): Promise; listCurrentEpochs(rootSessionId: string): Promise; diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index 0c2df7601c..523085f800 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -207,7 +207,6 @@ import type { WebSearchProvider, WebSearchResponse } from '@maka/core/web-search import type { BrowserState, BrowserViewRect } from '@maka/core/browser'; import { createBrowserSelectionCoordinator } from './browser-selection.js'; import type { SessionTodoItem } from '@maka/core/session-todo'; -import type { DeepResearchChangedEvent, DeepResearchClientProgress } from '@maka/core/deep-research-run'; import { isWebSearchProvider, MASKED_TOKEN_SENTINEL, @@ -1999,19 +1998,6 @@ const makaBridge = { ); }, }, - deepResearch: { - get(sessionId: string): Promise { - return invokeProjectedSessionRuntimeHost('deepResearch:get', sessionId); - }, - subscribeChanges(handler: (event: DeepResearchChangedEvent) => void): () => void { - return subscribeEveryRuntimeHostEvent('deepResearch:changed', (scope, event: DeepResearchChangedEvent) => - handler({ - ...event, - sessionId: recordRuntimeHostSessionScope(scope, event.sessionId), - }), - ); - }, - }, graphs: { async listEpochs(rootSessionId: string): Promise { const session = await runtimeHostSessionRef(rootSessionId); diff --git a/apps/desktop/src/renderer/app-shell-command-actions.ts b/apps/desktop/src/renderer/app-shell-command-actions.ts index 82ca021e16..7f309eb075 100644 --- a/apps/desktop/src/renderer/app-shell-command-actions.ts +++ b/apps/desktop/src/renderer/app-shell-command-actions.ts @@ -25,7 +25,6 @@ import type { SettingsSection, ThemePreference } from '@maka/core/settings'; import type { UiLocale } from '@maka/core/ui-locale'; import type { NavSelection } from "@maka/ui"; import type { DesktopManualDiagnosticTarget } from '../preload/diagnostics-contract.js'; -import type { SessionStartMode } from './application/contracts/session-start-mode.js'; import { defaultRuntimeHostDiagnosticTarget, runOnDefaultRuntimeHost, @@ -80,7 +79,6 @@ export interface AppShellCommandListOptions { captureComposerImportOwner: () => ComposerImportOwner; createSession: () => void; openSideConversation: () => void; - startModeSession: (mode: SessionStartMode) => Promise; openHelp: () => void; openScheduledTaskCreate: () => void; openProjectFolder: () => Promise; @@ -137,10 +135,6 @@ export function buildAppShellCommandList( defaultSlug: options.defaultConnection, onNewChat: () => optionsRef.current.createSession(), onOpenSideChat: () => optionsRef.current.openSideConversation(), - onStartDeepResearch: async () => { - const { startModeSession } = optionsRef.current; - await startModeSession("deep_research"); - }, onStartScheduledTask: () => optionsRef.current.openScheduledTaskCreate(), onOpenSettings: () => optionsRef.current.openSettings(), onOpenSettingsSection: (section) => diff --git a/apps/desktop/src/renderer/app-shell-session-start-actions.ts b/apps/desktop/src/renderer/app-shell-session-start-actions.ts deleted file mode 100644 index 48309d896f..0000000000 --- a/apps/desktop/src/renderer/app-shell-session-start-actions.ts +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import type { UiLocale } from '@maka/core/ui-locale'; -import type { NavSelection } from '@maka/ui'; -import type { DesktopNewTaskTarget } from '../preload/bridge-contract.js'; -import type { SessionStartMode } from './application/contracts/session-start-mode.js'; -import { getShellCopy, localizedShellErrorMessage } from './locales/shell-copy.js'; -import { - isNoRealConnectionError, - noRealConnectionReasonFromError, - noRealConnectionSetupDescription, -} from './model-connection-errors.js'; -import { - isSessionWorkspaceUnavailableError, - showSessionWorkspaceUnavailableToast, -} from './session-workspace-errors.js'; - -type ComposerImportOwner = { - sessionId: string | undefined; - navSection: NavSelection['section']; - newTaskDraftKey?: string; -}; - -type RefBox = { current: T }; - -type ComposerFocusHandle = { - focus(): void; -}; - -type ToastApi = { - error( - title: string, - description?: string, - diagnosticDetails?: string, - diagnosticTarget?: { profileId: string }, - ): void; -}; - -export interface AppShellSessionStartActions { - /** - * Open an empty session in a non-default mode (#1433). Text and - * Skills belong to the Composer, which creates its own session on - * first send; this is for entry points that pick the mode before any - * text exists, such as the command palette's Deep Research. - */ - startModeSession(mode: SessionStartMode): Promise; -} - -export function createAppShellSessionStartActions(deps: { - uiLocale: UiLocale; - activeIdRef: RefBox; - captureComposerImportOwner: () => ComposerImportOwner; - composerRef: RefBox; - isShellSurfaceOwnerActive: (owner: ComposerImportOwner) => boolean; - openSessionInChat: (sessionId: string, turnId?: string) => void; - newTaskTarget: DesktopNewTaskTarget | undefined; - sessionStartPendingRef: RefBox; - refreshOnboarding: () => void; - refreshSessions: () => Promise; - /** - * The shell's one handler for "no usable model connection" (app-shell.tsx), - * shared with the send path and the session-event stream. It carries the - * 打开模型设置 action, which is the only thing that resolves this state. - */ - showModelSetupToast: ( - description: string, - reason?: string, - diagnosticTarget?: { profileId: string }, - ) => void; - toastApi: ToastApi; -}): AppShellSessionStartActions { - const { - uiLocale, - activeIdRef, - captureComposerImportOwner, - composerRef, - isShellSurfaceOwnerActive, - openSessionInChat, - newTaskTarget, - sessionStartPendingRef, - refreshOnboarding, - refreshSessions, - showModelSetupToast, - toastApi, - } = deps; - const copy = getShellCopy(uiLocale).chatActions; - - async function startModeSession(mode: SessionStartMode): Promise { - if (sessionStartPendingRef.current) return false; - if (!newTaskTarget) return false; - const owner = captureComposerImportOwner(); - sessionStartPendingRef.current = true; - try { - // #1433: the one session-creation channel. Main derives the - // permission boundary, name and labels from `mode`. - const session = await window.maka.newTasks.create(newTaskTarget, { - mode, - }); - if (isShellSurfaceOwnerActive(owner)) { - openSessionInChat(session.id); - } - await refreshSessions(); - if (activeIdRef.current === session.id) { - composerRef.current?.focus(); - } - // Best-effort: mark onboarding completed. Failure must not - // turn a successful chat into a failure — backfill covers it. - void window.maka.onboarding.setMilestone('initial_onboarding', 'completed').catch(() => {}); - return true; - } catch (error) { - // `sessions:create` rejects rather than returning a reason code, so the - // two cases the old `quickChat:start` union spelled out are recovered - // here from the errors main actually throws — not from "everything that - // is not the other one": - // - // workspace_unavailable → `SESSION_WORKSPACE_UNAVAILABLE:` (project-context-root.ts) - // setup_required → `NO_REAL_CONNECTION::` (Runtime Host execution composition) - // - // Anything else is a genuine failure (storage, disk, a bug) and must - // not be silently relabelled as "your setup is incomplete". - if (isSessionWorkspaceUnavailableError(error)) { - if (isShellSurfaceOwnerActive(owner)) { - showSessionWorkspaceUnavailableToast(toastApi, uiLocale, { - profileId: newTaskTarget.profileId, - }); - } - return false; - } - if (isNoRealConnectionError(error)) { - // Refresh the snapshot so the first-run hero is accurate, then say - // what is missing. The hero alone is not an answer: it only takes the - // chat surface over while `sessions.length === 0 && !onboardingSettled` - // (app-shell.tsx), and onboarding-service backfills that milestone for - // anyone who already has a session — so for every existing user the - // refresh alone renders nothing and the command appears to do nothing. - // This is the same error class the send path handles, so it gets the - // same toast rather than a second answer to one question. - // - // The refresh is global and silent, so it runs either way. The toast - // is not: `showModelSetupToast` ends in `openSettingsSection('models')` - // (app-shell.tsx), so it NAVIGATES. Gate it exactly as the sibling - // branches gate theirs — an await that resolves after the user moved - // on must not pull them back out of wherever they went. - refreshOnboarding(); - if (isShellSurfaceOwnerActive(owner)) { - const reason = noRealConnectionReasonFromError(error); - showModelSetupToast( - noRealConnectionSetupDescription(reason, uiLocale), - reason, - { profileId: newTaskTarget.profileId }, - ); - } - return false; - } - if (isShellSurfaceOwnerActive(owner)) { - toastApi.error( - copy.sessionStartFailedTitle, - localizedShellErrorMessage(error, copy.sessionStartFailedFallback, uiLocale), - undefined, - { profileId: newTaskTarget.profileId }, - ); - } - return false; - } finally { - sessionStartPendingRef.current = false; - } - } - - return { startModeSession }; -} diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx index 2e4cb996e9..8973a377c0 100644 --- a/apps/desktop/src/renderer/app-shell.tsx +++ b/apps/desktop/src/renderer/app-shell.tsx @@ -164,7 +164,6 @@ import { createAppShellRevisionActions, type TurnRevisionDraft, } from './app-shell-revision-actions'; -import { createAppShellSessionStartActions } from './app-shell-session-start-actions'; import { createAppShellStopAction } from './app-shell-stop-action'; import { useStableActions } from './use-stable-actions'; import { @@ -556,7 +555,6 @@ function AppShellContent({ userLabel, setUserLabel, - refreshShellSettings, } = useShellAppearance({ toastApi, @@ -1031,7 +1029,6 @@ function AppShellContent({ // aborted) takes over with the existing chat surface. // Re-entrancy lock only — a ref, not state, because nothing renders // from it (#1433 removed its last reader with the first-run hero). - const sessionStartPendingRef = useRef(false); // Seed a snapshot captured before React mounted so the sidebar can paint // immediately. The subscription bootstrap reconciles once through the live // Session catalog on the next frame; later onboarding pulls still own @@ -1191,20 +1188,6 @@ function AppShellContent({ projectName: currentProject?.name, projectPath: projectInfo?.projectPath, }); - const { startModeSession } = useStableActions(createAppShellSessionStartActions, { - uiLocale, - activeIdRef, - captureComposerImportOwner, - composerRef, - isShellSurfaceOwnerActive, - openSessionInChat, - newTaskTarget: taskEntry.selectors.target, - sessionStartPendingRef, - refreshOnboarding: onboarding.refresh, - refreshSessions, - showModelSetupToast, - toastApi, - }); const openNewTaskSurface = useCallback(() => { imageNoticeLifecycle.reset(NEW_TASK_PENDING_KEY); const ownerToken = startNewSession(); @@ -2123,7 +2106,6 @@ function AppShellContent({ hiddenSessionIds: selectors.hiddenSessionIds, captureComposerImportOwner, createSession, - startModeSession, openHelp, openScheduledTaskCreate: () => { closePalette(); @@ -2635,16 +2617,6 @@ function AppShellContent({ } : undefined } - onContinueDeepResearchHandoff={(run) => { - const prompt = run.implementationPrompt; - if (!prompt) return; - void createSession().then(() => { - window.requestAnimationFrame(() => { - composerRef.current?.setText(prompt); - composerRef.current?.focus(); - }); - }); - }} sessionHealthNotice={sessionHealthNotice} sessionHealthModelPickerAvailable={ activeBoundarySurface.localInteractionAvailable diff --git a/apps/desktop/src/renderer/application/contracts/session-start-mode.ts b/apps/desktop/src/renderer/application/contracts/session-start-mode.ts deleted file mode 100644 index dfcc44e660..0000000000 --- a/apps/desktop/src/renderer/application/contracts/session-start-mode.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export type { SessionStartMode } from '@maka/core/session-start-mode'; diff --git a/apps/desktop/src/renderer/chat-message-surface.tsx b/apps/desktop/src/renderer/chat-message-surface.tsx index b44832834a..738cd7f435 100644 --- a/apps/desktop/src/renderer/chat-message-surface.tsx +++ b/apps/desktop/src/renderer/chat-message-surface.tsx @@ -18,7 +18,6 @@ */ import { useMemo, useState, type ComponentProps, type ReactNode } from 'react'; -import { isDeepResearchSession } from '@maka/core/deep-research'; import { type LlmConnection, type ProviderType } from '@maka/core/llm-connections'; import { type OnboardingState } from '@maka/core/onboarding'; import { type SettingsSection } from '@maka/core/settings'; @@ -37,7 +36,6 @@ import type { TaskReadinessNotice } from './task-readiness-notice'; import { getShellCopy } from './locales/shell-copy'; import { selectLiveTurns } from './features/conversation/index.js'; import { useExternalStoreSelector } from './application/contracts/session-catalog/use-external-store-selector.js'; -import { useDeepResearchRun } from './use-deep-research-run'; import { ChatRecoveryNotice, SessionHealthRecoveryNotice } from './chat-recovery-notice'; const selectShellRunRecord = (state: AppShellSessionUiState, sessionId: string | undefined) => @@ -56,7 +54,6 @@ const selectShellRunRecord = (state: AppShellSessionUiState, sessionId: string | interface ChatMessageSurfaceProps extends Omit< ComponentProps, - | 'deepResearchRun' | 'emptyOverride' | 'initialLiveContentSnapshot' | 'liveTurns' @@ -143,11 +140,6 @@ export function ChatMessageSurface({ return; } }; - const activeSession = chatViewRest.activeSession; - const deepResearchRun = useDeepResearchRun( - activeSession?.id, - isDeepResearchSession(activeSession?.labels), - ); const liveTurns = useExternalStoreSelector(sessionUiController, selectLiveTurns, activeSessionId); const liveTurn = liveTurns?.find((turn) => turn.turnId === chatViewRest.activeTurn?.turnId) ?? liveTurns?.at(-1); const seededLiveTurns = liveContentSeedRevision > 0 ? liveTurns : undefined; @@ -233,7 +225,6 @@ export function ChatMessageSurface({ // the activation reaching the DOM is always this session's. initialLiveContentSnapshot={activation.initialLiveContent} shellRunUpdates={shellRunUpdates} - deepResearchRun={deepResearchRun} emptyOverride={emptyOverride} goalIndicator={goalProjection.goalIndicator} /> diff --git a/apps/desktop/src/renderer/command-palette-commands.ts b/apps/desktop/src/renderer/command-palette-commands.ts index 509bcc03ec..d6c42b06e3 100644 --- a/apps/desktop/src/renderer/command-palette-commands.ts +++ b/apps/desktop/src/renderer/command-palette-commands.ts @@ -39,7 +39,6 @@ import { Plus, Settings as SettingsIcon, ShieldCheck, - Sparkles, Sun, SunMoon, Wifi, @@ -69,7 +68,6 @@ export function buildCommandList(args: { defaultSlug: string | null; onNewChat(): Promise | void; onOpenSideChat?(): Promise | void; - onStartDeepResearch?(): Promise | void; onOpenSettings(): void; onOpenSettingsSection(section: SettingsSection): void; onOpenShortcuts(): void; @@ -167,18 +165,6 @@ export function buildCommandList(args: { }, ] : []), - ...(args.onStartDeepResearch - ? [ - { - id: 'action:new-deep-research', - kind: 'action' as const, - ...staticCopy('action:new-deep-research'), - Icon: Sparkles, - keywords: [...copy.staticKeywords['action:new-deep-research']], - run: () => args.onStartDeepResearch!(), - }, - ] - : []), ...(args.onStartScheduledTask ? [ { diff --git a/apps/desktop/src/renderer/locales/shell-copy.ts b/apps/desktop/src/renderer/locales/shell-copy.ts index 6f7cb3b085..d5583bcdcf 100644 --- a/apps/desktop/src/renderer/locales/shell-copy.ts +++ b/apps/desktop/src/renderer/locales/shell-copy.ts @@ -37,7 +37,6 @@ import type { DesktopSessionUpdateFailureCode } from '../../shared/desktop-sessi export const STATIC_COMMAND_IDS = [ 'action:new-chat', 'action:side-chat', - 'action:new-deep-research', 'action:new-scheduled-task', 'action:open-settings', 'action:keyboard-help', @@ -86,7 +85,6 @@ const STATIC_COMMAND_KEYWORDS: Record = { '任务', '追问', ], - 'action:new-deep-research': ['deep', 'research', 'explore', 'readonly', '研究', '深度', '探索', '只读'], 'action:new-scheduled-task': ['plan', 'task', 'schedule', 'new', 'create', '计划', '提醒', '新建', '创建'], 'action:open-settings': ['settings', 'preferences', '设置', 'options'], 'action:keyboard-help': ['shortcuts', 'keyboard', 'help', '快捷键', '帮助'], @@ -548,11 +546,6 @@ const ZH_STATIC_COMMANDS: Record = { platformHint: { apple: '⌥⌘S', other: 'Ctrl+Alt+S' }, group: '操作', }, - 'action:new-deep-research': { - label: '新建深度研究', - hint: '只读探索', - group: '操作', - }, 'action:new-scheduled-task': { label: '新建定时任务', hint: '打开定时任务表单', @@ -638,11 +631,6 @@ const EN_STATIC_COMMANDS: Record = { platformHint: { apple: '⌥⌘S', other: 'Ctrl+Alt+S' }, group: 'Actions', }, - 'action:new-deep-research': { - label: 'New deep research', - hint: 'Read-only exploration', - group: 'Actions', - }, 'action:new-scheduled-task': { label: 'New scheduled task', hint: 'Open the task form', diff --git a/apps/desktop/src/renderer/maka-tokens.css b/apps/desktop/src/renderer/maka-tokens.css index 1702a5aef2..2ea47effb2 100644 --- a/apps/desktop/src/renderer/maka-tokens.css +++ b/apps/desktop/src/renderer/maka-tokens.css @@ -1166,8 +1166,7 @@ no --info hue: "info" is a semantic slot (Banner status), so it paints with the accent at 0.24 like its three siblings. It has to be the same CONSTRUCTION and not just the same weight — an opaque tint does not - compose, so a tinted chip on a tinted panel (deep-research's count pill, - plan-mode's in-progress marker) resolves to the parent's exact pixel and + compose, so a tinted chip on a tinted panel (plan-mode's in-progress marker) resolves to the parent's exact pixel and disappears. Alpha is also what upstream ships this token as (#0082FB33, 0.20); theme-neutral is what flattens it to an opaque #f1f1f1. diff --git a/apps/desktop/src/renderer/styles.css b/apps/desktop/src/renderer/styles.css index 91caf4f477..981df6e549 100644 --- a/apps/desktop/src/renderer/styles.css +++ b/apps/desktop/src/renderer/styles.css @@ -41,7 +41,6 @@ @import "./styles/sidebar.css" layer(components); @import "./styles/search-modal.css" layer(components); @import "./styles/prompt-suggestions.css" layer(components); -@import "./styles/deep-research.css" layer(components); @import "./styles/onboarding.css" layer(components); @import "./styles/module-pages.css" layer(components); @import "./styles/settings/select.css" layer(components); diff --git a/apps/desktop/src/renderer/styles/deep-research.css b/apps/desktop/src/renderer/styles/deep-research.css deleted file mode 100644 index d66de3c1cd..0000000000 --- a/apps/desktop/src/renderer/styles/deep-research.css +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* Deep research workflow + report/scope/evidence/progress. Relocated from sidebar.css — issue #546 PR3. */ -.maka-deep-research-workflow { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: var(--space-2); - margin: 0; - padding: 0; - list-style: none; -} - -.maka-deep-research-workflow li { - min-width: 0; - border: var(--border-width-hairline) solid var(--border); - border-radius: var(--radius-surface); - background: oklch(from var(--focus-ring) l c h / 0.035); - padding: var(--space-2-5); -} - -.maka-deep-research-workflow-title { - font: var(--maka-text-heading-4); - display: block; - margin: 0 0 var(--space-1); - color: var(--foreground); -} - -.maka-deep-research-workflow-body { - font: var(--maka-text-body); - display: block; - color: var(--muted-foreground); -} - -.maka-deep-research-report, -.maka-deep-research-scope, -.maka-deep-research-evidence, -.maka-deep-research-progress { - display: grid; - gap: var(--space-2); - padding: var(--space-3) 0 0; - border-top: var(--border-width-hairline) solid var(--border); -} - -.maka-deep-research-report h2, -.maka-deep-research-scope h2, -.maka-deep-research-evidence h2, -.maka-deep-research-progress h2 { - font: var(--maka-text-heading-4); - margin: 0; - color: var(--foreground); -} - -.maka-deep-research-report ul, -.maka-deep-research-scope ul, -.maka-deep-research-evidence ul, -.maka-deep-research-progress ul { - display: grid; - gap: var(--space-1-5); - margin: 0; - padding: 0; - list-style: none; -} - -.maka-deep-research-report li, -.maka-deep-research-scope li, -.maka-deep-research-evidence li, -.maka-deep-research-progress li { - display: grid; - grid-template-columns: minmax(72px, max-content) minmax(0, 1fr); - gap: var(--space-2-5); - align-items: baseline; - min-width: 0; -} - -.maka-deep-research-report-title, -.maka-deep-research-scope-label, -.maka-deep-research-evidence-title, -.maka-deep-research-progress-title { - font: var(--maka-text-heading-4); - color: var(--foreground); - white-space: nowrap; -} - -.maka-deep-research-report-body, -.maka-deep-research-scope-body, -.maka-deep-research-evidence-body, -.maka-deep-research-progress-body { - font: var(--maka-text-body); - min-width: 0; - color: var(--muted-foreground); -} - -.maka-deep-research-run-panel { - flex: 0 0 auto; - margin: 0 var(--space-4) var(--space-2-5); - padding: var(--space-3) var(--space-4); - border: var(--border-width-hairline) solid var(--border); - border-radius: var(--radius-surface); - background: var(--color-accent-muted); - color: var(--foreground); -} - -.maka-deep-research-run-panel[data-status="completed"] { - background: var(--color-success-muted); -} - -.maka-deep-research-run-summary, -.maka-deep-research-run-summary > div { - display: flex; - align-items: center; - gap: var(--space-2-5); -} - -.maka-deep-research-run-summary { - justify-content: space-between; - margin-bottom: var(--space-3); -} - -.maka-deep-research-run-summary strong { - font: var(--maka-text-heading-4); -} - -.maka-deep-research-run-summary span, -.maka-deep-research-run-panel p { - font: var(--maka-text-supporting); - color: var(--muted-foreground); -} - -.maka-deep-research-run-actions { - display: flex; - align-items: center; - gap: var(--space-2); -} - -/* #1879: a count pill beside an Astryx Button, and the same shape as - `.maka-session-workbar-count` and `.maka-tool-count`. It declared no type of - its own — it inherits — which is precisely how it stayed invisible to a chip - scan that required a rule to name its own role. Measured 24px natural (20px - line box plus 2 x 2px padding); `--h-control-sm` is 24, so nothing moves. */ -.maka-deep-research-run-count { - height: var(--h-control-sm); - display: inline-flex; - align-items: center; - padding: var(--space-0-5) var(--space-2); - border-radius: var(--radius-pill); - background: var(--color-accent-muted); - font-variant-numeric: tabular-nums; -} - -.maka-deep-research-handoff-button { - font: var(--maka-text-heading-5); - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--space-1-5); - min-height: var(--h-control-lg); - padding: 0 var(--space-3); - border: var(--border-width-hairline) solid var(--focus-ring); - border-radius: var(--radius-control); - background: var(--background); - color: var(--foreground); - transition: - background-color var(--duration-quick) var(--ease-out-strong), - border-color var(--duration-quick) var(--ease-out-strong); -} - -/* Two steps out of one rung: the neutral hover is the product's own - --state-hover-bg, and the press lands on the informational tint. The wash - family this replaced spent a second status alpha on the press alone. */ -.maka-deep-research-handoff-button:hover { - background: var(--state-hover-bg); -} - -.maka-deep-research-handoff-button:active { - background: var(--color-accent-muted); -} - -.maka-deep-research-handoff-button:focus-visible { - outline: var(--focus-ring-width) solid var(--focus-ring); - outline-offset: var(--focus-ring-offset); -} - -.maka-deep-research-run-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(210px, 100%), 1fr)); - gap: var(--space-3) var(--space-4); -} - -.maka-deep-research-run-grid h3 { - font: var(--maka-text-heading-5); - margin: 0 0 var(--space-1-5); - color: var(--muted-foreground); -} - -.maka-deep-research-run-grid ul { - display: grid; - gap: var(--space-1); - margin: 0; - padding: 0; - list-style: none; -} - -.maka-deep-research-run-grid li { - font: var(--maka-text-supporting); - display: flex; - gap: var(--space-1-5); - min-width: 0; - color: var(--muted-foreground); -} - -.maka-deep-research-run-grid li[data-status="completed"] > span { - color: var(--success-text); -} - -.maka-deep-research-run-grid li[data-status="blocked"], -.maka-deep-research-run-blockers { - color: var(--destructive-text); -} - -.maka-deep-research-run-grid code { - overflow: hidden; - min-width: 0; - color: inherit; - text-overflow: ellipsis; - white-space: nowrap; -} - -@media (max-width: 620px) { - .maka-deep-research-run-summary { - align-items: flex-start; - } - - .maka-deep-research-run-summary, - .maka-deep-research-run-actions { - flex-wrap: wrap; - } -} diff --git a/apps/desktop/src/renderer/styles/hero.css b/apps/desktop/src/renderer/styles/hero.css index d9eda66d42..e9627fd973 100644 --- a/apps/desktop/src/renderer/styles/hero.css +++ b/apps/desktop/src/renderer/styles/hero.css @@ -113,6 +113,6 @@ launch screen, so its greeting steps down from the first-run hero rung to 20px. The OnboardingHero variants keep the large scale — they are seen once, and there the headline is the whole message. */ -.maka-hero-empty-chat:not(.maka-hero-deep-research) h1 { +.maka-hero-empty-chat h1 { font: var(--maka-text-heading-1); } diff --git a/apps/desktop/src/renderer/use-deep-research-run.ts b/apps/desktop/src/renderer/use-deep-research-run.ts deleted file mode 100644 index 972a738cdf..0000000000 --- a/apps/desktop/src/renderer/use-deep-research-run.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { useEffect, useState } from 'react'; -import type { DeepResearchClientProgress } from '@maka/core/deep-research-run'; - -export function useDeepResearchRun( - sessionId: string | undefined, - enabled: boolean, -): DeepResearchClientProgress | undefined { - const [run, setRun] = useState(); - - useEffect(() => { - let active = true; - let request = 0; - if (!sessionId || !enabled) { - setRun(undefined); - return () => { - active = false; - }; - } - - const refresh = async () => { - const currentRequest = ++request; - const next = await window.maka.deepResearch.get(sessionId); - if (active && currentRequest === request) setRun(next); - }; - void refresh().catch(() => { - if (active) setRun(undefined); - }); - const unsubscribe = window.maka.deepResearch.subscribeChanges((event) => { - if (event.sessionId === sessionId) { - void refresh().catch(() => undefined); - } - }); - return () => { - active = false; - unsubscribe(); - }; - }, [enabled, sessionId]); - - return enabled && run?.sessionId === sessionId ? run : undefined; -} diff --git a/apps/desktop/stories/app-shell.stories.tsx b/apps/desktop/stories/app-shell.stories.tsx index cdc89a4b73..91122169bb 100644 --- a/apps/desktop/stories/app-shell.stories.tsx +++ b/apps/desktop/stories/app-shell.stories.tsx @@ -1653,7 +1653,7 @@ function GoalContextStory(props: { goal: NonNullable - -# Deep Research durable workspace - -This contract is an independent, minimal reproduction of the central systems -idea in [FS-Researcher: Test-Time Scaling for Long-Horizon Research Tasks with -File-System-Based Agents](https://arxiv.org/abs/2602.01566) (Zhu et al., ACL -2026). It supports the Deep Research direction tracked in -[issue #566](https://github.com/maka-agent/maka-agent/issues/566). - -The reproduction uses the paper's high-level two-stage method: - -1. Build a durable knowledge base by archiving raw sources before derived - evidence notes. -2. Write an outline, source-backed sections, and a final report from that - durable workspace. - -No source code, prompts, or documentation are copied from the paper's reference -repository. The Maka implementation is built independently on Maka's existing -event-ledger and Artifact Store contracts. - -## Status and audience - -This document is the stable architecture and safety contract for the initial -Deep Research workspace shipped by PR #1227. It is intended for: - -- reviewers checking issue #566 against the implementation; -- runtime and storage maintainers evolving the ledger or tools; -- Desktop maintainers consuming the projected run state; and -- contributors adding local exploration, web research, or report-generation - behavior without weakening the read-only boundary. - -Code and contract tests remain authoritative if this document and the -implementation disagree. - -## Reviewer summary - -The change deliberately separates workflow metadata from large research -content: - -| Concern | Authority | Main implementation | -| --- | --- | --- | -| Run lifecycle and invariants | Event projection | `packages/core/src/deep-research-run.ts` | -| Append-only persistence and replay | Deep Research store | `packages/storage/src/deep-research-store.ts` | -| Source, note, and report bodies | Existing Artifact Store | `packages/core/src/artifacts.ts` | -| Model-facing mutations and reads | Eight runtime tools | `packages/runtime/src/deep-research-tools.ts` | -| Session gating and IPC | Desktop main/preload | `apps/desktop/src/main/main.ts`, `apps/desktop/src/preload/preload.ts` | -| Visible progress and handoff | Desktop renderer/UI | `apps/desktop/src/renderer/use-deep-research-run.ts`, `packages/ui/src/chat-view.tsx` | - -The highest-risk review areas are: - -1. projection invariants and failure behavior for corrupt or conflicting events; -2. artifact ownership, source traceability, and integrity-checked reads; -3. root-session gating for Maka-owned write tools; and -4. the explicit transition from a read-only research session to a new normal - implementation task. - -## Paper-to-Maka mapping - -The paper describes a persistent workspace shared by a Context Builder and a -Report Writer. Maka adopts the systems boundary, not the reference -implementation: - -| FS-Researcher idea | Maka reproduction | Deliberate difference | -| --- | --- | --- | -| File system as external memory | JSONL event ledger plus Artifact Store | Maka reuses app-owned storage rather than exposing arbitrary project paths | -| Context Builder archives sources and notes | `source` and `evidence_note` artifacts with explicit provenance | Search and worker scheduling remain bounded, separately authorized substeps | -| Report Writer consumes the knowledge base | Outline, five report sections, final report, and handoff artifacts | Completion is enforced by projection invariants rather than prompt convention alone | -| Work survives context boundaries | Status projection and chunked artifact reads | Resume fails closed on corrupt state or integrity mismatch | -| Multiple agents coordinate through durable files | Research steps can record worker run ids and evidence | The initial slice records workers but does not introduce a new autonomous scheduler | - -## Scope - -This slice adds a bounded, inspectable research workspace. It provides: - -- an append-only Deep Research event ledger; -- restart-safe projection of objective, scope, stage, round, artifacts, - checklist, bounded research steps, report sections, checkpoints, and - completion; -- app-owned Markdown artifacts for raw sources, evidence notes, outlines, - report sections, the final report, and implementation handoff; -- chunked, integrity-checked artifact reads so a resumed model can recover - evidence bodies by id without rereading the original source; -- visible Desktop progress for checklist state, inspected files/symbols/URLs, - worker runs, blockers, artifact counts, and report draft state; -- direct source-artifact references for every derived artifact; -- root-session tools that are available only when the session has the - `mode:deep_research` label; -- completion invariants that require settled checklist items, all required - report sections, an archived source, a final report, and a structured - handoff. - -Search-provider selection, browser automation, ranking, automatic citation -formatting, and long-running scheduling remain separate concerns. The workspace -records local and web substeps but does not silently broaden their permissions. - -## Authority and data flow - -The research event ledger is the authority for workflow state and -relationships. The Artifact Store is the authority for large bodies. -Deep Research no longer accepts Task Ledger ids from production tools; -the persisted checkpoint `taskIds` field remains decode-compatible and is -written as empty during the migration window. - -```text -Deep Research root session - | - +-- deep_research_* tools - | - +-- sessions//deep-research/events.jsonl - | objective, checklist, steps, sections, refs, completion - | - +-- artifacts/ - raw sources, notes, outline, sections, report, handoff -``` - -Every mutation includes available run, turn, and tool-call references. The -event is projected and validated before it is appended. Corrupt JSONL fails -closed instead of returning a partial workspace. - -## Lifecycle and state model - -```text -not started - | - | research_started - v -knowledge_base (active or blocked) - | - | archived sources + evidence + bounded steps + checkpoints - v -report_writing (active or blocked) - | - | five completed sections + final report + handoff - v -completed -``` - -Stages and checkpoint rounds are monotonic. A completed run is terminal: -subsequent mutations are rejected, except an exact replay of the original -tool call. - -### Event types - -| Event | Purpose | Important validation | -| --- | --- | --- | -| `research_started` | Establish objective and scope | Must be first and unique | -| `research_artifact_recorded` | Attach source or derived artifact metadata | Derived artifacts must cite archived source artifacts | -| `research_checklist_updated` | Persist required review progress | Evidence and blocker fields must match the status | -| `research_step_recorded` | Record bounded local or web work | Requires roots or keywords, stop condition, and evidence contract | -| `research_checkpoint_recorded` | Mark resumable progress | Round and stage cannot regress | -| `research_completed` | Seal report and implementation handoff | Requires sources, settled checklist, five sections, report, and handoff | - -### Artifact roles - -| Role | Meaning | Source-reference rule | -| --- | --- | --- | -| `source` | Archived raw source or inspectable primary material | No parent source required | -| `evidence_note` | Derived finding or comparison note | Must cite one or more `source` artifacts | -| `outline` | Planned report structure | Must cite one or more `source` artifacts | -| `report_section` | One required report section | Must cite sources and carry section key/status | -| `report` | Final user-facing research report | Must cite one or more `source` artifacts | -| `handoff` | Structured implementation input | Must cite one or more `source` artifacts | - -## Tool protocol - -The root Deep Research agent follows this sequence: - -1. `deep_research_start` -2. `deep_research_save_artifact` with `role=source` for each important raw - source and its inspectable locator -3. derived evidence artifacts with direct `source_artifact_ids` -4. `deep_research_record_step` after each bounded local or web substep, - recording roots/queries, ignored paths, stopping condition, inspected refs, - worker ids, evidence, and blockers -5. `deep_research_update_checklist` as each required area progresses -6. `deep_research_checkpoint` after meaningful rounds and before compaction -7. `deep_research_status` after interruption or restart, followed by - `deep_research_read_artifact` for only the evidence bodies needed to resume -8. five source-backed report sections, each explicitly drafted or completed -9. final `role=report` and `role=handoff` artifacts -10. `deep_research_complete` with implementation tasks, recommended issues - and/or pull requests, and verification commands - -Mutation retries are idempotent by tool-call id. Exact replays return the -existing projection; reusing the same tool-call id with different start, -artifact, checklist, step, checkpoint, or completion input fails closed. -The id is unique across all Deep Research mutation tools, and artifact replay -comparison includes the exact name, summary, body hash, provenance, locator, -role, and report-section metadata. Replay lookup happens before terminal-state -rejection, so an exact retry remains safe after completion. -Save-artifact ids also derive from session, turn, and tool-call ids. If ledger -validation fails after an artifact body was created, the artifact is rolled -back. - -## Resume and failure semantics - -| Situation | Behavior | -| --- | --- | -| Process or model-context restart | Reproject the JSONL ledger, call `deep_research_status`, then read only required artifacts | -| Exact mutation retry | Return the existing projection without appending a duplicate event | -| Same tool-call id with different input | Reject the request as a semantic conflict | -| Invalid event or invariant regression | Validate before append and leave the ledger unchanged | -| Artifact created but event rejected | Roll back the newly created artifact | -| Corrupt JSONL | Fail closed; do not expose a partial run | -| Missing, deleted, or cross-session artifact | Reject the read | -| Artifact content hash mismatch | Reject the read as an integrity failure | -| Completion artifact missing, deleted, corrupt, cross-session, or wrong role/type | Reject completion before sealing the ledger | -| Interrupted or blocked research | Preserve checkpoint, blocker, inspected refs, and collected evidence for review/resume | - -## Read-only implementation handoff - -Completion does not change the research session's permission mode. The Desktop -surface states that the original session remains read-only and offers an -explicit **continue in a new task** action. That action: - -1. creates a normal, unlabeled task; -2. builds a bounded prompt from the structured handoff and provenance ids; -3. fills the composer without sending it; and -4. asks the implementation task to inspect current code and present a plan - before modifying project files. - -The user therefore chooses the mode transition and still reviews the seeded -prompt before any model call or project write. - -## Safety boundary - -Deep Research still uses the `explore` permission profile. The new tools are a -narrow exception for writes into Maka-owned state only. They do not expose a -general filesystem path and cannot edit the user's project. Ordinary sessions -and child agents do not receive these tools. - -Status and artifact text are secret-redacted and strip forged workspace and -artifact envelope tags before they are returned to the model. Artifact reads -verify session ownership, live state, source type, and the SHA-256 hash recorded -in the ledger. Completion repeats these checks for every archived source, the -current artifact for all five report sections, the final report, and the -handoff, including persisted Markdown type and Deep Research role. Generic -Artifact Pane deletion is disabled for ledger-owned artifacts so UI actions -cannot silently invalidate a completed workspace. - -## Compatibility and operating limits - -The initial run schema is version `1`. The ledger is append-only and has no -in-place migration path in this slice. A future schema change must either remain -backward-projectable or introduce an explicit migration with fixture coverage. - -Limits are defensive bounds, not product targets: - -| Limit | Value | -| --- | ---: | -| Artifacts per run | 2,000 | -| Research steps per run | 500 | -| Checkpoints per run | 500 | -| Checklist items per run | 50 | -| Inspected refs per step | 200 | -| Artifact body accepted by a tool | 512,000 characters | -| Artifact body returned in one read | 64,000 characters | -| Artifacts included in status output | Most recent 100 | - -These bounds prevent an untrusted or looping model from turning projection or -status rendering into an unbounded operation. Large research bodies are read in -chunks and remain outside the event ledger. - -## Non-goals and follow-up seams - -The initial reproduction does not claim to provide: - -- a new search provider, browser driver, citation-ranking algorithm, or - automatic bibliography formatter; -- an autonomous scheduler for worker runs; -- clickable artifact/ref drill-down throughout the progress panel; -- ledger compaction, indexing, or schema migration; -- a report-quality benchmark equivalent to the paper's evaluation; or -- permission to modify the user's project from a Deep Research session. - -Those capabilities should build on this contract in separate changes rather -than widening the initial persistence and permission boundary. - -## Reviewer checklist - -- [ ] Only root sessions labeled `mode:deep_research` receive the eight tools. -- [ ] Tool writes are limited to Maka-owned ledger and artifact state. -- [ ] Derived artifacts cannot be recorded without archived-source provenance. -- [ ] Exact retries are idempotent and conflicting retries fail closed. -- [ ] Stage/round regression, corrupt ledgers, and integrity mismatches are - rejected. -- [ ] Completion cannot bypass checklist, section, report, source, or handoff - requirements. -- [ ] Desktop progress comes from the durable projection rather than model-only - text. -- [ ] Completing research does not mutate the original session's `explore` - permission. -- [ ] The implementation handoff creates a normal task, fills but does not send - its bounded prompt, and leaves the original run inspectable. - -## Verification - -The focused tests cover: - -- happy-path two-stage projection and completion; -- rejection of evidence without archived source references; -- monotonic rounds and stages; -- checklist evidence and completion gates; -- bounded local/web steps with inspected refs, worker ids, and stopping - conditions; -- required report-section and structured-handoff gates; -- restart recovery from append-only JSONL; -- idempotent checkpoint and completion retries; -- rejection of conflicting input under a replayed tool-call id; -- integrity-checked chunked artifact recovery; -- corrupt-ledger fail-closed behavior; -- runtime schema checks, retry idempotency, and safe status rendering; -- Desktop root-session label gating, live progress IPC/UI wiring, server-rendered - progress component coverage, and explicit read-only-to-implementation handoff. - -Run: - -```sh -npm --workspace @maka/core run test:dist -npm --workspace @maka/storage run test:dist -npm --workspace @maka/runtime run test:dist -npm --workspace @maka/desktop run test:dist -``` diff --git a/packages/core/package.json b/packages/core/package.json index 6535ad6ae5..da92a0bb04 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -83,11 +83,8 @@ "./agent-graph-supervisor-wake": "./dist/agent-graph-supervisor-wake.js", "./session-todo": "./dist/session-todo.js", "./external-session": "./dist/external-session.js", - "./deep-research-run": "./dist/deep-research-run.js", - "./deep-research-client-progress": "./dist/deep-research-client-progress.js", "./daily-review": "./dist/daily-review.js", "./work-board": "./dist/work-board.js", - "./deep-research": "./dist/deep-research.js", "./session-start-mode": "./dist/session-start-mode.js", "./long-term-memory": "./dist/long-term-memory.js", "./local-memory": "./dist/local-memory.js", diff --git a/packages/core/src/__tests__/deep-research-run.test.ts b/packages/core/src/__tests__/deep-research-run.test.ts deleted file mode 100644 index 81060b7e4e..0000000000 --- a/packages/core/src/__tests__/deep-research-run.test.ts +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import { - projectDeepResearchEvents, - type DeepResearchArtifactRecordedEvent, - type DeepResearchChecklistUpdatedEvent, - type DeepResearchCheckpointRecordedEvent, - type DeepResearchCompletedEvent, - type DeepResearchEvent, - type DeepResearchReportSectionKey, - type DeepResearchStartedEvent, -} from '../deep-research-run.js'; - -const SESSION_ID = 'session-1'; -const HASH = `sha256:${'a'.repeat(64)}`; - -function started(): DeepResearchStartedEvent { - return { - eventId: 'event-start', - type: 'research_started', - sessionId: SESSION_ID, - ts: 1, - objective: 'Explain the durable research workspace.', - scopeLevel: 'standard', - }; -} - -function artifact( - artifactId: string, - role: DeepResearchArtifactRecordedEvent['artifact']['role'], - sourceArtifactIds: string[] = [], - reportSectionKey?: DeepResearchReportSectionKey, -): DeepResearchArtifactRecordedEvent { - return { - eventId: `event-${artifactId}`, - type: 'research_artifact_recorded', - sessionId: SESSION_ID, - ts: 2, - artifact: { - artifactId, - role, - name: `${artifactId}.md`, - createdAt: 2, - ...(role === 'source' ? { locator: 'https://example.com/source' } : {}), - contentHash: HASH, - sourceArtifactIds, - ...(reportSectionKey ? { reportSectionKey, reportSectionStatus: 'completed' as const } : {}), - }, - }; -} - -function checklist( - itemId: string, - evidenceArtifactIds: string[], -): DeepResearchChecklistUpdatedEvent { - const titles: Record = { - project_entrypoints: 'Map project entrypoints and execution setup', - core_flow: 'Trace the core implementation and data flow', - boundaries: 'Verify permissions, privacy, failure, and runtime boundaries', - verification_evidence: 'Collect tests, fixtures, and reproducible verification evidence', - }; - return { - eventId: `event-checklist-${itemId}`, - type: 'research_checklist_updated', - sessionId: SESSION_ID, - ts: 8, - item: { - itemId, - title: titles[itemId]!, - status: 'completed', - evidenceArtifactIds, - updatedAt: 8, - }, - }; -} - -function checkpoint( - round: number, - stage: DeepResearchCheckpointRecordedEvent['checkpoint']['stage'], - artifactIds: string[], -): DeepResearchCheckpointRecordedEvent { - return { - eventId: `event-checkpoint-${round}-${stage}`, - type: 'research_checkpoint_recorded', - sessionId: SESSION_ID, - ts: 3 + round, - checkpoint: { - checkpointId: `checkpoint-${round}-${stage}`, - round, - stage, - status: 'active', - summary: `Round ${round}`, - openQuestions: [], - nextSteps: ['Continue'], - taskIds: [], - artifactIds, - createdAt: 3 + round, - }, - }; -} - -function completed(reportArtifactId: string): DeepResearchCompletedEvent { - return { - eventId: 'event-complete', - type: 'research_completed', - sessionId: SESSION_ID, - ts: 10, - reportArtifactId, - handoff: { - artifactId: 'handoff-1', - implementationTasks: ['Implement the workspace.'], - recommendedIssues: ['Track UI progress.'], - recommendedPullRequests: [], - verificationCommands: ['npm test'], - }, - }; -} - -describe('Deep Research run projection', () => { - it('projects a source-grounded two-stage run through completion', () => { - const events: DeepResearchEvent[] = [ - started(), - artifact('source-1', 'source'), - artifact('note-1', 'evidence_note', ['source-1']), - checkpoint(1, 'knowledge_base', ['source-1', 'note-1']), - artifact('outline-1', 'outline', ['source-1']), - checkpoint(2, 'report_writing', ['source-1', 'note-1', 'outline-1']), - artifact('section-conclusion', 'report_section', ['source-1'], 'conclusion'), - artifact('section-evidence', 'report_section', ['source-1'], 'source_evidence'), - artifact('section-tradeoffs', 'report_section', ['source-1'], 'borrow_diverge_risk_gate'), - artifact( - 'section-implementation', - 'report_section', - ['source-1'], - 'implementation_recommendations', - ), - artifact('section-verification', 'report_section', ['source-1'], 'verification'), - artifact('report-1', 'report', ['source-1']), - artifact('handoff-1', 'handoff', ['source-1']), - checklist('project_entrypoints', ['source-1']), - checklist('core_flow', ['note-1']), - checklist('boundaries', ['source-1']), - checklist('verification_evidence', ['source-1']), - completed('report-1'), - ]; - - const projection = projectDeepResearchEvents(events); - - assert.deepEqual(projection.diagnostics, []); - assert.equal(projection.run?.status, 'completed'); - assert.equal(projection.run?.scopeLevel, 'standard'); - assert.equal(projection.run?.stage, 'completed'); - assert.equal(projection.run?.round, 2); - assert.equal(projection.run?.reportArtifactId, 'report-1'); - assert.equal(projection.run?.artifacts.length, 10); - assert.equal(projection.run?.checkpoints.length, 2); - assert.equal(projection.run?.handoff?.artifactId, 'handoff-1'); - }); - - it('rejects derived evidence that does not cite an archived source', () => { - const projection = projectDeepResearchEvents([ - started(), - artifact('note-1', 'evidence_note', ['missing-source']), - ]); - - assert.match(projection.diagnostics.join('\n'), /non-source artifact missing-source/); - assert.equal(projection.run?.artifacts.length, 0); - }); - - it('rejects checkpoint round and stage regression', () => { - const projection = projectDeepResearchEvents([ - started(), - artifact('source-1', 'source'), - checkpoint(2, 'report_writing', ['source-1']), - checkpoint(1, 'knowledge_base', ['source-1']), - ]); - - assert.match(projection.diagnostics.join('\n'), /round regressed/); - assert.equal(projection.run?.round, 2); - assert.equal(projection.run?.stage, 'report_writing'); - }); - - it('does not complete without a saved final report artifact', () => { - const missingReport = projectDeepResearchEvents([ - started(), - artifact('source-1', 'source'), - completed('missing-report'), - ]); - assert.match(missingReport.diagnostics.join('\n'), /missing report artifact/); - assert.equal(missingReport.run?.status, 'active'); - }); - - it('keeps completion gated on settled checklist items and report sections', () => { - const base: DeepResearchEvent[] = [ - started(), - artifact('source-1', 'source'), - artifact('report-1', 'report', ['source-1']), - artifact('handoff-1', 'handoff', ['source-1']), - ]; - const checklistBlocked = projectDeepResearchEvents([...base, completed('report-1')]); - assert.match(checklistBlocked.diagnostics.join('\n'), /checklist item project_entrypoints/); - - const sectionBlocked = projectDeepResearchEvents([ - ...base, - checklist('project_entrypoints', ['source-1']), - checklist('core_flow', ['source-1']), - checklist('boundaries', ['source-1']), - checklist('verification_evidence', ['source-1']), - completed('report-1'), - ]); - assert.match(sectionBlocked.diagnostics.join('\n'), /report section conclusion/); - assert.equal(sectionBlocked.run?.status, 'active'); - }); -}); diff --git a/packages/core/src/__tests__/deep-research-session.test.ts b/packages/core/src/__tests__/deep-research-session.test.ts deleted file mode 100644 index f4a3a3d27d..0000000000 --- a/packages/core/src/__tests__/deep-research-session.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { strict as assert } from 'node:assert'; -import { describe, it } from 'node:test'; -import { createGenesisExecutionBoundary } from '../sandbox-boundary.js'; - -describe('deep research session profile', () => { - it('gives explore sessions a managed read-only filesystem and restricted network', () => { - const boundary = createGenesisExecutionBoundary('explore'); - assert.equal(boundary.kind, 'managed'); - if (boundary.kind !== 'managed') return; - assert.equal(boundary.profile.name, 'read-only'); - assert.deepEqual(boundary.profile.fileSystem, { - kind: 'restricted', - entries: [{ kind: 'special', access: 'read', special: ':workspace_roots' }], - }); - assert.equal(boundary.profile.network.kind, 'restricted'); - }); -}); diff --git a/packages/core/src/__tests__/deep-research.test.ts b/packages/core/src/__tests__/deep-research.test.ts deleted file mode 100644 index d121adc850..0000000000 --- a/packages/core/src/__tests__/deep-research.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import assert from 'node:assert/strict'; -import { test } from 'node:test'; -import { buildDeepResearchSystemPromptFragment } from '../deep-research.js'; - -test('Deep Research directs inspection through the standard read-only tool surface', () => { - const prompt = buildDeepResearchSystemPromptFragment(); - - for (const toolName of ['Read', 'Glob', 'Grep', 'WebSearch']) { - assert.match(prompt, new RegExp(`\\b${toolName}\\b`)); - } - assert.doesNotMatch(prompt, /ExploreAgent/); -}); diff --git a/packages/core/src/__tests__/tool-quiet-preview.test.ts b/packages/core/src/__tests__/tool-quiet-preview.test.ts index 1404b67ad2..847fc19a5d 100644 --- a/packages/core/src/__tests__/tool-quiet-preview.test.ts +++ b/packages/core/src/__tests__/tool-quiet-preview.test.ts @@ -131,22 +131,6 @@ describe('projectToolArgsPreview', () => { ); }); - it('names deep research starts from their bounded objective preview', () => { - const preview = projectToolArgsPreview('deep_research_start', { - objective: 'Inspect the runtime host boundary', - scope_level: 'standard', - artifact_content: 'must not reach the live wire', - }); - assert.deepEqual(preview, { - objective: 'Inspect the runtime host boundary', - scope_level: 'standard', - }); - assert.equal( - formatToolInvocationLine({ toolName: 'deep_research_start', args: preview }, 'en'), - 'Inspect the runtime host boundary (standard)', - ); - }); - it('bounds long values and whole-preview size', () => { const preview = projectToolArgsPreview('Bash', { command: 'x'.repeat(5000) }); const command = (preview as { command: string }).command; diff --git a/packages/core/src/artifacts.ts b/packages/core/src/artifacts.ts index 51a598d5ba..171a8b0c66 100644 --- a/packages/core/src/artifacts.ts +++ b/packages/core/src/artifacts.ts @@ -137,8 +137,6 @@ export interface ArtifactRecord extends ArtifactDescriptor { * filesystem path to renderer code. */ relativePath: string; - /** Durable role for artifacts owned by a Deep Research workspace. */ - deepResearchRole?: import('./deep-research-run.js').DeepResearchArtifactRole; } interface ArtifactSourcePolicy { @@ -152,6 +150,7 @@ const ARTIFACT_SOURCE_POLICIES = { tool_result_projection: { userDeletable: false, userVisible: false, sharedReadable: true }, tool_result_archive: { userDeletable: false, userVisible: false, sharedReadable: false }, subagent_writeback: { userDeletable: false, userVisible: true, sharedReadable: false }, + // Kept so reports saved before Deep Research retirement remain readable. deep_research: { userDeletable: false, userVisible: true, sharedReadable: false }, user_upload: { userDeletable: true, userVisible: false, sharedReadable: true }, session_effect: { userDeletable: false, userVisible: false, sharedReadable: false }, diff --git a/packages/core/src/deep-research-client-progress.ts b/packages/core/src/deep-research-client-progress.ts deleted file mode 100644 index 19d628ba6e..0000000000 --- a/packages/core/src/deep-research-client-progress.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { buildDeepResearchImplementationPrompt } from './deep-research.js'; -import { - DEEP_RESEARCH_CLIENT_IMPLEMENTATION_PROMPT_MAX_BYTES, - DEEP_RESEARCH_CLIENT_OBJECTIVE_MAX_BYTES, - DEEP_RESEARCH_CLIENT_PROGRESS_MAX_BYTES, - DEEP_RESEARCH_CLIENT_RECENT_ITEMS_MAX, - DEEP_RESEARCH_CLIENT_TEXT_MAX_BYTES, - type DeepResearchClientProgress, - type DeepResearchRun, -} from './deep-research-run.js'; - -/** Build the single bounded product projection used by local and hosted clients. */ -export function projectDeepResearchClientProgress( - run: DeepResearchRun, -): DeepResearchClientProgress { - const blockers = [ - ...run.checklist.flatMap((item) => - item.blockedReason ? [labeledBlocker(item.title, item.blockedReason)] : [], - ), - ...run.steps.flatMap((step) => - step.blockedReason ? [labeledBlocker(step.objective, step.blockedReason)] : [], - ), - ]; - const base: DeepResearchClientProgress = { - sessionId: run.sessionId, - objective: truncateUtf8(run.objective, DEEP_RESEARCH_CLIENT_OBJECTIVE_MAX_BYTES), - scopeLevel: run.scopeLevel, - status: run.status, - stage: run.stage, - round: run.round, - createdAt: run.createdAt, - updatedAt: run.updatedAt, - artifactsCount: run.artifacts.length, - stepsCount: run.steps.length, - checklist: run.checklist.map((item) => ({ - itemId: item.itemId, - title: truncateUtf8(item.title, DEEP_RESEARCH_CLIENT_TEXT_MAX_BYTES), - status: item.status, - ...(item.blockedReason - ? { blockedReason: truncateUtf8(item.blockedReason, DEEP_RESEARCH_CLIENT_TEXT_MAX_BYTES) } - : {}), - })), - reportSections: run.reportSections.map(({ key, status }) => ({ key, status })), - recentInspectedRefs: run.steps - .flatMap((step) => step.inspectedRefs) - .slice(-DEEP_RESEARCH_CLIENT_RECENT_ITEMS_MAX) - .map((ref) => ({ - kind: ref.kind, - locator: truncateUtf8(ref.locator, DEEP_RESEARCH_CLIENT_TEXT_MAX_BYTES), - ...(ref.label - ? { label: truncateUtf8(ref.label, DEEP_RESEARCH_CLIENT_TEXT_MAX_BYTES) } - : {}), - })), - workerRunIds: [...new Set(run.steps.flatMap((step) => step.workerRunIds))].slice( - -DEEP_RESEARCH_CLIENT_RECENT_ITEMS_MAX, - ), - blockers: [ - ...new Set(blockers.map((value) => truncateUtf8(value, DEEP_RESEARCH_CLIENT_TEXT_MAX_BYTES))), - ].slice(-DEEP_RESEARCH_CLIENT_RECENT_ITEMS_MAX), - ...(run.reportArtifactId ? { reportArtifactId: run.reportArtifactId } : {}), - }; - if (encodedBytes(base) > DEEP_RESEARCH_CLIENT_PROGRESS_MAX_BYTES) { - throw new Error('Deep Research client progress cannot fit its required fields'); - } - if (run.status !== 'completed') return base; - return fitImplementationPrompt(base, buildDeepResearchImplementationPrompt(run)); -} - -function labeledBlocker(label: string, reason: string): string { - const separator = ': '; - const reasonBudget = Math.floor(DEEP_RESEARCH_CLIENT_TEXT_MAX_BYTES * 0.75); - const boundedReason = truncateUtf8(reason, reasonBudget); - const titleBudget = - DEEP_RESEARCH_CLIENT_TEXT_MAX_BYTES - utf8Bytes(separator) - utf8Bytes(boundedReason); - return `${truncateUtf8(label, titleBudget)}${separator}${boundedReason}`; -} - -function fitImplementationPrompt( - base: DeepResearchClientProgress, - prompt: string, -): DeepResearchClientProgress { - const bounded = truncateUtf8(prompt, DEEP_RESEARCH_CLIENT_IMPLEMENTATION_PROMPT_MAX_BYTES); - const full = { ...base, implementationPrompt: bounded }; - if (encodedBytes(full) <= DEEP_RESEARCH_CLIENT_PROGRESS_MAX_BYTES) return full; - const characters = Array.from(bounded); - let low = 1; - let high = characters.length; - let best = base; - while (low <= high) { - const midpoint = Math.floor((low + high) / 2); - const candidate = { - ...base, - implementationPrompt: truncateCharacters(bounded, midpoint), - }; - if (encodedBytes(candidate) <= DEEP_RESEARCH_CLIENT_PROGRESS_MAX_BYTES) { - best = candidate; - low = midpoint + 1; - } else { - high = midpoint - 1; - } - } - return best; -} - -function truncateCharacters(value: string, maxCharacters: number): string { - const characters = Array.from(value); - if (characters.length <= maxCharacters) return value; - if (maxCharacters === 1) return characters[0] ?? '…'; - return `${characters.slice(0, maxCharacters - 1).join('')}…`; -} - -function encodedBytes(value: unknown): number { - return new TextEncoder().encode(JSON.stringify(value)).byteLength; -} - -function utf8Bytes(value: string): number { - return new TextEncoder().encode(value).byteLength; -} - -function truncateUtf8(value: string, maxBytes: number): string { - const encoder = new TextEncoder(); - if (encoder.encode(value).byteLength <= maxBytes) return value; - const marker = '…'; - const markerBytes = encoder.encode(marker).byteLength; - let bytes = 0; - let output = ''; - for (const character of value) { - const characterBytes = encoder.encode(character).byteLength; - if (bytes + characterBytes + markerBytes > maxBytes) break; - output += character; - bytes += characterBytes; - } - return `${output}${marker}`; -} diff --git a/packages/core/src/deep-research-run.ts b/packages/core/src/deep-research-run.ts deleted file mode 100644 index daacf7d57c..0000000000 --- a/packages/core/src/deep-research-run.ts +++ /dev/null @@ -1,1055 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Durable Deep Research workspace contract. - * - * This independently reproduces the central systems idea from FS-Researcher - * (Zhu et al., ACL 2026): evidence collection and report writing share a - * persistent workspace that survives model-context and process boundaries. - * Maka keeps the canonical state as an append-only event ledger and stores - * large source/note/report bodies in the existing ArtifactStore. - * - * Paper: https://arxiv.org/abs/2602.01566 - */ - -import { isFiniteNumber, isRecord } from './record-schema.js'; -import { redactSecrets } from './redaction.js'; - -export const DEEP_RESEARCH_RUN_SCHEMA_VERSION = 1 as const; - -export const DEEP_RESEARCH_ACTIVE_STAGES = ['knowledge_base', 'report_writing'] as const; -export type DeepResearchActiveStage = (typeof DEEP_RESEARCH_ACTIVE_STAGES)[number]; - -export const DEEP_RESEARCH_STAGES = [...DEEP_RESEARCH_ACTIVE_STAGES, 'completed'] as const; -export type DeepResearchStage = (typeof DEEP_RESEARCH_STAGES)[number]; - -export const DEEP_RESEARCH_RUN_STATUSES = ['active', 'blocked', 'completed'] as const; -export type DeepResearchRunStatus = (typeof DEEP_RESEARCH_RUN_STATUSES)[number]; - -export const DEEP_RESEARCH_SCOPE_LEVELS = ['quick', 'standard', 'deep'] as const; -export type DeepResearchScopeLevel = (typeof DEEP_RESEARCH_SCOPE_LEVELS)[number]; - -export const DEEP_RESEARCH_ARTIFACT_ROLES = [ - 'source', - 'evidence_note', - 'outline', - 'report_section', - 'report', - 'handoff', -] as const; -export type DeepResearchArtifactRole = (typeof DEEP_RESEARCH_ARTIFACT_ROLES)[number]; - -export const DEEP_RESEARCH_CHECKLIST_STATUSES = [ - 'pending', - 'in_progress', - 'blocked', - 'completed', - 'skipped', -] as const; -export type DeepResearchChecklistStatus = (typeof DEEP_RESEARCH_CHECKLIST_STATUSES)[number]; - -export const DEEP_RESEARCH_REPORT_SECTION_KEYS = [ - 'conclusion', - 'source_evidence', - 'borrow_diverge_risk_gate', - 'implementation_recommendations', - 'verification', -] as const; -export type DeepResearchReportSectionKey = (typeof DEEP_RESEARCH_REPORT_SECTION_KEYS)[number]; - -export const DEEP_RESEARCH_REPORT_SECTION_STATUSES = ['pending', 'drafted', 'completed'] as const; -export type DeepResearchReportSectionStatus = - (typeof DEEP_RESEARCH_REPORT_SECTION_STATUSES)[number]; - -export const DEEP_RESEARCH_STEP_KINDS = ['local_exploration', 'web_research'] as const; -export type DeepResearchStepKind = (typeof DEEP_RESEARCH_STEP_KINDS)[number]; - -export const DEEP_RESEARCH_STEP_STATUSES = ['completed', 'blocked', 'stopped'] as const; -export type DeepResearchStepStatus = (typeof DEEP_RESEARCH_STEP_STATUSES)[number]; - -export const DEEP_RESEARCH_INSPECTED_REF_KINDS = [ - 'file', - 'symbol', - 'config', - 'test', - 'runtime', - 'url', -] as const; -export type DeepResearchInspectedRefKind = (typeof DEEP_RESEARCH_INSPECTED_REF_KINDS)[number]; - -export const DEEP_RESEARCH_EVENT_TYPES = [ - 'research_started', - 'research_artifact_recorded', - 'research_checklist_updated', - 'research_step_recorded', - 'research_checkpoint_recorded', - 'research_completed', -] as const; -export type DeepResearchEventType = (typeof DEEP_RESEARCH_EVENT_TYPES)[number]; - -export const DEEP_RESEARCH_OBJECTIVE_MAX_CHARS = 2_000; -export const DEEP_RESEARCH_ARTIFACT_NAME_MAX_CHARS = 240; -export const DEEP_RESEARCH_LOCATOR_MAX_CHARS = 4_096; -export const DEEP_RESEARCH_CHECKPOINT_TEXT_MAX_CHARS = 4_000; -export const DEEP_RESEARCH_CHECKPOINT_ITEM_MAX_CHARS = 1_000; -export const DEEP_RESEARCH_CHECKPOINT_ITEMS_MAX = 50; -export const DEEP_RESEARCH_REFS_MAX = 100; -export const DEEP_RESEARCH_ARTIFACTS_MAX = 2_000; -export const DEEP_RESEARCH_CHECKPOINTS_MAX = 500; -export const DEEP_RESEARCH_CHECKLIST_ITEMS_MAX = 50; -export const DEEP_RESEARCH_STEPS_MAX = 500; -export const DEEP_RESEARCH_STEP_TEXT_MAX_CHARS = 2_000; -export const DEEP_RESEARCH_STEP_LIST_ITEMS_MAX = 50; -export const DEEP_RESEARCH_INSPECTED_REFS_MAX = 200; -export const DEEP_RESEARCH_CLIENT_PROGRESS_MAX_BYTES = 46 * 1024; -export const DEEP_RESEARCH_CLIENT_RECENT_ITEMS_MAX = 8; -export const DEEP_RESEARCH_CLIENT_OBJECTIVE_MAX_BYTES = 4 * 1024; -export const DEEP_RESEARCH_CLIENT_TEXT_MAX_BYTES = 256; -export const DEEP_RESEARCH_CLIENT_IMPLEMENTATION_PROMPT_MAX_BYTES = 16 * 1024; - -export const DEEP_RESEARCH_DEFAULT_CHECKLIST = [ - { itemId: 'project_entrypoints', title: 'Map project entrypoints and execution setup' }, - { itemId: 'core_flow', title: 'Trace the core implementation and data flow' }, - { itemId: 'boundaries', title: 'Verify permissions, privacy, failure, and runtime boundaries' }, - { - itemId: 'verification_evidence', - title: 'Collect tests, fixtures, and reproducible verification evidence', - }, -] as const; - -export interface DeepResearchEventRefs { - runId?: string; - turnId?: string; - toolCallId?: string; -} - -export interface DeepResearchArtifactRef { - artifactId: string; - role: DeepResearchArtifactRole; - name: string; - /** Exact caller-provided summary used for semantic retry validation. */ - summary?: string; - createdAt: number; - /** URL, repository path, file path, or another human-inspectable source locator. */ - locator?: string; - /** sha256:, computed from the exact persisted artifact body. */ - contentHash: string; - /** Direct source artifacts supporting this derived note/report artifact. */ - sourceArtifactIds: string[]; - reportSectionKey?: DeepResearchReportSectionKey; - reportSectionStatus?: Exclude; -} - -export interface DeepResearchChecklistItem { - itemId: string; - title: string; - status: DeepResearchChecklistStatus; - evidenceArtifactIds: string[]; - blockedReason?: string; - updatedAt: number; -} - -export interface DeepResearchReportSectionState { - key: DeepResearchReportSectionKey; - status: DeepResearchReportSectionStatus; - artifactId?: string; - updatedAt: number; -} - -export interface DeepResearchInspectedRef { - kind: DeepResearchInspectedRefKind; - locator: string; - label?: string; - sourceArtifactId?: string; -} - -export interface DeepResearchStep { - stepId: string; - kind: DeepResearchStepKind; - status: DeepResearchStepStatus; - objective: string; - summary: string; - roots: string[]; - keywords: string[]; - ignoredPaths: string[]; - stoppingCondition: string; - expectedEvidence: string; - evidenceArtifactIds: string[]; - inspectedRefs: DeepResearchInspectedRef[]; - workerRunIds: string[]; - blockedReason?: string; - createdAt: number; -} - -export interface DeepResearchHandoff { - artifactId: string; - implementationTasks: string[]; - recommendedIssues: string[]; - recommendedPullRequests: string[]; - verificationCommands: string[]; -} - -export interface DeepResearchCheckpoint { - checkpointId: string; - round: number; - stage: DeepResearchActiveStage; - status: Exclude; - summary: string; - openQuestions: string[]; - nextSteps: string[]; - /** Existing Task Ledger ids/keys; the research ledger links rather than duplicates tasks. */ - taskIds: string[]; - /** Existing research artifact ids needed to resume this checkpoint. */ - artifactIds: string[]; - createdAt: number; -} - -interface DeepResearchEventBase { - eventId: string; - type: DeepResearchEventType; - sessionId: string; - ts: number; - refs?: DeepResearchEventRefs; -} - -export interface DeepResearchStartedEvent extends DeepResearchEventBase { - type: 'research_started'; - objective: string; - scopeLevel: DeepResearchScopeLevel; -} - -export interface DeepResearchArtifactRecordedEvent extends DeepResearchEventBase { - type: 'research_artifact_recorded'; - artifact: DeepResearchArtifactRef; -} - -export interface DeepResearchChecklistUpdatedEvent extends DeepResearchEventBase { - type: 'research_checklist_updated'; - item: DeepResearchChecklistItem; -} - -export interface DeepResearchStepRecordedEvent extends DeepResearchEventBase { - type: 'research_step_recorded'; - step: DeepResearchStep; -} - -export interface DeepResearchCheckpointRecordedEvent extends DeepResearchEventBase { - type: 'research_checkpoint_recorded'; - checkpoint: DeepResearchCheckpoint; -} - -export interface DeepResearchCompletedEvent extends DeepResearchEventBase { - type: 'research_completed'; - reportArtifactId: string; - handoff: DeepResearchHandoff; -} - -export type DeepResearchEvent = - | DeepResearchStartedEvent - | DeepResearchArtifactRecordedEvent - | DeepResearchChecklistUpdatedEvent - | DeepResearchStepRecordedEvent - | DeepResearchCheckpointRecordedEvent - | DeepResearchCompletedEvent; - -export interface DeepResearchRun { - schemaVersion: typeof DEEP_RESEARCH_RUN_SCHEMA_VERSION; - sessionId: string; - objective: string; - scopeLevel: DeepResearchScopeLevel; - status: DeepResearchRunStatus; - stage: DeepResearchStage; - round: number; - createdAt: number; - updatedAt: number; - artifacts: DeepResearchArtifactRef[]; - checklist: DeepResearchChecklistItem[]; - steps: DeepResearchStep[]; - reportSections: DeepResearchReportSectionState[]; - checkpoints: DeepResearchCheckpoint[]; - reportArtifactId?: string; - handoff?: DeepResearchHandoff; - completedAt?: number; -} - -/** Bounded product-facing progress shared by local and Host-backed clients. */ -export interface DeepResearchClientProgress { - sessionId: string; - objective: string; - scopeLevel: DeepResearchScopeLevel; - status: DeepResearchRunStatus; - stage: DeepResearchStage; - round: number; - createdAt: number; - updatedAt: number; - artifactsCount: number; - stepsCount: number; - checklist: Array< - Pick - >; - reportSections: Array>; - recentInspectedRefs: Array>; - workerRunIds: string[]; - blockers: string[]; - reportArtifactId?: string; - implementationPrompt?: string; -} - -export interface DeepResearchProjection { - run?: DeepResearchRun; - diagnostics: string[]; -} - -export interface DeepResearchMutationContext { - runId?: string; - turnId?: string; - toolCallId?: string; -} - -export interface DeepResearchChangedEvent { - sessionId: string; - ts: number; -} - -export interface DeepResearchStore { - read(sessionId: string): Promise; - readEvents(sessionId: string): Promise; - start( - sessionId: string, - objective: string, - scopeLevel: DeepResearchScopeLevel, - context?: DeepResearchMutationContext, - ): Promise; - recordArtifact( - sessionId: string, - artifact: DeepResearchArtifactRef, - context?: DeepResearchMutationContext, - ): Promise; - updateChecklist( - sessionId: string, - item: Omit, - context?: DeepResearchMutationContext, - ): Promise; - recordStep( - sessionId: string, - step: Omit, - context?: DeepResearchMutationContext, - ): Promise; - recordCheckpoint( - sessionId: string, - checkpoint: Omit, - context?: DeepResearchMutationContext, - ): Promise; - complete( - sessionId: string, - reportArtifactId: string, - handoff: DeepResearchHandoff, - context?: DeepResearchMutationContext, - ): Promise; - subscribe(listener: (event: DeepResearchChangedEvent) => void): () => void; -} - -export function isDeepResearchActiveStage(value: unknown): value is DeepResearchActiveStage { - return ( - typeof value === 'string' && (DEEP_RESEARCH_ACTIVE_STAGES as readonly string[]).includes(value) - ); -} - -export function isDeepResearchArtifactRole(value: unknown): value is DeepResearchArtifactRole { - return ( - typeof value === 'string' && (DEEP_RESEARCH_ARTIFACT_ROLES as readonly string[]).includes(value) - ); -} - -export function isDeepResearchScopeLevel(value: unknown): value is DeepResearchScopeLevel { - return ( - typeof value === 'string' && (DEEP_RESEARCH_SCOPE_LEVELS as readonly string[]).includes(value) - ); -} - -export function isDeepResearchChecklistStatus( - value: unknown, -): value is DeepResearchChecklistStatus { - return ( - typeof value === 'string' && - (DEEP_RESEARCH_CHECKLIST_STATUSES as readonly string[]).includes(value) - ); -} - -export function isDeepResearchReportSectionKey( - value: unknown, -): value is DeepResearchReportSectionKey { - return ( - typeof value === 'string' && - (DEEP_RESEARCH_REPORT_SECTION_KEYS as readonly string[]).includes(value) - ); -} - -export function isDeepResearchReportSectionStatus( - value: unknown, -): value is DeepResearchReportSectionStatus { - return ( - typeof value === 'string' && - (DEEP_RESEARCH_REPORT_SECTION_STATUSES as readonly string[]).includes(value) - ); -} - -export function isDeepResearchStepKind(value: unknown): value is DeepResearchStepKind { - return ( - typeof value === 'string' && (DEEP_RESEARCH_STEP_KINDS as readonly string[]).includes(value) - ); -} - -export function isDeepResearchStepStatus(value: unknown): value is DeepResearchStepStatus { - return ( - typeof value === 'string' && (DEEP_RESEARCH_STEP_STATUSES as readonly string[]).includes(value) - ); -} - -export function normalizeDeepResearchObjective(value: unknown): string | undefined { - if (typeof value !== 'string') return undefined; - const normalized = value.normalize('NFC').replace(/\s+/g, ' ').trim(); - if ( - normalized.length === 0 || - Array.from(normalized).length > DEEP_RESEARCH_OBJECTIVE_MAX_CHARS - ) { - return undefined; - } - return normalized; -} - -export function isDeepResearchEvent(value: unknown): value is DeepResearchEvent { - if ( - !isRecord(value) || - !isStableId(value.eventId) || - !(DEEP_RESEARCH_EVENT_TYPES as readonly unknown[]).includes(value.type) || - !isStableId(value.sessionId) || - !isFiniteNumber(value.ts) || - !isEventRefs(value.refs) - ) { - return false; - } - switch (value.type) { - case 'research_started': - return ( - normalizeDeepResearchObjective(value.objective) === value.objective && - isDeepResearchScopeLevel(value.scopeLevel) - ); - case 'research_artifact_recorded': - return isDeepResearchArtifactRef(value.artifact); - case 'research_checklist_updated': - return isDeepResearchChecklistItem(value.item); - case 'research_step_recorded': - return isDeepResearchStep(value.step); - case 'research_checkpoint_recorded': - return isDeepResearchCheckpoint(value.checkpoint); - case 'research_completed': - return isStableId(value.reportArtifactId) && isDeepResearchHandoff(value.handoff); - default: - return false; - } -} - -export function projectDeepResearchEvents( - events: readonly DeepResearchEvent[], -): DeepResearchProjection { - let run: DeepResearchRun | undefined; - const diagnostics: string[] = []; - const eventIds = new Set(); - const artifactIds = new Set(); - const checkpointIds = new Set(); - const stepIds = new Set(); - - for (const event of events) { - if (!isDeepResearchEvent(event)) { - diagnostics.push('invalid deep research event shape'); - continue; - } - if (eventIds.has(event.eventId)) { - diagnostics.push(`duplicate deep research event id ${event.eventId}`); - continue; - } - eventIds.add(event.eventId); - - if (event.type === 'research_started') { - if (run) { - diagnostics.push(`duplicate research_started for session ${event.sessionId}`); - continue; - } - run = { - schemaVersion: DEEP_RESEARCH_RUN_SCHEMA_VERSION, - sessionId: event.sessionId, - objective: event.objective, - scopeLevel: event.scopeLevel, - status: 'active', - stage: 'knowledge_base', - round: 0, - createdAt: event.ts, - updatedAt: event.ts, - artifacts: [], - checklist: defaultDeepResearchChecklist(event.ts), - steps: [], - reportSections: defaultReportSections(event.ts), - checkpoints: [], - }; - continue; - } - - if (!run) { - diagnostics.push(`${event.type} appeared before research_started`); - continue; - } - if (event.sessionId !== run.sessionId) { - diagnostics.push(`${event.type} belongs to another session`); - continue; - } - if (run.status === 'completed') { - diagnostics.push(`${event.type} appeared after research_completed`); - continue; - } - - switch (event.type) { - case 'research_artifact_recorded': { - if (run.artifacts.length >= DEEP_RESEARCH_ARTIFACTS_MAX) { - diagnostics.push(`deep research artifact cap ${DEEP_RESEARCH_ARTIFACTS_MAX} exceeded`); - break; - } - const artifact = event.artifact; - if (artifactIds.has(artifact.artifactId)) { - diagnostics.push(`duplicate research artifact ${artifact.artifactId}`); - break; - } - const sourceDiagnostic = validateSourceReferences(artifact, run.artifacts); - if (sourceDiagnostic) { - diagnostics.push(sourceDiagnostic); - break; - } - if (artifact.role === 'report_section') { - const currentSection = run.reportSections.find( - (section) => section.key === artifact.reportSectionKey, - ); - if ( - currentSection?.status === 'completed' && - artifact.reportSectionStatus === 'drafted' - ) { - diagnostics.push( - `report section ${artifact.reportSectionKey} cannot regress to drafted`, - ); - break; - } - } - artifactIds.add(artifact.artifactId); - const reportSections = - artifact.role === 'report_section' - ? applyReportSectionArtifact(run.reportSections, artifact) - : run.reportSections; - run = { - ...run, - artifacts: [ - ...run.artifacts, - { ...artifact, sourceArtifactIds: [...artifact.sourceArtifactIds] }, - ], - reportSections, - updatedAt: Math.max(run.updatedAt, event.ts), - }; - break; - } - case 'research_checklist_updated': { - const item = event.item; - const current = run.checklist.find((candidate) => candidate.itemId === item.itemId); - if (!current || current.title !== item.title) { - diagnostics.push(`research checklist references unknown item ${item.itemId}`); - break; - } - if ( - (current.status === 'completed' || current.status === 'skipped') && - current.status !== item.status - ) { - diagnostics.push(`research checklist item ${item.itemId} is already terminal`); - break; - } - const missingArtifact = item.evidenceArtifactIds.find((id) => !artifactIds.has(id)); - if (missingArtifact) { - diagnostics.push( - `research checklist item ${item.itemId} references unknown artifact ${missingArtifact}`, - ); - break; - } - if (item.status === 'completed' && item.evidenceArtifactIds.length === 0) { - diagnostics.push(`completed research checklist item ${item.itemId} requires evidence`); - break; - } - if (item.status === 'blocked' && !item.blockedReason) { - diagnostics.push(`blocked research checklist item ${item.itemId} requires a reason`); - break; - } - const checklist = run.checklist.map((candidate) => - candidate.itemId === item.itemId ? cloneChecklistItem(item) : candidate, - ); - run = { - ...run, - status: checklist.some((candidate) => candidate.status === 'blocked') - ? 'blocked' - : 'active', - checklist, - updatedAt: Math.max(run.updatedAt, event.ts), - }; - break; - } - case 'research_step_recorded': { - if (run.steps.length >= DEEP_RESEARCH_STEPS_MAX) { - diagnostics.push(`deep research step cap ${DEEP_RESEARCH_STEPS_MAX} exceeded`); - break; - } - const step = event.step; - if (stepIds.has(step.stepId)) { - diagnostics.push(`duplicate research step ${step.stepId}`); - break; - } - const missingEvidence = step.evidenceArtifactIds.find((id) => !artifactIds.has(id)); - if (missingEvidence) { - diagnostics.push( - `research step ${step.stepId} references unknown artifact ${missingEvidence}`, - ); - break; - } - const recordedArtifacts = run.artifacts; - const invalidInspectedSource = step.inspectedRefs.find((ref) => { - if (!ref.sourceArtifactId) return false; - return ( - recordedArtifacts.find((artifact) => artifact.artifactId === ref.sourceArtifactId) - ?.role !== 'source' - ); - }); - if (invalidInspectedSource?.sourceArtifactId) { - diagnostics.push( - `research step ${step.stepId} references non-source artifact ${invalidInspectedSource.sourceArtifactId}`, - ); - break; - } - if (step.status === 'completed' && step.evidenceArtifactIds.length === 0) { - diagnostics.push(`completed research step ${step.stepId} requires evidence artifacts`); - break; - } - if (step.status === 'blocked' && !step.blockedReason) { - diagnostics.push(`blocked research step ${step.stepId} requires a reason`); - break; - } - stepIds.add(step.stepId); - run = { - ...run, - status: step.status === 'blocked' ? 'blocked' : run.status, - steps: [...run.steps, cloneStep(step)], - updatedAt: Math.max(run.updatedAt, event.ts), - }; - break; - } - case 'research_checkpoint_recorded': { - if (run.checkpoints.length >= DEEP_RESEARCH_CHECKPOINTS_MAX) { - diagnostics.push( - `deep research checkpoint cap ${DEEP_RESEARCH_CHECKPOINTS_MAX} exceeded`, - ); - break; - } - const checkpoint = event.checkpoint; - if (checkpointIds.has(checkpoint.checkpointId)) { - diagnostics.push(`duplicate research checkpoint ${checkpoint.checkpointId}`); - break; - } - if (checkpoint.round < run.round) { - diagnostics.push(`research round regressed from ${run.round} to ${checkpoint.round}`); - break; - } - if (run.stage === 'report_writing' && checkpoint.stage === 'knowledge_base') { - diagnostics.push( - 'deep research stage cannot regress from report_writing to knowledge_base', - ); - break; - } - const missingArtifact = checkpoint.artifactIds.find((id) => !artifactIds.has(id)); - if (missingArtifact) { - diagnostics.push(`checkpoint references unknown artifact ${missingArtifact}`); - break; - } - checkpointIds.add(checkpoint.checkpointId); - run = { - ...run, - status: checkpoint.status, - stage: checkpoint.stage, - round: checkpoint.round, - checkpoints: [...run.checkpoints, cloneCheckpoint(checkpoint)], - updatedAt: Math.max(run.updatedAt, event.ts), - }; - break; - } - case 'research_completed': { - const report = run.artifacts.find( - (artifact) => artifact.artifactId === event.reportArtifactId, - ); - if (!report || report.role !== 'report') { - diagnostics.push( - `research_completed references missing report artifact ${event.reportArtifactId}`, - ); - break; - } - if (!run.artifacts.some((artifact) => artifact.role === 'source')) { - diagnostics.push('research_completed requires at least one archived source artifact'); - break; - } - const handoffArtifact = run.artifacts.find( - (artifact) => artifact.artifactId === event.handoff.artifactId, - ); - if (!handoffArtifact || handoffArtifact.role !== 'handoff') { - diagnostics.push( - `research_completed references missing handoff artifact ${event.handoff.artifactId}`, - ); - break; - } - const incompleteChecklist = run.checklist.find( - (item) => item.status !== 'completed' && item.status !== 'skipped', - ); - if (incompleteChecklist) { - diagnostics.push( - `research_completed requires checklist item ${incompleteChecklist.itemId} to be settled`, - ); - break; - } - const incompleteSection = run.reportSections.find( - (section) => section.status !== 'completed', - ); - if (incompleteSection) { - diagnostics.push(`research_completed requires report section ${incompleteSection.key}`); - break; - } - run = { - ...run, - status: 'completed', - stage: 'completed', - reportArtifactId: event.reportArtifactId, - handoff: cloneHandoff(event.handoff), - completedAt: event.ts, - updatedAt: Math.max(run.updatedAt, event.ts), - }; - break; - } - } - } - - return { ...(run ? { run } : {}), diagnostics }; -} - -function validateSourceReferences( - artifact: DeepResearchArtifactRef, - existing: readonly DeepResearchArtifactRef[], -): string | undefined { - if (artifact.role === 'source') { - if (!artifact.locator) return `source artifact ${artifact.artifactId} requires a locator`; - return artifact.sourceArtifactIds.length === 0 - ? undefined - : `source artifact ${artifact.artifactId} cannot cite another source artifact`; - } - if (artifact.sourceArtifactIds.length === 0) { - return `${artifact.role} artifact ${artifact.artifactId} requires source artifact references`; - } - const invalid = artifact.sourceArtifactIds.find((id) => { - const source = existing.find((item) => item.artifactId === id); - return source?.role !== 'source'; - }); - return invalid - ? `${artifact.role} artifact ${artifact.artifactId} references non-source artifact ${invalid}` - : undefined; -} - -function isDeepResearchArtifactRef(value: unknown): value is DeepResearchArtifactRef { - if (!isRecord(value)) return false; - if ( - !isStableId(value.artifactId) || - !isDeepResearchArtifactRole(value.role) || - !isBoundedText(value.name, DEEP_RESEARCH_ARTIFACT_NAME_MAX_CHARS) || - !( - value.summary === undefined || - isBoundedText(value.summary, DEEP_RESEARCH_CHECKPOINT_TEXT_MAX_CHARS) - ) || - !isFiniteNumber(value.createdAt) || - !( - value.locator === undefined || isBoundedText(value.locator, DEEP_RESEARCH_LOCATOR_MAX_CHARS) - ) || - typeof value.contentHash !== 'string' || - !/^sha256:[a-f0-9]{64}$/.test(value.contentHash) || - !isStableIdArray(value.sourceArtifactIds, DEEP_RESEARCH_REFS_MAX) - ) { - return false; - } - if (value.role === 'report_section') { - return ( - isDeepResearchReportSectionKey(value.reportSectionKey) && - (value.reportSectionStatus === 'drafted' || value.reportSectionStatus === 'completed') - ); - } - return value.reportSectionKey === undefined && value.reportSectionStatus === undefined; -} - -function isDeepResearchChecklistItem(value: unknown): value is DeepResearchChecklistItem { - return ( - isRecord(value) && - isStableId(value.itemId) && - isBoundedText(value.title, DEEP_RESEARCH_ARTIFACT_NAME_MAX_CHARS) && - isDeepResearchChecklistStatus(value.status) && - isStableIdArray(value.evidenceArtifactIds, DEEP_RESEARCH_REFS_MAX) && - (value.blockedReason === undefined || - isBoundedText(value.blockedReason, DEEP_RESEARCH_CHECKPOINT_TEXT_MAX_CHARS)) && - (value.status === 'blocked' - ? value.blockedReason !== undefined - : value.blockedReason === undefined) && - isFiniteNumber(value.updatedAt) - ); -} - -function isDeepResearchStep(value: unknown): value is DeepResearchStep { - return ( - isRecord(value) && - isStableId(value.stepId) && - isDeepResearchStepKind(value.kind) && - isDeepResearchStepStatus(value.status) && - isBoundedText(value.objective, DEEP_RESEARCH_STEP_TEXT_MAX_CHARS) && - isBoundedText(value.summary, DEEP_RESEARCH_STEP_TEXT_MAX_CHARS) && - isBoundedTextArray( - value.roots, - DEEP_RESEARCH_STEP_LIST_ITEMS_MAX, - DEEP_RESEARCH_LOCATOR_MAX_CHARS, - ) && - (value.kind !== 'local_exploration' || value.roots.length > 0) && - isBoundedTextArray( - value.keywords, - DEEP_RESEARCH_STEP_LIST_ITEMS_MAX, - DEEP_RESEARCH_CHECKPOINT_ITEM_MAX_CHARS, - ) && - (value.kind !== 'web_research' || value.keywords.length > 0) && - isBoundedTextArray( - value.ignoredPaths, - DEEP_RESEARCH_STEP_LIST_ITEMS_MAX, - DEEP_RESEARCH_LOCATOR_MAX_CHARS, - ) && - isBoundedText(value.stoppingCondition, DEEP_RESEARCH_STEP_TEXT_MAX_CHARS) && - isBoundedText(value.expectedEvidence, DEEP_RESEARCH_STEP_TEXT_MAX_CHARS) && - isStableIdArray(value.evidenceArtifactIds, DEEP_RESEARCH_REFS_MAX) && - Array.isArray(value.inspectedRefs) && - value.inspectedRefs.length <= DEEP_RESEARCH_INSPECTED_REFS_MAX && - value.inspectedRefs.every(isDeepResearchInspectedRef) && - isStableIdArray(value.workerRunIds, DEEP_RESEARCH_REFS_MAX) && - (value.blockedReason === undefined || - isBoundedText(value.blockedReason, DEEP_RESEARCH_STEP_TEXT_MAX_CHARS)) && - (value.status === 'blocked' - ? value.blockedReason !== undefined - : value.blockedReason === undefined) && - isFiniteNumber(value.createdAt) - ); -} - -function isDeepResearchInspectedRef(value: unknown): value is DeepResearchInspectedRef { - return ( - isRecord(value) && - typeof value.kind === 'string' && - (DEEP_RESEARCH_INSPECTED_REF_KINDS as readonly string[]).includes(value.kind) && - isBoundedText(value.locator, DEEP_RESEARCH_LOCATOR_MAX_CHARS) && - (value.label === undefined || - isBoundedText(value.label, DEEP_RESEARCH_CHECKPOINT_ITEM_MAX_CHARS)) && - (value.sourceArtifactId === undefined || isStableId(value.sourceArtifactId)) - ); -} - -function isDeepResearchHandoff(value: unknown): value is DeepResearchHandoff { - return ( - isRecord(value) && - isStableId(value.artifactId) && - isBoundedTextArray( - value.implementationTasks, - DEEP_RESEARCH_CHECKPOINT_ITEMS_MAX, - DEEP_RESEARCH_CHECKPOINT_ITEM_MAX_CHARS, - ) && - value.implementationTasks.length > 0 && - isBoundedTextArray( - value.recommendedIssues, - DEEP_RESEARCH_CHECKPOINT_ITEMS_MAX, - DEEP_RESEARCH_CHECKPOINT_ITEM_MAX_CHARS, - ) && - isBoundedTextArray( - value.recommendedPullRequests, - DEEP_RESEARCH_CHECKPOINT_ITEMS_MAX, - DEEP_RESEARCH_CHECKPOINT_ITEM_MAX_CHARS, - ) && - value.recommendedIssues.length + value.recommendedPullRequests.length > 0 && - isBoundedTextArray( - value.verificationCommands, - DEEP_RESEARCH_CHECKPOINT_ITEMS_MAX, - DEEP_RESEARCH_CHECKPOINT_ITEM_MAX_CHARS, - ) && - value.verificationCommands.length > 0 - ); -} - -function isDeepResearchCheckpoint(value: unknown): value is DeepResearchCheckpoint { - return ( - isRecord(value) && - isStableId(value.checkpointId) && - Number.isSafeInteger(value.round) && - (value.round as number) >= 1 && - isDeepResearchActiveStage(value.stage) && - (value.status === 'active' || value.status === 'blocked') && - isBoundedText(value.summary, DEEP_RESEARCH_CHECKPOINT_TEXT_MAX_CHARS) && - isBoundedTextArray( - value.openQuestions, - DEEP_RESEARCH_CHECKPOINT_ITEMS_MAX, - DEEP_RESEARCH_CHECKPOINT_ITEM_MAX_CHARS, - ) && - isBoundedTextArray( - value.nextSteps, - DEEP_RESEARCH_CHECKPOINT_ITEMS_MAX, - DEEP_RESEARCH_CHECKPOINT_ITEM_MAX_CHARS, - ) && - isStableIdArray(value.taskIds, DEEP_RESEARCH_REFS_MAX) && - isStableIdArray(value.artifactIds, DEEP_RESEARCH_REFS_MAX) && - isFiniteNumber(value.createdAt) - ); -} - -function isEventRefs(value: unknown): value is DeepResearchEventRefs | undefined { - if (value === undefined) return true; - if (!isRecord(value)) return false; - const keys = Object.keys(value); - if (keys.some((key) => !['runId', 'turnId', 'toolCallId'].includes(key))) return false; - return [value.runId, value.turnId, value.toolCallId].every( - (item) => item === undefined || isBoundedReference(item), - ); -} - -function isBoundedReference(value: unknown): value is string { - return typeof value === 'string' && value.length >= 1 && value.length <= 512; -} - -function isStableId(value: unknown): value is string { - return ( - typeof value === 'string' && - value.length >= 1 && - value.length <= 128 && - /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(value) && - redactSecrets(value) === value - ); -} - -function isStableIdArray(value: unknown, max: number): value is string[] { - return ( - Array.isArray(value) && - value.length <= max && - new Set(value).size === value.length && - value.every(isStableId) - ); -} - -function isBoundedText(value: unknown, max: number): value is string { - return typeof value === 'string' && value.trim().length > 0 && Array.from(value).length <= max; -} - -function isBoundedTextArray(value: unknown, maxItems: number, maxChars: number): value is string[] { - return ( - Array.isArray(value) && - value.length <= maxItems && - value.every((item) => isBoundedText(item, maxChars)) - ); -} - -function cloneCheckpoint(checkpoint: DeepResearchCheckpoint): DeepResearchCheckpoint { - return { - ...checkpoint, - openQuestions: [...checkpoint.openQuestions], - nextSteps: [...checkpoint.nextSteps], - taskIds: [...checkpoint.taskIds], - artifactIds: [...checkpoint.artifactIds], - }; -} - -function defaultDeepResearchChecklist(ts: number): DeepResearchChecklistItem[] { - return DEEP_RESEARCH_DEFAULT_CHECKLIST.map((item) => ({ - ...item, - status: 'pending', - evidenceArtifactIds: [], - updatedAt: ts, - })); -} - -function defaultReportSections(ts: number): DeepResearchReportSectionState[] { - return DEEP_RESEARCH_REPORT_SECTION_KEYS.map((key) => ({ - key, - status: 'pending', - updatedAt: ts, - })); -} - -function applyReportSectionArtifact( - sections: readonly DeepResearchReportSectionState[], - artifact: DeepResearchArtifactRef, -): DeepResearchReportSectionState[] { - if ( - artifact.role !== 'report_section' || - !artifact.reportSectionKey || - !artifact.reportSectionStatus - ) { - return [...sections]; - } - return sections.map((section) => - section.key === artifact.reportSectionKey - ? { - key: section.key, - status: artifact.reportSectionStatus!, - artifactId: artifact.artifactId, - updatedAt: artifact.createdAt, - } - : section, - ); -} - -function cloneChecklistItem(item: DeepResearchChecklistItem): DeepResearchChecklistItem { - return { - ...item, - evidenceArtifactIds: [...item.evidenceArtifactIds], - }; -} - -function cloneStep(step: DeepResearchStep): DeepResearchStep { - return { - ...step, - roots: [...step.roots], - keywords: [...step.keywords], - ignoredPaths: [...step.ignoredPaths], - evidenceArtifactIds: [...step.evidenceArtifactIds], - inspectedRefs: step.inspectedRefs.map((ref) => ({ ...ref })), - workerRunIds: [...step.workerRunIds], - }; -} - -function cloneHandoff(handoff: DeepResearchHandoff): DeepResearchHandoff { - return { - ...handoff, - implementationTasks: [...handoff.implementationTasks], - recommendedIssues: [...handoff.recommendedIssues], - recommendedPullRequests: [...handoff.recommendedPullRequests], - verificationCommands: [...handoff.verificationCommands], - }; -} diff --git a/packages/core/src/deep-research.ts b/packages/core/src/deep-research.ts deleted file mode 100644 index 3088a90fee..0000000000 --- a/packages/core/src/deep-research.ts +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** Read-only Deep Research session semantics and prompt contract. */ - -import type { DeepResearchRun } from './deep-research-run.js'; -import { SESSION_START_MODE_SPECS } from './session-start-mode.js'; - -export const DEEP_RESEARCH_SESSION_NAME = SESSION_START_MODE_SPECS.deep_research.name; -export const DEEP_RESEARCH_SESSION_LABEL = SESSION_START_MODE_SPECS.deep_research.labels[0]; - -export const DEEP_RESEARCH_WORKFLOW_STEPS = [ - { - title: '先定位入口', - body: '读目录、配置、启动链路和测试入口,建立项目地图。', - }, - { - title: '再追数据流', - body: '沿关键模块、IPC、存储、权限和运行时边界追到真实实现。', - }, - { - title: '然后对照参考', - body: '把可借鉴点拆成 borrow / diverge / risk / gate。', - }, - { - title: '最后给可合入方案', - body: '输出文件清单、风险边界和验证命令,不在只读模式里动手改。', - }, -] as const; - -export const DEEP_RESEARCH_REPORT_SECTIONS = [ - { - title: '结论先行', - body: '用 3-5 条讲清楚真实现状、主要差距和优先建议。', - }, - { - title: '源码证据', - body: '列出文件、函数、配置、测试和运行时路径,避免只给印象判断。', - }, - { - title: '借鉴拆解', - body: '每个可借鉴点都写 borrow / diverge / risk / gate。', - }, - { - title: '落地改进', - body: '给出按小步改进拆分的文件清单、边界和验证命令。', - }, -] as const; - -export const DEEP_RESEARCH_SCOPE_OPTIONS = [ - { - label: '快速', - body: '只扫入口、关键文件和最可能的数据流,适合已知范围的小问题。', - }, - { - label: '标准', - body: '默认深度:梳理核心链路、相关测试和主要风险,再给落地建议。', - }, - { - label: '深挖', - body: '跨模块、参考项目和边界条件多轮追踪;只在用户明确要求时使用。', - }, -] as const; - -export const DEEP_RESEARCH_EVIDENCE_CHECKLIST = [ - { - title: '项目入口', - body: '先看 README、package/config、启动脚本和目录分层,确认真实运行方式。', - }, - { - title: '核心链路', - body: '追 UI 入口、IPC/服务、存储、运行时调用和错误处理,不只看表面组件。', - }, - { - title: '边界条件', - body: '检查权限、隐身模式、token/路径暴露、失败重试和用户可见反馈。', - }, - { - title: '验证证据', - body: '找对应测试、fixture、smoke 文档和可复现命令;缺口要明确标出来。', - }, -] as const; - -export const DEEP_RESEARCH_PROGRESS_CHECKPOINTS = [ - { - title: '先建清单', - body: '研究范围超过三个相互关联的点时,先列出可核验的检查项再开始追代码。', - }, - { - title: '标当前项', - body: '推进时明确当前正在验证哪一项,拿到证据后再进入下一项。', - }, - { - title: '记阻塞点', - body: '找不到源码、运行时或测试证据时标成 blocked,不用猜测补空白。', - }, - { - title: '收敛方案', - body: '完成项必须汇总到 borrow / diverge / risk / gate 和可落地改进里。', - }, -] as const; - -export const DEEP_RESEARCH_STARTER_PROMPTS = [ - { - label: '研究一个参考项目', - prompt: - '请只读研究这个项目:先梳理目录结构、核心模块、启动链路、数据流和测试入口,然后列出我们可以借鉴的功能设计、需要规避的风险,以及可落地到 Maka 的改进顺序。', - }, - { - label: '完整读一遍参考项目', - prompt: - '请按深挖范围只读研究这个参考项目:先建立目录和模块地图,再逐层读核心功能、运行时、存储、权限、UI、测试和文档;每个可借鉴点都按 borrow / diverge / risk / gate 输出,并给出 Maka 的落地改进顺序。', - }, - { - label: '对比一个功能实现', - prompt: - '请只读对比这个功能在参考项目和 Maka 里的实现差异:指出关键文件、运行时边界、UI 入口、持久化方式、测试覆盖,以及最小可合入的改进方案。', - }, - { - label: '做一次安全边界审计', - prompt: - '请只读审计这个功能的安全边界:权限、token/密钥流、IPC/renderer 暴露、文件路径、隐私模式、日志与 telemetry。输出 blocking 风险和对应 contract test。', - }, -] as const; - -export function isDeepResearchSession(labels: readonly string[] | undefined): boolean { - return Array.isArray(labels) && labels.includes(DEEP_RESEARCH_SESSION_LABEL); -} - -export const DEEP_RESEARCH_IMPLEMENTATION_PROMPT_MAX_CHARS = 12_000; - -export function buildDeepResearchImplementationPrompt(run: DeepResearchRun): string { - if (run.status !== 'completed' || !run.handoff || !run.reportArtifactId) { - throw new Error('Deep Research implementation handoff requires a completed run'); - } - const lines: string[] = [ - 'This is a new implementation task created from a completed read-only Deep Research session.', - 'The original research session remains read-only. Inspect the current code and present an implementation plan before changing project files.', - '', - `Research objective: ${run.objective}`, - `Source session: ${run.sessionId}`, - `Final report artifact: ${run.reportArtifactId}`, - `Handoff artifact: ${run.handoff.artifactId}`, - '', - 'Implementation tasks:', - ...run.handoff.implementationTasks.map((item) => `- ${item}`), - '', - 'Recommended issues:', - ...(run.handoff.recommendedIssues.length > 0 - ? run.handoff.recommendedIssues.map((item) => `- ${item}`) - : ['- None specified.']), - '', - 'Recommended pull requests:', - ...(run.handoff.recommendedPullRequests.length > 0 - ? run.handoff.recommendedPullRequests.map((item) => `- ${item}`) - : ['- None specified.']), - '', - 'Verification commands:', - ...run.handoff.verificationCommands.map((item) => `- ${item}`), - ]; - const content = lines.join('\n'); - const characters = Array.from(content); - if (characters.length <= DEEP_RESEARCH_IMPLEMENTATION_PROMPT_MAX_CHARS) return content; - const marker = '\n[Handoff truncated to the safe composer limit.]'; - return ( - characters - .slice(0, DEEP_RESEARCH_IMPLEMENTATION_PROMPT_MAX_CHARS - Array.from(marker).length) - .join('') + marker - ); -} - -export function buildDeepResearchSystemPromptFragment(): string { - return [ - 'Deep research mode is active for this session.', - '', - 'Mode contract:', - '- Inspect first. Prefer Read, Glob, Grep, and WebSearch.', - '- Do not write, edit, delete, move, or rename user project files; do not install, run migrations, start services, or send network requests unless the user explicitly leaves research mode.', - '- The deep_research_* tools are the one write exception: they only update Maka-owned research artifacts and an append-only workspace ledger, never the user project.', - '- If implementation is needed, produce a concrete plan with files, risks, and verification commands instead of modifying files.', - '- Keep findings source-grounded: name files, functions, configs, tests, and observed behavior.', - '- Summarize borrow / diverge / risk / gate when comparing a reference project to Maka.', - '', - 'Durable workspace protocol:', - '- Call deep_research_start once with the concrete objective and scope level. After interruption or context compaction, call deep_research_status, then deep_research_read_artifact for the exact saved evidence needed to continue.', - '- Knowledge-base stage: archive each important raw source first with deep_research_save_artifact role=source, then save evidence notes that cite those source artifact ids.', - '- After each bounded local exploration or web-research substep, call deep_research_record_step with roots or query terms, ignored paths, a stopping condition, expected evidence, inspected files/symbols/URLs, worker run ids, persisted evidence ids, and any blocker.', - '- Keep the four durable checklist items current with deep_research_update_checklist. Completed items require evidence artifacts; blocked items require an explicit reason.', - '- Checkpoint every meaningful research round with deep_research_checkpoint, including open questions, next steps, related task ids, and the artifacts needed to resume.', - '- Report-writing stage: save an outline, then source-backed report_section artifacts for conclusion, source_evidence, borrow_diverge_risk_gate, implementation_recommendations, and verification. Mark each section completed only when it is ready.', - '- Save one final role=report artifact and one role=handoff artifact. The handoff must turn findings into implementation tasks, recommended issues and/or PRs, and verification commands without performing project writes.', - '- Call deep_research_complete only after every checklist item is completed or explicitly skipped, all five report sections are completed, and both report and handoff artifacts are persisted.', - '', - 'Research workflow:', - ...DEEP_RESEARCH_WORKFLOW_STEPS.map((step) => `- ${step.title}: ${step.body}`), - '', - 'Research scope budget:', - ...DEEP_RESEARCH_SCOPE_OPTIONS.map((option) => `- ${option.label}: ${option.body}`), - '- If the user does not specify a scope, use 标准. Use 深挖 only when the user explicitly asks for deep / exhaustive / full-project research.', - '', - 'Evidence checklist:', - ...DEEP_RESEARCH_EVIDENCE_CHECKLIST.map((item) => `- ${item.title}: ${item.body}`), - '- If any checklist area cannot be verified from available files or runtime context, call that out explicitly instead of guessing.', - '', - 'Progress checkpoints:', - ...DEEP_RESEARCH_PROGRESS_CHECKPOINTS.map((item) => `- ${item.title}: ${item.body}`), - '- Treat the checklist as a control loop for multi-step research, not as a hidden task system. Keep it visible in the answer or status update when the research spans multiple modules.', - '', - 'Final report contract:', - ...DEEP_RESEARCH_REPORT_SECTIONS.map((section) => `- ${section.title}: ${section.body}`), - ].join('\n'); -} diff --git a/packages/core/src/session-start-mode.ts b/packages/core/src/session-start-mode.ts index b90dc13859..08845378fa 100644 --- a/packages/core/src/session-start-mode.ts +++ b/packages/core/src/session-start-mode.ts @@ -33,11 +33,6 @@ export interface SessionStartModeSpec { } export const SESSION_START_MODE_SPECS = { - deep_research: { - name: 'Deep Research', - labels: ['mode:deep_research'], - permissionMode: 'explore', - }, bot: { labels: ['mode:bot'], permissionMode: 'explore', @@ -49,6 +44,8 @@ export const SESSION_START_MODES: readonly SessionStartMode[] = Object.keys( SESSION_START_MODE_SPECS, ) as SessionStartMode[]; export const SESSION_START_MODE_LABELS: readonly string[] = [ + // Historical mode labels remain reserved when editing old Sessions. + 'mode:deep_research', ...new Set(Object.values(SESSION_START_MODE_SPECS).flatMap((spec) => spec.labels)), ]; diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 924704f44b..35cc3d9446 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -62,8 +62,6 @@ import { markPersisted, type PersistedValue } from './persisted-value.js'; import type { SubagentWorkspaceBinding } from './subagent-workspace.js'; import { decodeTurnOrigin, type TurnOrigin } from './turn-origin.js'; -export { DEEP_RESEARCH_SESSION_LABEL, isDeepResearchSession } from './deep-research.js'; - /** Runtime execution states. Archive visibility is represented by `isArchived`. */ export const SESSION_STATUSES = [ 'active', diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts index fded36e497..910df91cd0 100644 --- a/packages/core/src/settings.ts +++ b/packages/core/src/settings.ts @@ -486,7 +486,7 @@ export interface PrivacySettings { } /** - * `explore` is excluded — it's reserved for Deep Research sessions and + * `explore` is excluded — it's reserved for read-only sessions and * Bot-incoming guards and is never a mode the user picks, in the composer * dropdown or here. Derived from the canonical PERMISSION_MODES (not a * hand-copied literal) so adding a future mode updates every consumer — diff --git a/packages/core/src/tool-quiet-preview.ts b/packages/core/src/tool-quiet-preview.ts index 760d8990d4..697006f21a 100644 --- a/packages/core/src/tool-quiet-preview.ts +++ b/packages/core/src/tool-quiet-preview.ts @@ -269,14 +269,6 @@ export function formatToolInvocationLine( return parts.join(' · '); } - if (name === 'deep_research_start') { - const objective = stringField(args, 'objective'); - if (objective) { - const scopeLevel = stringField(args, 'scope_level'); - return redactSecrets(scopeLevel ? `${objective} (${scopeLevel})` : objective); - } - } - if (name === 'GoalSet') { const condition = stringField(args, 'condition'); if (condition) return redactSecrets(condition); @@ -365,11 +357,6 @@ const ARGS_PREVIEW_SCALAR_KEYS = [ 'ref', ] as const; -// This tool's objective is the compact row's durable headline. Keep it -// explicit rather than widening the generic wire allowlist with a broad key -// such as `input`: non-WriteStdin tools otherwise retain arbitrary payloads. -const DEEP_RESEARCH_START_PREVIEW_SCALAR_KEYS = ['objective', 'scope_level'] as const; - const ARGS_PREVIEW_NUMBER_KEYS = ['offset', 'limit'] as const; function boundPreviewString(value: string): string { @@ -453,13 +440,9 @@ export function projectToolArgsPreview( // Apply the canonical activity projection first so WriteStdin's inputPreview // shape (bounded, display-safe) is what the whitelist picks up. const projected = asRecord(projectToolActivityArgs(toolName, args)) ?? record; - const scalarKeys = - toolName === 'deep_research_start' - ? [...ARGS_PREVIEW_SCALAR_KEYS, ...DEEP_RESEARCH_START_PREVIEW_SCALAR_KEYS] - : ARGS_PREVIEW_SCALAR_KEYS; const picked = new Map(); - for (const key of scalarKeys) { + for (const key of ARGS_PREVIEW_SCALAR_KEYS) { if (isSensitiveKey(key)) continue; const value = previewStringField(projected, key); if (value !== undefined) picked.set(key, value); @@ -492,7 +475,7 @@ export function projectToolArgsPreview( // Enforce the whole-preview budget by dropping lowest-priority fields; the // first picked (highest-priority) field always survives. const keysByPriority = [ - ...scalarKeys, + ...ARGS_PREVIEW_SCALAR_KEYS, ...ARGS_PREVIEW_NUMBER_KEYS, 'inputPreview', 'size', diff --git a/packages/runtime-host/src/__tests__/deep-research-protocol.test.ts b/packages/runtime-host/src/__tests__/deep-research-protocol.test.ts deleted file mode 100644 index 6696814a19..0000000000 --- a/packages/runtime-host/src/__tests__/deep-research-protocol.test.ts +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import assert from 'node:assert/strict'; -import { test } from 'node:test'; -import { - DEEP_RESEARCH_DEFAULT_CHECKLIST, - DEEP_RESEARCH_REPORT_SECTION_KEYS, - projectDeepResearchEvents, - type DeepResearchArtifactRef, - type DeepResearchEvent, -} from '@maka/core/deep-research-run'; -import { - decodeDeepResearchQueryResult, - decodeRequestFrame, - decodeResponseFrame, - DEEP_RESEARCH_RECENT_REFS_MAX, - DEEP_RESEARCH_RESULT_MAX_BYTES, - HOST_OPERATION_SPECS, -} from '../protocol/index.js'; -import { projectDeepResearchRun } from '../server/deep-research-coordinator.js'; - -const snapshot = { - kind: 'snapshot' as const, - sessionId: 'session-1', - revision: 3, - objective: 'Trace the Runtime Host boundary', - scopeLevel: 'standard' as const, - status: 'active' as const, - stage: 'knowledge_base' as const, - round: 1, - createdAt: 1, - updatedAt: 2, - artifactsCount: 2, - stepsCount: 1, - checklist: [ - { - itemId: 'core_flow', - title: 'Trace the core implementation and data flow', - status: 'in_progress' as const, - blockedReason: null, - }, - ], - reportSections: [{ key: 'conclusion' as const, status: 'pending' as const }], - recentInspectedRefs: [{ kind: 'file' as const, locator: 'src/main.ts', label: null }], - workerRunIds: ['worker.run:1'], - blockers: [], - reportArtifactId: null, - implementationPrompt: null, -}; - -test('Deep Research protocol exposes one bounded read-only projection', () => { - assert.equal(HOST_OPERATION_SPECS['deep-research.query'].mode, 'query'); - assert.deepEqual( - decodeRequestFrame({ - requestId: 'request-1', - operation: 'deep-research.query', - input: { sessionId: 'session-1' }, - }), - { - requestId: 'request-1', - operation: 'deep-research.query', - input: { sessionId: 'session-1' }, - }, - ); - assert.deepEqual( - decodeResponseFrame({ - requestId: 'request-1', - operation: 'deep-research.query', - ok: true, - result: snapshot, - }), - { - requestId: 'request-1', - operation: 'deep-research.query', - ok: true, - result: snapshot, - }, - ); -}); - -test('Deep Research projection rejects open, oversized, and cross-Session shapes', () => { - assert.deepEqual(decodeDeepResearchQueryResult(snapshot), snapshot); - assert.throws(() => decodeDeepResearchQueryResult({ ...snapshot, extra: true })); - assert.throws(() => - decodeDeepResearchQueryResult({ - ...snapshot, - recentInspectedRefs: Array.from({ length: DEEP_RESEARCH_RECENT_REFS_MAX + 1 }, () => ({ - kind: 'file', - locator: 'src/main.ts', - label: null, - })), - }), - ); - assert.throws(() => - HOST_OPERATION_SPECS['deep-research.query'].assertOutputForInput?.( - { sessionId: 'session-1' }, - { ...snapshot, sessionId: 'session-2' }, - ), - ); - assert.throws(() => - decodeDeepResearchQueryResult({ kind: 'not_started', sessionId: 'session-1', revision: 1 }), - ); -}); - -test('Deep Research producer fits escaping-heavy legal state to its encoded result budget', () => { - const projectionResult = projectDeepResearchEvents(escapingHeavyCompletedEvents()); - assert.deepEqual(projectionResult.diagnostics, []); - assert.ok(projectionResult.run); - assert.equal(projectionResult.run.status, 'completed'); - - const projection = projectDeepResearchRun(projectionResult.run, 7); - assert.deepEqual(decodeDeepResearchQueryResult(projection), projection); - assert.ok( - Buffer.byteLength(JSON.stringify(projection), 'utf8') <= DEEP_RESEARCH_RESULT_MAX_BYTES, - ); -}); - -function escapingHeavyCompletedEvents(): DeepResearchEvent[] { - const escaped = '\u0000"\\'; - const sessionId = 'session-research'; - let eventIndex = 0; - let timestamp = 0; - const event = >( - value: T, - ): T & Pick => ({ - ...value, - eventId: `event-${++eventIndex}`, - sessionId, - ts: ++timestamp, - }); - const artifact = ( - value: Omit, - ): DeepResearchArtifactRef => ({ - ...value, - createdAt: timestamp + 1, - contentHash: `sha256:${'a'.repeat(64)}`, - }); - const sourceArtifact = artifact({ - artifactId: 'source-1', - role: 'source', - name: 'source.md', - locator: 'https://example.com/source', - sourceArtifactIds: [], - }); - const evidenceArtifact = artifact({ - artifactId: 'evidence-1', - role: 'evidence_note', - name: 'evidence.md', - sourceArtifactIds: [sourceArtifact.artifactId], - }); - const reportSectionArtifacts = DEEP_RESEARCH_REPORT_SECTION_KEYS.map((key) => - artifact({ - artifactId: `section-${key}`, - role: 'report_section', - name: `${key}.md`, - sourceArtifactIds: [sourceArtifact.artifactId], - reportSectionKey: key, - reportSectionStatus: 'completed', - }), - ); - const reportArtifact = artifact({ - artifactId: 'report-1', - role: 'report', - name: 'report.md', - sourceArtifactIds: [sourceArtifact.artifactId], - }); - const handoffArtifact = artifact({ - artifactId: 'handoff-1', - role: 'handoff', - name: 'handoff.md', - sourceArtifactIds: [sourceArtifact.artifactId], - }); - const artifacts = [ - sourceArtifact, - evidenceArtifact, - ...reportSectionArtifacts, - reportArtifact, - handoffArtifact, - ]; - - return [ - event({ - type: 'research_started', - objective: 'Inspect the Runtime Host boundary', - scopeLevel: 'deep', - }), - ...artifacts.map((entry) => event({ type: 'research_artifact_recorded', artifact: entry })), - ...DEEP_RESEARCH_DEFAULT_CHECKLIST.map((item) => - event({ - type: 'research_checklist_updated', - item: { - ...item, - status: 'completed' as const, - evidenceArtifactIds: [evidenceArtifact.artifactId], - updatedAt: timestamp + 1, - }, - }), - ), - event({ - type: 'research_step_recorded', - step: { - stepId: 'step-1', - kind: 'local_exploration', - status: 'completed', - objective: 'Inspect the boundary', - summary: 'Boundary inspected', - roots: ['.'], - keywords: [], - ignoredPaths: [], - stoppingCondition: 'Enough evidence', - expectedEvidence: 'Source references', - evidenceArtifactIds: [evidenceArtifact.artifactId], - inspectedRefs: Array.from({ length: DEEP_RESEARCH_RECENT_REFS_MAX }, (_, index) => ({ - kind: 'file', - locator: escaped.repeat(1_365), - label: `${index}-${escaped.repeat(330)}`, - sourceArtifactId: sourceArtifact.artifactId, - })), - workerRunIds: Array.from( - { length: DEEP_RESEARCH_RECENT_REFS_MAX }, - (_, index) => `worker.run:${index}`, - ), - createdAt: timestamp + 1, - }, - }), - event({ - type: 'research_checkpoint_recorded', - checkpoint: { - checkpointId: 'checkpoint-1', - round: 1, - stage: 'report_writing', - status: 'active', - summary: 'Report sections completed', - openQuestions: [], - nextSteps: ['Complete the research run'], - taskIds: [], - artifactIds: [reportArtifact.artifactId, handoffArtifact.artifactId], - createdAt: timestamp + 1, - }, - }), - event({ - type: 'research_completed', - reportArtifactId: reportArtifact.artifactId, - handoff: { - artifactId: handoffArtifact.artifactId, - implementationTasks: Array.from({ length: 50 }, () => escaped.repeat(333)), - recommendedIssues: ['issue-1'], - recommendedPullRequests: [], - verificationCommands: ['npm test'], - }, - }), - ]; -} diff --git a/packages/runtime-host/src/__tests__/deep-research-two-client-uds.test.ts b/packages/runtime-host/src/__tests__/deep-research-two-client-uds.test.ts deleted file mode 100644 index a3f1f1f9dd..0000000000 --- a/packages/runtime-host/src/__tests__/deep-research-two-client-uds.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { defineInteractiveRuntimeHostComposition } from '../server/host-composition.js'; -import assert from 'node:assert/strict'; -import { mkdtemp, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { test } from 'node:test'; -import { openInteractiveDeepResearchStoreForWrite } from '@maka/storage/deep-research-authority'; -import { openInteractiveExecutionStoresForWrite } from '@maka/storage/execution-stores'; -import { resolveStorageRoot, tryAcquireInteractiveRootOwner } from '@maka/storage/root-authority'; -import { - connectRuntimeHost, - RuntimeHostOperationError, - type RuntimeHostConnection, -} from '../client/index.js'; -import { RUNTIME_HOST_PROTOCOL_VERSION } from '../protocol/index.js'; -import { createExecutionRuntimeHostComposition } from '../server/execution-composition.js'; -import { RuntimeHostKernel } from '../server/host-kernel.js'; - -const PROTOCOL = { - min: RUNTIME_HOST_PROTOCOL_VERSION, - max: RUNTIME_HOST_PROTOCOL_VERSION, -} as const; - -test('two Clients and a restarted production Host share one Deep Research projection', async () => { - const base = await mkdtemp(join(tmpdir(), 'maka-host-deep-research-uds-')); - const root = join(base, 'interactive'); - const capability = await resolveStorageRoot({ path: root, kind: 'interactive' }); - let owner = await tryAcquireInteractiveRootOwner(capability); - assert.ok(owner); - if (!owner) return; - let host: Awaited> | undefined; - let desktop: RuntimeHostConnection | undefined; - let tui: RuntimeHostConnection | undefined; - try { - const setupStores = await openInteractiveExecutionStoresForWrite(owner.lease); - const deepResearch = await openInteractiveDeepResearchStoreForWrite(owner.lease); - const session = await setupStores.sessionStore.create({ - cwd: root, - llmConnectionId: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc', - llmConnectionSlug: 'fake', - model: 'fake-model', - permissionMode: 'explore', - labels: ['mode:deep_research'], - }); - await deepResearch.start( - session.id, - 'Establish one durable Host-owned research workspace', - 'standard', - { turnId: 'turn-1', toolCallId: 'research-start' }, - ); - await deepResearch.updateChecklist( - session.id, - { - itemId: 'project_entrypoints', - status: 'in_progress', - evidenceArtifactIds: [], - }, - { turnId: 'turn-1', toolCallId: 'research-checklist' }, - ); - const sourceRevision = (await setupStores.sessionStore.readHeaderRecordSnapshot(session.id)) - .revision; - deepResearch.close(); - - host = await RuntimeHostKernel.start({ - owner, - idleGraceMs: 30_000, - composition: defineInteractiveRuntimeHostComposition(createExecutionRuntimeHostComposition), - }); - owner = undefined; - [desktop, tui] = await Promise.all([connect(root), connect(root)]); - - const [desktopProjection, tuiProjection] = await Promise.all([ - desktop.request('deep-research.query', { sessionId: session.id }), - tui.request('deep-research.query', { sessionId: session.id }), - ]); - assert.deepEqual(tuiProjection, desktopProjection); - assert.equal(desktopProjection.kind, 'snapshot'); - if (desktopProjection.kind !== 'snapshot') return; - assert.equal(desktopProjection.revision, 2); - assert.equal(desktopProjection.status, 'active'); - assert.equal( - desktopProjection.checklist.find((item) => item.itemId === 'project_entrypoints')?.status, - 'in_progress', - ); - await assert.rejects( - desktop.request('session.branch.create', { - sourceSessionId: session.id, - targetSessionId: 'deep-research-branch', - sourceTurnId: 'turn-1', - expectedSourceRevision: sourceRevision, - }), - (error: unknown) => - error instanceof RuntimeHostOperationError && error.code === 'operation_unavailable', - ); - - await Promise.all([desktop.close(), tui.close()]); - desktop = undefined; - tui = undefined; - await host.close(); - host = undefined; - - owner = await tryAcquireInteractiveRootOwner(capability); - assert.ok(owner); - if (!owner) return; - host = await RuntimeHostKernel.start({ - owner, - idleGraceMs: 30_000, - composition: defineInteractiveRuntimeHostComposition(createExecutionRuntimeHostComposition), - }); - owner = undefined; - tui = await connect(root); - - assert.deepEqual( - await tui.request('deep-research.query', { sessionId: session.id }), - desktopProjection, - ); - } finally { - await Promise.allSettled([desktop?.close(), tui?.close()]); - await host?.close().catch(() => undefined); - await owner?.close().catch(() => undefined); - await rm(base, { recursive: true, force: true }); - } -}); - -async function connect(rootPath: string): Promise { - const result = await connectRuntimeHost({ rootPath, protocol: PROTOCOL }); - assert.equal(result.kind, 'connected'); - if (result.kind !== 'connected') throw new Error('Unable to connect to Runtime Host'); - return result.connection; -} diff --git a/packages/runtime-host/src/__tests__/interactive-run-composer.test.ts b/packages/runtime-host/src/__tests__/interactive-run-composer.test.ts index e45f7ffb5e..7dfb6f412e 100644 --- a/packages/runtime-host/src/__tests__/interactive-run-composer.test.ts +++ b/packages/runtime-host/src/__tests__/interactive-run-composer.test.ts @@ -35,36 +35,15 @@ import type { HostMemoryCoordinator } from '../server/memory-coordinator.js'; import type { HostSkillCatalogCoordinator } from '../server/skill-catalog-coordinator.js'; import { WORKHUB_BROWSER_TOOL_NAMES } from './fixtures/workhub-capabilities.js'; -test('the interactive tool surface does not expose the retired ExploreAgent tool', () => { +test('the interactive tool surface does not expose retired tools', () => { const composer = createFixtureComposer(); assert.equal( - composer.tools.some(({ name }) => name === 'ExploreAgent'), + composer.tools.some(({ name }) => name === 'ExploreAgent' || name.startsWith('deep_research_')), false, ); }); -test('Deep Research keeps standard inspection tools and its durable workspace tools', () => { - const tool = (name: string): MakaTool => ({ - name, - description: name, - parameters: {}, - impl: async () => name, - }); - const composer = createFixtureComposer({ - hostTools: [tool('WebSearch')], - deepResearch: { tools: [tool('deep_research_status')] }, - }); - const names = new Set(composer.tools.map(({ name }) => name)); - - for (const name of ['Read', 'Glob', 'Grep', 'WebSearch', 'deep_research_status']) { - assert.equal(names.has(name), true, `expected Deep Research tool ${name}`); - } - for (const name of ['Write', 'Edit', 'Bash', 'ExploreAgent']) { - assert.equal(names.has(name), false, `unexpected Deep Research tool ${name}`); - } -}); - test('the composer resolves scoped Tool additions without rebuilding the backend', () => { let additions: readonly MakaTool[] = []; const dynamic = tool('dynamic_tool'); diff --git a/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts b/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts index 79fd2c809d..f188d121a8 100644 --- a/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts @@ -31,7 +31,6 @@ import { join } from 'node:path'; import test from 'node:test'; import { createDefaultRuntimePolicy } from '@maka/core/runtime-policy'; import { createGenesisExecutionBoundary } from '@maka/core/sandbox-boundary'; -import { DEEP_RESEARCH_SESSION_LABEL, DEEP_RESEARCH_SESSION_NAME } from '@maka/core/deep-research'; import { type ModelOverride } from '@maka/core/model-thinking'; import { WORKHUB_COORDINATION_SESSION_ID, @@ -264,7 +263,7 @@ test('read marker pages past a hidden tail to reach the newest visible message', test('metadata replacement preserves execution-semantic labels and ignores injected ones', async () => { const fixture = createFixture({ - labels: ['old-user-label', DEEP_RESEARCH_SESSION_LABEL], + labels: ['old-user-label', 'mode:deep_research'], manager: { runningTurnIds: () => ['turn-live'], }, @@ -275,7 +274,7 @@ test('metadata replacement preserves execution-semantic labels and ignores injec sessionId: fixture.sessionId, expectedRevision: fixture.revision(), patch: { - labels: ['new-user-label', DEEP_RESEARCH_SESSION_LABEL], + labels: ['new-user-label', 'mode:deep_research'], }, }, context, @@ -288,7 +287,7 @@ test('metadata replacement preserves execution-semantic labels and ignores injec if ('kind' in outcome.result.session) { assert.fail('Metadata replacement returned an unsupported Session projection'); } - assert.deepEqual(outcome.result.session.labels, ['new-user-label', DEEP_RESEARCH_SESSION_LABEL]); + assert.deepEqual(outcome.result.session.labels, ['new-user-label', 'mode:deep_research']); assert.equal(Object.hasOwn(outcome.result.session, 'liveRunState'), false); assert.equal(fixture.drainRequests(), 0); }); @@ -547,7 +546,7 @@ test('creation rejects reserved execution labels before claiming a Session ident { sessionId: fixture.sessionId, workspace: { kind: 'host_path', path: process.cwd() }, - labels: [DEEP_RESEARCH_SESSION_LABEL], + labels: ['mode:deep_research'], modelTarget: { kind: 'default' }, }, context, @@ -1307,41 +1306,6 @@ test('new tasks snapshot the current global Code Mode setting', async () => { assert.deepEqual(modes.slice(2), ['code_mode', 'direct']); }); -test('creation materializes Deep Research semantics inside the Host transaction', async () => { - let created: Parameters[0] | undefined; - const fixture = createFixture({ - stores: { - createStableSession: async (request) => { - created = request; - return { - kind: 'existing', - record: headerSnapshot(sessionHeader(request.sessionId, request.input.labels ?? []), 3), - }; - }, - }, - }); - - const outcome = await fixture.coordinator.handlers['session.create']( - { - sessionId: fixture.sessionId, - workspace: { kind: 'host_path', path: process.cwd() }, - mode: 'deep_research', - name: 'Caller override', - labels: ['customer-label'], - modelTarget: { kind: 'default' }, - permissionMode: 'ask', - }, - context, - ); - - assert.equal(outcome.ok, true); - assert.ok(created); - assert.equal(created.input.name, DEEP_RESEARCH_SESSION_NAME); - assert.deepEqual(created.input.labels, ['customer-label', DEEP_RESEARCH_SESSION_LABEL]); - assert.equal(created.input.permissionMode, 'explore'); - assert.equal(fixture.drainRequests(), 0); -}); - test('bot mode grants explore while keeping the Bot-supplied Session name', async () => { let created: Parameters[0] | undefined; const fixture = createFixture({ diff --git a/packages/runtime-host/src/__tests__/session-catalog-protocol.test.ts b/packages/runtime-host/src/__tests__/session-catalog-protocol.test.ts index 318f87f36f..3ca78b61ef 100644 --- a/packages/runtime-host/src/__tests__/session-catalog-protocol.test.ts +++ b/packages/runtime-host/src/__tests__/session-catalog-protocol.test.ts @@ -509,14 +509,14 @@ describe('Session catalog protocol', () => { input: { sessionId: 'session-mode', workspace: { kind: 'host_path', path: '/workspace' }, - mode: 'deep_research', + mode: 'bot', modelTarget: { kind: 'default' }, }, }); if ('kind' in decoded || decoded.operation !== 'session.create') { assert.fail('Expected Session create frame'); } - assert.equal(decoded.input.mode, 'deep_research'); + assert.equal(decoded.input.mode, 'bot'); assert.throws( () => decodeClientFrame({ @@ -525,7 +525,7 @@ describe('Session catalog protocol', () => { input: { sessionId: 'session-invalid-mode', workspace: { kind: 'host_path', path: '/workspace' }, - mode: 'unknown', + mode: 'deep_research', modelTarget: { kind: 'default' }, }, }), diff --git a/packages/runtime-host/src/__tests__/session-catalog-two-client-uds.test.ts b/packages/runtime-host/src/__tests__/session-catalog-two-client-uds.test.ts index 60e58ec304..d53f65f79d 100644 --- a/packages/runtime-host/src/__tests__/session-catalog-two-client-uds.test.ts +++ b/packages/runtime-host/src/__tests__/session-catalog-two-client-uds.test.ts @@ -28,7 +28,6 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { test } from 'node:test'; import { DatabaseSync } from 'node:sqlite'; -import { DEEP_RESEARCH_SESSION_LABEL, DEEP_RESEARCH_SESSION_NAME } from '@maka/core/deep-research'; import { openInteractiveArtifactStoreForWrite } from '@maka/storage/artifact-stores'; import { openInteractiveExecutionStoresForWrite } from '@maka/storage/execution-stores'; import { seedInvocation } from '@maka/runtime/test-only/invocation-fixture'; @@ -215,20 +214,6 @@ test('two Clients share stable Session creation, CAS configuration, and catalog }); if ('kind' in planSession) assert.fail('Plan Session must be wire-representable'); assert.equal(planSession.collaborationMode, 'plan'); - const researchSession = requireSessionProjection( - await desktop.request('session.create', { - sessionId: 'deep-research-session', - workspace: { kind: 'host_path', path: root }, - mode: 'deep_research', - name: 'Caller override', - labels: ['customer-label'], - modelTarget: { kind: 'default' }, - permissionMode: 'bypass', - }), - ); - assert.equal(researchSession.name, DEEP_RESEARCH_SESSION_NAME); - assert.deepEqual(researchSession.labels, ['customer-label', DEEP_RESEARCH_SESSION_LABEL]); - assert.equal(researchSession.permissionMode, 'explore'); const sandboxChoice = requireSessionProjection( await desktop.request('session.create', { diff --git a/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts b/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts index 090ee312e0..036da2347b 100644 --- a/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts +++ b/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts @@ -448,6 +448,8 @@ async function verifyConcurrentRevisionAuthority( assert.equal(branch.branchOfTurnId, 'turn-1'); assert.equal(branch.isFlagged, true); assert.equal(branch.connectionLocked, true); + assert.equal(branch.permissionMode, 'explore'); + assert.deepEqual(branch.labels, ['mode:deep_research']); const artifactPage = await desktop.request('artifact.query', { kind: 'list_start', @@ -455,8 +457,23 @@ async function verifyConcurrentRevisionAuthority( }); assert.equal(artifactPage.kind, 'page'); if (artifactPage.kind !== 'page') assert.fail('Branch Artifact query must return a page'); - assert.equal(artifactPage.artifacts.length, 3); + assert.equal(artifactPage.artifacts.length, 4); assert.notEqual(artifactPage.artifacts[0]?.id, 'source-artifact'); + const report = artifactPage.artifacts.find((item) => item.name === 'report.md'); + assert.ok(report); + assert.deepEqual( + await desktop.request('artifact.query', { + kind: 'read_text', + sessionId: branch.id, + artifactId: report.id, + }), + { + kind: 'text', + sessionId: branch.id, + artifactId: report.id, + preview: { ok: true, text: '# Existing research report' }, + }, + ); const todo = await tui.request('session.todo.query', { sessionId: branch.id }); assert.deepEqual(todo.items, []); @@ -858,7 +875,8 @@ async function seedSource( llmConnectionId: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc', llmConnectionSlug: 'fake', model: 'fake-model', - permissionMode: 'ask', + permissionMode: 'explore', + labels: ['mode:deep_research'], }); const coordination = await execution.sessionStore.createStableSession({ sessionId: WORKHUB_COORDINATION_SESSION_ID, @@ -1002,6 +1020,16 @@ async function seedSource( source: 'user_upload', now: 1, }); + await artifacts.create({ + id: 'legacy-research-report', + sessionId: source.id, + turnId: 'turn-1', + name: 'report.md', + kind: 'file', + content: '# Existing research report', + source: 'deep_research', + now: 2, + }); const projectionArtifact = await artifacts.create({ id: 'source-projection-artifact', sessionId: source.id, @@ -1725,7 +1753,7 @@ async function verifyDurableBranch( // must rewrite it to a fresh target artifact id, never leave the source id. assert.notEqual(ref.relativePath, 'source-artifact'); const branchArtifacts = await artifacts.listPage(branchSessionId, { offset: 0, limit: 10 }); - assert.equal(branchArtifacts.total, 3); + assert.equal(branchArtifacts.total, 4); assert.deepEqual(await artifacts.readTextInSession(branchSessionId, ref.relativePath), { ok: true, text: 'retained bytes', diff --git a/packages/runtime-host/src/protocol/deep-research.ts b/packages/runtime-host/src/protocol/deep-research.ts deleted file mode 100644 index e2bd7696ee..0000000000 --- a/packages/runtime-host/src/protocol/deep-research.ts +++ /dev/null @@ -1,385 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - DEEP_RESEARCH_CHECKLIST_ITEMS_MAX, - DEEP_RESEARCH_CLIENT_IMPLEMENTATION_PROMPT_MAX_BYTES, - DEEP_RESEARCH_CLIENT_OBJECTIVE_MAX_BYTES, - DEEP_RESEARCH_CLIENT_RECENT_ITEMS_MAX, - DEEP_RESEARCH_CLIENT_TEXT_MAX_BYTES, - DEEP_RESEARCH_INSPECTED_REF_KINDS, - DEEP_RESEARCH_REPORT_SECTION_KEYS, - DEEP_RESEARCH_REPORT_SECTION_STATUSES, - DEEP_RESEARCH_RUN_STATUSES, - DEEP_RESEARCH_SCOPE_LEVELS, - DEEP_RESEARCH_STAGES, - DEEP_RESEARCH_CHECKLIST_STATUSES, - type DeepResearchChecklistStatus, - type DeepResearchClientProgress, - type DeepResearchInspectedRefKind, - type DeepResearchReportSectionKey, - type DeepResearchReportSectionStatus, - type DeepResearchRunStatus, - type DeepResearchScopeLevel, - type DeepResearchStage, -} from '@maka/core/deep-research-run'; -import { - requireCount, - requireEncodedByteLimit, - requireEntityId, - requireExactRecord, - requireShapedRecord, - requireUtf8String, -} from './codec.js'; -import { invalidProtocolFrame } from './errors.js'; -import { defineOperation } from './operation-spec.js'; - -export const DEEP_RESEARCH_RESULT_MAX_BYTES = 48 * 1024; -export const DEEP_RESEARCH_RECENT_REFS_MAX = DEEP_RESEARCH_CLIENT_RECENT_ITEMS_MAX; -export { DEEP_RESEARCH_CLIENT_OBJECTIVE_MAX_BYTES, DEEP_RESEARCH_CLIENT_TEXT_MAX_BYTES }; -export const DEEP_RESEARCH_IMPLEMENTATION_PROMPT_MAX_BYTES = - DEEP_RESEARCH_CLIENT_IMPLEMENTATION_PROMPT_MAX_BYTES; -const DEEP_RESEARCH_STABLE_ID_MAX_BYTES = 128; -const DEEP_RESEARCH_STABLE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/; - -const QUERY_ERRORS = [ - 'host_not_ready', - 'host_draining', - 'operation_unavailable', - 'not_found', - 'session_archived', - 'invalid_request', - 'internal_failure', -] as const; - -export interface DeepResearchQueryInput { - readonly sessionId: string; -} - -export interface DeepResearchChecklistProjection { - readonly itemId: string; - readonly title: string; - readonly status: DeepResearchChecklistStatus; - readonly blockedReason: string | null; -} - -export interface DeepResearchReportSectionProjection { - readonly key: DeepResearchReportSectionKey; - readonly status: DeepResearchReportSectionStatus; -} - -export interface DeepResearchInspectedRefProjection { - readonly kind: DeepResearchInspectedRefKind; - readonly locator: string; - readonly label: string | null; -} - -export type DeepResearchQueryResult = - | { - readonly kind: 'not_started'; - readonly sessionId: string; - readonly revision: 0; - } - | { - readonly kind: 'snapshot'; - readonly sessionId: string; - readonly revision: number; - readonly objective: string; - readonly scopeLevel: DeepResearchScopeLevel; - readonly status: DeepResearchRunStatus; - readonly stage: DeepResearchStage; - readonly round: number; - readonly createdAt: number; - readonly updatedAt: number; - readonly artifactsCount: number; - readonly stepsCount: number; - readonly checklist: readonly DeepResearchChecklistProjection[]; - readonly reportSections: readonly DeepResearchReportSectionProjection[]; - readonly recentInspectedRefs: readonly DeepResearchInspectedRefProjection[]; - readonly workerRunIds: readonly string[]; - readonly blockers: readonly string[]; - readonly reportArtifactId: string | null; - readonly implementationPrompt: string | null; - }; - -export function encodeDeepResearchSnapshot( - progress: DeepResearchClientProgress, - revision: number, -): Extract { - return decodeDeepResearchQueryResult({ - kind: 'snapshot', - revision, - ...progress, - checklist: progress.checklist.map((item) => ({ - ...item, - blockedReason: item.blockedReason ?? null, - })), - recentInspectedRefs: progress.recentInspectedRefs.map((ref) => ({ - ...ref, - label: ref.label ?? null, - })), - reportArtifactId: progress.reportArtifactId ?? null, - implementationPrompt: progress.implementationPrompt ?? null, - }) as Extract; -} - -export const DEEP_RESEARCH_OPERATION_SPECS = { - 'deep-research.query': defineOperation< - DeepResearchQueryInput, - DeepResearchQueryResult, - (typeof QUERY_ERRORS)[number] - >({ - mode: 'query', - availability: 'ready', - errors: QUERY_ERRORS, - decodeInput: decodeDeepResearchQueryInput, - decodeOutput: decodeDeepResearchQueryResult, - assertOutputForInput(input, output) { - if (output.sessionId !== input.sessionId) { - throw invalidProtocolFrame('Deep Research result belongs to a different Session'); - } - }, - }), -} as const; - -export function decodeDeepResearchQueryInput(value: unknown): DeepResearchQueryInput { - const input = requireExactRecord(value, 'Deep Research query input', ['sessionId']); - return { sessionId: requireEntityId(input.sessionId, 'sessionId') }; -} - -export function decodeDeepResearchQueryResult(value: unknown): DeepResearchQueryResult { - requireEncodedByteLimit(value, 'Deep Research query result', DEEP_RESEARCH_RESULT_MAX_BYTES); - const record = requireShapedRecord( - value, - 'Deep Research query result', - ['kind', 'sessionId', 'revision'], - [ - 'objective', - 'scopeLevel', - 'status', - 'stage', - 'round', - 'createdAt', - 'updatedAt', - 'artifactsCount', - 'stepsCount', - 'checklist', - 'reportSections', - 'recentInspectedRefs', - 'workerRunIds', - 'blockers', - 'reportArtifactId', - 'implementationPrompt', - ], - ); - const sessionId = requireEntityId(record.sessionId, 'sessionId'); - const revision = requireCount(record.revision, 'Deep Research revision'); - if (record.kind === 'not_started') { - requireExactRecord(record, 'Deep Research not-started result', [ - 'kind', - 'sessionId', - 'revision', - ]); - if (revision !== 0) throw invalidProtocolFrame('Invalid Deep Research not-started revision'); - return { kind: 'not_started', sessionId, revision: 0 }; - } - if (record.kind !== 'snapshot') { - throw invalidProtocolFrame('Invalid Deep Research query result kind'); - } - requireExactRecord(record, 'Deep Research snapshot result', [ - 'kind', - 'sessionId', - 'revision', - 'objective', - 'scopeLevel', - 'status', - 'stage', - 'round', - 'createdAt', - 'updatedAt', - 'artifactsCount', - 'stepsCount', - 'checklist', - 'reportSections', - 'recentInspectedRefs', - 'workerRunIds', - 'blockers', - 'reportArtifactId', - 'implementationPrompt', - ]); - if (revision === 0) throw invalidProtocolFrame('Invalid Deep Research snapshot revision'); - return { - kind: 'snapshot', - sessionId, - revision, - objective: requireUtf8String( - record.objective, - 'Deep Research objective', - DEEP_RESEARCH_CLIENT_OBJECTIVE_MAX_BYTES, - ), - scopeLevel: requireEnum( - record.scopeLevel, - DEEP_RESEARCH_SCOPE_LEVELS, - 'Deep Research scope level', - ), - status: requireEnum(record.status, DEEP_RESEARCH_RUN_STATUSES, 'Deep Research status'), - stage: requireEnum(record.stage, DEEP_RESEARCH_STAGES, 'Deep Research stage'), - round: requireCount(record.round, 'Deep Research round'), - createdAt: requireCount(record.createdAt, 'Deep Research createdAt'), - updatedAt: requireCount(record.updatedAt, 'Deep Research updatedAt'), - artifactsCount: requireCount(record.artifactsCount, 'Deep Research artifact count'), - stepsCount: requireCount(record.stepsCount, 'Deep Research step count'), - checklist: decodeChecklist(record.checklist), - reportSections: decodeReportSections(record.reportSections), - recentInspectedRefs: decodeInspectedRefs(record.recentInspectedRefs), - workerRunIds: decodeIds(record.workerRunIds, 'Deep Research worker run ids'), - blockers: decodeTexts(record.blockers, 'Deep Research blockers'), - reportArtifactId: nullableId(record.reportArtifactId, 'Deep Research report artifact id'), - implementationPrompt: nullableText( - record.implementationPrompt, - 'Deep Research implementation prompt', - DEEP_RESEARCH_IMPLEMENTATION_PROMPT_MAX_BYTES, - ), - }; -} - -function decodeTexts(value: unknown, name: string): string[] { - if (!Array.isArray(value) || value.length > DEEP_RESEARCH_RECENT_REFS_MAX) { - throw invalidProtocolFrame(`Invalid ${name}`); - } - return value.map((item) => requireUtf8String(item, name, DEEP_RESEARCH_CLIENT_TEXT_MAX_BYTES)); -} - -function decodeChecklist(value: unknown): DeepResearchChecklistProjection[] { - if (!Array.isArray(value) || value.length > DEEP_RESEARCH_CHECKLIST_ITEMS_MAX) { - throw invalidProtocolFrame('Invalid Deep Research checklist'); - } - return value.map((candidate) => { - const item = requireExactRecord(candidate, 'Deep Research checklist item', [ - 'itemId', - 'title', - 'status', - 'blockedReason', - ]); - return { - itemId: requireEntityId(item.itemId, 'Deep Research checklist item id'), - title: requireUtf8String( - item.title, - 'Deep Research checklist title', - DEEP_RESEARCH_CLIENT_TEXT_MAX_BYTES, - ), - status: requireEnum( - item.status, - DEEP_RESEARCH_CHECKLIST_STATUSES, - 'Deep Research checklist status', - ), - blockedReason: nullableText( - item.blockedReason, - 'Deep Research checklist blocker', - DEEP_RESEARCH_CLIENT_TEXT_MAX_BYTES, - ), - }; - }); -} - -function decodeReportSections(value: unknown): DeepResearchReportSectionProjection[] { - if (!Array.isArray(value) || value.length > DEEP_RESEARCH_REPORT_SECTION_KEYS.length) { - throw invalidProtocolFrame('Invalid Deep Research report sections'); - } - return value.map((candidate) => { - const section = requireExactRecord(candidate, 'Deep Research report section', [ - 'key', - 'status', - ]); - return { - key: requireEnum( - section.key, - DEEP_RESEARCH_REPORT_SECTION_KEYS, - 'Deep Research report section key', - ), - status: requireEnum( - section.status, - DEEP_RESEARCH_REPORT_SECTION_STATUSES, - 'Deep Research report section status', - ), - }; - }); -} - -function decodeInspectedRefs(value: unknown): DeepResearchInspectedRefProjection[] { - if (!Array.isArray(value) || value.length > DEEP_RESEARCH_RECENT_REFS_MAX) { - throw invalidProtocolFrame('Invalid Deep Research inspected refs'); - } - return value.map((candidate) => { - const ref = requireExactRecord(candidate, 'Deep Research inspected ref', [ - 'kind', - 'locator', - 'label', - ]); - return { - kind: requireEnum( - ref.kind, - DEEP_RESEARCH_INSPECTED_REF_KINDS, - 'Deep Research inspected ref kind', - ), - locator: requireUtf8String( - ref.locator, - 'Deep Research inspected ref locator', - DEEP_RESEARCH_CLIENT_TEXT_MAX_BYTES, - ), - label: nullableText( - ref.label, - 'Deep Research inspected ref label', - DEEP_RESEARCH_CLIENT_TEXT_MAX_BYTES, - ), - }; - }); -} - -function decodeIds(value: unknown, label: string): string[] { - if (!Array.isArray(value) || value.length > DEEP_RESEARCH_RECENT_REFS_MAX) { - throw invalidProtocolFrame(`Invalid ${label}`); - } - return value.map((candidate) => requireStableResearchId(candidate, label)); -} - -function nullableId(value: unknown, label: string): string | null { - return value === null ? null : requireEntityId(value, label); -} - -function nullableText(value: unknown, label: string, maxBytes: number): string | null { - return value === null ? null : requireUtf8String(value, label, maxBytes); -} - -function requireStableResearchId(value: unknown, label: string): string { - const id = requireUtf8String(value, label, DEEP_RESEARCH_STABLE_ID_MAX_BYTES); - if (!DEEP_RESEARCH_STABLE_ID.test(id)) throw invalidProtocolFrame(`Invalid ${label}`); - return id; -} - -function requireEnum( - value: unknown, - values: T, - label: string, -): T[number] { - if (typeof value !== 'string' || !(values as readonly string[]).includes(value)) { - throw invalidProtocolFrame(`Invalid ${label}`); - } - return value as T[number]; -} diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts index eac5206acb..eeeb2bf852 100644 --- a/packages/runtime-host/src/protocol/index.ts +++ b/packages/runtime-host/src/protocol/index.ts @@ -101,7 +101,7 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const; export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const; // Increment when the same protocol version no longer guarantees safe Client-Host // interoperability. Mismatches are rejected before domain commands are admitted. -export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 169 as const; +export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 170 as const; // 169: The message execution query reports an identity the Host can prove was // never admitted as a positive `not_admitted` resolution instead of omitting // it, so silence stops meaning both "not admitted" and "cannot say yet". diff --git a/packages/runtime-host/src/protocol/operations.ts b/packages/runtime-host/src/protocol/operations.ts index 1861d33abb..f9fc87ff6e 100644 --- a/packages/runtime-host/src/protocol/operations.ts +++ b/packages/runtime-host/src/protocol/operations.ts @@ -23,7 +23,6 @@ import { AGENT_GRAPH_OPERATION_SPECS } from './agent-graph.js'; import { requireExactRecord, requireId, requireRecord, requireString } from './codec.js'; import { CONNECTION_EFFECT_OPERATION_SPECS } from './connection-effects.js'; import { CONFIGURATION_OPERATION_SPECS } from './configuration.js'; -import { DEEP_RESEARCH_OPERATION_SPECS } from './deep-research.js'; import { DAILY_REVIEW_OPERATION_SPECS } from './daily-review.js'; import { CONTEXT_OPERATION_SPECS } from './context.js'; import { EXECUTION_INSPECT_OPERATION_SPECS } from './execution-inspect.js'; @@ -157,7 +156,6 @@ export type { export * from './connection-effects.js'; export * from './access-authority.js'; export * from './configuration.js'; -export * from './deep-research.js'; export * from './daily-review.js'; export * from './context.js'; export * from './agent-graph.js'; @@ -201,7 +199,6 @@ export const HOST_OPERATION_SPECS = composeOperationSpecMaps( TURN_OPERATION_SPECS, CONTEXT_OPERATION_SPECS, CONNECTION_EFFECT_OPERATION_SPECS, - DEEP_RESEARCH_OPERATION_SPECS, DAILY_REVIEW_OPERATION_SPECS, EXECUTION_INSPECT_OPERATION_SPECS, EXTERNAL_SESSION_OPERATION_SPECS, @@ -279,7 +276,6 @@ export const REMOTE_OWNER_OPERATION_GRANTS = Object.freeze([ 'credential.vault.set', 'daily-review.mutate', 'daily-review.query', - 'deep-research.query', 'execution.inspect.query', 'external-session.catalog.query', 'external-session.import', diff --git a/packages/runtime-host/src/protocol/session-continuity.ts b/packages/runtime-host/src/protocol/session-continuity.ts index 95bba3299f..f376c04519 100644 --- a/packages/runtime-host/src/protocol/session-continuity.ts +++ b/packages/runtime-host/src/protocol/session-continuity.ts @@ -245,13 +245,7 @@ export interface SessionTranscriptAdvancedFrame extends SubscriptionEnvelope { throughSequence: number; } -export const SESSION_DOMAINS = [ - 'todo', - 'plan', - 'deep_research', - 'usage', - 'runtime_resource', -] as const; +export const SESSION_DOMAINS = ['todo', 'plan', 'usage', 'runtime_resource'] as const; export type SessionDomain = (typeof SESSION_DOMAINS)[number]; export const SESSION_RUNTIME_RESOURCE_CHANGES_MAX = 64; diff --git a/packages/runtime-host/src/server/deep-research-coordinator.ts b/packages/runtime-host/src/server/deep-research-coordinator.ts deleted file mode 100644 index ade7750dbb..0000000000 --- a/packages/runtime-host/src/server/deep-research-coordinator.ts +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { projectDeepResearchEvents, type DeepResearchRun } from '@maka/core/deep-research-run'; -import { projectDeepResearchClientProgress } from '@maka/core/deep-research-client-progress'; -import { isDeepResearchSession } from '@maka/core/deep-research'; -import { buildDeepResearchTools } from '@maka/runtime/deep-research-tools'; -import { type MakaTool } from '@maka/runtime/tool-runtime'; -import { - authenticateInteractiveArtifactStoreWriter, - type InteractiveArtifactStoreWriter, -} from '@maka/storage/artifact-stores'; -import { - authenticateInteractiveDeepResearchStoreWriter, - type InteractiveDeepResearchStoreWriter, -} from '@maka/storage/deep-research-authority'; -import { - isSessionNotFoundError, - type ExecutionSessionWriter, -} from '@maka/storage/execution-stores'; -import { - encodeDeepResearchSnapshot, - type DeepResearchQueryInput, - type DeepResearchQueryResult, - type OperationOutcome, -} from '../protocol/index.js'; -import type { DeepResearchOperationHandlerMap } from './operation-dispatcher.js'; -import { SessionAdmissionGate } from './session-admission-gate.js'; - -export interface HostDeepResearchCoordinatorInput { - readonly store: InteractiveDeepResearchStoreWriter; - readonly artifacts: InteractiveArtifactStoreWriter; - readonly sessions: Pick; - readonly sessionAdmission: SessionAdmissionGate; - readonly onProjectionChanged: (sessionId: string) => void; -} - -/** Host-owned Deep Research ledger, model-tool, and Client projection boundary. */ -export class HostDeepResearchCoordinator { - readonly handlers: DeepResearchOperationHandlerMap = { - 'deep-research.query': (input) => - this.#sessionAdmission.run(input.sessionId, () => this.#query(input)), - }; - - readonly #store: InteractiveDeepResearchStoreWriter; - readonly #artifacts: InteractiveArtifactStoreWriter; - readonly #sessions: HostDeepResearchCoordinatorInput['sessions']; - readonly #sessionAdmission: SessionAdmissionGate; - readonly #unsubscribe: () => void; - - constructor(input: HostDeepResearchCoordinatorInput) { - this.#store = authenticateInteractiveDeepResearchStoreWriter(input.store); - this.#artifacts = authenticateInteractiveArtifactStoreWriter(input.artifacts); - this.#sessions = input.sessions; - this.#sessionAdmission = input.sessionAdmission; - this.#unsubscribe = this.#store.subscribe(({ sessionId }) => { - input.onProjectionChanged(sessionId); - }); - } - - toolsForSession(sessionId: string): readonly MakaTool[] { - return buildDeepResearchTools({ - store: this.#store, - artifactStore: { - create: async (input) => { - if (input.sessionId !== sessionId) { - throw new Error('Deep Research tool attempted to write another Session'); - } - return this.#artifacts.create(input); - }, - get: async (artifactId) => - (await this.#artifacts.getInSession(sessionId, artifactId)).record ?? null, - readText: (artifactId, options) => - this.#artifacts.readTextInSession(sessionId, artifactId, options), - delete: (artifactId) => - this.#artifacts.deleteOwnedArtifactInSession(sessionId, artifactId, 'deep_research'), - }, - }); - } - - close(): void { - this.#unsubscribe(); - } - - async #query(input: DeepResearchQueryInput): Promise> { - const unavailable = await this.#assertSessionAvailable(input.sessionId); - if (unavailable) return failure(unavailable.code, unavailable.message); - try { - const events = await this.#store.readEvents(input.sessionId); - if (events.length === 0) { - return success({ kind: 'not_started', sessionId: input.sessionId, revision: 0 }); - } - const projection = projectDeepResearchEvents(events); - if (projection.diagnostics.length > 0 || !projection.run) { - return failure('internal_failure', 'Deep Research projection is unavailable'); - } - return success(projectDeepResearchRun(projection.run, events.length)); - } catch (error) { - if (isSessionNotFoundError(error)) return failure('not_found', 'Session does not exist'); - return failure('internal_failure', 'Deep Research projection is unavailable'); - } - } - - async #assertSessionAvailable(sessionId: string): Promise< - | { - code: 'not_found' | 'session_archived' | 'invalid_request' | 'internal_failure'; - message: string; - } - | undefined - > { - try { - const header = await this.#sessions.readHeaderSnapshot(sessionId); - if (header.isArchived) { - return { code: 'session_archived', message: 'Session is archived' }; - } - if (!isDeepResearchSession(header.labels)) { - return { - code: 'invalid_request', - message: 'Session is not a Deep Research Session', - }; - } - return undefined; - } catch (error) { - if (isSessionNotFoundError(error)) { - return { code: 'not_found', message: 'Session does not exist' }; - } - return { code: 'internal_failure', message: 'Session authority is unavailable' }; - } - } -} - -export function projectDeepResearchRun( - run: DeepResearchRun, - revision: number, -): DeepResearchQueryResult { - return encodeDeepResearchSnapshot(projectDeepResearchClientProgress(run), revision); -} - -function success(result: DeepResearchQueryResult): OperationOutcome<'deep-research.query'> { - return { ok: true, result }; -} - -function failure( - code: Extract, { ok: false }>['error']['code'], - message: string, -): OperationOutcome<'deep-research.query'> { - return { ok: false, error: { code, message } }; -} diff --git a/packages/runtime-host/src/server/execution-composition.ts b/packages/runtime-host/src/server/execution-composition.ts index b45308a83c..7636185e95 100644 --- a/packages/runtime-host/src/server/execution-composition.ts +++ b/packages/runtime-host/src/server/execution-composition.ts @@ -36,7 +36,6 @@ import { type RuntimeInvocationRecord, } from '@maka/core/runtime-invocation'; import { - isDeepResearchSession, type SessionHeader, WORKHUB_COORDINATION_SESSION_ID, WORKHUB_COORDINATION_REPLACEMENT_SCHEMA_VERSION, @@ -150,7 +149,6 @@ import { HostChangeFeed } from './host-change-feed.js'; import { HostConfigurationCoordinator } from './configuration-coordinator.js'; import { HostContextCoordinator } from './context-coordinator.js'; import { HostClientCapabilityCoordinator } from './client-capability-coordinator.js'; -import { HostDeepResearchCoordinator } from './deep-research-coordinator.js'; import { HostDailyReviewCoordinator } from './daily-review-coordinator.js'; import { prepareHostAiSdkBackend } from './execution-model-composition.js'; import { @@ -402,7 +400,6 @@ export async function createExecutionRuntimeHostComposition( const oauthCredentials = new HostOAuthExecutionAuthority(runtimePolicyStores); const openedScheduledTaskStore = storage.scheduledTasks; const openedPlanStore = storage.plan; - const openedDeepResearchStore = storage.deepResearch; const openedDailyReviewStore = storage.dailyReview; const openedGoalStore = storage.goal; const memoryStore = storage.memoryBundle; @@ -872,7 +869,6 @@ export async function createExecutionRuntimeHostComposition( let scheduledTasks: HostScheduledTaskCoordinator | undefined; let scheduledTaskTool: MakaTool | undefined; let goal: HostGoalCoordinator | undefined; - let deepResearch: HostDeepResearchCoordinator | undefined; let dailyReview: HostDailyReviewCoordinator | undefined; const rootPort: HostMessageRootPort = { readLatestRootTurnLineage: (identity) => @@ -952,14 +948,6 @@ export async function createExecutionRuntimeHostComposition( unsubscribeUsageChanges = openedUsageStores.subscribeSessionUsageChanges((sessionId) => continuityCoordinator.enqueueSessionDomainChanged(sessionId, 'usage'), ); - deepResearch = new HostDeepResearchCoordinator({ - store: openedDeepResearchStore, - artifacts: openedArtifactStore, - sessions: stores.sessionStore, - sessionAdmission, - onProjectionChanged: (sessionId) => - continuityCoordinator.enqueueSessionDomainChanged(sessionId, 'deep_research'), - }); dailyReview = new HostDailyReviewCoordinator({ store: openedDailyReviewStore, usage: openedUsageStores, @@ -1074,9 +1062,6 @@ export async function createExecutionRuntimeHostComposition( resolveHostTavilyWebSearchReadiness(runtimePolicyStores.operations), ...(scheduledTaskTool ? { scheduledTaskTool } : {}), planStore, - deepResearchTools: requireDeepResearch(deepResearch).toolsForSession( - backendContext.sessionId, - ), goalTools: requireGoal(goal).tools, builtinTools, hostTools, @@ -1265,13 +1250,6 @@ export async function createExecutionRuntimeHostComposition( mode: header.collaborationMode ?? 'agent', permissionMode: header.permissionMode, }, - ...(isDeepResearchSession(header.labels) - ? { - deepResearch: { - tools: requireDeepResearch(deepResearch).toolsForSession(sessionId), - }, - } - : {}), }).tools.map((tool) => tool.name); } finally { capabilitySnapshot?.release(); @@ -2547,7 +2525,6 @@ export async function createExecutionRuntimeHostComposition( // writer: publishing a per-Session `plan` invalidation for state that is // being removed would only wake subscribers to read nothing. await openedPlanStore.purgeSessionState(sessionId); - await openedDeepResearchStore.purgeSessionState(sessionId); }, purgeAgentGraphState: async (sessionId) => { for (const graphId of await requireGraphCoordinator(graphCoordinator).listGraphIds( @@ -2758,11 +2735,6 @@ export async function createExecutionRuntimeHostComposition( drain: [() => clientCapabilities.beginDrain()], close: [() => clientCapabilities.close()], }), - createRuntimeHostDomainModule({ - id: 'deep-research', - handlers: [requireDeepResearch(deepResearch).handlers], - close: [() => deepResearch?.close()], - }), createRuntimeHostDomainModule({ id: 'daily-review', handlers: [requireDailyReview(dailyReview).handlers], @@ -3227,13 +3199,6 @@ function requireScheduledTasks( return coordinator; } -function requireDeepResearch( - coordinator: HostDeepResearchCoordinator | undefined, -): HostDeepResearchCoordinator { - if (!coordinator) throw new Error('Runtime Host Deep Research coordinator is not composed'); - return coordinator; -} - function requireDailyReview( coordinator: HostDailyReviewCoordinator | undefined, ): HostDailyReviewCoordinator { diff --git a/packages/runtime-host/src/server/interactive-run-composer.ts b/packages/runtime-host/src/server/interactive-run-composer.ts index 8de2f851ca..177720ba84 100644 --- a/packages/runtime-host/src/server/interactive-run-composer.ts +++ b/packages/runtime-host/src/server/interactive-run-composer.ts @@ -23,10 +23,6 @@ import { isSideConversationSession, } from '@maka/core/side-conversation'; import { type RunCompositionSourceRevision } from '@maka/core/run-composition'; -import { - buildDeepResearchSystemPromptFragment, - isDeepResearchSession, -} from '@maka/core/deep-research'; import { activePlanExecution, type PlanSessionState, type PlanStore } from '@maka/core/plan'; import type { PermissionMode } from '@maka/core/permission'; import { createHash } from 'node:crypto'; @@ -55,7 +51,6 @@ import { } from '@maka/runtime/skills'; import { buildSessionTodoTools, type SessionTodoToolStore } from '@maka/runtime/session-todo-tools'; import { buildWorkspaceInstructionsPromptFragment } from '@maka/runtime/system-prompt/workspace-instructions'; -import { isDeepResearchToolAllowed } from '@maka/runtime/deep-research-tools'; import { listRunnableBuiltinAgentDefinitions } from '@maka/runtime/agent-catalog'; import { renderPlanModePrompt, selectCollaborationTools } from '@maka/runtime/plan-mode'; import { routeWebFetchTools } from '@maka/runtime/web-fetch-tool'; @@ -129,9 +124,6 @@ export interface InteractiveRunComposerInput { readonly mode: 'agent' | 'plan'; readonly permissionMode?: PermissionMode; }; - readonly deepResearch?: { - readonly tools: readonly MakaTool[]; - }; readonly resolveProfileSystemPrompt?: ( context: HostModelPromptContext, basePrompt: string, @@ -165,7 +157,6 @@ export function createInteractiveRunComposer(input: InteractiveRunComposerInput) input.goalTools, input.parentAgentTools, input.plan, - input.deepResearch?.tools, ); const clientCapabilityTools = input.boundTools !== undefined || @@ -177,11 +168,10 @@ export function createInteractiveRunComposer(input: InteractiveRunComposerInput) const additionalTools = hasToolCeiling ? [] : (input.resolveAdditionalTools?.(stableHostTools) ?? []); - const unscopedCandidateTools = [...stableHostTools, ...additionalTools]; - const routedCandidateTools = input.deepResearch - ? unscopedCandidateTools.filter(isDeepResearchToolAllowed) - : unscopedCandidateTools; - const candidateTools = projectHostedExecutionTools(routedCandidateTools, input.toolProfile); + const candidateTools = projectHostedExecutionTools( + [...stableHostTools, ...additionalTools], + input.toolProfile, + ); const selectedTools = input.plan ? selectCollaborationTools({ mode: input.plan.mode, @@ -262,7 +252,6 @@ export function createInteractiveRunComposer(input: InteractiveRunComposerInput) input.plan?.mode === 'plan' ? renderPlanModePrompt({ fullAccess: input.plan.permissionMode === 'bypass' }) : undefined, - input.deepResearch ? buildDeepResearchSystemPromptFragment() : undefined, input.sideConversation ? buildSideConversationSystemPromptFragment() : undefined, ]); // Keep each turn's source revisions independent while sharing identical @@ -344,7 +333,6 @@ export interface InteractiveRunComposerFactoryInput readonly childTools?: readonly MakaTool[]; readonly worktreePatchWriteBackAvailable?: boolean; readonly planStore?: PlanStore; - readonly deepResearchTools?: readonly MakaTool[]; /** Internal dependency seam for deterministic Host shell-resolution tests. */ readonly resolveTurnShellPlan?: typeof resolveTurnShellPlan; } @@ -499,9 +487,6 @@ export function createInteractiveRunComposerFactory( }, } : {}), - ...(isDeepResearchSession(backendContext.header.labels) && !backendContext.tools - ? { deepResearch: { tools: requireDeepResearchTools(input.deepResearchTools) } } - : {}), skillBudget: contextWindow === null ? {} : { contextWindow }, shell, ...(input.resolveProfileSystemPrompt @@ -546,7 +531,6 @@ function buildDefaultHostTools( goalTools: readonly MakaTool[] = [], parentAgentTools: readonly MakaTool[] = [], plan?: InteractiveRunComposerInput['plan'], - deepResearchTools: readonly MakaTool[] = [], ): MakaTool[] { // Full access has no boundary to widen, so neither the Bash declaration nor // the widening tool is offered. An unknown mode is not Full access. @@ -583,7 +567,6 @@ function buildDefaultHostTools( ...goalTools.map((tool) => tool.name), ...parentAgentTools.map((tool) => tool.name), ...planTools.map((tool) => tool.name), - ...deepResearchTools.map((tool) => tool.name), ]; const skillHost = buildHostCapabilitiesFromBinding(toolNames); const shadowTracker = new SkillShadowSelectionTracker(); @@ -599,15 +582,9 @@ function buildDefaultHostTools( ...goalTools, ...parentAgentTools, ...planTools, - ...deepResearchTools, ]; } -function requireDeepResearchTools(tools: readonly MakaTool[] | undefined): readonly MakaTool[] { - if (!tools) throw new Error('Runtime Host Deep Research tools are not composed'); - return tools; -} - function filterToolGroups(groups: readonly ToolGroup[], names: ReadonlySet): ToolGroup[] { const seenIds = new Set(); return groups.flatMap((group) => { diff --git a/packages/runtime-host/src/server/operation-dispatcher.ts b/packages/runtime-host/src/server/operation-dispatcher.ts index fc22044f06..ff70cf0b22 100644 --- a/packages/runtime-host/src/server/operation-dispatcher.ts +++ b/packages/runtime-host/src/server/operation-dispatcher.ts @@ -44,7 +44,6 @@ import { CONFIGURATION_OPERATION_SPECS } from '../protocol/configuration.js'; import { CONNECTION_EFFECT_OPERATION_SPECS } from '../protocol/connection-effects.js'; import { CONTEXT_OPERATION_SPECS } from '../protocol/context.js'; import { DAILY_REVIEW_OPERATION_SPECS } from '../protocol/daily-review.js'; -import { DEEP_RESEARCH_OPERATION_SPECS } from '../protocol/deep-research.js'; import { EXECUTION_INSPECT_OPERATION_SPECS } from '../protocol/execution-inspect.js'; import { EXTERNAL_SESSION_OPERATION_SPECS } from '../protocol/external-session.js'; import { SESSION_BUNDLE_OPERATION_SPECS } from '../protocol/session-bundle.js'; @@ -156,7 +155,6 @@ export type ClientCapabilityOperationKey = keyof typeof CLIENT_CAPABILITY_OPERAT export type ScheduledTaskOperationKey = keyof typeof SCHEDULED_TASK_OPERATION_SPECS; export type PlanOperationKey = keyof typeof PLAN_OPERATION_SPECS; export type ProjectCatalogOperationKey = keyof typeof PROJECT_CATALOG_OPERATION_SPECS; -export type DeepResearchOperationKey = keyof typeof DEEP_RESEARCH_OPERATION_SPECS; export type DailyReviewOperationKey = keyof typeof DAILY_REVIEW_OPERATION_SPECS; export type WebSearchOperationKey = keyof typeof WEB_SEARCH_OPERATION_SPECS; export type NetworkProxyOperationKey = keyof typeof NETWORK_PROXY_OPERATION_SPECS; @@ -225,7 +223,6 @@ export type ProjectCatalogOperationHandlerMap = Pick< OperationHandlerMap, ProjectCatalogOperationKey >; -export type DeepResearchOperationHandlerMap = Pick; export type DailyReviewOperationHandlerMap = Pick; export type WebSearchOperationHandlerMap = Pick; export type RecallOperationKey = keyof typeof RECALL_OPERATION_SPECS; diff --git a/packages/runtime-host/src/server/session-revision-coordinator.ts b/packages/runtime-host/src/server/session-revision-coordinator.ts index 7e120478f0..595494963c 100644 --- a/packages/runtime-host/src/server/session-revision-coordinator.ts +++ b/packages/runtime-host/src/server/session-revision-coordinator.ts @@ -18,7 +18,6 @@ */ import { createHash, randomUUID } from 'node:crypto'; -import { isDeepResearchSession } from '@maka/core/deep-research'; import { SIDE_CONVERSATION_SESSION_LABEL } from '@maka/core/side-conversation'; import type { RuntimeEvent } from '@maka/core/runtime-event'; import type { CreateSessionInput } from '@maka/core/runtime-inputs'; @@ -345,12 +344,6 @@ export class HostSessionRevisionCoordinator { 'Linked child Sessions cannot be copied as ordinary conversations', ); } - if (isDeepResearchSession(sourceHeader.labels)) { - return copyFailure( - 'operation_unavailable', - 'Deep Research Sessions cannot be copied without an exact research ledger boundary', - ); - } const copyUnavailableReason = runtimeHostConversationCopyUnavailableReason(sourceHeader); if (copyUnavailableReason) return copyFailure('operation_unavailable', copyUnavailableReason); if (kind !== 'side_conversation' && this.options.isSessionActive(input.sourceSessionId)) { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index b1034072f2..d17fd58157 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -14,7 +14,6 @@ "./shell-tools": "./dist/shell-tools.js", "./shell-run-manager": "./dist/shell-run-manager.js", "./background-task-health-tool": "./dist/background-task-health-tool.js", - "./deep-research-tools": "./dist/deep-research-tools.js", "./durable-tool-result-projection": "./dist/durable-tool-result-projection.js", "./tool-artifacts": "./dist/tool-artifacts.js", "./model-factory": "./dist/model-factory.js", diff --git a/packages/runtime/src/__tests__/deep-research-tools.test.ts b/packages/runtime/src/__tests__/deep-research-tools.test.ts deleted file mode 100644 index 7be150d75d..0000000000 --- a/packages/runtime/src/__tests__/deep-research-tools.test.ts +++ /dev/null @@ -1,582 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import assert from 'node:assert/strict'; -import { mkdtemp, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { describe, it } from 'node:test'; -import { z } from 'zod'; -import type { ArtifactRecord } from '@maka/core/artifacts'; -import type { DeepResearchRun } from '@maka/core/deep-research-run'; -import { createSqliteDeepResearchStore } from '@maka/storage/deep-research-store'; -import { - DEEP_RESEARCH_CHECKPOINT_TOOL_NAME, - DEEP_RESEARCH_COMPLETE_TOOL_NAME, - DEEP_RESEARCH_READ_ARTIFACT_TOOL_NAME, - DEEP_RESEARCH_RECORD_STEP_TOOL_NAME, - DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME, - DEEP_RESEARCH_START_TOOL_NAME, - DEEP_RESEARCH_STATUS_TOOL_NAME, - DEEP_RESEARCH_UPDATE_CHECKLIST_TOOL_NAME, - buildDeepResearchTools, - isDeepResearchToolAllowed, - renderDeepResearchRunStatus, - type DeepResearchArtifactStore, -} from '../deep-research-tools.js'; -import type { MakaTool, MakaToolContext } from '../tool-runtime.js'; - -const SESSION_ID = 'session-1'; - -class FakeArtifactStore implements DeepResearchArtifactStore { - readonly records: ArtifactRecord[] = []; - readonly deleted: string[] = []; - readonly contents = new Map(); - - async create(input: Parameters[0]): Promise { - const record: ArtifactRecord = { - id: input.id, - sessionId: input.sessionId, - turnId: input.turnId, - createdAt: 100 + this.records.length, - name: input.name, - kind: input.kind, - relativePath: `${input.sessionId}/${input.id}-${input.name}`, - sizeBytes: input.content.length, - mimeType: input.mimeType, - source: input.source, - summary: input.summary, - deepResearchRole: input.deepResearchRole, - }; - this.records.push(record); - this.contents.set(record.id, input.content); - return record; - } - - async get(artifactId: string): Promise { - return this.records.find((record) => record.id === artifactId) ?? null; - } - - async readText( - artifactId: string, - ): Promise<{ ok: true; text: string } | { ok: false; reason: string }> { - const text = this.contents.get(artifactId); - return text === undefined ? { ok: false, reason: 'not_found' } : { ok: true, text }; - } - - async delete(artifactId: string): Promise { - this.deleted.push(artifactId); - const index = this.records.findIndex((item) => item.id === artifactId); - if (index >= 0) this.records.splice(index, 1); - this.contents.delete(artifactId); - } -} - -function context(toolCallId: string): MakaToolContext { - return { - sessionId: SESSION_ID, - runId: 'run-1', - turnId: 'turn-1', - cwd: '/tmp', - toolCallId, - abortSignal: new AbortController().signal, - emitOutput: () => {}, - }; -} - -function findTool(tools: MakaTool[], name: string): MakaTool { - const tool = tools.find((item) => item.name === name); - assert.ok(tool, `expected tool ${name}`); - return tool; -} - -async function execute( - tools: MakaTool[], - name: string, - input: Record, - callId: string, -): Promise { - const tool = findTool(tools, name); - const parsed = (tool.parameters as z.ZodType>).parse(input); - return String(await tool.impl(parsed, context(callId))); -} - -async function withTempRoot(fn: (root: string) => Promise): Promise { - const root = await mkdtemp(join(tmpdir(), 'maka-runtime-deep-research-')); - try { - await fn(root); - } finally { - await rm(root, { recursive: true, force: true }); - } -} - -describe('Deep Research runtime tools', () => { - it('admits only the explicit Deep Research tool surface', () => { - const standardResearchNames = ['AskUserQuestion', 'Read', 'Glob', 'Grep', 'WebSearch']; - const canonicalNames = [ - DEEP_RESEARCH_START_TOOL_NAME, - DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME, - DEEP_RESEARCH_READ_ARTIFACT_TOOL_NAME, - DEEP_RESEARCH_UPDATE_CHECKLIST_TOOL_NAME, - DEEP_RESEARCH_RECORD_STEP_TOOL_NAME, - DEEP_RESEARCH_CHECKPOINT_TOOL_NAME, - DEEP_RESEARCH_STATUS_TOOL_NAME, - DEEP_RESEARCH_COMPLETE_TOOL_NAME, - ]; - assert.ok(standardResearchNames.every((name) => isDeepResearchToolAllowed({ name }))); - assert.ok(canonicalNames.every((name) => isDeepResearchToolAllowed({ name }))); - assert.equal(isDeepResearchToolAllowed({ name: 'ExploreAgent' }), false); - assert.equal(isDeepResearchToolAllowed({ name: 'deep_research_unsafe_fixture' }), false); - }); - - it('runs the source-checkpoint-report lifecycle and makes artifact retries idempotent', async () => { - await withTempRoot(async (root) => { - const artifactStore = new FakeArtifactStore(); - const store = createSqliteDeepResearchStore(root); - const tools = buildDeepResearchTools({ - store, - artifactStore, - }); - - await execute( - tools, - DEEP_RESEARCH_START_TOOL_NAME, - { objective: 'Reproduce a filesystem-backed research loop.' }, - 'call-start', - ); - const sourceOutput = await execute( - tools, - DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME, - { - role: 'source', - name: 'paper.md', - content: '# Paper evidence', - summary: 'Archived paper evidence.', - locator: 'https://arxiv.org/abs/2602.01566', - }, - 'call-source', - ); - const sourceId = artifactStore.records[0]?.id; - assert.ok(sourceId); - assert.match(sourceOutput, new RegExp(sourceId)); - - const sourceRead = await execute( - tools, - DEEP_RESEARCH_READ_ARTIFACT_TOOL_NAME, - { artifact_id: sourceId, max_chars: 6 }, - 'call-read-source', - ); - assert.match(sourceRead, /role="source"/); - assert.match(sourceRead, /# Pape/); - assert.match(sourceRead, /Truncated: true/); - - artifactStore.contents.set(sourceId, '# Tampered evidence'); - await assert.rejects( - () => - execute( - tools, - DEEP_RESEARCH_READ_ARTIFACT_TOOL_NAME, - { artifact_id: sourceId }, - 'call-read-tampered', - ), - /content no longer matches/, - ); - artifactStore.contents.set(sourceId, '# Paper evidence'); - await assert.rejects( - () => - execute( - tools, - DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME, - { - role: 'source', - name: 'paper.md', - content: '# Different paper evidence', - summary: 'Archived paper evidence.', - locator: 'https://arxiv.org/abs/2602.01566', - }, - 'call-source', - ), - /retried with different content/, - ); - await assert.rejects( - () => - execute( - tools, - DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME, - { - role: 'source', - name: 'renamed-paper.md', - content: '# Paper evidence', - summary: 'Archived paper evidence.', - locator: 'https://arxiv.org/abs/2602.01566', - }, - 'call-source', - ), - /retried with different content or metadata/, - ); - await assert.rejects( - () => - execute( - tools, - DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME, - { - role: 'source', - name: 'paper.md', - content: '# Paper evidence', - summary: 'A different summary.', - locator: 'https://arxiv.org/abs/2602.01566', - }, - 'call-source', - ), - /retried with different content or metadata/, - ); - await assert.rejects( - () => - execute( - tools, - DEEP_RESEARCH_START_TOOL_NAME, - { objective: 'Reproduce a filesystem-backed research loop.' }, - 'call-source', - ), - /already used for research_artifact_recorded/, - ); - const retryOutput = await execute( - tools, - DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME, - { - role: 'source', - name: 'paper.md', - content: '# Paper evidence', - summary: 'Archived paper evidence.', - locator: 'https://arxiv.org/abs/2602.01566', - }, - 'call-source', - ); - assert.match(retryOutput, /already saved/); - assert.equal(artifactStore.records.length, 1); - - await execute( - tools, - DEEP_RESEARCH_RECORD_STEP_TOOL_NAME, - { - kind: 'web_research', - status: 'completed', - objective: 'Inspect the paper contract.', - summary: 'The durable filesystem workspace contract is supported.', - keywords: ['FS-Researcher durable workspace'], - stopping_condition: 'Stop after the primary paper is archived.', - expected_evidence: 'A source artifact containing the paper findings.', - evidence_artifact_ids: [sourceId], - inspected_refs: [ - { - kind: 'url', - locator: 'https://arxiv.org/abs/2602.01566', - source_artifact_id: sourceId, - }, - ], - worker_run_ids: ['run-paper-review'], - }, - 'call-step', - ); - - for (const itemId of [ - 'project_entrypoints', - 'core_flow', - 'boundaries', - 'verification_evidence', - ]) { - await execute( - tools, - DEEP_RESEARCH_UPDATE_CHECKLIST_TOOL_NAME, - { - item_id: itemId, - status: 'completed', - evidence_artifact_ids: [sourceId], - }, - `call-checklist-${itemId}`, - ); - } - - const checkpointInput = { - round: 1, - stage: 'knowledge_base', - status: 'active', - summary: 'The persistence contract is understood.', - next_steps: ['Write the final report.'], - artifact_ids: [sourceId], - }; - await execute(tools, DEEP_RESEARCH_CHECKPOINT_TOOL_NAME, checkpointInput, 'call-checkpoint'); - for (const [sectionKey, name] of [ - ['conclusion', 'conclusion.md'], - ['source_evidence', 'source-evidence.md'], - ['borrow_diverge_risk_gate', 'tradeoffs.md'], - ['implementation_recommendations', 'implementation.md'], - ['verification', 'verification.md'], - ] as const) { - await execute( - tools, - DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME, - { - role: 'report_section', - name, - content: `# ${sectionKey}\n\nSource-backed section.`, - summary: `${sectionKey} section.`, - source_artifact_ids: [sourceId], - report_section_key: sectionKey, - report_section_status: 'completed', - }, - `call-section-${sectionKey}`, - ); - } - await execute(tools, DEEP_RESEARCH_CHECKPOINT_TOOL_NAME, checkpointInput, 'call-checkpoint'); - await assert.rejects( - () => - execute( - tools, - DEEP_RESEARCH_CHECKPOINT_TOOL_NAME, - { ...checkpointInput, summary: 'Conflicting retry.' }, - 'call-checkpoint', - ), - /retried with different input/, - ); - await execute( - tools, - DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME, - { - role: 'report', - name: 'report.md', - content: '# Final report\n\nSource-backed conclusion.', - summary: 'Final report.', - source_artifact_ids: [sourceId], - }, - 'call-report', - ); - const reportId = artifactStore.records[6]?.id; - assert.ok(reportId); - await execute( - tools, - DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME, - { - role: 'handoff', - name: 'handoff.md', - content: '# Handoff\n\nImplement the durable workspace and verify it.', - summary: 'Structured implementation handoff.', - source_artifact_ids: [sourceId], - }, - 'call-handoff', - ); - const handoffId = artifactStore.records[7]?.id; - assert.ok(handoffId); - const completeInput = { - report_artifact_id: reportId, - handoff_artifact_id: handoffId, - implementation_tasks: ['Implement the durable research workspace.'], - recommended_issues: ['Track progress UI acceptance.'], - verification_commands: ['npm test'], - }; - const sourceRecord = artifactStore.records[0]!; - artifactStore.records.splice(0, 1); - await assert.rejects( - () => - execute(tools, DEEP_RESEARCH_COMPLETE_TOOL_NAME, completeInput, 'call-complete-deleted'), - /missing or deleted/, - ); - artifactStore.records.unshift(sourceRecord); - - const sectionRecord = artifactStore.records[1]!; - const sectionContent = artifactStore.contents.get(sectionRecord.id)!; - artifactStore.contents.set(sectionRecord.id, '# Tampered report section'); - await assert.rejects( - () => - execute(tools, DEEP_RESEARCH_COMPLETE_TOOL_NAME, completeInput, 'call-complete-tampered'), - /content does not match the ledger/, - ); - artifactStore.contents.set(sectionRecord.id, sectionContent); - - const reportRecord = artifactStore.records[6]!; - reportRecord.deepResearchRole = 'source'; - await assert.rejects( - () => execute(tools, DEEP_RESEARCH_COMPLETE_TOOL_NAME, completeInput, 'call-complete-role'), - /type or role does not match/, - ); - reportRecord.deepResearchRole = 'report'; - - const handoffRecord = artifactStore.records[7]!; - handoffRecord.sessionId = 'another-session'; - await assert.rejects( - () => - execute(tools, DEEP_RESEARCH_COMPLETE_TOOL_NAME, completeInput, 'call-complete-session'), - /belongs to another workspace/, - ); - handoffRecord.sessionId = SESSION_ID; - - const completion = await execute( - tools, - DEEP_RESEARCH_COMPLETE_TOOL_NAME, - completeInput, - 'call-complete', - ); - assert.match(completion, /status="completed"/); - const completionRetry = await execute( - tools, - DEEP_RESEARCH_COMPLETE_TOOL_NAME, - completeInput, - 'call-complete', - ); - assert.match(completionRetry, /status="completed"/); - - const artifactRetryAfterCompletion = await execute( - tools, - DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME, - { - role: 'source', - name: 'paper.md', - content: '# Paper evidence', - summary: 'Archived paper evidence.', - locator: 'https://arxiv.org/abs/2602.01566', - }, - 'call-source', - ); - assert.match(artifactRetryAfterCompletion, /already saved/); - - const status = await execute(tools, DEEP_RESEARCH_STATUS_TOOL_NAME, {}, 'call-status'); - assert.match(status, new RegExp(`Final report: ${reportId}`)); - assert.match(status, new RegExp(`Handoff artifact: ${handoffId}`)); - assert.equal((await store.readEvents(SESSION_ID)).length, 16); - }); - }); - - it('rejects untraceable derived artifacts at the schema boundary', async () => { - await withTempRoot(async (root) => { - const tools = buildDeepResearchTools({ - store: createSqliteDeepResearchStore(root), - artifactStore: new FakeArtifactStore(), - }); - const save = findTool(tools, DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME); - const result = (save.parameters as z.ZodType).safeParse({ - role: 'evidence_note', - name: 'note.md', - content: 'Unsupported claim.', - summary: 'No source.', - }); - assert.equal(result.success, false); - - const update = findTool(tools, DEEP_RESEARCH_UPDATE_CHECKLIST_TOOL_NAME); - assert.equal( - (update.parameters as z.ZodType).safeParse({ - item_id: 'core_flow', - status: 'completed', - }).success, - false, - ); - - const step = findTool(tools, DEEP_RESEARCH_RECORD_STEP_TOOL_NAME); - assert.equal( - (step.parameters as z.ZodType).safeParse({ - kind: 'local_exploration', - status: 'stopped', - objective: 'Inspect the implementation.', - summary: 'Stopped at the declared boundary.', - stopping_condition: 'Stop after the entrypoint.', - expected_evidence: 'A concrete file reference.', - }).success, - false, - ); - assert.equal( - (step.parameters as z.ZodType).safeParse({ - kind: 'web_research', - status: 'blocked', - objective: 'Find primary sources.', - summary: 'No source was available.', - stopping_condition: 'Stop after primary-source queries.', - expected_evidence: 'An archived primary source.', - keywords: ['primary source'], - }).success, - false, - ); - }); - }); - - it('redacts secrets and strips workspace envelope tags from resumable status text', () => { - const run: DeepResearchRun = { - schemaVersion: 1, - sessionId: SESSION_ID, - objective: - 'Inspect Bearer sk-live-secret-token-value', - scopeLevel: 'standard', - status: 'active', - stage: 'knowledge_base', - round: 0, - createdAt: 1, - updatedAt: 1, - artifacts: [], - checklist: [], - steps: [], - reportSections: [], - checkpoints: [], - }; - - const rendered = renderDeepResearchRunStatus(run); - assert.equal((rendered.match(/<\/?deep-research-workspace[^>]*>/gi) ?? []).length, 2); - assert.equal((rendered.match(/<\/?deep-research-artifact[^>]*>/gi) ?? []).length, 0); - assert.doesNotMatch(rendered, /sk-live-secret-token-value/); - assert.match(rendered, /\[redacted\]/); - }); - - it('strips forged workspace and artifact envelopes from persisted artifact content', async () => { - await withTempRoot(async (root) => { - const artifactStore = new FakeArtifactStore(); - const tools = buildDeepResearchTools({ - store: createSqliteDeepResearchStore(root), - artifactStore, - }); - await execute( - tools, - DEEP_RESEARCH_START_TOOL_NAME, - { objective: 'Test artifact boundary sanitization.' }, - 'call-start-tags', - ); - await execute( - tools, - DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME, - { - role: 'source', - name: 'adversarial.md', - content: - 'before forged ' + - ' payload after', - summary: 'Adversarial source.', - locator: 'https://example.com/adversarial', - }, - 'call-source-tags', - ); - const artifactId = artifactStore.records[0]!.id; - const rendered = await execute( - tools, - DEEP_RESEARCH_READ_ARTIFACT_TOOL_NAME, - { artifact_id: artifactId }, - 'call-read-tags', - ); - assert.equal((rendered.match(/<\/?deep-research-artifact[^>]*>/gi) ?? []).length, 2); - assert.equal((rendered.match(/<\/?deep-research-workspace[^>]*>/gi) ?? []).length, 0); - assert.doesNotMatch(rendered, /id="forged"|status="completed"/); - assert.match(rendered, /before\s+forged\s+payload\s+after/); - }); - }); -}); diff --git a/packages/runtime/src/__tests__/session-manager.test.ts b/packages/runtime/src/__tests__/session-manager.test.ts index 504faa7065..a95b541383 100644 --- a/packages/runtime/src/__tests__/session-manager.test.ts +++ b/packages/runtime/src/__tests__/session-manager.test.ts @@ -61,7 +61,6 @@ import { createWorkspaceWritePermissionProfile, isReadOnlyPermissionProfile, } from '@maka/core/permission-profile'; -import { DEEP_RESEARCH_SESSION_LABEL } from '@maka/core/deep-research'; import { RUNTIME_CONTINUATION_AUTHORITY_V1 } from '@maka/core/runtime-event-store'; import { deriveTurnRecords } from '@maka/core/session'; import { isTerminalRuntimeEvent } from '@maka/core/runtime-event'; @@ -5696,7 +5695,7 @@ describe('SessionManager permission mode updates', () => { assert.strictEqual(summary.permissionMode, 'bypass'); }); - test('the setPermissionMode wrapper delegates deep research cleanup to configuration authority', async () => { + test('legacy research Sessions stay read-only after restart until explicitly changed', async () => { const store = new VersionedConfigurationMemorySessionStore(); const backends = new BackendRegistry(); backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); @@ -5704,15 +5703,33 @@ describe('SessionManager permission mode updates', () => { const session = await manager.createSession( makeInput({ permissionMode: 'explore', - labels: [DEEP_RESEARCH_SESSION_LABEL, 'kept'], + labels: ['mode:deep_research', 'kept'], }), ); - const summary = await manager.setPermissionMode(session.id, 'ask'); + const boundaryBeforeRestart = await manager.readExecutionBoundary(session.id); + const restarted = new SessionManager({ + store, + backends, + newId: nextId('restarted'), + now: nextNow(6_100), + }); + await drain( + restarted.sendMessage(session.id, { + turnId: 'legacy-follow-up', + text: 'Read the existing report', + }), + ); + assert.equal((await store.readHeader(session.id)).permissionMode, 'explore'); + assert.deepEqual(await restarted.readExecutionBoundary(session.id), boundaryBeforeRestart); + const summary = await restarted.setPermissionMode(session.id, 'ask'); assert.strictEqual(summary.permissionMode, 'ask'); - assert.deepStrictEqual(summary.labels, ['kept']); - assert.deepStrictEqual((await store.readHeader(session.id)).labels, ['kept']); + assert.deepStrictEqual(summary.labels, ['mode:deep_research', 'kept']); + assert.deepStrictEqual((await store.readHeader(session.id)).labels, [ + 'mode:deep_research', + 'kept', + ]); }); test('temporarily preserves setPermissionMode for legacy SessionStore implementations', async () => { diff --git a/packages/runtime/src/deep-research-tools.ts b/packages/runtime/src/deep-research-tools.ts deleted file mode 100644 index 018776ac85..0000000000 --- a/packages/runtime/src/deep-research-tools.ts +++ /dev/null @@ -1,1004 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { createHash } from 'node:crypto'; -import { z } from 'zod'; -import { - DEEP_RESEARCH_ACTIVE_STAGES, - DEEP_RESEARCH_ARTIFACT_NAME_MAX_CHARS, - DEEP_RESEARCH_ARTIFACT_ROLES, - DEEP_RESEARCH_CHECKPOINT_ITEM_MAX_CHARS, - DEEP_RESEARCH_CHECKPOINT_ITEMS_MAX, - DEEP_RESEARCH_CHECKPOINT_TEXT_MAX_CHARS, - DEEP_RESEARCH_CHECKLIST_STATUSES, - DEEP_RESEARCH_DEFAULT_CHECKLIST, - DEEP_RESEARCH_INSPECTED_REF_KINDS, - DEEP_RESEARCH_LOCATOR_MAX_CHARS, - DEEP_RESEARCH_OBJECTIVE_MAX_CHARS, - DEEP_RESEARCH_REFS_MAX, - DEEP_RESEARCH_REPORT_SECTION_KEYS, - DEEP_RESEARCH_REPORT_SECTION_STATUSES, - DEEP_RESEARCH_SCOPE_LEVELS, - DEEP_RESEARCH_STEP_KINDS, - DEEP_RESEARCH_STEP_LIST_ITEMS_MAX, - DEEP_RESEARCH_STEP_STATUSES, - DEEP_RESEARCH_STEP_TEXT_MAX_CHARS, - normalizeDeepResearchObjective, - type DeepResearchRun, - type DeepResearchArtifactRef, - type DeepResearchEvent, - type DeepResearchStore, -} from '@maka/core/deep-research-run'; -import { redactSecrets } from '@maka/core/redaction'; -import { type ArtifactRecord } from '@maka/core/artifacts'; -import type { MakaTool, MakaToolContext } from './tool-runtime.js'; - -export const DEEP_RESEARCH_START_TOOL_NAME = 'deep_research_start'; -export const DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME = 'deep_research_save_artifact'; -export const DEEP_RESEARCH_READ_ARTIFACT_TOOL_NAME = 'deep_research_read_artifact'; -export const DEEP_RESEARCH_UPDATE_CHECKLIST_TOOL_NAME = 'deep_research_update_checklist'; -export const DEEP_RESEARCH_RECORD_STEP_TOOL_NAME = 'deep_research_record_step'; -export const DEEP_RESEARCH_CHECKPOINT_TOOL_NAME = 'deep_research_checkpoint'; -export const DEEP_RESEARCH_STATUS_TOOL_NAME = 'deep_research_status'; -export const DEEP_RESEARCH_COMPLETE_TOOL_NAME = 'deep_research_complete'; - -const DEEP_RESEARCH_ALLOWED_TOOL_NAMES = new Set([ - 'AskUserQuestion', - 'Read', - 'Glob', - 'Grep', - 'WebSearch', - DEEP_RESEARCH_START_TOOL_NAME, - DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME, - DEEP_RESEARCH_READ_ARTIFACT_TOOL_NAME, - DEEP_RESEARCH_UPDATE_CHECKLIST_TOOL_NAME, - DEEP_RESEARCH_RECORD_STEP_TOOL_NAME, - DEEP_RESEARCH_CHECKPOINT_TOOL_NAME, - DEEP_RESEARCH_STATUS_TOOL_NAME, - DEEP_RESEARCH_COMPLETE_TOOL_NAME, -]); - -export const DEEP_RESEARCH_ARTIFACT_CONTENT_MAX_CHARS = 512_000; -export const DEEP_RESEARCH_ARTIFACT_READ_DEFAULT_CHARS = 32_000; -export const DEEP_RESEARCH_ARTIFACT_READ_MAX_CHARS = 64_000; -export const DEEP_RESEARCH_STATUS_ARTIFACTS_MAX = 100; - -const stableIdSchema = z - .string() - .trim() - .min(1) - .max(128) - .regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/, 'Expected a stable research, task, or artifact id.') - .refine( - (value) => redactSecrets(value) === value, - 'Research references cannot contain secret-like values.', - ); - -export interface DeepResearchArtifactStore { - create(input: { - sessionId: string; - turnId: string; - name: string; - kind: 'file'; - content: string; - mimeType: 'text/markdown'; - source: 'deep_research'; - summary: string; - deepResearchRole: (typeof DEEP_RESEARCH_ARTIFACT_ROLES)[number]; - id: string; - }): Promise; - get(artifactId: string): Promise; - readText( - artifactId: string, - options?: { maxBytes?: number }, - ): Promise<{ ok: true; text: string } | { ok: false; reason: string }>; - delete(artifactId: string): Promise; -} - -export interface BuildDeepResearchToolsDeps { - store: DeepResearchStore; - artifactStore: DeepResearchArtifactStore; -} - -export function buildDeepResearchTools(deps: BuildDeepResearchToolsDeps): MakaTool[] { - return [ - buildStartTool(deps), - buildSaveArtifactTool(deps), - buildReadArtifactTool(deps), - buildUpdateChecklistTool(deps), - buildRecordStepTool(deps), - buildCheckpointTool(deps), - buildStatusTool(deps), - buildCompleteTool(deps), - ]; -} - -export function isDeepResearchToolAllowed(tool: Pick): boolean { - return DEEP_RESEARCH_ALLOWED_TOOL_NAMES.has(tool.name); -} - -function buildStartTool(deps: BuildDeepResearchToolsDeps): MakaTool< - { - objective: string; - scope_level: (typeof DEEP_RESEARCH_SCOPE_LEVELS)[number]; - }, - string -> { - return { - name: DEEP_RESEARCH_START_TOOL_NAME, - displayName: 'Initialize Research Workspace', - description: - 'Initialize the durable Deep Research workspace for this session. Call once before archiving sources, ' + - 'writing evidence notes, or checkpointing. Retrying the same tool call is safe.', - parameters: z.object({ - objective: z - .string() - .trim() - .min(1) - .max(DEEP_RESEARCH_OBJECTIVE_MAX_CHARS) - .describe('The concrete research question and requested outcome.'), - scope_level: z - .enum(DEEP_RESEARCH_SCOPE_LEVELS) - .default('standard') - .describe('Research budget: quick, standard, or deep.'), - }), - impl: async (input, ctx) => { - const objective = normalizeDeepResearchObjective(input.objective); - if (!objective) throw new Error('Deep Research objective is invalid'); - const run = await deps.store.start( - ctx.sessionId, - objective, - input.scope_level, - mutationContext(ctx), - ); - return renderRunStatus(run); - }, - }; -} - -function buildReadArtifactTool(deps: BuildDeepResearchToolsDeps): MakaTool< - { - artifact_id: string; - offset_chars?: number; - max_chars?: number; - }, - string -> { - return { - name: DEEP_RESEARCH_READ_ARTIFACT_TOOL_NAME, - displayName: 'Read Research Artifact', - description: - 'Read a bounded chunk of a persisted artifact from this Deep Research workspace. ' + - 'Use artifact ids from deep_research_status to recover evidence after interruption or restart.', - parameters: z.object({ - artifact_id: stableIdSchema.describe('Research artifact id from the current workspace.'), - offset_chars: z - .number() - .int() - .min(0) - .max(DEEP_RESEARCH_ARTIFACT_CONTENT_MAX_CHARS) - .optional() - .describe('Zero-based character offset for chunked reads.'), - max_chars: z - .number() - .int() - .min(1) - .max(DEEP_RESEARCH_ARTIFACT_READ_MAX_CHARS) - .optional() - .describe( - `Maximum characters to return (default ${DEEP_RESEARCH_ARTIFACT_READ_DEFAULT_CHARS}).`, - ), - }), - impl: async (input, ctx) => { - const run = await deps.store.read(ctx.sessionId); - if (!run) { - throw new Error(`Call ${DEEP_RESEARCH_START_TOOL_NAME} before reading research artifacts`); - } - const ref = run.artifacts.find((artifact) => artifact.artifactId === input.artifact_id); - if (!ref) throw new Error('Research artifact is not part of this session workspace'); - const record = await deps.artifactStore.get(input.artifact_id); - if (!record || record.sessionId !== ctx.sessionId || record.source !== 'deep_research') { - throw new Error('Research artifact is missing, deleted, or belongs to another session'); - } - const read = await deps.artifactStore.readText(input.artifact_id, { - maxBytes: DEEP_RESEARCH_ARTIFACT_CONTENT_MAX_CHARS * 4, - }); - if (!read.ok) throw new Error(`Research artifact could not be read: ${read.reason}`); - const contentHash = `sha256:${createHash('sha256').update(read.text).digest('hex')}`; - if (contentHash !== ref.contentHash) { - throw new Error('Research artifact content no longer matches the durable research ledger'); - } - const offset = input.offset_chars ?? 0; - const maxChars = input.max_chars ?? DEEP_RESEARCH_ARTIFACT_READ_DEFAULT_CHARS; - const selected: string[] = []; - let total = 0; - for (const character of read.text) { - if (total >= offset && total < offset + maxChars) selected.push(character); - total += 1; - } - const end = Math.min(total, offset + maxChars); - const chunk = safeResearchArtifactContent(selected.join('')); - return [ - ``, - `Name: ${normalizeInlineText(ref.name)}`, - ...(ref.locator ? [`Locator: ${normalizeInlineText(ref.locator)}`] : []), - `Truncated: ${end < total}`, - '', - chunk, - '', - ].join('\n'); - }, - }; -} - -function buildSaveArtifactTool(deps: BuildDeepResearchToolsDeps): MakaTool< - { - role: (typeof DEEP_RESEARCH_ARTIFACT_ROLES)[number]; - name: string; - content: string; - summary: string; - locator?: string; - source_artifact_ids?: string[]; - report_section_key?: (typeof DEEP_RESEARCH_REPORT_SECTION_KEYS)[number]; - report_section_status?: Exclude< - (typeof DEEP_RESEARCH_REPORT_SECTION_STATUSES)[number], - 'pending' - >; - }, - string -> { - return { - name: DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME, - displayName: 'Save Research Artifact', - description: - 'Persist a Markdown research artifact outside the model context. Archive raw source material as role=source ' + - 'before writing derived evidence notes or report content. Derived artifacts must cite source artifact ids.', - parameters: z - .object({ - role: z - .enum(DEEP_RESEARCH_ARTIFACT_ROLES) - .describe('Artifact role in the two-stage research workspace.'), - name: z - .string() - .trim() - .min(1) - .max(DEEP_RESEARCH_ARTIFACT_NAME_MAX_CHARS) - .describe('Human-readable Markdown filename.'), - content: z - .string() - .min(1) - .max(DEEP_RESEARCH_ARTIFACT_CONTENT_MAX_CHARS) - .describe('Exact Markdown body to persist.'), - summary: z - .string() - .trim() - .min(1) - .max(DEEP_RESEARCH_CHECKPOINT_TEXT_MAX_CHARS) - .describe('Short description shown in the artifact list.'), - locator: z - .string() - .trim() - .min(1) - .max(DEEP_RESEARCH_LOCATOR_MAX_CHARS) - .optional() - .describe( - 'Required for source artifacts: URL, repository path, or other inspectable source locator.', - ), - source_artifact_ids: z - .array(stableIdSchema) - .max(DEEP_RESEARCH_REFS_MAX) - .optional() - .describe('Direct raw source artifact ids supporting this derived artifact.'), - report_section_key: z - .enum(DEEP_RESEARCH_REPORT_SECTION_KEYS) - .optional() - .describe('Required when role=report_section.'), - report_section_status: z - .enum(['drafted', 'completed']) - .optional() - .describe('Required when role=report_section.'), - }) - .superRefine((input, ctx) => { - const sourceIds = input.source_artifact_ids ?? []; - if (input.role === 'source' && !input.locator) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['locator'], - message: 'Source artifacts require a locator.', - }); - } - if (input.role === 'source' && sourceIds.length > 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['source_artifact_ids'], - message: 'Source artifacts cannot cite other research artifacts.', - }); - } - if (input.role !== 'source' && sourceIds.length === 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['source_artifact_ids'], - message: `${input.role} artifacts require direct source artifact ids.`, - }); - } - if ( - input.role === 'report_section' && - (!input.report_section_key || !input.report_section_status) - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['report_section_key'], - message: 'Report section artifacts require a section key and status.', - }); - } - if ( - input.role !== 'report_section' && - (input.report_section_key || input.report_section_status) - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['report_section_key'], - message: 'Report section metadata is only valid for role=report_section.', - }); - } - }), - impl: async (input, ctx) => { - const artifactId = stableArtifactId(ctx); - const sourceArtifactIds = dedupe(input.source_artifact_ids ?? []); - const inputHash = `sha256:${createHash('sha256').update(input.content).digest('hex')}`; - const replayEvent = await findToolCallEvent(deps.store, ctx.sessionId, ctx.toolCallId); - if (replayEvent) { - if (replayEvent.type !== 'research_artifact_recorded') { - throw new Error( - `Deep Research tool call ${ctx.toolCallId} was already used for ${replayEvent.type}`, - ); - } - const replay = replayEvent.artifact; - const replayRecord = await deps.artifactStore.get(replay.artifactId); - if ( - replay.artifactId !== artifactId || - replay.role !== input.role || - replay.name !== input.name || - (replay.summary ?? replayRecord?.summary) !== input.summary || - replay.locator !== input.locator || - replay.contentHash !== inputHash || - !sameStringArray(replay.sourceArtifactIds, sourceArtifactIds) || - replay.reportSectionKey !== input.report_section_key || - replay.reportSectionStatus !== input.report_section_status - ) { - throw new Error( - 'Deep Research artifact tool call was retried with different content or metadata', - ); - } - const replayRun = await deps.store.read(ctx.sessionId); - if (!replayRun) throw new Error('Deep Research artifact replay is missing its workspace'); - return `Research artifact ${artifactId} was already saved.\n${renderRunStatus(replayRun)}`; - } - await requireActiveRun(deps.store, ctx.sessionId); - const artifact = await deps.artifactStore.create({ - sessionId: ctx.sessionId, - turnId: ctx.turnId, - name: input.name, - kind: 'file', - content: input.content, - mimeType: 'text/markdown', - source: 'deep_research', - summary: input.summary, - deepResearchRole: input.role, - id: artifactId, - }); - let run: DeepResearchRun; - try { - run = await deps.store.recordArtifact( - ctx.sessionId, - { - artifactId, - role: input.role, - name: input.name, - summary: input.summary, - createdAt: artifact.createdAt, - ...(input.locator ? { locator: input.locator } : {}), - contentHash: inputHash, - sourceArtifactIds, - ...(input.report_section_key ? { reportSectionKey: input.report_section_key } : {}), - ...(input.report_section_status - ? { reportSectionStatus: input.report_section_status } - : {}), - }, - mutationContext(ctx), - ); - } catch (error) { - await deps.artifactStore.delete(artifactId).catch(() => undefined); - throw error; - } - return `Saved ${input.role} artifact ${artifactId}.\n${renderRunStatus(run)}`; - }, - }; -} - -function buildUpdateChecklistTool(deps: BuildDeepResearchToolsDeps): MakaTool< - { - item_id: string; - status: (typeof DEEP_RESEARCH_CHECKLIST_STATUSES)[number]; - evidence_artifact_ids?: string[]; - blocked_reason?: string; - }, - string -> { - return { - name: DEEP_RESEARCH_UPDATE_CHECKLIST_TOOL_NAME, - displayName: 'Update Research Checklist', - description: - 'Update one durable Deep Research checklist item. Completed items require saved evidence artifacts; ' + - 'blocked items require a concrete blocker that remains visible after restart.', - parameters: z - .object({ - item_id: stableIdSchema.refine( - (value) => DEEP_RESEARCH_DEFAULT_CHECKLIST.some((item) => item.itemId === value), - 'Unknown Deep Research checklist item.', - ), - status: z.enum(DEEP_RESEARCH_CHECKLIST_STATUSES), - evidence_artifact_ids: z.array(stableIdSchema).max(DEEP_RESEARCH_REFS_MAX).optional(), - blocked_reason: z.string().trim().min(1).max(DEEP_RESEARCH_STEP_TEXT_MAX_CHARS).optional(), - }) - .superRefine((input, ctx) => { - if (input.status === 'completed' && (input.evidence_artifact_ids?.length ?? 0) === 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['evidence_artifact_ids'], - message: 'Completed checklist items require evidence artifacts.', - }); - } - if (input.status === 'blocked' && !input.blocked_reason) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['blocked_reason'], - message: 'Blocked checklist items require a reason.', - }); - } - if (input.status !== 'blocked' && input.blocked_reason) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['blocked_reason'], - message: 'A blocked reason is only valid for blocked checklist items.', - }); - } - }), - impl: async (input, ctx) => { - await requireRun(deps.store, ctx.sessionId); - const run = await deps.store.updateChecklist( - ctx.sessionId, - { - itemId: input.item_id, - status: input.status, - evidenceArtifactIds: dedupe(input.evidence_artifact_ids ?? []), - ...(input.blocked_reason ? { blockedReason: input.blocked_reason } : {}), - }, - mutationContext(ctx), - ); - return renderRunStatus(run); - }, - }; -} - -function buildRecordStepTool(deps: BuildDeepResearchToolsDeps): MakaTool< - { - kind: (typeof DEEP_RESEARCH_STEP_KINDS)[number]; - status: (typeof DEEP_RESEARCH_STEP_STATUSES)[number]; - objective: string; - summary: string; - roots?: string[]; - keywords?: string[]; - ignored_paths?: string[]; - stopping_condition: string; - expected_evidence: string; - evidence_artifact_ids?: string[]; - inspected_refs?: Array<{ - kind: (typeof DEEP_RESEARCH_INSPECTED_REF_KINDS)[number]; - locator: string; - label?: string; - source_artifact_id?: string; - }>; - worker_run_ids?: string[]; - blocked_reason?: string; - }, - string -> { - const boundedText = z.string().trim().min(1).max(DEEP_RESEARCH_STEP_TEXT_MAX_CHARS); - const boundedList = z - .array(z.string().trim().min(1).max(DEEP_RESEARCH_LOCATOR_MAX_CHARS)) - .max(DEEP_RESEARCH_STEP_LIST_ITEMS_MAX); - return { - name: DEEP_RESEARCH_RECORD_STEP_TOOL_NAME, - displayName: 'Record Research Step', - description: - 'Record a bounded local-exploration or web-research step, including its search roots/query terms, ' + - 'stopping condition, inspected references, worker runs, evidence, and any blocker.', - parameters: z - .object({ - kind: z.enum(DEEP_RESEARCH_STEP_KINDS), - status: z.enum(DEEP_RESEARCH_STEP_STATUSES), - objective: boundedText, - summary: boundedText, - roots: boundedList.optional(), - keywords: boundedList.optional(), - ignored_paths: boundedList.optional(), - stopping_condition: boundedText, - expected_evidence: boundedText, - evidence_artifact_ids: z.array(stableIdSchema).max(DEEP_RESEARCH_REFS_MAX).optional(), - inspected_refs: z - .array( - z.object({ - kind: z.enum(DEEP_RESEARCH_INSPECTED_REF_KINDS), - locator: z.string().trim().min(1).max(DEEP_RESEARCH_LOCATOR_MAX_CHARS), - label: z.string().trim().min(1).max(DEEP_RESEARCH_STEP_TEXT_MAX_CHARS).optional(), - source_artifact_id: stableIdSchema.optional(), - }), - ) - .max(DEEP_RESEARCH_STEP_LIST_ITEMS_MAX) - .optional(), - worker_run_ids: z.array(stableIdSchema).max(DEEP_RESEARCH_STEP_LIST_ITEMS_MAX).optional(), - blocked_reason: boundedText.optional(), - }) - .superRefine((input, ctx) => { - if (input.kind === 'local_exploration' && (input.roots?.length ?? 0) === 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['roots'], - message: 'Local exploration requires at least one bounded root.', - }); - } - if (input.kind === 'web_research' && (input.keywords?.length ?? 0) === 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['keywords'], - message: 'Web research requires at least one query or keyword.', - }); - } - if (input.status === 'completed' && (input.evidence_artifact_ids?.length ?? 0) === 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['evidence_artifact_ids'], - message: 'Completed research steps require persisted evidence.', - }); - } - if (input.status === 'blocked' && !input.blocked_reason) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['blocked_reason'], - message: 'Blocked research steps require a reason.', - }); - } - if (input.status !== 'blocked' && input.blocked_reason) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['blocked_reason'], - message: 'A blocked reason is only valid for blocked research steps.', - }); - } - }), - impl: async (input, ctx) => { - await requireRun(deps.store, ctx.sessionId); - const run = await deps.store.recordStep( - ctx.sessionId, - { - kind: input.kind, - status: input.status, - objective: input.objective, - summary: input.summary, - roots: dedupe(input.roots ?? []), - keywords: dedupe(input.keywords ?? []), - ignoredPaths: dedupe(input.ignored_paths ?? []), - stoppingCondition: input.stopping_condition, - expectedEvidence: input.expected_evidence, - evidenceArtifactIds: dedupe(input.evidence_artifact_ids ?? []), - inspectedRefs: (input.inspected_refs ?? []).map((ref) => ({ - kind: ref.kind, - locator: ref.locator, - ...(ref.label ? { label: ref.label } : {}), - ...(ref.source_artifact_id ? { sourceArtifactId: ref.source_artifact_id } : {}), - })), - workerRunIds: dedupe(input.worker_run_ids ?? []), - ...(input.blocked_reason ? { blockedReason: input.blocked_reason } : {}), - }, - mutationContext(ctx), - ); - return renderRunStatus(run); - }, - }; -} - -function buildCheckpointTool(deps: BuildDeepResearchToolsDeps): MakaTool< - { - round: number; - stage: (typeof DEEP_RESEARCH_ACTIVE_STAGES)[number]; - status: 'active' | 'blocked'; - summary: string; - open_questions?: string[]; - next_steps?: string[]; - artifact_ids?: string[]; - }, - string -> { - const itemArray = z - .array(z.string().trim().min(1).max(DEEP_RESEARCH_CHECKPOINT_ITEM_MAX_CHARS)) - .max(DEEP_RESEARCH_CHECKPOINT_ITEMS_MAX); - const refArray = z.array(stableIdSchema).max(DEEP_RESEARCH_REFS_MAX); - return { - name: DEEP_RESEARCH_CHECKPOINT_TOOL_NAME, - displayName: 'Checkpoint Research', - description: - 'Record a durable research checkpoint after a meaningful round or before context compaction. ' + - 'Include unresolved questions, next steps, and the artifacts needed to resume.', - parameters: z.object({ - round: z.number().int().min(1).describe('Monotonic research round number.'), - stage: z.enum(DEEP_RESEARCH_ACTIVE_STAGES).describe('Current two-stage workflow phase.'), - status: z - .enum(['active', 'blocked']) - .describe('Whether research can proceed without outside input.'), - summary: z - .string() - .trim() - .min(1) - .max(DEEP_RESEARCH_CHECKPOINT_TEXT_MAX_CHARS) - .describe('What was established during this round.'), - open_questions: itemArray - .optional() - .describe('Questions still requiring evidence or resolution.'), - next_steps: itemArray.optional().describe('Concrete continuation steps.'), - artifact_ids: refArray.optional().describe('Known research artifact ids required to resume.'), - }), - impl: async (input, ctx) => { - await requireRun(deps.store, ctx.sessionId); - const run = await deps.store.recordCheckpoint( - ctx.sessionId, - { - round: input.round, - stage: input.stage, - status: input.status, - summary: input.summary, - openQuestions: dedupe(input.open_questions ?? []), - nextSteps: dedupe(input.next_steps ?? []), - taskIds: [], - artifactIds: dedupe(input.artifact_ids ?? []), - }, - mutationContext(ctx), - ); - return renderRunStatus(run); - }, - }; -} - -function buildStatusTool( - deps: BuildDeepResearchToolsDeps, -): MakaTool, string> { - return { - name: DEEP_RESEARCH_STATUS_TOOL_NAME, - displayName: 'Read Research Workspace', - description: - 'Read the durable Deep Research workspace projection. Use after interruption, context compaction, ' + - 'or process restart to recover the objective, stage, latest checkpoint, and artifact inventory.', - parameters: z.object({}), - impl: async (_input, ctx) => { - const run = await deps.store.read(ctx.sessionId); - return run ? renderRunStatus(run) : ''; - }, - }; -} - -function buildCompleteTool(deps: BuildDeepResearchToolsDeps): MakaTool< - { - report_artifact_id: string; - handoff_artifact_id: string; - implementation_tasks: string[]; - recommended_issues?: string[]; - recommended_pull_requests?: string[]; - verification_commands: string[]; - }, - string -> { - const handoffList = z - .array(z.string().trim().min(1).max(DEEP_RESEARCH_CHECKPOINT_ITEM_MAX_CHARS)) - .max(DEEP_RESEARCH_CHECKPOINT_ITEMS_MAX); - return { - name: DEEP_RESEARCH_COMPLETE_TOOL_NAME, - displayName: 'Complete Research', - description: - 'Complete Deep Research only after every checklist item and required report section is settled. ' + - 'A saved handoff artifact and structured implementation, issue/PR, and verification guidance are required.', - parameters: z - .object({ - report_artifact_id: stableIdSchema.describe( - 'Artifact id of the final source-backed report.', - ), - handoff_artifact_id: stableIdSchema.describe( - 'Artifact id of the saved role=handoff artifact.', - ), - implementation_tasks: handoffList.min(1), - recommended_issues: handoffList.optional(), - recommended_pull_requests: handoffList.optional(), - verification_commands: handoffList.min(1), - }) - .superRefine((input, ctx) => { - if ( - (input.recommended_issues?.length ?? 0) === 0 && - (input.recommended_pull_requests?.length ?? 0) === 0 - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['recommended_issues'], - message: 'Provide at least one recommended issue or pull request.', - }); - } - }), - impl: async (input, ctx) => { - const handoff = { - artifactId: input.handoff_artifact_id, - implementationTasks: dedupe(input.implementation_tasks), - recommendedIssues: dedupe(input.recommended_issues ?? []), - recommendedPullRequests: dedupe(input.recommended_pull_requests ?? []), - verificationCommands: dedupe(input.verification_commands), - }; - const replay = await findToolCallEvent(deps.store, ctx.sessionId, ctx.toolCallId); - if (replay) { - const run = await deps.store.complete( - ctx.sessionId, - input.report_artifact_id, - handoff, - mutationContext(ctx), - ); - return renderRunStatus(run); - } - const existing = await requireActiveRun(deps.store, ctx.sessionId); - await validateCompletionArtifacts( - deps.artifactStore, - existing, - input.report_artifact_id, - input.handoff_artifact_id, - ); - const run = await deps.store.complete( - ctx.sessionId, - input.report_artifact_id, - handoff, - mutationContext(ctx), - ); - return renderRunStatus(run); - }, - }; -} - -async function requireActiveRun( - store: DeepResearchStore, - sessionId: string, -): Promise { - const run = await store.read(sessionId); - if (!run) { - throw new Error(`Call ${DEEP_RESEARCH_START_TOOL_NAME} before using the research workspace`); - } - if (run.status === 'completed') { - throw new Error('Deep Research workspace is already completed'); - } - return run; -} - -async function requireRun(store: DeepResearchStore, sessionId: string): Promise { - const run = await store.read(sessionId); - if (!run) { - throw new Error(`Call ${DEEP_RESEARCH_START_TOOL_NAME} before using the research workspace`); - } - return run; -} - -async function findToolCallEvent( - store: DeepResearchStore, - sessionId: string, - toolCallId: string, -): Promise { - return (await store.readEvents(sessionId)).find((event) => event.refs?.toolCallId === toolCallId); -} - -async function validateCompletionArtifacts( - artifactStore: DeepResearchArtifactStore, - run: DeepResearchRun, - reportArtifactId: string, - handoffArtifactId: string, -): Promise { - const required = new Map(); - for (const source of run.artifacts.filter((artifact) => artifact.role === 'source')) { - required.set(source.artifactId, source); - } - for (const section of run.reportSections) { - if (!section.artifactId) { - throw new Error(`Deep Research report section ${section.key} has no current artifact`); - } - const ref = run.artifacts.find((artifact) => artifact.artifactId === section.artifactId); - if (!ref || ref.role !== 'report_section' || ref.reportSectionKey !== section.key) { - throw new Error( - `Deep Research report section ${section.key} has an invalid current artifact`, - ); - } - required.set(ref.artifactId, ref); - } - for (const [artifactId, role] of [ - [reportArtifactId, 'report'], - [handoffArtifactId, 'handoff'], - ] as const) { - const ref = run.artifacts.find((artifact) => artifact.artifactId === artifactId); - if (!ref || ref.role !== role) { - throw new Error(`Deep Research ${role} artifact ${artifactId} is missing from the ledger`); - } - required.set(ref.artifactId, ref); - } - for (const ref of required.values()) { - await validateArtifactIntegrity(artifactStore, run.sessionId, ref); - } -} - -async function validateArtifactIntegrity( - artifactStore: DeepResearchArtifactStore, - sessionId: string, - ref: DeepResearchArtifactRef, -): Promise { - const record = await artifactStore.get(ref.artifactId); - if (!record) { - throw new Error(`Deep Research artifact ${ref.artifactId} is missing or deleted`); - } - if (record.sessionId !== sessionId || record.source !== 'deep_research') { - throw new Error(`Deep Research artifact ${ref.artifactId} belongs to another workspace`); - } - if ( - record.kind !== 'file' || - record.mimeType !== 'text/markdown' || - record.deepResearchRole !== ref.role - ) { - throw new Error( - `Deep Research artifact ${ref.artifactId} type or role does not match the ledger`, - ); - } - const read = await artifactStore.readText(ref.artifactId, { - maxBytes: DEEP_RESEARCH_ARTIFACT_CONTENT_MAX_CHARS * 4, - }); - if (!read.ok) { - throw new Error(`Deep Research artifact ${ref.artifactId} could not be read: ${read.reason}`); - } - const contentHash = `sha256:${createHash('sha256').update(read.text).digest('hex')}`; - if (contentHash !== ref.contentHash) { - throw new Error(`Deep Research artifact ${ref.artifactId} content does not match the ledger`); - } -} - -function mutationContext(ctx: MakaToolContext): { - runId?: string; - turnId: string; - toolCallId: string; -} { - return { - ...(ctx.runId ? { runId: ctx.runId } : {}), - turnId: ctx.turnId, - toolCallId: ctx.toolCallId, - }; -} - -function stableArtifactId(ctx: MakaToolContext): string { - const digest = createHash('sha256') - .update(`${ctx.sessionId}\n${ctx.turnId}\n${ctx.toolCallId}`) - .digest('hex'); - const uuidLike = [ - digest.slice(0, 8), - digest.slice(8, 12), - digest.slice(12, 16), - digest.slice(16, 20), - digest.slice(20, 32), - ].join('-'); - return `dr-${uuidLike}`; -} - -function dedupe(values: readonly string[]): string[] { - return [...new Set(values)]; -} - -function sameStringArray(left: readonly string[], right: readonly string[]): boolean { - return left.length === right.length && left.every((value, index) => value === right[index]); -} - -function normalizeInlineText(value: string): string { - return redactSecrets(value) - .replace(/<\/?deep-research-(?:workspace|artifact)\b[^>]{0,4096}>/gi, '') - .replace(/\s+/g, ' ') - .trim(); -} - -function safeResearchArtifactContent(value: string): string { - return redactSecrets(value).replace( - /<\/?deep-research-(?:workspace|artifact)\b[^>]{0,4096}>/gi, - '', - ); -} - -export function renderDeepResearchRunStatus(run: DeepResearchRun): string { - const latest = run.checkpoints.at(-1); - const lines = [ - ``, - `Objective: ${normalizeInlineText(run.objective)}`, - `Artifacts: ${run.artifacts.length} (${DEEP_RESEARCH_ARTIFACT_ROLES.map( - (role) => `${role}=${run.artifacts.filter((artifact) => artifact.role === role).length}`, - ).join(', ')})`, - ]; - const visibleArtifacts = run.artifacts.slice(-DEEP_RESEARCH_STATUS_ARTIFACTS_MAX); - for (const artifact of visibleArtifacts) { - lines.push( - `- ${normalizeInlineText(artifact.artifactId)} [${artifact.role}] ${normalizeInlineText(artifact.name)}`, - ); - } - lines.push('Checklist:'); - for (const item of run.checklist) { - lines.push( - `- [${item.status}] ${normalizeInlineText(item.itemId)}: ${normalizeInlineText(item.title)}` + - (item.evidenceArtifactIds.length > 0 - ? ` (evidence: ${item.evidenceArtifactIds.join(', ')})` - : '') + - (item.blockedReason ? ` (blocked: ${normalizeInlineText(item.blockedReason)})` : ''), - ); - } - lines.push('Report sections:'); - for (const section of run.reportSections) { - lines.push( - `- [${section.status}] ${section.key}${section.artifactId ? ` (${section.artifactId})` : ''}`, - ); - } - lines.push(`Research steps: ${run.steps.length}`); - for (const step of run.steps.slice(-10)) { - lines.push( - `- [${step.status}] ${step.kind}: ${normalizeInlineText(step.summary)}` + - (step.workerRunIds.length > 0 ? ` (workers: ${step.workerRunIds.join(', ')})` : '') + - (step.blockedReason ? ` (blocked: ${normalizeInlineText(step.blockedReason)})` : ''), - ); - } - if (run.artifacts.length > visibleArtifacts.length) { - lines.push( - `- ${run.artifacts.length - visibleArtifacts.length} older artifact(s) omitted from this status view`, - ); - } - if (latest) { - lines.push(`Latest checkpoint: ${normalizeInlineText(latest.summary)}`); - if (latest.openQuestions.length > 0) { - lines.push(`Open questions: ${latest.openQuestions.map(normalizeInlineText).join(' | ')}`); - } - if (latest.nextSteps.length > 0) { - lines.push(`Next steps: ${latest.nextSteps.map(normalizeInlineText).join(' | ')}`); - } - if (latest.artifactIds.length > 0) { - lines.push(`Resume artifacts: ${latest.artifactIds.join(', ')}`); - } - } - if (run.reportArtifactId) lines.push(`Final report: ${run.reportArtifactId}`); - if (run.handoff) { - lines.push(`Handoff artifact: ${run.handoff.artifactId}`); - lines.push( - `Implementation tasks: ${run.handoff.implementationTasks.map(normalizeInlineText).join(' | ')}`, - ); - lines.push( - `Verification commands: ${run.handoff.verificationCommands.map(normalizeInlineText).join(' | ')}`, - ); - } - lines.push(''); - return lines.join('\n'); -} - -const renderRunStatus = renderDeepResearchRunStatus; diff --git a/packages/runtime/src/session-manager.ts b/packages/runtime/src/session-manager.ts index 6146073f02..2905f8d5cd 100644 --- a/packages/runtime/src/session-manager.ts +++ b/packages/runtime/src/session-manager.ts @@ -96,7 +96,6 @@ import { type PlanStore, } from '@maka/core/plan'; import { DEFAULT_SESSION_NAME } from '@maka/core/session-name'; -import { DEEP_RESEARCH_SESSION_LABEL, isDeepResearchSession } from '@maka/core/deep-research'; import { SUBAGENT_SESSION_RUNTIME_SCHEMA_VERSION, SUBAGENT_SESSION_SPAWN_SCHEMA_VERSION, @@ -1204,12 +1203,7 @@ export class SessionManager { current.header, input.configuration.collaborationMode, ); - const leavingDeepResearch = - isDeepResearchSession(current.header.labels) && - input.configuration.permissionMode !== 'explore'; - const labels = leavingDeepResearch - ? current.header.labels.filter((label) => label !== DEEP_RESEARCH_SESSION_LABEL) - : current.header.labels; + const labels = current.header.labels; return () => store.updateSessionConfiguration(sessionId, { expectedVersion: input.expectedRevision, @@ -1693,18 +1687,14 @@ export class SessionManager { ): Promise { const previous = await this.deps.store.readHeader(sessionId); const boundary = await this.deps.store.readExecutionBoundary(sessionId); - const leavingDeepResearch = isDeepResearchSession(previous.labels) && mode !== 'explore'; if ( previous.permissionMode === mode && - executionBoundaryMatchesPermissionMode(boundary, mode) && - !leavingDeepResearch + executionBoundaryMatchesPermissionMode(boundary, mode) ) { return headerToSummary(previous); } - const labels = leavingDeepResearch - ? previous.labels.filter((label) => label !== DEEP_RESEARCH_SESSION_LABEL) - : previous.labels; + const labels = previous.labels; const kind = mode === 'bypass' ? 'bypass' : 'managed'; await this.commitExecutionBoundaryTransition(sessionId, boundary, mode, async () => { const current = await this.deps.store.readHeader(sessionId); diff --git a/packages/storage/package.json b/packages/storage/package.json index 5f171c0d36..43b40dc190 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -14,8 +14,6 @@ "./tool-result-archive-evidence": "./dist/tool-result-archive-evidence.js", "./credential-store": "./dist/credential-store.js", "./daily-review-authority": "./dist/daily-review-authority.js", - "./deep-research-authority": "./dist/deep-research-authority.js", - "./deep-research-store": "./dist/deep-research-store.js", "./encrypted-file-managed-secret-store": "./dist/encrypted-file-managed-secret-store.js", "./execution-stores": "./dist/execution-stores.js", "./execution-persistence-provider": "./dist/execution-persistence-provider.js", diff --git a/packages/storage/src/__tests__/artifact-copy-replay.test.ts b/packages/storage/src/__tests__/artifact-copy-replay.test.ts index a91f455bbd..048a7f75a6 100644 --- a/packages/storage/src/__tests__/artifact-copy-replay.test.ts +++ b/packages/storage/src/__tests__/artifact-copy-replay.test.ts @@ -88,7 +88,6 @@ test('verified copy rejects conflicting ownership and every retained metadata fi { sizeBytes: original.sizeBytes + 1 }, { createdAt: original.createdAt + 1 }, { summary: 'different summary' }, - { deepResearchRole: 'report' }, ]; for (const patch of variants) { const changed = { ...original, ...patch }; diff --git a/packages/storage/src/__tests__/artifact-store.test.ts b/packages/storage/src/__tests__/artifact-store.test.ts index 6119b08a40..cd67227dee 100644 --- a/packages/storage/src/__tests__/artifact-store.test.ts +++ b/packages/storage/src/__tests__/artifact-store.test.ts @@ -575,7 +575,7 @@ describe('SQLite Artifact store', () => { }); }); - test('persists complete canonical deep-research and archived tool-result records', async () => { + test('reopens legacy research reports and archived tool results', async () => { await withWorkspace(async (root) => { const store = createArtifactStore(root); const report = await store.create({ @@ -588,7 +588,6 @@ describe('SQLite Artifact store', () => { mimeType: 'text/html', source: 'deep_research', summary: 'Canonical research report', - deepResearchRole: 'report', now: 100, }); const archive = await store.create({ @@ -604,6 +603,9 @@ describe('SQLite Artifact store', () => { now: 200, }); + const legacyReport = { ...report, deepResearchRole: 'report' }; + await writeArtifactMetadata(root, [legacyReport]); + const reopened = createArtifactStore(root); assert.deepEqual(await getArtifact(reopened, report.id), report); assert.deepEqual(await getArtifact(reopened, archive.id), archive); @@ -1326,14 +1328,6 @@ describe('SQLite Artifact store', () => { () => store.create({ ...artifactInput('bad/id', 'no', 1) }), /Artifact id must be a canonical entity ID/, ); - await assert.rejects( - () => - store.create({ - ...artifactInput('invalid-role', 'no', 1), - deepResearchRole: 'invalid' as never, - }), - /Invalid Artifact deep-research role/, - ); await assert.rejects(() => stat(join(root, 'artifacts')), { code: 'ENOENT' }); }); @@ -1386,7 +1380,6 @@ function deepResearchArtifactInput(id: string, content: string) { mimeType: 'text/markdown', source: 'deep_research' as const, summary: 'Stable research artifact', - deepResearchRole: 'source' as const, }; } diff --git a/packages/storage/src/__tests__/public-entrypoints.test.ts b/packages/storage/src/__tests__/public-entrypoints.test.ts index 0144408fa2..a82c61b94d 100644 --- a/packages/storage/src/__tests__/public-entrypoints.test.ts +++ b/packages/storage/src/__tests__/public-entrypoints.test.ts @@ -49,8 +49,6 @@ const SQLITE_BACKED_ENTRYPOINTS = [ './agent-run-store', './artifact-stores', './daily-review-authority', - './deep-research-authority', - './deep-research-store', './execution-stores', './git-worktree-child-executor', './goal-authority', diff --git a/packages/storage/src/__tests__/sqlite-workflow-store.test.ts b/packages/storage/src/__tests__/sqlite-workflow-store.test.ts index 26a8084bc2..aa612b2db5 100644 --- a/packages/storage/src/__tests__/sqlite-workflow-store.test.ts +++ b/packages/storage/src/__tests__/sqlite-workflow-store.test.ts @@ -23,7 +23,6 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { after, describe, test } from 'node:test'; import { DatabaseSync } from 'node:sqlite'; -import { createSqliteDeepResearchStore } from '../deep-research-store.js'; import { createOperationalStateBackup, restoreOperationalStateBackup, @@ -675,38 +674,6 @@ describe('SQLite workflow stores', () => { }); }); - test('persists Deep Research events', async () => { - await withRoot(async (root) => { - const store = createSqliteDeepResearchStore(root, { - newId: () => 'research-1', - now: () => 200, - }); - await store.start(SESSION_ID, 'Map the SQLite authority', 'deep'); - store.close(); - - const reopened = createSqliteDeepResearchStore(root); - try { - assert.equal((await reopened.read(SESSION_ID))?.objective, 'Map the SQLite authority'); - } finally { - reopened.close(); - } - }); - }); - - test('purges Deep Research events for retired Sessions', async () => { - await withRoot(async (root) => { - const store = createSqliteDeepResearchStore(root); - try { - await store.start(SESSION_ID, 'Remove the retired research workspace', 'standard'); - await store.purgeSessionState(SESSION_ID); - assert.equal(await store.read(SESSION_ID), undefined); - assert.deepEqual(await store.readEvents(SESSION_ID), []); - } finally { - store.close(); - } - }); - }); - test('persists Scheduled Tasks and admits each fire once', async () => { await withRoot(async (root) => { const now = Date.now(); diff --git a/packages/storage/src/artifact-metadata-codec.ts b/packages/storage/src/artifact-metadata-codec.ts index 217833f97b..adabf324a5 100644 --- a/packages/storage/src/artifact-metadata-codec.ts +++ b/packages/storage/src/artifact-metadata-codec.ts @@ -27,7 +27,6 @@ import { isArtifactTurnKey, isCanonicalArtifactEntityId, } from '@maka/core/artifacts'; -import { isDeepResearchArtifactRole } from '@maka/core/deep-research-run'; const ARTIFACT_KIND_SET = new Set(ARTIFACT_KINDS); const ARTIFACT_SOURCE_SET = new Set(ARTIFACT_SOURCES); @@ -112,7 +111,6 @@ function decodeArtifactRecord(value: unknown, index: number): ArtifactRecord { value.sizeBytes < 0 || !isOptionalNonEmptyString(value.mimeType) || !isOptionalNonEmptyString(value.summary) || - (value.deepResearchRole !== undefined && !isDeepResearchArtifactRole(value.deepResearchRole)) || typeof value.source !== 'string' ) { throw invalidMetadataRecord(index); @@ -122,7 +120,10 @@ function decodeArtifactRecord(value: unknown, index: number): ArtifactRecord { if (value.relativePath !== `${value.sessionId}/${value.id}-${value.name}`) { throw invalidMetadataRecord(index); } - return value as unknown as ArtifactRecord; + // Old reports carry a workflow role that no longer has a consumer. + const record = { ...value }; + delete record.deepResearchRole; + return record as unknown as ArtifactRecord; } function isCompatibleArtifactName(name: string): boolean { diff --git a/packages/storage/src/artifact-store.ts b/packages/storage/src/artifact-store.ts index 7e2976674a..e52e119f11 100644 --- a/packages/storage/src/artifact-store.ts +++ b/packages/storage/src/artifact-store.ts @@ -47,10 +47,6 @@ import { isArtifactTurnKey, isCanonicalArtifactEntityId, } from '@maka/core/artifacts'; -import { - isDeepResearchArtifactRole, - type DeepResearchArtifactRole, -} from '@maka/core/deep-research-run'; import { sniffAttachmentMimeType } from '@maka/core/attachments'; import { isSafeRelativeArtifactPath, @@ -106,7 +102,6 @@ export interface CreateArtifactInput { mimeType?: string; source: ArtifactSource; summary?: string; - deepResearchRole?: DeepResearchArtifactRole; now?: number; id?: string; } @@ -285,12 +280,6 @@ class SqliteArtifactStore implements ArtifactAuthorityStore { if (!ARTIFACT_SOURCE_SET.has(acceptedInput.source)) { throw new Error('Invalid Artifact source'); } - if ( - acceptedInput.deepResearchRole !== undefined && - !isDeepResearchArtifactRole(acceptedInput.deepResearchRole) - ) { - throw new Error('Invalid Artifact deep-research role'); - } if ( acceptedInput.now !== undefined && (!Number.isSafeInteger(acceptedInput.now) || acceptedInput.now < 0) @@ -325,9 +314,6 @@ class SqliteArtifactStore implements ArtifactAuthorityStore { ...(acceptedInput.mimeType ? { mimeType: acceptedInput.mimeType } : {}), source: acceptedInput.source, ...(acceptedInput.summary ? { summary: acceptedInput.summary } : {}), - ...(acceptedInput.deepResearchRole - ? { deepResearchRole: acceptedInput.deepResearchRole } - : {}), }, (targetPath) => writeFile(targetPath, acceptedInput.content, { flag: 'wx' }), ); @@ -620,7 +606,6 @@ class SqliteArtifactStore implements ArtifactAuthorityStore { existing.mimeType !== optionalCanonicalText(input.mimeType) || existing.source !== input.source || existing.summary !== optionalCanonicalText(input.summary) || - existing.deepResearchRole !== input.deepResearchRole || (input.now !== undefined && existing.createdAt !== input.now) ) { throw artifactReplayConflict(canonical.id); @@ -1172,8 +1157,7 @@ function sameArtifactRecord(a: ArtifactRecord, b: ArtifactRecord): boolean { a.sizeBytes === b.sizeBytes && a.mimeType === b.mimeType && a.source === b.source && - a.summary === b.summary && - a.deepResearchRole === b.deepResearchRole + a.summary === b.summary ); } diff --git a/packages/storage/src/deep-research-authority.ts b/packages/storage/src/deep-research-authority.ts deleted file mode 100644 index 72945ebd84..0000000000 --- a/packages/storage/src/deep-research-authority.ts +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import type { - DeepResearchArtifactRef, - DeepResearchChecklistItem, - DeepResearchCheckpoint, - DeepResearchChangedEvent, - DeepResearchEvent, - DeepResearchHandoff, - DeepResearchMutationContext, - DeepResearchRun, - DeepResearchScopeLevel, - DeepResearchStep, - DeepResearchStore, -} from '@maka/core/deep-research-run'; -import { - assertStorageRootLease, - runWithStorageRootLease, - StorageRootAuthorityError, - type StorageRootLease, -} from './root-authority.js'; -import { - createSqliteDeepResearchStore, - type SqliteDeepResearchStore, -} from './deep-research-store.js'; - -const writerBrand: unique symbol = Symbol('InteractiveDeepResearchStoreWriter'); -const writers = new WeakSet(); -const writerByLease = new WeakMap(); -const writerOpeningByLease = new WeakMap>(); - -export interface InteractiveDeepResearchStoreWriter extends DeepResearchStore { - readonly kind: 'interactive'; - readonly access: 'write'; - readonly [writerBrand]: true; - purgeSessionState(sessionId: string): Promise; - close(): void; -} - -export function authenticateInteractiveDeepResearchStoreWriter( - writer: InteractiveDeepResearchStoreWriter, -): InteractiveDeepResearchStoreWriter { - if (!writers.has(writer)) { - throw new StorageRootAuthorityError( - 'invalid_lease', - 'Expected an authentic interactive Deep Research Store writer', - ); - } - return writer; -} - -export async function openInteractiveDeepResearchStoreForWrite( - lease: StorageRootLease<'interactive', 'write'>, -): Promise { - await assertStorageRootLease(lease, 'interactive', 'write'); - const existing = writerByLease.get(lease); - if (existing) return existing; - const opening = writerOpeningByLease.get(lease); - if (opening) return opening; - - const pending = Promise.resolve().then(async () => { - let store: SqliteDeepResearchStore | undefined; - try { - store = await runWithStorageRootLease(lease, 'interactive', 'write', async (root) => { - const opened = createSqliteDeepResearchStore(root); - try { - await opened.ready(); - return opened; - } catch (error) { - opened.close(); - throw error; - } - }); - await assertStorageRootLease(lease, 'interactive', 'write'); - const recoveredExisting = writerByLease.get(lease); - if (recoveredExisting) { - store.close(); - return recoveredExisting; - } - const writer = createWriterFacade(lease, store); - writers.add(writer); - writerByLease.set(lease, writer); - return writer; - } catch (error) { - store?.close(); - throw error; - } - }); - writerOpeningByLease.set(lease, pending); - try { - return await pending; - } finally { - if (writerOpeningByLease.get(lease) === pending) writerOpeningByLease.delete(lease); - } -} - -function createWriterFacade( - lease: StorageRootLease<'interactive', 'write'>, - store: SqliteDeepResearchStore, -): InteractiveDeepResearchStoreWriter { - let closed = false; - const run = (operation: () => Promise): Promise => { - if (closed) { - return Promise.reject( - new StorageRootAuthorityError('invalid_lease', 'Deep Research Store writer is closed'), - ); - } - return runWithStorageRootLease(lease, 'interactive', 'write', operation); - }; - const writer: InteractiveDeepResearchStoreWriter = { - kind: 'interactive', - access: 'write', - [writerBrand]: true, - read: (sessionId) => run(() => store.read(sessionId)), - readEvents: (sessionId) => run(() => store.readEvents(sessionId)), - start: (sessionId, objective, scopeLevel, context) => - run(() => store.start(sessionId, objective, scopeLevel, cloneContext(context))), - recordArtifact: (sessionId, artifact, context) => - run(() => store.recordArtifact(sessionId, cloneArtifact(artifact), cloneContext(context))), - updateChecklist: (sessionId, item, context) => - run(() => store.updateChecklist(sessionId, cloneChecklist(item), cloneContext(context))), - recordStep: (sessionId, step, context) => - run(() => store.recordStep(sessionId, cloneStep(step), cloneContext(context))), - recordCheckpoint: (sessionId, checkpoint, context) => - run(() => - store.recordCheckpoint(sessionId, cloneCheckpoint(checkpoint), cloneContext(context)), - ), - complete: (sessionId, reportArtifactId, handoff, context) => - run(() => - store.complete(sessionId, reportArtifactId, cloneHandoff(handoff), cloneContext(context)), - ), - subscribe: (listener) => store.subscribe(listener), - purgeSessionState: (sessionId) => run(() => store.purgeSessionState(sessionId)), - close: () => { - if (closed) return; - closed = true; - if (writerByLease.get(lease) === writer) writerByLease.delete(lease); - writers.delete(writer); - store.close(); - }, - }; - return Object.freeze(writer); -} - -function cloneContext( - context: DeepResearchMutationContext | undefined, -): DeepResearchMutationContext | undefined { - return context ? { ...context } : undefined; -} - -function cloneArtifact(artifact: DeepResearchArtifactRef): DeepResearchArtifactRef { - return structuredClone(artifact); -} - -function cloneChecklist( - item: Omit, -): Omit { - return structuredClone(item); -} - -function cloneStep( - step: Omit, -): Omit { - return structuredClone(step); -} - -function cloneCheckpoint( - checkpoint: Omit, -): Omit { - return structuredClone(checkpoint); -} - -function cloneHandoff(handoff: DeepResearchHandoff): DeepResearchHandoff { - return structuredClone(handoff); -} - -export type { - DeepResearchChangedEvent, - DeepResearchEvent, - DeepResearchRun, - DeepResearchScopeLevel, -}; diff --git a/packages/storage/src/deep-research-store.ts b/packages/storage/src/deep-research-store.ts deleted file mode 100644 index 3075811057..0000000000 --- a/packages/storage/src/deep-research-store.ts +++ /dev/null @@ -1,511 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { randomUUID } from 'node:crypto'; -import { resolve } from 'node:path'; -import type { DatabaseSync } from 'node:sqlite'; -import { - isDeepResearchEvent, - isDeepResearchScopeLevel, - normalizeDeepResearchObjective, - projectDeepResearchEvents, - type DeepResearchArtifactRef, - type DeepResearchChecklistItem, - type DeepResearchChangedEvent, - type DeepResearchCheckpoint, - type DeepResearchEvent, - type DeepResearchEventRefs, - type DeepResearchHandoff, - type DeepResearchMutationContext, - type DeepResearchRun, - type DeepResearchScopeLevel, - type DeepResearchStep, - type DeepResearchStore, -} from '@maka/core/deep-research-run'; -import { assertSafeSessionId } from './session-store.js'; -import { chainWrite } from './write-queue.js'; -import { - acquireOperationalStateDatabase, - type OperationalStateDatabaseLease, -} from './operational-state-store.js'; - -export type { DeepResearchStore } from '@maka/core/deep-research-run'; - -export interface CreateDeepResearchStoreOptions { - newId?: () => string; - now?: () => number; -} - -export interface SqliteDeepResearchStore extends DeepResearchStore { - ready(): Promise; - purgeSessionState(sessionId: string): Promise; - close(): void; -} - -export type CreateSqliteDeepResearchStoreOptions = CreateDeepResearchStoreOptions; - -export function createSqliteDeepResearchStore( - workspaceRoot: string, - options: CreateSqliteDeepResearchStoreOptions = {}, -): SqliteDeepResearchStore { - return new SqliteDeepResearchStoreImpl( - workspaceRoot, - options.newId ?? randomUUID, - options.now ?? Date.now, - ); -} - -class SqliteDeepResearchStoreImpl implements SqliteDeepResearchStore { - readonly #lease: OperationalStateDatabaseLease; - private readonly writeQueues = new Map>(); - private readonly subscribers = new Set<(event: DeepResearchChangedEvent) => void>(); - - constructor( - workspaceRoot: string, - private readonly newId: () => string, - private readonly now: () => number, - ) { - this.#lease = acquireOperationalStateDatabase(resolve(workspaceRoot)); - } - - ready(): Promise { - return Promise.resolve(); - } - - close(): void { - this.#lease.close(); - } - - async read(sessionId: string): Promise { - const events = await this.readEvents(sessionId); - return this.project(events); - } - - async readEvents(sessionId: string): Promise { - assertSafeSessionId(sessionId); - return readSqliteDeepResearchEvents(this.#lease.database, sessionId); - } - - async purgeSessionState(sessionId: string): Promise { - assertSafeSessionId(sessionId); - await chainWrite(this.writeQueues, sessionId, async () => { - this.#lease.transaction('write', () => { - this.#lease.database - .prepare('DELETE FROM workflow_deep_research_events WHERE session_id = ?') - .run(sessionId); - }); - }); - } - - subscribe(listener: (event: DeepResearchChangedEvent) => void): () => void { - this.subscribers.add(listener); - return () => this.subscribers.delete(listener); - } - - async start( - sessionId: string, - objective: string, - scopeLevel: DeepResearchScopeLevel, - context: DeepResearchMutationContext = {}, - ): Promise { - const normalized = normalizeDeepResearchObjective(objective); - if (!normalized) throw new Error('Deep Research objective must be a non-empty bounded string'); - if (!isDeepResearchScopeLevel(scopeLevel)) throw new Error('Invalid Deep Research scope level'); - return this.mutate( - sessionId, - 'research_started', - context, - (events) => { - if (events.length > 0) throw new Error('Deep Research workspace is already initialized'); - const ts = this.now(); - return { - eventId: this.newId(), - type: 'research_started', - sessionId, - ts, - objective: normalized, - scopeLevel, - ...refsFromContext(context), - }; - }, - (event) => - event.type === 'research_started' && - event.objective === normalized && - event.scopeLevel === scopeLevel, - ); - } - - async recordArtifact( - sessionId: string, - artifact: DeepResearchArtifactRef, - context: DeepResearchMutationContext = {}, - ): Promise { - return this.mutate( - sessionId, - 'research_artifact_recorded', - context, - () => ({ - eventId: this.newId(), - type: 'research_artifact_recorded', - sessionId, - ts: this.now(), - artifact: { - ...artifact, - sourceArtifactIds: [...artifact.sourceArtifactIds], - }, - ...refsFromContext(context), - }), - (event) => - event.type === 'research_artifact_recorded' && sameArtifact(event.artifact, artifact), - ); - } - - async updateChecklist( - sessionId: string, - item: Omit, - context: DeepResearchMutationContext = {}, - ): Promise { - return this.mutate( - sessionId, - 'research_checklist_updated', - context, - (events) => { - const run = this.project(events); - const current = run?.checklist.find((candidate) => candidate.itemId === item.itemId); - if (!current) throw new Error(`Unknown Deep Research checklist item ${item.itemId}`); - return { - eventId: this.newId(), - type: 'research_checklist_updated', - sessionId, - ts: this.now(), - item: { - ...item, - title: current.title, - evidenceArtifactIds: [...item.evidenceArtifactIds], - updatedAt: this.now(), - }, - ...refsFromContext(context), - }; - }, - (event) => - event.type === 'research_checklist_updated' && - event.item.itemId === item.itemId && - event.item.status === item.status && - event.item.blockedReason === item.blockedReason && - sameStrings(event.item.evidenceArtifactIds, item.evidenceArtifactIds), - ); - } - - async recordStep( - sessionId: string, - step: Omit, - context: DeepResearchMutationContext = {}, - ): Promise { - return this.mutate( - sessionId, - 'research_step_recorded', - context, - () => ({ - eventId: this.newId(), - type: 'research_step_recorded', - sessionId, - ts: this.now(), - step: { - ...step, - stepId: this.newId(), - roots: [...step.roots], - keywords: [...step.keywords], - ignoredPaths: [...step.ignoredPaths], - evidenceArtifactIds: [...step.evidenceArtifactIds], - inspectedRefs: step.inspectedRefs.map((ref) => ({ ...ref })), - workerRunIds: [...step.workerRunIds], - createdAt: this.now(), - }, - ...refsFromContext(context), - }), - (event) => event.type === 'research_step_recorded' && sameStep(event.step, step), - ); - } - - async recordCheckpoint( - sessionId: string, - checkpoint: Omit, - context: DeepResearchMutationContext = {}, - ): Promise { - return this.mutate( - sessionId, - 'research_checkpoint_recorded', - context, - () => ({ - eventId: this.newId(), - type: 'research_checkpoint_recorded', - sessionId, - ts: this.now(), - checkpoint: { - ...checkpoint, - checkpointId: this.newId(), - createdAt: this.now(), - openQuestions: [...checkpoint.openQuestions], - nextSteps: [...checkpoint.nextSteps], - taskIds: [...checkpoint.taskIds], - artifactIds: [...checkpoint.artifactIds], - }, - ...refsFromContext(context), - }), - (event) => - event.type === 'research_checkpoint_recorded' && - sameCheckpoint(event.checkpoint, checkpoint), - ); - } - - async complete( - sessionId: string, - reportArtifactId: string, - handoff: DeepResearchHandoff, - context: DeepResearchMutationContext = {}, - ): Promise { - return this.mutate( - sessionId, - 'research_completed', - context, - () => ({ - eventId: this.newId(), - type: 'research_completed', - sessionId, - ts: this.now(), - reportArtifactId, - handoff: { - ...handoff, - implementationTasks: [...handoff.implementationTasks], - recommendedIssues: [...handoff.recommendedIssues], - recommendedPullRequests: [...handoff.recommendedPullRequests], - verificationCommands: [...handoff.verificationCommands], - }, - ...refsFromContext(context), - }), - (event) => - event.type === 'research_completed' && - event.reportArtifactId === reportArtifactId && - sameHandoff(event.handoff, handoff), - ); - } - - private async mutate( - sessionId: string, - expectedType: DeepResearchEvent['type'], - context: DeepResearchMutationContext, - buildEvent: (events: readonly DeepResearchEvent[]) => DeepResearchEvent, - replayMatches?: (event: DeepResearchEvent) => boolean, - ): Promise { - assertSafeSessionId(sessionId); - let nextRun: DeepResearchRun | undefined; - await chainWrite(this.writeQueues, sessionId, async () => { - const current = await this.readEvents(sessionId); - if (context.toolCallId) { - const replay = current.find((event) => event.refs?.toolCallId === context.toolCallId); - if (replay) { - if (replay.type !== expectedType) { - throw new Error( - `Deep Research tool call ${context.toolCallId} was already used for ${replay.type}`, - ); - } - if (replayMatches && !replayMatches(replay)) { - throw new Error( - `Deep Research tool call ${context.toolCallId} was retried with different input`, - ); - } - nextRun = this.project(current); - return; - } - } - const event = buildEvent(current); - if (!isDeepResearchEvent(event)) { - throw new Error('Invalid Deep Research mutation event'); - } - const next = projectDeepResearchEvents([...current, event]); - if (next.diagnostics.length > 0 || !next.run) { - throw new Error( - `Deep Research mutation rejected: ${next.diagnostics.join('; ') || 'missing run projection'}`, - ); - } - await this.appendEvent(sessionId, event); - nextRun = next.run; - const changed = { sessionId, ts: event.ts }; - for (const subscriber of this.subscribers) { - try { - subscriber(changed); - } catch { - // Durable mutation success must not depend on a best-effort UI subscriber. - } - } - }); - if (!nextRun) throw new Error('Deep Research mutation did not produce a run'); - return nextRun; - } - - private project(events: readonly DeepResearchEvent[]): DeepResearchRun | undefined { - const projection = projectDeepResearchEvents(events); - if (projection.diagnostics.length > 0) { - throw new Error( - `Deep Research ledger projection failed: ${projection.diagnostics.join('; ')}`, - ); - } - return projection.run; - } - - private async appendEvent(sessionId: string, event: DeepResearchEvent): Promise { - this.#lease.transaction('write', () => { - insertDeepResearchEvent(this.#lease.database, sessionId, event); - }); - } -} - -function readSqliteDeepResearchEvents( - database: DatabaseSync, - sessionId: string, -): DeepResearchEvent[] { - assertSafeSessionId(sessionId); - const rows = database - .prepare(` - SELECT record_json - FROM workflow_deep_research_events - WHERE session_id = ? - ORDER BY sequence - `) - .all(sessionId) as Array<{ record_json?: unknown }>; - return rows.map((row, index) => { - if (typeof row.record_json !== 'string') { - throw new Error(`Invalid SQLite Deep Research event at sequence ${index}`); - } - const parsed = JSON.parse(row.record_json); - if (!isDeepResearchEvent(parsed) || parsed.sessionId !== sessionId) { - throw new Error(`Invalid SQLite Deep Research event at sequence ${index}`); - } - return parsed; - }); -} - -function insertDeepResearchEvent( - database: DatabaseSync, - sessionId: string, - event: DeepResearchEvent, -): void { - const row = database - .prepare(` - SELECT COALESCE(MAX(sequence), -1) + 1 AS sequence - FROM workflow_deep_research_events - WHERE session_id = ? - `) - .get(sessionId) as { sequence?: unknown }; - if (typeof row.sequence !== 'number' || !Number.isSafeInteger(row.sequence)) { - throw new Error('Invalid next Deep Research event sequence'); - } - database - .prepare(` - INSERT INTO workflow_deep_research_events( - session_id, sequence, event_id, record_json - ) VALUES (?, ?, ?, ?) - `) - .run(sessionId, row.sequence, event.eventId, JSON.stringify(event)); -} - -function refsFromContext(context: DeepResearchMutationContext): { refs?: DeepResearchEventRefs } { - const refs: DeepResearchEventRefs = { - ...(context.runId ? { runId: context.runId } : {}), - ...(context.turnId ? { turnId: context.turnId } : {}), - ...(context.toolCallId ? { toolCallId: context.toolCallId } : {}), - }; - return Object.keys(refs).length > 0 ? { refs } : {}; -} - -function sameStrings(left: readonly string[], right: readonly string[]): boolean { - return left.length === right.length && left.every((value, index) => value === right[index]); -} - -function sameArtifact(left: DeepResearchArtifactRef, right: DeepResearchArtifactRef): boolean { - return ( - left.artifactId === right.artifactId && - left.role === right.role && - left.name === right.name && - left.summary === right.summary && - left.createdAt === right.createdAt && - left.locator === right.locator && - left.contentHash === right.contentHash && - left.reportSectionKey === right.reportSectionKey && - left.reportSectionStatus === right.reportSectionStatus && - sameStrings(left.sourceArtifactIds, right.sourceArtifactIds) - ); -} - -function sameStep( - left: DeepResearchStep, - right: Omit, -): boolean { - return ( - left.kind === right.kind && - left.status === right.status && - left.objective === right.objective && - left.summary === right.summary && - left.stoppingCondition === right.stoppingCondition && - left.expectedEvidence === right.expectedEvidence && - left.blockedReason === right.blockedReason && - sameStrings(left.roots, right.roots) && - sameStrings(left.keywords, right.keywords) && - sameStrings(left.ignoredPaths, right.ignoredPaths) && - sameStrings(left.evidenceArtifactIds, right.evidenceArtifactIds) && - sameStrings(left.workerRunIds, right.workerRunIds) && - left.inspectedRefs.length === right.inspectedRefs.length && - left.inspectedRefs.every((ref, index) => { - const candidate = right.inspectedRefs[index]; - return ( - candidate !== undefined && - ref.kind === candidate.kind && - ref.locator === candidate.locator && - ref.label === candidate.label && - ref.sourceArtifactId === candidate.sourceArtifactId - ); - }) - ); -} - -function sameCheckpoint( - left: DeepResearchCheckpoint, - right: Omit, -): boolean { - return ( - left.round === right.round && - left.stage === right.stage && - left.status === right.status && - left.summary === right.summary && - sameStrings(left.openQuestions, right.openQuestions) && - sameStrings(left.nextSteps, right.nextSteps) && - sameStrings(left.taskIds, right.taskIds) && - sameStrings(left.artifactIds, right.artifactIds) - ); -} - -function sameHandoff(left: DeepResearchHandoff, right: DeepResearchHandoff): boolean { - return ( - left.artifactId === right.artifactId && - sameStrings(left.implementationTasks, right.implementationTasks) && - sameStrings(left.recommendedIssues, right.recommendedIssues) && - sameStrings(left.recommendedPullRequests, right.recommendedPullRequests) && - sameStrings(left.verificationCommands, right.verificationCommands) - ); -} diff --git a/packages/storage/src/sqlite-workflow-schema.ts b/packages/storage/src/sqlite-workflow-schema.ts index d0e1f27812..279d9529d7 100644 --- a/packages/storage/src/sqlite-workflow-schema.ts +++ b/packages/storage/src/sqlite-workflow-schema.ts @@ -62,15 +62,6 @@ export function migrateSqliteWorkflowDatabase(db: DatabaseSync): void { UNIQUE (session_id, store_version) ); - CREATE TABLE IF NOT EXISTS workflow_deep_research_events ( - session_id TEXT NOT NULL, - sequence INTEGER NOT NULL CHECK (sequence >= 0), - event_id TEXT NOT NULL, - record_json TEXT NOT NULL, - PRIMARY KEY (session_id, sequence), - UNIQUE (session_id, event_id) - ); - CREATE TABLE IF NOT EXISTS workflow_scheduled_tasks ( task_id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, diff --git a/packages/storage/src/storage-writer-composition.ts b/packages/storage/src/storage-writer-composition.ts index 66d108cb08..568b922a20 100644 --- a/packages/storage/src/storage-writer-composition.ts +++ b/packages/storage/src/storage-writer-composition.ts @@ -21,7 +21,6 @@ import { openInteractiveArtifactStoreForWrite } from './artifact-stores.js'; import type { ContextOffloadLimits } from '@maka/core/context-offload'; import { openInteractiveContextOffloadStoreForWrite } from './context-offload-store.js'; import { openInteractiveDailyReviewAuthorityForWrite } from './daily-review-authority.js'; -import { openInteractiveDeepResearchStoreForWrite } from './deep-research-authority.js'; import { openInteractiveExecutionStoresForWrite } from './execution-stores.js'; import type { ExecutionPersistenceProvider } from './execution-persistence-provider.js'; import type { InteractiveGoalAuthorityWriter } from './goal-authority.js'; @@ -53,7 +52,6 @@ export interface StorageWriterComposition { readonly runtimePolicy: Awaited>; readonly scheduledTasks: Awaited>; readonly plan: Awaited>; - readonly deepResearch: Awaited>; readonly dailyReview: Awaited>; readonly goal: InteractiveGoalAuthorityWriter; readonly memoryBundle: Awaited>; @@ -141,10 +139,6 @@ async function createComposition( closeWriter, ); const plan = await openWriter(() => openInteractivePlanStoreForWrite(lease), closeWriter); - const deepResearch = await openWriter( - () => openInteractiveDeepResearchStoreForWrite(lease), - closeWriter, - ); const dailyReview = await openWriter( () => openInteractiveDailyReviewAuthorityForWrite(lease), closeWriter, @@ -190,7 +184,6 @@ async function createComposition( runtimePolicy, scheduledTasks, plan, - deepResearch, dailyReview, goal, memoryBundle, diff --git a/packages/ui/src/chat-empty-hero.tsx b/packages/ui/src/chat-empty-hero.tsx index 30d503a7ad..a763791ea2 100644 --- a/packages/ui/src/chat-empty-hero.tsx +++ b/packages/ui/src/chat-empty-hero.tsx @@ -17,34 +17,6 @@ * under the License. */ -/** - * Empty-chat hero surfaces (`EmptyChatHero`, `DeepResearchEmptyHero`) - * + their locale-aware copy bundle + the time-of-day greeting helper. - * - * PR-UI-LIB-EXTRACT-8 (WAWQAQ msg `510fef52`, round 9/10): pulled - * out of `components.tsx`. `detectDayPeriod` and `DayPeriod` were - * already public (consumed by `apps/desktop/src/renderer/main.tsx` - * and three contract tests — `empty-hero-day-period`, - * `deep-research-visible-surface-contract`, and - * `visible-copy-hygiene-contract`); the two hero components and - * the locale copy bundle were panel-internal. byte-for-byte - * equivalent; behavior unchanged; `index.ts` re-exports this - * module so the `@maka/ui` public API surface stays identical. - * - * Why this seam: the empty-chat hero is the first thing every - * user sees on a fresh session. Its day-period boundary - * (5/11/14/18) is pinned by a contract test - * because e2e-fixture fixtures freeze `Date.now()` but not the - * `Date` constructor — getting this wrong silently drifts the - * rendered greeting. The DeepResearch variant is also where the read-only - * deep-research workflow rules live. Both deserve their own - * surface so the boundary rules sit next to the surface they - * govern, not buried in a 7000-line file. - */ - -import { ICON_SIZE, Sparkles } from './icons.js'; -import { Item } from '@astryxdesign/core/Item'; - import { MakaWordmark } from './maka-wordmark.js'; import { useUiLocale } from './locale-context.js'; import { getConversationCopy, type DayPeriod } from './conversation-copy.js'; @@ -86,7 +58,7 @@ export function EmptyChatHero(props: { // card, without a grid of starter chips competing for the first // viewport. `onPromptSuggestion` stays in the signature for callers // that still pass it, but the generic empty-chat surface no longer - // renders suggestions; Deep Research keeps its specialized starters. + // renders suggestions. const label = props.userLabel?.trim(); const locale = useUiLocale(); const copy = getConversationCopy(locale).empty; @@ -120,88 +92,3 @@ export function EmptyChatHero(props: { ); } - -export function DeepResearchEmptyHero(props: { onPromptSuggestion?(prompt: string): void }) { - const copy = getConversationCopy(useUiLocale()).deepResearchEmpty; - return ( -
-
- - -

{copy.title}

-

{copy.intro}

-
-
    - {copy.workflow.map((step) => ( -
  1. - {step.title} - {step.body} -
  2. - ))} -
-
-

{copy.reportTitle}

-
    - {copy.report.map((section) => ( -
  • - {section.title} - {section.body} -
  • - ))} -
-
-
-

{copy.scopeTitle}

-
    - {copy.scope.map((option) => ( -
  • - {option.label} - {option.body} -
  • - ))} -
-
-
-

{copy.evidenceTitle}

-
    - {copy.evidence.map((item) => ( -
  • - {item.title} - {item.body} -
  • - ))} -
-
-
-

{copy.progressTitle}

-
    - {copy.progress.map((item) => ( -
  • - {item.title} - {item.body} -
  • - ))} -
-
- {props.onPromptSuggestion && ( -
    - {copy.starters.map((suggestion) => ( -
  • - props.onPromptSuggestion?.(suggestion.prompt)} - /> -
  • - ))} -
- )} -
- ); -} diff --git a/packages/ui/src/chat-view.tsx b/packages/ui/src/chat-view.tsx index 50f9e03485..33a1351ad6 100644 --- a/packages/ui/src/chat-view.tsx +++ b/packages/ui/src/chat-view.tsx @@ -33,9 +33,8 @@ import { Virtualizer, type CustomContainerComponentProps, type VirtualizerHandle import { ICON_SIZE, AlertTriangle, - ArrowRight, } from './icons.js'; -import { DeepResearchEmptyHero, EmptyChatHero } from './chat-empty-hero.js'; +import { EmptyChatHero } from './chat-empty-hero.js'; import type { ChatModelChoice } from './chat-model-helpers.js'; import { mergePromptAnchorRailTurns, @@ -43,7 +42,6 @@ import { type PromptAnchorRailTurn, } from './prompt-anchor-rail.js'; import { useMessageSelectionQuote } from './use-message-selection-quote.js'; -import type { DeepResearchClientProgress } from '@maka/core/deep-research-run'; import type { ProviderType } from '@maka/core/llm-connections'; import type { SessionSummary, StoredMessage } from '@maka/core/session'; import type { @@ -52,7 +50,6 @@ import type { QuoteRef, ShellRunUpdate, } from '@maka/core/events'; -import { isDeepResearchSession } from '@maka/core/deep-research'; import { Button, ButtonGroup, ChatMessageList, EmptyState, HStack, Spinner, Text } from '@astryxdesign/core'; import { useChatLayoutContext } from '@astryxdesign/core/Chat'; import { useLayer } from '@astryxdesign/core/Layer'; @@ -184,10 +181,6 @@ export function ChatView(props: { */ activeTurn?: { readonly turnId: string; readonly awaitingInput?: boolean; readonly compacting?: boolean }; activeSession?: SessionSummary; - /** Durable Deep Research projection supplied by the host for visible progress and resume state. */ - deepResearchRun?: DeepResearchClientProgress; - /** Explicitly starts a normal implementation task from a completed read-only research run. */ - onContinueDeepResearchHandoff?(run: DeepResearchClientProgress): void; activeConnectionLabel?: string; activeModel?: string; activeModelLabel?: string; @@ -652,7 +645,7 @@ export function ChatView(props: { left controls so the new-session screen and active-session screen share the same "create / pick mode / send" rhythm. */} {/* No status strip on the empty-session screen: it has no session, so - none of the chips (memory / deep-research / goal) can apply. The + none of the chips (memory / goal) can apply. The header used to be rendered here anyway, holding a lone spacer, to occupy the window titlebar line — which the shell's titlebar row now owns. */} @@ -700,8 +693,6 @@ export function ChatView(props: { ); } - - const deepResearchActive = isDeepResearchSession(props.activeSession.labels); const hasVisibleConversationItem = conversationItemPlacement.byTurn.size > 0 || conversationItemPlacement.orphan !== undefined; const showEmptyState = @@ -735,11 +726,7 @@ export function ChatView(props: { /> ) : props.emptyOverride ?? ( - deepResearchActive ? ( - - ) : ( - - ) + ); /** * Nothing to show is exactly when this matters most: WorkHub filters the @@ -781,7 +768,6 @@ export function ChatView(props: { onRevisionNavigate={props.onRevisionNavigate} memoryActive={props.memoryActive} onOpenMemorySettings={props.onOpenMemorySettings} - deepResearchActive={deepResearchActive} goal={props.goalIndicator} actions={hasConversationHeaderActions ? ( ) : undefined} /> - {deepResearchActive && props.deepResearchRun && ( - - )}
{/* ChatSurfaceLayout hosts the rail outside bounded transcript columns. */} ): { }, [scrollRef]); return held; } - -export function DeepResearchProgressPanel({ - run, - onContinue, - copy, -}: { - run: DeepResearchClientProgress; - onContinue?: (run: DeepResearchClientProgress) => void; - copy: ReturnType['chat']['deepResearchProgress']; -}) { - const completedItems = run.checklist.filter( - (item) => item.status === 'completed' || item.status === 'skipped', - ).length; - - return ( -
-
-
- {copy.title} - - {run.status === 'completed' - ? copy.completedSummary - : copy.activeSummary(run.stage, run.scopeLevel, run.round)} - -
-
- - {completedItems}/{run.checklist.length} - - {run.status === 'completed' && run.implementationPrompt && onContinue && ( -
-
-
-
-

{copy.checklistTitle}

-
    - {run.checklist.map((item) => ( -
  • - {item.status === 'completed' ? '✓' : item.status === 'blocked' ? '!' : '·'} - {item.title} -
  • - ))} -
-
-
-

{copy.reportTitle}

-
    - {run.reportSections.map((section) => ( -
  • - {section.status === 'completed' ? '✓' : section.status === 'drafted' ? '◐' : '·'} - {copy.sectionLabels[section.key]} -
  • - ))} -
-
-
-

{copy.inspectedTitle}

- {run.recentInspectedRefs.length > 0 ? ( -
    - {run.recentInspectedRefs.map((ref, index) => ( -
  • - {ref.kind} - {ref.locator} -
  • - ))} -
- ) :

{copy.inspectedEmpty}

} -
-
-

{copy.executionTitle}

-

{copy.executionSummary(run.stepsCount, run.artifactsCount)}

- {run.workerRunIds.length > 0 &&

{copy.workersLabel}: {run.workerRunIds.join(', ')}

} - {run.blockers.length > 0 ? ( -
    - {run.blockers.map((blocker) =>
  • {blocker}
  • )} -
- ) :

{copy.noBlockers}

} -
-
-
- ); -} /** * Locale-aware copy bundle for the empty-chat hero. Mirrors the * locale split applied to `PROMPT_SUGGESTIONS_BY_LOCALE` (PR-UI-14) diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts index ba48e08572..e0c9393b20 100644 --- a/packages/ui/src/conversation-copy.ts +++ b/packages/ui/src/conversation-copy.ts @@ -17,25 +17,13 @@ * under the License. */ -import type { DeepResearchReportSectionKey } from '@maka/core/deep-research-run'; import type { ProviderRetryReason } from '@maka/core/events'; import type { PermissionMode } from '@maka/core/permission'; import type { SessionBlockedReason, SessionStatus } from '@maka/core/session'; import type { ThinkingLevel } from '@maka/core/model-thinking'; import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; -import { - DEEP_RESEARCH_EVIDENCE_CHECKLIST, - DEEP_RESEARCH_PROGRESS_CHECKPOINTS, - DEEP_RESEARCH_REPORT_SECTIONS, - DEEP_RESEARCH_SCOPE_OPTIONS, - DEEP_RESEARCH_STARTER_PROMPTS, - DEEP_RESEARCH_WORKFLOW_STEPS, -} from '@maka/core/deep-research'; export type DayPeriod = 'morning' | 'noon' | 'afternoon' | 'evening'; -type ResearchItem = Readonly<{ title: string; body: string }>; -type ResearchOption = Readonly<{ label: string; body: string }>; -type ResearchStarter = Readonly<{ label: string; prompt: string }>; /** Compact token count: 999 → "999", 45,200 → "45.2k", 128,000 → "128k", 1,048,576 → "1M". */ function formatCompactTokenCount(count: number): string { @@ -96,28 +84,6 @@ export interface ConversationCopy { headlineWithLabel: (greeting: string, label: string) => string; headlineFallback: (greeting: string, tail: string) => string; }; - deepResearchEmpty: { - ariaLabel: string; - eyebrow: string; - title: string; - intro: string; - workflowAriaLabel: string; - workflow: readonly ResearchItem[]; - reportAriaLabel: string; - reportTitle: string; - report: readonly ResearchItem[]; - scopeAriaLabel: string; - scopeTitle: string; - scope: readonly ResearchOption[]; - evidenceAriaLabel: string; - evidenceTitle: string; - evidence: readonly ResearchItem[]; - progressAriaLabel: string; - progressTitle: string; - progress: readonly ResearchItem[]; - startersAriaLabel: string; - starters: readonly ResearchStarter[]; - }; composer: { placeholder: string; textareaAriaLabel: string; @@ -382,26 +348,6 @@ export interface ConversationCopy { memory: string; memoryAriaLabel: string; memoryTitle: string; - deepResearch: string; - deepResearchAriaLabel: string; - deepResearchTitle: string; - deepResearchProgress: { - ariaLabel: string; - title: string; - completedSummary: string; - activeSummary: (stage: string, scope: string, round: number) => string; - handoffTitle: string; - handoffAction: string; - checklistTitle: string; - reportTitle: string; - inspectedTitle: string; - inspectedEmpty: string; - executionTitle: string; - executionSummary: (steps: number, artifacts: number) => string; - workersLabel: string; - noBlockers: string; - sectionLabels: Record; - }; clearGoal: (condition: string, iteration: number, max: number, status: string) => string; clearGoalAriaLabel: (iteration: number, max: number) => string; goalProgress: (iteration: number, max: number) => string; @@ -508,15 +454,6 @@ const CONVERSATION_COPY = { greetingTail: { morning: '清醒的早晨适合理清思路', noon: '专注的午间适合一鼓作气', afternoon: '舒缓的下午适合慢慢推进', evening: '安静的夜晚适合深度思考' }, headlineWithLabel: (greeting, label) => `${greeting} ${label},今天想做点什么?`, headlineFallback: (greeting, tail) => `${greeting},${tail}。`, }, - deepResearchEmpty: { - ariaLabel: '深度研究空任务', eyebrow: '深度研究 · 只读探索', title: '先把项目读透,再决定怎么改。', intro: '这个任务固定在只读权限:优先阅读、搜索和分析代码;需要动手实现时,先输出文件、风险和验证命令。', - workflowAriaLabel: '深度研究流程', workflow: DEEP_RESEARCH_WORKFLOW_STEPS, - reportAriaLabel: '深度研究输出结构', reportTitle: '输出必须能直接落地', report: DEEP_RESEARCH_REPORT_SECTIONS, - scopeAriaLabel: '深度研究范围', scopeTitle: '默认按标准深度研究', scope: DEEP_RESEARCH_SCOPE_OPTIONS, - evidenceAriaLabel: '深度研究证据清单', evidenceTitle: '每次研究都要留证据', evidence: DEEP_RESEARCH_EVIDENCE_CHECKLIST, - progressAriaLabel: '深度研究检查点', progressTitle: '多步研究要按检查点推进', progress: DEEP_RESEARCH_PROGRESS_CHECKPOINTS, - startersAriaLabel: '深度研究起手式', starters: DEEP_RESEARCH_STARTER_PROMPTS, - }, composer: { placeholder: '描述任务,@ 引用文件或会话,/ 选择技能…', textareaAriaLabel: '消息输入框', pastedQuoteLabel: '粘贴的文本', selectedSkillsAriaLabel: '已选择的 Skill', removeSkillAriaLabel: (name) => `移除 Skill:${name}`, awaitingPermission: '等待你确认权限…', sending: '正在发送…', importing: '正在导入…', sendLabel: '发送', @@ -621,30 +558,7 @@ const CONVERSATION_COPY = { }, chat: { conversationAriaLabel: (name) => `对话:${name}`, - memory: '记忆', memoryAriaLabel: '本地记忆已启用', memoryTitle: '本地 MEMORY.md 已加入 agent 系统提示。点击进入设置 · 记忆管理。', deepResearch: '深度研究', deepResearchAriaLabel: '深度研究,只读探索', deepResearchTitle: '深度研究任务使用只读探索边界:先阅读和分析,默认不改文件。', - deepResearchProgress: { - ariaLabel: '深度研究实时进度', - title: '研究进度', - completedSummary: '研究完成 · 原任务保持只读', - activeSummary: (stage, scope, round) => `${stage} · ${scope} · 第 ${round} 轮`, - handoffTitle: '新建普通任务并填入研究 handoff;不会自动发送,也不会改变原研究任务权限', - handoffAction: '在新任务中继续实现', - checklistTitle: '检查清单', - reportTitle: '报告草稿', - inspectedTitle: '已检查位置', - inspectedEmpty: '等待记录文件、符号或来源。', - executionTitle: '执行与阻塞', - executionSummary: (steps, artifacts) => `${steps} 个研究步骤 · ${artifacts} 个持久化证据`, - workersLabel: 'Workers', - noBlockers: '当前无阻塞。', - sectionLabels: { - conclusion: '结论', - source_evidence: '证据', - borrow_diverge_risk_gate: '取舍与风险', - implementation_recommendations: '实施建议', - verification: '验证', - }, - }, + memory: '记忆', memoryAriaLabel: '本地记忆已启用', memoryTitle: '本地 MEMORY.md 已加入 agent 系统提示。点击进入设置 · 记忆管理。', clearGoal: (condition, iteration, max, status) => `自主执行目标进行中:「${condition}」(第 ${iteration}/${max} 轮,${status})。系统每轮后自动续行;点击可清除目标、停止续行。`, clearGoalAriaLabel: (iteration, max) => `清除自主执行目标(已进行 ${iteration}/${max} 轮)`, goalProgress: (iteration, max) => `目标 ${iteration} / ${max}`, goalRunningAriaLabel: '自主目标正在运行', goalWaitingAriaLabel: '自主目标正在等待条件变化', goalPausedAriaLabel: '自主目标已暂停', pauseGoalAriaLabel: (iteration, max) => `暂停自主执行目标(已进行 ${iteration}/${max} 轮)`, resumeGoalAriaLabel: (iteration, max) => `恢复自主执行目标(已进行 ${iteration}/${max} 轮)`, pauseGoal: (condition, iteration, max, status) => `暂停自主执行目标:「${condition}」(第 ${iteration}/${max} 轮,${status})。暂停后立即停止自动续行,不再消耗令牌;可随时恢复。`, resumeGoal: (condition, iteration, max) => `恢复自主执行目标:「${condition}」(第 ${iteration}/${max} 轮)。恢复后立即继续自动续行。`, goalElapsed: (elapsedMs) => formatGoalElapsedUnits(elapsedMs, { second: ' 秒', minute: ' 分钟', hour: ' 小时', day: ' 天' }), goalTokens: (spent, budget) => `${formatCompactTokenCount(spent)} / ${formatCompactTokenCount(budget)}`, loadFailed: '任务载入失败', loading: '载入中…', retryLoad: '重试载入', loadEarlierHistory: '载入更早的记录', quoteSelection: '引用', askInSidePanel: '在侧栏追问', noMessages: '暂无消息', @@ -667,15 +581,6 @@ const CONVERSATION_COPY = { greetingTail: { morning: '清醒的早晨適合理清思路', noon: '專注的午間適合一鼓作氣', afternoon: '舒緩的下午適合慢慢推進', evening: '安靜的夜晚適合深度思考' }, headlineWithLabel: (greeting, label) => `${greeting} ${label},今天想做點什麼?`, headlineFallback: (greeting, tail) => `${greeting},${tail}。`, }, - deepResearchEmpty: { - ariaLabel: '深度研究空任務', eyebrow: '深度研究 · 只讀探索', title: '先把專案讀透,再決定怎麼改。', intro: '這個任務固定在只讀權限:優先閱讀、搜尋和分析程式碼;需要動手實現時,先輸出檔案、風險和驗證命令。', - workflowAriaLabel: '深度研究流程', workflow: DEEP_RESEARCH_WORKFLOW_STEPS, - reportAriaLabel: '深度研究輸出結構', reportTitle: '輸出必須能直接落地', report: DEEP_RESEARCH_REPORT_SECTIONS, - scopeAriaLabel: '深度研究範圍', scopeTitle: '預設按標準深度研究', scope: DEEP_RESEARCH_SCOPE_OPTIONS, - evidenceAriaLabel: '深度研究證據清單', evidenceTitle: '每次研究都要留證據', evidence: DEEP_RESEARCH_EVIDENCE_CHECKLIST, - progressAriaLabel: '深度研究檢查點', progressTitle: '多步研究要按檢查點推進', progress: DEEP_RESEARCH_PROGRESS_CHECKPOINTS, - startersAriaLabel: '深度研究起手式', starters: DEEP_RESEARCH_STARTER_PROMPTS, - }, composer: { placeholder: '描述任務,@ 引用檔案,/ 選擇技能…', textareaAriaLabel: '訊息輸入框', pastedQuoteLabel: '貼上的文本', selectedSkillsAriaLabel: '已選擇的 Skill', removeSkillAriaLabel: (name) => `移除 Skill:${name}`, awaitingPermission: '等待你確認權限…', sending: '正在傳送…', importing: '正在匯入…', sendLabel: '傳送', @@ -780,30 +685,7 @@ const CONVERSATION_COPY = { }, chat: { conversationAriaLabel: (name) => `對話:${name}`, - memory: '記憶', memoryAriaLabel: '本地記憶已啟用', memoryTitle: '本地 MEMORY.md 已加入 agent 系統提示。點選進入設定 · 記憶管理。', deepResearch: '深度研究', deepResearchAriaLabel: '深度研究,只讀探索', deepResearchTitle: '深度研究任務使用只讀探索邊界:先閱讀和分析,預設不改檔案。', - deepResearchProgress: { - ariaLabel: '深度研究即時進度', - title: '研究進度', - completedSummary: '研究完成 · 原任務保持只讀', - activeSummary: (stage, scope, round) => `${stage} · ${scope} · 第 ${round} 輪`, - handoffTitle: '建立普通任務並填入研究 handoff;不會自動傳送,也不會改變原研究任務權限', - handoffAction: '在新任務中繼續實現', - checklistTitle: '檢查清單', - reportTitle: '報告草稿', - inspectedTitle: '已檢查位置', - inspectedEmpty: '等待記錄檔案、符號或來源。', - executionTitle: '執行與阻塞', - executionSummary: (steps, artifacts) => `${steps} 個研究步驟 · ${artifacts} 個持久化證據`, - workersLabel: 'Workers', - noBlockers: '目前無阻塞。', - sectionLabels: { - conclusion: '結論', - source_evidence: '證據', - borrow_diverge_risk_gate: '取捨與風險', - implementation_recommendations: '實施建議', - verification: '驗證', - }, - }, + memory: '記憶', memoryAriaLabel: '本地記憶已啟用', memoryTitle: '本地 MEMORY.md 已加入 agent 系統提示。點選進入設定 · 記憶管理。', clearGoal: (condition, iteration, max, status) => `自主執行目標進行中:「${condition}」(第 ${iteration}/${max} 輪,${status})。系統每輪後自動續行;點選可清除目標、停止續行。`, clearGoalAriaLabel: (iteration, max) => `清除自主執行目標(已進行 ${iteration}/${max} 輪)`, goalProgress: (iteration, max) => `目標 ${iteration} / ${max}`, goalRunningAriaLabel: '自主目標正在執行', goalWaitingAriaLabel: '自主目標正在等待條件變化', goalPausedAriaLabel: '自主目標已暫停', pauseGoalAriaLabel: (iteration, max) => `暫停自主執行目標(已進行 ${iteration}/${max} 輪)`, resumeGoalAriaLabel: (iteration, max) => `恢復自主執行目標(已進行 ${iteration}/${max} 輪)`, pauseGoal: (condition, iteration, max, status) => `暫停自主執行目標:「${condition}」(第 ${iteration}/${max} 輪,${status})。暫停後立即停止自動續行,不再消耗權杖;可隨時恢復。`, resumeGoal: (condition, iteration, max) => `恢復自主執行目標:「${condition}」(第 ${iteration}/${max} 輪)。恢復後立即繼續自動續行。`, goalElapsed: (elapsedMs) => formatGoalElapsedUnits(elapsedMs, { second: ' 秒', minute: ' 分鐘', hour: ' 小時', day: ' 天' }), goalTokens: (spent, budget) => `${formatCompactTokenCount(spent)} / ${formatCompactTokenCount(budget)}`, loadFailed: '任務載入失敗', loading: '載入中…', retryLoad: '重試載入', loadEarlierHistory: '載入更早的記錄', quoteSelection: '引用', askInSidePanel: '在側欄追問', noMessages: '暫無訊息', @@ -826,44 +708,6 @@ const CONVERSATION_COPY = { greetingTail: { morning: 'A clear morning is good for untangling ideas', noon: 'A focused midday is good for a single big push', afternoon: 'A calm afternoon is good for steady progress', evening: 'A quiet evening is good for deep thinking' }, headlineWithLabel: (greeting, label) => `${greeting} ${label} — what shall we tackle today?`, headlineFallback: (greeting, tail) => `${greeting} — ${tail}.`, }, - deepResearchEmpty: { - ariaLabel: 'Empty Deep Research task', eyebrow: 'Deep Research · Read-only exploration', title: 'Understand the project before deciding what to change.', intro: 'This task stays read only: inspect, search, and analyze first. When implementation is needed, report the files, risks, and verification commands.', - workflowAriaLabel: 'Deep Research workflow', workflow: [ - { title: 'Find the entry points', body: 'Read the directory layout, configuration, startup path, and test entry points to build a project map.' }, - { title: 'Trace the data flow', body: 'Follow key modules through IPC, storage, permissions, and runtime boundaries to the real implementation.' }, - { title: 'Compare references', body: 'Break each reusable idea into borrow / diverge / risk / gate.' }, - { title: 'Propose a mergeable plan', body: 'List files, risk boundaries, and verification commands without changing files in read-only mode.' }, - ], - reportAriaLabel: 'Deep Research report structure', reportTitle: 'The report must be actionable', report: [ - { title: 'Lead with conclusions', body: 'Use three to five points to explain the current state, major gaps, and priorities.' }, - { title: 'Cite source evidence', body: 'Name files, functions, configuration, tests, and runtime paths instead of relying on impressions.' }, - { title: 'Break down what to borrow', body: 'Describe each idea as borrow / diverge / risk / gate.' }, - { title: 'Make it implementable', body: 'Give a small-step file plan, boundaries, and verification commands.' }, - ], - scopeAriaLabel: 'Deep Research scope', scopeTitle: 'Standard depth by default', scope: [ - { label: 'Quick', body: 'Scan entry points, key files, and the likeliest data flow for a narrowly scoped question.' }, - { label: 'Standard', body: 'Trace the core path, related tests, and major risks before recommending changes.' }, - { label: 'Deep', body: 'Run multi-pass investigation across modules, references, and edge cases only when explicitly requested.' }, - ], - evidenceAriaLabel: 'Deep Research evidence checklist', evidenceTitle: 'Leave evidence for every investigation', evidence: [ - { title: 'Project entry points', body: 'Check the README, package/config files, startup scripts, and directory layers to confirm how the project runs.' }, - { title: 'Core path', body: 'Trace UI entry points, IPC/services, storage, runtime calls, and error handling.' }, - { title: 'Boundaries', body: 'Check permissions, privacy mode, token/path exposure, retries, and user-visible feedback.' }, - { title: 'Verification evidence', body: 'Find tests, fixtures, smoke documentation, and reproducible commands; call out missing evidence.' }, - ], - progressAriaLabel: 'Deep Research checkpoints', progressTitle: 'Advance multi-step research through checkpoints', progress: [ - { title: 'Build a checklist', body: 'When the scope has more than three related areas, list verifiable checks before tracing code.' }, - { title: 'Mark the current check', body: 'State what is being verified and move on only after collecting evidence.' }, - { title: 'Record blockers', body: 'Mark missing source, runtime, or test evidence as blocked instead of guessing.' }, - { title: 'Converge on a plan', body: 'Roll completed checks into borrow / diverge / risk / gate and actionable improvements.' }, - ], - startersAriaLabel: 'Deep Research starters', starters: [ - { label: 'Research a reference project', prompt: 'Read this project without changing files. Map its structure, core modules, startup path, data flow, and tests; then list reusable design ideas, risks, and an implementation order for Maka.' }, - { label: 'Read a reference project end to end', prompt: 'Perform a deep, read-only study of this project. Map modules and trace core features, runtime, storage, permissions, UI, tests, and docs. Express each idea as borrow / diverge / risk / gate and recommend an implementation order for Maka.' }, - { label: 'Compare a feature implementation', prompt: 'Compare this feature in the reference project and Maka without changing files. Identify key files, runtime boundaries, UI entry points, persistence, tests, and the smallest mergeable improvement.' }, - { label: 'Audit security boundaries', prompt: 'Audit this feature read only: permissions, token and secret flow, IPC/renderer exposure, file paths, privacy mode, logs, and telemetry. Report blocking risks and corresponding contract tests.' }, - ], - }, composer: { placeholder: 'Describe a task, @ to reference files or sessions, / for skills…', textareaAriaLabel: 'Message input', pastedQuoteLabel: 'Pasted text', selectedSkillsAriaLabel: 'Selected Skills', removeSkillAriaLabel: (name) => `Remove Skill: ${name}`, awaitingPermission: 'Waiting for your permission decision…', sending: 'Sending…', importing: 'Importing…', sendLabel: 'Send', @@ -965,30 +809,7 @@ const CONVERSATION_COPY = { }, chat: { conversationAriaLabel: (name) => `Conversation: ${name}`, - memory: 'Memory', memoryAriaLabel: 'Local memory enabled', memoryTitle: 'Local MEMORY.md is included in the agent system prompt. Click to manage it in Settings · Memory.', deepResearch: 'Deep Research', deepResearchAriaLabel: 'Deep Research, read-only exploration', deepResearchTitle: 'Deep Research uses a read-only boundary: inspect and analyze first, without changing files by default.', - deepResearchProgress: { - ariaLabel: 'Live Deep Research progress', - title: 'Research progress', - completedSummary: 'Research complete · Original task remains read-only', - activeSummary: (stage, scope, round) => `${stage} · ${scope} · Round ${round}`, - handoffTitle: 'Create a normal task with the research handoff. It will not send automatically or change the original research task permissions.', - handoffAction: 'Continue implementation in a new task', - checklistTitle: 'Checklist', - reportTitle: 'Report draft', - inspectedTitle: 'Inspected locations', - inspectedEmpty: 'Waiting for recorded files, symbols, or sources.', - executionTitle: 'Execution and blockers', - executionSummary: (steps, artifacts) => `${steps} research steps · ${artifacts} persisted evidence items`, - workersLabel: 'Workers', - noBlockers: 'No current blockers.', - sectionLabels: { - conclusion: 'Conclusion', - source_evidence: 'Evidence', - borrow_diverge_risk_gate: 'Tradeoffs and risks', - implementation_recommendations: 'Implementation recommendations', - verification: 'Verification', - }, - }, + memory: 'Memory', memoryAriaLabel: 'Local memory enabled', memoryTitle: 'Local MEMORY.md is included in the agent system prompt. Click to manage it in Settings · Memory.', clearGoal: (condition, iteration, max, status) => `Autonomous goal in progress: “${condition}” (iteration ${iteration}/${max}, ${status}). Maka continues after each iteration; click to clear the goal and stop continuing.`, clearGoalAriaLabel: (iteration, max) => `Clear autonomous goal after ${iteration}/${max} iterations`, goalProgress: (iteration, max) => `Goal ${iteration} of ${max}`, goalRunningAriaLabel: 'Autonomous goal running', goalWaitingAriaLabel: 'Autonomous goal waiting for conditions to change', goalPausedAriaLabel: 'Autonomous goal paused', pauseGoalAriaLabel: (iteration, max) => `Pause autonomous goal after ${iteration}/${max} iterations`, resumeGoalAriaLabel: (iteration, max) => `Resume autonomous goal after ${iteration}/${max} iterations`, pauseGoal: (condition, iteration, max, status) => `Pause autonomous goal: “${condition}” (iteration ${iteration}/${max}, ${status}). Pausing stops autonomous continuation immediately — no more tokens burn; resume any time.`, resumeGoal: (condition, iteration, max) => `Resume autonomous goal: “${condition}” (iteration ${iteration}/${max}). Resuming continues autonomous iteration immediately.`, goalElapsed: (elapsedMs) => formatGoalElapsedUnits(elapsedMs, { second: 's', minute: 'm', hour: 'h', day: 'd' }), goalTokens: (spent, budget) => `${formatCompactTokenCount(spent)} / ${formatCompactTokenCount(budget)}`, loadFailed: 'Task failed to load', loading: 'Loading…', retryLoad: 'Retry', loadEarlierHistory: 'Load earlier history', quoteSelection: 'Quote', askInSidePanel: 'Ask in side panel', noMessages: 'No messages yet', diff --git a/packages/ui/src/session-context-layer.tsx b/packages/ui/src/session-context-layer.tsx index 9cb3999a6b..77525e2e86 100644 --- a/packages/ui/src/session-context-layer.tsx +++ b/packages/ui/src/session-context-layer.tsx @@ -94,7 +94,6 @@ export function SessionContextLayer(props: { onRevisionNavigate?(sessionId: string): void; memoryActive?: boolean; onOpenMemorySettings?(): void; - deepResearchActive?: boolean; goal?: SessionContextGoal; actions?: ReactNode; }) { @@ -274,28 +273,6 @@ export function SessionContextLayer(props: { }); } - if (props.deepResearchActive) { - contextItems.push({ - key: 'deep-research', - element: ( - } - /> - ), - overflowItems: [{ - label: copy.deepResearchAriaLabel, - icon: , - isDisabled: true, - }], - }); - } - if (props.memoryActive) { contextItems.push({ key: 'memory', From b65f08c4a9310234029601019723ab0efdb7ed1c Mon Sep 17 00:00:00 2001 From: AstroHan Date: Mon, 21 Sep 2026 11:24:10 +0800 Subject: [PATCH 2/4] fix(ci): update shell hook inventory after research removal Generated-by: Codex --- scripts/check-app-shell-hooks.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-app-shell-hooks.mjs b/scripts/check-app-shell-hooks.mjs index 4fc61e064c..0fb1256075 100644 --- a/scripts/check-app-shell-hooks.mjs +++ b/scripts/check-app-shell-hooks.mjs @@ -142,7 +142,7 @@ export const ALLOWED = { useShellMemoryPill: 1, useShellResume: 1, useShellRunUpdates: 1, - useStableActions: 6, + useStableActions: 5, useState: 11, useTaskSubmissionReadiness: 1, useToast: 1, From 93c0301c0b7fc61c7ac4be4bd116270f87b1de1d Mon Sep 17 00:00:00 2001 From: AstroHan Date: Mon, 21 Sep 2026 11:41:43 +0800 Subject: [PATCH 3/4] fix(storage): preserve retired research schema during upgrades Generated-by: Codex --- .../__tests__/operational-state-store.test.ts | 46 ++++++++++++++++++- .../storage/src/sqlite-workflow-schema.ts | 11 +++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/storage/src/__tests__/operational-state-store.test.ts b/packages/storage/src/__tests__/operational-state-store.test.ts index 219ba3fe8d..b5b472b91b 100644 --- a/packages/storage/src/__tests__/operational-state-store.test.ts +++ b/packages/storage/src/__tests__/operational-state-store.test.ts @@ -18,7 +18,7 @@ */ import assert from 'node:assert/strict'; -import { chmod, mkdtemp, readFile, rm } from 'node:fs/promises'; +import { chmod, mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { DatabaseSync } from 'node:sqlite'; @@ -847,6 +847,50 @@ test('rolls back every scope when migration publication fails', async () => { } }); +test('preserves retired research events through upgrade, reopen, and backup', async () => { + const root = await mkdtemp(join(tmpdir(), 'maka-operational-retired-research-')); + const restoredRoot = join(root, 'restored'); + const record = '{"kind":"started","objective":"Existing research"}'; + try { + const databasePath = join(root, 'runtime.sqlite'); + await restoreV016Database(databasePath); + const legacy = new DatabaseSync(databasePath); + try { + legacy.exec('DELETE FROM automation_pending_fires; DELETE FROM automation_definitions'); + legacy + .prepare(` + INSERT INTO workflow_deep_research_events(session_id, sequence, event_id, record_json) + VALUES ('legacy-research', 0, 'start', ?) + `) + .run(record); + } finally { + legacy.close(); + } + + acquireOperationalStateDatabase(root).close(); + await mkdir(restoredRoot); + for (const stateRoot of [root, restoredRoot]) { + const lease = acquireOperationalStateDatabase(stateRoot, { + schemaMigration: 'require_current', + }); + try { + const row = lease.database + .prepare(` + SELECT record_json FROM workflow_deep_research_events + WHERE session_id = 'legacy-research' AND event_id = 'start' + `) + .get(); + assert.equal(row?.record_json, record); + if (stateRoot === root) await lease.backup(join(restoredRoot, 'runtime.sqlite')); + } finally { + lease.close(); + } + } + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + test('migrates released Reminder state after Automation is retired', async () => { const root = await mkdtemp(join(tmpdir(), 'maka-operational-v016-')); try { diff --git a/packages/storage/src/sqlite-workflow-schema.ts b/packages/storage/src/sqlite-workflow-schema.ts index 279d9529d7..88f1941ff4 100644 --- a/packages/storage/src/sqlite-workflow-schema.ts +++ b/packages/storage/src/sqlite-workflow-schema.ts @@ -62,6 +62,17 @@ export function migrateSqliteWorkflowDatabase(db: DatabaseSync): void { UNIQUE (session_id, store_version) ); + -- Retained for existing State Roots: schema validation compares the complete DDL. + -- The research writer is retired, but its stored events must survive upgrades. + CREATE TABLE IF NOT EXISTS workflow_deep_research_events ( + session_id TEXT NOT NULL, + sequence INTEGER NOT NULL CHECK (sequence >= 0), + event_id TEXT NOT NULL, + record_json TEXT NOT NULL, + PRIMARY KEY (session_id, sequence), + UNIQUE (session_id, event_id) + ); + CREATE TABLE IF NOT EXISTS workflow_scheduled_tasks ( task_id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, From b3f7d78b03e2b3dd17a8df9bec6a249c9daebe50 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Mon, 21 Sep 2026 11:56:02 +0800 Subject: [PATCH 4/4] fix(runtime-host): retire persisted research access grant Generated-by: Codex --- .../access-credential-grant-migration.test.ts | 23 +++++++++++++++++++ .../src/server/access-credential-store.ts | 1 + 2 files changed, 24 insertions(+) diff --git a/packages/runtime-host/src/__tests__/access-credential-grant-migration.test.ts b/packages/runtime-host/src/__tests__/access-credential-grant-migration.test.ts index b6dedf06f5..99acf7b37f 100644 --- a/packages/runtime-host/src/__tests__/access-credential-grant-migration.test.ts +++ b/packages/runtime-host/src/__tests__/access-credential-grant-migration.test.ts @@ -172,6 +172,29 @@ test('retired WorkHub grants are released without granting active-turn authority assert.deepEqual(rewritten.credentials, [{ ...original, operationGrants: ['host.status'] }]); }); +test('retires the research grant while preserving the released credential across restart', async () => { + const original = storedCredential(['host.status', 'deep-research.query', 'artifact.query']); + const path = await writeAccessFile({ + schemaVersion: 3, + credentials: [original], + sessionGrants: [], + turnAccessRequests: [], + }); + + const file = await readAccessCredentialFile(path); + assert.deepEqual(unresolvedPersistedGrants(file), []); + await writeAccessCredentialFile(path, file); + const reopened = await readAccessCredentialFile(path); + assert.deepEqual(effectiveOperationGrants(reopened.credentials[0]!), [ + 'host.status', + 'artifact.query', + ]); + const rewritten = JSON.parse(await readFile(path, 'utf8')); + assert.deepEqual(rewritten.credentials, [ + { ...original, operationGrants: ['host.status', 'artifact.query'] }, + ]); +}); + test('a Session Guest holds the current guest policy, not what its record says', async () => { const path = await writeAccessFile({ schemaVersion: 3, diff --git a/packages/runtime-host/src/server/access-credential-store.ts b/packages/runtime-host/src/server/access-credential-store.ts index a4faff9aea..177d9e977e 100644 --- a/packages/runtime-host/src/server/access-credential-store.ts +++ b/packages/runtime-host/src/server/access-credential-store.ts @@ -81,6 +81,7 @@ const PERSISTED_GRANT_MIGRATIONS: ReadonlyMap = ['execution.inspect.resolve', { kind: 'release' }], // Retired in favor of editing and resending the original user message. ['turn.regenerate', { kind: 'release' }], + ['deep-research.query', { kind: 'release' }], // Direct WorkHub actions and record writes were retired. Their grants do not // authorize actFromTurn, which requires the active coordination Turn. ['workhub.coordination.act', { kind: 'release' }],