@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,242 @@
1
+ import type { TextPart, ToolPart, ReasoningPart, MessageEvent } from '@capekai/types';
2
+ import { createPart, updatePart, getPart, persistStreamingPartSnapshots } from '../storage/runtime';
3
+ import { parseToolInput } from './part-utils';
4
+ import { randomUUID } from 'crypto';
5
+
6
+ const STREAM_PART_PERSIST_INTERVAL_MS = 300;
7
+
8
+ export interface StreamHandlerContext {
9
+ messageId: string;
10
+ sessionId: string;
11
+ toolParts: ToolPart[];
12
+ currentText: string;
13
+ currentTextPartId: string | null;
14
+ currentTextCreatedAt: number | null;
15
+ currentReasoning: string;
16
+ currentReasoningPartId: string | null;
17
+ currentReasoningCreatedAt: number | null;
18
+ yieldFn: (event: MessageEvent) => void;
19
+ }
20
+
21
+ interface StreamPersistenceState {
22
+ persistedText: string;
23
+ persistedReasoning: string;
24
+ lastTextPersistedAt: number;
25
+ lastReasoningPersistedAt: number;
26
+ }
27
+
28
+ export function createStreamHandlers(ctx: StreamHandlerContext) {
29
+ const persistence: StreamPersistenceState = {
30
+ persistedText: '',
31
+ persistedReasoning: '',
32
+ lastTextPersistedAt: 0,
33
+ lastReasoningPersistedAt: 0,
34
+ };
35
+
36
+ function shouldPersist(lastPersistedAt: number): boolean {
37
+ return Date.now() - lastPersistedAt >= STREAM_PART_PERSIST_INTERVAL_MS;
38
+ }
39
+
40
+ async function persistText(syncFts: boolean): Promise<void> {
41
+ if (!ctx.currentTextPartId || ctx.currentText === persistence.persistedText) return;
42
+ if (syncFts) {
43
+ // Final flush persists the complete text; message finalization performs explicit FTS sync.
44
+ await updatePart(ctx.currentTextPartId, { text: ctx.currentText }, { syncFts: false });
45
+ persistence.persistedText = ctx.currentText;
46
+ persistence.lastTextPersistedAt = Date.now();
47
+ } else {
48
+ // Intermediate snapshot: no read-before-write
49
+ await persistStreamingPartSnapshots([{
50
+ id: ctx.currentTextPartId,
51
+ messageId: ctx.messageId,
52
+ sessionId: ctx.sessionId,
53
+ type: 'text',
54
+ createdAt: ctx.currentTextCreatedAt ?? Date.now(),
55
+ text: ctx.currentText,
56
+ }]);
57
+ persistence.persistedText = ctx.currentText;
58
+ persistence.lastTextPersistedAt = Date.now();
59
+ }
60
+ }
61
+
62
+ async function persistReasoning(syncFts: boolean): Promise<void> {
63
+ if (!ctx.currentReasoningPartId || ctx.currentReasoning === persistence.persistedReasoning) return;
64
+ if (syncFts) {
65
+ await updatePart(ctx.currentReasoningPartId, { text: ctx.currentReasoning }, { syncFts: false });
66
+ persistence.persistedReasoning = ctx.currentReasoning;
67
+ persistence.lastReasoningPersistedAt = Date.now();
68
+ } else {
69
+ await persistStreamingPartSnapshots([{
70
+ id: ctx.currentReasoningPartId,
71
+ messageId: ctx.messageId,
72
+ sessionId: ctx.sessionId,
73
+ type: 'reasoning',
74
+ createdAt: ctx.currentReasoningCreatedAt ?? Date.now(),
75
+ text: ctx.currentReasoning,
76
+ }]);
77
+ persistence.persistedReasoning = ctx.currentReasoning;
78
+ persistence.lastReasoningPersistedAt = Date.now();
79
+ }
80
+ }
81
+
82
+ function resetTextState(): void {
83
+ ctx.currentTextPartId = null;
84
+ ctx.currentTextCreatedAt = null;
85
+ ctx.currentText = '';
86
+ persistence.persistedText = '';
87
+ persistence.lastTextPersistedAt = 0;
88
+ }
89
+
90
+ function resetReasoningState(): void {
91
+ ctx.currentReasoningPartId = null;
92
+ ctx.currentReasoningCreatedAt = null;
93
+ ctx.currentReasoning = '';
94
+ persistence.persistedReasoning = '';
95
+ persistence.lastReasoningPersistedAt = 0;
96
+ }
97
+
98
+ return {
99
+ async handleTextDelta(delta: { text: string | undefined }): Promise<void> {
100
+ const textContent = delta.text || '';
101
+ if (textContent) {
102
+ ctx.currentText += textContent;
103
+
104
+ if (ctx.currentTextPartId) {
105
+ ctx.yieldFn({ type: 'part.append', sessionId: ctx.sessionId, partId: ctx.currentTextPartId, field: 'text', delta: textContent });
106
+ if (shouldPersist(persistence.lastTextPersistedAt)) {
107
+ await persistText(false);
108
+ }
109
+ } else {
110
+ ctx.currentTextPartId = randomUUID();
111
+ ctx.currentTextCreatedAt = Date.now();
112
+ const textPart: TextPart = {
113
+ id: ctx.currentTextPartId,
114
+ messageId: ctx.messageId,
115
+ createdAt: ctx.currentTextCreatedAt,
116
+ type: 'text',
117
+ text: textContent,
118
+ };
119
+ ctx.yieldFn({ type: 'part.created', sessionId: ctx.sessionId, part: textPart });
120
+ await createPart(textPart, ctx.sessionId, { syncFts: false });
121
+ persistence.persistedText = textContent;
122
+ persistence.lastTextPersistedAt = Date.now();
123
+ }
124
+ }
125
+ },
126
+
127
+ async handleReasoningDelta(delta: { text: string | undefined }): Promise<void> {
128
+ const reasoningContent = delta.text || '';
129
+ if (reasoningContent) {
130
+ ctx.currentReasoning += reasoningContent;
131
+
132
+ if (ctx.currentReasoningPartId) {
133
+ ctx.yieldFn({ type: 'part.append', sessionId: ctx.sessionId, partId: ctx.currentReasoningPartId, field: 'reasoning', delta: reasoningContent });
134
+ if (shouldPersist(persistence.lastReasoningPersistedAt)) {
135
+ await persistReasoning(false);
136
+ }
137
+ } else {
138
+ ctx.currentReasoningPartId = randomUUID();
139
+ ctx.currentReasoningCreatedAt = Date.now();
140
+ const reasoningPart: ReasoningPart = {
141
+ id: ctx.currentReasoningPartId,
142
+ messageId: ctx.messageId,
143
+ createdAt: ctx.currentReasoningCreatedAt,
144
+ type: 'reasoning',
145
+ text: reasoningContent,
146
+ };
147
+ ctx.yieldFn({ type: 'part.created', sessionId: ctx.sessionId, part: reasoningPart });
148
+ await createPart(reasoningPart, ctx.sessionId, { syncFts: false });
149
+ persistence.persistedReasoning = reasoningContent;
150
+ persistence.lastReasoningPersistedAt = Date.now();
151
+ }
152
+ }
153
+ },
154
+
155
+ async handleToolCall(delta: { toolCallId: string; toolName: string; input: unknown }): Promise<void> {
156
+ await this.flushPending();
157
+
158
+ const toolPartId = randomUUID();
159
+ const toolPart: ToolPart = {
160
+ id: toolPartId,
161
+ messageId: ctx.messageId,
162
+ createdAt: Date.now(),
163
+ type: 'tool',
164
+ callId: delta.toolCallId,
165
+ name: delta.toolName,
166
+ state: {
167
+ status: 'pending',
168
+ input: parseToolInput(delta.input),
169
+ },
170
+ };
171
+ ctx.toolParts.push(toolPart);
172
+
173
+ ctx.yieldFn({ type: 'part.created', sessionId: ctx.sessionId, part: toolPart });
174
+ await createPart(toolPart, ctx.sessionId, { syncFts: false });
175
+
176
+ resetTextState();
177
+ resetReasoningState();
178
+ },
179
+
180
+ async handleToolResult(delta: { toolCallId: string; output: unknown }): Promise<void> {
181
+ const existingToolPart = ctx.toolParts.find((tp) => tp.callId === delta.toolCallId);
182
+
183
+ if (existingToolPart) {
184
+ const latestPart = await getPart(existingToolPart.id) as ToolPart | null;
185
+ const latestState = latestPart?.state;
186
+
187
+ let resultData: unknown;
188
+ if (typeof delta.output === 'string') {
189
+ try {
190
+ resultData = JSON.parse(delta.output);
191
+ } catch {
192
+ resultData = delta.output;
193
+ }
194
+ } else if (delta.output && typeof delta.output === 'object' && 'value' in delta.output) {
195
+ resultData = (delta.output as { value: unknown }).value;
196
+ } else {
197
+ resultData = delta.output;
198
+ }
199
+
200
+ const isErrorResult = !!(resultData && typeof resultData === 'object' && 'error' in resultData);
201
+
202
+ const existingChildSessionId = latestState && 'childSessionId' in latestState
203
+ ? latestState.childSessionId
204
+ : undefined;
205
+
206
+ const updatedToolPart: ToolPart = {
207
+ ...existingToolPart,
208
+ state: isErrorResult
209
+ ? {
210
+ status: 'error' as const,
211
+ input: existingToolPart.state.input,
212
+ error: String((resultData as { error: unknown }).error),
213
+ startedAt: Date.now(),
214
+ failedAt: Date.now(),
215
+ ...(existingChildSessionId && { childSessionId: existingChildSessionId }),
216
+ }
217
+ : {
218
+ status: 'completed' as const,
219
+ input: existingToolPart.state.input,
220
+ output: resultData,
221
+ startedAt: Date.now(),
222
+ completedAt: Date.now(),
223
+ ...(existingChildSessionId && { childSessionId: existingChildSessionId }),
224
+ },
225
+ };
226
+
227
+ const index = ctx.toolParts.indexOf(existingToolPart);
228
+ if (index !== -1) {
229
+ ctx.toolParts[index] = updatedToolPart;
230
+ }
231
+
232
+ ctx.yieldFn({ type: 'part.updated', sessionId: ctx.sessionId, part: updatedToolPart });
233
+ await updatePart(updatedToolPart.id, { state: updatedToolPart.state }, { syncFts: false });
234
+ }
235
+ },
236
+
237
+ async flushPending(): Promise<void> {
238
+ await persistText(true);
239
+ await persistReasoning(true);
240
+ },
241
+ };
242
+ }
@@ -0,0 +1,68 @@
1
+ import type { ResponseFormat } from '@capekai/types';
2
+
3
+ /**
4
+ * Builds a system-prompt instruction that tells the model to respond with JSON
5
+ * conforming to the given schema. Used for providers that strip the schema
6
+ * from `response_format` (e.g. GLM/Zhipu, MiniMax) — they only send
7
+ * `{ type: "json_object" }` to the API, so the model needs the schema inline.
8
+ */
9
+ export function buildSchemaPromptInstruction(responseFormat: ResponseFormat): string {
10
+ const schemaStr = JSON.stringify(responseFormat.schema, null, 2);
11
+ return [
12
+ `You must respond with ONLY valid JSON that conforms to the following JSON Schema.`,
13
+ `Do not include any text before or after the JSON object. Do not wrap it in markdown code fences.`,
14
+ `Response format name: ${responseFormat.name}`,
15
+ ...(responseFormat.description ? [`Description: ${responseFormat.description}`] : []),
16
+ '',
17
+ 'JSON Schema:',
18
+ schemaStr,
19
+ ].join('\n');
20
+ }
21
+
22
+ /**
23
+ * Attempts to extract a JSON object from the raw text output of a model.
24
+ * Handles common LLM quirks:
25
+ * - Leading/trailing whitespace
26
+ * - Markdown code fences (```json ... ```)
27
+ * - Preamble text before the JSON (finds first `{` and last `}`)
28
+ */
29
+ export function extractJsonFromText(text: string): Record<string, unknown> | null {
30
+ if (!text || !text.trim()) {
31
+ return null;
32
+ }
33
+
34
+ let cleaned = text.trim();
35
+
36
+ // Strip markdown code fences if present
37
+ const fenceMatch = cleaned.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
38
+ if (fenceMatch) {
39
+ cleaned = fenceMatch[1].trim();
40
+ }
41
+
42
+ // If the whole thing is valid JSON, use it directly
43
+ try {
44
+ const parsed = JSON.parse(cleaned);
45
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
46
+ return parsed as Record<string, unknown>;
47
+ }
48
+ } catch {
49
+ // Fall through to brace-matching
50
+ }
51
+
52
+ // Find the outermost JSON object via first `{` and last `}`
53
+ const firstBrace = cleaned.indexOf('{');
54
+ const lastBrace = cleaned.lastIndexOf('}');
55
+ if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
56
+ const jsonStr = cleaned.slice(firstBrace, lastBrace + 1);
57
+ try {
58
+ const parsed = JSON.parse(jsonStr);
59
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
60
+ return parsed as Record<string, unknown>;
61
+ }
62
+ } catch {
63
+ // Not valid JSON
64
+ }
65
+ }
66
+
67
+ return null;
68
+ }
@@ -0,0 +1,71 @@
1
+ import { tool, jsonSchema } from 'ai';
2
+ import {
3
+ getContributedDomainToolPayloads,
4
+ getDomainToolFallback,
5
+ mergeDomainToolVisualization,
6
+ type DomainToolPayload,
7
+ } from '../../runtime/domain-tool-source';
8
+ import type { ToolMap } from './types';
9
+
10
+ /**
11
+ * C5 memory and skills domain tools (agent phase). The pre-C5 builder
12
+ * imported the memory and skills implementations directly; it now consumes
13
+ * the agent-scoped domain payloads through the generic
14
+ * contributed-domain-tool seam (`agent_memory`, `agent_skill_manage`), with
15
+ * the explicitly installed fallbacks covering the unscoped path. The agent
16
+ * directory gate stays here: agent tools build only when an agent directory
17
+ * exists for the session preconfig.
18
+ */
19
+ export interface AgentToolsOptions {
20
+ agentDir: string;
21
+ }
22
+
23
+ export async function buildAgentTools(options: AgentToolsOptions): Promise<ToolMap> {
24
+ const { agentDir } = options;
25
+ const tools: ToolMap = {};
26
+
27
+ const scopedDomainPayloads = getContributedDomainToolPayloads();
28
+ const domainPayload = (name: string): DomainToolPayload | null =>
29
+ scopedDomainPayloads === null
30
+ ? getDomainToolFallback(name)
31
+ : scopedDomainPayloads.get(name) ?? null;
32
+
33
+ const agentMemoryPayload = domainPayload('agent_memory');
34
+ if (agentMemoryPayload) {
35
+ tools['agent_memory'] = tool({
36
+ description: agentMemoryPayload.description,
37
+ inputSchema: jsonSchema(agentMemoryPayload.inputSchema),
38
+ execute: async (args: Record<string, unknown>) =>
39
+ agentMemoryPayload.execute(args, {
40
+ workspaceId: '',
41
+ sessionId: '',
42
+ ask: async () => {
43
+ throw new Error('Cannot ask user: no broadcast channel available');
44
+ },
45
+ agentDir,
46
+ }).then((result) => mergeDomainToolVisualization(agentMemoryPayload, args, result)),
47
+ });
48
+ }
49
+
50
+ const agentSkillManagePayload = domainPayload('agent_skill_manage');
51
+ const agentSkillManageDefinition = await agentSkillManagePayload?.resolveDefinition?.('', {
52
+ agentDir,
53
+ });
54
+ if (agentSkillManagePayload && agentSkillManageDefinition) {
55
+ tools['agent_skill_manage'] = tool({
56
+ description: agentSkillManageDefinition.description,
57
+ inputSchema: jsonSchema(agentSkillManageDefinition.inputSchema),
58
+ execute: async (args: Record<string, unknown>) =>
59
+ agentSkillManagePayload.execute(args, {
60
+ workspaceId: '',
61
+ sessionId: '',
62
+ ask: async () => {
63
+ throw new Error('Cannot ask user: no broadcast channel available');
64
+ },
65
+ agentDir,
66
+ }).then((result) => mergeDomainToolVisualization(agentSkillManagePayload, args, result)),
67
+ });
68
+ }
69
+
70
+ return tools;
71
+ }
@@ -0,0 +1,179 @@
1
+ import { tool, jsonSchema } from 'ai';
2
+ import { getTool } from '../../tools/registry';
3
+ import { executeTool } from '../../tools/executor';
4
+ import { createLlmApi } from '../../tools/llm-api';
5
+ import { createWorkspaceCapability } from '../../workspace/policy';
6
+ import {
7
+ createAskApi,
8
+ rejectPendingAsksByToolCallId,
9
+ type AskBroadcastFn,
10
+ } from '../../permission/ask-user-api';
11
+ import { getToolWorkspaceHost } from '../../runtime/host-dependencies';
12
+ import { transitionToolToRunningByCallId } from '../../storage/runtime';
13
+ import { interruptManager } from '../interrupt';
14
+ import {
15
+ getContributedDomainToolPayloads,
16
+ getDomainToolFallback,
17
+ mergeDomainToolVisualization,
18
+ type DomainToolPayload,
19
+ } from '../../runtime/domain-tool-source';
20
+ import { isToolAllowedInContext, type ToolExecutionScope } from '../tool-capabilities';
21
+ import type { ToolMap } from './types';
22
+ import type { BroadcastFn } from '../../runtime/host-dependencies';
23
+
24
+ export interface ExternalToolsOptions {
25
+ toolNames: string[];
26
+ canSpawnSubagents?: boolean | string[] | null;
27
+ allowSelfAsSubagent?: boolean;
28
+ broadcastFn?: AskBroadcastFn;
29
+ broadcast: BroadcastFn;
30
+ sessionId: string;
31
+ workspaceId: string | undefined;
32
+ workspacePath: string | undefined;
33
+ rootSessionId: string;
34
+ executionScopes: ReadonlySet<ToolExecutionScope>;
35
+ modelId?: string;
36
+ providerId?: string;
37
+ additionalPaths?: string[];
38
+ }
39
+
40
+ export async function buildExternalTools(options: ExternalToolsOptions): Promise<ToolMap> {
41
+ const {
42
+ toolNames,
43
+ canSpawnSubagents,
44
+ allowSelfAsSubagent,
45
+ broadcastFn,
46
+ broadcast,
47
+ sessionId,
48
+ workspaceId,
49
+ workspacePath,
50
+ rootSessionId,
51
+ executionScopes,
52
+ modelId,
53
+ providerId,
54
+ additionalPaths,
55
+ } = options;
56
+
57
+ const tools: ToolMap = {};
58
+
59
+ const canSpawn = canSpawnSubagents === true
60
+ || (Array.isArray(canSpawnSubagents) && canSpawnSubagents.length > 0);
61
+ const allowedSubagentIds = Array.isArray(canSpawnSubagents) ? canSpawnSubagents : undefined;
62
+
63
+ // The task tool is owned by the C5 subagent domain plugin. Composed scopes
64
+ // provide its payload through the generic contributed-domain-tool seam;
65
+ // the unscoped path uses the explicitly installed legacy fallback. The
66
+ // domain owns the depth gate (isEnabled); the preconfig spawn policy and
67
+ // the 'task'-name guard stay here because they arrive per build.
68
+ const scopedDomainPayloads = getContributedDomainToolPayloads();
69
+ const taskPayload: DomainToolPayload | null = scopedDomainPayloads === null
70
+ ? getDomainToolFallback('task')
71
+ : scopedDomainPayloads.get('task') ?? null;
72
+ const shouldIncludeTask = taskPayload !== null
73
+ && !toolNames.includes('task')
74
+ && canSpawn
75
+ && await taskPayload.isEnabled?.(workspaceId ?? '', sessionId) === true;
76
+
77
+ // Phase 1a: registry tools in toolNames order, exactly as pre-C5.
78
+ for (const name of toolNames) {
79
+ const loadedTool = await getTool(name);
80
+ if (!loadedTool) continue;
81
+
82
+ const { definition } = loadedTool;
83
+
84
+ if (!isToolAllowedInContext(definition.capabilities, executionScopes)) {
85
+ continue;
86
+ }
87
+
88
+ tools[name] = tool({
89
+ description: definition.description,
90
+ inputSchema: jsonSchema(definition.inputSchema),
91
+ execute: async (args: Record<string, unknown>, { toolCallId }: { toolCallId: string }) => {
92
+ const toolAbortController = interruptManager.registerToolExecution(sessionId, toolCallId);
93
+
94
+ try {
95
+ const llmFactory = () => createLlmApi(modelId, providerId, sessionId);
96
+ const askFactory = (tcId: string) =>
97
+ broadcastFn
98
+ ? createAskApi(sessionId, tcId, definition.name, broadcastFn, workspaceId, rootSessionId)
99
+ : (() => { throw new Error('Cannot ask user: no broadcast channel available (broadcastFn not provided)'); }) as import('@capekai/tool').AskApi;
100
+
101
+ const workspace = createWorkspaceCapability(getToolWorkspaceHost({
102
+ workspaceId,
103
+ workspacePath,
104
+ additionalPaths,
105
+ sessionId,
106
+ }));
107
+ const result = await executeTool({
108
+ tool: loadedTool,
109
+ args,
110
+ workspace,
111
+ sessionId,
112
+ workspaceId,
113
+ toolCallId,
114
+ abortSignal: toolAbortController.signal,
115
+ timeout: definition.timeout,
116
+ createLlmApi: llmFactory,
117
+ createAskApi: askFactory,
118
+ });
119
+
120
+ if (!result.success) {
121
+ return { error: result.error ?? 'Tool execution failed' };
122
+ }
123
+
124
+ if (result.visualization && result.result && typeof result.result === 'object') {
125
+ return { ...result.result as Record<string, unknown>, _visualization: result.visualization };
126
+ }
127
+
128
+ return result.result;
129
+ } finally {
130
+ interruptManager.unregisterToolExecution(sessionId, toolCallId);
131
+ await rejectPendingAsksByToolCallId(toolCallId);
132
+ }
133
+ },
134
+ });
135
+ }
136
+
137
+ // Phase 1b: the task tool after the registry tools, preserving the exact
138
+ // pre-C5 build order. The dynamic description and schema come from the
139
+ // payload's per-build resolver (composed deps or the legacy fallback).
140
+ if (shouldIncludeTask && taskPayload) {
141
+ const taskDefinition = await taskPayload.resolveDefinition?.(sessionId, {
142
+ canSpawnSubagents,
143
+ allowSelfAsSubagent,
144
+ });
145
+ if (taskDefinition) {
146
+ tools['task'] = tool({
147
+ description: taskDefinition.description,
148
+ inputSchema: jsonSchema(taskDefinition.inputSchema),
149
+ execute: async (args: Record<string, unknown>, { toolCallId }: { toolCallId: string }) => {
150
+ const toolAbortController = interruptManager.registerToolExecution(sessionId, toolCallId);
151
+
152
+ try {
153
+ return await taskPayload.execute(args, {
154
+ workspaceId: workspaceId ?? '',
155
+ sessionId,
156
+ ask: async () => {
157
+ throw new Error('Cannot ask user: no broadcast channel available (broadcastFn not provided)');
158
+ },
159
+ workspacePath,
160
+ abortSignal: toolAbortController.signal,
161
+ onSessionCreated: async (childSessionId: string) => {
162
+ const updatedPart = await transitionToolToRunningByCallId(sessionId, toolCallId, childSessionId);
163
+ if (updatedPart) {
164
+ broadcast({ kind: 'part', action: 'updated', sessionId, part: updatedPart });
165
+ }
166
+ },
167
+ allowedSubagentIds,
168
+ broadcast,
169
+ }).then((result) => mergeDomainToolVisualization(taskPayload, args, result));
170
+ } finally {
171
+ interruptManager.unregisterToolExecution(sessionId, toolCallId);
172
+ }
173
+ },
174
+ });
175
+ }
176
+ }
177
+
178
+ return tools;
179
+ }
@@ -0,0 +1,16 @@
1
+ import type { Tool } from 'ai';
2
+ import type { AskBroadcastFn } from '../../permission/ask-user-api';
3
+
4
+ export interface ToolBuildContext {
5
+ sessionId: string;
6
+ workspaceId: string | undefined;
7
+ workspacePath: string | undefined;
8
+ rootSessionId: string;
9
+ modelId?: string;
10
+ providerId?: string;
11
+ broadcastFn?: AskBroadcastFn;
12
+ additionalPaths?: string[];
13
+ agentId?: string | null;
14
+ }
15
+
16
+ export type ToolMap = Record<string, Tool>;