@galaxy-stack/ai-coder-core 0.3.0-alpha.1 → 0.3.0-alpha.7
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/CHANGELOG.d/2026-09-19-research-rejection-evidence.md +15 -0
- package/CHANGELOG.d/2026-09-21-evidence-missing-remediation.md +15 -0
- package/CHANGELOG.d/2026-09-21-observation-nudge-context.md +12 -0
- package/CHANGELOG.d/2026-09-22-model-retry-budget.md +18 -0
- package/CHANGELOG.d/2026-09-22-no-progress-recovery-turn.md +19 -0
- package/CHANGELOG.d/2026-09-23-agent-platform.md +5 -0
- package/CHANGELOG.d/2026-09-23-conservative-output-reserve.md +16 -0
- package/CHANGELOG.d/2026-09-24-approval-workspace-review.md +7 -0
- package/CHANGELOG.d/2026-09-24-incremental-ollama.md +5 -0
- package/CHANGELOG.md +71 -1
- package/README.md +78 -4
- package/dist/adapters/node/config/manual-provider-config.d.ts +19 -0
- package/dist/adapters/node/config/manual-provider-config.d.ts.map +1 -0
- package/dist/adapters/node/config/manual-provider-config.js +91 -0
- package/dist/adapters/node/config/manual-provider-config.js.map +1 -0
- package/dist/adapters/node/host/command-containment.d.ts +45 -0
- package/dist/adapters/node/host/command-containment.d.ts.map +1 -0
- package/dist/adapters/node/host/command-containment.js +580 -0
- package/dist/adapters/node/host/command-containment.js.map +1 -0
- package/dist/adapters/node/host/content-hash.d.ts +2 -0
- package/dist/adapters/node/host/content-hash.d.ts.map +1 -0
- package/dist/adapters/node/host/content-hash.js +5 -0
- package/dist/adapters/node/host/content-hash.js.map +1 -0
- package/dist/adapters/node/host/file-run-store.d.ts +29 -0
- package/dist/adapters/node/host/file-run-store.d.ts.map +1 -0
- package/dist/adapters/node/host/file-run-store.js +94 -0
- package/dist/adapters/node/host/file-run-store.js.map +1 -0
- package/dist/adapters/node/host/host-environment.d.ts +9 -0
- package/dist/adapters/node/host/host-environment.d.ts.map +1 -0
- package/dist/adapters/node/host/host-environment.js +29 -0
- package/dist/adapters/node/host/host-environment.js.map +1 -0
- package/dist/adapters/node/host/node-command-port.d.ts +21 -0
- package/dist/adapters/node/host/node-command-port.d.ts.map +1 -0
- package/dist/adapters/node/host/node-command-port.js +281 -0
- package/dist/adapters/node/host/node-command-port.js.map +1 -0
- package/dist/adapters/node/host/node-workspace-evidence-verifier.d.ts +22 -0
- package/dist/adapters/node/host/node-workspace-evidence-verifier.d.ts.map +1 -0
- package/dist/adapters/node/host/node-workspace-evidence-verifier.js +384 -0
- package/dist/adapters/node/host/node-workspace-evidence-verifier.js.map +1 -0
- package/dist/adapters/node/host/node-workspace-port.d.ts +87 -0
- package/dist/adapters/node/host/node-workspace-port.d.ts.map +1 -0
- package/dist/adapters/node/host/node-workspace-port.js +860 -0
- package/dist/adapters/node/host/node-workspace-port.js.map +1 -0
- package/dist/adapters/node/host/node-workspace-snapshot.d.ts +61 -0
- package/dist/adapters/node/host/node-workspace-snapshot.d.ts.map +1 -0
- package/dist/adapters/node/host/node-workspace-snapshot.js +281 -0
- package/dist/adapters/node/host/node-workspace-snapshot.js.map +1 -0
- package/dist/adapters/node/host/path-scope.d.ts +17 -0
- package/dist/adapters/node/host/path-scope.d.ts.map +1 -0
- package/dist/adapters/node/host/path-scope.js +89 -0
- package/dist/adapters/node/host/path-scope.js.map +1 -0
- package/dist/adapters/node/host/project-tools.d.ts +42 -0
- package/dist/adapters/node/host/project-tools.d.ts.map +1 -0
- package/dist/adapters/node/host/project-tools.js +362 -0
- package/dist/adapters/node/host/project-tools.js.map +1 -0
- package/dist/adapters/node/mcp/mcp-client.d.ts +206 -0
- package/dist/adapters/node/mcp/mcp-client.d.ts.map +1 -0
- package/dist/adapters/node/mcp/mcp-client.js +123 -0
- package/dist/adapters/node/mcp/mcp-client.js.map +1 -0
- package/dist/adapters/node/mcp/oauth-provider.d.ts +39 -0
- package/dist/adapters/node/mcp/oauth-provider.d.ts.map +1 -0
- package/dist/adapters/node/mcp/oauth-provider.js +168 -0
- package/dist/adapters/node/mcp/oauth-provider.js.map +1 -0
- package/dist/adapters/node/memory/sqlite-memory.d.ts +22 -0
- package/dist/adapters/node/memory/sqlite-memory.d.ts.map +1 -0
- package/dist/adapters/node/memory/sqlite-memory.js +83 -0
- package/dist/adapters/node/memory/sqlite-memory.js.map +1 -0
- package/dist/adapters/node/provider/ollama-chat-stream.d.ts +17 -0
- package/dist/adapters/node/provider/ollama-chat-stream.d.ts.map +1 -0
- package/dist/adapters/node/provider/ollama-chat-stream.js +176 -0
- package/dist/adapters/node/provider/ollama-chat-stream.js.map +1 -0
- package/dist/adapters/node/provider/ollama-coding-model.d.ts +89 -0
- package/dist/adapters/node/provider/ollama-coding-model.d.ts.map +1 -0
- package/dist/adapters/node/provider/ollama-coding-model.js +464 -0
- package/dist/adapters/node/provider/ollama-coding-model.js.map +1 -0
- package/dist/adapters/node/provider/ollama-research-port.d.ts +32 -0
- package/dist/adapters/node/provider/ollama-research-port.d.ts.map +1 -0
- package/dist/adapters/node/provider/ollama-research-port.js +346 -0
- package/dist/adapters/node/provider/ollama-research-port.js.map +1 -0
- package/dist/adapters/node/skills/directory-skills.d.ts +12 -0
- package/dist/adapters/node/skills/directory-skills.d.ts.map +1 -0
- package/dist/adapters/node/skills/directory-skills.js +90 -0
- package/dist/adapters/node/skills/directory-skills.js.map +1 -0
- package/dist/adapters/node/tools/tool-executor.d.ts +91 -0
- package/dist/adapters/node/tools/tool-executor.d.ts.map +1 -0
- package/dist/adapters/node/tools/tool-executor.js +948 -0
- package/dist/adapters/node/tools/tool-executor.js.map +1 -0
- package/dist/adapters/node/tools/workspace-review.d.ts +13 -0
- package/dist/adapters/node/tools/workspace-review.d.ts.map +1 -0
- package/dist/adapters/node/tools/workspace-review.js +81 -0
- package/dist/adapters/node/tools/workspace-review.js.map +1 -0
- package/dist/agent/index.d.ts +79 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +97 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/approval/approval-policy.d.ts.map +1 -1
- package/dist/approval/approval-policy.js +4 -1
- package/dist/approval/approval-policy.js.map +1 -1
- package/dist/context/context-profile.d.ts +1 -1
- package/dist/context/context-profile.js +2 -2
- package/dist/prompt/prompt-assembler.d.ts +1 -0
- package/dist/prompt/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt/prompt-assembler.js +16 -2
- package/dist/prompt/prompt-assembler.js.map +1 -1
- package/dist/runtime/run-controller.d.ts.map +1 -1
- package/dist/runtime/run-controller.js +126 -23
- package/dist/runtime/run-controller.js.map +1 -1
- package/dist/runtime/runtime-types.d.ts +19 -0
- package/dist/runtime/runtime-types.d.ts.map +1 -1
- package/docs/AGENT_PLATFORM.md +91 -0
- package/docs/GALAXY_AGENT_PLATFORM_PLAN.md +318 -0
- package/docs/GALAXY_AGENT_PLATFORM_TODO.md +158 -0
- package/package.json +19 -5
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
import { AiCoderTokenEstimator, AiCoderToolRegistry, createAiCoderCoreToolEffectMetadata, createAiCoderApprovalPolicy, createAiCoderToolRegistrySnapshot, validateAiCoderJsonSchema, } from "../../../index.js";
|
|
2
|
+
import { posix } from "node:path";
|
|
3
|
+
import { sha256Text } from "../host/content-hash.js";
|
|
4
|
+
import { DEPENDENCY_AWARE_WORKSPACE_SNAPSHOT_OPTIONS, diffNodeWorkspaceSnapshots, NodeWorkspaceSnapshotter, } from "../host/node-workspace-snapshot.js";
|
|
5
|
+
import { detectProject, validateProject } from "../host/project-tools.js";
|
|
6
|
+
export class ContractToolError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
retryable;
|
|
9
|
+
suggestedAction;
|
|
10
|
+
constructor(code, message, retryable = false, suggestedAction) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.retryable = retryable;
|
|
14
|
+
this.suggestedAction = suggestedAction;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export const LAB_AVAILABLE_TOOL_IDS = Object.freeze([
|
|
18
|
+
"catalog.search",
|
|
19
|
+
"task.checkpoint",
|
|
20
|
+
"workspace.list",
|
|
21
|
+
"workspace.glob",
|
|
22
|
+
"workspace.grep",
|
|
23
|
+
"workspace.read",
|
|
24
|
+
"workspace.edit",
|
|
25
|
+
"workspace.write",
|
|
26
|
+
"command.run",
|
|
27
|
+
"project.detect",
|
|
28
|
+
"project.validate",
|
|
29
|
+
"git.exec",
|
|
30
|
+
]);
|
|
31
|
+
export const LAB_GRANTED_PERMISSIONS = Object.freeze([
|
|
32
|
+
"core.storage",
|
|
33
|
+
"fs.workspace",
|
|
34
|
+
"process.execute",
|
|
35
|
+
]);
|
|
36
|
+
export const LAB_RESEARCH_TOOL_IDS = Object.freeze(["research.search", "research.fetch"]);
|
|
37
|
+
export const LAB_OPTIONAL_CONTRACT_TOOL_IDS = Object.freeze([
|
|
38
|
+
"research.fetch",
|
|
39
|
+
"research.search",
|
|
40
|
+
"command.session",
|
|
41
|
+
"preview.manage",
|
|
42
|
+
"perception.analyze",
|
|
43
|
+
"artifact.create",
|
|
44
|
+
"artifact.list",
|
|
45
|
+
"artifact.read",
|
|
46
|
+
"user.ask",
|
|
47
|
+
]);
|
|
48
|
+
export const LAB_FULL_CONTRACT_TOOL_IDS = Object.freeze([
|
|
49
|
+
...LAB_AVAILABLE_TOOL_IDS,
|
|
50
|
+
...LAB_OPTIONAL_CONTRACT_TOOL_IDS,
|
|
51
|
+
]);
|
|
52
|
+
export const LAB_FULL_CONTRACT_PERMISSIONS = Object.freeze([
|
|
53
|
+
...LAB_GRANTED_PERMISSIONS,
|
|
54
|
+
"core.artifacts",
|
|
55
|
+
"network.outbound",
|
|
56
|
+
"preview.local",
|
|
57
|
+
"user.interaction",
|
|
58
|
+
]);
|
|
59
|
+
const RECOVERABLE_READ_ERROR_CODES = new Set([
|
|
60
|
+
"ALREADY_EXISTS", "CANCELED", "CONFLICT", "DEADLINE_EXCEEDED", "INVALID_INPUT",
|
|
61
|
+
"IO_ERROR", "LIMIT_EXCEEDED", "NOT_FOUND", "PERMISSION_DENIED", "PRECONDITION_FAILED",
|
|
62
|
+
"UNAVAILABLE", "UNSUPPORTED",
|
|
63
|
+
]);
|
|
64
|
+
function recoverableReadError(error) {
|
|
65
|
+
if (!error || typeof error !== "object" || !("code" in error) || typeof error.code !== "string")
|
|
66
|
+
return null;
|
|
67
|
+
const code = error.code;
|
|
68
|
+
if (!RECOVERABLE_READ_ERROR_CODES.has(code))
|
|
69
|
+
return null;
|
|
70
|
+
return Object.freeze({
|
|
71
|
+
code,
|
|
72
|
+
message: error instanceof Error ? error.message : String(error),
|
|
73
|
+
retryable: code === "IO_ERROR" || code === "UNAVAILABLE",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
class ToolAdapterError extends Error {
|
|
77
|
+
code;
|
|
78
|
+
retryable;
|
|
79
|
+
suggestedAction;
|
|
80
|
+
status;
|
|
81
|
+
constructor(code, message, retryable = false, suggestedAction, status) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.code = code;
|
|
84
|
+
this.retryable = retryable;
|
|
85
|
+
this.suggestedAction = suggestedAction;
|
|
86
|
+
this.status = status;
|
|
87
|
+
this.name = "ToolAdapterError";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
class UnknownSideEffectOutcomeError extends Error {
|
|
91
|
+
constructor(message, options) {
|
|
92
|
+
super(message, options);
|
|
93
|
+
this.name = "UnknownSideEffectOutcomeError";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function assertTargetMutation(canonicalToolId, mutations, expected) {
|
|
97
|
+
const target = mutations.writes.find((write) => write.path === expected.path);
|
|
98
|
+
const expectedBeforeKind = expected.beforeHash === null ? "missing" : "file";
|
|
99
|
+
if (target === undefined
|
|
100
|
+
|| target.beforeKind !== expectedBeforeKind
|
|
101
|
+
|| target.afterKind !== "file"
|
|
102
|
+
|| target.beforeHash !== expected.beforeHash
|
|
103
|
+
|| target.afterHash !== expected.afterHash) {
|
|
104
|
+
throw new UnknownSideEffectOutcomeError(`${canonicalToolId} returned success, but its target mutation does not match the independent workspace snapshot.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function structuredMutationFailure(canonicalToolId, mutations, error) {
|
|
108
|
+
if (mutations.observedMutations.length > 0) {
|
|
109
|
+
throw new UnknownSideEffectOutcomeError(`${canonicalToolId} returned ${error.code} after changing ${mutations.observedMutations.length} workspace path(s); side-effect outcome is unknown.`);
|
|
110
|
+
}
|
|
111
|
+
throw new ToolAdapterError(error.code, error.message, error.retryable, error.suggestedAction);
|
|
112
|
+
}
|
|
113
|
+
const MAX_REPORTED_DERIVED_MUTATIONS = 32;
|
|
114
|
+
function derivedMutationObservation(mutations) {
|
|
115
|
+
const paths = mutations.observedMutations
|
|
116
|
+
.filter((mutation) => mutation.evidenceClass === "derived")
|
|
117
|
+
.map((mutation) => mutation.path);
|
|
118
|
+
if (paths.length === 0)
|
|
119
|
+
return undefined;
|
|
120
|
+
return Object.freeze({
|
|
121
|
+
count: paths.length,
|
|
122
|
+
paths: Object.freeze(paths.slice(0, MAX_REPORTED_DERIVED_MUTATIONS)),
|
|
123
|
+
truncated: paths.length > MAX_REPORTED_DERIVED_MUTATIONS,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function mutationEffects(mutations) {
|
|
127
|
+
if (mutations.observedMutations.length === 0)
|
|
128
|
+
return undefined;
|
|
129
|
+
return Object.freeze({
|
|
130
|
+
stateVersion: mutations.stateVersion,
|
|
131
|
+
...(mutations.writes.length === 0 ? {} : { writes: mutations.writes }),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function portData(result) {
|
|
135
|
+
if (result.ok)
|
|
136
|
+
return result.data;
|
|
137
|
+
const error = result.error;
|
|
138
|
+
const status = error.details && typeof error.details === "object"
|
|
139
|
+
&& Number.isInteger(error.details.status)
|
|
140
|
+
? error.details.status
|
|
141
|
+
: undefined;
|
|
142
|
+
throw new ToolAdapterError(error.code, error.message, error.retryable, error.suggestedAction, status);
|
|
143
|
+
}
|
|
144
|
+
function stringArgument(argumentsValue, name, fallback) {
|
|
145
|
+
const value = argumentsValue[name];
|
|
146
|
+
if (typeof value === "string")
|
|
147
|
+
return value;
|
|
148
|
+
if (fallback !== undefined)
|
|
149
|
+
return fallback;
|
|
150
|
+
throw new Error(`Missing string argument '${name}'.`);
|
|
151
|
+
}
|
|
152
|
+
function optionalString(argumentsValue, name) {
|
|
153
|
+
const value = argumentsValue[name];
|
|
154
|
+
return typeof value === "string" ? value : undefined;
|
|
155
|
+
}
|
|
156
|
+
function optionalNumber(argumentsValue, name) {
|
|
157
|
+
const value = argumentsValue[name];
|
|
158
|
+
return typeof value === "number" ? value : undefined;
|
|
159
|
+
}
|
|
160
|
+
function optionalBoolean(argumentsValue, name) {
|
|
161
|
+
const value = argumentsValue[name];
|
|
162
|
+
return typeof value === "boolean" ? value : undefined;
|
|
163
|
+
}
|
|
164
|
+
function stringArray(argumentsValue, name) {
|
|
165
|
+
const value = argumentsValue[name];
|
|
166
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
167
|
+
}
|
|
168
|
+
function errorResult(call, canonicalToolId, code, message, retryable = false, effects, suggestedAction, status) {
|
|
169
|
+
const error = Object.freeze({
|
|
170
|
+
code,
|
|
171
|
+
message,
|
|
172
|
+
retryable,
|
|
173
|
+
...(status === undefined ? {} : { status }),
|
|
174
|
+
...(suggestedAction === undefined ? {} : { suggestedAction }),
|
|
175
|
+
});
|
|
176
|
+
return Object.freeze({
|
|
177
|
+
canonicalToolId,
|
|
178
|
+
content: JSON.stringify({ ok: false, error, toolCallId: call.toolCallId }),
|
|
179
|
+
error,
|
|
180
|
+
ok: false,
|
|
181
|
+
summary: message,
|
|
182
|
+
trust: "trusted",
|
|
183
|
+
...(effects === undefined ? {} : { effects, effectsAuthority: "host" }),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function normalizedProjectPath(value) {
|
|
187
|
+
const normalized = posix.normalize(value.replaceAll("\\", "/"));
|
|
188
|
+
const relative = normalized.replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
189
|
+
return relative.length === 0 ? "." : relative;
|
|
190
|
+
}
|
|
191
|
+
function gitPathArguments(paths, offset = 0) {
|
|
192
|
+
const environment = {};
|
|
193
|
+
const references = paths.map((path, index) => {
|
|
194
|
+
if (path.includes("\0"))
|
|
195
|
+
throw new ToolAdapterError("INVALID_INPUT", "Git paths must not contain a null byte.");
|
|
196
|
+
const name = `GALAXY_GIT_PATH_${offset + index}`;
|
|
197
|
+
environment[name] = path;
|
|
198
|
+
// Environment expansion happens once, so shell metacharacters in a path
|
|
199
|
+
// remain data inside the surrounding quotes instead of becoming syntax.
|
|
200
|
+
return process.platform === "win32" ? `"%${name}%"` : `"\${${name}}"`;
|
|
201
|
+
});
|
|
202
|
+
const commandArguments = references.join(" ");
|
|
203
|
+
return Object.freeze({
|
|
204
|
+
arguments: commandArguments,
|
|
205
|
+
environment: Object.freeze(environment),
|
|
206
|
+
suffix: commandArguments.length === 0 ? "" : ` -- ${commandArguments}`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
function gitOutputWouldBeBound(descriptor, action, stdout, stderr) {
|
|
210
|
+
return Buffer.byteLength(JSON.stringify({ action, stdout, stderr, exitCode: 0, truncated: false }), "utf8")
|
|
211
|
+
> descriptor.maxOutputBytes;
|
|
212
|
+
}
|
|
213
|
+
const MAX_UNTRACKED_DIFF_FILES = 256;
|
|
214
|
+
const NULL_DEVICE = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
215
|
+
function provenance(source, trust, contentHash) {
|
|
216
|
+
return Object.freeze({
|
|
217
|
+
source,
|
|
218
|
+
trust,
|
|
219
|
+
retrievedAt: "1970-01-01T00:00:00.000Z",
|
|
220
|
+
...(contentHash === undefined ? {} : { contentHash }),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
const RESEARCH_OUTPUT_ESTIMATOR = new AiCoderTokenEstimator();
|
|
224
|
+
function researchOutputFits(output, descriptor) {
|
|
225
|
+
const serialized = JSON.stringify(output);
|
|
226
|
+
// Core's estimator calibrates as high as 2. Reserve that ceiling here so its
|
|
227
|
+
// generic head/tail limiter never cuts a successful research JSON envelope.
|
|
228
|
+
return Buffer.byteLength(serialized, "utf8") <= descriptor.maxOutputBytes
|
|
229
|
+
&& RESEARCH_OUTPUT_ESTIMATOR.estimateText(serialized, "json") * 2 <= descriptor.maxOutputTokens;
|
|
230
|
+
}
|
|
231
|
+
function shortenResearchText(value) {
|
|
232
|
+
const codePoints = Array.from(value);
|
|
233
|
+
return codePoints.slice(0, Math.floor(codePoints.length * 0.75)).join("");
|
|
234
|
+
}
|
|
235
|
+
function durableResearchSummary(value, maxCharacters) {
|
|
236
|
+
const normalized = value.replace(/\s+/gu, " ").trim();
|
|
237
|
+
const bounded = Array.from(normalized).slice(0, maxCharacters).join("");
|
|
238
|
+
return bounded || "Source returned no additional excerpt; retain its URL and provenance only.";
|
|
239
|
+
}
|
|
240
|
+
function researchCacheUrl(value) {
|
|
241
|
+
try {
|
|
242
|
+
const url = new URL(value);
|
|
243
|
+
url.hash = "";
|
|
244
|
+
return url.href;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
export class NodeToolExecutor {
|
|
251
|
+
options;
|
|
252
|
+
approvalPolicy;
|
|
253
|
+
contractTools;
|
|
254
|
+
contractToolsEnabled;
|
|
255
|
+
grantedPermissions;
|
|
256
|
+
idempotencyCache = new Map();
|
|
257
|
+
researchFetchCache = new Map();
|
|
258
|
+
researchSearchCache = new Map();
|
|
259
|
+
taskCheckpointStore;
|
|
260
|
+
registry = null;
|
|
261
|
+
async mutationBaseline(context) {
|
|
262
|
+
try {
|
|
263
|
+
const snapshotter = await NodeWorkspaceSnapshotter.create(context.workspaceRoot, this.options.workspaceSnapshot ?? DEPENDENCY_AWARE_WORKSPACE_SNAPSHOT_OPTIONS);
|
|
264
|
+
return Object.freeze({ before: await snapshotter.capture(context), snapshotter });
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
const known = recoverableReadError(error);
|
|
268
|
+
throw new ToolAdapterError(known?.code ?? "IO_ERROR", `Unable to establish the workspace mutation baseline before tool side effects: ${error instanceof Error ? error.message : String(error)}`, known?.retryable ?? false);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
constructor(options) {
|
|
272
|
+
this.options = options;
|
|
273
|
+
this.contractToolsEnabled = options.contractTools !== undefined;
|
|
274
|
+
this.contractTools = options.contractTools ?? null;
|
|
275
|
+
this.grantedPermissions = new Set([
|
|
276
|
+
...(this.contractToolsEnabled ? LAB_FULL_CONTRACT_PERMISSIONS : LAB_GRANTED_PERMISSIONS),
|
|
277
|
+
...(options.research === undefined ? [] : ["network.outbound"]),
|
|
278
|
+
]);
|
|
279
|
+
this.taskCheckpointStore = options.taskCheckpointStore ?? new Map();
|
|
280
|
+
this.approvalPolicy = createAiCoderApprovalPolicy({
|
|
281
|
+
approvalProfile: options.approvalProfile ?? "balanced",
|
|
282
|
+
grantedPermissions: this.grantedPermissions,
|
|
283
|
+
...(options.approval === undefined ? {} : { approvalCallback: options.approval }),
|
|
284
|
+
...(options.approvalTimeoutMs === undefined ? {} : { approvalTimeoutMs: options.approvalTimeoutMs }),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
async getToolSet(context) {
|
|
288
|
+
const registry = this.ensureRegistry(context);
|
|
289
|
+
const metadata = createAiCoderCoreToolEffectMetadata(registry.activeDescriptors);
|
|
290
|
+
return Object.freeze({
|
|
291
|
+
canonicalToolIds: metadata.canonicalToolIds,
|
|
292
|
+
definitions: registry.definitions,
|
|
293
|
+
effectCapabilities: metadata.effectCapabilities,
|
|
294
|
+
snapshotHash: registry.activeHash,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
async restoreToolSet(input, context) {
|
|
298
|
+
const registry = this.ensureRegistry(context);
|
|
299
|
+
for (const name of input.names) {
|
|
300
|
+
const descriptor = registry.snapshot.descriptors.find((tool) => tool.modelName === name);
|
|
301
|
+
if (descriptor === undefined || !registry.activate(descriptor.id)) {
|
|
302
|
+
throw new Error(`Checkpoint references unavailable tool '${name}'.`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (registry.activeHash !== input.snapshotHash) {
|
|
306
|
+
throw new Error(`Checkpoint tool snapshot mismatch: expected ${input.snapshotHash}, received ${registry.activeHash}.`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async execute(call, context) {
|
|
310
|
+
const registry = this.ensureRegistry(context);
|
|
311
|
+
const descriptor = registry.resolveModelName(call.name);
|
|
312
|
+
if (descriptor === null) {
|
|
313
|
+
return errorResult(call, call.name, "UNKNOWN_TOOL", `Tool '${call.name}' is not active in this turn.`);
|
|
314
|
+
}
|
|
315
|
+
const argumentsValidation = validateAiCoderJsonSchema(descriptor.inputSchema, call.arguments);
|
|
316
|
+
if (!argumentsValidation.valid) {
|
|
317
|
+
return errorResult(call, descriptor.id, "INVALID_TOOL_ARGUMENTS", argumentsValidation.errors.join(" "));
|
|
318
|
+
}
|
|
319
|
+
const cached = descriptor.idempotency === "with_key" ? this.idempotencyCache.get(context.idempotencyKey) : undefined;
|
|
320
|
+
if (cached !== undefined)
|
|
321
|
+
return cached;
|
|
322
|
+
const policy = await this.approvalPolicy(descriptor, call.arguments, context);
|
|
323
|
+
const approvalEffect = policy.allowed
|
|
324
|
+
? policy.decision === "approved_by_host" ? "granted" : "not_required"
|
|
325
|
+
: "denied";
|
|
326
|
+
if (!policy.allowed) {
|
|
327
|
+
return errorResult(call, descriptor.id, policy.decision.toUpperCase(), policy.reason, false, Object.freeze({ approval: "denied" }));
|
|
328
|
+
}
|
|
329
|
+
let dispatched;
|
|
330
|
+
try {
|
|
331
|
+
dispatched = await this.dispatch(descriptor, call.arguments, context);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
if (error instanceof UnknownSideEffectOutcomeError)
|
|
335
|
+
throw error;
|
|
336
|
+
if (error instanceof ToolAdapterError) {
|
|
337
|
+
return errorResult(call, descriptor.id, error.code, error.message, error.retryable, undefined, error.suggestedAction, error.status);
|
|
338
|
+
}
|
|
339
|
+
const readFailure = descriptor.mutability === "read" ? recoverableReadError(error) : null;
|
|
340
|
+
if (readFailure !== null) {
|
|
341
|
+
return errorResult(call, descriptor.id, readFailure.code, readFailure.message, readFailure.retryable);
|
|
342
|
+
}
|
|
343
|
+
throw new UnknownSideEffectOutcomeError(`Adapter failed after dispatching ${descriptor.id}; side-effect outcome is unknown: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
344
|
+
}
|
|
345
|
+
const outputValidation = validateAiCoderJsonSchema(descriptor.outputSchema, dispatched.output);
|
|
346
|
+
if (!outputValidation.valid) {
|
|
347
|
+
throw new UnknownSideEffectOutcomeError(`Adapter output violated ${descriptor.id} after dispatch; side-effect evidence cannot be trusted: ${outputValidation.errors.join(" ")}`);
|
|
348
|
+
}
|
|
349
|
+
const effects = Object.freeze({
|
|
350
|
+
...(dispatched.effects ?? {}),
|
|
351
|
+
approval: approvalEffect,
|
|
352
|
+
});
|
|
353
|
+
const result = Object.freeze({
|
|
354
|
+
canonicalToolId: descriptor.id,
|
|
355
|
+
content: JSON.stringify(dispatched.output),
|
|
356
|
+
effects,
|
|
357
|
+
effectsAuthority: "host",
|
|
358
|
+
ok: true,
|
|
359
|
+
outputLimits: Object.freeze({
|
|
360
|
+
maxBytes: descriptor.maxOutputBytes,
|
|
361
|
+
maxTokens: descriptor.maxOutputTokens,
|
|
362
|
+
tailFraction: 0.25,
|
|
363
|
+
}),
|
|
364
|
+
summary: dispatched.summary,
|
|
365
|
+
trust: dispatched.trust,
|
|
366
|
+
});
|
|
367
|
+
if (descriptor.idempotency === "with_key")
|
|
368
|
+
this.idempotencyCache.set(context.idempotencyKey, result);
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
ensureRegistry(context) {
|
|
372
|
+
if (this.registry !== null)
|
|
373
|
+
return this.registry;
|
|
374
|
+
const profileToolIds = [
|
|
375
|
+
...(this.contractToolsEnabled ? LAB_FULL_CONTRACT_TOOL_IDS : LAB_AVAILABLE_TOOL_IDS),
|
|
376
|
+
...(this.options.research === undefined ? [] : LAB_RESEARCH_TOOL_IDS),
|
|
377
|
+
];
|
|
378
|
+
const snapshot = createAiCoderToolRegistrySnapshot({
|
|
379
|
+
availableToolIds: this.options.enableGit === false
|
|
380
|
+
? new Set(profileToolIds.filter((id) => id !== "git.exec"))
|
|
381
|
+
: new Set(profileToolIds),
|
|
382
|
+
capabilities: this.options.capabilities,
|
|
383
|
+
grantedPermissions: this.grantedPermissions,
|
|
384
|
+
mode: context.mode,
|
|
385
|
+
});
|
|
386
|
+
this.registry = new AiCoderToolRegistry(snapshot);
|
|
387
|
+
return this.registry;
|
|
388
|
+
}
|
|
389
|
+
async dispatch(descriptor, argumentsValue, context) {
|
|
390
|
+
if (this.options.research !== undefined && descriptor.id === "research.search") {
|
|
391
|
+
const query = stringArgument(argumentsValue, "query");
|
|
392
|
+
const limit = optionalNumber(argumentsValue, "maxResults") ?? 3;
|
|
393
|
+
const cacheKey = JSON.stringify({ limit, query: query.trim() });
|
|
394
|
+
const cached = this.researchSearchCache.get(cacheKey);
|
|
395
|
+
if (cached !== undefined)
|
|
396
|
+
return cached;
|
|
397
|
+
const data = portData(await this.options.research.search({
|
|
398
|
+
query,
|
|
399
|
+
limit,
|
|
400
|
+
}, context));
|
|
401
|
+
const source = Object.freeze({
|
|
402
|
+
...provenance("ollama.web_search", "untrusted_external"),
|
|
403
|
+
retrievedAt: new Date().toISOString(),
|
|
404
|
+
});
|
|
405
|
+
const output = {
|
|
406
|
+
results: data.results.map((hit) => Object.freeze({ ...hit, provenance: source })),
|
|
407
|
+
truncated: data.pagination.hasMore,
|
|
408
|
+
provenance: source,
|
|
409
|
+
};
|
|
410
|
+
while (!researchOutputFits(output, descriptor) && output.results.length > 0) {
|
|
411
|
+
output.truncated = true;
|
|
412
|
+
if (output.results.some((hit) => (hit.snippet?.length ?? 0) > 0)) {
|
|
413
|
+
output.results = output.results.map((hit) => Object.freeze({
|
|
414
|
+
...hit,
|
|
415
|
+
...(hit.snippet === undefined ? {} : { snippet: shortenResearchText(hit.snippet) }),
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
output.results.pop();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const dispatched = Object.freeze({
|
|
423
|
+
effects: Object.freeze({
|
|
424
|
+
researchSources: Object.freeze(output.results.map((hit) => Object.freeze({
|
|
425
|
+
contentHash: null,
|
|
426
|
+
kind: "search",
|
|
427
|
+
summary: durableResearchSummary(hit.snippet ?? hit.title, 400),
|
|
428
|
+
title: hit.title.slice(0, 512),
|
|
429
|
+
truncated: output.truncated,
|
|
430
|
+
url: hit.url,
|
|
431
|
+
}))),
|
|
432
|
+
}),
|
|
433
|
+
output: Object.freeze(output),
|
|
434
|
+
summary: `Public research returned ${output.results.length} source(s)${output.truncated ? " with bounded or filtered content" : ""}.`,
|
|
435
|
+
trust: "external",
|
|
436
|
+
});
|
|
437
|
+
this.researchSearchCache.set(cacheKey, dispatched);
|
|
438
|
+
return dispatched;
|
|
439
|
+
}
|
|
440
|
+
if (this.options.research !== undefined && descriptor.id === "research.fetch") {
|
|
441
|
+
const requestedUrl = stringArgument(argumentsValue, "url");
|
|
442
|
+
const cacheKey = researchCacheUrl(requestedUrl);
|
|
443
|
+
const cached = this.researchFetchCache.get(cacheKey);
|
|
444
|
+
if (cached !== undefined)
|
|
445
|
+
return cached;
|
|
446
|
+
const data = portData(await this.options.research.extract({
|
|
447
|
+
url: requestedUrl,
|
|
448
|
+
maxBytes: Math.min(optionalNumber(argumentsValue, "maxBytes") ?? 12_000, 12_000),
|
|
449
|
+
}, context));
|
|
450
|
+
const output = {
|
|
451
|
+
url: data.url,
|
|
452
|
+
content: data.content,
|
|
453
|
+
contentHash: data.contentSha256,
|
|
454
|
+
truncated: data.pagination.hasMore,
|
|
455
|
+
provenance: {
|
|
456
|
+
...provenance("ollama.web_fetch", "untrusted_external", data.contentSha256),
|
|
457
|
+
contentHash: data.contentSha256,
|
|
458
|
+
retrievedAt: new Date().toISOString(),
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
while (!researchOutputFits(output, descriptor) && output.content.length > 0) {
|
|
462
|
+
output.content = shortenResearchText(output.content);
|
|
463
|
+
output.truncated = true;
|
|
464
|
+
output.contentHash = sha256Text(output.content);
|
|
465
|
+
output.provenance.contentHash = output.contentHash;
|
|
466
|
+
}
|
|
467
|
+
if (!researchOutputFits(output, descriptor)) {
|
|
468
|
+
throw new ToolAdapterError("LIMIT_EXCEEDED", "Research source metadata exceeds the structured output budget.", false);
|
|
469
|
+
}
|
|
470
|
+
const dispatched = Object.freeze({
|
|
471
|
+
effects: Object.freeze({
|
|
472
|
+
researchSources: Object.freeze([Object.freeze({
|
|
473
|
+
contentHash: output.contentHash,
|
|
474
|
+
kind: "fetch",
|
|
475
|
+
summary: durableResearchSummary(output.content, 1_000),
|
|
476
|
+
truncated: output.truncated,
|
|
477
|
+
url: output.url,
|
|
478
|
+
})]),
|
|
479
|
+
}),
|
|
480
|
+
output: Object.freeze(output),
|
|
481
|
+
summary: `Fetched bounded public source ${data.url}${output.truncated ? " (truncated)" : ""}.`,
|
|
482
|
+
trust: "external",
|
|
483
|
+
});
|
|
484
|
+
this.researchFetchCache.set(cacheKey, dispatched);
|
|
485
|
+
this.researchFetchCache.set(researchCacheUrl(data.url), dispatched);
|
|
486
|
+
return dispatched;
|
|
487
|
+
}
|
|
488
|
+
switch (descriptor.id) {
|
|
489
|
+
case "catalog.search": {
|
|
490
|
+
const result = this.ensureRegistry(context).search(stringArgument(argumentsValue, "query", ""), optionalString(argumentsValue, "category"), optionalNumber(argumentsValue, "limit"), optionalString(argumentsValue, "cursor"));
|
|
491
|
+
return Object.freeze({
|
|
492
|
+
output: result,
|
|
493
|
+
summary: `Activated ${result.activated.length} matching tool(s).`,
|
|
494
|
+
trust: "trusted",
|
|
495
|
+
effects: Object.freeze({ stateVersion: result.activeHash }),
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
case "task.checkpoint": {
|
|
499
|
+
const action = stringArgument(argumentsValue, "action");
|
|
500
|
+
let checkpoint = this.taskCheckpointStore.get(context.runId);
|
|
501
|
+
if (action === "update") {
|
|
502
|
+
checkpoint = Object.freeze({
|
|
503
|
+
goal: stringArgument(argumentsValue, "goal", checkpoint?.goal ?? `Task ${context.taskId}`),
|
|
504
|
+
progress: stringArgument(argumentsValue, "progress", checkpoint?.progress ?? ""),
|
|
505
|
+
decisions: Object.freeze(stringArray(argumentsValue, "decisions")),
|
|
506
|
+
nextStep: stringArgument(argumentsValue, "nextStep", checkpoint?.nextStep ?? ""),
|
|
507
|
+
updatedAt: "1970-01-01T00:00:00.000Z",
|
|
508
|
+
});
|
|
509
|
+
this.taskCheckpointStore.set(context.runId, checkpoint);
|
|
510
|
+
}
|
|
511
|
+
if (checkpoint === undefined) {
|
|
512
|
+
checkpoint = Object.freeze({
|
|
513
|
+
goal: `Task ${context.taskId}`,
|
|
514
|
+
progress: "",
|
|
515
|
+
decisions: Object.freeze([]),
|
|
516
|
+
nextStep: "Inspect the workspace.",
|
|
517
|
+
updatedAt: "1970-01-01T00:00:00.000Z",
|
|
518
|
+
});
|
|
519
|
+
this.taskCheckpointStore.set(context.runId, checkpoint);
|
|
520
|
+
}
|
|
521
|
+
return Object.freeze({
|
|
522
|
+
output: Object.freeze({ action, checkpointId: context.runId, updated: action === "update", checkpoint }),
|
|
523
|
+
summary: action === "update" ? "Updated the bounded task checkpoint." : "Read the bounded task checkpoint.",
|
|
524
|
+
trust: "trusted",
|
|
525
|
+
effects: Object.freeze({
|
|
526
|
+
nextAction: checkpoint.nextStep,
|
|
527
|
+
plan: Object.freeze({
|
|
528
|
+
completed: checkpoint.progress.trim() ? Object.freeze([checkpoint.progress]) : Object.freeze([]),
|
|
529
|
+
decisions: Object.freeze([...checkpoint.decisions]),
|
|
530
|
+
inProgress: checkpoint.nextStep.trim() || null,
|
|
531
|
+
pending: Object.freeze([]),
|
|
532
|
+
}),
|
|
533
|
+
stateVersion: sha256Text(JSON.stringify(checkpoint)),
|
|
534
|
+
}),
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
case "workspace.list": {
|
|
538
|
+
const depth = optionalNumber(argumentsValue, "depth");
|
|
539
|
+
const limit = optionalNumber(argumentsValue, "limit");
|
|
540
|
+
const cursor = optionalString(argumentsValue, "cursor");
|
|
541
|
+
const result = await this.options.workspace.listDir({
|
|
542
|
+
path: stringArgument(argumentsValue, "path"),
|
|
543
|
+
...(depth === undefined ? {} : { depth }),
|
|
544
|
+
...(limit === undefined ? {} : { limit }),
|
|
545
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
546
|
+
}, context);
|
|
547
|
+
const data = portData(result);
|
|
548
|
+
const output = Object.freeze({
|
|
549
|
+
entries: data.entries,
|
|
550
|
+
truncated: data.pagination.hasMore,
|
|
551
|
+
...(data.pagination.nextCursor === undefined ? {} : { nextCursor: data.pagination.nextCursor }),
|
|
552
|
+
});
|
|
553
|
+
return Object.freeze({
|
|
554
|
+
output,
|
|
555
|
+
summary: `Listed ${data.entries.length} workspace entries.`,
|
|
556
|
+
trust: "workspace",
|
|
557
|
+
effects: Object.freeze({ inspectedPaths: Object.freeze([stringArgument(argumentsValue, "path")]) }),
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
case "workspace.glob": {
|
|
561
|
+
const kind = optionalString(argumentsValue, "kind");
|
|
562
|
+
const path = optionalString(argumentsValue, "path");
|
|
563
|
+
const limit = optionalNumber(argumentsValue, "limit");
|
|
564
|
+
const cursor = optionalString(argumentsValue, "cursor");
|
|
565
|
+
const result = await this.options.workspace.searchPaths({
|
|
566
|
+
query: stringArgument(argumentsValue, "pattern"),
|
|
567
|
+
mode: "glob",
|
|
568
|
+
...(kind === undefined || kind === "any" ? {} : { kind: kind }),
|
|
569
|
+
...(path === undefined ? {} : { path }),
|
|
570
|
+
...(limit === undefined ? {} : { limit }),
|
|
571
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
572
|
+
}, context);
|
|
573
|
+
const data = portData(result);
|
|
574
|
+
return Object.freeze({
|
|
575
|
+
output: Object.freeze({
|
|
576
|
+
matches: data.matches,
|
|
577
|
+
truncated: data.pagination.hasMore,
|
|
578
|
+
...(data.pagination.nextCursor === undefined ? {} : { nextCursor: data.pagination.nextCursor }),
|
|
579
|
+
}),
|
|
580
|
+
summary: `Found ${data.matches.length} matching workspace paths.`,
|
|
581
|
+
trust: "workspace",
|
|
582
|
+
effects: Object.freeze({ inspectedPaths: Object.freeze([optionalString(argumentsValue, "path") ?? "."]) }),
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
case "workspace.grep": {
|
|
586
|
+
const regex = optionalBoolean(argumentsValue, "regex");
|
|
587
|
+
const caseSensitive = optionalBoolean(argumentsValue, "caseSensitive");
|
|
588
|
+
const glob = optionalString(argumentsValue, "glob");
|
|
589
|
+
const path = optionalString(argumentsValue, "path");
|
|
590
|
+
const limit = optionalNumber(argumentsValue, "limit");
|
|
591
|
+
const cursor = optionalString(argumentsValue, "cursor");
|
|
592
|
+
const result = await this.options.workspace.searchText({
|
|
593
|
+
query: stringArgument(argumentsValue, "query"),
|
|
594
|
+
...(regex === undefined ? {} : { regex }),
|
|
595
|
+
...(caseSensitive === undefined ? {} : { caseSensitive }),
|
|
596
|
+
...(glob === undefined ? {} : { glob }),
|
|
597
|
+
...(path === undefined ? {} : { path }),
|
|
598
|
+
...(limit === undefined ? {} : { limit }),
|
|
599
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
600
|
+
}, context);
|
|
601
|
+
const data = portData(result);
|
|
602
|
+
const matches = data.matches.map((match) => Object.freeze({
|
|
603
|
+
path: match.path,
|
|
604
|
+
line: match.line,
|
|
605
|
+
...(match.column === undefined ? {} : { column: match.column }),
|
|
606
|
+
preview: match.preview,
|
|
607
|
+
...(match.contentSha256 === undefined ? {} : { contentHash: match.contentSha256 }),
|
|
608
|
+
}));
|
|
609
|
+
return Object.freeze({
|
|
610
|
+
output: Object.freeze({
|
|
611
|
+
matches: Object.freeze(matches),
|
|
612
|
+
truncated: data.pagination.hasMore,
|
|
613
|
+
provenance: provenance("workspace.searchText", "untrusted_workspace"),
|
|
614
|
+
...(data.pagination.nextCursor === undefined ? {} : { nextCursor: data.pagination.nextCursor }),
|
|
615
|
+
}),
|
|
616
|
+
summary: `Found ${matches.length} text matches.`,
|
|
617
|
+
trust: "workspace",
|
|
618
|
+
effects: Object.freeze({ inspectedPaths: Object.freeze([...new Set(matches.map((match) => match.path))]) }),
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
case "workspace.read": {
|
|
622
|
+
const startLine = optionalNumber(argumentsValue, "startLine");
|
|
623
|
+
const endLine = optionalNumber(argumentsValue, "endLine");
|
|
624
|
+
const maxBytes = optionalNumber(argumentsValue, "maxBytes");
|
|
625
|
+
const cursor = optionalString(argumentsValue, "cursor");
|
|
626
|
+
const result = await this.options.workspace.readText({
|
|
627
|
+
path: stringArgument(argumentsValue, "path"),
|
|
628
|
+
...(startLine === undefined ? {} : { startLine }),
|
|
629
|
+
...(endLine === undefined ? {} : { endLine }),
|
|
630
|
+
...(maxBytes === undefined ? {} : { maxBytes }),
|
|
631
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
632
|
+
}, context);
|
|
633
|
+
const data = portData(result);
|
|
634
|
+
return Object.freeze({
|
|
635
|
+
output: Object.freeze({
|
|
636
|
+
path: data.path,
|
|
637
|
+
content: data.content,
|
|
638
|
+
contentHash: data.contentSha256,
|
|
639
|
+
startLine: data.startLine,
|
|
640
|
+
endLine: data.endLine,
|
|
641
|
+
truncated: data.truncated,
|
|
642
|
+
provenance: provenance("workspace.readText", "untrusted_workspace", data.contentSha256),
|
|
643
|
+
...(data.pagination.nextCursor === undefined ? {} : { nextCursor: data.pagination.nextCursor }),
|
|
644
|
+
}),
|
|
645
|
+
summary: `Read ${data.path}:${data.startLine}-${data.endLine}.`,
|
|
646
|
+
trust: "workspace",
|
|
647
|
+
effects: Object.freeze({ inspectedPaths: Object.freeze([data.path]) }),
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
case "workspace.edit": {
|
|
651
|
+
const replaceAll = optionalBoolean(argumentsValue, "replaceAll");
|
|
652
|
+
const { before, snapshotter } = await this.mutationBaseline(context);
|
|
653
|
+
const result = await this.options.workspace.applyPatch({
|
|
654
|
+
path: stringArgument(argumentsValue, "path"),
|
|
655
|
+
oldText: stringArgument(argumentsValue, "oldText"),
|
|
656
|
+
newText: stringArgument(argumentsValue, "newText"),
|
|
657
|
+
...(replaceAll === undefined ? {} : { replaceAll }),
|
|
658
|
+
precondition: argumentsValue.precondition,
|
|
659
|
+
}, context);
|
|
660
|
+
const after = await snapshotter.capture(context);
|
|
661
|
+
const mutations = diffNodeWorkspaceSnapshots(before, after);
|
|
662
|
+
if (!result.ok)
|
|
663
|
+
structuredMutationFailure("workspace.edit", mutations, result.error);
|
|
664
|
+
const data = result.data;
|
|
665
|
+
assertTargetMutation("workspace.edit", mutations, {
|
|
666
|
+
afterHash: data.afterContentSha256,
|
|
667
|
+
beforeHash: data.beforeContentSha256,
|
|
668
|
+
path: data.path,
|
|
669
|
+
});
|
|
670
|
+
const derivedMutations = derivedMutationObservation(mutations);
|
|
671
|
+
return Object.freeze({
|
|
672
|
+
output: Object.freeze({
|
|
673
|
+
...data,
|
|
674
|
+
...(derivedMutations === undefined ? {} : { derivedMutations }),
|
|
675
|
+
}),
|
|
676
|
+
summary: `Applied ${data.replacements} replacement(s) to ${data.path}.`,
|
|
677
|
+
trust: "trusted",
|
|
678
|
+
effects: Object.freeze({
|
|
679
|
+
writes: mutations.writes,
|
|
680
|
+
stateVersion: mutations.stateVersion,
|
|
681
|
+
}),
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
case "workspace.write": {
|
|
685
|
+
const { before, snapshotter } = await this.mutationBaseline(context);
|
|
686
|
+
const result = await this.options.workspace.writeText({
|
|
687
|
+
path: stringArgument(argumentsValue, "path"),
|
|
688
|
+
content: stringArgument(argumentsValue, "content", ""),
|
|
689
|
+
precondition: argumentsValue.precondition,
|
|
690
|
+
}, context);
|
|
691
|
+
const after = await snapshotter.capture(context);
|
|
692
|
+
const mutations = diffNodeWorkspaceSnapshots(before, after);
|
|
693
|
+
if (!result.ok)
|
|
694
|
+
structuredMutationFailure("workspace.write", mutations, result.error);
|
|
695
|
+
const data = result.data;
|
|
696
|
+
assertTargetMutation("workspace.write", mutations, {
|
|
697
|
+
afterHash: data.afterContentSha256,
|
|
698
|
+
beforeHash: data.beforeContentSha256 ?? null,
|
|
699
|
+
path: data.path,
|
|
700
|
+
});
|
|
701
|
+
const derivedMutations = derivedMutationObservation(mutations);
|
|
702
|
+
return Object.freeze({
|
|
703
|
+
output: Object.freeze({
|
|
704
|
+
...data,
|
|
705
|
+
...(derivedMutations === undefined ? {} : { derivedMutations }),
|
|
706
|
+
}),
|
|
707
|
+
summary: `Wrote ${data.length} characters to ${data.path}.`,
|
|
708
|
+
trust: "trusted",
|
|
709
|
+
effects: Object.freeze({
|
|
710
|
+
writes: mutations.writes,
|
|
711
|
+
stateVersion: mutations.stateVersion,
|
|
712
|
+
}),
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
case "command.run": {
|
|
716
|
+
const cwd = optionalString(argumentsValue, "cwd") ?? ".";
|
|
717
|
+
const timeoutMs = optionalNumber(argumentsValue, "timeoutMs");
|
|
718
|
+
const command = stringArgument(argumentsValue, "command");
|
|
719
|
+
const { before, snapshotter } = await this.mutationBaseline(context);
|
|
720
|
+
const execution = await (async () => {
|
|
721
|
+
try {
|
|
722
|
+
const result = await this.options.command.run({
|
|
723
|
+
command,
|
|
724
|
+
cwd,
|
|
725
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
726
|
+
...(typeof argumentsValue.env === "object" && argumentsValue.env !== null
|
|
727
|
+
? { env: argumentsValue.env }
|
|
728
|
+
: {}),
|
|
729
|
+
}, context);
|
|
730
|
+
const after = await snapshotter.capture(context);
|
|
731
|
+
return Object.freeze({ mutations: diffNodeWorkspaceSnapshots(before, after), result });
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
throw new UnknownSideEffectOutcomeError(`Unable to execute and verify command.run; side-effect outcome is unknown: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
735
|
+
}
|
|
736
|
+
})();
|
|
737
|
+
const { mutations, result } = execution;
|
|
738
|
+
const derivedMutations = derivedMutationObservation(mutations);
|
|
739
|
+
const observedEffects = mutationEffects(mutations);
|
|
740
|
+
if (!result.ok) {
|
|
741
|
+
if (mutations.observedMutations.length === 0) {
|
|
742
|
+
throw new ToolAdapterError(result.error.code, result.error.message, result.error.retryable, result.error.suggestedAction);
|
|
743
|
+
}
|
|
744
|
+
return Object.freeze({
|
|
745
|
+
output: Object.freeze({
|
|
746
|
+
command,
|
|
747
|
+
cwd,
|
|
748
|
+
exitCode: -1,
|
|
749
|
+
stdout: "",
|
|
750
|
+
stderr: `${result.error.code}: ${result.error.message}`,
|
|
751
|
+
timedOut: result.error.code === "DEADLINE_EXCEEDED",
|
|
752
|
+
cancelled: result.error.code === "CANCELED",
|
|
753
|
+
truncated: false,
|
|
754
|
+
...(derivedMutations === undefined ? {} : { derivedMutations }),
|
|
755
|
+
}),
|
|
756
|
+
summary: `Command adapter returned ${result.error.code} after changing ${mutations.observedMutations.length} workspace path(s).`,
|
|
757
|
+
trust: "external",
|
|
758
|
+
effects: observedEffects,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
const data = portData(result);
|
|
762
|
+
return Object.freeze({
|
|
763
|
+
output: Object.freeze({
|
|
764
|
+
command: data.command,
|
|
765
|
+
cwd,
|
|
766
|
+
exitCode: data.exitCode ?? -1,
|
|
767
|
+
stdout: data.stdout,
|
|
768
|
+
stderr: data.stderr,
|
|
769
|
+
timedOut: data.status === "timed_out",
|
|
770
|
+
cancelled: data.status === "canceled",
|
|
771
|
+
truncated: data.stdoutTruncated || data.stderrTruncated,
|
|
772
|
+
...(derivedMutations === undefined ? {} : { derivedMutations }),
|
|
773
|
+
}),
|
|
774
|
+
summary: `Command ${data.status} with exit code ${String(data.exitCode)}.`,
|
|
775
|
+
trust: "external",
|
|
776
|
+
...(observedEffects === undefined ? {} : { effects: observedEffects }),
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
case "project.detect": {
|
|
780
|
+
const detected = await detectProject(this.options.workspace, optionalString(argumentsValue, "path") ?? ".", context);
|
|
781
|
+
return Object.freeze({
|
|
782
|
+
output: detected,
|
|
783
|
+
summary: `Detected ${detected.manifests.length} manifest(s) and ${detected.languages.length} language(s).`,
|
|
784
|
+
trust: "workspace",
|
|
785
|
+
effects: Object.freeze({ inspectedPaths: Object.freeze([detected.projectRoot]) }),
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
case "project.validate": {
|
|
789
|
+
const checks = stringArray(argumentsValue, "checks");
|
|
790
|
+
const timeoutMs = optionalNumber(argumentsValue, "timeoutMs");
|
|
791
|
+
const projectPath = normalizedProjectPath(optionalString(argumentsValue, "path") ?? ".");
|
|
792
|
+
const { before, snapshotter } = await this.mutationBaseline(context);
|
|
793
|
+
const execution = await (async () => {
|
|
794
|
+
try {
|
|
795
|
+
const result = await validateProject(this.options.workspace, this.options.command, {
|
|
796
|
+
checks,
|
|
797
|
+
path: projectPath,
|
|
798
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
799
|
+
}, context);
|
|
800
|
+
const after = await snapshotter.capture(context);
|
|
801
|
+
return Object.freeze({ mutations: diffNodeWorkspaceSnapshots(before, after), result });
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
throw new UnknownSideEffectOutcomeError(`Unable to execute and verify project.validate; side-effect outcome is unknown: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
805
|
+
}
|
|
806
|
+
})();
|
|
807
|
+
const { mutations, result } = execution;
|
|
808
|
+
const derivedMutations = derivedMutationObservation(mutations);
|
|
809
|
+
const outputResults = Object.freeze(result.results.map((item) => Object.freeze({
|
|
810
|
+
check: item.check,
|
|
811
|
+
status: item.status,
|
|
812
|
+
summary: item.summary,
|
|
813
|
+
...(item.command === undefined ? {} : { command: item.command }),
|
|
814
|
+
...(typeof item.exitCode === "number" ? { exitCode: item.exitCode } : {}),
|
|
815
|
+
})));
|
|
816
|
+
return Object.freeze({
|
|
817
|
+
output: Object.freeze({
|
|
818
|
+
cancelled: result.cancelled,
|
|
819
|
+
passed: result.passed,
|
|
820
|
+
results: outputResults,
|
|
821
|
+
...(derivedMutations === undefined ? {} : { derivedMutations }),
|
|
822
|
+
}),
|
|
823
|
+
summary: result.passed ? "Project validation passed." : "Project validation did not pass.",
|
|
824
|
+
trust: "external",
|
|
825
|
+
effects: Object.freeze({
|
|
826
|
+
...(mutations.observedMutations.length === 0 ? {} : {
|
|
827
|
+
stateVersion: mutations.stateVersion,
|
|
828
|
+
...(mutations.writes.length === 0 ? {} : { writes: mutations.writes }),
|
|
829
|
+
}),
|
|
830
|
+
validations: Object.freeze(result.results.map((item) => Object.freeze({
|
|
831
|
+
id: `project.validate:${item.check}:${projectPath}`,
|
|
832
|
+
detail: `${item.check}: ${item.summary}`,
|
|
833
|
+
scope: "workspace",
|
|
834
|
+
status: item.status === "passed" ? "passed" : item.status === "skipped" ? "not_run" : "failed",
|
|
835
|
+
}))),
|
|
836
|
+
}),
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
case "research.fetch":
|
|
840
|
+
case "research.search":
|
|
841
|
+
case "command.session":
|
|
842
|
+
case "preview.manage":
|
|
843
|
+
case "perception.analyze":
|
|
844
|
+
case "artifact.create":
|
|
845
|
+
case "artifact.list":
|
|
846
|
+
case "artifact.read":
|
|
847
|
+
case "user.ask": {
|
|
848
|
+
if (this.contractTools === null) {
|
|
849
|
+
throw new ToolAdapterError("UNAVAILABLE", `${descriptor.id} is available only in the explicit deterministic contract-tool profile.`);
|
|
850
|
+
}
|
|
851
|
+
try {
|
|
852
|
+
return await this.contractTools.execute(descriptor.id, argumentsValue, context);
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
if (error instanceof ContractToolError) {
|
|
856
|
+
throw new ToolAdapterError(error.code, error.message, error.retryable, error.suggestedAction);
|
|
857
|
+
}
|
|
858
|
+
throw error;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
case "git.exec": {
|
|
862
|
+
const action = stringArgument(argumentsValue, "action");
|
|
863
|
+
const paths = stringArray(argumentsValue, "paths");
|
|
864
|
+
const pathArguments = gitPathArguments(paths);
|
|
865
|
+
const limit = Math.max(1, Math.min(1_000, optionalNumber(argumentsValue, "limit") ?? 50));
|
|
866
|
+
let stdout;
|
|
867
|
+
let stderr;
|
|
868
|
+
let truncated;
|
|
869
|
+
if (action === "diff") {
|
|
870
|
+
const runGit = async (command, acceptedExitCodes = [0], environment = pathArguments.environment) => {
|
|
871
|
+
const data = portData(await this.options.command.run({
|
|
872
|
+
command,
|
|
873
|
+
cwd: ".",
|
|
874
|
+
env: environment,
|
|
875
|
+
maxOutputBytes: descriptor.maxOutputBytes,
|
|
876
|
+
}, context));
|
|
877
|
+
if (data.status !== "exited" || data.exitCode === null || !acceptedExitCodes.includes(data.exitCode)) {
|
|
878
|
+
throw new ToolAdapterError("GIT_COMMAND_FAILED", data.stderr.trim() || data.stdout.trim() || `${command} ended with ${data.status}/${String(data.exitCode)}.`);
|
|
879
|
+
}
|
|
880
|
+
return data;
|
|
881
|
+
};
|
|
882
|
+
const workingTree = await runGit(`git diff --no-ext-diff --binary${pathArguments.suffix}`);
|
|
883
|
+
const staged = await runGit(`git diff --cached --no-ext-diff --binary${pathArguments.suffix}`);
|
|
884
|
+
const untrackedList = await runGit(`git ls-files --others --exclude-standard -z${pathArguments.suffix}`);
|
|
885
|
+
const listedUntracked = untrackedList.stdoutTruncated || untrackedList.stderrTruncated
|
|
886
|
+
? []
|
|
887
|
+
: untrackedList.stdout.split("\0").filter((path) => path.length > 0);
|
|
888
|
+
const untracked = [];
|
|
889
|
+
for (const path of listedUntracked.slice(0, MAX_UNTRACKED_DIFF_FILES)) {
|
|
890
|
+
const untrackedPath = gitPathArguments([path], paths.length);
|
|
891
|
+
untracked.push(await runGit(`git diff --no-index --no-ext-diff --binary -- ${NULL_DEVICE} ${untrackedPath.arguments}`, [0, 1], untrackedPath.environment));
|
|
892
|
+
}
|
|
893
|
+
const sections = [
|
|
894
|
+
workingTree.stdout.length ? `[working-tree]\n${workingTree.stdout}` : "",
|
|
895
|
+
staged.stdout.length ? `[staged]\n${staged.stdout}` : "",
|
|
896
|
+
...untracked.map((item) => item.stdout.length ? `[untracked]\n${item.stdout}` : ""),
|
|
897
|
+
].filter((section) => section.length > 0);
|
|
898
|
+
stdout = sections.join("\n");
|
|
899
|
+
stderr = [workingTree.stderr, staged.stderr, untrackedList.stderr, ...untracked.map((item) => item.stderr)]
|
|
900
|
+
.filter((value) => value.length > 0)
|
|
901
|
+
.join("\n");
|
|
902
|
+
truncated = listedUntracked.length > MAX_UNTRACKED_DIFF_FILES
|
|
903
|
+
|| workingTree.stdoutTruncated || workingTree.stderrTruncated
|
|
904
|
+
|| staged.stdoutTruncated || staged.stderrTruncated
|
|
905
|
+
|| untrackedList.stdoutTruncated || untrackedList.stderrTruncated
|
|
906
|
+
|| untracked.some((item) => item.stdoutTruncated || item.stderrTruncated)
|
|
907
|
+
|| gitOutputWouldBeBound(descriptor, action, stdout, stderr);
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
const command = action === "status"
|
|
911
|
+
? `git status --short${pathArguments.suffix}`
|
|
912
|
+
: `git log --oneline --no-decorate -n ${limit}${pathArguments.suffix}`;
|
|
913
|
+
const data = portData(await this.options.command.run({
|
|
914
|
+
command,
|
|
915
|
+
cwd: ".",
|
|
916
|
+
env: pathArguments.environment,
|
|
917
|
+
}, context));
|
|
918
|
+
if (data.status !== "exited" || data.exitCode !== 0) {
|
|
919
|
+
throw new ToolAdapterError("GIT_COMMAND_FAILED", data.stderr.trim() || data.stdout.trim() || `Git ${action} ended with ${data.status}/${String(data.exitCode)}.`);
|
|
920
|
+
}
|
|
921
|
+
stdout = data.stdout;
|
|
922
|
+
stderr = data.stderr;
|
|
923
|
+
truncated = data.stdoutTruncated || data.stderrTruncated;
|
|
924
|
+
}
|
|
925
|
+
return Object.freeze({
|
|
926
|
+
output: Object.freeze({
|
|
927
|
+
action,
|
|
928
|
+
stdout,
|
|
929
|
+
stderr,
|
|
930
|
+
exitCode: 0,
|
|
931
|
+
truncated,
|
|
932
|
+
}),
|
|
933
|
+
summary: `Git ${action} exited with 0${truncated ? " and returned truncated output" : ""}.`,
|
|
934
|
+
trust: "workspace",
|
|
935
|
+
effects: Object.freeze({
|
|
936
|
+
...(action === "diff" && !truncated
|
|
937
|
+
? { diffReview: Object.freeze({ diffHash: sha256Text(JSON.stringify({ action, paths, stdout })) }) }
|
|
938
|
+
: {}),
|
|
939
|
+
inspectedPaths: Object.freeze(paths.length === 0 ? ["."] : paths),
|
|
940
|
+
}),
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
default:
|
|
944
|
+
throw new Error(`No deterministic adapter exists for ${descriptor.id}.`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
//# sourceMappingURL=tool-executor.js.map
|