@capekai/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/package.json +105 -0
- package/src/adapters/ai-sdk.ts +84 -0
- package/src/compaction/contracts.ts +82 -0
- package/src/compaction/executor.ts +161 -0
- package/src/compaction/policy.ts +318 -0
- package/src/compaction/recovery.ts +139 -0
- package/src/compaction/task.ts +540 -0
- package/src/configuration/contracts.ts +58 -0
- package/src/configuration/defaults.ts +27 -0
- package/src/configuration/runtime.ts +42 -0
- package/src/configuration/single-model.ts +75 -0
- package/src/context/assembler.ts +112 -0
- package/src/context/index.ts +2 -0
- package/src/context/sources.ts +119 -0
- package/src/context/workspace.ts +63 -0
- package/src/core/agent.ts +401 -0
- package/src/core/build-tools.ts +139 -0
- package/src/core/chat-handler.ts +858 -0
- package/src/core/error-handling.ts +18 -0
- package/src/core/fork.ts +103 -0
- package/src/core/interrupt.ts +192 -0
- package/src/core/message-utils.ts +261 -0
- package/src/core/model-utils.ts +149 -0
- package/src/core/part-utils.ts +88 -0
- package/src/core/provider-utils.ts +67 -0
- package/src/core/revert.ts +46 -0
- package/src/core/step-handlers.ts +157 -0
- package/src/core/stream/finalization.ts +65 -0
- package/src/core/stream/stream-config.ts +82 -0
- package/src/core/stream-handlers.ts +242 -0
- package/src/core/structured-output.ts +68 -0
- package/src/core/tool-builders/agent-tools.ts +71 -0
- package/src/core/tool-builders/external-tools.ts +179 -0
- package/src/core/tool-builders/types.ts +16 -0
- package/src/core/tool-builders/workspace-tools.ts +293 -0
- package/src/core/tool-capabilities.ts +65 -0
- package/src/goals/evaluator.ts +171 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/loop.ts +167 -0
- package/src/goals/service.ts +39 -0
- package/src/index.ts +10 -0
- package/src/internal/ask-authority.ts +29 -0
- package/src/internal/composition.ts +44 -0
- package/src/internal/configuration.ts +22 -0
- package/src/internal/execution.ts +108 -0
- package/src/internal/hosts.ts +64 -0
- package/src/internal/plugins.ts +71 -0
- package/src/internal/providers.ts +32 -0
- package/src/internal/sandbox.ts +19 -0
- package/src/internal/tools.ts +48 -0
- package/src/internal/workspace.ts +25 -0
- package/src/kernel/diagnostics.ts +249 -0
- package/src/kernel/errors.ts +120 -0
- package/src/kernel/events.ts +82 -0
- package/src/kernel/index.ts +72 -0
- package/src/kernel/kernel.ts +62 -0
- package/src/kernel/lifecycle.ts +72 -0
- package/src/kernel/plugin.ts +218 -0
- package/src/kernel/registry.ts +493 -0
- package/src/kernel/scope.ts +776 -0
- package/src/kernel/service-key.ts +19 -0
- package/src/kernel/types.ts +317 -0
- package/src/memory/index.ts +2 -0
- package/src/memory/memory-tool.ts +75 -0
- package/src/memory/registry.ts +172 -0
- package/src/permission/ask-user-api.ts +70 -0
- package/src/permission/contracts.ts +135 -0
- package/src/permission/permission-request-manager.ts +58 -0
- package/src/permission/policy.ts +277 -0
- package/src/permission/runtime.ts +612 -0
- package/src/plugins/compaction-policy.ts +46 -0
- package/src/plugins/compose.ts +171 -0
- package/src/plugins/context-sections.ts +246 -0
- package/src/plugins/default-agent-driver.ts +14 -0
- package/src/plugins/facade-plugins.ts +129 -0
- package/src/plugins/goal-domain.ts +82 -0
- package/src/plugins/legacy-system-message.ts +152 -0
- package/src/plugins/loaded-tools.ts +23 -0
- package/src/plugins/memory-domain.ts +264 -0
- package/src/plugins/orchestrator-session.ts +29 -0
- package/src/plugins/permission-policy.ts +49 -0
- package/src/plugins/retry-policy.ts +28 -0
- package/src/plugins/scheduler-domain.ts +192 -0
- package/src/plugins/service-keys.ts +294 -0
- package/src/plugins/session-search-domain.ts +238 -0
- package/src/plugins/skills-domain.ts +272 -0
- package/src/plugins/subagent-domain.ts +287 -0
- package/src/plugins/tool-catalog.ts +78 -0
- package/src/plugins/tool-output-policy.ts +52 -0
- package/src/plugins/value-plugins.ts +150 -0
- package/src/plugins/workflow-domain.ts +198 -0
- package/src/plugins/workspace-policy.ts +37 -0
- package/src/providers/registry.ts +63 -0
- package/src/providers/types.ts +44 -0
- package/src/retry/policy.ts +282 -0
- package/src/retry/stream-chat.ts +312 -0
- package/src/runtime/agent-runtime.ts +83 -0
- package/src/runtime/default-agent-driver.ts +23 -0
- package/src/runtime/domain-tool-source.ts +156 -0
- package/src/runtime/events.ts +61 -0
- package/src/runtime/host-dependencies.ts +71 -0
- package/src/runtime/host-guidance.ts +22 -0
- package/src/runtime/host-layout.ts +23 -0
- package/src/runtime/host.ts +129 -0
- package/src/runtime/standalone-host.ts +118 -0
- package/src/sandbox/controller.ts +204 -0
- package/src/sandbox/model.ts +305 -0
- package/src/sandbox/provider.ts +53 -0
- package/src/sandbox/types.ts +110 -0
- package/src/scheduler/host.ts +22 -0
- package/src/scheduler/scheduler-tool.ts +172 -0
- package/src/session-search/host.ts +56 -0
- package/src/session-search/index.ts +23 -0
- package/src/session-search/session-search-tool.ts +151 -0
- package/src/skills/index.ts +3 -0
- package/src/skills/registry.ts +63 -0
- package/src/skills/skill-manage-tool.ts +205 -0
- package/src/skills/skill-tool.ts +42 -0
- package/src/storage/contracts.ts +159 -0
- package/src/storage/memory.ts +321 -0
- package/src/storage/options.ts +75 -0
- package/src/storage/runtime.ts +115 -0
- package/src/storage/sqlite-tool-output-artifacts.ts +106 -0
- package/src/storage/sqlite.ts +321 -0
- package/src/storage/tool-output-artifacts.ts +75 -0
- package/src/storage.ts +31 -0
- package/src/subagent/child-session.ts +282 -0
- package/src/subagent/guidance.ts +8 -0
- package/src/subagent/policy.ts +198 -0
- package/src/subagent/task-tool.ts +584 -0
- package/src/tool-output/contracts.ts +111 -0
- package/src/tool-output/policy.ts +410 -0
- package/src/tool.ts +1 -0
- package/src/tools/executor.ts +258 -0
- package/src/tools/install-manifest.ts +40 -0
- package/src/tools/llm-api.ts +77 -0
- package/src/tools/registry.ts +206 -0
- package/src/tools/tool-artifact.ts +182 -0
- package/src/tools/tool-source.ts +53 -0
- package/src/utils/errors.ts +334 -0
- package/src/utils/strip-visualization.ts +50 -0
- package/src/workflow/decomposer.ts +139 -0
- package/src/workflow/execution.ts +523 -0
- package/src/workflow/orchestrator-session.ts +161 -0
- package/src/workflow/synthesizer.ts +130 -0
- package/src/workspace/contracts.ts +135 -0
- package/src/workspace/policy.ts +327 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import type { ToolDefinition } from '@capekai/tool'
|
|
2
|
+
import type { TextPart, Session, ResponseFormat, Preconfig } from '@capekai/types';
|
|
3
|
+
import {
|
|
4
|
+
emitRuntimeEvent,
|
|
5
|
+
emitSessionCreated,
|
|
6
|
+
emitSessionUpdated,
|
|
7
|
+
emitToSession,
|
|
8
|
+
} from '../runtime/host-dependencies';
|
|
9
|
+
import { getPreconfigOrAgent, listSubagentPreconfigs } from '../context';
|
|
10
|
+
import {
|
|
11
|
+
createSession,
|
|
12
|
+
getSession,
|
|
13
|
+
getWorkspaceAutoApproveSeverity,
|
|
14
|
+
updateSession,
|
|
15
|
+
} from '../storage/runtime';
|
|
16
|
+
import type { BroadcastFn, BroadcastSessionFn } from '../runtime/host';
|
|
17
|
+
import type { RuntimeEvent } from '../runtime/events';
|
|
18
|
+
import type { SessionUpdates } from '../storage/contracts';
|
|
19
|
+
import { executeChildSession } from './child-session';
|
|
20
|
+
import { resolveModelId, resolveProviderId } from '../core/provider-utils';
|
|
21
|
+
import {
|
|
22
|
+
collectSubagentAncestry,
|
|
23
|
+
evaluateSubagentTarget,
|
|
24
|
+
getSubagentResumeError,
|
|
25
|
+
isSubagentSpawningDisabled,
|
|
26
|
+
isValidSubagentTargetPreconfig,
|
|
27
|
+
resolveEffectiveSubagentTargets,
|
|
28
|
+
type ResolveSubagentTargetsOptions,
|
|
29
|
+
} from './policy';
|
|
30
|
+
|
|
31
|
+
import { randomUUID } from 'crypto';
|
|
32
|
+
|
|
33
|
+
/** Subagent domain: the task tool definition and execution. The unscoped
|
|
34
|
+
* exports (`getSubagentToolDefinition`, `executeSubagent`,
|
|
35
|
+
* `canSpawnSubagent`) keep the pre-C5 module-accessor behavior; the WithDeps
|
|
36
|
+
* variants run against injected session, preconfig, broadcast, and child
|
|
37
|
+
* execution dependencies captured by the domain plugin at composition. */
|
|
38
|
+
|
|
39
|
+
const MAX_SUBAGENT_DEPTH = 2;
|
|
40
|
+
|
|
41
|
+
export interface SubagentInput {
|
|
42
|
+
description: string;
|
|
43
|
+
prompt: string;
|
|
44
|
+
subagent_type: string;
|
|
45
|
+
task_id?: string;
|
|
46
|
+
sessionId: string;
|
|
47
|
+
workspaceId?: string;
|
|
48
|
+
workspacePath?: string;
|
|
49
|
+
abortSignal?: AbortSignal;
|
|
50
|
+
onSessionCreated?: (childSessionId: string) => void | Promise<void>;
|
|
51
|
+
allowedSubagentIds?: string[];
|
|
52
|
+
broadcast?: BroadcastFn;
|
|
53
|
+
broadcastSessionCreated?: BroadcastSessionFn;
|
|
54
|
+
broadcastSessionUpdated?: BroadcastSessionFn;
|
|
55
|
+
broadcastToSession?: BroadcastFn;
|
|
56
|
+
/** Optional JSON Schema for structured subagent output */
|
|
57
|
+
outputSchema?: Record<string, unknown>;
|
|
58
|
+
executeChild?: typeof executeChildSession;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SubagentOutput {
|
|
62
|
+
task_id: string;
|
|
63
|
+
result: string;
|
|
64
|
+
error?: string;
|
|
65
|
+
/** Structured JSON result when outputSchema was provided */
|
|
66
|
+
structuredResult?: Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface SubagentServiceSessionAccess {
|
|
70
|
+
getSession(id: string): Session | null | Promise<Session | null>;
|
|
71
|
+
createSession(session: Omit<Session, 'createdAt' | 'updatedAt'> & { createdAt?: string; updatedAt?: string }): Session | Promise<Session>;
|
|
72
|
+
updateSession(id: string, updates: SessionUpdates): Session | null | Promise<Session | null>;
|
|
73
|
+
getWorkspaceAutoApproveSeverity(workspaceId: string): Promise<Session['autoApproveSeverity']>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SubagentServicePreconfigs {
|
|
77
|
+
getPreconfigOrAgent(id: string): Promise<Preconfig | null>;
|
|
78
|
+
listSubagentPreconfigs(): Promise<Preconfig[]>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SubagentServiceBroadcasts {
|
|
82
|
+
event: BroadcastFn;
|
|
83
|
+
sessionCreated: BroadcastSessionFn;
|
|
84
|
+
sessionUpdated: BroadcastSessionFn;
|
|
85
|
+
toSession: (sessionId: string, event: RuntimeEvent) => void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface SubagentServiceDeps {
|
|
89
|
+
sessionAccess: SubagentServiceSessionAccess;
|
|
90
|
+
preconfigs: SubagentServicePreconfigs;
|
|
91
|
+
broadcasts: SubagentServiceBroadcasts;
|
|
92
|
+
executeChild: typeof executeChildSession;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Pure assembly of the task tool definition from a resolved target list.
|
|
96
|
+
* Moved byte-for-byte from the pre-C5 `getSubagentToolDefinition` assembly. */
|
|
97
|
+
export function buildTaskToolDefinition(subagents: Preconfig[]): ToolDefinition {
|
|
98
|
+
const agentList = subagents
|
|
99
|
+
.map((a) => `- ${a.id}: ${a.description ?? 'This subagent should only be called manually by the user.'}`)
|
|
100
|
+
.join('\n');
|
|
101
|
+
|
|
102
|
+
const subagentTypeEnum = subagents.length > 0
|
|
103
|
+
? subagents.map(s => s.id)
|
|
104
|
+
: [];
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
name: 'task',
|
|
108
|
+
description: `Launch a new agent to handle complex, multistep tasks autonomously.
|
|
109
|
+
|
|
110
|
+
Available agent types and the tools they have access to:
|
|
111
|
+
${agentList}
|
|
112
|
+
|
|
113
|
+
When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
|
|
114
|
+
|
|
115
|
+
Usage notes:
|
|
116
|
+
1. Launch multiple agents concurrently whenever possible, to maximize performance
|
|
117
|
+
2. The agent's outputs should generally be trusted
|
|
118
|
+
3. Each agent invocation starts with a fresh context unless you provide task_id to resume the same subagent session (which continues with its previous messages and tool outputs). When starting fresh, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
|
|
119
|
+
4. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches), since it is not aware of the user's intent. Tell it how to verify its work if possible (e.g., relevant test commands).
|
|
120
|
+
5. If the agent description mentions that it should be proactively used, then you should try your best to use it without the user having to ask you to do so first. Use your judgement.
|
|
121
|
+
6. Use the outputSchema parameter (a JSON Schema) to get structured, machine-readable output from a subagent instead of a free-text prose response. The subagent will be constrained to return JSON conforming to that schema.
|
|
122
|
+
|
|
123
|
+
When to use outputSchema (IMPORTANT - prefer it over free text in these cases):
|
|
124
|
+
- AGGREGATION: When spawning multiple parallel agents and you need to combine, filter, compare, or deduplicate their results. Define a shared schema for all agents so you can merge results programmatically.
|
|
125
|
+
- DATA EXTRACTION: When a subagent is searching, reading, or analyzing something and you need specific fields back (e.g., { findings: [{ file, line, issue, severity }], summary: string }).
|
|
126
|
+
- DECISIONS: When a subagent must return a verdict, classification, or yes/no with reasoning (e.g., { approved: boolean, concerns: string[], confidence: number }).
|
|
127
|
+
- LIST GENERATION: When a subagent finds or produces a list you need to iterate over (e.g., { files: string[], commands: string[] }).
|
|
128
|
+
|
|
129
|
+
When NOT to use it:
|
|
130
|
+
- The task is exploratory and the output shape is unpredictable (e.g., "summarize what you found about X").
|
|
131
|
+
- The subagent is writing code or modifying files directly — its result is the code changes, not a report.
|
|
132
|
+
|
|
133
|
+
Pattern for aggregation (map-reduce): define one schema, spawn N agents each with that outputSchema, then in your next turn merge the returned JSON objects. This keeps your context clean because you can reason about the data instead of re-parsing prose from each agent.
|
|
134
|
+
|
|
135
|
+
Note: Subagent depth is limited to 2 levels. You cannot spawn further subagents at the maximum depth.`,
|
|
136
|
+
timeout: 300000,
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
description: {
|
|
141
|
+
type: 'string',
|
|
142
|
+
description: 'A short (3-5 words) description of the task',
|
|
143
|
+
},
|
|
144
|
+
prompt: {
|
|
145
|
+
type: 'string',
|
|
146
|
+
description:
|
|
147
|
+
'The task for the agent to perform. Should contain a highly detailed task description specifying exactly what information the agent should return in its final message',
|
|
148
|
+
},
|
|
149
|
+
subagent_type: {
|
|
150
|
+
type: 'string',
|
|
151
|
+
description: 'The type of specialized agent to use for this agent',
|
|
152
|
+
...(subagentTypeEnum.length > 0 && { enum: subagentTypeEnum }),
|
|
153
|
+
},
|
|
154
|
+
task_id: {
|
|
155
|
+
type: 'string',
|
|
156
|
+
description:
|
|
157
|
+
'Set this to resume a previous subagent session (continues with its previous messages and tool outputs)',
|
|
158
|
+
},
|
|
159
|
+
outputSchema: {
|
|
160
|
+
type: 'object',
|
|
161
|
+
description: 'Optional JSON Schema that the subagent must conform to in its final response. Use this when you need structured, parseable output (e.g., extracted data, categorized findings, structured analysis). When omitted, the subagent returns free text.',
|
|
162
|
+
additionalProperties: true,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
required: ['description', 'prompt', 'subagent_type'],
|
|
166
|
+
},
|
|
167
|
+
outputSchema: {
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: {
|
|
170
|
+
task_id: { type: 'string' },
|
|
171
|
+
result: { type: 'string' },
|
|
172
|
+
error: { type: 'string' },
|
|
173
|
+
structuredResult: { type: 'object', additionalProperties: true },
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function computeSessionDepth(sessionId: string, getSessionFn: (id: string) => Session | null | Promise<Session | null>): Promise<number> {
|
|
180
|
+
return (await collectSubagentAncestry(sessionId, getSessionFn)).depth;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Unscoped depth gate: reads the module-level session accessor. */
|
|
184
|
+
export async function canSpawnSubagent(sessionId: string): Promise<boolean> {
|
|
185
|
+
return canSpawnSubagentWithDeps(sessionId, getSession);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Composed depth gate over the captured session lookup. */
|
|
189
|
+
export async function canSpawnSubagentWithDeps(
|
|
190
|
+
sessionId: string,
|
|
191
|
+
getSessionFn: (id: string) => Session | null | Promise<Session | null>,
|
|
192
|
+
): Promise<boolean> {
|
|
193
|
+
const depth = await computeSessionDepth(sessionId, getSessionFn);
|
|
194
|
+
return depth < MAX_SUBAGENT_DEPTH;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface GetSubagentToolDefinitionOptions {
|
|
198
|
+
sessionId: string;
|
|
199
|
+
canSpawnSubagents: boolean | string[] | null | undefined;
|
|
200
|
+
allowSelfAsSubagent?: boolean;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Unscoped definition resolution: reads the module-level session and
|
|
204
|
+
* preconfig accessors exactly like the pre-C5 path. */
|
|
205
|
+
export async function getSubagentToolDefinition(
|
|
206
|
+
options: GetSubagentToolDefinitionOptions,
|
|
207
|
+
): Promise<ToolDefinition | null> {
|
|
208
|
+
return resolveTaskToolDefinitionWithDeps(options, {
|
|
209
|
+
getSession,
|
|
210
|
+
listPreconfigs: listSubagentPreconfigs,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Composed definition resolution over injected lookups. */
|
|
215
|
+
export async function resolveTaskToolDefinitionWithDeps(
|
|
216
|
+
options: GetSubagentToolDefinitionOptions,
|
|
217
|
+
deps: { getSession: (id: string) => Promise<Session | null>; listPreconfigs: () => Promise<Preconfig[]> },
|
|
218
|
+
): Promise<ToolDefinition | null> {
|
|
219
|
+
const subagents = await resolveEffectiveSubagentTargets({
|
|
220
|
+
sessionId: options.sessionId,
|
|
221
|
+
canSpawnSubagents: options.canSpawnSubagents,
|
|
222
|
+
allowSelfAsSubagent: options.allowSelfAsSubagent,
|
|
223
|
+
maximumDepthReached: !(await canSpawnSubagentWithDeps(options.sessionId, deps.getSession)),
|
|
224
|
+
} as ResolveSubagentTargetsOptions, deps);
|
|
225
|
+
|
|
226
|
+
if (subagents.length === 0) return null;
|
|
227
|
+
|
|
228
|
+
return buildTaskToolDefinition(subagents);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function moduleServiceDeps(): SubagentServiceDeps {
|
|
232
|
+
return {
|
|
233
|
+
sessionAccess: {
|
|
234
|
+
getSession,
|
|
235
|
+
createSession,
|
|
236
|
+
updateSession,
|
|
237
|
+
getWorkspaceAutoApproveSeverity,
|
|
238
|
+
},
|
|
239
|
+
preconfigs: {
|
|
240
|
+
getPreconfigOrAgent,
|
|
241
|
+
listSubagentPreconfigs,
|
|
242
|
+
},
|
|
243
|
+
broadcasts: {
|
|
244
|
+
event: emitRuntimeEvent,
|
|
245
|
+
sessionCreated: emitSessionCreated,
|
|
246
|
+
sessionUpdated: emitSessionUpdated,
|
|
247
|
+
toSession: (sessionId, event) => emitToSession(sessionId, event),
|
|
248
|
+
},
|
|
249
|
+
executeChild: executeChildSession,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Unscoped execution: keeps the pre-C5 module-accessor behavior and the
|
|
254
|
+
* `executeChild` override seam. */
|
|
255
|
+
export async function executeSubagent(input: SubagentInput): Promise<SubagentOutput> {
|
|
256
|
+
return runSubagent(input, moduleServiceDeps());
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Composed execution over the dependencies captured by the domain plugin. */
|
|
260
|
+
export async function executeSubagentWithDeps(
|
|
261
|
+
input: SubagentInput,
|
|
262
|
+
deps: SubagentServiceDeps,
|
|
263
|
+
): Promise<SubagentOutput> {
|
|
264
|
+
return runSubagent(input, deps);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function runSubagent(
|
|
268
|
+
input: SubagentInput,
|
|
269
|
+
deps: SubagentServiceDeps,
|
|
270
|
+
): Promise<SubagentOutput> {
|
|
271
|
+
const {
|
|
272
|
+
description,
|
|
273
|
+
prompt,
|
|
274
|
+
subagent_type,
|
|
275
|
+
task_id,
|
|
276
|
+
sessionId,
|
|
277
|
+
workspaceId,
|
|
278
|
+
workspacePath,
|
|
279
|
+
abortSignal,
|
|
280
|
+
onSessionCreated,
|
|
281
|
+
allowedSubagentIds,
|
|
282
|
+
broadcast: broadcastFn = deps.broadcasts.event,
|
|
283
|
+
broadcastSessionCreated: broadcastSessCreated = deps.broadcasts.sessionCreated,
|
|
284
|
+
broadcastSessionUpdated: broadcastSessUpdated = deps.broadcasts.sessionUpdated,
|
|
285
|
+
broadcastToSession: broadcastToSessionFn,
|
|
286
|
+
outputSchema,
|
|
287
|
+
executeChild = deps.executeChild,
|
|
288
|
+
} = input;
|
|
289
|
+
|
|
290
|
+
const getSessionFn = deps.sessionAccess.getSession;
|
|
291
|
+
const getPreconfigOrAgentFn = deps.preconfigs.getPreconfigOrAgent;
|
|
292
|
+
const listSubagentPreconfigsFn = deps.preconfigs.listSubagentPreconfigs;
|
|
293
|
+
const createSessionFn = deps.sessionAccess.createSession;
|
|
294
|
+
const updateSessionFn = deps.sessionAccess.updateSession;
|
|
295
|
+
const getWorkspaceAutoApproveSeverityFn = deps.sessionAccess.getWorkspaceAutoApproveSeverity;
|
|
296
|
+
|
|
297
|
+
// Check if already aborted before starting
|
|
298
|
+
if (abortSignal?.aborted) {
|
|
299
|
+
return {
|
|
300
|
+
task_id: '',
|
|
301
|
+
result: '',
|
|
302
|
+
error: 'Subagent execution aborted before start',
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Get parent session for model inheritance
|
|
307
|
+
const parentSession = await getSessionFn(sessionId);
|
|
308
|
+
|
|
309
|
+
// Resolve parent's actual model using same fallback chain as main chat:
|
|
310
|
+
// session > parent preconfig > config default
|
|
311
|
+
const parentPreconfig = parentSession?.preconfigId
|
|
312
|
+
? await getPreconfigOrAgentFn(parentSession.preconfigId)
|
|
313
|
+
: null;
|
|
314
|
+
const parentModelId = resolveModelId(parentSession, parentPreconfig);
|
|
315
|
+
const parentProviderId = resolveProviderId(parentSession, parentPreconfig);
|
|
316
|
+
|
|
317
|
+
// Check depth limit
|
|
318
|
+
const currentDepth = await computeSessionDepth(sessionId, getSessionFn);
|
|
319
|
+
if (currentDepth >= MAX_SUBAGENT_DEPTH) {
|
|
320
|
+
return {
|
|
321
|
+
task_id: '',
|
|
322
|
+
result: '',
|
|
323
|
+
error: `Maximum subagent depth (${MAX_SUBAGENT_DEPTH}) reached. Cannot spawn more subagents.`,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (parentPreconfig && isSubagentSpawningDisabled(parentPreconfig.canSpawnSubagents)) {
|
|
328
|
+
return {
|
|
329
|
+
task_id: '',
|
|
330
|
+
result: '',
|
|
331
|
+
error: 'Subagent spawning is disabled for this agent.',
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Validate subagent_type against the effective allowed list
|
|
336
|
+
const configuredAllowedIds = Array.isArray(parentPreconfig?.canSpawnSubagents)
|
|
337
|
+
? [...parentPreconfig.canSpawnSubagents]
|
|
338
|
+
: allowedSubagentIds ? [...allowedSubagentIds] : undefined;
|
|
339
|
+
if (parentSession?.preconfigId && parentPreconfig?.allowSelfAsSubagent) {
|
|
340
|
+
configuredAllowedIds?.push(parentSession.preconfigId);
|
|
341
|
+
}
|
|
342
|
+
if (configuredAllowedIds && !configuredAllowedIds.includes(subagent_type)) {
|
|
343
|
+
return {
|
|
344
|
+
task_id: '',
|
|
345
|
+
result: '',
|
|
346
|
+
error: `Subagent type "${subagent_type}" is not allowed for this agent. Allowed types: ${configuredAllowedIds.join(', ')}`,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const ancestry = await collectSubagentAncestry(sessionId, getSessionFn);
|
|
351
|
+
const policy = evaluateSubagentTarget({
|
|
352
|
+
targetPreconfigId: subagent_type,
|
|
353
|
+
currentPreconfigId: parentSession?.preconfigId ?? null,
|
|
354
|
+
ancestryPreconfigIds: ancestry.preconfigIds,
|
|
355
|
+
allowSelfAsSubagent: parentPreconfig?.allowSelfAsSubagent === true,
|
|
356
|
+
});
|
|
357
|
+
if (!policy.allowed) {
|
|
358
|
+
return {
|
|
359
|
+
task_id: '',
|
|
360
|
+
result: '',
|
|
361
|
+
error: policy.error,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let childSession: Session | undefined | null;
|
|
366
|
+
let resumeFromHistory = false;
|
|
367
|
+
|
|
368
|
+
// Set up abort handling variables outside try block for finally access
|
|
369
|
+
let wasAborted = false;
|
|
370
|
+
const abortHandler = () => {
|
|
371
|
+
wasAborted = true;
|
|
372
|
+
if (childSession) {
|
|
373
|
+
void (async () => {
|
|
374
|
+
await updateSessionFn(childSession!.id, { subagentStatus: 'interrupted' });
|
|
375
|
+
const updatedSession = await getSessionFn(childSession!.id);
|
|
376
|
+
if (updatedSession) {
|
|
377
|
+
broadcastSessUpdated(updatedSession);
|
|
378
|
+
}
|
|
379
|
+
})().catch((err: unknown) => {
|
|
380
|
+
console.error('[executeSubagent] Failed to persist abort status', err);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
const subagentPreconfig = await getPreconfigOrAgentFn(subagent_type);
|
|
387
|
+
if (!subagentPreconfig) {
|
|
388
|
+
const available = await listSubagentPreconfigsFn();
|
|
389
|
+
const availableNames = available.map((s) => s.id).join(', ');
|
|
390
|
+
return {
|
|
391
|
+
task_id: '',
|
|
392
|
+
result: '',
|
|
393
|
+
error: `Unknown subagent type: "${subagent_type}". Available subagents: ${availableNames || 'none'}`,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!isValidSubagentTargetPreconfig(
|
|
398
|
+
subagentPreconfig,
|
|
399
|
+
parentSession?.preconfigId ?? null,
|
|
400
|
+
parentPreconfig?.allowSelfAsSubagent === true,
|
|
401
|
+
)) {
|
|
402
|
+
return {
|
|
403
|
+
task_id: '',
|
|
404
|
+
result: '',
|
|
405
|
+
error: `Preconfig "${subagent_type}" cannot be used as a subagent.`,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (task_id) {
|
|
410
|
+
childSession = await getSessionFn(task_id);
|
|
411
|
+
if (!childSession) {
|
|
412
|
+
childSession = null;
|
|
413
|
+
} else {
|
|
414
|
+
const resumeError = getSubagentResumeError(childSession, sessionId, subagent_type);
|
|
415
|
+
if (resumeError) {
|
|
416
|
+
return {
|
|
417
|
+
task_id: '',
|
|
418
|
+
result: '',
|
|
419
|
+
error: resumeError,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
resumeFromHistory = true;
|
|
424
|
+
await updateSessionFn(childSession.id, { subagentStatus: 'running' });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!childSession) {
|
|
429
|
+
childSession = await createSessionFn({
|
|
430
|
+
id: randomUUID(),
|
|
431
|
+
workspaceId: workspaceId || parentSession?.workspaceId || '',
|
|
432
|
+
preconfigId: subagent_type,
|
|
433
|
+
title: `${description} (@${subagent_type} subagent)`,
|
|
434
|
+
status: 'active',
|
|
435
|
+
metadata: null,
|
|
436
|
+
parentId: sessionId,
|
|
437
|
+
agentName: subagent_type,
|
|
438
|
+
subagentStatus: 'running',
|
|
439
|
+
selectedModel: subagentPreconfig.model !== null
|
|
440
|
+
? subagentPreconfig.model
|
|
441
|
+
: parentModelId,
|
|
442
|
+
selectedProvider: subagentPreconfig.provider !== null
|
|
443
|
+
? subagentPreconfig.provider
|
|
444
|
+
: parentProviderId,
|
|
445
|
+
selectedVariant: subagentPreconfig.variant ?? null,
|
|
446
|
+
autoApproveSeverity: await getWorkspaceAutoApproveSeverityFn(workspaceId || ''),
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
broadcastSessCreated(childSession);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Notify caller of the child session ID immediately
|
|
453
|
+
if (onSessionCreated) {
|
|
454
|
+
await onSessionCreated(childSession.id);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Add abort listener to update child session status if parent aborts
|
|
458
|
+
if (abortSignal) {
|
|
459
|
+
abortSignal.addEventListener('abort', abortHandler);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Wrap inline schema as a transient ResponseFormat so the existing
|
|
463
|
+
// structured output pipeline in agent.ts applies to the subagent.
|
|
464
|
+
const responseFormat: ResponseFormat | undefined = outputSchema
|
|
465
|
+
? {
|
|
466
|
+
id: `inline-task-${randomUUID()}`,
|
|
467
|
+
name: 'Task Output',
|
|
468
|
+
schema: outputSchema,
|
|
469
|
+
createdAt: Date.now(),
|
|
470
|
+
updatedAt: Date.now(),
|
|
471
|
+
}
|
|
472
|
+
: undefined;
|
|
473
|
+
|
|
474
|
+
const result = await executeChild({
|
|
475
|
+
parentSessionId: sessionId,
|
|
476
|
+
childSessionId: childSession.id,
|
|
477
|
+
preconfig: subagentPreconfig,
|
|
478
|
+
prompt,
|
|
479
|
+
workspacePath,
|
|
480
|
+
workspaceId,
|
|
481
|
+
resumeFromHistory,
|
|
482
|
+
// Inherit model from parent if preconfig doesn't specify one
|
|
483
|
+
modelId: subagentPreconfig.model !== null
|
|
484
|
+
? subagentPreconfig.model
|
|
485
|
+
: parentModelId,
|
|
486
|
+
providerId: subagentPreconfig.provider !== null
|
|
487
|
+
? subagentPreconfig.provider
|
|
488
|
+
: parentProviderId,
|
|
489
|
+
variant: subagentPreconfig.variant ?? undefined,
|
|
490
|
+
broadcast: broadcastFn,
|
|
491
|
+
abortSignal,
|
|
492
|
+
broadcastToSession: broadcastToSessionFn ?? ((event: Parameters<BroadcastFn>[0]) => {
|
|
493
|
+
deps.broadcasts.toSession(sessionId, event);
|
|
494
|
+
}),
|
|
495
|
+
...(responseFormat ? { responseFormat } : {}),
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// Check if was aborted during execution
|
|
499
|
+
if (wasAborted) {
|
|
500
|
+
return {
|
|
501
|
+
task_id: childSession.id,
|
|
502
|
+
result: '',
|
|
503
|
+
error: 'Subagent execution was interrupted',
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Update subagent status based on execution result
|
|
508
|
+
if (result.error) {
|
|
509
|
+
await updateSessionFn(childSession.id, { subagentStatus: 'error' });
|
|
510
|
+
const updatedSession = await getSessionFn(childSession.id);
|
|
511
|
+
if (updatedSession) {
|
|
512
|
+
broadcastSessUpdated(updatedSession);
|
|
513
|
+
}
|
|
514
|
+
} else {
|
|
515
|
+
await updateSessionFn(childSession.id, { subagentStatus: 'completed' });
|
|
516
|
+
const updatedSession = await getSessionFn(childSession.id);
|
|
517
|
+
if (updatedSession) {
|
|
518
|
+
broadcastSessUpdated(updatedSession);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Extract ONLY the last text part - matching OpenCode behavior
|
|
523
|
+
// Note: executeChildSession now returns parts instead of content
|
|
524
|
+
const text = result.parts
|
|
525
|
+
.filter((part): part is TextPart => part.type === 'text')
|
|
526
|
+
.map((part) => part.text || '')
|
|
527
|
+
.pop() ?? '';
|
|
528
|
+
|
|
529
|
+
// Extract structured output if it was captured
|
|
530
|
+
const structuredResult = result.structuredOutput?.data;
|
|
531
|
+
|
|
532
|
+
// If there's an error and no text, return it directly
|
|
533
|
+
if (result.error && !text) {
|
|
534
|
+
return {
|
|
535
|
+
task_id: childSession.id,
|
|
536
|
+
result: '',
|
|
537
|
+
error: result.error,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Format output like OpenCode with task_result tags
|
|
542
|
+
const output = [
|
|
543
|
+
`task_id: ${childSession.id} (for resuming to continue this task if needed)`,
|
|
544
|
+
'',
|
|
545
|
+
'<task_result>',
|
|
546
|
+
text || 'No response generated',
|
|
547
|
+
'</task_result>',
|
|
548
|
+
structuredResult
|
|
549
|
+
? '\n<structured_result>\n' + JSON.stringify(structuredResult, null, 2) + '\n</structured_result>'
|
|
550
|
+
: '',
|
|
551
|
+
].join('\n');
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
task_id: childSession.id,
|
|
555
|
+
result: output,
|
|
556
|
+
...(structuredResult ? { structuredResult } : {}),
|
|
557
|
+
...(result.error && { error: result.error }),
|
|
558
|
+
};
|
|
559
|
+
} catch (err: unknown) {
|
|
560
|
+
console.error('[executeSubagent] AI SDK error', {
|
|
561
|
+
sessionId,
|
|
562
|
+
childSessionId: childSession?.id,
|
|
563
|
+
subagentType: subagent_type,
|
|
564
|
+
rawError: err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : err,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
if (childSession) {
|
|
568
|
+
await updateSessionFn(childSession.id, { subagentStatus: abortSignal?.aborted ? 'interrupted' : 'error' });
|
|
569
|
+
const updatedSession = await getSessionFn(childSession.id);
|
|
570
|
+
if (updatedSession) {
|
|
571
|
+
broadcastSessUpdated(updatedSession);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
task_id: childSession?.id ?? '',
|
|
576
|
+
result: '',
|
|
577
|
+
error: `Task tool error: ${err instanceof Error ? err.message : String(err)}`,
|
|
578
|
+
};
|
|
579
|
+
} finally {
|
|
580
|
+
if (abortSignal) {
|
|
581
|
+
abortSignal.removeEventListener('abort', abortHandler);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C6 tool-output policy contracts.
|
|
3
|
+
*
|
|
4
|
+
* The agent-scoped tool-output service owns the model-facing decisions that
|
|
5
|
+
* previously lived as module-level constants and a module-global WeakSet in
|
|
6
|
+
* `tools/tool-output-artifacts.ts` plus the legacy filesystem truncation in
|
|
7
|
+
* `utils/truncate-tool-result.ts`:
|
|
8
|
+
*
|
|
9
|
+
* - Bounding thresholds: 50k serialized chars threshold, 10k preview chars,
|
|
10
|
+
* 10k default page chars, 20k max page chars.
|
|
11
|
+
* - The stable artifact envelope: `type: 'tool-output-artifact'` with the
|
|
12
|
+
* strict artifact id, bounded preview, format, totalChars, and the exact
|
|
13
|
+
* retrieval message; the bounded fallback envelope for unserializable
|
|
14
|
+
* output or failed persistence (`tool-output-preview`).
|
|
15
|
+
* - The model-facing truncation decisions of the legacy
|
|
16
|
+
* `truncateToolResult` (exact note strings, `_persisted`/`_filePath`/
|
|
17
|
+
* `_originalSize` metadata, and the exact synchronous filesystem write
|
|
18
|
+
* behavior, including its pre-C6 non-fail-open filesystem errors).
|
|
19
|
+
* - Retrieval: strict ID validation and session-scoped page retrieval
|
|
20
|
+
* remain mandatory invariants enforced by the storage layer
|
|
21
|
+
* (`isToolOutputArtifactId` and the session match); the service owns the
|
|
22
|
+
* retrieval tool construction and the exact `Tool output artifact not
|
|
23
|
+
* found` failure.
|
|
24
|
+
*
|
|
25
|
+
* There is no current environment source for these thresholds; the plugin
|
|
26
|
+
* freezes the exact current constants into provider options at composition
|
|
27
|
+
* (the same documented pattern as the generic ask timeout in C6 step 3).
|
|
28
|
+
* The page-size limits (10k default, 20k max) are NOT options: they are
|
|
29
|
+
* mandatory storage-layer invariants (`DEFAULT_TOOL_OUTPUT_PAGE_CHARS` and
|
|
30
|
+
* `MAX_TOOL_OUTPUT_PAGE_CHARS` in `storage/tool-output-artifacts.ts`), so a
|
|
31
|
+
* custom provider cannot change retrieval pagination. The
|
|
32
|
+
* serialization/persistence failure behavior stays fail-open exactly
|
|
33
|
+
* where the current behavior is fail-open (the policy returns the bounded
|
|
34
|
+
* preview and never breaks the original tool result); the legacy filesystem
|
|
35
|
+
* truncation keeps its exact pre-C6 non-fail-open error propagation.
|
|
36
|
+
*
|
|
37
|
+
* Compression/observe mode is NOT part of this slice: no compression code
|
|
38
|
+
* exists in this branch (only an untracked plan document), so no observe
|
|
39
|
+
* behavior is introduced.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import type { Tool } from 'ai';
|
|
43
|
+
import type { ToolOutputArtifactFormat, ToolOutputArtifactPage } from '../storage/contracts';
|
|
44
|
+
|
|
45
|
+
export interface ToolOutputPolicyOptions {
|
|
46
|
+
/** Serialized-char threshold above which output becomes an artifact. */
|
|
47
|
+
thresholdChars: number;
|
|
48
|
+
/** Bounded preview length for the model-facing envelope. */
|
|
49
|
+
previewChars: number;
|
|
50
|
+
/** Exact retrieval tool name. */
|
|
51
|
+
retrievalToolName: string;
|
|
52
|
+
/** Legacy filesystem truncation threshold in serialized chars. */
|
|
53
|
+
truncationMaxChars: number;
|
|
54
|
+
/** Legacy filesystem truncation preview length in chars. */
|
|
55
|
+
truncationPreviewChars: number;
|
|
56
|
+
/** Legacy filesystem truncation temp directory root. */
|
|
57
|
+
truncationTempDir: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ToolOutputArtifactReference {
|
|
61
|
+
type: 'tool-output-artifact';
|
|
62
|
+
artifactId: string;
|
|
63
|
+
preview: string;
|
|
64
|
+
format: ToolOutputArtifactFormat;
|
|
65
|
+
totalChars: number;
|
|
66
|
+
complete: false;
|
|
67
|
+
message: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ToolOutputFallback {
|
|
71
|
+
type: 'tool-output-preview';
|
|
72
|
+
preview: string;
|
|
73
|
+
totalChars: number | null;
|
|
74
|
+
complete: false;
|
|
75
|
+
message: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ToolOutputPolicyContext {
|
|
79
|
+
sessionId: string;
|
|
80
|
+
workspaceId?: string;
|
|
81
|
+
toolCallId: string;
|
|
82
|
+
toolName: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ToolOutputArtifactService {
|
|
86
|
+
readonly id: string;
|
|
87
|
+
/** Frozen composition-time options. */
|
|
88
|
+
readonly options: Readonly<ToolOutputPolicyOptions>;
|
|
89
|
+
/** Applies the artifact envelope decision to one tool result. */
|
|
90
|
+
applyToolOutputPolicy(result: unknown, context: ToolOutputPolicyContext): Promise<unknown>;
|
|
91
|
+
/** Session-scoped retrieval over the active storage bundle. */
|
|
92
|
+
retrieveToolOutput(
|
|
93
|
+
sessionId: string,
|
|
94
|
+
input: { artifactId: string; offset?: number; limit?: number },
|
|
95
|
+
): Promise<ToolOutputArtifactPage | null>;
|
|
96
|
+
/** Builds the AI SDK retrieval tool bound to a session. */
|
|
97
|
+
buildRetrieveToolOutputAiTool(sessionId: string): Tool;
|
|
98
|
+
/** Wraps a tool map with the policy; per-service WeakSet excludes
|
|
99
|
+
* self-wrapping, the retrieval tool, and non-function tools. */
|
|
100
|
+
wrapToolsWithOutputPolicy(
|
|
101
|
+
tools: Record<string, Tool>,
|
|
102
|
+
context: Pick<ToolOutputPolicyContext, 'sessionId' | 'workspaceId'>,
|
|
103
|
+
): Record<string, Tool>;
|
|
104
|
+
/** Legacy filesystem truncation with the exact pre-C6 behavior. */
|
|
105
|
+
truncateToolResult(
|
|
106
|
+
result: unknown,
|
|
107
|
+
sessionId: string,
|
|
108
|
+
toolName: string,
|
|
109
|
+
outputDir?: string,
|
|
110
|
+
): unknown;
|
|
111
|
+
}
|