@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,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C6 tool-output policy default provider and scoped service.
|
|
3
|
+
*
|
|
4
|
+
* `createToolOutputService` reproduces the exact pre-C6 behavior: the
|
|
5
|
+
* artifact envelope, the bounded fallback, the retrieval tool, the
|
|
6
|
+
* per-service wrap WeakSet, and the legacy filesystem truncation. The
|
|
7
|
+
* strict ID validation and session-scoped retrieval stay in the storage
|
|
8
|
+
* layer (mandatory invariants); this service only decides what the model
|
|
9
|
+
* sees and how retrieval is invoked.
|
|
10
|
+
*
|
|
11
|
+
* Scope ownership: a composed agent scope gets its own service instance
|
|
12
|
+
* (frozen options and an isolated wrap WeakSet). Consumers that run outside
|
|
13
|
+
* a composed scope (the current Jean2 server path) fall back to one lazily
|
|
14
|
+
* created process-default service with the exact default constants, until
|
|
15
|
+
* C8 retires the compat surface.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
19
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import os from 'node:os';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { jsonSchema, tool, type Tool as AiTool } from 'ai';
|
|
23
|
+
import type { LoadedTool } from '@capekai/tool'
|
|
24
|
+
import { ToolContext, ToolDefinition, ToolResult } from '@capekai/tool';
|
|
25
|
+
import {
|
|
26
|
+
createToolOutputArtifact,
|
|
27
|
+
getToolOutputArtifactPage,
|
|
28
|
+
} from '../storage/runtime';
|
|
29
|
+
import type { ToolOutputArtifactFormat, ToolOutputArtifactPage } from '../storage/contracts';
|
|
30
|
+
import type {
|
|
31
|
+
ToolOutputArtifactReference,
|
|
32
|
+
ToolOutputArtifactService,
|
|
33
|
+
ToolOutputFallback,
|
|
34
|
+
ToolOutputPolicyContext,
|
|
35
|
+
ToolOutputPolicyOptions,
|
|
36
|
+
} from './contracts';
|
|
37
|
+
|
|
38
|
+
export const TOOL_OUTPUT_THRESHOLD_CHARS = 50_000;
|
|
39
|
+
export const TOOL_OUTPUT_PREVIEW_CHARS = 10_000;
|
|
40
|
+
export const RETRIEVE_TOOL_OUTPUT_NAME = 'retrieve-tool-output';
|
|
41
|
+
|
|
42
|
+
export interface ToolOutputServiceCreateOptions {
|
|
43
|
+
id?: string;
|
|
44
|
+
/** Frozen composition-time options. When omitted (the process-default
|
|
45
|
+
* fallback), the exact pre-C6 constants apply. */
|
|
46
|
+
options?: ToolOutputPolicyOptions;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function defaultOptions(): ToolOutputPolicyOptions {
|
|
50
|
+
return {
|
|
51
|
+
thresholdChars: TOOL_OUTPUT_THRESHOLD_CHARS,
|
|
52
|
+
previewChars: TOOL_OUTPUT_PREVIEW_CHARS,
|
|
53
|
+
retrievalToolName: RETRIEVE_TOOL_OUTPUT_NAME,
|
|
54
|
+
truncationMaxChars: 50_000,
|
|
55
|
+
truncationPreviewChars: 10_000,
|
|
56
|
+
truncationTempDir: path.join(os.tmpdir(), 'capek'),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Pure envelope guard: an artifact reference must carry the strict type,
|
|
61
|
+
* a string artifact id, a string preview, a json/text format, a numeric
|
|
62
|
+
* total, and `complete: false`. */
|
|
63
|
+
export function isToolOutputArtifactReference(value: unknown): value is ToolOutputArtifactReference {
|
|
64
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
65
|
+
const record = value as Record<string, unknown>;
|
|
66
|
+
return record.type === 'tool-output-artifact'
|
|
67
|
+
&& typeof record.artifactId === 'string'
|
|
68
|
+
&& typeof record.preview === 'string'
|
|
69
|
+
&& (record.format === 'json' || record.format === 'text')
|
|
70
|
+
&& typeof record.totalChars === 'number'
|
|
71
|
+
&& record.complete === false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function withVisualization(value: unknown, visualization: unknown): unknown {
|
|
75
|
+
if (visualization === undefined || !value || typeof value !== 'object' || Array.isArray(value)) return value;
|
|
76
|
+
return { ...value as Record<string, unknown>, _visualization: visualization };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function boundedFallback(value: unknown, totalChars: number | null, previewChars: number): ToolOutputFallback {
|
|
80
|
+
let preview: string;
|
|
81
|
+
try {
|
|
82
|
+
preview = typeof value === 'string' ? value : String(value);
|
|
83
|
+
} catch {
|
|
84
|
+
preview = '[Tool output could not be serialized]';
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
type: 'tool-output-preview',
|
|
88
|
+
preview: preview.slice(0, previewChars),
|
|
89
|
+
totalChars,
|
|
90
|
+
complete: false,
|
|
91
|
+
message: 'Exact tool output was not persisted. Only this bounded preview is available.',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const retrievalDefinitionBase: ToolDefinition = {
|
|
96
|
+
name: RETRIEVE_TOOL_OUTPUT_NAME,
|
|
97
|
+
description: 'Retrieve one bounded character page from an exact tool output artifact in the current session.',
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
artifactId: { type: 'string', format: 'uuid' },
|
|
102
|
+
offset: { type: 'integer', minimum: 0 },
|
|
103
|
+
limit: { type: 'integer', minimum: 1 },
|
|
104
|
+
},
|
|
105
|
+
required: ['artifactId'],
|
|
106
|
+
},
|
|
107
|
+
timeout: 30_000,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** The C6 default provider wrapping the exact pre-C6 behavior. */
|
|
111
|
+
export function createToolOutputService(
|
|
112
|
+
createOptions: ToolOutputServiceCreateOptions = {},
|
|
113
|
+
): ToolOutputArtifactService {
|
|
114
|
+
const id = createOptions.id ?? 'tool-output.default';
|
|
115
|
+
const options = createOptions.options ?? defaultOptions();
|
|
116
|
+
const outputPolicyWrappedTools = new WeakSet<object>();
|
|
117
|
+
|
|
118
|
+
async function applyToolOutputPolicy(result: unknown, context: ToolOutputPolicyContext): Promise<unknown> {
|
|
119
|
+
const visualization = result && typeof result === 'object' && !Array.isArray(result)
|
|
120
|
+
? (result as Record<string, unknown>)._visualization
|
|
121
|
+
: undefined;
|
|
122
|
+
const exact = result;
|
|
123
|
+
let serialized: string;
|
|
124
|
+
let content: string;
|
|
125
|
+
let format: ToolOutputArtifactFormat;
|
|
126
|
+
try {
|
|
127
|
+
serialized = JSON.stringify(exact, (key, value) => key === '_visualization' ? undefined : value);
|
|
128
|
+
if (serialized === undefined) throw new TypeError('Tool output is not JSON serializable');
|
|
129
|
+
format = typeof exact === 'string' ? 'text' : 'json';
|
|
130
|
+
content = format === 'text' ? exact as string : serialized;
|
|
131
|
+
} catch {
|
|
132
|
+
return withVisualization(boundedFallback(exact, null, options.previewChars), visualization);
|
|
133
|
+
}
|
|
134
|
+
if (serialized.length <= options.thresholdChars) return result;
|
|
135
|
+
|
|
136
|
+
const preview = content.slice(0, options.previewChars);
|
|
137
|
+
try {
|
|
138
|
+
const artifact = await createToolOutputArtifact({
|
|
139
|
+
sessionId: context.sessionId,
|
|
140
|
+
...(context.workspaceId ? { workspaceId: context.workspaceId } : {}),
|
|
141
|
+
toolCallId: context.toolCallId,
|
|
142
|
+
toolName: context.toolName,
|
|
143
|
+
content,
|
|
144
|
+
format,
|
|
145
|
+
});
|
|
146
|
+
const reference: ToolOutputArtifactReference = {
|
|
147
|
+
type: 'tool-output-artifact',
|
|
148
|
+
artifactId: artifact.id,
|
|
149
|
+
preview,
|
|
150
|
+
format,
|
|
151
|
+
totalChars: artifact.size,
|
|
152
|
+
complete: false,
|
|
153
|
+
message: `Exact output is available with ${options.retrievalToolName} using artifactId ${artifact.id}.`,
|
|
154
|
+
};
|
|
155
|
+
return withVisualization(reference, visualization);
|
|
156
|
+
} catch {
|
|
157
|
+
return withVisualization(boundedFallback(preview, content.length, options.previewChars), visualization);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function retrieveToolOutput(
|
|
162
|
+
sessionId: string,
|
|
163
|
+
input: { artifactId: string; offset?: number; limit?: number },
|
|
164
|
+
): Promise<ToolOutputArtifactPage | null> {
|
|
165
|
+
return getToolOutputArtifactPage(sessionId, input.artifactId, input.offset, input.limit);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function executeRetrieval(
|
|
169
|
+
input: Record<string, unknown>,
|
|
170
|
+
sessionId: string,
|
|
171
|
+
): Promise<ToolResult> {
|
|
172
|
+
// C6 step 6: the execution path uses the NON-REPLACEABLE runtime
|
|
173
|
+
// retrieval, so a replaced provider can never return foreign pages.
|
|
174
|
+
const page = await retrieveToolOutputForSession(sessionId, {
|
|
175
|
+
artifactId: String(input.artifactId ?? ''),
|
|
176
|
+
...(input.offset === undefined ? {} : { offset: Number(input.offset) }),
|
|
177
|
+
...(input.limit === undefined ? {} : { limit: Number(input.limit) }),
|
|
178
|
+
});
|
|
179
|
+
return page
|
|
180
|
+
? { success: true, result: page }
|
|
181
|
+
: { success: false, error: 'Tool output artifact not found' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function createRetrieveToolOutputStandardTool(): LoadedTool {
|
|
185
|
+
return {
|
|
186
|
+
definition: retrievalDefinitionBase,
|
|
187
|
+
path: 'builtin:@capekai/core',
|
|
188
|
+
execute: (input: Record<string, unknown>, context: ToolContext) =>
|
|
189
|
+
executeRetrieval(input, context.sessionId),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function buildRetrieveToolOutputAiTool(sessionId: string): AiTool {
|
|
194
|
+
return tool({
|
|
195
|
+
description: retrievalDefinitionBase.description,
|
|
196
|
+
inputSchema: jsonSchema(retrievalDefinitionBase.inputSchema),
|
|
197
|
+
execute: async (input: Record<string, unknown>) => {
|
|
198
|
+
const result = await executeRetrieval(input, sessionId);
|
|
199
|
+
return result.success ? result.result : { error: result.error };
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function wrapToolsWithOutputPolicy(
|
|
205
|
+
tools: Record<string, AiTool>,
|
|
206
|
+
context: Pick<ToolOutputPolicyContext, 'sessionId' | 'workspaceId'>,
|
|
207
|
+
): Record<string, AiTool> {
|
|
208
|
+
const wrapped: Record<string, AiTool> = {};
|
|
209
|
+
for (const [toolName, original] of Object.entries(tools)) {
|
|
210
|
+
const execute = original.execute;
|
|
211
|
+
if (toolName === options.retrievalToolName || typeof execute !== 'function' || outputPolicyWrappedTools.has(original)) {
|
|
212
|
+
wrapped[toolName] = original;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const policyWrapped = {
|
|
216
|
+
...original,
|
|
217
|
+
execute: async (...args: unknown[]) => {
|
|
218
|
+
const result = await (execute as (...executeArgs: unknown[]) => unknown).apply(original, args);
|
|
219
|
+
const executeOptions = args[1];
|
|
220
|
+
const toolCallId = executeOptions && typeof executeOptions === 'object'
|
|
221
|
+
? (executeOptions as { toolCallId?: unknown }).toolCallId
|
|
222
|
+
: undefined;
|
|
223
|
+
if (typeof toolCallId !== 'string' || !toolCallId) return result;
|
|
224
|
+
return applyToolOutputPolicy(result, {
|
|
225
|
+
...context,
|
|
226
|
+
toolCallId,
|
|
227
|
+
toolName,
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
} as AiTool;
|
|
231
|
+
outputPolicyWrappedTools.add(policyWrapped);
|
|
232
|
+
wrapped[toolName] = policyWrapped;
|
|
233
|
+
}
|
|
234
|
+
return wrapped;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function truncateToolResult(
|
|
238
|
+
result: unknown,
|
|
239
|
+
sessionId: string,
|
|
240
|
+
toolName: string,
|
|
241
|
+
outputDir: string = path.join(options.truncationTempDir, sessionId),
|
|
242
|
+
): unknown {
|
|
243
|
+
const serialized = JSON.stringify(result);
|
|
244
|
+
|
|
245
|
+
if (serialized.length <= options.truncationMaxChars) {
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const dir = outputDir;
|
|
250
|
+
mkdirSync(dir, { recursive: true });
|
|
251
|
+
|
|
252
|
+
const sanitizedToolName = toolName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
253
|
+
const filePath = `${dir}/${sanitizedToolName}-${Date.now()}.json`;
|
|
254
|
+
writeFileSync(filePath, serialized);
|
|
255
|
+
|
|
256
|
+
if (typeof result === 'string') {
|
|
257
|
+
const preview = result.slice(0, options.truncationPreviewChars);
|
|
258
|
+
const note = `\n\n[Result truncated: ${result.length} chars total. Full result persisted to ${filePath}. Use read-file tool to read it.]`;
|
|
259
|
+
return preview + note;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const truncatedJson = serialized.slice(0, options.truncationPreviewChars);
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const partialResult = JSON.parse(truncatedJson) as Record<string, unknown>;
|
|
266
|
+
const note = `[Result truncated: ${serialized.length} chars total. Full result persisted to ${filePath}. Use read-file tool to read it.]`;
|
|
267
|
+
|
|
268
|
+
if (partialResult && typeof partialResult === 'object' && !Array.isArray(partialResult)) {
|
|
269
|
+
if (typeof partialResult.content === 'string') {
|
|
270
|
+
partialResult.content = (partialResult.content as string).slice(0, options.truncationPreviewChars - note.length) + note;
|
|
271
|
+
} else {
|
|
272
|
+
partialResult._truncatedNote = note;
|
|
273
|
+
}
|
|
274
|
+
partialResult._persisted = true;
|
|
275
|
+
partialResult._filePath = filePath;
|
|
276
|
+
partialResult._originalSize = serialized.length;
|
|
277
|
+
return partialResult;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
...partialResult,
|
|
282
|
+
_persisted: true,
|
|
283
|
+
_filePath: filePath,
|
|
284
|
+
_originalSize: serialized.length,
|
|
285
|
+
};
|
|
286
|
+
} catch {
|
|
287
|
+
return {
|
|
288
|
+
content: truncatedJson + `\n\n[Result truncated: ${serialized.length} chars total. Full result persisted to ${filePath}. Use read-file tool to read it.]`,
|
|
289
|
+
_persisted: true,
|
|
290
|
+
_filePath: filePath,
|
|
291
|
+
_originalSize: serialized.length,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
id,
|
|
298
|
+
options,
|
|
299
|
+
applyToolOutputPolicy,
|
|
300
|
+
retrieveToolOutput,
|
|
301
|
+
buildRetrieveToolOutputAiTool,
|
|
302
|
+
createRetrieveToolOutputStandardTool,
|
|
303
|
+
wrapToolsWithOutputPolicy,
|
|
304
|
+
truncateToolResult,
|
|
305
|
+
} as ToolOutputArtifactService & { createRetrieveToolOutputStandardTool(): LoadedTool };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const scopedService = new AsyncLocalStorage<ToolOutputArtifactService>();
|
|
309
|
+
let processDefaultService: ToolOutputArtifactService | undefined;
|
|
310
|
+
|
|
311
|
+
/** Resolves the service seeded for the active agent scope, falling back to
|
|
312
|
+
* one lazily created process-default service for consumers that run outside
|
|
313
|
+
* a composed scope (the current Jean2 server path). The process default
|
|
314
|
+
* carries the exact pre-C6 constants. */
|
|
315
|
+
export function getToolOutputService(): ToolOutputArtifactService {
|
|
316
|
+
return scopedService.getStore()
|
|
317
|
+
?? (processDefaultService ??= createToolOutputService({ id: 'tool-output.process-default' }));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Seeds a service for the callback duration. `enterAgentScope` seeds the
|
|
321
|
+
* composed agent scope's service here. */
|
|
322
|
+
export function withToolOutputService<T>(service: ToolOutputArtifactService, callback: () => T): T {
|
|
323
|
+
return scopedService.run(service, callback);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Test-only reset of the lazily created process default. Exported from this
|
|
327
|
+
* module only; no package subpath re-exports it. */
|
|
328
|
+
export function resetDefaultToolOutputServiceForTests(): void {
|
|
329
|
+
processDefaultService = undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── Compatibility free functions over the scoped service ────────────────
|
|
333
|
+
|
|
334
|
+
/** NON-REPLACEABLE retrieval runtime: derives the caller session from the
|
|
335
|
+
* execution context and performs strict UUID/session-scoped storage
|
|
336
|
+
* retrieval itself. Provider advice (envelope/bounding) never controls
|
|
337
|
+
* which artifact or session can be read. */
|
|
338
|
+
export function retrieveToolOutputForSession(
|
|
339
|
+
sessionId: string,
|
|
340
|
+
input: { artifactId: string; offset?: number; limit?: number },
|
|
341
|
+
): Promise<ToolOutputArtifactPage | null> {
|
|
342
|
+
return getToolOutputArtifactPage(sessionId, input.artifactId, input.offset, input.limit);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Stable singleton factory over the non-replaceable retrieval runtime. */
|
|
346
|
+
export function getRetrieveToolOutputStandardTool(): LoadedTool {
|
|
347
|
+
return {
|
|
348
|
+
definition: retrievalDefinitionBase,
|
|
349
|
+
path: 'builtin:@capekai/core',
|
|
350
|
+
execute: async (input: Record<string, unknown>, context: ToolContext): Promise<ToolResult> => {
|
|
351
|
+
const page = await retrieveToolOutputForSession(context.sessionId, {
|
|
352
|
+
artifactId: String(input.artifactId ?? ''),
|
|
353
|
+
...(input.offset === undefined ? {} : { offset: Number(input.offset) }),
|
|
354
|
+
...(input.limit === undefined ? {} : { limit: Number(input.limit) }),
|
|
355
|
+
});
|
|
356
|
+
return page
|
|
357
|
+
? { success: true, result: page }
|
|
358
|
+
: { success: false, error: 'Tool output artifact not found' };
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Stable singleton used by the standard coding capability inventory. */
|
|
364
|
+
export const retrieveToolOutputStandardTool: LoadedTool = getRetrieveToolOutputStandardTool();
|
|
365
|
+
|
|
366
|
+
export async function applyToolOutputPolicy(result: unknown, context: ToolOutputPolicyContext): Promise<unknown> {
|
|
367
|
+
return getToolOutputService().applyToolOutputPolicy(result, context);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export async function retrieveToolOutput(
|
|
371
|
+
sessionId: string,
|
|
372
|
+
input: { artifactId: string; offset?: number; limit?: number },
|
|
373
|
+
): Promise<ToolOutputArtifactPage | null> {
|
|
374
|
+
return getToolOutputService().retrieveToolOutput(sessionId, input);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function wrapToolsWithOutputPolicy(
|
|
378
|
+
tools: Record<string, AiTool>,
|
|
379
|
+
context: Pick<ToolOutputPolicyContext, 'sessionId' | 'workspaceId'>,
|
|
380
|
+
): Record<string, AiTool> {
|
|
381
|
+
return getToolOutputService().wrapToolsWithOutputPolicy(tools, context);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function truncateToolResult(
|
|
385
|
+
result: unknown,
|
|
386
|
+
sessionId: string,
|
|
387
|
+
toolName: string,
|
|
388
|
+
outputDir?: string,
|
|
389
|
+
): unknown {
|
|
390
|
+
return getToolOutputService().truncateToolResult(result, sessionId, toolName, outputDir);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** The retrieval standard tool singleton lives in the compat forwarder for
|
|
394
|
+
* export-identity stability; the factory stays here and always uses the
|
|
395
|
+
* non-replaceable retrieval runtime. */
|
|
396
|
+
export function createRetrieveToolOutputStandardTool(): LoadedTool {
|
|
397
|
+
return getRetrieveToolOutputStandardTool();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function buildRetrieveToolOutputAiTool(sessionId: string): AiTool {
|
|
401
|
+
return getToolOutputService().buildRetrieveToolOutputAiTool(sessionId);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export type {
|
|
405
|
+
ToolOutputArtifactReference,
|
|
406
|
+
ToolOutputArtifactService,
|
|
407
|
+
ToolOutputFallback,
|
|
408
|
+
ToolOutputPolicyContext,
|
|
409
|
+
ToolOutputPolicyOptions,
|
|
410
|
+
} from './contracts';
|
package/src/tool.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@capekai/tool';
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { resolve, extname } from 'path';
|
|
2
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
3
|
+
import type {
|
|
4
|
+
ToolContext, ToolResult, LoadedTool, FileSystemApi, DirEntry, FileStat, EnvApi, ToolLogger, AskApi, LlmApi,
|
|
5
|
+
} from '@capekai/tool';
|
|
6
|
+
import type { WorkspaceCapability } from '../workspace/contracts';
|
|
7
|
+
|
|
8
|
+
function createThrowingStub<T>(name: string): T {
|
|
9
|
+
return new Proxy({}, {
|
|
10
|
+
get() {
|
|
11
|
+
throw new Error(`${name} API not available: this tool requires ${name} capabilities that were not provided`);
|
|
12
|
+
},
|
|
13
|
+
}) as T;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const EXTENSION_LANGUAGE_MAP: Record<string, string> = {
|
|
17
|
+
'.ts': 'typescript', '.tsx': 'typescript', '.js': 'javascript', '.jsx': 'javascript',
|
|
18
|
+
'.py': 'python', '.rb': 'ruby', '.go': 'go', '.rs': 'rust', '.java': 'java',
|
|
19
|
+
'.kt': 'kotlin', '.swift': 'swift', '.c': 'c', '.cpp': 'cpp', '.h': 'c', '.hpp': 'cpp',
|
|
20
|
+
'.cs': 'csharp', '.php': 'php', '.sh': 'bash', '.bash': 'bash', '.zsh': 'zsh', '.fish': 'fish', '.ps1': 'powershell',
|
|
21
|
+
'.html': 'html', '.css': 'css', '.scss': 'scss', '.less': 'less',
|
|
22
|
+
'.json': 'json', '.yaml': 'yaml', '.yml': 'yaml', '.toml': 'toml',
|
|
23
|
+
'.xml': 'xml', '.sql': 'sql', '.md': 'markdown', '.txt': 'text',
|
|
24
|
+
'.env': 'dotenv', '.gitignore': 'gitignore', '.dockerfile': 'dockerfile',
|
|
25
|
+
'.graphql': 'graphql', '.proto': 'protobuf',
|
|
26
|
+
'.svelte': 'svelte', '.vue': 'vue',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface ExecuteToolOptions {
|
|
30
|
+
tool: LoadedTool;
|
|
31
|
+
args: Record<string, unknown>;
|
|
32
|
+
workspace: WorkspaceCapability;
|
|
33
|
+
sessionId: string;
|
|
34
|
+
workspaceId?: string;
|
|
35
|
+
toolCallId?: string;
|
|
36
|
+
abortSignal?: AbortSignal;
|
|
37
|
+
timeout?: number;
|
|
38
|
+
createLlmApi?: (defaultModel?: string) => LlmApi;
|
|
39
|
+
createAskApi?: (toolCallId: string) => AskApi;
|
|
40
|
+
broadcastFn?: (event: { type: string; [key: string]: unknown }) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createFileSystemApi(workspace: WorkspaceCapability): FileSystemApi {
|
|
44
|
+
const { tempDir } = workspace;
|
|
45
|
+
|
|
46
|
+
const api: FileSystemApi = {
|
|
47
|
+
tempDir,
|
|
48
|
+
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Overloaded signature requires any for FileSystemApi compatibility
|
|
50
|
+
async readFile(path: string, encoding?: any): Promise<any> {
|
|
51
|
+
const resolved = api.resolve(path);
|
|
52
|
+
const fs = await import('fs/promises');
|
|
53
|
+
if (encoding) {
|
|
54
|
+
return fs.readFile(resolved, encoding);
|
|
55
|
+
}
|
|
56
|
+
const buffer = await fs.readFile(resolved);
|
|
57
|
+
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async writeFile(path: string, data: string | Uint8Array): Promise<void> {
|
|
61
|
+
const resolved = api.resolve(path);
|
|
62
|
+
const dir = resolve(resolved, '..');
|
|
63
|
+
const fs = await import('fs/promises');
|
|
64
|
+
await fs.mkdir(dir, { recursive: true });
|
|
65
|
+
await fs.writeFile(resolved, data);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
async appendFile(path: string, data: string | Uint8Array): Promise<void> {
|
|
69
|
+
const resolved = api.resolve(path);
|
|
70
|
+
const fs = await import('fs/promises');
|
|
71
|
+
await fs.appendFile(resolved, data);
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async readDir(path: string): Promise<DirEntry[]> {
|
|
75
|
+
const resolved = api.resolve(path);
|
|
76
|
+
const fs = await import('fs/promises');
|
|
77
|
+
const entries = await fs.readdir(resolved, { withFileTypes: true });
|
|
78
|
+
return entries.map(e => ({
|
|
79
|
+
name: e.name,
|
|
80
|
+
isDirectory: e.isDirectory(),
|
|
81
|
+
isFile: e.isFile(),
|
|
82
|
+
}));
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
async exists(path: string): Promise<boolean> {
|
|
86
|
+
const resolved = api.resolve(path);
|
|
87
|
+
return existsSync(resolved);
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
async stat(path: string): Promise<FileStat> {
|
|
91
|
+
const resolved = api.resolve(path);
|
|
92
|
+
const fs = await import('fs/promises');
|
|
93
|
+
const stat = await fs.stat(resolved);
|
|
94
|
+
return {
|
|
95
|
+
size: stat.size,
|
|
96
|
+
isDirectory: stat.isDirectory(),
|
|
97
|
+
isFile: stat.isFile(),
|
|
98
|
+
modifiedAt: stat.mtime,
|
|
99
|
+
createdAt: stat.birthtime,
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
async mkdir(path: string, options?: { recursive?: boolean }): Promise<void> {
|
|
104
|
+
const resolved = api.resolve(path);
|
|
105
|
+
const fs = await import('fs/promises');
|
|
106
|
+
await fs.mkdir(resolved, options);
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
async rm(path: string, options?: { recursive?: boolean }): Promise<void> {
|
|
110
|
+
const resolved = api.resolve(path);
|
|
111
|
+
const fs = await import('fs/promises');
|
|
112
|
+
await fs.rm(resolved, options);
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async rename(oldPath: string, newPath: string): Promise<void> {
|
|
116
|
+
const fs = await import('fs/promises');
|
|
117
|
+
await fs.rename(api.resolve(oldPath), api.resolve(newPath));
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
resolve(path: string): string {
|
|
121
|
+
return workspace.resolvePath(path);
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
detectLanguage(path: string): string {
|
|
125
|
+
const ext = extname(path);
|
|
126
|
+
return EXTENSION_LANGUAGE_MAP[ext] || 'text';
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
mkdirSync(tempDir, { recursive: true });
|
|
131
|
+
|
|
132
|
+
return api;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function createEnvApi(workspace: WorkspaceCapability): EnvApi {
|
|
136
|
+
return {
|
|
137
|
+
get(key: string): string | undefined {
|
|
138
|
+
return workspace.getEnvironmentValue(key);
|
|
139
|
+
},
|
|
140
|
+
require(key: string): string {
|
|
141
|
+
const value = workspace.getEnvironmentValue(key);
|
|
142
|
+
if (!value) {
|
|
143
|
+
throw new Error(`Required environment variable not set: ${key}`);
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function createLogger(toolName: string, sessionId: string): ToolLogger {
|
|
151
|
+
const prefix = `[tool:${toolName}:${sessionId.slice(0, 8)}]`;
|
|
152
|
+
return {
|
|
153
|
+
debug(message: string, data?: Record<string, unknown>): void {
|
|
154
|
+
console.debug(prefix, message, data || '');
|
|
155
|
+
},
|
|
156
|
+
info(message: string, data?: Record<string, unknown>): void {
|
|
157
|
+
console.info(prefix, message, data || '');
|
|
158
|
+
},
|
|
159
|
+
warn(message: string, data?: Record<string, unknown>): void {
|
|
160
|
+
console.warn(prefix, message, data || '');
|
|
161
|
+
},
|
|
162
|
+
error(message: string, data?: Record<string, unknown>): void {
|
|
163
|
+
console.error(prefix, message, data || '');
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function executeTool(options: ExecuteToolOptions): Promise<ToolResult> {
|
|
169
|
+
const {
|
|
170
|
+
tool,
|
|
171
|
+
args,
|
|
172
|
+
workspace,
|
|
173
|
+
sessionId,
|
|
174
|
+
abortSignal,
|
|
175
|
+
timeout = tool.definition.timeout ?? 30000,
|
|
176
|
+
createLlmApi,
|
|
177
|
+
createAskApi,
|
|
178
|
+
} = options;
|
|
179
|
+
|
|
180
|
+
const toolAbortController = new AbortController();
|
|
181
|
+
const forwardAbort = (): void => {
|
|
182
|
+
toolAbortController.abort(abortSignal?.reason);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (abortSignal?.aborted) {
|
|
186
|
+
forwardAbort();
|
|
187
|
+
} else {
|
|
188
|
+
abortSignal?.addEventListener('abort', forwardAbort, { once: true });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const ctx: ToolContext = {
|
|
192
|
+
sessionId,
|
|
193
|
+
workspacePath: workspace.effectiveRoot,
|
|
194
|
+
workspaceId: options.workspaceId,
|
|
195
|
+
abortSignal: toolAbortController.signal,
|
|
196
|
+
allowedPaths: workspace.allowedRoots,
|
|
197
|
+
fs: createFileSystemApi(workspace),
|
|
198
|
+
llm: createLlmApi ? createLlmApi() : createThrowingStub<LlmApi>('llm'),
|
|
199
|
+
ask: createAskApi ? createAskApi(options.toolCallId ?? '') : createThrowingStub<AskApi>('ask'),
|
|
200
|
+
env: createEnvApi(workspace),
|
|
201
|
+
logger: createLogger(tool.definition.name, sessionId),
|
|
202
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
203
|
+
resolvePath: workspace.resolvePath,
|
|
204
|
+
isWithinWorkspace: workspace.isWithinWorkspace,
|
|
205
|
+
isSensitivePath: workspace.isSensitivePath,
|
|
206
|
+
isBlockedPath: workspace.isBlockedPath,
|
|
207
|
+
addWorkspacePath: workspace.addWorkspacePath,
|
|
208
|
+
removeWorkspacePath: workspace.removeWorkspacePath,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const executePromise = tool.execute(args, ctx);
|
|
212
|
+
|
|
213
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
214
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
215
|
+
timeoutId = setTimeout(() => {
|
|
216
|
+
toolAbortController.abort(new Error(`Tool execution timed out after ${timeout}ms`));
|
|
217
|
+
reject(new Error(`Tool execution timed out after ${timeout}ms`));
|
|
218
|
+
}, timeout);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Abort must settle the race even when the tool ignores its abort signal
|
|
222
|
+
// (for example a tool blocked on ctx.ask()). Promise.race keeps handlers
|
|
223
|
+
// on every promise, so a late rejection after settlement cannot surface as
|
|
224
|
+
// an unhandled rejection.
|
|
225
|
+
const abortPromise = abortSignal
|
|
226
|
+
? new Promise<never>((_, reject) => {
|
|
227
|
+
if (abortSignal.aborted) {
|
|
228
|
+
reject(new Error('Tool execution interrupted'));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
abortSignal.addEventListener('abort', () => reject(new Error('Tool execution interrupted')), { once: true });
|
|
232
|
+
})
|
|
233
|
+
: null;
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const result = await Promise.race(
|
|
237
|
+
abortPromise ? [executePromise, timeoutPromise, abortPromise] : [executePromise, timeoutPromise],
|
|
238
|
+
);
|
|
239
|
+
return result;
|
|
240
|
+
} catch (err: unknown) {
|
|
241
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
242
|
+
|
|
243
|
+
if (abortSignal?.aborted) {
|
|
244
|
+
return {
|
|
245
|
+
success: false,
|
|
246
|
+
error: 'Tool execution interrupted',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
success: false,
|
|
252
|
+
error: message,
|
|
253
|
+
};
|
|
254
|
+
} finally {
|
|
255
|
+
abortSignal?.removeEventListener('abort', forwardAbort);
|
|
256
|
+
clearTimeout(timeoutId);
|
|
257
|
+
}
|
|
258
|
+
}
|