@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,523 @@
1
+ import { randomUUID } from 'crypto';
2
+ import type { ToolDefinition } from '@capekai/tool';
3
+ import type { WorkflowInput, WorkflowResult, WorkflowSubtask } from '@capekai/types';
4
+ import type { BroadcastFn, BroadcastSessionFn } from '../runtime/host';
5
+ import { getSession } from '../storage/runtime';
6
+ import { listSubagentPreconfigs } from '../context';
7
+ import {
8
+ canSpawnSubagent,
9
+ canSpawnSubagentWithDeps,
10
+ type SubagentInput,
11
+ type SubagentOutput,
12
+ } from '../subagent/task-tool';
13
+ import {
14
+ resolveEffectiveSubagentTargets,
15
+ type ResolveSubagentTargetsOptions,
16
+ } from '../subagent/policy';
17
+ import {
18
+ decomposeTask,
19
+ decomposeTaskWithDeps,
20
+ type DecomposeTaskDeps,
21
+ type DecomposeTaskOptions,
22
+ } from './decomposer';
23
+ import {
24
+ synthesizeResults,
25
+ synthesizeResultsWithDeps,
26
+ type LeafResult,
27
+ type SynthesizeResultsDeps,
28
+ type SynthesizeResultsOptions,
29
+ } from './synthesizer';
30
+ import {
31
+ runOrchestratorSession,
32
+ type OrchestratorSessionOptions,
33
+ type OrchestratorSessionResult,
34
+ } from './orchestrator-session';
35
+ import { executeSubagent } from '../subagent/task-tool';
36
+
37
+ /**
38
+ * Workflow domain: the workflow tool definition and execution. Moved
39
+ * byte-for-byte from `core/workflow.ts`; the unscoped exports keep the
40
+ * pre-C5 module-accessor behavior, and `executeWorkflowWithDeps` /
41
+ * `resolveWorkflowToolDefinitionWithDeps` run against the injected subagent
42
+ * execution, depth, target, and orchestrator-session contracts captured by
43
+ * the domain plugin at composition. Composed scopes never read module
44
+ * globals.
45
+ */
46
+
47
+ export { canSpawnSubagent };
48
+
49
+ /** Hardcoded concurrency limit for leaf agents. */
50
+ export const MAX_CONCURRENCY = 5;
51
+
52
+ /**
53
+ * Simple concurrency-limited async pool.
54
+ * Runs items through fn with at most `limit` in flight at once.
55
+ */
56
+ async function runWithConcurrency<T, R>(
57
+ items: T[],
58
+ limit: number,
59
+ fn: (item: T, index: number) => Promise<R>,
60
+ abortSignal?: AbortSignal,
61
+ ): Promise<R[]> {
62
+ const results: R[] = [];
63
+ const executing = new Set<Promise<void>>();
64
+ const queue = items.map((item, index) => ({ item, index }));
65
+
66
+ while (queue.length > 0 || executing.size > 0) {
67
+ if (abortSignal?.aborted) {
68
+ queue.length = 0;
69
+ }
70
+ while (!abortSignal?.aborted && executing.size < limit && queue.length > 0) {
71
+ const { item, index } = queue.shift()!;
72
+ const p = fn(item, index).then((result) => {
73
+ results[index] = result;
74
+ });
75
+ const wrapped = p.then(() => {
76
+ executing.delete(wrapped);
77
+ });
78
+ executing.add(wrapped);
79
+ }
80
+ if (executing.size > 0) {
81
+ await Promise.race(executing);
82
+ }
83
+ }
84
+
85
+ return results;
86
+ }
87
+
88
+ export interface WorkflowExecutionOptions {
89
+ sessionId: string;
90
+ workspaceId?: string;
91
+ workspacePath?: string;
92
+ abortSignal?: AbortSignal;
93
+ broadcast?: BroadcastFn;
94
+ broadcastSessionCreated?: BroadcastSessionFn;
95
+ broadcastSessionUpdated?: BroadcastSessionFn;
96
+ broadcastToSession?: BroadcastFn;
97
+ allowedSubagentIds?: string[];
98
+ executeLeaf?: typeof executeSubagent;
99
+ decompose?: typeof decomposeTask;
100
+ synthesize?: typeof synthesizeResults;
101
+ }
102
+
103
+ /** Injected dependencies captured by the workflow domain plugin. */
104
+ export interface WorkflowServiceDeps {
105
+ canSpawn(sessionId: string): boolean | Promise<boolean>;
106
+ listSubagents(): Promise<import('@capekai/types').Preconfig[]>;
107
+ executeLeaf(input: SubagentInput): Promise<SubagentOutput>;
108
+ orchestrator: { run(options: OrchestratorSessionOptions): Promise<OrchestratorSessionResult> };
109
+ }
110
+
111
+ function moduleWorkflowDeps(): WorkflowServiceDeps {
112
+ return {
113
+ canSpawn: canSpawnSubagent,
114
+ listSubagents: listSubagentPreconfigs,
115
+ executeLeaf: executeSubagent,
116
+ orchestrator: { run: runOrchestratorSession },
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Execute a full workflow: decompose → fan out → synthesize.
122
+ * Unscoped path: the pre-C5 module accessors.
123
+ */
124
+ export async function executeWorkflow(
125
+ input: WorkflowInput,
126
+ options: WorkflowExecutionOptions,
127
+ ): Promise<WorkflowResult> {
128
+ return runWorkflow(input, options, moduleWorkflowDeps());
129
+ }
130
+
131
+ /** Composed execution over the dependencies captured by the domain plugin. */
132
+ export async function executeWorkflowWithDeps(
133
+ input: WorkflowInput,
134
+ options: WorkflowExecutionOptions,
135
+ deps: WorkflowServiceDeps,
136
+ ): Promise<WorkflowResult> {
137
+ return runWorkflow(input, options, deps);
138
+ }
139
+
140
+ async function runWorkflow(
141
+ input: WorkflowInput,
142
+ options: WorkflowExecutionOptions,
143
+ deps: WorkflowServiceDeps,
144
+ ): Promise<WorkflowResult> {
145
+ const executeLeaf = options.executeLeaf ?? deps.executeLeaf;
146
+ const decompose: typeof decomposeTask = options.decompose
147
+ ?? ((decomposeOptions: DecomposeTaskOptions) => decomposeTaskWithDeps(decomposeOptions, {
148
+ listSubagents: deps.listSubagents,
149
+ orchestrator: deps.orchestrator,
150
+ } as DecomposeTaskDeps));
151
+ const synthesize: typeof synthesizeResults = options.synthesize
152
+ ?? ((synthesizeOptions: SynthesizeResultsOptions) => synthesizeResultsWithDeps(synthesizeOptions, {
153
+ orchestrator: deps.orchestrator,
154
+ } as SynthesizeResultsDeps));
155
+ const workflowId = `wf-${randomUUID()}`;
156
+ console.log('[workflow] Starting workflow', {
157
+ workflowId,
158
+ sessionId: options.sessionId,
159
+ promptPreview: input.prompt.slice(0, 100),
160
+ hasSubtasks: !!input.subtasks?.length,
161
+ subtaskCount: input.subtasks?.length ?? 0,
162
+ leafPreconfigId: input.leafPreconfigId,
163
+ hasOutputSchema: !!input.outputSchema,
164
+ });
165
+
166
+ // Check depth limit — workflow leaf agents spawn as children of this session
167
+ if (!(await deps.canSpawn(options.sessionId))) {
168
+ console.warn('[workflow] Blocked: max subagent depth reached', { sessionId: options.sessionId });
169
+ return {
170
+ workflow_id: workflowId,
171
+ result: '',
172
+ subtaskCount: 0,
173
+ error: 'Maximum subagent depth reached. Cannot spawn workflow agents.',
174
+ };
175
+ }
176
+
177
+ if (input.leafPreconfigId && options.allowedSubagentIds && !options.allowedSubagentIds.includes(input.leafPreconfigId)) {
178
+ return {
179
+ workflow_id: workflowId,
180
+ result: '',
181
+ subtaskCount: 0,
182
+ error: `Subagent type "${input.leafPreconfigId}" is not allowed for this workflow.`,
183
+ };
184
+ }
185
+
186
+ if (input.subtasks && options.allowedSubagentIds) {
187
+ const invalidTarget = input.subtasks.find((subtask) => {
188
+ const target = subtask.preconfigId || 'explore';
189
+ return !options.allowedSubagentIds!.includes(target);
190
+ });
191
+ if (invalidTarget) {
192
+ const target = invalidTarget.preconfigId || 'explore';
193
+ return {
194
+ workflow_id: workflowId,
195
+ result: '',
196
+ subtaskCount: 0,
197
+ error: `Subagent type "${target}" is not allowed for this workflow.`,
198
+ };
199
+ }
200
+ }
201
+
202
+ // ── Phase 1: Decompose (or use provided subtasks) ──────────────────────
203
+ let subtasks: WorkflowSubtask[];
204
+
205
+ if (input.subtasks && input.subtasks.length > 0) {
206
+ console.log('[workflow] Phase 1: SKIPPED (explicit subtasks provided)', { count: input.subtasks.length });
207
+ subtasks = input.subtasks;
208
+ } else {
209
+ console.log('[workflow] Phase 1: Decomposing task...');
210
+ try {
211
+ subtasks = await decompose({
212
+ prompt: input.prompt,
213
+ parentSessionId: options.sessionId,
214
+ abortSignal: options.abortSignal,
215
+ allowedSubagentIds: options.allowedSubagentIds,
216
+ broadcast: options.broadcast,
217
+ broadcastSessionCreated: options.broadcastSessionCreated,
218
+ broadcastSessionUpdated: options.broadcastSessionUpdated,
219
+ });
220
+ console.log('[workflow] Phase 1: Decomposition complete', { subtaskCount: subtasks.length });
221
+ } catch (err) {
222
+ const errAny = err as Record<string, unknown>;
223
+ console.error('[workflow] Phase 1: Decomposition FAILED', {
224
+ message: err instanceof Error ? err.message : String(err),
225
+ statusCode: errAny?.statusCode ?? errAny?.status,
226
+ url: errAny?.url,
227
+ responseBody: errAny?.responseBody ?? errAny?.response,
228
+ data: errAny?.data,
229
+ });
230
+ return {
231
+ workflow_id: workflowId,
232
+ result: '',
233
+ subtaskCount: 0,
234
+ error: `Decomposition failed: ${err instanceof Error ? err.message : String(err)}`,
235
+ };
236
+ }
237
+ }
238
+
239
+ // Apply leafPreconfigId override if provided
240
+ if (input.leafPreconfigId) {
241
+ console.log('[workflow] Applying leafPreconfigId override to all subtasks', { leafPreconfigId: input.leafPreconfigId });
242
+ subtasks = subtasks.map((s) => ({ ...s, preconfigId: input.leafPreconfigId }));
243
+ }
244
+
245
+ // ── Phase 2: Fan out leaf agents ───────────────────────────────────────
246
+ console.log('[workflow] Phase 2: Fanning out leaf agents', {
247
+ count: subtasks.length,
248
+ maxConcurrency: MAX_CONCURRENCY,
249
+ subtasks: subtasks.map((s, i) => ({ i, preconfigId: s.preconfigId || '(default explore)', promptPreview: s.prompt.slice(0, 60) })),
250
+ });
251
+
252
+ let completedCount = 0;
253
+ let failedCount = 0;
254
+
255
+ const leafResults = await runWithConcurrency(
256
+ subtasks,
257
+ MAX_CONCURRENCY,
258
+ async (subtask: WorkflowSubtask, index: number): Promise<LeafResult> => {
259
+ const subagentType = subtask.preconfigId || 'explore';
260
+ const label = `${input.description || input.prompt.slice(0, 30)}... #${index + 1}`;
261
+ console.log('[workflow] Phase 2: Starting leaf agent', { index, subagentType, promptPreview: subtask.prompt.slice(0, 80) });
262
+
263
+ try {
264
+ const subagentInput: SubagentInput = {
265
+ description: label,
266
+ prompt: subtask.prompt,
267
+ subagent_type: subagentType,
268
+ sessionId: options.sessionId,
269
+ workspaceId: options.workspaceId,
270
+ workspacePath: options.workspacePath,
271
+ abortSignal: options.abortSignal,
272
+ allowedSubagentIds: options.allowedSubagentIds,
273
+ broadcast: options.broadcast,
274
+ broadcastSessionCreated: options.broadcastSessionCreated,
275
+ broadcastSessionUpdated: options.broadcastSessionUpdated,
276
+ broadcastToSession: options.broadcastToSession,
277
+ ...(subtask.outputSchema ? { outputSchema: subtask.outputSchema } : {}),
278
+ };
279
+
280
+ const result: SubagentOutput = await executeLeaf(subagentInput);
281
+ console.log('[workflow] Phase 2: Leaf agent completed', {
282
+ index,
283
+ taskId: result.task_id,
284
+ hasText: !!result.result,
285
+ hasStructured: !!result.structuredResult,
286
+ hasError: !!result.error,
287
+ errorPreview: result.error?.slice(0, 100),
288
+ });
289
+
290
+ if (result.error) {
291
+ failedCount++;
292
+ } else {
293
+ completedCount++;
294
+ }
295
+
296
+ return {
297
+ index,
298
+ text: result.result,
299
+ ...(result.structuredResult ? { structuredResult: result.structuredResult } : {}),
300
+ ...(result.error ? { error: result.error } : {}),
301
+ };
302
+ } catch (err) {
303
+ console.error('[workflow] Phase 2: Leaf agent FAILED', { index, error: err instanceof Error ? err.message : String(err) });
304
+ failedCount++;
305
+ return {
306
+ index,
307
+ text: '',
308
+ error: err instanceof Error ? err.message : String(err),
309
+ };
310
+ }
311
+ },
312
+ options.abortSignal,
313
+ );
314
+
315
+ console.log('[workflow] Phase 2: All leaf agents done', { completed: completedCount, failed: failedCount, total: subtasks.length });
316
+
317
+ // Check if aborted before interpreting leaf outcomes
318
+ if (options.abortSignal?.aborted) {
319
+ return {
320
+ workflow_id: workflowId,
321
+ result: '',
322
+ subtaskCount: subtasks.length,
323
+ error: 'Workflow was interrupted',
324
+ };
325
+ }
326
+
327
+ // If ALL leaf agents failed, bail early with a clear error
328
+ if (completedCount === 0 && failedCount > 0) {
329
+ const errors = leafResults.map(r => `Subtask ${r.index + 1}: ${r.error}`).join('; ');
330
+ return {
331
+ workflow_id: workflowId,
332
+ result: '',
333
+ subtaskCount: subtasks.length,
334
+ error: `All ${failedCount} sub-agent(s) failed. Errors: ${errors}`,
335
+ };
336
+ }
337
+
338
+ // ── Phase 3: Synthesize ────────────────────────────────────────────────
339
+ console.log('[workflow] Phase 3: Synthesizing results...');
340
+ try {
341
+ const synthesis = await synthesize({
342
+ originalPrompt: input.prompt,
343
+ leafResults,
344
+ ...(input.outputSchema ? { outputSchema: input.outputSchema } : {}),
345
+ parentSessionId: options.sessionId,
346
+ abortSignal: options.abortSignal,
347
+ broadcast: options.broadcast,
348
+ broadcastSessionCreated: options.broadcastSessionCreated,
349
+ broadcastSessionUpdated: options.broadcastSessionUpdated,
350
+ });
351
+
352
+ const resultText = [
353
+ `Workflow completed. ${subtasks.length} sub-agent(s) executed (${leafResults.filter(r => r.error).length} failed).`,
354
+ '',
355
+ synthesis.text,
356
+ ].join('\n');
357
+
358
+ console.log('[workflow] Phase 3: Synthesis complete', { hasStructuredResult: !!synthesis.structuredResult, resultLength: synthesis.text?.length });
359
+
360
+ return {
361
+ workflow_id: workflowId,
362
+ result: resultText,
363
+ ...(synthesis.structuredResult ? { structuredResult: synthesis.structuredResult } : {}),
364
+ subtaskCount: subtasks.length,
365
+ };
366
+ } catch (err) {
367
+ console.error('[workflow] Phase 3: Synthesis FAILED, falling back to raw leaf results', err instanceof Error ? err.message : err);
368
+
369
+ // Fallback: return the individual leaf results so the caller still gets useful output
370
+ const fallbackText = [
371
+ `Workflow completed but synthesis failed. ${subtasks.length} sub-agent(s) executed (${leafResults.filter(r => r.error).length} failed).`,
372
+ 'Returning raw sub-agent results:',
373
+ '',
374
+ leafResults
375
+ .map((r) => {
376
+ const status = r.error ? '[FAILED]' : '[success]';
377
+ const body = r.error ? `Error: ${r.error}` : (r.text || '(no text output)');
378
+ return `Sub-agent ${r.index + 1} ${status}:\n${body}`;
379
+ })
380
+ .join('\n\n'),
381
+ ].join('\n');
382
+
383
+ return {
384
+ workflow_id: workflowId,
385
+ result: fallbackText,
386
+ subtaskCount: subtasks.length,
387
+ error: `Synthesis failed: ${err instanceof Error ? err.message : String(err)}`,
388
+ };
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Build the workflow tool definition for the AI SDK.
394
+ * Gated by canSpawnSubagents — same as the task tool.
395
+ */
396
+ export interface WorkflowToolDefinition extends ToolDefinition {
397
+ allowedSubagentIds: string[];
398
+ }
399
+
400
+ export interface GetWorkflowToolDefinitionOptions {
401
+ sessionId: string;
402
+ canSpawnSubagents: boolean | string[] | null | undefined;
403
+ allowSelfAsSubagent?: boolean;
404
+ }
405
+
406
+ /** Unscoped definition resolution: reads the module-level session and
407
+ * preconfig accessors exactly like the pre-C5 path. */
408
+ export async function getWorkflowToolDefinition(
409
+ options: GetWorkflowToolDefinitionOptions,
410
+ ): Promise<WorkflowToolDefinition | null> {
411
+ return resolveWorkflowToolDefinitionWithDeps(options, {
412
+ getSession,
413
+ listPreconfigs: listSubagentPreconfigs,
414
+ });
415
+ }
416
+
417
+ /** Composed definition resolution over injected lookups. */
418
+ export async function resolveWorkflowToolDefinitionWithDeps(
419
+ options: GetWorkflowToolDefinitionOptions,
420
+ deps: { getSession: (id: string) => Promise<import('@capekai/types').Session | null>; listPreconfigs: () => Promise<import('@capekai/types').Preconfig[]> },
421
+ ): Promise<WorkflowToolDefinition | null> {
422
+ const subagents = await resolveEffectiveSubagentTargets({
423
+ sessionId: options.sessionId,
424
+ canSpawnSubagents: options.canSpawnSubagents,
425
+ allowSelfAsSubagent: options.allowSelfAsSubagent,
426
+ maximumDepthReached: !(await canSpawnSubagentWithDeps(options.sessionId, deps.getSession)),
427
+ } as ResolveSubagentTargetsOptions, deps);
428
+
429
+ if (subagents.length === 0) return null;
430
+
431
+ return buildWorkflowToolDefinition(subagents);
432
+ }
433
+
434
+ /** Pure assembly of the workflow tool definition from a resolved target
435
+ * list. Moved byte-for-byte from the pre-C5 `getWorkflowToolDefinition`
436
+ * assembly. */
437
+ export function buildWorkflowToolDefinition(subagents: import('@capekai/types').Preconfig[]): WorkflowToolDefinition {
438
+ const allowedSubagentIds = subagents.map((subagent) => subagent.id);
439
+ const subagentList = allowedSubagentIds.join(', ');
440
+
441
+ return {
442
+ name: 'workflow',
443
+ description: `Orchestrate parallel multi-agent work via decompose → fan out → synthesize.
444
+
445
+ This tool is ideal for large tasks that benefit from parallelism: research across multiple angles, bulk file operations, codebase audits, or any task that can be split into independent pieces.
446
+
447
+ How it works:
448
+ 1. DECOMPOSE: Breaks your prompt into parallel subtasks (unless you provide \`subtasks\`).
449
+ 2. FAN OUT: Runs each subtask as an independent agent concurrently (max ${MAX_CONCURRENCY} at a time).
450
+ 3. SYNTHESIZE: Combines all results into one consolidated answer.
451
+
452
+ Unlike calling \`task\` multiple times yourself, this tool protects your context window: you only see the final synthesis, not all intermediate results.
453
+
454
+ Available leaf agent types: ${subagentList}
455
+
456
+ When to use \`workflow\` vs \`task\`:
457
+ - Use \`workflow\` for: large parallel tasks, research, bulk operations, anything needing 3+ agents.
458
+ - Use \`task\` for: single focused delegations, when you want direct access to results, or when subtasks depend on each other.
459
+
460
+ Parameters:
461
+ - \`prompt\` (required): The high-level task. The tool will decompose it automatically.
462
+ - \`subtasks\` (optional): Provide explicit subtasks to skip decomposition. Each has \`prompt\`, optional \`preconfigId\`, and optional \`outputSchema\`.
463
+ - \`leafPreconfigId\` (optional): Force all leaf agents to use this agent type (overrides decomposer's choices).
464
+ - \`outputSchema\` (optional): JSON Schema for structured final output. If provided, synthesis returns conforming JSON.`,
465
+ timeout: 600000,
466
+ inputSchema: {
467
+ type: 'object',
468
+ properties: {
469
+ prompt: {
470
+ type: 'string',
471
+ description: 'The high-level task to accomplish. The tool decomposes it into parallel subtasks unless "subtasks" is provided.',
472
+ },
473
+ description: {
474
+ type: 'string',
475
+ description: 'Short label for the workflow run (shown in UI).',
476
+ },
477
+ subtasks: {
478
+ type: 'array',
479
+ description: 'Explicit subtasks to run in parallel. Skips decomposition if provided.',
480
+ items: {
481
+ type: 'object',
482
+ properties: {
483
+ prompt: { type: 'string', description: 'The self-contained prompt for this subtask' },
484
+ preconfigId: {
485
+ type: 'string',
486
+ description: 'Agent type (preconfig ID) to use for this subtask',
487
+ enum: allowedSubagentIds,
488
+ },
489
+ outputSchema: {
490
+ type: 'object',
491
+ additionalProperties: true,
492
+ description: 'Optional JSON Schema for structured output from this subtask',
493
+ },
494
+ },
495
+ required: ['prompt'],
496
+ },
497
+ },
498
+ leafPreconfigId: {
499
+ type: 'string',
500
+ description: 'Force all leaf agents to use this agent type. Overrides per-subtask assignments.',
501
+ enum: allowedSubagentIds,
502
+ },
503
+ outputSchema: {
504
+ type: 'object',
505
+ additionalProperties: true,
506
+ description: 'Optional JSON Schema for the final synthesized output.',
507
+ },
508
+ },
509
+ required: ['prompt'],
510
+ },
511
+ outputSchema: {
512
+ type: 'object',
513
+ properties: {
514
+ workflow_id: { type: 'string' },
515
+ result: { type: 'string' },
516
+ structuredResult: { type: 'object', additionalProperties: true },
517
+ subtaskCount: { type: 'number' },
518
+ error: { type: 'string' },
519
+ },
520
+ },
521
+ allowedSubagentIds,
522
+ };
523
+ }
@@ -0,0 +1,161 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { streamText } from 'ai';
3
+ import type { AssistantMessage, TextPart, UserMessage } from '@capekai/types';
4
+ import {
5
+ emitRuntimeEvent,
6
+ emitSessionCreated,
7
+ emitSessionUpdated,
8
+ } from '../runtime/host-dependencies';
9
+ import { getModelsConfig } from '../configuration/runtime';
10
+ import {
11
+ createMessage,
12
+ createPart,
13
+ createSession,
14
+ getSession,
15
+ getWorkspaceAutoApproveSeverity,
16
+ updateSession,
17
+ } from '../storage/runtime';
18
+ import type { BroadcastFn, BroadcastSessionFn } from '../runtime/host';
19
+ import { getModelWithMetadata } from '../core/model-utils';
20
+ import { extractJsonFromText } from '../core/structured-output';
21
+
22
+ /**
23
+ * Workflow domain: the shared workflow/goals orchestrator model-turn
24
+ * service implementation. Moved verbatim from `core/workflow-orchestrator-
25
+ * session.ts`; the named contract (`capek.orchestrator-session`) lives in
26
+ * `plugins/service-keys.ts` and `plugins/orchestrator-session.ts` provides
27
+ * this implementation, so the goals slice consumes the same contract
28
+ * without owning workflow code. The two core edges below
29
+ * (model-utils, structured-output) stay until C7.
30
+ */
31
+
32
+ export interface OrchestratorSessionOptions {
33
+ parentSessionId: string;
34
+ title: string;
35
+ agentName: string;
36
+ systemPrompt: string;
37
+ userPrompt: string;
38
+ maxTokens?: number;
39
+ abortSignal?: AbortSignal;
40
+ broadcast?: BroadcastFn;
41
+ broadcastSessionCreated?: BroadcastSessionFn;
42
+ broadcastSessionUpdated?: BroadcastSessionFn;
43
+ }
44
+
45
+ export interface OrchestratorSessionResult {
46
+ text: string;
47
+ json: Record<string, unknown> | null;
48
+ sessionId: string;
49
+ }
50
+
51
+ export async function runOrchestratorSession(options: OrchestratorSessionOptions): Promise<OrchestratorSessionResult> {
52
+ const {
53
+ parentSessionId,
54
+ title,
55
+ agentName,
56
+ systemPrompt,
57
+ userPrompt,
58
+ maxTokens = 4096,
59
+ abortSignal,
60
+ broadcast = emitRuntimeEvent,
61
+ broadcastSessionCreated: broadcastSessCreated = emitSessionCreated,
62
+ broadcastSessionUpdated: broadcastSessUpdated = emitSessionUpdated,
63
+ } = options;
64
+ const parentSession = await getSession(parentSessionId);
65
+ const config = getModelsConfig();
66
+ const modelId = parentSession?.selectedModel || config.defaultModel;
67
+ const providerId = parentSession?.selectedProvider || config.defaultProvider;
68
+ const session = await createSession({
69
+ id: randomUUID(),
70
+ workspaceId: parentSession?.workspaceId || '',
71
+ preconfigId: null,
72
+ title,
73
+ status: 'active',
74
+ metadata: null,
75
+ parentId: parentSessionId,
76
+ agentName,
77
+ subagentStatus: 'running',
78
+ selectedModel: modelId,
79
+ selectedProvider: providerId,
80
+ autoApproveSeverity: await getWorkspaceAutoApproveSeverity(parentSession?.workspaceId || ''),
81
+ });
82
+ broadcastSessCreated(session);
83
+ console.log(`[workflow:${agentName}] Session created`, { sessionId: session.id, modelId, providerId });
84
+
85
+ try {
86
+ const userMsgId = randomUUID();
87
+ const userMessage: UserMessage = { id: userMsgId, sessionId: session.id, role: 'user', createdAt: Date.now() };
88
+ const userTextPart: TextPart = { id: randomUUID(), messageId: userMsgId, createdAt: Date.now(), type: 'text', text: userPrompt };
89
+ await createMessage(userMessage);
90
+ await createPart(userTextPart, session.id);
91
+ broadcast({ kind: 'message', action: 'created', message: userMessage });
92
+ broadcast({ kind: 'part', action: 'created', sessionId: session.id, part: userTextPart });
93
+
94
+ const { model, omitMaxOutputTokens, providerOptions, useProviderInstructions } = await getModelWithMetadata({
95
+ modelId,
96
+ providerId,
97
+ systemPrompt,
98
+ sessionId: parentSessionId,
99
+ });
100
+ console.log(`[workflow:${agentName}] Calling streamText...`);
101
+ const stream = streamText({
102
+ model,
103
+ system: useProviderInstructions ? undefined : systemPrompt,
104
+ messages: [{ role: 'user', content: userPrompt }],
105
+ maxOutputTokens: omitMaxOutputTokens ? undefined : maxTokens,
106
+ providerOptions: providerOptions as unknown as Parameters<typeof streamText>[0]['providerOptions'],
107
+ abortSignal,
108
+ });
109
+ const text = await stream.text;
110
+ const streamUsage = await stream.usage;
111
+ console.log(`[workflow:${agentName}] streamText returned`, { textLength: text?.length });
112
+ const assistantMsgId = randomUUID();
113
+ const assistantTextPart: TextPart = { id: randomUUID(), messageId: assistantMsgId, createdAt: Date.now(), type: 'text', text };
114
+ const parsedJson = extractJsonFromText(text);
115
+ const assistantMessage: AssistantMessage = {
116
+ id: assistantMsgId,
117
+ sessionId: session.id,
118
+ role: 'assistant',
119
+ status: 'completed',
120
+ modelId,
121
+ providerId,
122
+ agent: agentName,
123
+ tokens: {
124
+ prompt: streamUsage?.inputTokens ?? 0,
125
+ completion: streamUsage?.outputTokens ?? 0,
126
+ cacheRead: streamUsage?.inputTokenDetails.cacheReadTokens ?? 0,
127
+ cacheWrite: streamUsage?.inputTokenDetails.cacheWriteTokens ?? 0,
128
+ noCache: streamUsage?.inputTokenDetails.noCacheTokens ?? 0,
129
+ },
130
+ cost: 0,
131
+ createdAt: Date.now(),
132
+ completedAt: Date.now(),
133
+ ...(parsedJson ? { structuredOutput: { formatName: title, data: parsedJson } } : {}),
134
+ };
135
+ await createMessage(assistantMessage);
136
+ await createPart(assistantTextPart, session.id);
137
+ broadcast({ kind: 'message', action: 'created', message: assistantMessage });
138
+ broadcast({ kind: 'part', action: 'created', sessionId: session.id, part: assistantTextPart });
139
+ await updateSession(session.id, { subagentStatus: 'completed' });
140
+ const updatedSession = await getSession(session.id);
141
+ if (updatedSession) broadcastSessUpdated(updatedSession);
142
+ return { text, json: parsedJson, sessionId: session.id };
143
+ } catch (err) {
144
+ const errAny = err as Record<string, unknown>;
145
+ console.error(`[workflow:${agentName}] FAILED`, {
146
+ message: err instanceof Error ? err.message : String(err),
147
+ name: err instanceof Error ? err.name : undefined,
148
+ statusCode: errAny?.statusCode ?? errAny?.status,
149
+ url: errAny?.url,
150
+ responseBody: errAny?.responseBody ?? errAny?.response,
151
+ data: errAny?.data,
152
+ cause: err instanceof Error ? err.cause : undefined,
153
+ stack: err instanceof Error ? err.stack?.split('\n').slice(0, 5).join('\n') : undefined,
154
+ });
155
+
156
+ await updateSession(session.id, { subagentStatus: abortSignal?.aborted ? 'interrupted' : 'error' });
157
+ const updatedSession = await getSession(session.id);
158
+ if (updatedSession) broadcastSessUpdated(updatedSession);
159
+ throw err;
160
+ }
161
+ }