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__/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/__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/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' }], 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__/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/__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..88f1941ff4 100644 --- a/packages/storage/src/sqlite-workflow-schema.ts +++ b/packages/storage/src/sqlite-workflow-schema.ts @@ -62,6 +62,8 @@ 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), 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', 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,