@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.
Files changed (148) hide show
  1. package/README.md +12 -0
  2. package/package.json +105 -0
  3. package/src/adapters/ai-sdk.ts +84 -0
  4. package/src/compaction/contracts.ts +82 -0
  5. package/src/compaction/executor.ts +161 -0
  6. package/src/compaction/policy.ts +318 -0
  7. package/src/compaction/recovery.ts +139 -0
  8. package/src/compaction/task.ts +540 -0
  9. package/src/configuration/contracts.ts +58 -0
  10. package/src/configuration/defaults.ts +27 -0
  11. package/src/configuration/runtime.ts +42 -0
  12. package/src/configuration/single-model.ts +75 -0
  13. package/src/context/assembler.ts +112 -0
  14. package/src/context/index.ts +2 -0
  15. package/src/context/sources.ts +119 -0
  16. package/src/context/workspace.ts +63 -0
  17. package/src/core/agent.ts +401 -0
  18. package/src/core/build-tools.ts +139 -0
  19. package/src/core/chat-handler.ts +858 -0
  20. package/src/core/error-handling.ts +18 -0
  21. package/src/core/fork.ts +103 -0
  22. package/src/core/interrupt.ts +192 -0
  23. package/src/core/message-utils.ts +261 -0
  24. package/src/core/model-utils.ts +149 -0
  25. package/src/core/part-utils.ts +88 -0
  26. package/src/core/provider-utils.ts +67 -0
  27. package/src/core/revert.ts +46 -0
  28. package/src/core/step-handlers.ts +157 -0
  29. package/src/core/stream/finalization.ts +65 -0
  30. package/src/core/stream/stream-config.ts +82 -0
  31. package/src/core/stream-handlers.ts +242 -0
  32. package/src/core/structured-output.ts +68 -0
  33. package/src/core/tool-builders/agent-tools.ts +71 -0
  34. package/src/core/tool-builders/external-tools.ts +179 -0
  35. package/src/core/tool-builders/types.ts +16 -0
  36. package/src/core/tool-builders/workspace-tools.ts +293 -0
  37. package/src/core/tool-capabilities.ts +65 -0
  38. package/src/goals/evaluator.ts +171 -0
  39. package/src/goals/index.ts +3 -0
  40. package/src/goals/loop.ts +167 -0
  41. package/src/goals/service.ts +39 -0
  42. package/src/index.ts +10 -0
  43. package/src/internal/ask-authority.ts +29 -0
  44. package/src/internal/composition.ts +44 -0
  45. package/src/internal/configuration.ts +22 -0
  46. package/src/internal/execution.ts +108 -0
  47. package/src/internal/hosts.ts +64 -0
  48. package/src/internal/plugins.ts +71 -0
  49. package/src/internal/providers.ts +32 -0
  50. package/src/internal/sandbox.ts +19 -0
  51. package/src/internal/tools.ts +48 -0
  52. package/src/internal/workspace.ts +25 -0
  53. package/src/kernel/diagnostics.ts +249 -0
  54. package/src/kernel/errors.ts +120 -0
  55. package/src/kernel/events.ts +82 -0
  56. package/src/kernel/index.ts +72 -0
  57. package/src/kernel/kernel.ts +62 -0
  58. package/src/kernel/lifecycle.ts +72 -0
  59. package/src/kernel/plugin.ts +218 -0
  60. package/src/kernel/registry.ts +493 -0
  61. package/src/kernel/scope.ts +776 -0
  62. package/src/kernel/service-key.ts +19 -0
  63. package/src/kernel/types.ts +317 -0
  64. package/src/memory/index.ts +2 -0
  65. package/src/memory/memory-tool.ts +75 -0
  66. package/src/memory/registry.ts +172 -0
  67. package/src/permission/ask-user-api.ts +70 -0
  68. package/src/permission/contracts.ts +135 -0
  69. package/src/permission/permission-request-manager.ts +58 -0
  70. package/src/permission/policy.ts +277 -0
  71. package/src/permission/runtime.ts +612 -0
  72. package/src/plugins/compaction-policy.ts +46 -0
  73. package/src/plugins/compose.ts +171 -0
  74. package/src/plugins/context-sections.ts +246 -0
  75. package/src/plugins/default-agent-driver.ts +14 -0
  76. package/src/plugins/facade-plugins.ts +129 -0
  77. package/src/plugins/goal-domain.ts +82 -0
  78. package/src/plugins/legacy-system-message.ts +152 -0
  79. package/src/plugins/loaded-tools.ts +23 -0
  80. package/src/plugins/memory-domain.ts +264 -0
  81. package/src/plugins/orchestrator-session.ts +29 -0
  82. package/src/plugins/permission-policy.ts +49 -0
  83. package/src/plugins/retry-policy.ts +28 -0
  84. package/src/plugins/scheduler-domain.ts +192 -0
  85. package/src/plugins/service-keys.ts +294 -0
  86. package/src/plugins/session-search-domain.ts +238 -0
  87. package/src/plugins/skills-domain.ts +272 -0
  88. package/src/plugins/subagent-domain.ts +287 -0
  89. package/src/plugins/tool-catalog.ts +78 -0
  90. package/src/plugins/tool-output-policy.ts +52 -0
  91. package/src/plugins/value-plugins.ts +150 -0
  92. package/src/plugins/workflow-domain.ts +198 -0
  93. package/src/plugins/workspace-policy.ts +37 -0
  94. package/src/providers/registry.ts +63 -0
  95. package/src/providers/types.ts +44 -0
  96. package/src/retry/policy.ts +282 -0
  97. package/src/retry/stream-chat.ts +312 -0
  98. package/src/runtime/agent-runtime.ts +83 -0
  99. package/src/runtime/default-agent-driver.ts +23 -0
  100. package/src/runtime/domain-tool-source.ts +156 -0
  101. package/src/runtime/events.ts +61 -0
  102. package/src/runtime/host-dependencies.ts +71 -0
  103. package/src/runtime/host-guidance.ts +22 -0
  104. package/src/runtime/host-layout.ts +23 -0
  105. package/src/runtime/host.ts +129 -0
  106. package/src/runtime/standalone-host.ts +118 -0
  107. package/src/sandbox/controller.ts +204 -0
  108. package/src/sandbox/model.ts +305 -0
  109. package/src/sandbox/provider.ts +53 -0
  110. package/src/sandbox/types.ts +110 -0
  111. package/src/scheduler/host.ts +22 -0
  112. package/src/scheduler/scheduler-tool.ts +172 -0
  113. package/src/session-search/host.ts +56 -0
  114. package/src/session-search/index.ts +23 -0
  115. package/src/session-search/session-search-tool.ts +151 -0
  116. package/src/skills/index.ts +3 -0
  117. package/src/skills/registry.ts +63 -0
  118. package/src/skills/skill-manage-tool.ts +205 -0
  119. package/src/skills/skill-tool.ts +42 -0
  120. package/src/storage/contracts.ts +159 -0
  121. package/src/storage/memory.ts +321 -0
  122. package/src/storage/options.ts +75 -0
  123. package/src/storage/runtime.ts +115 -0
  124. package/src/storage/sqlite-tool-output-artifacts.ts +106 -0
  125. package/src/storage/sqlite.ts +321 -0
  126. package/src/storage/tool-output-artifacts.ts +75 -0
  127. package/src/storage.ts +31 -0
  128. package/src/subagent/child-session.ts +282 -0
  129. package/src/subagent/guidance.ts +8 -0
  130. package/src/subagent/policy.ts +198 -0
  131. package/src/subagent/task-tool.ts +584 -0
  132. package/src/tool-output/contracts.ts +111 -0
  133. package/src/tool-output/policy.ts +410 -0
  134. package/src/tool.ts +1 -0
  135. package/src/tools/executor.ts +258 -0
  136. package/src/tools/install-manifest.ts +40 -0
  137. package/src/tools/llm-api.ts +77 -0
  138. package/src/tools/registry.ts +206 -0
  139. package/src/tools/tool-artifact.ts +182 -0
  140. package/src/tools/tool-source.ts +53 -0
  141. package/src/utils/errors.ts +334 -0
  142. package/src/utils/strip-visualization.ts +50 -0
  143. package/src/workflow/decomposer.ts +139 -0
  144. package/src/workflow/execution.ts +523 -0
  145. package/src/workflow/orchestrator-session.ts +161 -0
  146. package/src/workflow/synthesizer.ts +130 -0
  147. package/src/workspace/contracts.ts +135 -0
  148. 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
+ }