@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,305 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { simulateReadableStream } from 'ai';
|
|
3
|
+
import { getSandboxController } from './controller';
|
|
4
|
+
import { getSession } from '../storage/runtime';
|
|
5
|
+
import type {
|
|
6
|
+
ErrorResponse,
|
|
7
|
+
LlmCallContext,
|
|
8
|
+
SandboxResponse,
|
|
9
|
+
SandboxToolDefinition,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
const ERROR_TYPE_TO_STATUS: Record<NonNullable<ErrorResponse['errorType']>, number> = {
|
|
13
|
+
rate_limit: 429,
|
|
14
|
+
server: 500,
|
|
15
|
+
timeout: 408,
|
|
16
|
+
auth: 401,
|
|
17
|
+
invalid_request: 400,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
interface SandboxPromptMessage {
|
|
21
|
+
role: string;
|
|
22
|
+
content: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SandboxModelCallOptions {
|
|
26
|
+
prompt: SandboxPromptMessage[];
|
|
27
|
+
tools?: unknown;
|
|
28
|
+
abortSignal?: AbortSignal;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface SandboxLanguageModelOptions {
|
|
32
|
+
sessionId: string;
|
|
33
|
+
modelId: string;
|
|
34
|
+
providerId: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const defaultGenerateUsage = {
|
|
38
|
+
inputTokens: {
|
|
39
|
+
total: 10,
|
|
40
|
+
noCache: 10,
|
|
41
|
+
cacheRead: undefined,
|
|
42
|
+
cacheWrite: undefined,
|
|
43
|
+
},
|
|
44
|
+
outputTokens: {
|
|
45
|
+
total: 20,
|
|
46
|
+
text: 20,
|
|
47
|
+
reasoning: undefined,
|
|
48
|
+
},
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
function toTools(tools: unknown): SandboxToolDefinition[] {
|
|
52
|
+
if (!tools) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (Array.isArray(tools)) {
|
|
57
|
+
return tools.map((tool, index) => {
|
|
58
|
+
const candidate = tool as {
|
|
59
|
+
name?: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
inputSchema?: unknown;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
name: candidate.name ?? `tool-${index + 1}`,
|
|
66
|
+
description: candidate.description ?? '',
|
|
67
|
+
inputSchema: candidate.inputSchema,
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return Object.entries(tools as Record<string, { description?: string; inputSchema?: unknown }>).map(([name, tool]) => ({
|
|
73
|
+
name,
|
|
74
|
+
description: tool.description ?? '',
|
|
75
|
+
inputSchema: tool.inputSchema,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function toSystemPrompt(prompt: SandboxPromptMessage[]): string | undefined {
|
|
80
|
+
const systemMessage = prompt.find((message) => message.role === 'system');
|
|
81
|
+
if (!systemMessage) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return typeof systemMessage.content === 'string' ? systemMessage.content : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function wrapStreamWithCompletion(
|
|
89
|
+
stream: ReadableStream<unknown>,
|
|
90
|
+
callId: string,
|
|
91
|
+
): ReadableStream<unknown> {
|
|
92
|
+
return new ReadableStream<unknown>({
|
|
93
|
+
async start(controller): Promise<void> {
|
|
94
|
+
const reader = stream.getReader();
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
while (true) {
|
|
98
|
+
const { done, value } = await reader.read();
|
|
99
|
+
if (done) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
controller.enqueue(value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
controller.close();
|
|
107
|
+
} catch (error: unknown) {
|
|
108
|
+
controller.error(error);
|
|
109
|
+
} finally {
|
|
110
|
+
reader.releaseLock();
|
|
111
|
+
getSandboxController().complete(callId);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
async cancel(reason: unknown): Promise<void> {
|
|
115
|
+
await stream.cancel(reason);
|
|
116
|
+
getSandboxController().complete(callId);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class SandboxLanguageModel {
|
|
122
|
+
readonly specificationVersion = 'v3' as const;
|
|
123
|
+
readonly provider: string;
|
|
124
|
+
readonly modelId: string;
|
|
125
|
+
readonly supportedUrls = Promise.resolve({});
|
|
126
|
+
|
|
127
|
+
private sessionId: string;
|
|
128
|
+
|
|
129
|
+
constructor(options: SandboxLanguageModelOptions) {
|
|
130
|
+
this.sessionId = options.sessionId;
|
|
131
|
+
this.modelId = options.modelId;
|
|
132
|
+
this.provider = options.providerId;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async doStream(options: SandboxModelCallOptions): Promise<{ stream: ReadableStream<unknown> }> {
|
|
136
|
+
const context = await this.createContext(options, 'stream');
|
|
137
|
+
const response = await getSandboxController().waitForResponse(context, options.abortSignal);
|
|
138
|
+
const stream = wrapStreamWithCompletion(this.responseToStream(response), context.callId);
|
|
139
|
+
|
|
140
|
+
return { stream };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async doGenerate(options: SandboxModelCallOptions): Promise<{
|
|
144
|
+
content: Array<{ type: 'text'; text: string }>;
|
|
145
|
+
finishReason: { unified: 'stop'; raw: undefined };
|
|
146
|
+
usage: typeof defaultGenerateUsage;
|
|
147
|
+
warnings: [];
|
|
148
|
+
}> {
|
|
149
|
+
const context = await this.createContext(options, 'generate');
|
|
150
|
+
const response = await getSandboxController().waitForResponse(context, options.abortSignal);
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
return this.responseToGenerateResult(response);
|
|
154
|
+
} finally {
|
|
155
|
+
getSandboxController().complete(context.callId);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async createContext(
|
|
160
|
+
options: SandboxModelCallOptions,
|
|
161
|
+
mode: 'stream' | 'generate',
|
|
162
|
+
): Promise<LlmCallContext> {
|
|
163
|
+
return {
|
|
164
|
+
callId: randomUUID(),
|
|
165
|
+
sessionId: this.sessionId,
|
|
166
|
+
depth: await this.computeDepth(),
|
|
167
|
+
mode,
|
|
168
|
+
messages: options.prompt.map((message) => ({
|
|
169
|
+
role: message.role,
|
|
170
|
+
content: message.content,
|
|
171
|
+
})),
|
|
172
|
+
systemPrompt: toSystemPrompt(options.prompt),
|
|
173
|
+
tools: toTools(options.tools),
|
|
174
|
+
modelId: this.modelId,
|
|
175
|
+
providerId: this.provider,
|
|
176
|
+
timestamp: Date.now(),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private responseToStream(response: SandboxResponse): ReadableStream<unknown> {
|
|
181
|
+
if (response.type === 'error') {
|
|
182
|
+
throw createClassifiedError(response);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const chunks: unknown[] = [];
|
|
186
|
+
|
|
187
|
+
switch (response.type) {
|
|
188
|
+
case 'text': {
|
|
189
|
+
const textId = randomUUID();
|
|
190
|
+
const splitAt = Math.max(1, Math.floor(response.content.length / 2));
|
|
191
|
+
const textChunks = response.content.length > 1
|
|
192
|
+
? [response.content.slice(0, splitAt), response.content.slice(splitAt)]
|
|
193
|
+
: [response.content];
|
|
194
|
+
chunks.push(
|
|
195
|
+
{ type: 'text-start', id: textId },
|
|
196
|
+
...textChunks.map((delta) => ({ type: 'text-delta', id: textId, delta })),
|
|
197
|
+
{ type: 'text-end', id: textId },
|
|
198
|
+
);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case 'reasoning': {
|
|
203
|
+
const reasoningId = randomUUID();
|
|
204
|
+
const textId = randomUUID();
|
|
205
|
+
chunks.push(
|
|
206
|
+
{ type: 'reasoning-start', id: reasoningId },
|
|
207
|
+
{ type: 'reasoning-delta', id: reasoningId, delta: response.reasoning },
|
|
208
|
+
{ type: 'reasoning-end', id: reasoningId },
|
|
209
|
+
{ type: 'text-start', id: textId },
|
|
210
|
+
{ type: 'text-delta', id: textId, delta: response.text },
|
|
211
|
+
{ type: 'text-end', id: textId },
|
|
212
|
+
);
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case 'tool-call': {
|
|
217
|
+
chunks.push({
|
|
218
|
+
type: 'tool-call',
|
|
219
|
+
toolCallId: response.toolCallId ?? randomUUID(),
|
|
220
|
+
toolName: response.toolName,
|
|
221
|
+
input: JSON.stringify(response.args),
|
|
222
|
+
});
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
case 'multi-tool-call': {
|
|
227
|
+
for (const call of response.calls) {
|
|
228
|
+
chunks.push({
|
|
229
|
+
type: 'tool-call',
|
|
230
|
+
toolCallId: call.toolCallId ?? randomUUID(),
|
|
231
|
+
toolName: call.toolName,
|
|
232
|
+
input: JSON.stringify(call.args),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
chunks.push({
|
|
240
|
+
type: 'finish',
|
|
241
|
+
finishReason: { unified: 'stop' as const, raw: undefined },
|
|
242
|
+
logprobs: undefined,
|
|
243
|
+
usage: defaultGenerateUsage,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return simulateReadableStream({ chunks });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private responseToGenerateResult(response: SandboxResponse): {
|
|
250
|
+
content: Array<{ type: 'text'; text: string }>;
|
|
251
|
+
finishReason: { unified: 'stop'; raw: undefined };
|
|
252
|
+
usage: typeof defaultGenerateUsage;
|
|
253
|
+
warnings: [];
|
|
254
|
+
} {
|
|
255
|
+
if (response.type === 'error') {
|
|
256
|
+
throw createClassifiedError(response);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const text = response.type === 'text'
|
|
260
|
+
? response.content
|
|
261
|
+
: response.type === 'reasoning'
|
|
262
|
+
? response.text
|
|
263
|
+
: JSON.stringify(response);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
content: [{ type: 'text', text }],
|
|
267
|
+
finishReason: { unified: 'stop', raw: undefined },
|
|
268
|
+
usage: defaultGenerateUsage,
|
|
269
|
+
warnings: [],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private async computeDepth(): Promise<number> {
|
|
274
|
+
let depth = 0;
|
|
275
|
+
let session = await getSession(this.sessionId);
|
|
276
|
+
|
|
277
|
+
while (session?.parentId) {
|
|
278
|
+
depth += 1;
|
|
279
|
+
session = await getSession(session.parentId);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return depth;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function createClassifiedError(response: ErrorResponse): Error & { status?: number; isRateLimitError?: boolean; isRetryableError?: boolean; isTimeoutError?: boolean } {
|
|
287
|
+
const err = new Error(response.error) as Error & { status?: number; isRateLimitError?: boolean; isRetryableError?: boolean; isTimeoutError?: boolean };
|
|
288
|
+
|
|
289
|
+
if (response.errorType) {
|
|
290
|
+
const status = ERROR_TYPE_TO_STATUS[response.errorType];
|
|
291
|
+
err.status = status;
|
|
292
|
+
|
|
293
|
+
if (response.errorType === 'rate_limit') {
|
|
294
|
+
err.isRateLimitError = true;
|
|
295
|
+
err.isRetryableError = true;
|
|
296
|
+
} else if (response.errorType === 'server') {
|
|
297
|
+
err.isRetryableError = true;
|
|
298
|
+
} else if (response.errorType === 'timeout') {
|
|
299
|
+
err.isTimeoutError = true;
|
|
300
|
+
err.isRetryableError = true;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return err;
|
|
305
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { LanguageModel } from 'ai';
|
|
2
|
+
import type { ProviderDescriptor, ProviderStatus } from '@capekai/types';
|
|
3
|
+
import type {
|
|
4
|
+
ConnectableProvider,
|
|
5
|
+
ModelFactoryOptions,
|
|
6
|
+
ModelFactoryResult,
|
|
7
|
+
TokenResponse,
|
|
8
|
+
} from '../providers/types';
|
|
9
|
+
import { SandboxLanguageModel } from './model';
|
|
10
|
+
|
|
11
|
+
export class SandboxProvider implements ConnectableProvider {
|
|
12
|
+
readonly descriptor: ProviderDescriptor = {
|
|
13
|
+
id: 'sandbox',
|
|
14
|
+
displayName: 'Sandbox (Interactive Mock)',
|
|
15
|
+
description: 'Interactive mock provider for sandbox testing',
|
|
16
|
+
authType: 'none',
|
|
17
|
+
connectable: false,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
getStatus(): ProviderStatus {
|
|
21
|
+
return {
|
|
22
|
+
provider: this.descriptor.id,
|
|
23
|
+
connected: true,
|
|
24
|
+
displayName: this.descriptor.displayName,
|
|
25
|
+
description: this.descriptor.description,
|
|
26
|
+
authType: this.descriptor.authType,
|
|
27
|
+
connectable: this.descriptor.connectable,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async connect() {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async onTokensReceived(_tokens: TokenResponse): Promise<void> {
|
|
36
|
+
// Sandbox provider doesn't use OAuth
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async disconnect(): Promise<void> {
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async createModel(options: ModelFactoryOptions): Promise<ModelFactoryResult> {
|
|
43
|
+
return {
|
|
44
|
+
model: new SandboxLanguageModel({
|
|
45
|
+
sessionId: options.sessionId ?? 'default',
|
|
46
|
+
modelId: options.modelId,
|
|
47
|
+
providerId: options.providerId,
|
|
48
|
+
}) as unknown as LanguageModel,
|
|
49
|
+
useProviderInstructions: false,
|
|
50
|
+
omitMaxOutputTokens: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export interface SandboxCallMessage {
|
|
2
|
+
role: string;
|
|
3
|
+
content: unknown;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface SandboxToolDefinition {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
inputSchema: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface LlmCallContext {
|
|
13
|
+
callId: string;
|
|
14
|
+
sessionId: string;
|
|
15
|
+
depth: number;
|
|
16
|
+
mode: 'stream' | 'generate';
|
|
17
|
+
messages: SandboxCallMessage[];
|
|
18
|
+
systemPrompt?: string;
|
|
19
|
+
tools: SandboxToolDefinition[];
|
|
20
|
+
modelId: string;
|
|
21
|
+
providerId: string;
|
|
22
|
+
timestamp: number;
|
|
23
|
+
parentCallId?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TextResponse {
|
|
27
|
+
type: 'text';
|
|
28
|
+
content: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ToolCallResponse {
|
|
32
|
+
type: 'tool-call';
|
|
33
|
+
toolName: string;
|
|
34
|
+
args: Record<string, unknown>;
|
|
35
|
+
toolCallId?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MultiToolCallResponse {
|
|
39
|
+
type: 'multi-tool-call';
|
|
40
|
+
calls: Array<{
|
|
41
|
+
toolName: string;
|
|
42
|
+
args: Record<string, unknown>;
|
|
43
|
+
toolCallId?: string;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ErrorResponse {
|
|
48
|
+
type: 'error';
|
|
49
|
+
error: string;
|
|
50
|
+
errorType?: 'rate_limit' | 'server' | 'timeout' | 'auth' | 'invalid_request';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ReasoningResponse {
|
|
54
|
+
type: 'reasoning';
|
|
55
|
+
reasoning: string;
|
|
56
|
+
text: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type SandboxResponse =
|
|
60
|
+
| TextResponse
|
|
61
|
+
| ToolCallResponse
|
|
62
|
+
| MultiToolCallResponse
|
|
63
|
+
| ErrorResponse
|
|
64
|
+
| ReasoningResponse;
|
|
65
|
+
|
|
66
|
+
export interface AutoResponderRule {
|
|
67
|
+
match: {
|
|
68
|
+
mode?: 'stream' | 'generate';
|
|
69
|
+
depth?: number | number[];
|
|
70
|
+
sessionId?: string | string[];
|
|
71
|
+
hasToolResults?: boolean;
|
|
72
|
+
};
|
|
73
|
+
response: SandboxResponse;
|
|
74
|
+
maxUses?: number;
|
|
75
|
+
label?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface SandboxHistoryEntry {
|
|
79
|
+
callId: string;
|
|
80
|
+
context: LlmCallContext;
|
|
81
|
+
response: SandboxResponse | null;
|
|
82
|
+
respondedAt: number | null;
|
|
83
|
+
completedAt: number | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface SandboxCallWaitingEvent {
|
|
87
|
+
type: 'sandbox.call_waiting';
|
|
88
|
+
context: LlmCallContext;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface SandboxRespondMessage {
|
|
92
|
+
type: 'sandbox.respond';
|
|
93
|
+
callId: string;
|
|
94
|
+
response: SandboxResponse;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface SandboxCallCompletedEvent {
|
|
98
|
+
type: 'sandbox.call_completed';
|
|
99
|
+
callId: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface SandboxHistoryEvent {
|
|
103
|
+
type: 'sandbox.history';
|
|
104
|
+
entries: SandboxHistoryEntry[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type SandboxControlEvent =
|
|
108
|
+
| SandboxCallWaitingEvent
|
|
109
|
+
| SandboxCallCompletedEvent
|
|
110
|
+
| SandboxHistoryEvent;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CreateScheduledJobInput, ScheduledJob, UpdateScheduledJobInput } from '@capekai/types';
|
|
2
|
+
|
|
3
|
+
export interface SchedulerHost {
|
|
4
|
+
create(workspaceId: string, input: CreateScheduledJobInput): ScheduledJob;
|
|
5
|
+
get(id: string): ScheduledJob | null;
|
|
6
|
+
list(workspaceId: string): ScheduledJob[];
|
|
7
|
+
update(id: string, updates: UpdateScheduledJobInput): ScheduledJob | null;
|
|
8
|
+
delete(id: string): boolean;
|
|
9
|
+
trigger(job: ScheduledJob): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const defaultHost: SchedulerHost = {
|
|
13
|
+
create() { throw new Error('Scheduler host is not configured'); },
|
|
14
|
+
get: () => null,
|
|
15
|
+
list: () => [],
|
|
16
|
+
update: () => null,
|
|
17
|
+
delete: () => false,
|
|
18
|
+
trigger: () => {},
|
|
19
|
+
};
|
|
20
|
+
let host = defaultHost;
|
|
21
|
+
export function configureSchedulerHost(value?: SchedulerHost): void { host = value ?? defaultHost; }
|
|
22
|
+
export function getSchedulerHost(): SchedulerHost { return host; }
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { PermissionAsk } from '@capekai/tool'
|
|
2
|
+
import type { PermissionRiskLevel } from '@capekai/tool'
|
|
3
|
+
import type { ScheduleConfig, ScheduleKind, ScheduledJob, UpdateScheduledJobInput } from '@capekai/types';
|
|
4
|
+
import { getSchedulerHost, type SchedulerHost } from './host';
|
|
5
|
+
|
|
6
|
+
export const schedulerToolDefinition = {
|
|
7
|
+
name: 'scheduler',
|
|
8
|
+
description: `Manage scheduled tasks for the current workspace. Create recurring or one-shot automated tasks that run as agent sessions on a schedule. Each run creates a new session (or reuses one) with the given prompt.
|
|
9
|
+
|
|
10
|
+
Actions:
|
|
11
|
+
- "create": Create a new scheduled job. Requires name, prompt, and schedule.
|
|
12
|
+
- "list": List all scheduled jobs in the workspace.
|
|
13
|
+
- "update": Update an existing job by ID. All fields optional except jobId.
|
|
14
|
+
- "pause": Pause a job (stops scheduling, keeps the job).
|
|
15
|
+
- "resume": Resume a paused job.
|
|
16
|
+
- "trigger": Run a job immediately (does not affect the schedule).
|
|
17
|
+
- "remove": Permanently delete a job.
|
|
18
|
+
|
|
19
|
+
Schedule types (convert the user's natural language to these):
|
|
20
|
+
- { type: "once", runAt: "2025-01-15T14:30:00.000Z" } — one-shot at an ISO timestamp
|
|
21
|
+
- { type: "interval", intervalMinutes: 120 } — recurring every N minutes
|
|
22
|
+
- { type: "daily", time: "09:00" } — daily at a specific time (HH:mm, server timezone)
|
|
23
|
+
- { type: "weekly", days: [1,2,3,4,5], time: "17:00" } — on specific weekdays (0=Sun, 1=Mon, ..., 6=Sat) at a time
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
- "every 2 hours" → { type: "interval", intervalMinutes: 120 }
|
|
27
|
+
- "daily at 9am" → { type: "daily", time: "09:00" }
|
|
28
|
+
- "every weekday at 5pm" → { type: "weekly", days: [1,2,3,4,5], time: "17:00" }
|
|
29
|
+
- "in 30 minutes" → { type: "once", runAt: "<ISO timestamp 30 min from now>" }
|
|
30
|
+
|
|
31
|
+
The prompt should be self-contained — it is the full instruction given to the agent for each run.`,
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: 'object' as const,
|
|
34
|
+
properties: {
|
|
35
|
+
action: { type: 'string' as const, enum: ['create', 'list', 'update', 'pause', 'resume', 'trigger', 'remove'], description: 'The action to perform.' },
|
|
36
|
+
jobId: { type: 'string' as const, description: 'Job ID (for update/pause/resume/trigger/remove actions). Use "list" to find job IDs.' },
|
|
37
|
+
name: { type: 'string' as const, description: 'Friendly name for the job (create/update).' },
|
|
38
|
+
prompt: { type: 'string' as const, description: 'The task instruction to run on each execution (create/update). Must be self-contained.' },
|
|
39
|
+
schedule: { type: 'object' as const, description: 'Schedule configuration (create/update). See tool description for format.', properties: { type: { type: 'string' as const, enum: ['once', 'interval', 'daily', 'weekly'] }, runAt: { type: 'string' as const, description: 'ISO timestamp (for type: "once")' }, intervalMinutes: { type: 'number' as const, description: 'Minutes between runs (for type: "interval")' }, time: { type: 'string' as const, description: 'HH:mm time (for type: "daily" or "weekly")' }, days: { type: 'array' as const, items: { type: 'number' as const }, description: 'Weekdays 0-6 (0=Sun) for type: "weekly"' } } },
|
|
40
|
+
repeatLimit: { type: 'number' as const, description: 'Maximum number of runs. Omit for infinite. (create/update)' },
|
|
41
|
+
reuseSession: { type: 'boolean' as const, description: 'If true, all runs accumulate in the same session. If false (default), each run creates a new session.' },
|
|
42
|
+
includeHistory: { type: 'boolean' as const, description: 'When reuseSession is true, whether the agent sees previous run history. Default false.' },
|
|
43
|
+
autoApproveSeverity: { type: 'string' as const, enum: ['off', 'none', 'low', 'medium', 'high'], description: 'Auto-approve severity for sessions created by this job. Omit or null to use workspace default.' },
|
|
44
|
+
notificationsEnabled: { type: 'boolean' as const, description: 'When true, scheduled runs may send completion, failure, and permission push notifications (subject to each browser subscription\'s existing preferences). Defaults to false (no notifications). (create/update)' },
|
|
45
|
+
}, required: ['action'],
|
|
46
|
+
}, timeout: 10000,
|
|
47
|
+
};
|
|
48
|
+
export interface SchedulerToolResult { success: boolean; action: string; title: string; job?: ScheduledJob; jobs?: ScheduledJob[]; jobId?: string; error?: string }
|
|
49
|
+
|
|
50
|
+
function parseSchedule(raw: Record<string, unknown>): { kind: ScheduleKind; config: ScheduleConfig } | { error: string } {
|
|
51
|
+
const type = raw.type as string;
|
|
52
|
+
if (!type) return { error: 'Schedule type is required' };
|
|
53
|
+
if (type === 'once') {
|
|
54
|
+
const runAt = raw.runAt as string;
|
|
55
|
+
if (!runAt) return { error: 'runAt (ISO timestamp) is required for type "once"' };
|
|
56
|
+
const timestamp = new Date(runAt).getTime();
|
|
57
|
+
return Number.isFinite(timestamp) ? { kind: 'once', config: { type: 'once', runAt: new Date(timestamp).toISOString() } } : { error: `Invalid runAt timestamp: ${runAt}` };
|
|
58
|
+
}
|
|
59
|
+
if (type === 'interval') {
|
|
60
|
+
const minutes = raw.intervalMinutes as number;
|
|
61
|
+
return minutes && minutes >= 1 ? { kind: 'interval', config: { type: 'interval', intervalMinutes: minutes } } : { error: 'intervalMinutes must be a positive number' };
|
|
62
|
+
}
|
|
63
|
+
if (type === 'daily') {
|
|
64
|
+
const time = raw.time as string;
|
|
65
|
+
return time && /^\d{2}:\d{2}$/.test(time) ? { kind: 'daily', config: { type: 'daily', time } } : { error: 'time must be in HH:mm format for type "daily"' };
|
|
66
|
+
}
|
|
67
|
+
if (type === 'weekly') {
|
|
68
|
+
const time = raw.time as string;
|
|
69
|
+
const days = raw.days as number[];
|
|
70
|
+
if (!time || !/^\d{2}:\d{2}$/.test(time)) return { error: 'time must be in HH:mm format for type "weekly"' };
|
|
71
|
+
if (!Array.isArray(days) || days.length === 0) return { error: 'days array is required for type "weekly"' };
|
|
72
|
+
const valid = days.filter((day) => typeof day === 'number' && day >= 0 && day <= 6);
|
|
73
|
+
return valid.length > 0 ? { kind: 'weekly', config: { type: 'weekly', days: valid, time } } : { error: 'days must contain valid weekday numbers (0-6)' };
|
|
74
|
+
}
|
|
75
|
+
return { error: `Unknown schedule type: ${type}` };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Unscoped execution path: reads the configured module-level host, exactly
|
|
79
|
+
* like the pre-C5 tool. */
|
|
80
|
+
export async function executeSchedulerTool(input: Record<string, unknown>, workspaceId: string, currentSessionId: string, risk: PermissionRiskLevel, askFn?: (ask: PermissionAsk) => Promise<unknown>): Promise<SchedulerToolResult> {
|
|
81
|
+
return runScheduler(getSchedulerHost(), input, workspaceId, currentSessionId, risk, askFn);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Composed execution path: the domain plugin captures the process-scoped
|
|
85
|
+
* host service at setup and passes it here, so composed execution never
|
|
86
|
+
* reads the mutable module-global host accessor. */
|
|
87
|
+
export async function executeSchedulerToolWithHost(host: SchedulerHost, input: Record<string, unknown>, workspaceId: string, currentSessionId: string, risk: PermissionRiskLevel, askFn?: (ask: PermissionAsk) => Promise<unknown>): Promise<SchedulerToolResult> {
|
|
88
|
+
return runScheduler(host, input, workspaceId, currentSessionId, risk, askFn);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function runScheduler(host: SchedulerHost, input: Record<string, unknown>, workspaceId: string, currentSessionId: string, risk: PermissionRiskLevel, askFn?: (ask: PermissionAsk) => Promise<unknown>): Promise<SchedulerToolResult> {
|
|
92
|
+
const action = input.action as string;
|
|
93
|
+
if (action !== 'list' && risk !== 'none' && askFn) {
|
|
94
|
+
const verb = ({ create: 'create', update: 'update', pause: 'pause', resume: 'resume', trigger: 'trigger', remove: 'delete' } as Record<string, string>)[action] || action;
|
|
95
|
+
const name = (input.name as string) || (input.jobId as string) || '';
|
|
96
|
+
console.log(`[scheduler-tool] Requesting permission for "${verb}"...`);
|
|
97
|
+
const approved = await askFn({ type: 'permission', question: name ? `Allow scheduler to ${verb} scheduled job "${name.slice(0, 80)}"?` : `Allow scheduler to ${verb} a scheduled job?`, description: `Tool: scheduler\nAction: ${verb}${name ? `\nJob: ${name.slice(0, 200)}` : ''}`, risk, resource: 'scheduler', action: verb });
|
|
98
|
+
if (!approved) return { success: false, action, title: 'Permission denied', error: 'USER_REJECTION' };
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
switch (action) {
|
|
102
|
+
case 'create': return create(input, workspaceId, currentSessionId, host);
|
|
103
|
+
case 'list': { const jobs = host.list(workspaceId); return { success: true, action, title: `${jobs.length} scheduled job${jobs.length === 1 ? '' : 's'}`, jobs }; }
|
|
104
|
+
case 'update': return update(input, workspaceId, host);
|
|
105
|
+
case 'pause': return stateChange(input, workspaceId, 'paused', host);
|
|
106
|
+
case 'resume': return stateChange(input, workspaceId, 'active', host);
|
|
107
|
+
case 'trigger': return trigger(input, workspaceId, host);
|
|
108
|
+
case 'remove': return remove(input, workspaceId, host);
|
|
109
|
+
default: return { success: false, action, title: 'Invalid action', error: `Unknown action: ${action}` };
|
|
110
|
+
}
|
|
111
|
+
} catch (error: unknown) {
|
|
112
|
+
console.error(`[scheduler-tool] Action "${action}" failed:`, error);
|
|
113
|
+
return { success: false, action, title: 'Internal error', error: error instanceof Error ? error.message : String(error) };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function create(input: Record<string, unknown>, workspaceId: string, sessionId: string, host: SchedulerHost): SchedulerToolResult {
|
|
118
|
+
const name = input.name as string;
|
|
119
|
+
const prompt = input.prompt as string;
|
|
120
|
+
const schedule = input.schedule as Record<string, unknown>;
|
|
121
|
+
if (!name || typeof name !== 'string' || name.trim() === '') return { success: false, action: 'create', title: 'Validation error', error: 'name is required' };
|
|
122
|
+
if (!prompt || typeof prompt !== 'string' || prompt.trim() === '') return { success: false, action: 'create', title: 'Validation error', error: 'prompt is required' };
|
|
123
|
+
if (!schedule || typeof schedule !== 'object') return { success: false, action: 'create', title: 'Validation error', error: 'schedule is required' };
|
|
124
|
+
const parsed = parseSchedule(schedule);
|
|
125
|
+
if ('error' in parsed) return { success: false, action: 'create', title: 'Validation error', error: parsed.error };
|
|
126
|
+
const job = host.create(workspaceId, { name: name.trim(), prompt: prompt.trim(), scheduleKind: parsed.kind, scheduleConfig: parsed.config, repeatLimit: input.repeatLimit as number | null | undefined, reuseSession: input.reuseSession as boolean | undefined, includeHistory: input.includeHistory as boolean | undefined, originSessionId: sessionId, autoApproveSeverity: input.autoApproveSeverity as ScheduledJob['autoApproveSeverity'], notificationsEnabled: input.notificationsEnabled as boolean | undefined });
|
|
127
|
+
return { success: true, action: 'create', title: `Scheduled job "${job.name}" created`, job };
|
|
128
|
+
}
|
|
129
|
+
function wrongWorkspace(job: ScheduledJob, workspaceId: string, action: string): SchedulerToolResult | null {
|
|
130
|
+
return job.workspaceId === workspaceId
|
|
131
|
+
? null
|
|
132
|
+
: { success: false, action, title: 'Access denied', error: 'Job does not belong to this workspace' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function update(input: Record<string, unknown>, workspaceId: string, host: SchedulerHost): SchedulerToolResult {
|
|
136
|
+
const id = input.jobId as string;
|
|
137
|
+
if (!id) return { success: false, action: 'update', title: 'Validation error', error: 'jobId is required' };
|
|
138
|
+
const existing = host.get(id);
|
|
139
|
+
if (!existing) return { success: false, action: 'update', title: 'Not found', error: `Job ${id} not found` };
|
|
140
|
+
const denied = wrongWorkspace(existing, workspaceId, 'update');
|
|
141
|
+
if (denied) return denied;
|
|
142
|
+
const updates: UpdateScheduledJobInput = {};
|
|
143
|
+
for (const key of ['name', 'prompt', 'repeatLimit', 'reuseSession', 'includeHistory', 'autoApproveSeverity', 'notificationsEnabled'] as const) if (input[key] !== undefined) Object.assign(updates, { [key]: input[key] });
|
|
144
|
+
if (input.schedule) {
|
|
145
|
+
const parsed = parseSchedule(input.schedule as Record<string, unknown>);
|
|
146
|
+
if ('error' in parsed) return { success: false, action: 'update', title: 'Validation error', error: parsed.error };
|
|
147
|
+
updates.scheduleKind = parsed.kind; updates.scheduleConfig = parsed.config;
|
|
148
|
+
}
|
|
149
|
+
const job = host.update(id, updates);
|
|
150
|
+
return job ? { success: true, action: 'update', title: `Scheduled job "${job.name}" updated`, job } : { success: false, action: 'update', title: 'Update failed', error: 'Failed to update job' };
|
|
151
|
+
}
|
|
152
|
+
function stateChange(input: Record<string, unknown>, workspaceId: string, state: 'active' | 'paused', host: SchedulerHost): SchedulerToolResult {
|
|
153
|
+
const id = input.jobId as string; const action = input.action as string;
|
|
154
|
+
if (!id) return { success: false, action, title: 'Validation error', error: 'jobId is required' };
|
|
155
|
+
const existing = host.get(id); if (!existing) return { success: false, action, title: 'Not found', error: `Job ${id} not found` };
|
|
156
|
+
const denied = wrongWorkspace(existing, workspaceId, action); if (denied) return denied;
|
|
157
|
+
return { success: true, action, title: `Job "${existing.name}" ${state === 'paused' ? 'paused' : 'resumed'}`, job: host.update(id, { state }) ?? undefined };
|
|
158
|
+
}
|
|
159
|
+
function trigger(input: Record<string, unknown>, workspaceId: string, host: SchedulerHost): SchedulerToolResult {
|
|
160
|
+
const id = input.jobId as string;
|
|
161
|
+
if (!id) return { success: false, action: 'trigger', title: 'Validation error', error: 'jobId is required' };
|
|
162
|
+
const job = host.get(id); if (!job) return { success: false, action: 'trigger', title: 'Not found', error: `Job ${id} not found` };
|
|
163
|
+
const denied = wrongWorkspace(job, workspaceId, 'trigger'); if (denied) return denied;
|
|
164
|
+
host.trigger(job); return { success: true, action: 'trigger', title: `Job "${job.name}" triggered`, jobId: id };
|
|
165
|
+
}
|
|
166
|
+
function remove(input: Record<string, unknown>, workspaceId: string, host: SchedulerHost): SchedulerToolResult {
|
|
167
|
+
const id = input.jobId as string;
|
|
168
|
+
if (!id) return { success: false, action: 'remove', title: 'Validation error', error: 'jobId is required' };
|
|
169
|
+
const job = host.get(id); if (!job) return { success: false, action: 'remove', title: 'Not found', error: `Job ${id} not found` };
|
|
170
|
+
const denied = wrongWorkspace(job, workspaceId, 'remove'); if (denied) return denied;
|
|
171
|
+
host.delete(id); return { success: true, action: 'remove', title: `Job "${job.name}" deleted`, jobId: id };
|
|
172
|
+
}
|