blob: adab6715b9578ed6cd329067d66c310de82382c5 [file]
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,
redactSecrets,
type ArtifactRecord,
type DeepResearchRun,
type DeepResearchArtifactRef,
type DeepResearchEvent,
type DeepResearchStore,
} from '@maka/core';
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',
'ExploreAgent',
'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<ArtifactRecord>;
get(artifactId: string): Promise<ArtifactRecord | null>;
readText(
artifactId: string,
options?: { maxBytes?: number; includeDeleted?: boolean },
): Promise<{ ok: true; text: string } | { ok: false; reason: string }>;
delete(artifactId: string): Promise<void>;
}
export interface BuildDeepResearchToolsDeps {
store: DeepResearchStore;
artifactStore: DeepResearchArtifactStore;
onArtifactCreated?: (event: {
reason: 'created';
artifactId: string;
sessionId: string;
ts: number;
}) => void | Promise<void>;
}
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<MakaTool, 'name'>): 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' ||
record.status !== 'live'
) {
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 characters = Array.from(read.text);
const end = Math.min(characters.length, offset + maxChars);
const chunk = safeResearchArtifactContent(characters.slice(offset, end).join(''));
return [
`<deep-research-artifact id="${ref.artifactId}" role="${ref.role}" offset="${offset}" end="${end}" total="${characters.length}">`,
`Name: ${normalizeInlineText(ref.name)}`,
...(ref.locator ? [`Locator: ${normalizeInlineText(ref.locator)}`] : []),
`Truncated: ${end < characters.length}`,
'',
chunk,
'</deep-research-artifact>',
].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;
}
try {
await deps.onArtifactCreated?.({
reason: 'created',
artifactId,
sessionId: ctx.sessionId,
ts: artifact.createdAt,
});
} catch {
// Renderer notification is best effort; both durable authorities already committed.
}
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[];
task_ids?: 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, task ids, 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.'),
task_ids: refArray.optional().describe('Related ids from the session Task Ledger.'),
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: dedupe(input.task_ids ?? []),
artifactIds: dedupe(input.artifact_ids ?? []),
},
mutationContext(ctx),
);
return renderRunStatus(run);
},
};
}
function buildStatusTool(
deps: BuildDeepResearchToolsDeps,
): MakaTool<Record<string, never>, 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) : '<deep-research-workspace state="uninitialized" />';
},
};
}
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<DeepResearchRun> {
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<DeepResearchRun> {
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<DeepResearchEvent | undefined> {
return (await store.readEvents(sessionId)).find((event) => event.refs?.toolCallId === toolCallId);
}
async function validateCompletionArtifacts(
artifactStore: DeepResearchArtifactStore,
run: DeepResearchRun,
reportArtifactId: string,
handoffArtifactId: string,
): Promise<void> {
const required = new Map<string, DeepResearchArtifactRef>();
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<void> {
const record = await artifactStore.get(ref.artifactId);
if (!record || record.status !== 'live') {
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 = [
`<deep-research-workspace status="${run.status}" stage="${run.stage}" scope="${run.scopeLevel}" round="${run.round}">`,
`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('</deep-research-workspace>');
return lines.join('\n');
}
const renderRunStatus = renderDeepResearchRunStatus;