tinker-agent 1.0.65 → 1.2.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/CHANGELOG.md +36 -0
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +90 -9
- package/bin/tinker.js +39 -0
- package/node_modules/ansi-regex/index.d.ts +33 -0
- package/node_modules/ansi-regex/index.js +14 -0
- package/node_modules/ansi-regex/license +9 -0
- package/node_modules/ansi-regex/package.json +61 -0
- package/node_modules/ansi-regex/readme.md +66 -0
- package/node_modules/ansi-styles/index.d.ts +236 -0
- package/node_modules/ansi-styles/index.js +223 -0
- package/node_modules/ansi-styles/license +9 -0
- package/node_modules/ansi-styles/package.json +54 -0
- package/node_modules/ansi-styles/readme.md +173 -0
- package/node_modules/chalk/license +9 -0
- package/node_modules/chalk/package.json +83 -0
- package/node_modules/chalk/readme.md +297 -0
- package/node_modules/chalk/source/index.d.ts +325 -0
- package/node_modules/chalk/source/index.js +225 -0
- package/node_modules/chalk/source/utilities.js +33 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/node_modules/character-entities/index.d.ts +6 -0
- package/node_modules/character-entities/index.js +2132 -0
- package/node_modules/character-entities/license +22 -0
- package/node_modules/character-entities/package.json +78 -0
- package/node_modules/character-entities/readme.md +152 -0
- package/node_modules/decode-named-character-reference/index.d.ts +13 -0
- package/node_modules/decode-named-character-reference/index.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts +6 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.js +32 -0
- package/node_modules/decode-named-character-reference/index.js +19 -0
- package/node_modules/decode-named-character-reference/license +22 -0
- package/node_modules/decode-named-character-reference/package.json +90 -0
- package/node_modules/decode-named-character-reference/readme.md +136 -0
- package/node_modules/get-east-asian-width/index.d.ts +60 -0
- package/node_modules/get-east-asian-width/index.js +30 -0
- package/node_modules/get-east-asian-width/license +9 -0
- package/node_modules/get-east-asian-width/lookup-data.js +21 -0
- package/node_modules/get-east-asian-width/lookup.js +138 -0
- package/node_modules/get-east-asian-width/package.json +71 -0
- package/node_modules/get-east-asian-width/readme.md +65 -0
- package/node_modules/get-east-asian-width/utilities.js +24 -0
- package/node_modules/has-flag/index.d.ts +39 -0
- package/node_modules/has-flag/index.js +8 -0
- package/node_modules/has-flag/license +9 -0
- package/node_modules/has-flag/package.json +49 -0
- package/node_modules/has-flag/readme.md +74 -0
- package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
- package/node_modules/is-fullwidth-code-point/index.js +12 -0
- package/node_modules/is-fullwidth-code-point/license +9 -0
- package/node_modules/is-fullwidth-code-point/package.json +53 -0
- package/node_modules/is-fullwidth-code-point/readme.md +31 -0
- package/node_modules/markdansi/LICENSE +21 -0
- package/node_modules/markdansi/README.md +167 -0
- package/node_modules/markdansi/dist/ast.d.ts +99 -0
- package/node_modules/markdansi/dist/ast.js +1 -0
- package/node_modules/markdansi/dist/cli.d.ts +17 -0
- package/node_modules/markdansi/dist/cli.js +208 -0
- package/node_modules/markdansi/dist/hyperlink.d.ts +9 -0
- package/node_modules/markdansi/dist/hyperlink.js +26 -0
- package/node_modules/markdansi/dist/index.d.ts +10 -0
- package/node_modules/markdansi/dist/index.js +16 -0
- package/node_modules/markdansi/dist/parser.d.ts +2 -0
- package/node_modules/markdansi/dist/parser.js +205 -0
- package/node_modules/markdansi/dist/render.d.ts +9 -0
- package/node_modules/markdansi/dist/render.js +680 -0
- package/node_modules/markdansi/dist/stream.d.ts +39 -0
- package/node_modules/markdansi/dist/stream.js +192 -0
- package/node_modules/markdansi/dist/theme.d.ts +18 -0
- package/node_modules/markdansi/dist/theme.js +105 -0
- package/node_modules/markdansi/dist/types.d.ts +58 -0
- package/node_modules/markdansi/dist/types.js +1 -0
- package/node_modules/markdansi/dist/wrap.d.ts +10 -0
- package/node_modules/markdansi/dist/wrap.js +73 -0
- package/node_modules/markdansi/docs/spec.md +110 -0
- package/node_modules/markdansi/package.json +78 -0
- package/node_modules/markdansi/tsconfig.json +18 -0
- package/node_modules/marked/LICENSE +44 -0
- package/node_modules/marked/README.md +115 -0
- package/node_modules/marked/bin/main.js +284 -0
- package/node_modules/marked/bin/marked.js +16 -0
- package/node_modules/marked/lib/marked.d.ts +760 -0
- package/node_modules/marked/lib/marked.esm.js +77 -0
- package/node_modules/marked/lib/marked.esm.js.map +7 -0
- package/node_modules/marked/lib/marked.umd.js +79 -0
- package/node_modules/marked/lib/marked.umd.js.map +7 -0
- package/node_modules/marked/man/marked.1 +113 -0
- package/node_modules/marked/man/marked.1.md +93 -0
- package/node_modules/marked/package.json +103 -0
- package/node_modules/slice-ansi/index.d.ts +19 -0
- package/node_modules/slice-ansi/index.js +317 -0
- package/node_modules/slice-ansi/license +10 -0
- package/node_modules/slice-ansi/package.json +59 -0
- package/node_modules/slice-ansi/readme.md +55 -0
- package/node_modules/slice-ansi/tokenize-ansi.js +752 -0
- package/node_modules/string-width/index.d.ts +39 -0
- package/node_modules/string-width/index.js +203 -0
- package/node_modules/string-width/license +9 -0
- package/node_modules/string-width/package.json +65 -0
- package/node_modules/string-width/readme.md +66 -0
- package/node_modules/strip-ansi/index.d.ts +15 -0
- package/node_modules/strip-ansi/index.js +19 -0
- package/node_modules/strip-ansi/license +9 -0
- package/node_modules/strip-ansi/package.json +59 -0
- package/node_modules/strip-ansi/readme.md +37 -0
- package/node_modules/supports-color/browser.d.ts +1 -0
- package/node_modules/supports-color/browser.js +35 -0
- package/node_modules/supports-color/index.d.ts +55 -0
- package/node_modules/supports-color/index.js +202 -0
- package/node_modules/supports-color/license +9 -0
- package/node_modules/supports-color/package.json +64 -0
- package/node_modules/supports-color/readme.md +75 -0
- package/node_modules/supports-hyperlinks/browser.js +10 -0
- package/node_modules/supports-hyperlinks/index.d.ts +25 -0
- package/node_modules/supports-hyperlinks/index.js +163 -0
- package/node_modules/supports-hyperlinks/license +10 -0
- package/node_modules/supports-hyperlinks/package.json +47 -0
- package/node_modules/supports-hyperlinks/readme.md +45 -0
- package/package.json +41 -10
- package/patches/markdansi@0.3.2.patch +22 -2
- package/src/agent/context-builder.ts +24 -10
- package/src/agent/context-meter.ts +89 -5
- package/src/agent/loop.ts +107 -10
- package/src/agent/runtime-session.ts +1437 -82
- package/src/agent/session-ledger.ts +169 -29
- package/src/agent/types.ts +12 -1
- package/src/agent/user-prompt-projection.ts +62 -0
- package/src/cli/config.ts +57 -14
- package/src/cli/model-profiles.ts +124 -0
- package/src/cli/run-runner.ts +4 -1
- package/src/cli/tui-runner.tsx +68 -30
- package/src/context/compiled-context-hash.ts +1 -1
- package/src/context/compiled-context-validator.ts +92 -15
- package/src/context/context-automation-policy.ts +121 -0
- package/src/context/context-manager.ts +390 -11
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +11 -1
- package/src/context/context-revision-compiler.ts +190 -27
- package/src/context/context-revision.ts +105 -19
- package/src/context/context-surface.ts +329 -0
- package/src/context/prefix-retirement-planner.ts +570 -0
- package/src/context/protocol-frame.ts +30 -3
- package/src/context/recall-retirement-contract.ts +27 -0
- package/src/context/swap-planner.ts +34 -10
- package/src/events/observation-text-log.ts +3 -3
- package/src/events/stdout-event-printer.ts +39 -3
- package/src/events/types.ts +172 -35
- package/src/ids/runtime-id.ts +6 -0
- package/src/ids/uuid-v7.ts +7 -0
- package/src/image/image-asset-store.ts +319 -0
- package/src/image/image-input-policy.ts +14 -0
- package/src/image/image-probe.ts +233 -0
- package/src/image/image-types.ts +262 -0
- package/src/instructions/project-instructions.ts +4 -1
- package/src/mcp/mcp-manager.ts +83 -19
- package/src/model/fake-model-client.ts +9 -1
- package/src/model/input-token-estimator.ts +25 -0
- package/src/model/model-client.ts +60 -0
- package/src/model/model-request-preflight.ts +1 -0
- package/src/model/moonshot-input-token-estimator.ts +107 -0
- package/src/model/openai-chat-mapping.ts +77 -1
- package/src/model/openai-chat-model-client.ts +389 -59
- package/src/model/openai-chat-stream.ts +272 -0
- package/src/model/prompt-prefix-hash.ts +2 -4
- package/src/model/token-estimator.ts +4 -0
- package/src/observation/observation-builder.ts +29 -0
- package/src/session/resume-projection.ts +94 -34
- package/src/session/session-catalog.ts +4 -4
- package/src/session/session-errors.ts +1 -1
- package/src/session/session-history-reader.ts +151 -4
- package/src/session/session-last-response-reader.ts +189 -0
- package/src/session/session-schema.ts +349 -35
- package/src/session/session-store.ts +3266 -255
- package/src/session/sqlite-session-ledger.ts +9 -4
- package/src/skills/skill-catalog.ts +182 -0
- package/src/skills/skill-context.ts +364 -0
- package/src/skills/skill-loader.ts +779 -0
- package/src/skills/skill-tool.ts +297 -0
- package/src/tools/edit.ts +51 -64
- package/src/tools/ensure-parent-directory.ts +6 -0
- package/src/tools/read.ts +2 -2
- package/src/tools/recall.ts +43 -40
- package/src/tools/registry.ts +24 -3
- package/src/tools/ripgrep.ts +3 -2
- package/src/tools/types.ts +43 -4
- package/src/tools/write.ts +20 -31
- package/src/tui/app.tsx +333 -96
- package/src/tui/clipboard.ts +5 -0
- package/src/tui/components/mcp-panel.tsx +28 -0
- package/src/tui/components/prompt-input.tsx +790 -257
- package/src/tui/components/skills-panel.tsx +31 -0
- package/src/tui/components/timeline.tsx +30 -1
- package/src/tui/event-store.ts +85 -2
- package/src/tui/file-mention.ts +234 -0
- package/src/tui/line-editor.ts +1 -0
- package/src/tui/project-slash-commands.ts +229 -0
- package/src/tui/prompt-draft.ts +510 -0
- package/src/tui/prompt-history.ts +294 -22
- package/src/tui/slash-commands.ts +46 -2
- package/src/tui/tui-session-controller.ts +110 -48
- package/src/tui/workspace-file-search.ts +94 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { lstat, readdir, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
SKILL_RESOURCE_MAX_DEPTH,
|
|
5
|
+
SKILL_RESOURCE_MAX_ENTRIES,
|
|
6
|
+
SkillError,
|
|
7
|
+
type LoadedSkill,
|
|
8
|
+
type SkillCatalogSnapshot,
|
|
9
|
+
} from "./skill-loader";
|
|
10
|
+
import { createSkillToolDefinition } from "./skill-catalog";
|
|
11
|
+
import type { SkillActivationCoordinator } from "./skill-context";
|
|
12
|
+
import { defineToolExecutor, type ToolExecutor } from "../tools/types";
|
|
13
|
+
import { cancellationError, throwIfTurnCancelled } from "../agent/turn-cancellation";
|
|
14
|
+
|
|
15
|
+
export function createSkillToolExecutor(input: {
|
|
16
|
+
catalog: SkillCatalogSnapshot;
|
|
17
|
+
coordinator: SkillActivationCoordinator;
|
|
18
|
+
}): ToolExecutor {
|
|
19
|
+
return defineToolExecutor("skill", {
|
|
20
|
+
definition: createSkillToolDefinition(input.catalog),
|
|
21
|
+
async execute(args, _call, context) {
|
|
22
|
+
throwIfTurnCancelled(context.signal);
|
|
23
|
+
let name: string;
|
|
24
|
+
try {
|
|
25
|
+
name = parseSkillArgs(args);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return failed(
|
|
28
|
+
"",
|
|
29
|
+
error instanceof SkillError ? error.code : "SKILL_FIELD_INVALID",
|
|
30
|
+
errorMessage(error),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
const skill = input.catalog.skills.get(name);
|
|
34
|
+
if (skill === undefined) {
|
|
35
|
+
return failed(name, "SKILL_NOT_FOUND", `Unknown Agent Skill: ${name}.`);
|
|
36
|
+
}
|
|
37
|
+
const status = input.coordinator.status(name);
|
|
38
|
+
if (status.status === "active") {
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
status: "already_active",
|
|
42
|
+
name,
|
|
43
|
+
scope: status.skill.scope,
|
|
44
|
+
sha256: status.skill.sha256,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (status.status === "loaded") {
|
|
48
|
+
return {
|
|
49
|
+
ok: true,
|
|
50
|
+
status: "already_loaded",
|
|
51
|
+
name,
|
|
52
|
+
scope: status.skill.scope,
|
|
53
|
+
lifecycle: status.lifecycle,
|
|
54
|
+
sha256: status.skill.sha256,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
input.coordinator.reserve(skill);
|
|
60
|
+
const resources = await listSkillResources(skill, context.signal);
|
|
61
|
+
throwIfTurnCancelled(context.signal);
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
status: "loaded",
|
|
65
|
+
name,
|
|
66
|
+
scope: skill.scope,
|
|
67
|
+
directory: skill.directory,
|
|
68
|
+
skillFilePath: skill.skillFilePath,
|
|
69
|
+
content: skill.content,
|
|
70
|
+
byteLength: skill.byteLength,
|
|
71
|
+
sha256: skill.sha256,
|
|
72
|
+
resources: resources.paths,
|
|
73
|
+
resourcesTruncated: resources.truncated,
|
|
74
|
+
};
|
|
75
|
+
} catch (error) {
|
|
76
|
+
input.coordinator.release(name);
|
|
77
|
+
if (context.signal.aborted) {
|
|
78
|
+
throw cancellationError(context.signal, error);
|
|
79
|
+
}
|
|
80
|
+
const code =
|
|
81
|
+
error instanceof SkillError ? error.code : "SKILL_RESOURCE_INVALID";
|
|
82
|
+
return failed(name, code, errorMessage(error));
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseSkillArgs(args: unknown): string {
|
|
89
|
+
if (
|
|
90
|
+
typeof args !== "object" ||
|
|
91
|
+
args === null ||
|
|
92
|
+
Array.isArray(args) ||
|
|
93
|
+
Object.keys(args).length !== 1 ||
|
|
94
|
+
typeof (args as Record<string, unknown>).name !== "string" ||
|
|
95
|
+
(args as Record<string, string>).name.trim() === ""
|
|
96
|
+
) {
|
|
97
|
+
throw new SkillError(
|
|
98
|
+
"SKILL_FIELD_INVALID",
|
|
99
|
+
"Skill arguments must be exactly { name: <available-skill-name> }.",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return (args as { name: string }).name;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function listSkillResources(
|
|
106
|
+
skill: LoadedSkill,
|
|
107
|
+
signal: AbortSignal,
|
|
108
|
+
): Promise<{
|
|
109
|
+
paths: readonly string[];
|
|
110
|
+
truncated: boolean;
|
|
111
|
+
}> {
|
|
112
|
+
const files: string[] = [];
|
|
113
|
+
let depthTruncated = false;
|
|
114
|
+
for (const rootName of ["assets", "references", "scripts"] as const) {
|
|
115
|
+
throwIfTurnCancelled(signal);
|
|
116
|
+
if (files.length > SKILL_RESOURCE_MAX_ENTRIES) {
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
const logicalRoot = path.join(skill.directory, rootName);
|
|
120
|
+
let rootStats;
|
|
121
|
+
try {
|
|
122
|
+
rootStats = await lstat(logicalRoot);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (isErrno(error, "ENOENT")) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
throw resourceError(skill, rootName, error);
|
|
128
|
+
}
|
|
129
|
+
const canonicalRoot = await resolveResourcePath(skill, logicalRoot, rootName);
|
|
130
|
+
const targetStats = await stat(canonicalRoot);
|
|
131
|
+
if (!targetStats.isDirectory()) {
|
|
132
|
+
if (rootStats.isSymbolicLink()) {
|
|
133
|
+
throw resourceError(
|
|
134
|
+
skill,
|
|
135
|
+
rootName,
|
|
136
|
+
"resource root symlink is not a directory",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
await walkResources({
|
|
142
|
+
skill,
|
|
143
|
+
logicalDirectory: logicalRoot,
|
|
144
|
+
logicalRelative: rootName,
|
|
145
|
+
depth: 1,
|
|
146
|
+
canonicalAncestors: new Set([canonicalRoot]),
|
|
147
|
+
files,
|
|
148
|
+
signal,
|
|
149
|
+
onDepthTruncated: () => {
|
|
150
|
+
depthTruncated = true;
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
files.sort(compareText);
|
|
155
|
+
const truncated = depthTruncated || files.length > SKILL_RESOURCE_MAX_ENTRIES;
|
|
156
|
+
return {
|
|
157
|
+
paths: Object.freeze(files.slice(0, SKILL_RESOURCE_MAX_ENTRIES)),
|
|
158
|
+
truncated,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function walkResources(input: {
|
|
163
|
+
skill: LoadedSkill;
|
|
164
|
+
logicalDirectory: string;
|
|
165
|
+
logicalRelative: string;
|
|
166
|
+
depth: number;
|
|
167
|
+
canonicalAncestors: ReadonlySet<string>;
|
|
168
|
+
files: string[];
|
|
169
|
+
signal: AbortSignal;
|
|
170
|
+
onDepthTruncated(): void;
|
|
171
|
+
}): Promise<void> {
|
|
172
|
+
throwIfTurnCancelled(input.signal);
|
|
173
|
+
let entries;
|
|
174
|
+
try {
|
|
175
|
+
entries = await readdir(input.logicalDirectory, { withFileTypes: true });
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw resourceError(input.skill, input.logicalRelative, error);
|
|
178
|
+
}
|
|
179
|
+
entries.sort((left, right) => compareText(left.name, right.name));
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
throwIfTurnCancelled(input.signal);
|
|
182
|
+
if (input.files.length > SKILL_RESOURCE_MAX_ENTRIES) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const logicalPath = path.join(input.logicalDirectory, entry.name);
|
|
186
|
+
const relativePath = `${input.logicalRelative}/${entry.name}`;
|
|
187
|
+
const canonicalPath = await resolveResourcePath(
|
|
188
|
+
input.skill,
|
|
189
|
+
logicalPath,
|
|
190
|
+
relativePath,
|
|
191
|
+
);
|
|
192
|
+
const targetStats = await stat(canonicalPath);
|
|
193
|
+
if (targetStats.isFile()) {
|
|
194
|
+
input.files.push(relativePath.split(path.sep).join("/"));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (!targetStats.isDirectory()) {
|
|
198
|
+
throw resourceError(
|
|
199
|
+
input.skill,
|
|
200
|
+
relativePath,
|
|
201
|
+
"resource must resolve to a regular file or directory",
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (input.depth >= SKILL_RESOURCE_MAX_DEPTH) {
|
|
205
|
+
input.onDepthTruncated();
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (input.canonicalAncestors.has(canonicalPath)) {
|
|
209
|
+
throw resourceError(input.skill, relativePath, "resource symlink cycle detected");
|
|
210
|
+
}
|
|
211
|
+
await walkResources({
|
|
212
|
+
...input,
|
|
213
|
+
logicalDirectory: logicalPath,
|
|
214
|
+
logicalRelative: relativePath,
|
|
215
|
+
depth: input.depth + 1,
|
|
216
|
+
canonicalAncestors: new Set([...input.canonicalAncestors, canonicalPath]),
|
|
217
|
+
});
|
|
218
|
+
if (input.files.length > SKILL_RESOURCE_MAX_ENTRIES) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function resolveResourcePath(
|
|
225
|
+
skill: LoadedSkill,
|
|
226
|
+
logicalPath: string,
|
|
227
|
+
relativePath: string,
|
|
228
|
+
): Promise<string> {
|
|
229
|
+
try {
|
|
230
|
+
const canonical = await realpath(logicalPath);
|
|
231
|
+
if (!isWithin(skill.directory, canonical)) {
|
|
232
|
+
throw resourceError(
|
|
233
|
+
skill,
|
|
234
|
+
relativePath,
|
|
235
|
+
"resource resolves outside the skill directory",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return canonical;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (error instanceof SkillError) {
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
throw resourceError(skill, relativePath, error);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function failed(
|
|
248
|
+
name: string,
|
|
249
|
+
errorCode: string,
|
|
250
|
+
error: string,
|
|
251
|
+
): {
|
|
252
|
+
ok: false;
|
|
253
|
+
status: "failed";
|
|
254
|
+
name: string;
|
|
255
|
+
errorCode: string;
|
|
256
|
+
error: string;
|
|
257
|
+
} {
|
|
258
|
+
return { ok: false, status: "failed", name, errorCode, error };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function resourceError(
|
|
262
|
+
skill: LoadedSkill,
|
|
263
|
+
relativePath: string,
|
|
264
|
+
cause: unknown,
|
|
265
|
+
): SkillError {
|
|
266
|
+
return new SkillError(
|
|
267
|
+
"SKILL_RESOURCE_INVALID",
|
|
268
|
+
`Agent Skill ${skill.scope}/${skill.name} resource ${relativePath} is invalid: ${errorMessage(cause)}.`,
|
|
269
|
+
{ cause },
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function isWithin(root: string, candidate: string): boolean {
|
|
274
|
+
const relative = path.relative(root, candidate);
|
|
275
|
+
return (
|
|
276
|
+
relative !== ".." &&
|
|
277
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
278
|
+
!path.isAbsolute(relative)
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isErrno(error: unknown, code: string): boolean {
|
|
283
|
+
return (
|
|
284
|
+
typeof error === "object" &&
|
|
285
|
+
error !== null &&
|
|
286
|
+
"code" in error &&
|
|
287
|
+
error.code === code
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function compareText(left: string, right: string): number {
|
|
292
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function errorMessage(error: unknown): string {
|
|
296
|
+
return error instanceof Error ? error.message : String(error);
|
|
297
|
+
}
|
package/src/tools/edit.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { Buffer } from "node:buffer";
|
|
3
2
|
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
4
3
|
import { throwIfTurnCancelled } from "../agent/turn-cancellation";
|
|
5
4
|
import { computeFilePatch } from "./file-diff";
|
|
6
|
-
import {
|
|
5
|
+
import { ensureParentDirectory } from "./ensure-parent-directory";
|
|
6
|
+
import { sha256Bytes, sha256Text } from "./hash";
|
|
7
7
|
import { resolveWorkspacePath } from "./path-safety";
|
|
8
8
|
import { defineToolExecutor } from "./types";
|
|
9
9
|
import type {
|
|
10
10
|
EditFileRawResult,
|
|
11
|
-
|
|
11
|
+
FileSnapshotStore,
|
|
12
12
|
ToolExecutionContext,
|
|
13
13
|
ToolExecutor,
|
|
14
14
|
} from "./types";
|
|
@@ -22,7 +22,7 @@ type EditArgs = {
|
|
|
22
22
|
|
|
23
23
|
export type EditToolOptions = {
|
|
24
24
|
workspaceRoot: string;
|
|
25
|
-
snapshots:
|
|
25
|
+
snapshots: FileSnapshotStore;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
export function createEditToolExecutor(options: EditToolOptions): ToolExecutor {
|
|
@@ -30,7 +30,7 @@ export function createEditToolExecutor(options: EditToolOptions): ToolExecutor {
|
|
|
30
30
|
definition: {
|
|
31
31
|
name: "Edit",
|
|
32
32
|
description:
|
|
33
|
-
"Replace an exact string in
|
|
33
|
+
"Replace an exact string in an existing file. Exact replacement requires a current version established by a successful Read, Write, or Edit; when none exists, call Read first, and a successful paginated Read is sufficient. Set old_string='' to create a file or write to an empty file; missing parent directories are created when creating a file.",
|
|
34
34
|
parameters: {
|
|
35
35
|
type: "object",
|
|
36
36
|
additionalProperties: false,
|
|
@@ -41,7 +41,8 @@ export function createEditToolExecutor(options: EditToolOptions): ToolExecutor {
|
|
|
41
41
|
},
|
|
42
42
|
old_string: {
|
|
43
43
|
type: "string",
|
|
44
|
-
description:
|
|
44
|
+
description:
|
|
45
|
+
"Exact text to replace. Use an empty string only to create a file or write to an empty file.",
|
|
45
46
|
},
|
|
46
47
|
new_string: {
|
|
47
48
|
type: "string",
|
|
@@ -84,17 +85,6 @@ export function createEditToolExecutor(options: EditToolOptions): ToolExecutor {
|
|
|
84
85
|
};
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
const parentPath = path.dirname(absolutePath);
|
|
88
|
-
const parentCheck = await directoryExists(parentPath);
|
|
89
|
-
if (!parentCheck.ok) {
|
|
90
|
-
return {
|
|
91
|
-
ok: false,
|
|
92
|
-
filePath: input.file_path,
|
|
93
|
-
absolutePath,
|
|
94
|
-
error: parentCheck.error,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
88
|
const target = await targetFileState(absolutePath);
|
|
99
89
|
if (!target.ok) {
|
|
100
90
|
return {
|
|
@@ -126,7 +116,7 @@ export function createEditToolExecutor(options: EditToolOptions): ToolExecutor {
|
|
|
126
116
|
}
|
|
127
117
|
|
|
128
118
|
const snapshot = options.snapshots.get(absolutePath);
|
|
129
|
-
if (snapshot === undefined
|
|
119
|
+
if (snapshot === undefined) {
|
|
130
120
|
return {
|
|
131
121
|
ok: false,
|
|
132
122
|
filePath: input.file_path,
|
|
@@ -136,15 +126,14 @@ export function createEditToolExecutor(options: EditToolOptions): ToolExecutor {
|
|
|
136
126
|
};
|
|
137
127
|
}
|
|
138
128
|
|
|
139
|
-
if (target.
|
|
129
|
+
if (target.sha256 !== snapshot.sha256) {
|
|
140
130
|
return {
|
|
141
131
|
ok: false,
|
|
142
132
|
filePath: input.file_path,
|
|
143
133
|
absolutePath,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
error:
|
|
147
|
-
"File changed after it was last read or edited. Read it again before Edit.",
|
|
134
|
+
currentSha256: target.sha256,
|
|
135
|
+
lastObservedSha256: snapshot.sha256,
|
|
136
|
+
error: "File changed after it was last observed. Read it again before Edit.",
|
|
148
137
|
};
|
|
149
138
|
}
|
|
150
139
|
|
|
@@ -181,7 +170,7 @@ export function createEditToolExecutor(options: EditToolOptions): ToolExecutor {
|
|
|
181
170
|
replacementCount: input.replace_all ? replacementCount : 1,
|
|
182
171
|
replaceAll: input.replace_all,
|
|
183
172
|
created: false,
|
|
184
|
-
|
|
173
|
+
expectedSha256: target.sha256,
|
|
185
174
|
readRequiredOnChange: true,
|
|
186
175
|
snapshots: options.snapshots,
|
|
187
176
|
signal: context.signal,
|
|
@@ -229,7 +218,7 @@ async function writeEmptyTarget(input: {
|
|
|
229
218
|
absolutePath: string;
|
|
230
219
|
target: TargetFileState;
|
|
231
220
|
newContent: string;
|
|
232
|
-
snapshots:
|
|
221
|
+
snapshots: FileSnapshotStore;
|
|
233
222
|
signal: AbortSignal;
|
|
234
223
|
}): Promise<EditFileRawResult> {
|
|
235
224
|
if (input.target.exists && input.target.content.length > 0) {
|
|
@@ -250,7 +239,7 @@ async function writeEmptyTarget(input: {
|
|
|
250
239
|
replacementCount: 0,
|
|
251
240
|
replaceAll: false,
|
|
252
241
|
created: !input.target.exists,
|
|
253
|
-
|
|
242
|
+
expectedSha256: input.target.exists ? input.target.sha256 : undefined,
|
|
254
243
|
readRequiredOnChange: false,
|
|
255
244
|
snapshots: input.snapshots,
|
|
256
245
|
signal: input.signal,
|
|
@@ -266,15 +255,15 @@ async function writeEditedContent(input: {
|
|
|
266
255
|
replacementCount: number;
|
|
267
256
|
replaceAll: boolean;
|
|
268
257
|
created: boolean;
|
|
269
|
-
|
|
258
|
+
expectedSha256?: string;
|
|
270
259
|
readRequiredOnChange: boolean;
|
|
271
|
-
snapshots:
|
|
260
|
+
snapshots: FileSnapshotStore;
|
|
272
261
|
signal: AbortSignal;
|
|
273
262
|
}): Promise<EditFileRawResult> {
|
|
274
263
|
throwIfTurnCancelled(input.signal);
|
|
275
264
|
|
|
276
|
-
if (input.
|
|
277
|
-
const currentState = await
|
|
265
|
+
if (input.expectedSha256 !== undefined) {
|
|
266
|
+
const currentState = await targetFileState(input.absolutePath);
|
|
278
267
|
throwIfTurnCancelled(input.signal);
|
|
279
268
|
|
|
280
269
|
if (!currentState.ok) {
|
|
@@ -286,13 +275,22 @@ async function writeEditedContent(input: {
|
|
|
286
275
|
};
|
|
287
276
|
}
|
|
288
277
|
|
|
289
|
-
if (currentState.
|
|
278
|
+
if (!currentState.exists) {
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
filePath: input.filePath,
|
|
282
|
+
absolutePath: input.absolutePath,
|
|
283
|
+
error: "File changed while Edit was being prepared: File does not exist.",
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (currentState.sha256 !== input.expectedSha256) {
|
|
290
288
|
return {
|
|
291
289
|
ok: false,
|
|
292
290
|
filePath: input.filePath,
|
|
293
291
|
absolutePath: input.absolutePath,
|
|
294
|
-
|
|
295
|
-
|
|
292
|
+
currentSha256: currentState.sha256,
|
|
293
|
+
lastObservedSha256: input.expectedSha256,
|
|
296
294
|
error: input.readRequiredOnChange
|
|
297
295
|
? "File changed while Edit was being prepared. Read it again before Edit."
|
|
298
296
|
: "File changed while Edit was being prepared. Retry Edit with the current file state.",
|
|
@@ -300,6 +298,20 @@ async function writeEditedContent(input: {
|
|
|
300
298
|
}
|
|
301
299
|
}
|
|
302
300
|
|
|
301
|
+
if (input.created) {
|
|
302
|
+
try {
|
|
303
|
+
await ensureParentDirectory(input.absolutePath);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return {
|
|
306
|
+
ok: false,
|
|
307
|
+
filePath: input.filePath,
|
|
308
|
+
absolutePath: input.absolutePath,
|
|
309
|
+
error: `Failed to create parent directory: ${errorMessage(error)}`,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
throwIfTurnCancelled(input.signal);
|
|
313
|
+
}
|
|
314
|
+
|
|
303
315
|
await writeFile(input.absolutePath, input.newContent, "utf8");
|
|
304
316
|
const newSha256 = sha256Text(input.newContent);
|
|
305
317
|
const writtenInfo = await stat(input.absolutePath);
|
|
@@ -330,33 +342,6 @@ async function writeEditedContent(input: {
|
|
|
330
342
|
};
|
|
331
343
|
}
|
|
332
344
|
|
|
333
|
-
async function directoryExists(
|
|
334
|
-
directoryPath: string,
|
|
335
|
-
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
336
|
-
try {
|
|
337
|
-
const info = await stat(directoryPath);
|
|
338
|
-
return info.isDirectory()
|
|
339
|
-
? { ok: true }
|
|
340
|
-
: { ok: false, error: "Parent path is not a directory." };
|
|
341
|
-
} catch {
|
|
342
|
-
return { ok: false, error: "Parent directory does not exist." };
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
async function fileMtime(
|
|
347
|
-
filePath: string,
|
|
348
|
-
): Promise<{ ok: true; mtimeMs: number } | { ok: false; error: string }> {
|
|
349
|
-
try {
|
|
350
|
-
const info = await stat(filePath);
|
|
351
|
-
if (!info.isFile()) {
|
|
352
|
-
return { ok: false, error: "Path is not a file." };
|
|
353
|
-
}
|
|
354
|
-
return { ok: true, mtimeMs: info.mtimeMs };
|
|
355
|
-
} catch (error) {
|
|
356
|
-
return { ok: false, error: errorMessage(error) };
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
345
|
type TargetFileState =
|
|
361
346
|
| { ok: true; exists: false }
|
|
362
347
|
| {
|
|
@@ -376,7 +361,7 @@ async function targetFileState(
|
|
|
376
361
|
return { ok: false, error: "Path is not a file." };
|
|
377
362
|
}
|
|
378
363
|
|
|
379
|
-
const
|
|
364
|
+
const bytes = await readFile(absolutePath);
|
|
380
365
|
const currentInfo = await stat(absolutePath);
|
|
381
366
|
if (currentInfo.mtimeMs > info.mtimeMs) {
|
|
382
367
|
return { ok: false, error: "File changed while it was being read." };
|
|
@@ -385,8 +370,8 @@ async function targetFileState(
|
|
|
385
370
|
return {
|
|
386
371
|
ok: true,
|
|
387
372
|
exists: true,
|
|
388
|
-
content,
|
|
389
|
-
sha256:
|
|
373
|
+
content: bytes.toString("utf8"),
|
|
374
|
+
sha256: sha256Bytes(bytes),
|
|
390
375
|
mtimeMs: currentInfo.mtimeMs,
|
|
391
376
|
};
|
|
392
377
|
} catch (error) {
|
|
@@ -415,11 +400,13 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
415
400
|
}
|
|
416
401
|
|
|
417
402
|
function isNotFound(error: unknown): boolean {
|
|
403
|
+
// ENOTDIR means a parent component is a file, so the target file cannot exist.
|
|
404
|
+
// Treat it as missing here so each Edit mode can apply its documented behavior.
|
|
418
405
|
return (
|
|
419
406
|
typeof error === "object" &&
|
|
420
407
|
error !== null &&
|
|
421
408
|
"code" in error &&
|
|
422
|
-
error.code === "ENOENT"
|
|
409
|
+
(error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
423
410
|
);
|
|
424
411
|
}
|
|
425
412
|
|
package/src/tools/read.ts
CHANGED
|
@@ -5,8 +5,8 @@ import { sha256Bytes } from "./hash";
|
|
|
5
5
|
import { resolveWorkspacePath } from "./path-safety";
|
|
6
6
|
import { defineToolExecutor } from "./types";
|
|
7
7
|
import type {
|
|
8
|
+
FileSnapshotStore,
|
|
8
9
|
ReadFileRawResult,
|
|
9
|
-
ReadSnapshotStore,
|
|
10
10
|
ToolExecutionContext,
|
|
11
11
|
ToolExecutor,
|
|
12
12
|
} from "./types";
|
|
@@ -19,7 +19,7 @@ type ReadArgs = {
|
|
|
19
19
|
|
|
20
20
|
export type ReadToolOptions = {
|
|
21
21
|
workspaceRoot: string;
|
|
22
|
-
snapshots:
|
|
22
|
+
snapshots: FileSnapshotStore;
|
|
23
23
|
maxContentBytes?: number;
|
|
24
24
|
};
|
|
25
25
|
|
package/src/tools/recall.ts
CHANGED
|
@@ -18,9 +18,51 @@ import {
|
|
|
18
18
|
type RecallRawResult,
|
|
19
19
|
type RecallToolErrorCode,
|
|
20
20
|
type ToolExecutionContext,
|
|
21
|
+
type ToolDefinition,
|
|
21
22
|
type ToolExecutor,
|
|
22
23
|
} from "./types";
|
|
23
24
|
|
|
25
|
+
export const RECALL_TOOL_DEFINITION: ToolDefinition = Object.freeze({
|
|
26
|
+
name: "Recall",
|
|
27
|
+
description:
|
|
28
|
+
"Search or retrieve immutable model-visible history from the current session. Search matches literal substrings: use a short distinctive anchor such as a path, symbol, project, command fragment, or error text, not a whole natural-language question. Results are historical snapshots and may differ from the current workspace. Use search to find a source, get to retrieve exact content, and Read/Grep for current files.",
|
|
29
|
+
parameters: {
|
|
30
|
+
type: "object",
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
properties: {
|
|
33
|
+
mode: { type: "string", enum: ["search", "get"] },
|
|
34
|
+
query: { type: "string", maxLength: 1024 },
|
|
35
|
+
roles: {
|
|
36
|
+
type: "array",
|
|
37
|
+
minItems: 1,
|
|
38
|
+
uniqueItems: true,
|
|
39
|
+
items: { type: "string", enum: ["user", "assistant", "tool"] },
|
|
40
|
+
},
|
|
41
|
+
tool_names: {
|
|
42
|
+
type: "array",
|
|
43
|
+
minItems: 1,
|
|
44
|
+
maxItems: 16,
|
|
45
|
+
uniqueItems: true,
|
|
46
|
+
items: { type: "string", minLength: 1 },
|
|
47
|
+
},
|
|
48
|
+
turn_from: { type: "integer", minimum: 1 },
|
|
49
|
+
turn_to: { type: "integer", minimum: 1 },
|
|
50
|
+
limit: { type: "integer", minimum: 1, maximum: 20, default: 10 },
|
|
51
|
+
offset: { type: "integer", minimum: 0, default: 0 },
|
|
52
|
+
snapshot_through_ordinal: { type: "integer", minimum: 1 },
|
|
53
|
+
source: { type: "string" },
|
|
54
|
+
byte_offset: { type: "integer", minimum: 0, default: 0 },
|
|
55
|
+
byte_limit: {
|
|
56
|
+
type: "integer",
|
|
57
|
+
minimum: 256,
|
|
58
|
+
maximum: 20_000,
|
|
59
|
+
default: 12_000,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
required: ["mode"],
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
24
66
|
type RecallSearchArgs = {
|
|
25
67
|
mode: "search";
|
|
26
68
|
query: string;
|
|
@@ -55,46 +97,7 @@ export function createRecallToolExecutor(options: {
|
|
|
55
97
|
historyReader: SessionHistoryReader;
|
|
56
98
|
}): ToolExecutor {
|
|
57
99
|
return defineToolExecutor("recall", {
|
|
58
|
-
definition:
|
|
59
|
-
name: "Recall",
|
|
60
|
-
description:
|
|
61
|
-
"Search or retrieve immutable model-visible history from the current session. Results are historical snapshots and may differ from the current workspace. Use search to find a source, get to retrieve exact content, and Read/Grep for current files.",
|
|
62
|
-
parameters: {
|
|
63
|
-
type: "object",
|
|
64
|
-
additionalProperties: false,
|
|
65
|
-
properties: {
|
|
66
|
-
mode: { type: "string", enum: ["search", "get"] },
|
|
67
|
-
query: { type: "string", maxLength: 1024 },
|
|
68
|
-
roles: {
|
|
69
|
-
type: "array",
|
|
70
|
-
minItems: 1,
|
|
71
|
-
uniqueItems: true,
|
|
72
|
-
items: { type: "string", enum: ["user", "assistant", "tool"] },
|
|
73
|
-
},
|
|
74
|
-
tool_names: {
|
|
75
|
-
type: "array",
|
|
76
|
-
minItems: 1,
|
|
77
|
-
maxItems: 16,
|
|
78
|
-
uniqueItems: true,
|
|
79
|
-
items: { type: "string", minLength: 1 },
|
|
80
|
-
},
|
|
81
|
-
turn_from: { type: "integer", minimum: 1 },
|
|
82
|
-
turn_to: { type: "integer", minimum: 1 },
|
|
83
|
-
limit: { type: "integer", minimum: 1, maximum: 20, default: 10 },
|
|
84
|
-
offset: { type: "integer", minimum: 0, default: 0 },
|
|
85
|
-
snapshot_through_ordinal: { type: "integer", minimum: 1 },
|
|
86
|
-
source: { type: "string" },
|
|
87
|
-
byte_offset: { type: "integer", minimum: 0, default: 0 },
|
|
88
|
-
byte_limit: {
|
|
89
|
-
type: "integer",
|
|
90
|
-
minimum: 256,
|
|
91
|
-
maximum: 20_000,
|
|
92
|
-
default: 12_000,
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
required: ["mode"],
|
|
96
|
-
},
|
|
97
|
-
},
|
|
100
|
+
definition: RECALL_TOOL_DEFINITION,
|
|
98
101
|
async execute(
|
|
99
102
|
args,
|
|
100
103
|
_call,
|