@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,401 @@
|
|
|
1
|
+
import { streamText, stepCountIs } from 'ai';
|
|
2
|
+
import type { MessageWithParts, ToolPart, StepPart, Preconfig, MessageEvent, AssistantMessage, ResponseFormat } from '@capekai/types';
|
|
3
|
+
import { createMessage, updateMessage, getSession, updateSession, transitionToolToInterrupted, syncMessageFts } from '../storage/runtime';
|
|
4
|
+
|
|
5
|
+
import { findModel, getMaxOutputTokens, getModelsConfig } from '../configuration/runtime';
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
7
|
+
import { interruptManager } from './interrupt';
|
|
8
|
+
import { emitSessionUpdated } from '../runtime/host-dependencies';
|
|
9
|
+
import { rejectPendingAsksBySession } from '../permission/ask-user-api';
|
|
10
|
+
import { getModelWithMetadata } from './model-utils';
|
|
11
|
+
|
|
12
|
+
import { createStepCallbacks, type CallbackEvent, type UsageEventData } from './step-handlers';
|
|
13
|
+
import { createStreamHandlers } from './stream-handlers';
|
|
14
|
+
import { convertToAiSdkMessages } from './message-utils';
|
|
15
|
+
import { buildAiSdkTools, type BuildToolsOptions } from './build-tools';
|
|
16
|
+
import { getContextAssembler } from '../context/assembler';
|
|
17
|
+
import { getAgentDirectory } from '../context';
|
|
18
|
+
import { initializeWorkspaceDiscovery } from '../tools/tool-source';
|
|
19
|
+
import { resolveEffectiveSubagentTargets } from '../subagent/policy';
|
|
20
|
+
import { join } from 'path';
|
|
21
|
+
|
|
22
|
+
import { classifyApiError } from '../utils/errors';
|
|
23
|
+
import { createErrorEvent, type ErrorEvent } from './error-handling';
|
|
24
|
+
import type { CompactionPolicy } from '../compaction/contracts';
|
|
25
|
+
import { computeAutoThreshold } from '../compaction/policy';
|
|
26
|
+
import { buildStreamConfig } from './stream/stream-config';
|
|
27
|
+
import { extractFinalizationData } from './stream/finalization';
|
|
28
|
+
|
|
29
|
+
export interface ChatOptions {
|
|
30
|
+
sessionId: string;
|
|
31
|
+
preconfig: Preconfig;
|
|
32
|
+
messages: MessageWithParts[];
|
|
33
|
+
modelId?: string;
|
|
34
|
+
providerId?: string;
|
|
35
|
+
variant?: string;
|
|
36
|
+
workspacePath?: string;
|
|
37
|
+
workspaceId?: string;
|
|
38
|
+
additionalPaths?: string[];
|
|
39
|
+
maxSteps?: number;
|
|
40
|
+
compactionPolicy?: CompactionPolicy;
|
|
41
|
+
broadcastFn?: BuildToolsOptions['broadcastFn'];
|
|
42
|
+
responseFormat?: ResponseFormat;
|
|
43
|
+
retryAbortController?: AbortController;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function collectInterruptedToolPartEvents(
|
|
47
|
+
toolParts: ToolPart[],
|
|
48
|
+
sessionId: string,
|
|
49
|
+
): Promise<MessageEvent[]> {
|
|
50
|
+
const events: MessageEvent[] = [];
|
|
51
|
+
for (const toolPart of toolParts) {
|
|
52
|
+
if (toolPart.state.status === 'pending' || toolPart.state.status === 'running') {
|
|
53
|
+
const updatedPart = await transitionToolToInterrupted(toolPart.id, 'user_request');
|
|
54
|
+
if (updatedPart) {
|
|
55
|
+
events.push({ type: 'part.updated', sessionId, part: updatedPart });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return events;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildInterruptedMessage(assistantMessage: AssistantMessage): AssistantMessage {
|
|
63
|
+
return {
|
|
64
|
+
...assistantMessage,
|
|
65
|
+
status: 'interrupted' as const,
|
|
66
|
+
error: 'Interrupted by user',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ChatResult {
|
|
71
|
+
message: AssistantMessage;
|
|
72
|
+
toolCalls: ToolPart[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function* streamChat(options: ChatOptions): AsyncGenerator<MessageEvent | { type: 'usage'; usage: UsageEventData; model: string; variant: string | null } | { type: 'needs_compaction'; sessionId: string } | ErrorEvent> {
|
|
76
|
+
const { sessionId: _sessionId, preconfig, messages, modelId, providerId, variant, workspacePath, workspaceId, maxSteps, compactionPolicy } = options;
|
|
77
|
+
|
|
78
|
+
const managesSessionLifecycle = !options.retryAbortController;
|
|
79
|
+
const session = await getSession(_sessionId);
|
|
80
|
+
const abortController = options.retryAbortController
|
|
81
|
+
?? interruptManager.registerSession(_sessionId, session?.parentId ?? undefined);
|
|
82
|
+
|
|
83
|
+
// Initialize MCP for workspace
|
|
84
|
+
if (workspacePath) {
|
|
85
|
+
initializeWorkspaceDiscovery(workspacePath).catch((err: unknown) => {
|
|
86
|
+
console.error('Failed to initialize MCP:', err);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check if this is a main session (not a subagent) and set runningAt
|
|
91
|
+
const isMainSession = session && !session.parentId;
|
|
92
|
+
if (isMainSession && managesSessionLifecycle) {
|
|
93
|
+
const updatedSession = await updateSession(_sessionId, { runningAt: new Date().toISOString() });
|
|
94
|
+
if (updatedSession) {
|
|
95
|
+
emitSessionUpdated(updatedSession);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Resolve model: session override > preconfig > env default
|
|
100
|
+
const resolvedModelId = modelId || (preconfig.model ?? undefined);
|
|
101
|
+
|
|
102
|
+
const toolNames = preconfig.tools || [];
|
|
103
|
+
const resolvedProviderId = providerId;
|
|
104
|
+
|
|
105
|
+
// Inject agent home directory as additional path if this is an agent
|
|
106
|
+
const agentDir = await getAgentDirectory(preconfig.id);
|
|
107
|
+
const effectiveAdditionalPaths = agentDir
|
|
108
|
+
? [...(options.additionalPaths || []), join(agentDir, 'home')]
|
|
109
|
+
: options.additionalPaths;
|
|
110
|
+
|
|
111
|
+
const aiTools = await buildAiSdkTools({
|
|
112
|
+
toolNames,
|
|
113
|
+
workspacePath,
|
|
114
|
+
workspaceId,
|
|
115
|
+
sessionId: _sessionId,
|
|
116
|
+
modelId: resolvedModelId,
|
|
117
|
+
providerId: resolvedProviderId,
|
|
118
|
+
canSpawnSubagents: preconfig.canSpawnSubagents,
|
|
119
|
+
allowSelfAsSubagent: preconfig.allowSelfAsSubagent,
|
|
120
|
+
allowedSkills: preconfig.skills,
|
|
121
|
+
broadcastFn: options.broadcastFn,
|
|
122
|
+
additionalPaths: effectiveAdditionalPaths,
|
|
123
|
+
agentId: preconfig.id,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const selfDelegationAvailable = Boolean(aiTools.task)
|
|
127
|
+
&& preconfig.allowSelfAsSubagent === true
|
|
128
|
+
&& (await resolveEffectiveSubagentTargets({
|
|
129
|
+
sessionId: _sessionId,
|
|
130
|
+
canSpawnSubagents: preconfig.canSpawnSubagents,
|
|
131
|
+
allowSelfAsSubagent: true,
|
|
132
|
+
})).some((candidate: { id: string }) => candidate.id === preconfig.id);
|
|
133
|
+
|
|
134
|
+
// Build system message through the ordered context assembler contract
|
|
135
|
+
const systemMessage = await getContextAssembler().build({
|
|
136
|
+
preconfig,
|
|
137
|
+
workspacePath,
|
|
138
|
+
workspaceId,
|
|
139
|
+
additionalPaths: effectiveAdditionalPaths,
|
|
140
|
+
selfDelegationAvailable,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const { model, useProviderInstructions, omitMaxOutputTokens, providerOptions: baseProviderOptions } =
|
|
144
|
+
await getModelWithMetadata({
|
|
145
|
+
modelId: resolvedModelId,
|
|
146
|
+
providerId,
|
|
147
|
+
systemPrompt: systemMessage,
|
|
148
|
+
sessionId: _sessionId,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Compute auto-compaction threshold
|
|
152
|
+
const { threshold: autoThreshold, contextWindow } = computeAutoThreshold(resolvedModelId, compactionPolicy);
|
|
153
|
+
|
|
154
|
+
// Convert messages for ai-sdk
|
|
155
|
+
const modelDef = resolvedModelId ? findModel(resolvedModelId) : undefined;
|
|
156
|
+
const aiMessages = await convertToAiSdkMessages(messages, modelDef?.capabilities);
|
|
157
|
+
|
|
158
|
+
// Build stream config (variants, providerOptions, structured output)
|
|
159
|
+
const streamConfig = buildStreamConfig({
|
|
160
|
+
modelId: resolvedModelId,
|
|
161
|
+
providerId,
|
|
162
|
+
variant,
|
|
163
|
+
systemMessage,
|
|
164
|
+
baseProviderOptions,
|
|
165
|
+
responseFormat: options.responseFormat,
|
|
166
|
+
temperature: preconfig.settings?.temperature as number | undefined,
|
|
167
|
+
maxSteps,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const messageId = randomUUID();
|
|
171
|
+
const stepCtx = {
|
|
172
|
+
messageId,
|
|
173
|
+
sessionId: _sessionId,
|
|
174
|
+
stepParts: [] as StepPart[],
|
|
175
|
+
yieldFn: null as ((event: CallbackEvent) => void) | null,
|
|
176
|
+
isMainSession,
|
|
177
|
+
contextWindow,
|
|
178
|
+
autoThreshold,
|
|
179
|
+
resolvedModelId,
|
|
180
|
+
variant,
|
|
181
|
+
needsCompaction: false,
|
|
182
|
+
latestUsage: {
|
|
183
|
+
promptTokens: 0,
|
|
184
|
+
completionTokens: 0,
|
|
185
|
+
totalTokens: 0,
|
|
186
|
+
cacheReadTokens: 0,
|
|
187
|
+
cacheWriteTokens: 0,
|
|
188
|
+
noCacheTokens: 0,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const { experimental_onStepStart, onStepFinish } = createStepCallbacks(stepCtx);
|
|
193
|
+
|
|
194
|
+
const result = streamText({
|
|
195
|
+
model,
|
|
196
|
+
system: useProviderInstructions ? undefined : streamConfig.systemMessage,
|
|
197
|
+
messages: aiMessages,
|
|
198
|
+
tools: aiTools,
|
|
199
|
+
maxOutputTokens: omitMaxOutputTokens ? undefined : getMaxOutputTokens(resolvedModelId),
|
|
200
|
+
providerOptions: streamConfig.providerOptions as Parameters<typeof streamText>[0]['providerOptions'],
|
|
201
|
+
temperature: streamConfig.temperature,
|
|
202
|
+
stopWhen: stepCountIs(streamConfig.maxSteps),
|
|
203
|
+
abortSignal: abortController.signal,
|
|
204
|
+
experimental_onStepStart,
|
|
205
|
+
onStepFinish,
|
|
206
|
+
...(streamConfig.streamOutput ? { output: streamConfig.streamOutput } : {}),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Create assistant message
|
|
210
|
+
const assistantMessage: AssistantMessage = {
|
|
211
|
+
id: messageId,
|
|
212
|
+
sessionId: _sessionId,
|
|
213
|
+
role: 'assistant',
|
|
214
|
+
status: 'streaming',
|
|
215
|
+
createdAt: Date.now(),
|
|
216
|
+
modelId: resolvedModelId || getModelsConfig().defaultModel,
|
|
217
|
+
providerId: providerId || getModelsConfig().defaultProvider,
|
|
218
|
+
tokens: { prompt: 0, completion: 0 },
|
|
219
|
+
cost: 0,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
await createMessage(assistantMessage);
|
|
223
|
+
yield { type: 'message.created', message: assistantMessage };
|
|
224
|
+
|
|
225
|
+
// Set up event queue for callbacks
|
|
226
|
+
const eventQueue: Array<CallbackEvent> = [];
|
|
227
|
+
stepCtx.yieldFn = (event) => { eventQueue.push(event); };
|
|
228
|
+
|
|
229
|
+
const streamCtx = {
|
|
230
|
+
messageId,
|
|
231
|
+
sessionId: _sessionId,
|
|
232
|
+
toolParts: [] as ToolPart[],
|
|
233
|
+
currentText: '',
|
|
234
|
+
currentTextPartId: null as string | null,
|
|
235
|
+
currentTextCreatedAt: null as number | null,
|
|
236
|
+
currentReasoning: '',
|
|
237
|
+
currentReasoningPartId: null as string | null,
|
|
238
|
+
currentReasoningCreatedAt: null as number | null,
|
|
239
|
+
yieldFn: (event: MessageEvent) => { eventQueue.push(event); },
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const handlers = createStreamHandlers(streamCtx);
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
for await (const delta of result.fullStream) {
|
|
246
|
+
if (abortController.signal.aborted) {
|
|
247
|
+
await handlers.flushPending();
|
|
248
|
+
for (const event of await collectInterruptedToolPartEvents(streamCtx.toolParts, _sessionId)) {
|
|
249
|
+
yield event;
|
|
250
|
+
}
|
|
251
|
+
const interruptedMessage = buildInterruptedMessage(assistantMessage);
|
|
252
|
+
yield { type: 'message.updated', message: interruptedMessage };
|
|
253
|
+
await updateMessage(messageId, interruptedMessage, { syncFts: false });
|
|
254
|
+
await syncMessageFts(messageId);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
switch (delta.type) {
|
|
259
|
+
case 'text-delta':
|
|
260
|
+
await handlers.handleTextDelta(delta);
|
|
261
|
+
break;
|
|
262
|
+
case 'reasoning-delta':
|
|
263
|
+
await handlers.handleReasoningDelta(delta);
|
|
264
|
+
break;
|
|
265
|
+
case 'tool-call':
|
|
266
|
+
await handlers.handleToolCall(delta);
|
|
267
|
+
break;
|
|
268
|
+
case 'tool-result':
|
|
269
|
+
await handlers.handleToolResult(delta);
|
|
270
|
+
break;
|
|
271
|
+
case 'error': {
|
|
272
|
+
const error = (delta as { type: 'error'; error: unknown }).error;
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
while (eventQueue.length > 0) {
|
|
278
|
+
const event = eventQueue.shift()!;
|
|
279
|
+
yield event;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
await handlers.flushPending();
|
|
283
|
+
} catch (err) {
|
|
284
|
+
await handlers.flushPending();
|
|
285
|
+
const classified = classifyApiError(err);
|
|
286
|
+
|
|
287
|
+
console.error('[streamChat] AI SDK error', {
|
|
288
|
+
sessionId: _sessionId,
|
|
289
|
+
model: resolvedModelId,
|
|
290
|
+
provider: providerId,
|
|
291
|
+
errorType: classified.type,
|
|
292
|
+
errorMessage: classified.message,
|
|
293
|
+
retryable: classified.retryable,
|
|
294
|
+
rawError: err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : err,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (abortController.signal.aborted) {
|
|
298
|
+
for (const event of await collectInterruptedToolPartEvents(streamCtx.toolParts, _sessionId)) {
|
|
299
|
+
yield event;
|
|
300
|
+
}
|
|
301
|
+
const interruptedMessage = buildInterruptedMessage(assistantMessage);
|
|
302
|
+
yield { type: 'message.updated', message: interruptedMessage };
|
|
303
|
+
await updateMessage(messageId, interruptedMessage, { syncFts: false });
|
|
304
|
+
await syncMessageFts(messageId);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!classified.retryable) {
|
|
309
|
+
const errorMessage: AssistantMessage = {
|
|
310
|
+
...assistantMessage,
|
|
311
|
+
status: 'error',
|
|
312
|
+
error: classified.message,
|
|
313
|
+
};
|
|
314
|
+
yield { type: 'message.updated', message: errorMessage };
|
|
315
|
+
await updateMessage(messageId, errorMessage, { syncFts: false });
|
|
316
|
+
await syncMessageFts(messageId);
|
|
317
|
+
yield createErrorEvent(classified);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
throw classified;
|
|
322
|
+
} finally {
|
|
323
|
+
if (managesSessionLifecycle) {
|
|
324
|
+
interruptManager.unregisterSession(_sessionId);
|
|
325
|
+
await rejectPendingAsksBySession(_sessionId);
|
|
326
|
+
|
|
327
|
+
if (isMainSession) {
|
|
328
|
+
const updatedSession = await updateSession(_sessionId, { runningAt: null });
|
|
329
|
+
if (updatedSession) {
|
|
330
|
+
emitSessionUpdated(updatedSession);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Extract finalization data (usage + structured output)
|
|
337
|
+
const { usageData, structuredOutputData } = await extractFinalizationData({
|
|
338
|
+
result,
|
|
339
|
+
responseFormat: options.responseFormat,
|
|
340
|
+
usePromptBasedStructuredOutput: streamConfig.usePromptBasedStructuredOutput,
|
|
341
|
+
accumulatedText: streamCtx.currentText,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const finalMessage: AssistantMessage = {
|
|
345
|
+
...assistantMessage,
|
|
346
|
+
status: 'completed',
|
|
347
|
+
completedAt: Date.now(),
|
|
348
|
+
tokens: {
|
|
349
|
+
prompt: usageData?.inputTokens ?? 0,
|
|
350
|
+
completion: usageData?.outputTokens ?? 0,
|
|
351
|
+
cacheRead: usageData?.inputTokenDetails.cacheReadTokens ?? 0,
|
|
352
|
+
cacheWrite: usageData?.inputTokenDetails.cacheWriteTokens ?? 0,
|
|
353
|
+
noCache: usageData?.inputTokenDetails.noCacheTokens ?? 0,
|
|
354
|
+
},
|
|
355
|
+
...(structuredOutputData ? { structuredOutput: structuredOutputData } : {}),
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
yield { type: 'message.updated', message: finalMessage };
|
|
359
|
+
|
|
360
|
+
// Sync FTS once after all final parts and message state are persisted
|
|
361
|
+
await syncMessageFts(messageId);
|
|
362
|
+
|
|
363
|
+
if (usageData) {
|
|
364
|
+
yield {
|
|
365
|
+
type: 'usage',
|
|
366
|
+
usage: {
|
|
367
|
+
promptTokens: stepCtx.latestUsage.promptTokens,
|
|
368
|
+
completionTokens: stepCtx.latestUsage.completionTokens,
|
|
369
|
+
totalTokens: stepCtx.latestUsage.totalTokens,
|
|
370
|
+
cacheReadTokens: stepCtx.latestUsage.cacheReadTokens,
|
|
371
|
+
cacheWriteTokens: stepCtx.latestUsage.cacheWriteTokens,
|
|
372
|
+
noCacheTokens: stepCtx.latestUsage.noCacheTokens,
|
|
373
|
+
},
|
|
374
|
+
model: resolvedModelId || getModelsConfig().defaultModel,
|
|
375
|
+
variant: variant || null,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (isMainSession && stepCtx.needsCompaction) {
|
|
380
|
+
yield { type: 'needs_compaction', sessionId: _sessionId };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function chat(options: ChatOptions): Promise<ChatResult> {
|
|
385
|
+
let finalMessage: AssistantMessage | null = null;
|
|
386
|
+
const toolCalls: ToolPart[] = [];
|
|
387
|
+
|
|
388
|
+
for await (const event of streamChat(options)) {
|
|
389
|
+
if (event.type === 'part.created' && event.part.type === 'tool') {
|
|
390
|
+
toolCalls.push(event.part);
|
|
391
|
+
}
|
|
392
|
+
if (event.type === 'message.updated' && event.message.role === 'assistant') {
|
|
393
|
+
finalMessage = event.message;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
message: finalMessage!,
|
|
399
|
+
toolCalls,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
emitRuntimeEvent,
|
|
3
|
+
type BroadcastFn,
|
|
4
|
+
} from '../runtime/host-dependencies';
|
|
5
|
+
import { getAgentDirectory } from '../context';
|
|
6
|
+
import { discoverWorkspaceTools } from '../tools/tool-source';
|
|
7
|
+
import {
|
|
8
|
+
buildRetrieveToolOutputAiTool,
|
|
9
|
+
RETRIEVE_TOOL_OUTPUT_NAME,
|
|
10
|
+
wrapToolsWithOutputPolicy,
|
|
11
|
+
} from '../tool-output/policy';
|
|
12
|
+
import { getSession } from '../storage/runtime';
|
|
13
|
+
import type { AskBroadcastFn } from '../permission/ask-user-api';
|
|
14
|
+
import { hasScopedToolRegistryResolver } from '../tools/registry';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { buildExternalTools } from './tool-builders/external-tools';
|
|
17
|
+
import { buildWorkspaceTools } from './tool-builders/workspace-tools';
|
|
18
|
+
import { buildAgentTools } from './tool-builders/agent-tools';
|
|
19
|
+
import { resolveToolExecutionScopes } from './tool-capabilities';
|
|
20
|
+
import type { ToolMap } from './tool-builders/types';
|
|
21
|
+
|
|
22
|
+
export interface BuildToolsOptions {
|
|
23
|
+
toolNames: string[];
|
|
24
|
+
workspacePath: string | undefined;
|
|
25
|
+
workspaceId: string | undefined;
|
|
26
|
+
sessionId: string;
|
|
27
|
+
rootSessionId?: string;
|
|
28
|
+
modelId?: string;
|
|
29
|
+
providerId?: string;
|
|
30
|
+
canSpawnSubagents?: boolean | string[] | null;
|
|
31
|
+
allowSelfAsSubagent?: boolean;
|
|
32
|
+
allowedSkills?: string[] | null;
|
|
33
|
+
broadcastFn?: AskBroadcastFn;
|
|
34
|
+
additionalPaths?: string[];
|
|
35
|
+
agentId?: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function buildAiSdkTools(
|
|
39
|
+
options: BuildToolsOptions,
|
|
40
|
+
broadcast: BroadcastFn = emitRuntimeEvent,
|
|
41
|
+
): Promise<Record<string, import('ai').Tool>> {
|
|
42
|
+
const {
|
|
43
|
+
toolNames,
|
|
44
|
+
workspacePath,
|
|
45
|
+
workspaceId,
|
|
46
|
+
sessionId,
|
|
47
|
+
rootSessionId: explicitRootSessionId,
|
|
48
|
+
modelId,
|
|
49
|
+
providerId,
|
|
50
|
+
canSpawnSubagents,
|
|
51
|
+
allowSelfAsSubagent,
|
|
52
|
+
allowedSkills,
|
|
53
|
+
broadcastFn,
|
|
54
|
+
additionalPaths,
|
|
55
|
+
agentId,
|
|
56
|
+
} = options;
|
|
57
|
+
|
|
58
|
+
// Resolve root session ID by walking up the parent chain
|
|
59
|
+
const rootSessionId = explicitRootSessionId ?? (await (async () => {
|
|
60
|
+
let current = sessionId;
|
|
61
|
+
let session = await getSession(current);
|
|
62
|
+
while (session?.parentId) {
|
|
63
|
+
current = session.parentId;
|
|
64
|
+
session = await getSession(current);
|
|
65
|
+
}
|
|
66
|
+
return current;
|
|
67
|
+
})());
|
|
68
|
+
|
|
69
|
+
// Resolve execution scopes for capability filtering (separate from ask-routing root)
|
|
70
|
+
const executionScopes = await resolveToolExecutionScopes(sessionId);
|
|
71
|
+
|
|
72
|
+
const canSpawn = canSpawnSubagents === true
|
|
73
|
+
|| (Array.isArray(canSpawnSubagents) && canSpawnSubagents.length > 0);
|
|
74
|
+
const allowedSubagentIds = Array.isArray(canSpawnSubagents) ? canSpawnSubagents : undefined;
|
|
75
|
+
|
|
76
|
+
// Resolve agent directory for skills
|
|
77
|
+
const agentDir = agentId ? await getAgentDirectory(agentId) : undefined;
|
|
78
|
+
const agentSkillsDir = agentDir ? join(agentDir, 'skills') : undefined;
|
|
79
|
+
|
|
80
|
+
// Phase 1: External tools (task subagent + registry tools)
|
|
81
|
+
const externalTools = await buildExternalTools({
|
|
82
|
+
toolNames,
|
|
83
|
+
canSpawnSubagents,
|
|
84
|
+
allowSelfAsSubagent,
|
|
85
|
+
broadcastFn,
|
|
86
|
+
broadcast,
|
|
87
|
+
sessionId,
|
|
88
|
+
workspaceId,
|
|
89
|
+
workspacePath,
|
|
90
|
+
rootSessionId,
|
|
91
|
+
executionScopes,
|
|
92
|
+
modelId,
|
|
93
|
+
providerId,
|
|
94
|
+
additionalPaths,
|
|
95
|
+
});
|
|
96
|
+
const tools: ToolMap = { ...externalTools };
|
|
97
|
+
|
|
98
|
+
// Phase 2: Workspace-gated tools (memory, workflow, skills, search, scheduler)
|
|
99
|
+
if (workspaceId && workspacePath) {
|
|
100
|
+
const workspaceTools = await buildWorkspaceTools({
|
|
101
|
+
workspaceId,
|
|
102
|
+
workspacePath,
|
|
103
|
+
rootSessionId,
|
|
104
|
+
sessionId,
|
|
105
|
+
canSpawn,
|
|
106
|
+
canSpawnSubagents,
|
|
107
|
+
allowSelfAsSubagent,
|
|
108
|
+
allowedSubagentIds,
|
|
109
|
+
broadcastFn,
|
|
110
|
+
agentId,
|
|
111
|
+
allowedSkills,
|
|
112
|
+
agentSkillsDir,
|
|
113
|
+
});
|
|
114
|
+
Object.assign(tools, workspaceTools);
|
|
115
|
+
|
|
116
|
+
// Phase 3: MCP tools
|
|
117
|
+
try {
|
|
118
|
+
const mcpTools = await discoverWorkspaceTools(workspacePath, sessionId);
|
|
119
|
+
Object.assign(tools, mcpTools);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error('Failed to load MCP tools:', err);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Phase 4: Agent-specific tools (agent_memory, agent_skill_manage)
|
|
126
|
+
if (agentDir) {
|
|
127
|
+
const agentTools = await buildAgentTools({ agentDir });
|
|
128
|
+
Object.assign(tools, agentTools);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The unscoped legacy Jean2 path keeps the unconditional retrieval
|
|
132
|
+
// injection. Under a scoped contributed resolver, retrieval is an
|
|
133
|
+
// ordinary contributed tool: it enters through toolNames -> resolver ->
|
|
134
|
+
// buildExternalTools exactly when its contribution is visible.
|
|
135
|
+
if (!hasScopedToolRegistryResolver()) {
|
|
136
|
+
tools[RETRIEVE_TOOL_OUTPUT_NAME] = buildRetrieveToolOutputAiTool(sessionId);
|
|
137
|
+
}
|
|
138
|
+
return wrapToolsWithOutputPolicy(tools, { sessionId, workspaceId });
|
|
139
|
+
}
|