@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,334 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const OVERFLOW_PATTERNS = [
|
|
4
|
+
/context window exceeds limit/i,
|
|
5
|
+
/prompt is too long/i,
|
|
6
|
+
/exceeds the context window/i,
|
|
7
|
+
/input token count.*exceeds.*maximum/i,
|
|
8
|
+
/maximum context length is \d+/i,
|
|
9
|
+
/exceeds the limit of \d+/i,
|
|
10
|
+
/exceeded model token limit/i,
|
|
11
|
+
/context[_ ]length[_ ]exceeded/i,
|
|
12
|
+
/request entity too large/i,
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export enum ApiErrorType {
|
|
16
|
+
RateLimit = 'rate_limit',
|
|
17
|
+
ServerError = 'server_error',
|
|
18
|
+
Timeout = 'timeout',
|
|
19
|
+
Network = 'network',
|
|
20
|
+
Quota = 'quota',
|
|
21
|
+
Authentication = 'authentication',
|
|
22
|
+
ContextOverflow = 'context_overflow',
|
|
23
|
+
InvalidRequest = 'invalid_request',
|
|
24
|
+
Unknown = 'unknown',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ClassifiedError {
|
|
28
|
+
type: ApiErrorType;
|
|
29
|
+
retryable: boolean;
|
|
30
|
+
retryAfterMs?: number;
|
|
31
|
+
message: string;
|
|
32
|
+
originalError: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const ERROR_RATE_LIMIT = 'rate_limit';
|
|
36
|
+
export const ERROR_SERVER_ERROR = 'server_error';
|
|
37
|
+
export const ERROR_TIMEOUT = 'timeout';
|
|
38
|
+
export const ERROR_AUTH = 'authentication';
|
|
39
|
+
export const ERROR_INVALID_REQUEST = 'invalid_request';
|
|
40
|
+
export const ERROR_CHAT_FAILED = 'chat_error';
|
|
41
|
+
|
|
42
|
+
interface HeaderGetter {
|
|
43
|
+
get(name: string): string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface AiSdkError {
|
|
47
|
+
status?: number;
|
|
48
|
+
statusCode?: number;
|
|
49
|
+
isRateLimitError?: boolean;
|
|
50
|
+
isRetryableError?: boolean;
|
|
51
|
+
isTimeoutError?: boolean;
|
|
52
|
+
message?: string;
|
|
53
|
+
name?: string;
|
|
54
|
+
code?: string;
|
|
55
|
+
cause?: unknown;
|
|
56
|
+
originalError?: unknown;
|
|
57
|
+
response?: {
|
|
58
|
+
headers?: HeaderGetter;
|
|
59
|
+
};
|
|
60
|
+
responseHeaders?: Record<string, string>;
|
|
61
|
+
responseBody?: string;
|
|
62
|
+
data?: {
|
|
63
|
+
error?: {
|
|
64
|
+
type?: string;
|
|
65
|
+
message?: string;
|
|
66
|
+
resets_in_seconds?: number;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getErrorMessage(error: unknown): string {
|
|
72
|
+
if (error instanceof Error) {
|
|
73
|
+
return error.message;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') {
|
|
77
|
+
return error.message;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return String(error);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getStatusCode(error: AiSdkError): number | undefined {
|
|
84
|
+
return error.status ?? error.statusCode;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getRetryAfterMsFromHeaders(headers: HeaderGetter | Record<string, string> | undefined): number | undefined {
|
|
88
|
+
if (!headers) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let retryAfter: string | null | undefined;
|
|
93
|
+
|
|
94
|
+
if (typeof (headers as HeaderGetter).get === 'function') {
|
|
95
|
+
retryAfter = (headers as HeaderGetter).get('retry-after');
|
|
96
|
+
} else {
|
|
97
|
+
const recordHeaders = headers as Record<string, string>;
|
|
98
|
+
retryAfter = recordHeaders['retry-after'] ?? recordHeaders['Retry-After'];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!retryAfter) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const retryAfterSeconds = Number(retryAfter);
|
|
106
|
+
if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds >= 0) {
|
|
107
|
+
return retryAfterSeconds * 1000;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const retryAt = Date.parse(retryAfter);
|
|
111
|
+
if (Number.isNaN(retryAt)) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return Math.max(0, retryAt - Date.now());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getRetryAfterMsFromError(error: AiSdkError): number | undefined {
|
|
119
|
+
const headerRetryAfter = getRetryAfterMsFromHeaders(error.response?.headers)
|
|
120
|
+
?? getRetryAfterMsFromHeaders(error.responseHeaders);
|
|
121
|
+
if (headerRetryAfter !== undefined) {
|
|
122
|
+
return headerRetryAfter;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const resetSeconds = error.data?.error?.resets_in_seconds;
|
|
126
|
+
if (typeof resetSeconds === 'number' && Number.isFinite(resetSeconds) && resetSeconds >= 0) {
|
|
127
|
+
return resetSeconds * 1000;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getNestedErrors(error: AiSdkError): unknown[] {
|
|
134
|
+
return [error.cause, error.originalError].filter(value => value !== undefined);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isUsageLimitError(error: AiSdkError, message: string): boolean {
|
|
138
|
+
if (error.data?.error?.type === 'usage_limit_reached') {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const responseBody = error.responseBody;
|
|
143
|
+
if (typeof responseBody === 'string' && responseBody.toLowerCase().includes('usage_limit_reached')) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const lower = message.toLowerCase();
|
|
148
|
+
return lower.includes('usage limit has been reached') || lower.includes('usage_limit_reached');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const TRANSIENT_NETWORK_CODES = new Set([
|
|
152
|
+
'ECONNRESET',
|
|
153
|
+
'ECONNREFUSED',
|
|
154
|
+
'EAI_AGAIN',
|
|
155
|
+
'ENETDOWN',
|
|
156
|
+
'ENETUNREACH',
|
|
157
|
+
'EPIPE',
|
|
158
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
159
|
+
'UND_ERR_SOCKET',
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
function isTransientNetworkError(error: AiSdkError, message: string): boolean {
|
|
163
|
+
if (error.code && TRANSIENT_NETWORK_CODES.has(error.code)) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const lower = message.toLowerCase();
|
|
168
|
+
return lower.includes('fetch failed')
|
|
169
|
+
|| lower.includes('network error')
|
|
170
|
+
|| lower.includes('socket hang up')
|
|
171
|
+
|| lower.includes('connection reset')
|
|
172
|
+
|| lower.includes('connection closed');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function classifyApiError(error: unknown): ClassifiedError {
|
|
176
|
+
const message = getErrorMessage(error);
|
|
177
|
+
|
|
178
|
+
if (error && typeof error === 'object') {
|
|
179
|
+
const aiError = error as AiSdkError;
|
|
180
|
+
|
|
181
|
+
if (aiError.isTimeoutError || aiError.name === 'TimeoutError' || aiError.code === 'ETIMEDOUT') {
|
|
182
|
+
return {
|
|
183
|
+
type: ApiErrorType.Timeout,
|
|
184
|
+
retryable: true,
|
|
185
|
+
message,
|
|
186
|
+
originalError: error,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const status = getStatusCode(aiError);
|
|
191
|
+
|
|
192
|
+
if (isTransientNetworkError(aiError, message)) {
|
|
193
|
+
return {
|
|
194
|
+
type: ApiErrorType.Network,
|
|
195
|
+
retryable: true,
|
|
196
|
+
message,
|
|
197
|
+
originalError: error,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (isUsageLimitError(aiError, message)) {
|
|
202
|
+
return {
|
|
203
|
+
type: ApiErrorType.Quota,
|
|
204
|
+
retryable: false,
|
|
205
|
+
message,
|
|
206
|
+
originalError: error,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (aiError.isRateLimitError || status === 429) {
|
|
211
|
+
const retryAfterMs = getRetryAfterMsFromError(aiError) ?? 60000;
|
|
212
|
+
return {
|
|
213
|
+
type: ApiErrorType.RateLimit,
|
|
214
|
+
retryable: true,
|
|
215
|
+
retryAfterMs,
|
|
216
|
+
message,
|
|
217
|
+
originalError: error,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (status === 401 || status === 403) {
|
|
222
|
+
return {
|
|
223
|
+
type: ApiErrorType.Authentication,
|
|
224
|
+
retryable: false,
|
|
225
|
+
message,
|
|
226
|
+
originalError: error,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (status === 400 || status === 413) {
|
|
231
|
+
const errorMessage = message;
|
|
232
|
+
for (const pattern of OVERFLOW_PATTERNS) {
|
|
233
|
+
if (pattern.test(errorMessage)) {
|
|
234
|
+
return {
|
|
235
|
+
type: ApiErrorType.ContextOverflow,
|
|
236
|
+
retryable: false,
|
|
237
|
+
message: errorMessage,
|
|
238
|
+
originalError: error,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (status === 400) {
|
|
245
|
+
return {
|
|
246
|
+
type: ApiErrorType.InvalidRequest,
|
|
247
|
+
retryable: false,
|
|
248
|
+
message,
|
|
249
|
+
originalError: error,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (status !== undefined && status >= 500 && status < 600) {
|
|
254
|
+
return {
|
|
255
|
+
type: ApiErrorType.ServerError,
|
|
256
|
+
retryable: true,
|
|
257
|
+
message,
|
|
258
|
+
originalError: error,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
for (const nestedError of getNestedErrors(aiError)) {
|
|
263
|
+
const nestedClassified = classifyApiError(nestedError);
|
|
264
|
+
if (nestedClassified.type !== ApiErrorType.Unknown) {
|
|
265
|
+
return {
|
|
266
|
+
...nestedClassified,
|
|
267
|
+
message: nestedClassified.message || message,
|
|
268
|
+
originalError: error,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (aiError.isRetryableError) {
|
|
274
|
+
return {
|
|
275
|
+
type: ApiErrorType.Unknown,
|
|
276
|
+
retryable: true,
|
|
277
|
+
message,
|
|
278
|
+
originalError: error,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
type: ApiErrorType.Unknown,
|
|
285
|
+
retryable: false,
|
|
286
|
+
message,
|
|
287
|
+
originalError: error,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface RetryOptions {
|
|
292
|
+
maxRetries?: number;
|
|
293
|
+
baseDelayMs?: number;
|
|
294
|
+
maxDelayMs?: number;
|
|
295
|
+
onRetry?: (attempt: number, classifiedError: ClassifiedError) => void;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function withRetry<T>(
|
|
299
|
+
fn: () => Promise<T>,
|
|
300
|
+
options: RetryOptions = {},
|
|
301
|
+
): Promise<T> {
|
|
302
|
+
const {
|
|
303
|
+
maxRetries = 3,
|
|
304
|
+
baseDelayMs = 1000,
|
|
305
|
+
maxDelayMs = 30000,
|
|
306
|
+
onRetry,
|
|
307
|
+
} = options;
|
|
308
|
+
|
|
309
|
+
let lastError: ClassifiedError | null = null;
|
|
310
|
+
|
|
311
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
312
|
+
try {
|
|
313
|
+
return await fn();
|
|
314
|
+
} catch (err: unknown) {
|
|
315
|
+
const classifiedError = classifyApiError(err);
|
|
316
|
+
lastError = classifiedError;
|
|
317
|
+
|
|
318
|
+
if (!classifiedError.retryable || attempt >= maxRetries) {
|
|
319
|
+
throw classifiedError;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const exponentialDelay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
|
|
323
|
+
const delay = classifiedError.retryAfterMs
|
|
324
|
+
? Math.max(exponentialDelay, classifiedError.retryAfterMs)
|
|
325
|
+
: exponentialDelay;
|
|
326
|
+
|
|
327
|
+
onRetry?.(attempt + 1, classifiedError);
|
|
328
|
+
|
|
329
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
throw lastError;
|
|
334
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AnyVisualization } from '@capekai/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Recursively strip _visualization fields from any object.
|
|
5
|
+
* Used before passing tool output to LLM context to avoid token bloat.
|
|
6
|
+
*
|
|
7
|
+
* @param output - The object to strip visualization from
|
|
8
|
+
* @returns A new object with all _visualization fields removed
|
|
9
|
+
*/
|
|
10
|
+
export function stripVisualization<T>(output: T): T {
|
|
11
|
+
if (output === null || output === undefined) {
|
|
12
|
+
return output;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (Array.isArray(output)) {
|
|
16
|
+
return output.map(item => stripVisualization(item)) as T;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof output === 'object') {
|
|
20
|
+
const result: Record<string, unknown> = {};
|
|
21
|
+
|
|
22
|
+
for (const [key, value] of Object.entries(output as Record<string, unknown>)) {
|
|
23
|
+
// Skip _visualization field
|
|
24
|
+
if (key === '_visualization') {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
result[key] = stripVisualization(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return result as T;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return output;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract visualization metadata from tool output.
|
|
39
|
+
*
|
|
40
|
+
* @param output - The tool output object
|
|
41
|
+
* @returns The visualization metadata if present, undefined otherwise
|
|
42
|
+
*/
|
|
43
|
+
export function extractVisualization(
|
|
44
|
+
output: unknown,
|
|
45
|
+
): AnyVisualization | undefined {
|
|
46
|
+
if (output && typeof output === 'object' && '_visualization' in output) {
|
|
47
|
+
return (output as Record<string, unknown>)._visualization as AnyVisualization;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { listSubagentPreconfigs } from '../context';
|
|
2
|
+
import type { WorkflowSubtask, Preconfig } from '@capekai/types';
|
|
3
|
+
import type { BroadcastFn, BroadcastSessionFn } from '../runtime/host';
|
|
4
|
+
import {
|
|
5
|
+
runOrchestratorSession,
|
|
6
|
+
type OrchestratorSessionOptions,
|
|
7
|
+
type OrchestratorSessionResult,
|
|
8
|
+
} from './orchestrator-session';
|
|
9
|
+
|
|
10
|
+
/** Workflow domain: task decomposition. Moved byte-for-byte from
|
|
11
|
+
* `core/workflow-decomposer.ts`; the unscoped export keeps the pre-C5
|
|
12
|
+
* module-accessor behavior, and the WithDeps variant runs against the
|
|
13
|
+
* injected subagent listing and orchestrator contract captured by the
|
|
14
|
+
* domain plugin at composition. */
|
|
15
|
+
|
|
16
|
+
export const MAX_SUBTASKS = 50;
|
|
17
|
+
|
|
18
|
+
export interface DecomposeTaskOptions {
|
|
19
|
+
prompt: string;
|
|
20
|
+
parentSessionId: string;
|
|
21
|
+
abortSignal?: AbortSignal;
|
|
22
|
+
allowedSubagentIds?: string[];
|
|
23
|
+
broadcast?: BroadcastFn;
|
|
24
|
+
broadcastSessionCreated?: BroadcastSessionFn;
|
|
25
|
+
broadcastSessionUpdated?: BroadcastSessionFn;
|
|
26
|
+
runOrchestrator?: typeof runOrchestratorSession;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DecomposeTaskDeps {
|
|
30
|
+
listSubagents(): Promise<Preconfig[]>;
|
|
31
|
+
orchestrator: { run(options: OrchestratorSessionOptions): Promise<OrchestratorSessionResult> };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Unscoped decomposition: module subagent listing plus the module
|
|
35
|
+
* orchestrator implementation, exactly like the pre-C5 path. */
|
|
36
|
+
export async function decomposeTask(options: DecomposeTaskOptions): Promise<WorkflowSubtask[]> {
|
|
37
|
+
return decomposeTaskWithDeps(options, {
|
|
38
|
+
listSubagents: listSubagentPreconfigs,
|
|
39
|
+
orchestrator: { run: runOrchestratorSession },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Composed decomposition over the injected deps. */
|
|
44
|
+
export async function decomposeTaskWithDeps(
|
|
45
|
+
options: DecomposeTaskOptions,
|
|
46
|
+
deps: DecomposeTaskDeps,
|
|
47
|
+
): Promise<WorkflowSubtask[]> {
|
|
48
|
+
console.log('[workflow:decompose] Starting decomposition', {
|
|
49
|
+
parentSessionId: options.parentSessionId,
|
|
50
|
+
promptPreview: options.prompt.slice(0, 100),
|
|
51
|
+
allowedSubagentIds: options.allowedSubagentIds,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let preconfigs = await deps.listSubagents();
|
|
55
|
+
|
|
56
|
+
// Filter to only allowed subagent types if specified
|
|
57
|
+
if (options.allowedSubagentIds && options.allowedSubagentIds.length > 0) {
|
|
58
|
+
const allowedSet = new Set(options.allowedSubagentIds);
|
|
59
|
+
preconfigs = preconfigs.filter(p => allowedSet.has(p.id));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (preconfigs.length === 0) {
|
|
63
|
+
throw new Error('No subagent types available for this session');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log('[workflow:decompose] Available subagent preconfigs:', preconfigs.map(p => p.id));
|
|
67
|
+
const preconfigList = preconfigs
|
|
68
|
+
.map((p) => `- ${p.id}: ${p.description ?? 'Specialized agent'}`)
|
|
69
|
+
.join('\n');
|
|
70
|
+
|
|
71
|
+
const system = [
|
|
72
|
+
'You are a task decomposer. Given a high-level task, break it into independent',
|
|
73
|
+
'parallel subtasks that can be executed by specialist agents.',
|
|
74
|
+
'',
|
|
75
|
+
`Available agent types:\n${preconfigList}`,
|
|
76
|
+
'',
|
|
77
|
+
'Rules:',
|
|
78
|
+
'- Each subtask must be self-contained (the agent has no context beyond the prompt).',
|
|
79
|
+
'- Aim for 2-10 subtasks. Too few wastes parallelism; too many creates overhead.',
|
|
80
|
+
'- Assign the most appropriate agent type (preconfigId) for each subtask from the available list above.',
|
|
81
|
+
'- The preconfigId MUST be one of the listed agent types. Do NOT invent types.',
|
|
82
|
+
'- Subtasks should NOT depend on each other\'s results (they run in parallel).',
|
|
83
|
+
'',
|
|
84
|
+
'You must respond with ONLY valid JSON (no markdown fences, no extra text) in this exact format:',
|
|
85
|
+
'{"subtasks":[{"prompt":"...","preconfigId":"..."}]}',
|
|
86
|
+
].join('\n');
|
|
87
|
+
|
|
88
|
+
const result = await (options.runOrchestrator ?? deps.orchestrator.run)({
|
|
89
|
+
parentSessionId: options.parentSessionId,
|
|
90
|
+
title: `Decompose: ${options.prompt.slice(0, 50)}`,
|
|
91
|
+
agentName: 'decomposer',
|
|
92
|
+
systemPrompt: system,
|
|
93
|
+
userPrompt: options.prompt,
|
|
94
|
+
maxTokens: 4096,
|
|
95
|
+
abortSignal: options.abortSignal,
|
|
96
|
+
broadcast: options.broadcast,
|
|
97
|
+
broadcastSessionCreated: options.broadcastSessionCreated,
|
|
98
|
+
broadcastSessionUpdated: options.broadcastSessionUpdated,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const parsed = result.json;
|
|
102
|
+
if (!parsed?.subtasks || !Array.isArray(parsed.subtasks)) {
|
|
103
|
+
console.error('[workflow:decompose] Failed to parse subtasks from response', { text: result.text });
|
|
104
|
+
throw new Error('Decomposer failed to produce valid subtasks');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let subtasks = parsed.subtasks as WorkflowSubtask[];
|
|
108
|
+
|
|
109
|
+
if (subtasks.length === 0) {
|
|
110
|
+
throw new Error('Decomposition produced no subtasks');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Sanitize: ensure preconfigId is one of the allowed types, fall back to first available
|
|
114
|
+
const validIds = new Set(preconfigs.map(p => p.id));
|
|
115
|
+
const fallbackId = preconfigs[0]?.id;
|
|
116
|
+
subtasks = subtasks.map(s => ({
|
|
117
|
+
...s,
|
|
118
|
+
preconfigId: s.preconfigId && validIds.has(s.preconfigId)
|
|
119
|
+
? s.preconfigId
|
|
120
|
+
: fallbackId,
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
console.log('[workflow:decompose] Decomposition complete', {
|
|
124
|
+
subtaskCount: subtasks.length,
|
|
125
|
+
subtasks: subtasks.map((s, i) => ({
|
|
126
|
+
i,
|
|
127
|
+
preconfigId: s.preconfigId,
|
|
128
|
+
promptPreview: s.prompt.slice(0, 80),
|
|
129
|
+
})),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Safety cap
|
|
133
|
+
if (subtasks.length > MAX_SUBTASKS) {
|
|
134
|
+
console.warn(`[workflow:decompose] Decomposer returned ${subtasks.length} subtasks, capping to ${MAX_SUBTASKS}`);
|
|
135
|
+
subtasks = subtasks.slice(0, MAX_SUBTASKS);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return subtasks;
|
|
139
|
+
}
|