blob: 11092c4d7b83acd4d5f252d1dd1eaf8cee755108 [file]
import { z } from 'zod';
import {
TASK_ID_MAX_CHARS,
decodeCanonicalToolResultContent,
isSafeTaskId,
isSafeSubagentPresetId,
type TaskLedgerStore,
type ToolResultContent,
} from '@maka/core';
import type { MakaTool, MakaToolContext } from './tool-runtime.js';
import {
AGENT_WORKSPACE_SAME_WORKSPACE,
AGENT_WORKSPACE_WORKTREE,
AGENT_WRITE_BACK_PATCH,
AGENT_WRITE_BACK_SUMMARY,
BUILTIN_AGENT_DEFINITIONS,
agentProfilesForDefinitions,
buildToolsForAgentDefinition,
requireAgentDefinitionByProfile,
type AgentDefinition,
} from './agent-catalog.js';
import { ChildAgentProgressProjector } from './child-agent-progress.js';
export const AGENT_SPAWN_TOOL_NAME = 'agent_spawn';
export const AGENT_LIST_TOOL_NAME = 'agent_list';
export const AGENT_OUTPUT_TOOL_NAME = 'agent_output';
export const AGENT_TOOL_GROUP_ID = 'agent';
export const AGENT_TOOL_NAMES = [
AGENT_SPAWN_TOOL_NAME,
AGENT_LIST_TOOL_NAME,
AGENT_OUTPUT_TOOL_NAME,
] as const;
export const CHILD_AGENT_TOOL_NAMES = [
...new Set(BUILTIN_AGENT_DEFINITIONS.flatMap((definition) => definition.tools)),
] as readonly string[];
const AGENT_SPAWN_WRITE_BACK_MODES = [AGENT_WRITE_BACK_SUMMARY, AGENT_WRITE_BACK_PATCH] as const;
const AGENT_SPAWN_ISOLATION_MODES = [
AGENT_WORKSPACE_SAME_WORKSPACE,
AGENT_WORKSPACE_WORKTREE,
] as const;
const CHILD_PROGRESS_ERROR_MAX_CHARS = 1_000;
const AGENT_LIST_PAGE_SIZE = 8;
// Active tool-result archival starts at roughly 8k characters with the default
// token estimate. Keep discovery safely below it even with maximal catalog text.
const AGENT_LIST_MAX_RESPONSE_CHARS = 7_000;
const AGENT_LIST_DESCRIPTION_MAX_CHARS = 240;
const AGENT_LIST_MODEL_MAX_CHARS = 160;
/**
* Which schema fields each `agent_output` locator needs. A rejection that only
* says "its matching identity fields" leaves the model guessing which of the
* four optional id fields to add, so name them.
*/
const LOCATOR_REQUIRED_FIELDS = {
child_session_latest: 'child_session_id',
child_session_run: 'child_session_id and run_id',
legacy_run: 'run_id',
legacy_turn: 'turn_id',
} as const satisfies Record<string, string>;
type SubagentToolResult = Extract<ToolResultContent, { kind: 'subagent' }>;
export function buildChildAgentTools(tools: readonly MakaTool[]): MakaTool[] {
const seen = new Set<string>();
const out: MakaTool[] = [];
for (const definition of BUILTIN_AGENT_DEFINITIONS) {
for (const tool of buildToolsForAgentDefinition(tools, definition)) {
if (seen.has(tool.name)) continue;
seen.add(tool.name);
out.push(tool);
}
}
return out;
}
export function buildSubagentSpawnTool(
deps: { taskLedger?: TaskLedgerStore; definitions?: readonly AgentDefinition[] } = {},
): MakaTool<
{
profile?: string;
subagent_id?: string;
task: string;
write_back?: string;
isolation?: string;
task_id?: string;
},
unknown
> {
const definitions = deps.definitions ?? BUILTIN_AGENT_DEFINITIONS;
const profiles = agentProfilesForDefinitions(definitions);
return {
name: AGENT_SPAWN_TOOL_NAME,
displayName: 'Agent',
description:
'Run one bounded foreground child task. Prefer agent_list, then select the user-approved subagent_id whose description fits the task; profile is retained for legacy callers. If both selectors are present, subagent_id wins and profile is ignored.',
parameters: z.preprocess(
(input) => cleanSubagentSpawnInput(input, deps.taskLedger !== undefined),
z
.object({
profile: z.enum(profiles).optional().describe('Legacy child capability profile.'),
subagent_id: z
.string()
.min(1)
.max(128)
.refine(isSafeSubagentPresetId)
.optional()
.describe('User-approved subagent preset id from agent_list.'),
task: z
.string()
.min(1)
.max(60_000)
.describe('Bounded task for the selected child agent.'),
write_back: z
.enum(AGENT_SPAWN_WRITE_BACK_MODES)
.optional()
.describe(
'Requested child write-back mode. Each built-in profile declares its supported modes.',
),
isolation: z
.enum(AGENT_SPAWN_ISOLATION_MODES)
.optional()
.describe(
'Requested child workspace isolation. Worktree profiles fail closed until a worktree child executor is available.',
),
...(deps.taskLedger
? {
task_id: z
.string()
.min(1)
.max(TASK_ID_MAX_CHARS)
.refine(isSafeTaskId)
.optional()
.describe('Existing task UUID or short key to bind to this child run.'),
}
: {}),
})
.strip()
.superRefine((input, ctx) => {
if (!input.profile && !input.subagent_id) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
'No child selector was provided. Call agent_list and pass a returned subagent_id to agent_spawn, ' +
`or pass one legacy profile: ${profiles.join(', ')}.`,
});
return;
}
if (input.subagent_id) return;
if (!input.profile) return;
const definition = requireAgentDefinitionByProfile(definitions, input.profile);
const requestedWriteBack = input.write_back ?? definition.contract.defaultWriteBack;
if (!definition.contract.supportedWriteBack.some((mode) => mode === requestedWriteBack)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['write_back'],
message: `Agent profile "${definition.profile}" does not support write_back "${requestedWriteBack}".`,
});
}
const requestedIsolation = input.isolation ?? definition.contract.workspace;
if (requestedIsolation !== definition.contract.workspace) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['isolation'],
message: `Agent profile "${definition.profile}" requires isolation "${definition.contract.workspace}", not "${requestedIsolation}".`,
});
}
}),
),
categoryHint: 'subagent',
nesting: 'direct_only',
impl: async (input, ctx) => {
const definition = input.subagent_id
? await resolvePresetDefinition(input.subagent_id, ctx, definitions)
: requireAgentDefinitionByProfile(definitions, input.profile!);
const requestedWriteBack = input.write_back ?? definition.contract.defaultWriteBack;
if (!definition.contract.supportedWriteBack.some((mode) => mode === requestedWriteBack)) {
throw new Error(
`Agent profile "${definition.profile}" does not support write_back "${requestedWriteBack}".`,
);
}
const requestedIsolation = input.isolation ?? definition.contract.workspace;
if (requestedIsolation !== definition.contract.workspace) {
throw new Error(
`Agent profile "${definition.profile}" requires isolation "${definition.contract.workspace}", not "${requestedIsolation}".`,
);
}
if (!ctx.spawnChildSession) {
throw new Error(
'agent_spawn is not available in this session, so no child agent was started. ' +
'Retrying agent_spawn will fail the same way — do the task yourself with the tools you already have.',
{
cause: new Error('spawnChildSession capability is unavailable in this runtime context'),
},
);
}
// task_id is meaningful only in compositions that advertise task
// binding. Older or over-eager callers may still send it elsewhere;
// ignore it instead of turning an optional integration into a refusal.
const taskId = deps.taskLedger ? input.task_id : undefined;
const boundTask = taskId ? await deps.taskLedger!.get(ctx.sessionId, taskId) : undefined;
if (taskId && !boundTask) throw new Error(`No such task in this session: ${taskId}`);
let claimedOwner:
| {
actor: 'child_agent';
sessionId: string;
agentId: string;
turnId: string;
}
| undefined;
let result: Omit<SubagentToolResult, 'kind'>;
const progress = new ChildAgentProgressProjector(ctx);
ctx.emitOutput('stdout', `Starting child agent: ${definition.name}\n`);
try {
result = projectSubagentToolResult(
await ctx.spawnChildSession({
agentProfile: definition.profile,
...(input.subagent_id ? { subagentId: input.subagent_id } : {}),
prompt: input.task,
...(boundTask
? {
onReady: async ({ childSessionId, turnId, agentId }) => {
const owner = {
actor: 'child_agent' as const,
sessionId: childSessionId,
agentId,
turnId,
};
await deps.taskLedger!.claim(ctx.sessionId, boundTask.id, owner, {
runId: ctx.runId,
turnId: ctx.turnId,
toolCallId: ctx.toolCallId,
source: 'system',
actor: 'main_agent',
reason: `assigned to child agent ${agentId}`,
});
claimedOwner = owner;
},
}
: {}),
onEvent: (event) => progress.observe(event),
}),
);
} catch (error) {
ctx.emitOutput(
'stderr',
`Child agent ${definition.name} failed: ${boundedChildError(error)}\n`,
);
if (boundTask && claimedOwner) {
await deps.taskLedger!.settleAgentOutcome(
ctx.sessionId,
boundTask.id,
{
status: 'failed',
owner: claimedOwner,
reason:
error instanceof Error
? error.message
: 'Child agent failed before returning a result',
},
{
turnId: claimedOwner.turnId,
toolCallId: ctx.toolCallId,
source: 'system',
actor: 'child_agent',
},
);
}
throw error;
}
ctx.emitOutput('stdout', `Child agent ${definition.name}: ${result.status}\n`);
if (boundTask && claimedOwner) {
const owner = {
...claimedOwner,
...(result.runId ? { runId: result.runId } : {}),
turnId: result.turnId,
};
await deps.taskLedger!.settleAgentOutcome(
ctx.sessionId,
boundTask.id,
{
status: result.status,
owner,
reason: result.failureClass ?? result.summary,
},
{
runId: result.runId,
turnId: result.turnId,
toolCallId: ctx.toolCallId,
source: 'system',
actor: 'child_agent',
},
);
}
return {
kind: 'subagent',
...result,
} satisfies SubagentToolResult;
},
};
}
function cleanSubagentSpawnInput(input: unknown, taskBindingAvailable: boolean): unknown {
if (!input || typeof input !== 'object' || Array.isArray(input)) return input;
const cleaned = { ...(input as Record<string, unknown>) };
if (cleaned.subagent_id !== undefined) delete cleaned.profile;
if (!taskBindingAvailable) delete cleaned.task_id;
return cleaned;
}
async function resolvePresetDefinition(
subagentId: string,
ctx: MakaToolContext,
definitions: readonly AgentDefinition[],
): Promise<AgentDefinition> {
if (!ctx.listChildAgents) {
throw new Error('listChildAgents capability is unavailable in this runtime context');
}
const catalog = await ctx.listChildAgents();
if (!catalog || typeof catalog !== 'object' || Array.isArray(catalog)) {
throw new Error('agent_list returned an invalid catalog');
}
const presets = (catalog as { presets?: unknown }).presets;
if (!Array.isArray(presets)) throw new Error('Configured subagent catalog is unavailable');
const preset = presets.find(
(candidate): candidate is { id: string; profile: string; availability?: { status?: string } } =>
Boolean(candidate) &&
typeof candidate === 'object' &&
!Array.isArray(candidate) &&
(candidate as { id?: unknown }).id === subagentId &&
typeof (candidate as { profile?: unknown }).profile === 'string',
);
if (!preset) throw new Error(`Unknown subagent_id "${subagentId}". Call agent_list first.`);
if (preset.availability?.status !== 'available') {
throw new Error(`Subagent preset "${subagentId}" is unavailable.`);
}
return requireAgentDefinitionByProfile(definitions, preset.profile);
}
function projectSubagentToolResult(value: unknown): Omit<SubagentToolResult, 'kind'> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error('Child agent returned an invalid result');
}
const raw = value as Record<string, unknown>;
const decoded = decodeCanonicalToolResultContent({
kind: 'subagent',
...(raw.childSessionId !== undefined ? { childSessionId: raw.childSessionId } : {}),
...(raw.agentId !== undefined ? { agentId: raw.agentId } : {}),
agentName: raw.agentName,
turnId: raw.turnId,
...(raw.runId !== undefined ? { runId: raw.runId } : {}),
status: raw.status,
permissionMode: raw.permissionMode,
summary: raw.summary,
artifactIds: raw.artifactIds,
...(raw.startedAt !== undefined ? { startedAt: raw.startedAt } : {}),
...(raw.completedAt !== undefined ? { completedAt: raw.completedAt } : {}),
...(raw.durationMs !== undefined ? { durationMs: raw.durationMs } : {}),
...(raw.eventCount !== undefined ? { eventCount: raw.eventCount } : {}),
...(raw.failureClass !== undefined ? { failureClass: raw.failureClass } : {}),
});
if (decoded.kind !== 'subagent') throw new Error('Child agent returned an invalid result');
const { kind: _kind, ...result } = decoded as SubagentToolResult;
return result;
}
function boundedChildError(error: unknown): string {
const message = error instanceof Error ? error.message : 'unknown error';
return message.length <= CHILD_PROGRESS_ERROR_MAX_CHARS
? message
: `${message.slice(0, CHILD_PROGRESS_ERROR_MAX_CHARS - 1)}…`;
}
export function buildSubagentListTool(): MakaTool<
{ view?: 'selection' | 'catalog'; cursor?: string },
unknown
> {
return {
name: AGENT_LIST_TOOL_NAME,
displayName: 'Agent List',
description:
'List a compact page of subagents to select. The default selection view returns runnable user-approved subagent_id values first, followed by legacy choices with separate agent_id (Graph) and profile (agent_spawn) selectors. Use view=catalog only to diagnose unavailable routes. Child execution history is intentionally excluded; use refs returned by agent_spawn or asynchronous graph work with agent_output.',
parameters: z
.object({
view: z
.enum(['selection', 'catalog'])
.default('selection')
.describe(
'selection lists runnable choices; catalog also includes unavailable choices and reasons.',
),
cursor: z
.string()
.regex(/^\d+$/)
.optional()
.describe('next_cursor returned by the previous agent_list page.'),
})
.strip(),
categoryHint: 'read',
nesting: 'direct_only',
impl: async (input, ctx) => {
// Runtime Host supplies this capability to production clients.
// A headless embedder can still construct ToolRuntime without it, so
// keep the failure explicit at the embedding boundary.
if (!ctx.listChildAgents) {
throw new Error(
'agent_list is not available in this session, so no agent catalog could be read. ' +
'Retrying agent_list will fail the same way — pick a child agent profile from the agent_spawn schema instead.',
{ cause: new Error('listChildAgents capability is unavailable in this runtime context') },
);
}
return projectAgentList(await ctx.listChildAgents(), input);
},
};
}
function projectAgentList(
catalog: unknown,
input: { view?: 'selection' | 'catalog'; cursor?: string },
): unknown {
// The host capability remains a rich control-plane projection because spawn
// and swarm resolve presets through it. Only the model-facing list is narrowed.
if (!catalog || typeof catalog !== 'object' || Array.isArray(catalog)) {
throw new Error('agent_list returned an invalid catalog');
}
const raw = catalog as Record<string, unknown>;
const definitions = Array.isArray(raw.definitions) ? raw.definitions : [];
const definitionByProfile = new Map<string, Record<string, unknown>>();
for (const candidate of definitions) {
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) continue;
const definition = candidate as Record<string, unknown>;
if (typeof definition.profile === 'string') {
definitionByProfile.set(definition.profile, definition);
}
}
const view = input.view ?? 'selection';
const presets = (Array.isArray(raw.presets) ? raw.presets : [])
.flatMap((candidate) => {
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return [];
const preset = candidate as Record<string, unknown>;
if (
typeof preset.id !== 'string' ||
typeof preset.name !== 'string' ||
typeof preset.description !== 'string' ||
typeof preset.profile !== 'string' ||
typeof preset.model !== 'string'
) {
return [];
}
const availability = effectivePresetAvailability(
preset,
definitionByProfile.get(preset.profile),
);
return [
{
subagent_id: preset.id,
name: boundedCatalogText(preset.name, 128),
description: boundedCatalogText(preset.description, AGENT_LIST_DESCRIPTION_MAX_CHARS),
profile: preset.profile,
model: boundedCatalogText(preset.model, AGENT_LIST_MODEL_MAX_CHARS),
...(typeof preset.thinkingLevel === 'string'
? { thinking_level: boundedCatalogText(preset.thinkingLevel, 32) }
: {}),
...availability,
},
];
})
.filter((preset) => view === 'catalog' || preset.status === 'available');
const offset = Math.min(Number.parseInt(input.cursor ?? '0', 10), presets.length);
const pagePresets = presets.slice(offset, offset + AGENT_LIST_PAGE_SIZE);
const legacyProfiles = definitions.flatMap((candidate) => {
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return [];
const definition = candidate as Record<string, unknown>;
if (
typeof definition.id !== 'string' ||
typeof definition.profile !== 'string' ||
typeof definition.name !== 'string' ||
typeof definition.description !== 'string'
) {
return [];
}
const availability = catalogAvailability(definition.availability);
if (view === 'selection' && availability.status !== 'available') return [];
const contract =
definition.contract &&
typeof definition.contract === 'object' &&
!Array.isArray(definition.contract)
? (definition.contract as Record<string, unknown>)
: undefined;
return [
{
agent_id: definition.id,
profile: definition.profile,
name: boundedCatalogText(definition.name, 128),
description: boundedCatalogText(definition.description, AGENT_LIST_DESCRIPTION_MAX_CHARS),
...(typeof contract?.workspace === 'string'
? { workspace: boundedCatalogText(contract.workspace, 32) }
: {}),
...(typeof contract?.defaultWriteBack === 'string'
? { write_back: boundedCatalogText(contract.defaultWriteBack, 32) }
: {}),
...availability,
},
];
});
const buildPage = () => {
const nextOffset = offset + pagePresets.length;
return {
presets: pagePresets,
legacy_profiles: legacyProfiles,
page: {
returned: pagePresets.length,
total: presets.length,
...(nextOffset < presets.length ? { next_cursor: String(nextOffset) } : {}),
},
view,
};
};
while (
pagePresets.length > 1 &&
JSON.stringify(buildPage()).length > AGENT_LIST_MAX_RESPONSE_CHARS
) {
pagePresets.pop();
}
return buildPage();
}
function effectivePresetAvailability(
preset: Record<string, unknown>,
definition: Record<string, unknown> | undefined,
): { status: 'available' } | { status: 'unavailable'; reason: string } {
const presetAvailability = catalogAvailability(preset.availability);
if (presetAvailability.status === 'unavailable') return presetAvailability;
if (!definition) return { status: 'unavailable', reason: 'unknown_profile' };
const definitionAvailability = catalogAvailability(definition.availability);
if (definitionAvailability.status === 'unavailable') {
return {
status: 'unavailable',
reason: `profile_${definitionAvailability.reason}`,
};
}
return { status: 'available' };
}
function catalogAvailability(
value: unknown,
): { status: 'available' } | { status: 'unavailable'; reason: string } {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return { status: 'unavailable', reason: 'availability_unknown' };
}
const availability = value as Record<string, unknown>;
if (availability.status === 'available') return { status: 'available' };
return {
status: 'unavailable',
reason:
typeof availability.reason === 'string'
? boundedCatalogText(availability.reason, 120)
: 'availability_unknown',
};
}
function boundedCatalogText(value: string, maxChars: number): string {
return value.length <= maxChars ? value : `${value.slice(0, maxChars - 1)}…`;
}
export function buildSubagentOutputTool(): MakaTool<
{
locator?: 'child_session_latest' | 'child_session_run' | 'legacy_run' | 'legacy_turn';
child_session_id?: string;
run_id?: string;
turn_id?: string;
max_events?: number;
max_bytes?: number;
view?: 'result' | 'events' | 'runtime_events' | 'all';
},
unknown
> {
return {
name: AGENT_OUTPUT_TOOL_NAME,
displayName: 'Agent Output',
description:
'Inspect bounded child output. Use view=result for the final committed model text plus its Graph result record id; runtime_events is the default compatibility view. Always set locator: child_session_run for a graph childSessionId/currentRunId, child_session_latest for its latest run, or a legacy locator. Use view=all only for targeted diagnostics.',
parameters: z.preprocess(
cleanSubagentOutputInput,
z
.object({
locator: z
.enum(['child_session_latest', 'child_session_run', 'legacy_run', 'legacy_turn'])
.optional()
.describe(
'Explicit locator discriminator. The runtime applies only fields selected by this value.',
),
child_session_id: z
.string()
.min(1)
.optional()
.describe('Linked child Session id. Without run_id, inspects its latest AgentRun.'),
run_id: z.string().min(1).optional(),
turn_id: z.string().min(1).optional(),
max_events: z.number().int().min(1).max(100).optional(),
max_bytes: z
.number()
.int()
.min(1024)
.max(128 * 1024)
.optional(),
view: z.enum(['result', 'events', 'runtime_events', 'all']).optional(),
})
.strip()
.superRefine((input, ctx) => {
if (input.locator) {
const valid =
(input.locator === 'child_session_latest' && Boolean(input.child_session_id)) ||
(input.locator === 'child_session_run' &&
Boolean(input.child_session_id) &&
Boolean(input.run_id)) ||
(input.locator === 'legacy_run' && Boolean(input.run_id)) ||
(input.locator === 'legacy_turn' && Boolean(input.turn_id));
if (!valid) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `locator=${input.locator} requires ${LOCATOR_REQUIRED_FIELDS[input.locator]}.`,
});
}
return;
}
if (input.child_session_id) {
if (input.turn_id) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['turn_id'],
message: 'turn_id cannot be combined with child_session_id',
});
}
return;
}
if (Number(!!input.run_id) + Number(!!input.turn_id) !== 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Provide child_session_id, or exactly one legacy run_id/turn_id',
});
}
}),
),
categoryHint: 'read',
nesting: 'direct_only',
impl: async (input, ctx) => {
if (!ctx.readChildAgentOutput) {
// Same reachability as `agent_list` above.
throw new Error(
'agent_output is not available in this session, so no child output could be read. ' +
'Retrying agent_output will fail the same way — use the summary returned when that child completed.',
{
cause: new Error(
'readChildAgentOutput capability is unavailable in this runtime context',
),
},
);
}
const explicitLocator =
input.locator === 'child_session_latest'
? {
execution: {
kind: 'child_session' as const,
sessionId: input.child_session_id!,
},
}
: input.locator === 'child_session_run'
? {
execution: {
kind: 'child_session' as const,
sessionId: input.child_session_id!,
currentRunId: input.run_id!,
},
}
: input.locator === 'legacy_run'
? {
execution: {
kind: 'legacy_child_run' as const,
sessionId: ctx.sessionId,
runId: input.run_id!,
},
}
: input.locator === 'legacy_turn'
? { turnId: input.turn_id! }
: undefined;
return await ctx.readChildAgentOutput({
...(explicitLocator ??
(input.child_session_id
? {
execution: {
kind: 'child_session' as const,
sessionId: input.child_session_id,
...(input.run_id ? { currentRunId: input.run_id } : {}),
},
}
: input.run_id
? {
execution: {
kind: 'legacy_child_run' as const,
sessionId: ctx.sessionId,
runId: input.run_id,
},
}
: {})),
...(input.locator === undefined && input.turn_id ? { turnId: input.turn_id } : {}),
...(input.max_events !== undefined ? { maxEvents: input.max_events } : {}),
...(input.max_bytes !== undefined ? { maxBytes: input.max_bytes } : {}),
...(input.view !== undefined ? { view: input.view } : {}),
});
},
};
}
function cleanSubagentOutputInput(input: unknown): unknown {
if (!input || typeof input !== 'object' || Array.isArray(input)) return input;
const cleaned = { ...(input as Record<string, unknown>) };
switch (cleaned.locator) {
case 'child_session_latest':
delete cleaned.run_id;
delete cleaned.turn_id;
break;
case 'child_session_run':
case 'legacy_run':
delete cleaned.turn_id;
if (cleaned.locator === 'legacy_run') delete cleaned.child_session_id;
break;
case 'legacy_turn':
delete cleaned.child_session_id;
delete cleaned.run_id;
break;
}
return cleaned;
}
export function buildSubagentProjectionTools(): MakaTool[] {
return [buildSubagentListTool(), buildSubagentOutputTool()];
}
export function buildParentAgentTools(
deps: { taskLedger?: TaskLedgerStore; definitions?: readonly AgentDefinition[] } = {},
): MakaTool[] {
const definitions = deps.definitions ?? BUILTIN_AGENT_DEFINITIONS;
return [
...(definitions.length > 0 ? [buildSubagentSpawnTool({ ...deps, definitions })] : []),
...buildSubagentProjectionTools(),
];
}