@capekai/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +12 -0
  2. package/package.json +105 -0
  3. package/src/adapters/ai-sdk.ts +84 -0
  4. package/src/compaction/contracts.ts +82 -0
  5. package/src/compaction/executor.ts +161 -0
  6. package/src/compaction/policy.ts +318 -0
  7. package/src/compaction/recovery.ts +139 -0
  8. package/src/compaction/task.ts +540 -0
  9. package/src/configuration/contracts.ts +58 -0
  10. package/src/configuration/defaults.ts +27 -0
  11. package/src/configuration/runtime.ts +42 -0
  12. package/src/configuration/single-model.ts +75 -0
  13. package/src/context/assembler.ts +112 -0
  14. package/src/context/index.ts +2 -0
  15. package/src/context/sources.ts +119 -0
  16. package/src/context/workspace.ts +63 -0
  17. package/src/core/agent.ts +401 -0
  18. package/src/core/build-tools.ts +139 -0
  19. package/src/core/chat-handler.ts +858 -0
  20. package/src/core/error-handling.ts +18 -0
  21. package/src/core/fork.ts +103 -0
  22. package/src/core/interrupt.ts +192 -0
  23. package/src/core/message-utils.ts +261 -0
  24. package/src/core/model-utils.ts +149 -0
  25. package/src/core/part-utils.ts +88 -0
  26. package/src/core/provider-utils.ts +67 -0
  27. package/src/core/revert.ts +46 -0
  28. package/src/core/step-handlers.ts +157 -0
  29. package/src/core/stream/finalization.ts +65 -0
  30. package/src/core/stream/stream-config.ts +82 -0
  31. package/src/core/stream-handlers.ts +242 -0
  32. package/src/core/structured-output.ts +68 -0
  33. package/src/core/tool-builders/agent-tools.ts +71 -0
  34. package/src/core/tool-builders/external-tools.ts +179 -0
  35. package/src/core/tool-builders/types.ts +16 -0
  36. package/src/core/tool-builders/workspace-tools.ts +293 -0
  37. package/src/core/tool-capabilities.ts +65 -0
  38. package/src/goals/evaluator.ts +171 -0
  39. package/src/goals/index.ts +3 -0
  40. package/src/goals/loop.ts +167 -0
  41. package/src/goals/service.ts +39 -0
  42. package/src/index.ts +10 -0
  43. package/src/internal/ask-authority.ts +29 -0
  44. package/src/internal/composition.ts +44 -0
  45. package/src/internal/configuration.ts +22 -0
  46. package/src/internal/execution.ts +108 -0
  47. package/src/internal/hosts.ts +64 -0
  48. package/src/internal/plugins.ts +71 -0
  49. package/src/internal/providers.ts +32 -0
  50. package/src/internal/sandbox.ts +19 -0
  51. package/src/internal/tools.ts +48 -0
  52. package/src/internal/workspace.ts +25 -0
  53. package/src/kernel/diagnostics.ts +249 -0
  54. package/src/kernel/errors.ts +120 -0
  55. package/src/kernel/events.ts +82 -0
  56. package/src/kernel/index.ts +72 -0
  57. package/src/kernel/kernel.ts +62 -0
  58. package/src/kernel/lifecycle.ts +72 -0
  59. package/src/kernel/plugin.ts +218 -0
  60. package/src/kernel/registry.ts +493 -0
  61. package/src/kernel/scope.ts +776 -0
  62. package/src/kernel/service-key.ts +19 -0
  63. package/src/kernel/types.ts +317 -0
  64. package/src/memory/index.ts +2 -0
  65. package/src/memory/memory-tool.ts +75 -0
  66. package/src/memory/registry.ts +172 -0
  67. package/src/permission/ask-user-api.ts +70 -0
  68. package/src/permission/contracts.ts +135 -0
  69. package/src/permission/permission-request-manager.ts +58 -0
  70. package/src/permission/policy.ts +277 -0
  71. package/src/permission/runtime.ts +612 -0
  72. package/src/plugins/compaction-policy.ts +46 -0
  73. package/src/plugins/compose.ts +171 -0
  74. package/src/plugins/context-sections.ts +246 -0
  75. package/src/plugins/default-agent-driver.ts +14 -0
  76. package/src/plugins/facade-plugins.ts +129 -0
  77. package/src/plugins/goal-domain.ts +82 -0
  78. package/src/plugins/legacy-system-message.ts +152 -0
  79. package/src/plugins/loaded-tools.ts +23 -0
  80. package/src/plugins/memory-domain.ts +264 -0
  81. package/src/plugins/orchestrator-session.ts +29 -0
  82. package/src/plugins/permission-policy.ts +49 -0
  83. package/src/plugins/retry-policy.ts +28 -0
  84. package/src/plugins/scheduler-domain.ts +192 -0
  85. package/src/plugins/service-keys.ts +294 -0
  86. package/src/plugins/session-search-domain.ts +238 -0
  87. package/src/plugins/skills-domain.ts +272 -0
  88. package/src/plugins/subagent-domain.ts +287 -0
  89. package/src/plugins/tool-catalog.ts +78 -0
  90. package/src/plugins/tool-output-policy.ts +52 -0
  91. package/src/plugins/value-plugins.ts +150 -0
  92. package/src/plugins/workflow-domain.ts +198 -0
  93. package/src/plugins/workspace-policy.ts +37 -0
  94. package/src/providers/registry.ts +63 -0
  95. package/src/providers/types.ts +44 -0
  96. package/src/retry/policy.ts +282 -0
  97. package/src/retry/stream-chat.ts +312 -0
  98. package/src/runtime/agent-runtime.ts +83 -0
  99. package/src/runtime/default-agent-driver.ts +23 -0
  100. package/src/runtime/domain-tool-source.ts +156 -0
  101. package/src/runtime/events.ts +61 -0
  102. package/src/runtime/host-dependencies.ts +71 -0
  103. package/src/runtime/host-guidance.ts +22 -0
  104. package/src/runtime/host-layout.ts +23 -0
  105. package/src/runtime/host.ts +129 -0
  106. package/src/runtime/standalone-host.ts +118 -0
  107. package/src/sandbox/controller.ts +204 -0
  108. package/src/sandbox/model.ts +305 -0
  109. package/src/sandbox/provider.ts +53 -0
  110. package/src/sandbox/types.ts +110 -0
  111. package/src/scheduler/host.ts +22 -0
  112. package/src/scheduler/scheduler-tool.ts +172 -0
  113. package/src/session-search/host.ts +56 -0
  114. package/src/session-search/index.ts +23 -0
  115. package/src/session-search/session-search-tool.ts +151 -0
  116. package/src/skills/index.ts +3 -0
  117. package/src/skills/registry.ts +63 -0
  118. package/src/skills/skill-manage-tool.ts +205 -0
  119. package/src/skills/skill-tool.ts +42 -0
  120. package/src/storage/contracts.ts +159 -0
  121. package/src/storage/memory.ts +321 -0
  122. package/src/storage/options.ts +75 -0
  123. package/src/storage/runtime.ts +115 -0
  124. package/src/storage/sqlite-tool-output-artifacts.ts +106 -0
  125. package/src/storage/sqlite.ts +321 -0
  126. package/src/storage/tool-output-artifacts.ts +75 -0
  127. package/src/storage.ts +31 -0
  128. package/src/subagent/child-session.ts +282 -0
  129. package/src/subagent/guidance.ts +8 -0
  130. package/src/subagent/policy.ts +198 -0
  131. package/src/subagent/task-tool.ts +584 -0
  132. package/src/tool-output/contracts.ts +111 -0
  133. package/src/tool-output/policy.ts +410 -0
  134. package/src/tool.ts +1 -0
  135. package/src/tools/executor.ts +258 -0
  136. package/src/tools/install-manifest.ts +40 -0
  137. package/src/tools/llm-api.ts +77 -0
  138. package/src/tools/registry.ts +206 -0
  139. package/src/tools/tool-artifact.ts +182 -0
  140. package/src/tools/tool-source.ts +53 -0
  141. package/src/utils/errors.ts +334 -0
  142. package/src/utils/strip-visualization.ts +50 -0
  143. package/src/workflow/decomposer.ts +139 -0
  144. package/src/workflow/execution.ts +523 -0
  145. package/src/workflow/orchestrator-session.ts +161 -0
  146. package/src/workflow/synthesizer.ts +130 -0
  147. package/src/workspace/contracts.ts +135 -0
  148. package/src/workspace/policy.ts +327 -0
@@ -0,0 +1,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
+ }