@capekai/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/package.json +105 -0
- package/src/adapters/ai-sdk.ts +84 -0
- package/src/compaction/contracts.ts +82 -0
- package/src/compaction/executor.ts +161 -0
- package/src/compaction/policy.ts +318 -0
- package/src/compaction/recovery.ts +139 -0
- package/src/compaction/task.ts +540 -0
- package/src/configuration/contracts.ts +58 -0
- package/src/configuration/defaults.ts +27 -0
- package/src/configuration/runtime.ts +42 -0
- package/src/configuration/single-model.ts +75 -0
- package/src/context/assembler.ts +112 -0
- package/src/context/index.ts +2 -0
- package/src/context/sources.ts +119 -0
- package/src/context/workspace.ts +63 -0
- package/src/core/agent.ts +401 -0
- package/src/core/build-tools.ts +139 -0
- package/src/core/chat-handler.ts +858 -0
- package/src/core/error-handling.ts +18 -0
- package/src/core/fork.ts +103 -0
- package/src/core/interrupt.ts +192 -0
- package/src/core/message-utils.ts +261 -0
- package/src/core/model-utils.ts +149 -0
- package/src/core/part-utils.ts +88 -0
- package/src/core/provider-utils.ts +67 -0
- package/src/core/revert.ts +46 -0
- package/src/core/step-handlers.ts +157 -0
- package/src/core/stream/finalization.ts +65 -0
- package/src/core/stream/stream-config.ts +82 -0
- package/src/core/stream-handlers.ts +242 -0
- package/src/core/structured-output.ts +68 -0
- package/src/core/tool-builders/agent-tools.ts +71 -0
- package/src/core/tool-builders/external-tools.ts +179 -0
- package/src/core/tool-builders/types.ts +16 -0
- package/src/core/tool-builders/workspace-tools.ts +293 -0
- package/src/core/tool-capabilities.ts +65 -0
- package/src/goals/evaluator.ts +171 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/loop.ts +167 -0
- package/src/goals/service.ts +39 -0
- package/src/index.ts +10 -0
- package/src/internal/ask-authority.ts +29 -0
- package/src/internal/composition.ts +44 -0
- package/src/internal/configuration.ts +22 -0
- package/src/internal/execution.ts +108 -0
- package/src/internal/hosts.ts +64 -0
- package/src/internal/plugins.ts +71 -0
- package/src/internal/providers.ts +32 -0
- package/src/internal/sandbox.ts +19 -0
- package/src/internal/tools.ts +48 -0
- package/src/internal/workspace.ts +25 -0
- package/src/kernel/diagnostics.ts +249 -0
- package/src/kernel/errors.ts +120 -0
- package/src/kernel/events.ts +82 -0
- package/src/kernel/index.ts +72 -0
- package/src/kernel/kernel.ts +62 -0
- package/src/kernel/lifecycle.ts +72 -0
- package/src/kernel/plugin.ts +218 -0
- package/src/kernel/registry.ts +493 -0
- package/src/kernel/scope.ts +776 -0
- package/src/kernel/service-key.ts +19 -0
- package/src/kernel/types.ts +317 -0
- package/src/memory/index.ts +2 -0
- package/src/memory/memory-tool.ts +75 -0
- package/src/memory/registry.ts +172 -0
- package/src/permission/ask-user-api.ts +70 -0
- package/src/permission/contracts.ts +135 -0
- package/src/permission/permission-request-manager.ts +58 -0
- package/src/permission/policy.ts +277 -0
- package/src/permission/runtime.ts +612 -0
- package/src/plugins/compaction-policy.ts +46 -0
- package/src/plugins/compose.ts +171 -0
- package/src/plugins/context-sections.ts +246 -0
- package/src/plugins/default-agent-driver.ts +14 -0
- package/src/plugins/facade-plugins.ts +129 -0
- package/src/plugins/goal-domain.ts +82 -0
- package/src/plugins/legacy-system-message.ts +152 -0
- package/src/plugins/loaded-tools.ts +23 -0
- package/src/plugins/memory-domain.ts +264 -0
- package/src/plugins/orchestrator-session.ts +29 -0
- package/src/plugins/permission-policy.ts +49 -0
- package/src/plugins/retry-policy.ts +28 -0
- package/src/plugins/scheduler-domain.ts +192 -0
- package/src/plugins/service-keys.ts +294 -0
- package/src/plugins/session-search-domain.ts +238 -0
- package/src/plugins/skills-domain.ts +272 -0
- package/src/plugins/subagent-domain.ts +287 -0
- package/src/plugins/tool-catalog.ts +78 -0
- package/src/plugins/tool-output-policy.ts +52 -0
- package/src/plugins/value-plugins.ts +150 -0
- package/src/plugins/workflow-domain.ts +198 -0
- package/src/plugins/workspace-policy.ts +37 -0
- package/src/providers/registry.ts +63 -0
- package/src/providers/types.ts +44 -0
- package/src/retry/policy.ts +282 -0
- package/src/retry/stream-chat.ts +312 -0
- package/src/runtime/agent-runtime.ts +83 -0
- package/src/runtime/default-agent-driver.ts +23 -0
- package/src/runtime/domain-tool-source.ts +156 -0
- package/src/runtime/events.ts +61 -0
- package/src/runtime/host-dependencies.ts +71 -0
- package/src/runtime/host-guidance.ts +22 -0
- package/src/runtime/host-layout.ts +23 -0
- package/src/runtime/host.ts +129 -0
- package/src/runtime/standalone-host.ts +118 -0
- package/src/sandbox/controller.ts +204 -0
- package/src/sandbox/model.ts +305 -0
- package/src/sandbox/provider.ts +53 -0
- package/src/sandbox/types.ts +110 -0
- package/src/scheduler/host.ts +22 -0
- package/src/scheduler/scheduler-tool.ts +172 -0
- package/src/session-search/host.ts +56 -0
- package/src/session-search/index.ts +23 -0
- package/src/session-search/session-search-tool.ts +151 -0
- package/src/skills/index.ts +3 -0
- package/src/skills/registry.ts +63 -0
- package/src/skills/skill-manage-tool.ts +205 -0
- package/src/skills/skill-tool.ts +42 -0
- package/src/storage/contracts.ts +159 -0
- package/src/storage/memory.ts +321 -0
- package/src/storage/options.ts +75 -0
- package/src/storage/runtime.ts +115 -0
- package/src/storage/sqlite-tool-output-artifacts.ts +106 -0
- package/src/storage/sqlite.ts +321 -0
- package/src/storage/tool-output-artifacts.ts +75 -0
- package/src/storage.ts +31 -0
- package/src/subagent/child-session.ts +282 -0
- package/src/subagent/guidance.ts +8 -0
- package/src/subagent/policy.ts +198 -0
- package/src/subagent/task-tool.ts +584 -0
- package/src/tool-output/contracts.ts +111 -0
- package/src/tool-output/policy.ts +410 -0
- package/src/tool.ts +1 -0
- package/src/tools/executor.ts +258 -0
- package/src/tools/install-manifest.ts +40 -0
- package/src/tools/llm-api.ts +77 -0
- package/src/tools/registry.ts +206 -0
- package/src/tools/tool-artifact.ts +182 -0
- package/src/tools/tool-source.ts +53 -0
- package/src/utils/errors.ts +334 -0
- package/src/utils/strip-visualization.ts +50 -0
- package/src/workflow/decomposer.ts +139 -0
- package/src/workflow/execution.ts +523 -0
- package/src/workflow/orchestrator-session.ts +161 -0
- package/src/workflow/synthesizer.ts +130 -0
- package/src/workspace/contracts.ts +135 -0
- package/src/workspace/policy.ts +327 -0
|
@@ -0,0 +1,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>;
|