@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,282 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import type { MessageWithParts, Part, TextPart, Preconfig, UserMessage, ResponseFormat, StructuredOutputData } from '@capekai/types';
|
|
3
|
+
import {
|
|
4
|
+
emitRuntimeEvent,
|
|
5
|
+
emitTerminal,
|
|
6
|
+
emitToAskTargets,
|
|
7
|
+
emitToController,
|
|
8
|
+
} from '../runtime/host-dependencies';
|
|
9
|
+
import { getLLMSubagentMaxSteps } from '../configuration/runtime';
|
|
10
|
+
import {
|
|
11
|
+
buildEffectiveContextHistory,
|
|
12
|
+
createMessage,
|
|
13
|
+
createPart,
|
|
14
|
+
getSession,
|
|
15
|
+
getWorkspace,
|
|
16
|
+
updateMessage,
|
|
17
|
+
updateSession,
|
|
18
|
+
} from '../storage/runtime';
|
|
19
|
+
import type { AskBroadcastFn, BroadcastFn } from '../runtime/host';
|
|
20
|
+
import { classifyApiError } from '../utils/errors';
|
|
21
|
+
import { streamChatWithRetry } from '../retry/stream-chat';
|
|
22
|
+
|
|
23
|
+
export async function executeChildSession(options: {
|
|
24
|
+
parentSessionId: string;
|
|
25
|
+
childSessionId: string;
|
|
26
|
+
preconfig: Preconfig;
|
|
27
|
+
prompt: string;
|
|
28
|
+
workspacePath?: string;
|
|
29
|
+
workspaceId?: string;
|
|
30
|
+
resumeFromHistory?: boolean;
|
|
31
|
+
modelId?: string | null;
|
|
32
|
+
providerId?: string | null;
|
|
33
|
+
variant?: string | null;
|
|
34
|
+
broadcast?: BroadcastFn;
|
|
35
|
+
broadcastToSession?: BroadcastFn;
|
|
36
|
+
/** Optional response format for structured subagent output */
|
|
37
|
+
responseFormat?: ResponseFormat;
|
|
38
|
+
abortSignal?: AbortSignal;
|
|
39
|
+
streamChat?: typeof streamChatWithRetry;
|
|
40
|
+
}): Promise<{
|
|
41
|
+
parts: Part[];
|
|
42
|
+
error?: string;
|
|
43
|
+
structuredOutput?: StructuredOutputData;
|
|
44
|
+
}> {
|
|
45
|
+
const {
|
|
46
|
+
childSessionId,
|
|
47
|
+
preconfig,
|
|
48
|
+
prompt,
|
|
49
|
+
workspacePath,
|
|
50
|
+
workspaceId,
|
|
51
|
+
resumeFromHistory,
|
|
52
|
+
modelId,
|
|
53
|
+
providerId,
|
|
54
|
+
variant,
|
|
55
|
+
broadcast: broadcastFn = emitRuntimeEvent,
|
|
56
|
+
broadcastToSession: broadcastToSessionFn = broadcastFn,
|
|
57
|
+
responseFormat,
|
|
58
|
+
abortSignal,
|
|
59
|
+
streamChat = streamChatWithRetry,
|
|
60
|
+
} = options;
|
|
61
|
+
|
|
62
|
+
// Resolve additionalPaths from workspace
|
|
63
|
+
const workspace = workspaceId ? await getWorkspace(workspaceId) : null;
|
|
64
|
+
const additionalPaths = workspace?.additionalPaths;
|
|
65
|
+
|
|
66
|
+
let messages: MessageWithParts[];
|
|
67
|
+
|
|
68
|
+
if (resumeFromHistory) {
|
|
69
|
+
// Load full history with parts (same function handleChat uses)
|
|
70
|
+
const { messages: historyMessages } = await buildEffectiveContextHistory(childSessionId);
|
|
71
|
+
|
|
72
|
+
// Create the new user message
|
|
73
|
+
const newMsgId = randomUUID();
|
|
74
|
+
const newMessage: UserMessage = {
|
|
75
|
+
id: newMsgId,
|
|
76
|
+
sessionId: childSessionId,
|
|
77
|
+
role: 'user',
|
|
78
|
+
createdAt: Date.now(),
|
|
79
|
+
};
|
|
80
|
+
const textPart: TextPart = {
|
|
81
|
+
id: randomUUID(),
|
|
82
|
+
messageId: newMsgId,
|
|
83
|
+
createdAt: Date.now(),
|
|
84
|
+
type: 'text',
|
|
85
|
+
text: prompt,
|
|
86
|
+
};
|
|
87
|
+
messages = [...historyMessages, { message: newMessage, parts: [textPart] }];
|
|
88
|
+
await createMessage(newMessage);
|
|
89
|
+
await createPart(textPart, childSessionId);
|
|
90
|
+
} else {
|
|
91
|
+
const msgId = randomUUID();
|
|
92
|
+
const userMessage: UserMessage = {
|
|
93
|
+
id: msgId,
|
|
94
|
+
sessionId: childSessionId,
|
|
95
|
+
role: 'user',
|
|
96
|
+
createdAt: Date.now(),
|
|
97
|
+
};
|
|
98
|
+
const textPart: TextPart = {
|
|
99
|
+
id: randomUUID(),
|
|
100
|
+
messageId: msgId,
|
|
101
|
+
createdAt: Date.now(),
|
|
102
|
+
type: 'text',
|
|
103
|
+
text: prompt,
|
|
104
|
+
};
|
|
105
|
+
messages = [{ message: userMessage, parts: [textPart] }];
|
|
106
|
+
await createMessage(userMessage);
|
|
107
|
+
await createPart(textPart, childSessionId);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const finalParts: Part[] = [];
|
|
111
|
+
let error: string | undefined;
|
|
112
|
+
let structuredOutput: StructuredOutputData | undefined;
|
|
113
|
+
const retryAbortController = new AbortController();
|
|
114
|
+
const abortHandler = () => retryAbortController.abort(abortSignal?.reason);
|
|
115
|
+
if (abortSignal?.aborted) {
|
|
116
|
+
abortHandler();
|
|
117
|
+
} else {
|
|
118
|
+
abortSignal?.addEventListener('abort', abortHandler, { once: true });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function findRootSessionId(sessionId: string): Promise<string> {
|
|
122
|
+
let current = sessionId;
|
|
123
|
+
let session = await getSession(current);
|
|
124
|
+
while (session?.parentId) {
|
|
125
|
+
current = session.parentId;
|
|
126
|
+
session = await getSession(current);
|
|
127
|
+
}
|
|
128
|
+
return current;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const rootSessionId = await findRootSessionId(childSessionId);
|
|
132
|
+
|
|
133
|
+
const askBroadcastFn: AskBroadcastFn = (message) => {
|
|
134
|
+
// Route permission asks to the root session so the user always sees them
|
|
135
|
+
if (message.type === 'ask.request') {
|
|
136
|
+
const rewritten = {
|
|
137
|
+
...message,
|
|
138
|
+
sessionId: rootSessionId,
|
|
139
|
+
ask: {
|
|
140
|
+
...message.ask,
|
|
141
|
+
_originSessionId: message.sessionId,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
const authority = message.authority ?? { visibilityScope: 'controller_only' as const, resolutionMode: 'controller_only' as const };
|
|
145
|
+
emitToAskTargets(rootSessionId, authority, {
|
|
146
|
+
kind: 'ask',
|
|
147
|
+
action: 'requested',
|
|
148
|
+
sessionId: rewritten.sessionId,
|
|
149
|
+
toolCallId: rewritten.toolCallId,
|
|
150
|
+
toolName: rewritten.toolName,
|
|
151
|
+
ask: rewritten.ask,
|
|
152
|
+
requestId: rewritten.requestId,
|
|
153
|
+
authority: rewritten.authority,
|
|
154
|
+
});
|
|
155
|
+
} else if (message.type === 'ask.timeout') {
|
|
156
|
+
const rewritten = {
|
|
157
|
+
...message,
|
|
158
|
+
sessionId: rootSessionId,
|
|
159
|
+
};
|
|
160
|
+
emitToController(rootSessionId, {
|
|
161
|
+
kind: 'ask',
|
|
162
|
+
action: 'timed_out',
|
|
163
|
+
sessionId: rewritten.sessionId,
|
|
164
|
+
toolCallId: rewritten.toolCallId,
|
|
165
|
+
requestId: rewritten.requestId,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
for await (const event of streamChat({
|
|
172
|
+
sessionId: childSessionId,
|
|
173
|
+
preconfig,
|
|
174
|
+
messages,
|
|
175
|
+
workspacePath,
|
|
176
|
+
workspaceId,
|
|
177
|
+
additionalPaths,
|
|
178
|
+
modelId: modelId ?? undefined,
|
|
179
|
+
providerId: providerId ?? undefined,
|
|
180
|
+
variant: variant ?? undefined,
|
|
181
|
+
maxSteps: getLLMSubagentMaxSteps(),
|
|
182
|
+
broadcastFn: askBroadcastFn,
|
|
183
|
+
retryAbortController,
|
|
184
|
+
...(responseFormat ? { responseFormat } : {}),
|
|
185
|
+
})) {
|
|
186
|
+
if (event.type === 'message.created') {
|
|
187
|
+
broadcastToSessionFn({ kind: 'message', action: 'created', message: event.message });
|
|
188
|
+
} else if (event.type === 'part.created') {
|
|
189
|
+
finalParts.push(event.part);
|
|
190
|
+
broadcastToSessionFn({ kind: 'part', action: 'created', sessionId: event.sessionId, part: event.part });
|
|
191
|
+
} else if (event.type === 'part.append' && event.field === 'text') {
|
|
192
|
+
const part = finalParts.find(p => p.id === event.partId);
|
|
193
|
+
if (part && part.type === 'text') {
|
|
194
|
+
part.text = (part.text || '') + event.delta;
|
|
195
|
+
}
|
|
196
|
+
broadcastToSessionFn({
|
|
197
|
+
kind: 'part',
|
|
198
|
+
action: 'append',
|
|
199
|
+
sessionId: event.sessionId,
|
|
200
|
+
partId: event.partId,
|
|
201
|
+
field: event.field,
|
|
202
|
+
delta: event.delta,
|
|
203
|
+
});
|
|
204
|
+
} else if (event.type === 'part.updated') {
|
|
205
|
+
broadcastToSessionFn({ kind: 'part', action: 'updated', sessionId: event.sessionId, part: event.part });
|
|
206
|
+
} else if (event.type === 'message.updated' && event.message.role === 'assistant') {
|
|
207
|
+
// Capture structured output if present on the final message
|
|
208
|
+
if ('structuredOutput' in event.message && event.message.structuredOutput) {
|
|
209
|
+
structuredOutput = event.message.structuredOutput as StructuredOutputData;
|
|
210
|
+
}
|
|
211
|
+
await updateMessage(event.message.id, event.message, { syncFts: false });
|
|
212
|
+
if (event.message.mode !== 'retry_failed') {
|
|
213
|
+
emitTerminal(event.message, childSessionId);
|
|
214
|
+
}
|
|
215
|
+
broadcastToSessionFn({ kind: 'message', action: 'updated', message: event.message });
|
|
216
|
+
} else if (event.type === 'usage') {
|
|
217
|
+
const currentSession = await getSession(childSessionId);
|
|
218
|
+
if (currentSession) {
|
|
219
|
+
await updateSession(childSessionId, {
|
|
220
|
+
promptTokens: event.usage.promptTokens,
|
|
221
|
+
completionTokens: event.usage.completionTokens,
|
|
222
|
+
totalTokens: event.usage.totalTokens,
|
|
223
|
+
cacheReadTokens: event.usage.cacheReadTokens,
|
|
224
|
+
cacheWriteTokens: event.usage.cacheWriteTokens,
|
|
225
|
+
noCacheTokens: event.usage.noCacheTokens,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
broadcastToSessionFn({
|
|
229
|
+
kind: 'usage',
|
|
230
|
+
sessionId: childSessionId,
|
|
231
|
+
usage: event.usage,
|
|
232
|
+
model: event.model,
|
|
233
|
+
variant: event.variant ?? undefined,
|
|
234
|
+
});
|
|
235
|
+
} else if (event.type === 'chat.retry') {
|
|
236
|
+
broadcastToSessionFn({
|
|
237
|
+
kind: 'retry',
|
|
238
|
+
sessionId: event.sessionId,
|
|
239
|
+
status: event.status,
|
|
240
|
+
attempt: event.retryNumber,
|
|
241
|
+
maxAttempts: event.maxRetries,
|
|
242
|
+
errorType: event.errorType,
|
|
243
|
+
message: event.message,
|
|
244
|
+
delayMs: event.delayMs,
|
|
245
|
+
retryAt: event.retryAt,
|
|
246
|
+
});
|
|
247
|
+
} else if (event.type === 'error.rate_limit') {
|
|
248
|
+
error ??= event.message;
|
|
249
|
+
console.warn(`[Child Session ${childSessionId}] Rate limited after retries: ${event.message}`);
|
|
250
|
+
} else if (event.type === 'error.server') {
|
|
251
|
+
error ??= event.message;
|
|
252
|
+
console.warn(`[Child Session ${childSessionId}] Server error: ${event.message}`);
|
|
253
|
+
} else if (event.type === 'error.timeout') {
|
|
254
|
+
error ??= event.message;
|
|
255
|
+
console.warn(`[Child Session ${childSessionId}] Timeout: ${event.message}`);
|
|
256
|
+
} else if (event.type === 'error' || event.type === 'error.auth' || event.type === 'error.invalid_request') {
|
|
257
|
+
const errMsg = event.message;
|
|
258
|
+
if (!error) {
|
|
259
|
+
error = errMsg;
|
|
260
|
+
}
|
|
261
|
+
console.error(`[Child Session ${childSessionId}] ${event.type}: ${errMsg}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
const classified = classifyApiError(err);
|
|
266
|
+
error = classified.message;
|
|
267
|
+
|
|
268
|
+
if (classified.retryable) {
|
|
269
|
+
console.error(`[Child Session ${childSessionId}] Retryable error (${classified.type}): ${classified.message}`);
|
|
270
|
+
} else {
|
|
271
|
+
console.error(`[Child Session ${childSessionId}] Non-retryable error (${classified.type}): ${classified.message}`);
|
|
272
|
+
}
|
|
273
|
+
} finally {
|
|
274
|
+
abortSignal?.removeEventListener('abort', abortHandler);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
parts: finalParts,
|
|
279
|
+
error,
|
|
280
|
+
...(structuredOutput ? { structuredOutput } : {}),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Subagent domain: self-delegation context guidance. Moved byte-for-byte
|
|
2
|
+
* from the pre-C5 fixed system-message builder; the legacy builder imports
|
|
3
|
+
* it here so both the fixed and the composed paths emit identical bytes. */
|
|
4
|
+
export function selfDelegationGuidance(preconfigId: string): string {
|
|
5
|
+
return `SELF-DELEGATION:
|
|
6
|
+
- You may use the task tool with subagent_type "${preconfigId}" to delegate work to a fresh instance of yourself.
|
|
7
|
+
- This permission applies only to the immediate child. Reusing "${preconfigId}" later in the same ancestry chain is blocked.`;
|
|
8
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { Preconfig, Session } from '@capekai/types';
|
|
2
|
+
import { listPreconfigs as runtimeListPreconfigs } from '../context';
|
|
3
|
+
import { getSession as runtimeGetSession } from '../storage/runtime';
|
|
4
|
+
|
|
5
|
+
export type SubagentPolicyReason =
|
|
6
|
+
| 'allowed'
|
|
7
|
+
| 'maximum_depth'
|
|
8
|
+
| 'target_not_allowed'
|
|
9
|
+
| 'self_disabled'
|
|
10
|
+
| 'repeated_ancestor';
|
|
11
|
+
|
|
12
|
+
export interface SubagentPolicyResult {
|
|
13
|
+
allowed: boolean;
|
|
14
|
+
reason: SubagentPolicyReason;
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SubagentAncestry {
|
|
19
|
+
preconfigIds: string[];
|
|
20
|
+
depth: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ResolveSubagentTargetsOptions {
|
|
24
|
+
sessionId: string;
|
|
25
|
+
canSpawnSubagents?: boolean | string[] | null;
|
|
26
|
+
allowSelfAsSubagent?: boolean;
|
|
27
|
+
currentPreconfig?: Preconfig | null;
|
|
28
|
+
maximumDepthReached?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SubagentTargetResolutionDeps {
|
|
32
|
+
getSession?: (id: string) => Session | null | Promise<Session | null>;
|
|
33
|
+
listPreconfigs?: () => Promise<Preconfig[]>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isValidSubagentPreconfig(preconfig: Pick<Preconfig, 'mode'>): boolean {
|
|
37
|
+
const mode = preconfig.mode ?? 'primary';
|
|
38
|
+
return mode === 'subagent' || mode === 'both';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isValidSubagentTargetPreconfig(
|
|
42
|
+
preconfig: Pick<Preconfig, 'id' | 'mode'>,
|
|
43
|
+
currentPreconfigId: string | null,
|
|
44
|
+
allowSelfAsSubagent: boolean,
|
|
45
|
+
): boolean {
|
|
46
|
+
return isValidSubagentPreconfig(preconfig)
|
|
47
|
+
|| (allowSelfAsSubagent && preconfig.id === currentPreconfigId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isSubagentSpawningDisabled(
|
|
51
|
+
canSpawnSubagents: boolean | string[] | null | undefined,
|
|
52
|
+
): boolean {
|
|
53
|
+
return canSpawnSubagents === false
|
|
54
|
+
|| canSpawnSubagents === null
|
|
55
|
+
|| (Array.isArray(canSpawnSubagents) && canSpawnSubagents.length === 0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getSubagentResumeError(
|
|
59
|
+
childSession: Pick<Session, 'parentId' | 'preconfigId'>,
|
|
60
|
+
parentSessionId: string,
|
|
61
|
+
targetPreconfigId: string,
|
|
62
|
+
): string | null {
|
|
63
|
+
if (childSession.parentId !== parentSessionId) {
|
|
64
|
+
return 'Invalid task_id: does not belong to this session';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (childSession.preconfigId !== targetPreconfigId) {
|
|
68
|
+
return `Invalid task_id: belongs to subagent type "${childSession.preconfigId ?? 'unknown'}", not "${targetPreconfigId}"`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Ancestry collection with the session lookup injected. The default
|
|
75
|
+
* parameter keeps the pre-C5 module-accessor signature for the legacy
|
|
76
|
+
* unscoped path; composed scopes pass their captured storage lookup. */
|
|
77
|
+
export async function collectSubagentAncestry(
|
|
78
|
+
sessionId: string,
|
|
79
|
+
getSession: (id: string) => Session | null | Promise<Session | null> = runtimeGetSession,
|
|
80
|
+
): Promise<SubagentAncestry> {
|
|
81
|
+
const preconfigIds: string[] = [];
|
|
82
|
+
const visitedSessionIds = new Set<string>();
|
|
83
|
+
let currentSessionId: string | null = sessionId;
|
|
84
|
+
let depth = 0;
|
|
85
|
+
|
|
86
|
+
while (currentSessionId && !visitedSessionIds.has(currentSessionId)) {
|
|
87
|
+
visitedSessionIds.add(currentSessionId);
|
|
88
|
+
const session = await getSession(currentSessionId);
|
|
89
|
+
if (!session) break;
|
|
90
|
+
|
|
91
|
+
if (session.preconfigId) {
|
|
92
|
+
preconfigIds.push(session.preconfigId);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!session.parentId) break;
|
|
96
|
+
depth++;
|
|
97
|
+
currentSessionId = session.parentId;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { preconfigIds, depth };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function evaluateSubagentTarget(options: {
|
|
104
|
+
targetPreconfigId: string;
|
|
105
|
+
currentPreconfigId: string | null;
|
|
106
|
+
ancestryPreconfigIds: string[];
|
|
107
|
+
allowSelfAsSubagent: boolean;
|
|
108
|
+
allowedSubagentIds?: string[];
|
|
109
|
+
maximumDepthReached?: boolean;
|
|
110
|
+
}): SubagentPolicyResult {
|
|
111
|
+
const {
|
|
112
|
+
targetPreconfigId,
|
|
113
|
+
currentPreconfigId,
|
|
114
|
+
ancestryPreconfigIds,
|
|
115
|
+
allowSelfAsSubagent,
|
|
116
|
+
allowedSubagentIds,
|
|
117
|
+
maximumDepthReached,
|
|
118
|
+
} = options;
|
|
119
|
+
|
|
120
|
+
if (maximumDepthReached) {
|
|
121
|
+
return { allowed: false, reason: 'maximum_depth' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (allowedSubagentIds && !allowedSubagentIds.includes(targetPreconfigId)) {
|
|
125
|
+
return { allowed: false, reason: 'target_not_allowed' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (currentPreconfigId === targetPreconfigId) {
|
|
129
|
+
if (!allowSelfAsSubagent) {
|
|
130
|
+
return {
|
|
131
|
+
allowed: false,
|
|
132
|
+
reason: 'self_disabled',
|
|
133
|
+
error: `Preconfig "${targetPreconfigId}" is not allowed to use itself as a subagent.`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (ancestryPreconfigIds.slice(1).includes(targetPreconfigId)) {
|
|
138
|
+
return {
|
|
139
|
+
allowed: false,
|
|
140
|
+
reason: 'repeated_ancestor',
|
|
141
|
+
error: `Preconfig "${targetPreconfigId}" is already present in this subagent chain.`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { allowed: true, reason: 'allowed' };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (ancestryPreconfigIds.includes(targetPreconfigId)) {
|
|
149
|
+
return {
|
|
150
|
+
allowed: false,
|
|
151
|
+
reason: 'repeated_ancestor',
|
|
152
|
+
error: `Preconfig "${targetPreconfigId}" is already present in this subagent chain.`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { allowed: true, reason: 'allowed' };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Target resolution with session and preconfig listing injected. The
|
|
160
|
+
* optional deps default to the module accessors, preserving the pre-C5
|
|
161
|
+
* signature for the legacy unscoped path. */
|
|
162
|
+
export async function resolveEffectiveSubagentTargets(
|
|
163
|
+
options: ResolveSubagentTargetsOptions,
|
|
164
|
+
deps: SubagentTargetResolutionDeps = {},
|
|
165
|
+
): Promise<Preconfig[]> {
|
|
166
|
+
const getSession = deps.getSession ?? runtimeGetSession;
|
|
167
|
+
const listPreconfigs = deps.listPreconfigs ?? runtimeListPreconfigs;
|
|
168
|
+
|
|
169
|
+
const spawningEnabled = options.canSpawnSubagents === true
|
|
170
|
+
|| (Array.isArray(options.canSpawnSubagents) && options.canSpawnSubagents.length > 0);
|
|
171
|
+
if (!spawningEnabled || options.maximumDepthReached) return [];
|
|
172
|
+
|
|
173
|
+
const ancestry = await collectSubagentAncestry(options.sessionId, getSession);
|
|
174
|
+
const currentSession = await getSession(options.sessionId);
|
|
175
|
+
const currentPreconfigId = currentSession?.preconfigId ?? options.currentPreconfig?.id ?? null;
|
|
176
|
+
const allowSelfAsSubagent = options.allowSelfAsSubagent
|
|
177
|
+
?? options.currentPreconfig?.allowSelfAsSubagent
|
|
178
|
+
?? false;
|
|
179
|
+
const configuredIds = Array.isArray(options.canSpawnSubagents)
|
|
180
|
+
? options.canSpawnSubagents
|
|
181
|
+
: undefined;
|
|
182
|
+
const effectiveAllowedIds = configuredIds
|
|
183
|
+
? [...new Set([...configuredIds, ...(currentPreconfigId && allowSelfAsSubagent ? [currentPreconfigId] : [])])]
|
|
184
|
+
: undefined;
|
|
185
|
+
|
|
186
|
+
const candidates = await listPreconfigs();
|
|
187
|
+
return candidates.filter((candidate) => isValidSubagentTargetPreconfig(
|
|
188
|
+
candidate,
|
|
189
|
+
currentPreconfigId,
|
|
190
|
+
allowSelfAsSubagent,
|
|
191
|
+
) && evaluateSubagentTarget({
|
|
192
|
+
targetPreconfigId: candidate.id,
|
|
193
|
+
currentPreconfigId,
|
|
194
|
+
ancestryPreconfigIds: ancestry.preconfigIds,
|
|
195
|
+
allowSelfAsSubagent,
|
|
196
|
+
allowedSubagentIds: effectiveAllowedIds,
|
|
197
|
+
}).allowed);
|
|
198
|
+
}
|