@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,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C6 retry stream loop. `streamChatWithRetry` keeps its exact pre-C6 event
|
|
3
|
+
* shape, wire errors, message finalization, interrupt registration, and
|
|
4
|
+
* running-state updates; every policy decision (classification, backoff,
|
|
5
|
+
* circuit state, and the tool-activity side-effect barrier) resolves through
|
|
6
|
+
* `getRetryPolicy()`, so composed agent scopes run on their own agent-scoped
|
|
7
|
+
* policy while unscoped consumers keep the process-default behavior.
|
|
8
|
+
*
|
|
9
|
+
* Its core edges are named and AST-gated by the `retry-domain-no-core` rule:
|
|
10
|
+
* the session interrupt manager for backoff cancellation and the turn stream
|
|
11
|
+
* it wraps. Both stay in core until their owning phases (C6/C7).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ChatOptions } from '../core/agent';
|
|
15
|
+
import type { UsageEventData } from '../core/step-handlers';
|
|
16
|
+
import type {
|
|
17
|
+
AssistantMessage, AuthErrorMessage, ChatRetryMessage, ContextOverflowErrorMessage, ErrorMessage, InvalidRequestErrorMessage, MessageEvent, RateLimitErrorMessage, ServerErrorMessage, TimeoutErrorMessage, ToolPart } from '@capekai/types';
|
|
18
|
+
import {
|
|
19
|
+
ApiErrorType,
|
|
20
|
+
ERROR_CHAT_FAILED,
|
|
21
|
+
ERROR_RATE_LIMIT,
|
|
22
|
+
ERROR_SERVER_ERROR,
|
|
23
|
+
ERROR_TIMEOUT,
|
|
24
|
+
type ClassifiedError,
|
|
25
|
+
} from '../utils/errors';
|
|
26
|
+
import { emitSessionUpdated } from '../runtime/host-dependencies';
|
|
27
|
+
import {
|
|
28
|
+
getPartsByMessage,
|
|
29
|
+
getSession,
|
|
30
|
+
syncMessageFts,
|
|
31
|
+
transitionToolToInterrupted,
|
|
32
|
+
updateMessage,
|
|
33
|
+
updateSession,
|
|
34
|
+
} from '../storage/runtime';
|
|
35
|
+
import { rejectPendingAsksBySession } from '../permission/ask-user-api';
|
|
36
|
+
import { interruptManager } from '../core/interrupt';
|
|
37
|
+
import {
|
|
38
|
+
getRetryPolicy,
|
|
39
|
+
RetryDelayAbortedError,
|
|
40
|
+
type StreamRetryPolicy,
|
|
41
|
+
} from './policy';
|
|
42
|
+
|
|
43
|
+
export type StreamChatEvent =
|
|
44
|
+
| MessageEvent
|
|
45
|
+
| { type: 'usage'; usage: UsageEventData; model: string; variant: string | null }
|
|
46
|
+
| { type: 'needs_compaction'; sessionId: string }
|
|
47
|
+
| ChatRetryMessage
|
|
48
|
+
| RateLimitErrorMessage
|
|
49
|
+
| ServerErrorMessage
|
|
50
|
+
| TimeoutErrorMessage
|
|
51
|
+
| AuthErrorMessage
|
|
52
|
+
| ContextOverflowErrorMessage
|
|
53
|
+
| InvalidRequestErrorMessage
|
|
54
|
+
| ErrorMessage;
|
|
55
|
+
|
|
56
|
+
export type StreamChatFn = (options: ChatOptions) => AsyncGenerator<StreamChatEvent>;
|
|
57
|
+
|
|
58
|
+
async function finalizeFailedAttempt(
|
|
59
|
+
message: AssistantMessage | null,
|
|
60
|
+
classifiedError: { message: string },
|
|
61
|
+
retryFailed: boolean,
|
|
62
|
+
): Promise<MessageEvent[]> {
|
|
63
|
+
if (!message) return [];
|
|
64
|
+
|
|
65
|
+
const events: MessageEvent[] = [];
|
|
66
|
+
const parts = await getPartsByMessage(message.id);
|
|
67
|
+
for (const part of parts) {
|
|
68
|
+
if (part.type !== 'tool') continue;
|
|
69
|
+
const toolPart = part as ToolPart;
|
|
70
|
+
if (toolPart.state.status !== 'pending' && toolPart.state.status !== 'running') continue;
|
|
71
|
+
const interruptedPart = await transitionToolToInterrupted(toolPart.id, 'error');
|
|
72
|
+
if (interruptedPart) {
|
|
73
|
+
events.push({ type: 'part.updated', sessionId: message.sessionId, part: interruptedPart });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const errorMessage: AssistantMessage = {
|
|
78
|
+
...message,
|
|
79
|
+
status: 'error',
|
|
80
|
+
error: classifiedError.message,
|
|
81
|
+
completedAt: Date.now(),
|
|
82
|
+
...(retryFailed ? { mode: 'retry_failed' as const } : {}),
|
|
83
|
+
};
|
|
84
|
+
updateMessage(message.id, errorMessage, { syncFts: false });
|
|
85
|
+
syncMessageFts(message.id);
|
|
86
|
+
events.push({ type: 'message.updated', message: errorMessage });
|
|
87
|
+
return events;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createFinalErrorEvent(classifiedError: ClassifiedError): StreamChatEvent {
|
|
91
|
+
if (classifiedError.type === ApiErrorType.RateLimit) {
|
|
92
|
+
return {
|
|
93
|
+
type: 'error.rate_limit',
|
|
94
|
+
code: ERROR_RATE_LIMIT,
|
|
95
|
+
message: classifiedError.message,
|
|
96
|
+
retryAfterMs: classifiedError.retryAfterMs ?? 5_000,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (classifiedError.type === ApiErrorType.ServerError || classifiedError.type === ApiErrorType.Network) {
|
|
100
|
+
return {
|
|
101
|
+
type: 'error.server',
|
|
102
|
+
code: ERROR_SERVER_ERROR,
|
|
103
|
+
message: classifiedError.message,
|
|
104
|
+
retryAfterMs: classifiedError.retryAfterMs,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (classifiedError.type === ApiErrorType.ContextOverflow) {
|
|
108
|
+
return {
|
|
109
|
+
type: 'error.context_overflow',
|
|
110
|
+
code: 'context_overflow',
|
|
111
|
+
message: classifiedError.message,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (classifiedError.type === ApiErrorType.Timeout) {
|
|
115
|
+
return {
|
|
116
|
+
type: 'error.timeout',
|
|
117
|
+
code: ERROR_TIMEOUT,
|
|
118
|
+
message: classifiedError.message,
|
|
119
|
+
retryAfterMs: classifiedError.retryAfterMs,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
type: 'error',
|
|
124
|
+
code: ERROR_CHAT_FAILED,
|
|
125
|
+
message: classifiedError.message,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function* streamChatWithRetry(
|
|
130
|
+
options: ChatOptions,
|
|
131
|
+
streamChatFn?: StreamChatFn,
|
|
132
|
+
policyOptions: StreamRetryPolicy = {},
|
|
133
|
+
): AsyncGenerator<StreamChatEvent> {
|
|
134
|
+
const policy = getRetryPolicy();
|
|
135
|
+
const maxRetries = policyOptions.maxRetries ?? policy.defaults.maxRetries;
|
|
136
|
+
const baseDelayMs = policyOptions.baseDelayMs ?? policy.defaults.baseDelayMs;
|
|
137
|
+
const maxDelayMs = policyOptions.maxDelayMs ?? policy.defaults.maxDelayMs;
|
|
138
|
+
const jitterRatio = policyOptions.jitterRatio ?? policy.defaults.jitterRatio;
|
|
139
|
+
const circuitKey = policy.circuitKey(options.providerId, options.modelId);
|
|
140
|
+
const session = await getSession(options.sessionId);
|
|
141
|
+
const abortController = interruptManager.registerSession(options.sessionId, session?.parentId ?? undefined);
|
|
142
|
+
const isMainSession = session && !session.parentId;
|
|
143
|
+
|
|
144
|
+
if (isMainSession) {
|
|
145
|
+
const updatedSession = await updateSession(options.sessionId, { runningAt: new Date().toISOString() });
|
|
146
|
+
if (updatedSession) {
|
|
147
|
+
emitSessionUpdated(updatedSession);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const circuitRemainingMs = policy.openCircuitRemainingMs(circuitKey);
|
|
153
|
+
if (circuitRemainingMs > 0) {
|
|
154
|
+
const message = 'Provider is temporarily unavailable after repeated failures.';
|
|
155
|
+
yield {
|
|
156
|
+
type: 'chat.retry',
|
|
157
|
+
sessionId: options.sessionId,
|
|
158
|
+
status: 'exhausted',
|
|
159
|
+
retryNumber: 0,
|
|
160
|
+
maxRetries,
|
|
161
|
+
errorType: 'server_error',
|
|
162
|
+
message,
|
|
163
|
+
};
|
|
164
|
+
yield {
|
|
165
|
+
type: 'error.server',
|
|
166
|
+
code: ERROR_SERVER_ERROR,
|
|
167
|
+
message,
|
|
168
|
+
retryAfterMs: circuitRemainingMs,
|
|
169
|
+
};
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let retries = 0;
|
|
174
|
+
while (retries <= maxRetries) {
|
|
175
|
+
let lastAssistantMessage: AssistantMessage | null = null;
|
|
176
|
+
let attemptHadToolActivity = false;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const stream = streamChatFn ?? (await import('../core/agent')).streamChat;
|
|
180
|
+
for await (const event of stream({ ...options, retryAbortController: abortController })) {
|
|
181
|
+
if (event.type === 'message.created' || event.type === 'message.updated') {
|
|
182
|
+
if (event.message.role === 'assistant') {
|
|
183
|
+
lastAssistantMessage = event.message as AssistantMessage;
|
|
184
|
+
}
|
|
185
|
+
} else if (
|
|
186
|
+
(event.type === 'part.created' || event.type === 'part.updated')
|
|
187
|
+
&& event.part.type === 'tool'
|
|
188
|
+
) {
|
|
189
|
+
attemptHadToolActivity = true;
|
|
190
|
+
}
|
|
191
|
+
yield event;
|
|
192
|
+
}
|
|
193
|
+
policy.resetCircuit(circuitKey);
|
|
194
|
+
return;
|
|
195
|
+
} catch (err) {
|
|
196
|
+
const classifiedError = policy.classify(err);
|
|
197
|
+
const retryNumber = retries + 1;
|
|
198
|
+
const policyCanRetry = policy.canRetry({
|
|
199
|
+
retryNumber,
|
|
200
|
+
maxRetries,
|
|
201
|
+
classified: classifiedError,
|
|
202
|
+
attemptHadToolActivity,
|
|
203
|
+
aborted: abortController.signal.aborted,
|
|
204
|
+
});
|
|
205
|
+
// C6 step 6: mandatory side-effect barrier enforced by the runtime
|
|
206
|
+
// loop from non-overridable runtime evidence. A custom policy can
|
|
207
|
+
// only advise; it can never authorize a replay after tool activity
|
|
208
|
+
// or after the run was aborted.
|
|
209
|
+
const canRetry = policyCanRetry
|
|
210
|
+
&& !attemptHadToolActivity
|
|
211
|
+
&& !abortController.signal.aborted;
|
|
212
|
+
const circuitOpened = classifiedError.retryable
|
|
213
|
+
&& !canRetry
|
|
214
|
+
&& !abortController.signal.aborted
|
|
215
|
+
? policy.recordCircuitFailure(circuitKey)
|
|
216
|
+
: false;
|
|
217
|
+
|
|
218
|
+
console.error('[streamChatWithRetry] AI SDK error', {
|
|
219
|
+
sessionId: options.sessionId,
|
|
220
|
+
model: options.modelId,
|
|
221
|
+
provider: options.providerId,
|
|
222
|
+
attempt: retries + 1,
|
|
223
|
+
maxRetries,
|
|
224
|
+
errorType: classifiedError.type,
|
|
225
|
+
errorMessage: classifiedError.message,
|
|
226
|
+
retryable: classifiedError.retryable,
|
|
227
|
+
attemptHadToolActivity,
|
|
228
|
+
circuitOpened,
|
|
229
|
+
rawError: err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : err,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
for (const event of await finalizeFailedAttempt(lastAssistantMessage, classifiedError, canRetry)) {
|
|
233
|
+
yield event;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!canRetry) {
|
|
237
|
+
if (classifiedError.retryable) {
|
|
238
|
+
const message = policy.exhaustedMessage({ attemptHadToolActivity, circuitOpened })
|
|
239
|
+
?? classifiedError.message;
|
|
240
|
+
yield {
|
|
241
|
+
type: 'chat.retry',
|
|
242
|
+
sessionId: options.sessionId,
|
|
243
|
+
status: abortController.signal.aborted ? 'cancelled' : 'exhausted',
|
|
244
|
+
retryNumber: retries,
|
|
245
|
+
maxRetries,
|
|
246
|
+
errorType: policy.retryErrorType(classifiedError),
|
|
247
|
+
message,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
if (!abortController.signal.aborted) {
|
|
251
|
+
yield createFinalErrorEvent(classifiedError);
|
|
252
|
+
}
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
retries = retryNumber;
|
|
257
|
+
const delayMs = policy.calculateDelay(
|
|
258
|
+
retryNumber,
|
|
259
|
+
classifiedError,
|
|
260
|
+
baseDelayMs,
|
|
261
|
+
maxDelayMs,
|
|
262
|
+
jitterRatio,
|
|
263
|
+
);
|
|
264
|
+
const retryAt = Date.now() + delayMs;
|
|
265
|
+
const retryMessage: ChatRetryMessage = {
|
|
266
|
+
type: 'chat.retry',
|
|
267
|
+
sessionId: options.sessionId,
|
|
268
|
+
status: 'scheduled',
|
|
269
|
+
retryNumber,
|
|
270
|
+
maxRetries,
|
|
271
|
+
errorType: policy.retryErrorType(classifiedError),
|
|
272
|
+
message: classifiedError.message,
|
|
273
|
+
delayMs,
|
|
274
|
+
retryAt,
|
|
275
|
+
};
|
|
276
|
+
yield retryMessage;
|
|
277
|
+
console.log(`[streamChatWithRetry] Retrying ${options.sessionId} (${retryNumber}/${maxRetries}) in ${delayMs}ms`);
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
await policy.waitForRetry(delayMs, abortController.signal);
|
|
281
|
+
} catch (delayError) {
|
|
282
|
+
if (!(delayError instanceof RetryDelayAbortedError)) {
|
|
283
|
+
throw delayError;
|
|
284
|
+
}
|
|
285
|
+
yield {
|
|
286
|
+
...retryMessage,
|
|
287
|
+
status: 'cancelled',
|
|
288
|
+
delayMs: undefined,
|
|
289
|
+
retryAt: undefined,
|
|
290
|
+
};
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
yield {
|
|
295
|
+
...retryMessage,
|
|
296
|
+
status: 'started',
|
|
297
|
+
delayMs: undefined,
|
|
298
|
+
retryAt: undefined,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} finally {
|
|
303
|
+
interruptManager.unregisterSession(options.sessionId);
|
|
304
|
+
await rejectPendingAsksBySession(options.sessionId);
|
|
305
|
+
if (isMainSession) {
|
|
306
|
+
const updatedSession = await updateSession(options.sessionId, { runningAt: null });
|
|
307
|
+
if (updatedSession) {
|
|
308
|
+
emitSessionUpdated(updatedSession);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { CapekPlugin, AgentScopeHandle, RunScopeHandle } from '../kernel/types';
|
|
2
|
+
|
|
3
|
+
export interface AgentRunContext {
|
|
4
|
+
runId: string;
|
|
5
|
+
signal: AbortSignal;
|
|
6
|
+
scope: RunScopeHandle;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AgentDriver<Input, Result> {
|
|
10
|
+
run(context: AgentRunContext, input: Input): Promise<Result>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AgentRuntimeRunOptions {
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
cancellationReason?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AgentRuntime<Input, Result> {
|
|
19
|
+
run(runId: string, input: Input, options?: AgentRuntimeRunOptions): Promise<Result>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AgentRuntimeOptions<Input, Result> {
|
|
23
|
+
agentScope: AgentScopeHandle;
|
|
24
|
+
driver: AgentDriver<Input, Result>;
|
|
25
|
+
runPlugins?: (input: Input) => readonly CapekPlugin<unknown>[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createAgentRuntime<Input, Result>(
|
|
29
|
+
options: AgentRuntimeOptions<Input, Result>,
|
|
30
|
+
): AgentRuntime<Input, Result> {
|
|
31
|
+
return {
|
|
32
|
+
async run(runId, input, runOptions = {}) {
|
|
33
|
+
const runScope = await options.agentScope.createRunScope(
|
|
34
|
+
runId,
|
|
35
|
+
options.runPlugins?.(input) ?? [],
|
|
36
|
+
);
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const cancel = (): void => {
|
|
39
|
+
if (!controller.signal.aborted) {
|
|
40
|
+
controller.abort(runOptions.signal?.reason ?? new Error(runOptions.cancellationReason ?? 'Agent run cancelled'));
|
|
41
|
+
}
|
|
42
|
+
runScope.cancel(runOptions.cancellationReason ?? 'caller signal');
|
|
43
|
+
};
|
|
44
|
+
runOptions.signal?.addEventListener('abort', cancel, { once: true });
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
if (runOptions.signal?.aborted) {
|
|
48
|
+
cancel();
|
|
49
|
+
throw controller.signal.reason;
|
|
50
|
+
}
|
|
51
|
+
await runScope.start();
|
|
52
|
+
const execution = options.driver.run({ runId, signal: controller.signal, scope: runScope }, input);
|
|
53
|
+
const barrier = runScope.registerCleanupBarrier(execution.then(() => {}, () => {}));
|
|
54
|
+
try {
|
|
55
|
+
const result = await execution;
|
|
56
|
+
await runScope.markTerminal('completed');
|
|
57
|
+
return result;
|
|
58
|
+
} finally {
|
|
59
|
+
barrier.dispose();
|
|
60
|
+
}
|
|
61
|
+
} catch (error: unknown) {
|
|
62
|
+
if (runScope.runStatus === 'created' || runScope.runStatus === 'running') {
|
|
63
|
+
if (controller.signal.aborted || runOptions.signal?.aborted) {
|
|
64
|
+
await runScope.cancel(runOptions.cancellationReason ?? 'caller signal').completion;
|
|
65
|
+
} else {
|
|
66
|
+
await runScope.markTerminal('failed');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
} finally {
|
|
71
|
+
if (!controller.signal.aborted) {
|
|
72
|
+
controller.abort(new Error('Agent run settled'));
|
|
73
|
+
}
|
|
74
|
+
runOptions.signal?.removeEventListener('abort', cancel);
|
|
75
|
+
if (runScope.runStatus === 'created' || runScope.runStatus === 'running') {
|
|
76
|
+
await runScope.cancel('runtime cleanup').completion;
|
|
77
|
+
} else {
|
|
78
|
+
await runScope.dispose();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AgentDriver, AgentRunContext } from './agent-runtime';
|
|
2
|
+
|
|
3
|
+
export interface DriverAdvance<Result> {
|
|
4
|
+
result: Result;
|
|
5
|
+
continuation: 'complete' | 'continue';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface DefaultDriverInput<Result> {
|
|
9
|
+
advance(context: AgentRunContext): Promise<DriverAdvance<Result>>;
|
|
10
|
+
maxContinuations?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class DefaultAgentDriver implements AgentDriver<DefaultDriverInput<unknown>, unknown> {
|
|
14
|
+
async run(context: AgentRunContext, input: DefaultDriverInput<unknown>): Promise<unknown> {
|
|
15
|
+
const maxContinuations = input.maxContinuations ?? 1_000;
|
|
16
|
+
for (let continuation = 0; continuation <= maxContinuations; continuation += 1) {
|
|
17
|
+
if (context.signal.aborted) throw context.signal.reason;
|
|
18
|
+
const turn = await input.advance(context);
|
|
19
|
+
if (turn.continuation === 'complete') return turn.result;
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Agent driver exceeded ${maxContinuations} continuations`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import type { Ask, ToolDisplayConfig } from '@capekai/tool';
|
|
3
|
+
import type { AnyVisualization } from '@capekai/types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal contributed-domain-tool payload context (C5).
|
|
7
|
+
*
|
|
8
|
+
* The turn-execution core must not import optional domains. A domain plugin
|
|
9
|
+
* attaches its executable payload to its kernel tool contribution under
|
|
10
|
+
* `DOMAIN_TOOL_PAYLOAD_FIELD`; `enterAgentScope` collects every visible
|
|
11
|
+
* payload into a scoped map keyed by tool name. Remaining C5 domains reuse
|
|
12
|
+
* this seam instead of adding domain-specific ALS layers.
|
|
13
|
+
*
|
|
14
|
+
* Three states the core distinguishes:
|
|
15
|
+
* - getContributedDomainToolPayloads() === null: no composed agent scope is
|
|
16
|
+
* entered; a registered legacy fallback may apply.
|
|
17
|
+
* - an empty map: a composed scope is entered but carries no domain
|
|
18
|
+
* payloads; legacy fallbacks must never apply.
|
|
19
|
+
* - a map with entries: the composed scope's own domain payloads.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export const DOMAIN_TOOL_PAYLOAD_FIELD = 'capekDomainToolPayload';
|
|
23
|
+
|
|
24
|
+
export interface DomainToolExecuteContext {
|
|
25
|
+
readonly workspaceId: string;
|
|
26
|
+
readonly sessionId: string;
|
|
27
|
+
/** Required: risk-bearing execution must always be able to ask for
|
|
28
|
+
* permission. Callers supply a real ask function even for risk 'none'. */
|
|
29
|
+
readonly ask: (ask: Ask) => Promise<unknown>;
|
|
30
|
+
readonly agentId?: string | null;
|
|
31
|
+
/** Domain-specific fields travel through this index signature. */
|
|
32
|
+
readonly [field: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DomainToolPayload {
|
|
36
|
+
readonly name: string;
|
|
37
|
+
readonly description: string;
|
|
38
|
+
readonly inputSchema: Readonly<Record<string, unknown>>;
|
|
39
|
+
/** Client display hints declared by the domain (collapsed-row summary
|
|
40
|
+
* template). Travels through tool catalogs to clients; not consumed by
|
|
41
|
+
* the runtime. Mirrors ToolDefinition.display. */
|
|
42
|
+
readonly display?: ToolDisplayConfig;
|
|
43
|
+
/** Optional result visualization: called with the tool input and the
|
|
44
|
+
* domain's execute result after success. The builders merge the returned
|
|
45
|
+
* visualization into the tool output as `_visualization` (stripped again
|
|
46
|
+
* before LLM consumption). */
|
|
47
|
+
readonly visualize?: (
|
|
48
|
+
input: Record<string, unknown>,
|
|
49
|
+
result: Record<string, unknown>,
|
|
50
|
+
) => AnyVisualization | undefined;
|
|
51
|
+
/** The domain's availability predicate (workspace settings gate). The
|
|
52
|
+
* owning domain uses the same predicate for its context contribution. */
|
|
53
|
+
readonly isEnabled?: (workspaceId: string, sessionId?: string) => boolean | Promise<boolean>;
|
|
54
|
+
/** Optional per-build definition resolver for tools whose description or
|
|
55
|
+
* schema depends on per-session data (the task tool's resolved subagent
|
|
56
|
+
* list, the workflow tool's allowed leaf agents). Returns null when the
|
|
57
|
+
* tool must be omitted for this session. `allowedSubagentIds` carries a
|
|
58
|
+
* build-time-captured target list when the domain needs it. */
|
|
59
|
+
readonly resolveDefinition?: (
|
|
60
|
+
sessionId: string,
|
|
61
|
+
options?: Record<string, unknown>,
|
|
62
|
+
) => Promise<{
|
|
63
|
+
description: string;
|
|
64
|
+
inputSchema: Readonly<Record<string, unknown>>;
|
|
65
|
+
allowedSubagentIds?: string[];
|
|
66
|
+
} | null>;
|
|
67
|
+
execute(
|
|
68
|
+
input: Record<string, unknown>,
|
|
69
|
+
context: DomainToolExecuteContext,
|
|
70
|
+
): Promise<Record<string, unknown>>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isDomainToolPayload(value: unknown): value is DomainToolPayload {
|
|
74
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
75
|
+
const candidate = value as Record<string, unknown>;
|
|
76
|
+
return typeof candidate.name === 'string'
|
|
77
|
+
&& typeof candidate.description === 'string'
|
|
78
|
+
&& typeof candidate.inputSchema === 'object' && candidate.inputSchema !== null
|
|
79
|
+
&& typeof candidate.execute === 'function';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Merges a payload's `visualize` result into the execute output as
|
|
83
|
+
* `_visualization`, mirroring the external-tool builder's merge. Only a
|
|
84
|
+
* successful object-shaped output can carry a visualization. */
|
|
85
|
+
export function mergeDomainToolVisualization(
|
|
86
|
+
payload: DomainToolPayload,
|
|
87
|
+
input: Record<string, unknown>,
|
|
88
|
+
result: Record<string, unknown>,
|
|
89
|
+
): Record<string, unknown> {
|
|
90
|
+
if (!payload.visualize) return result;
|
|
91
|
+
if (typeof result.error === 'string' && result.error.length > 0) return result;
|
|
92
|
+
const visualization = payload.visualize(input, result);
|
|
93
|
+
if (!visualization) return result;
|
|
94
|
+
return { ...result, _visualization: visualization };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const scopedPayloads = new AsyncLocalStorage<ReadonlyMap<string, DomainToolPayload>>();
|
|
98
|
+
|
|
99
|
+
export function withContributedDomainToolPayloads<T>(
|
|
100
|
+
payloads: ReadonlyMap<string, DomainToolPayload>,
|
|
101
|
+
callback: () => T,
|
|
102
|
+
): T {
|
|
103
|
+
return scopedPayloads.run(payloads, callback);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** null means no composed agent scope is entered; an empty map is a real
|
|
107
|
+
* composed scope with zero domain payloads and disables fallbacks. */
|
|
108
|
+
export function getContributedDomainToolPayloads(): ReadonlyMap<string, DomainToolPayload> | null {
|
|
109
|
+
return scopedPayloads.getStore() ?? null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const fallbacks = new Map<string, DomainToolPayload>();
|
|
113
|
+
|
|
114
|
+
export function registerDomainToolFallback(name: string, payload: DomainToolPayload): void {
|
|
115
|
+
fallbacks.set(name, payload);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getDomainToolFallback(name: string): DomainToolPayload | null {
|
|
119
|
+
return fallbacks.get(name) ?? null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Registered domain-tool fallback definitions for tool catalogs: name,
|
|
123
|
+
* description, schema, and display hints. Introspects the fallback registry
|
|
124
|
+
* the server installs at bootstrap. */
|
|
125
|
+
export function listDomainToolFallbackDefinitions(): Array<{
|
|
126
|
+
name: string;
|
|
127
|
+
description: string;
|
|
128
|
+
inputSchema: Record<string, unknown>;
|
|
129
|
+
display?: ToolDisplayConfig;
|
|
130
|
+
}> {
|
|
131
|
+
return [...fallbacks.values()].map((payload) => ({
|
|
132
|
+
name: payload.name,
|
|
133
|
+
description: payload.description,
|
|
134
|
+
inputSchema: payload.inputSchema as Record<string, unknown>,
|
|
135
|
+
...(payload.display ? { display: payload.display } : {}),
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isTestExecution(): boolean {
|
|
140
|
+
return process.env.NODE_ENV === 'test'
|
|
141
|
+
|| (globalThis as { Bun?: { env?: Record<string, string> } }).Bun?.env?.NODE_ENV === 'test';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Test-only destructive reset. Fails closed outside test execution: a
|
|
145
|
+
* production process must never be able to wipe the unscoped fallback
|
|
146
|
+
* registry. The production installation path (configureRuntimeHost plus the
|
|
147
|
+
* six install*ToolFallback calls) is the idempotent way to restore the
|
|
148
|
+
* complete inventory. */
|
|
149
|
+
export function resetDomainToolFallbacksForTests(): void {
|
|
150
|
+
if (!isTestExecution()) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
'Domain tool fallback reset is only available during test execution',
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
fallbacks.clear();
|
|
156
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Ask } from '@capekai/tool'
|
|
2
|
+
import type { AskAuthority, AssistantMessage, Message, MessageWithParts, Part, QueuedMessage, Session } from '@capekai/types';
|
|
3
|
+
import type { UsageEventData } from '../core/step-handlers';
|
|
4
|
+
|
|
5
|
+
export type RuntimeEvent =
|
|
6
|
+
| { kind: 'message'; action: 'created' | 'updated'; message: Message }
|
|
7
|
+
| { kind: 'part'; action: 'created' | 'updated'; sessionId: string; part: Part }
|
|
8
|
+
| { kind: 'part'; action: 'append'; sessionId: string; partId: string; field: 'text' | 'reasoning'; delta: string }
|
|
9
|
+
| { kind: 'session'; action: 'created' | 'updated' | 'renamed'; session: Session }
|
|
10
|
+
| { kind: 'session'; action: 'state'; sessionId: string; messages: MessageWithParts[] }
|
|
11
|
+
| { kind: 'usage'; sessionId: string; usage: UsageEventData; model: string; variant?: string }
|
|
12
|
+
| {
|
|
13
|
+
kind: 'retry';
|
|
14
|
+
sessionId: string;
|
|
15
|
+
status: 'scheduled' | 'started' | 'exhausted' | 'cancelled';
|
|
16
|
+
attempt: number;
|
|
17
|
+
maxAttempts: number;
|
|
18
|
+
errorType: 'rate_limit' | 'server_error' | 'timeout' | 'network';
|
|
19
|
+
message: string;
|
|
20
|
+
delayMs?: number;
|
|
21
|
+
retryAt?: number;
|
|
22
|
+
}
|
|
23
|
+
| { kind: 'failure'; category: 'generic'; code: string; message: string; sessionId?: string }
|
|
24
|
+
| { kind: 'failure'; category: 'rate_limit'; code: 'rate_limit'; message: string; sessionId?: string; retryAfterMs: number }
|
|
25
|
+
| { kind: 'failure'; category: 'server'; code: 'server_error'; message: string; sessionId?: string; retryAfterMs?: number }
|
|
26
|
+
| { kind: 'failure'; category: 'timeout'; code: 'timeout'; message: string; sessionId?: string; retryAfterMs?: number }
|
|
27
|
+
| { kind: 'queue'; action: 'added'; sessionId: string; message: QueuedMessage }
|
|
28
|
+
| { kind: 'queue'; action: 'sending'; sessionId: string; queueId: string }
|
|
29
|
+
| {
|
|
30
|
+
kind: 'ask';
|
|
31
|
+
action: 'requested';
|
|
32
|
+
sessionId: string;
|
|
33
|
+
toolCallId: string;
|
|
34
|
+
toolName: string;
|
|
35
|
+
ask: Ask;
|
|
36
|
+
requestId?: string;
|
|
37
|
+
authority?: AskAuthority;
|
|
38
|
+
}
|
|
39
|
+
| { kind: 'ask'; action: 'timed_out'; sessionId: string; toolCallId: string; requestId?: string }
|
|
40
|
+
| { kind: 'terminal'; message: AssistantMessage; sessionId: string };
|
|
41
|
+
|
|
42
|
+
export type RuntimeAudience<Origin = unknown> =
|
|
43
|
+
| { scope: 'global' }
|
|
44
|
+
| { scope: 'session'; sessionId: string }
|
|
45
|
+
| { scope: 'origin'; origin: Origin }
|
|
46
|
+
| { scope: 'controller'; sessionId: string }
|
|
47
|
+
| { scope: 'ask_targets'; sessionId: string; authority: AskAuthority }
|
|
48
|
+
| { scope: 'host' };
|
|
49
|
+
|
|
50
|
+
export interface RuntimeDelivery<Origin = unknown> {
|
|
51
|
+
audience: RuntimeAudience<Origin>;
|
|
52
|
+
event: RuntimeEvent;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type RuntimeEventSink = (event: RuntimeEvent) => void;
|
|
56
|
+
|
|
57
|
+
export interface RuntimeEventContext<Origin = unknown> {
|
|
58
|
+
emit(delivery: RuntimeDelivery<Origin>): void;
|
|
59
|
+
observe?(delivery: RuntimeDelivery<Origin>): void;
|
|
60
|
+
attachOriginToSession(origin: Origin, sessionId: string): void;
|
|
61
|
+
}
|