@mikode13/harness 4.0.1 → 5.1.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 +299 -17
- package/dist/agent/domain/agent.d.ts +32 -1
- package/dist/agent/domain/agent.js.map +1 -1
- package/dist/agent/domain/approval.d.ts +9 -0
- package/dist/agent/domain/approval.js.map +1 -1
- package/dist/agent/domain/runContext.d.ts +31 -0
- package/dist/agent/domain/runContext.js +47 -0
- package/dist/agent/domain/runContext.js.map +1 -0
- package/dist/diff/domain/unifiedDiff.d.ts +19 -0
- package/dist/diff/domain/unifiedDiff.js +166 -0
- package/dist/diff/domain/unifiedDiff.js.map +1 -0
- package/dist/engines/domain/model/historyFollower.d.ts +41 -0
- package/dist/engines/domain/model/historyFollower.js +158 -0
- package/dist/engines/domain/model/historyFollower.js.map +1 -0
- package/dist/engines/domain/model/llmAgent.d.ts +42 -4
- package/dist/engines/domain/model/llmAgent.js +287 -31
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/factory/infrastructure/agentFactory.d.ts +14 -0
- package/dist/factory/infrastructure/agentFactory.js +20 -2
- package/dist/factory/infrastructure/agentFactory.js.map +1 -1
- package/dist/factory/infrastructure/agentLLMFactory.d.ts +119 -14
- package/dist/factory/infrastructure/agentLLMFactory.js +284 -57
- package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
- package/dist/index.d.ts +13 -5
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/llm/domain/conversation.d.ts +44 -4
- package/dist/llm/domain/conversation.js +56 -7
- package/dist/llm/domain/conversation.js.map +1 -1
- package/dist/llm/domain/llm.d.ts +3 -1
- package/dist/llm/domain/llm.js.map +1 -1
- package/dist/llm/domain/tool.d.ts +13 -0
- package/dist/llm/domain/tool.js +12 -1
- package/dist/llm/domain/tool.js.map +1 -1
- package/dist/llm/infrastructure/claudeLLMClient.d.ts +3 -2
- package/dist/llm/infrastructure/claudeLLMClient.js +38 -10
- package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -1
- package/dist/llm/infrastructure/openAILLMClient.d.ts +2 -1
- package/dist/llm/infrastructure/openAILLMClient.js +67 -17
- package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
- package/dist/orchestration/domain/model/orchestratorAgent.d.ts +17 -3
- package/dist/orchestration/domain/model/orchestratorAgent.js +65 -8
- package/dist/orchestration/domain/model/orchestratorAgent.js.map +1 -1
- package/dist/recovery/domain/fileChanges.d.ts +31 -0
- package/dist/recovery/domain/fileChanges.js +48 -0
- package/dist/recovery/domain/fileChanges.js.map +1 -0
- package/dist/recovery/domain/history.d.ts +114 -0
- package/dist/recovery/domain/history.js +62 -0
- package/dist/recovery/domain/history.js.map +1 -0
- package/dist/recovery/domain/recoveryStore.d.ts +233 -0
- package/dist/recovery/domain/recoveryStore.js +32 -0
- package/dist/recovery/domain/recoveryStore.js.map +1 -0
- package/dist/recovery/domain/retention.d.ts +33 -0
- package/dist/recovery/domain/retention.js +90 -0
- package/dist/recovery/domain/retention.js.map +1 -0
- package/dist/recovery/domain/runChanges.d.ts +37 -0
- package/dist/recovery/domain/runChanges.js +109 -0
- package/dist/recovery/domain/runChanges.js.map +1 -0
- package/dist/recovery/domain/runTree.d.ts +32 -0
- package/dist/recovery/domain/runTree.js +90 -0
- package/dist/recovery/domain/runTree.js.map +1 -0
- package/dist/recovery/infrastructure/createHistory.d.ts +11 -0
- package/dist/recovery/infrastructure/createHistory.js +17 -0
- package/dist/recovery/infrastructure/createHistory.js.map +1 -0
- package/dist/recovery/infrastructure/fileRecoveryStore.d.ts +133 -0
- package/dist/recovery/infrastructure/fileRecoveryStore.js +1043 -0
- package/dist/recovery/infrastructure/fileRecoveryStore.js.map +1 -0
- package/dist/recovery/infrastructure/workspaceFiles.d.ts +30 -0
- package/dist/recovery/infrastructure/workspaceFiles.js +125 -0
- package/dist/recovery/infrastructure/workspaceFiles.js.map +1 -0
- package/dist/tools/domain/accessPolicy.d.ts +30 -0
- package/dist/tools/domain/accessPolicy.js +11 -0
- package/dist/tools/domain/accessPolicy.js.map +1 -0
- package/dist/tools/domain/commands.d.ts +93 -0
- package/dist/tools/domain/commands.js +191 -0
- package/dist/tools/domain/commands.js.map +1 -0
- package/dist/tools/domain/editResponse.d.ts +14 -0
- package/dist/tools/domain/editResponse.js +79 -0
- package/dist/tools/domain/editResponse.js.map +1 -0
- package/dist/tools/domain/fileEdits.d.ts +64 -0
- package/dist/tools/domain/fileEdits.js +21 -0
- package/dist/tools/domain/fileEdits.js.map +1 -0
- package/dist/tools/domain/lineLength.d.ts +7 -0
- package/dist/tools/domain/lineLength.js +26 -0
- package/dist/tools/domain/lineLength.js.map +1 -0
- package/dist/tools/domain/preparedCall.d.ts +44 -0
- package/dist/tools/domain/preparedCall.js +26 -0
- package/dist/tools/domain/preparedCall.js.map +1 -0
- package/dist/tools/domain/readRegistry.d.ts +46 -0
- package/dist/tools/domain/readRegistry.js +62 -0
- package/dist/tools/domain/readRegistry.js.map +1 -0
- package/dist/tools/domain/textLines.d.ts +26 -0
- package/dist/tools/domain/textLines.js +44 -0
- package/dist/tools/domain/textLines.js.map +1 -0
- package/dist/tools/domain/tool.d.ts +14 -0
- package/dist/tools/domain/tool.js +7 -1
- package/dist/tools/domain/tool.js.map +1 -1
- package/dist/tools/domain/v4aPatch.d.ts +17 -0
- package/dist/tools/domain/v4aPatch.js +236 -0
- package/dist/tools/domain/v4aPatch.js.map +1 -0
- package/dist/tools/infrastructure/boundedWorkspace.d.ts +11 -2
- package/dist/tools/infrastructure/boundedWorkspace.js +9 -1
- package/dist/tools/infrastructure/boundedWorkspace.js.map +1 -1
- package/dist/tools/infrastructure/commandCheckpoint.d.ts +55 -0
- package/dist/tools/infrastructure/commandCheckpoint.js +178 -0
- package/dist/tools/infrastructure/commandCheckpoint.js.map +1 -0
- package/dist/tools/infrastructure/commandProcess.d.ts +47 -0
- package/dist/tools/infrastructure/commandProcess.js +217 -0
- package/dist/tools/infrastructure/commandProcess.js.map +1 -0
- package/dist/tools/infrastructure/commandTool.d.ts +36 -0
- package/dist/tools/infrastructure/commandTool.js +285 -0
- package/dist/tools/infrastructure/commandTool.js.map +1 -0
- package/dist/tools/infrastructure/createWorkspace.d.ts +10 -0
- package/dist/tools/infrastructure/createWorkspace.js +11 -3
- package/dist/tools/infrastructure/createWorkspace.js.map +1 -1
- package/dist/tools/infrastructure/defineTool.d.ts +18 -2
- package/dist/tools/infrastructure/defineTool.js +52 -6
- package/dist/tools/infrastructure/defineTool.js.map +1 -1
- package/dist/tools/infrastructure/editFormats.d.ts +22 -0
- package/dist/tools/infrastructure/editFormats.js +243 -0
- package/dist/tools/infrastructure/editFormats.js.map +1 -0
- package/dist/tools/infrastructure/fileEditor.d.ts +64 -0
- package/dist/tools/infrastructure/fileEditor.js +306 -0
- package/dist/tools/infrastructure/fileEditor.js.map +1 -0
- package/dist/tools/infrastructure/fileTools.d.ts +81 -0
- package/dist/tools/infrastructure/fileTools.js +152 -0
- package/dist/tools/infrastructure/fileTools.js.map +1 -0
- package/dist/tools/infrastructure/gitIgnoreRules.d.ts +15 -0
- package/dist/tools/infrastructure/gitIgnoreRules.js +74 -0
- package/dist/tools/infrastructure/gitIgnoreRules.js.map +1 -0
- package/dist/tools/infrastructure/gitWorkspace.d.ts +1 -1
- package/dist/tools/infrastructure/gitWorkspace.js +1 -1
- package/dist/tools/infrastructure/gitWorkspace.js.map +1 -1
- package/dist/tools/infrastructure/ignoreFileChanges.d.ts +14 -0
- package/dist/tools/infrastructure/ignoreFileChanges.js +62 -0
- package/dist/tools/infrastructure/ignoreFileChanges.js.map +1 -0
- package/dist/tools/infrastructure/policyWorkspace.d.ts +52 -0
- package/dist/tools/infrastructure/policyWorkspace.js +32 -0
- package/dist/tools/infrastructure/policyWorkspace.js.map +1 -0
- package/dist/tools/infrastructure/ripgrepWorkspace.d.ts +2 -1
- package/dist/tools/infrastructure/ripgrepWorkspace.js +1 -1
- package/dist/tools/infrastructure/ripgrepWorkspace.js.map +1 -1
- package/dist/tools/infrastructure/rootsAccessPolicy.d.ts +100 -0
- package/dist/tools/infrastructure/rootsAccessPolicy.js +301 -0
- package/dist/tools/infrastructure/rootsAccessPolicy.js.map +1 -0
- package/dist/tools/infrastructure/runProcess.d.ts +4 -1
- package/dist/tools/infrastructure/runProcess.js +4 -2
- package/dist/tools/infrastructure/runProcess.js.map +1 -1
- package/dist/tools/infrastructure/secretPaths.d.ts +8 -0
- package/dist/tools/infrastructure/secretPaths.js +76 -0
- package/dist/tools/infrastructure/secretPaths.js.map +1 -0
- package/dist/tools/infrastructure/showChangesTool.d.ts +12 -0
- package/dist/tools/infrastructure/showChangesTool.js +44 -0
- package/dist/tools/infrastructure/showChangesTool.js.map +1 -0
- package/dist/tools/infrastructure/trackedEdits.d.ts +33 -0
- package/dist/tools/infrastructure/trackedEdits.js +70 -0
- package/dist/tools/infrastructure/trackedEdits.js.map +1 -0
- package/dist/tools/infrastructure/trackedReadFile.d.ts +21 -0
- package/dist/tools/infrastructure/trackedReadFile.js +79 -0
- package/dist/tools/infrastructure/trackedReadFile.js.map +1 -0
- package/dist/tools/infrastructure/workspaceTools.d.ts +34 -0
- package/dist/tools/infrastructure/workspaceTools.js +69 -40
- package/dist/tools/infrastructure/workspaceTools.js.map +1 -1
- package/dist/tools/infrastructure/workspaceWrites.d.ts +23 -0
- package/dist/tools/infrastructure/workspaceWrites.js +42 -0
- package/dist/tools/infrastructure/workspaceWrites.js.map +1 -0
- package/dist/tools/infrastructure/writeSession.d.ts +35 -0
- package/dist/tools/infrastructure/writeSession.js +81 -0
- package/dist/tools/infrastructure/writeSession.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Whether a file or folder name is one of the usual credential files. */
|
|
2
|
+
export declare function isSecretName(name: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Whether an absolute path names a file the harness treats as a secret by default. It goes by
|
|
5
|
+
* names and places, so it is a filter for the usual suspects, not a detector: a key pasted into
|
|
6
|
+
* an ordinary source file is not found.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isSecretPath(path: string, home: string): boolean;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isAbsolute, relative, sep } from 'node:path';
|
|
2
|
+
// Every name is compared in lower case: on macOS `.ENV` is the same file as `.env`.
|
|
3
|
+
/** File names that hold credentials wherever they are. */
|
|
4
|
+
const secretNames = new Set([
|
|
5
|
+
'.env',
|
|
6
|
+
'.envrc',
|
|
7
|
+
'.netrc',
|
|
8
|
+
'.pgpass',
|
|
9
|
+
'.git-credentials',
|
|
10
|
+
'id_rsa',
|
|
11
|
+
'id_dsa',
|
|
12
|
+
'id_ecdsa',
|
|
13
|
+
'id_ed25519',
|
|
14
|
+
'id_ecdsa_sk',
|
|
15
|
+
'id_ed25519_sk',
|
|
16
|
+
]);
|
|
17
|
+
/** Endings of key stores, private keys and state files that embed secrets. */
|
|
18
|
+
const secretEndings = [
|
|
19
|
+
'.pem',
|
|
20
|
+
'.key',
|
|
21
|
+
'.p12',
|
|
22
|
+
'.pfx',
|
|
23
|
+
'.jks',
|
|
24
|
+
'.keystore',
|
|
25
|
+
'.tfstate',
|
|
26
|
+
'.tfstate.backup',
|
|
27
|
+
];
|
|
28
|
+
/** `.env.local` and its kind hold real values; these endings mark a template to copy from. */
|
|
29
|
+
const environmentTemplates = ['.example', '.sample', '.template'];
|
|
30
|
+
/** Folders under the home directory that only hold credentials. */
|
|
31
|
+
const secretHomeFolders = [
|
|
32
|
+
'.ssh',
|
|
33
|
+
'.aws',
|
|
34
|
+
'.gnupg',
|
|
35
|
+
'.azure',
|
|
36
|
+
'.kube',
|
|
37
|
+
'.password-store',
|
|
38
|
+
'.config/gcloud',
|
|
39
|
+
'.config/gh',
|
|
40
|
+
'Library/Keychains',
|
|
41
|
+
].map(folder => folder.toLowerCase());
|
|
42
|
+
/** Files under the home directory that hold registry or tool tokens. */
|
|
43
|
+
const secretHomeFiles = ['.npmrc', '.pypirc', '.docker/config.json', '.config/git/credentials'].map(file => file.toLowerCase());
|
|
44
|
+
/** `path` below `folder`, as lower-case components, or undefined when it is not inside. */
|
|
45
|
+
function inside(folder, path) {
|
|
46
|
+
const below = relative(folder.toLowerCase(), path.toLowerCase());
|
|
47
|
+
if (below === '..' || below.startsWith(`..${sep}`) || isAbsolute(below))
|
|
48
|
+
return undefined;
|
|
49
|
+
return below.split(sep).join('/');
|
|
50
|
+
}
|
|
51
|
+
/** Whether a file or folder name is one of the usual credential files. */
|
|
52
|
+
export function isSecretName(name) {
|
|
53
|
+
const lower = name.toLowerCase();
|
|
54
|
+
if (secretNames.has(lower))
|
|
55
|
+
return true;
|
|
56
|
+
if (lower.startsWith('.env.')) {
|
|
57
|
+
return !environmentTemplates.some(ending => lower.endsWith(ending));
|
|
58
|
+
}
|
|
59
|
+
return secretEndings.some(ending => lower.endsWith(ending));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Whether an absolute path names a file the harness treats as a secret by default. It goes by
|
|
63
|
+
* names and places, so it is a filter for the usual suspects, not a detector: a key pasted into
|
|
64
|
+
* an ordinary source file is not found.
|
|
65
|
+
*/
|
|
66
|
+
export function isSecretPath(path, home) {
|
|
67
|
+
const name = path.split(sep).at(-1) ?? '';
|
|
68
|
+
if (isSecretName(name))
|
|
69
|
+
return true;
|
|
70
|
+
const underHome = inside(home, path);
|
|
71
|
+
if (underHome === undefined || underHome === '')
|
|
72
|
+
return false;
|
|
73
|
+
return (secretHomeFolders.some(folder => underHome === folder || underHome.startsWith(`${folder}/`)) ||
|
|
74
|
+
secretHomeFiles.includes(underHome));
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=secretPaths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secretPaths.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/secretPaths.ts"],"names":[],"mappings":"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","sourcesContent":["import { isAbsolute, relative, sep } from 'node:path';\n\n// Every name is compared in lower case: on macOS `.ENV` is the same file as `.env`.\n\n/** File names that hold credentials wherever they are. */\nconst secretNames = new Set([\n\t'.env',\n\t'.envrc',\n\t'.netrc',\n\t'.pgpass',\n\t'.git-credentials',\n\t'id_rsa',\n\t'id_dsa',\n\t'id_ecdsa',\n\t'id_ed25519',\n\t'id_ecdsa_sk',\n\t'id_ed25519_sk',\n]);\n\n/** Endings of key stores, private keys and state files that embed secrets. */\nconst secretEndings = [\n\t'.pem',\n\t'.key',\n\t'.p12',\n\t'.pfx',\n\t'.jks',\n\t'.keystore',\n\t'.tfstate',\n\t'.tfstate.backup',\n];\n\n/** `.env.local` and its kind hold real values; these endings mark a template to copy from. */\nconst environmentTemplates = ['.example', '.sample', '.template'];\n\n/** Folders under the home directory that only hold credentials. */\nconst secretHomeFolders = [\n\t'.ssh',\n\t'.aws',\n\t'.gnupg',\n\t'.azure',\n\t'.kube',\n\t'.password-store',\n\t'.config/gcloud',\n\t'.config/gh',\n\t'Library/Keychains',\n].map(folder => folder.toLowerCase());\n\n/** Files under the home directory that hold registry or tool tokens. */\nconst secretHomeFiles = ['.npmrc', '.pypirc', '.docker/config.json', '.config/git/credentials'].map(\n\tfile => file.toLowerCase(),\n);\n\n/** `path` below `folder`, as lower-case components, or undefined when it is not inside. */\nfunction inside(folder: string, path: string): string | undefined {\n\tconst below = relative(folder.toLowerCase(), path.toLowerCase());\n\tif (below === '..' || below.startsWith(`..${sep}`) || isAbsolute(below)) return undefined;\n\treturn below.split(sep).join('/');\n}\n\n/** Whether a file or folder name is one of the usual credential files. */\nexport function isSecretName(name: string): boolean {\n\tconst lower = name.toLowerCase();\n\tif (secretNames.has(lower)) return true;\n\tif (lower.startsWith('.env.')) {\n\t\treturn !environmentTemplates.some(ending => lower.endsWith(ending));\n\t}\n\treturn secretEndings.some(ending => lower.endsWith(ending));\n}\n\n/**\n * Whether an absolute path names a file the harness treats as a secret by default. It goes by\n * names and places, so it is a filter for the usual suspects, not a detector: a key pasted into\n * an ordinary source file is not found.\n */\nexport function isSecretPath(path: string, home: string): boolean {\n\tconst name = path.split(sep).at(-1) ?? '';\n\tif (isSecretName(name)) return true;\n\n\tconst underHome = inside(home, path);\n\tif (underHome === undefined || underHome === '') return false;\n\treturn (\n\t\tsecretHomeFolders.some(folder => underHome === folder || underHome.startsWith(`${folder}/`)) ||\n\t\tsecretHomeFiles.includes(underHome)\n\t);\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RecoveryStore } from '#src/recovery/domain/recoveryStore';
|
|
2
|
+
import { type AccessPolicy } from '../domain/accessPolicy.ts';
|
|
3
|
+
import type { PreparingTool } from '../domain/preparedCall.ts';
|
|
4
|
+
/**
|
|
5
|
+
* `showChanges` for the planner and the reviewer: what the current task has changed so far, as a
|
|
6
|
+
* diff per file, from the run's own record. It shows only what the role may read: a secret the
|
|
7
|
+
* executor was allowed to write is counted, never shown.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createShowChangesTool({ store, policy, }: {
|
|
10
|
+
store: RecoveryStore;
|
|
11
|
+
policy: AccessPolicy;
|
|
12
|
+
}): PreparingTool;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { showChanges } from '#src/recovery/domain/runChanges';
|
|
3
|
+
import { AccessDeniedError } from "../domain/accessPolicy.js";
|
|
4
|
+
import { definePreparingTool } from "./defineTool.js";
|
|
5
|
+
// A reviewer reads a change whole, but not a rewrite of the whole repository.
|
|
6
|
+
const maxLines = 600;
|
|
7
|
+
const nothingYet = 'Nothing has been changed in this task yet.';
|
|
8
|
+
/**
|
|
9
|
+
* `showChanges` for the planner and the reviewer: what the current task has changed so far, as a
|
|
10
|
+
* diff per file, from the run's own record. It shows only what the role may read: a secret the
|
|
11
|
+
* executor was allowed to write is counted, never shown.
|
|
12
|
+
*/
|
|
13
|
+
export function createShowChangesTool({ store, policy, }) {
|
|
14
|
+
return definePreparingTool({
|
|
15
|
+
name: 'showChanges',
|
|
16
|
+
description: 'Shows what this task has changed in the repository so far, as a diff per file, in the order each file was first changed.',
|
|
17
|
+
input: z.object({}),
|
|
18
|
+
summarize: () => 'changes in this task',
|
|
19
|
+
prepare: (_, __, { run }) => Promise.resolve({
|
|
20
|
+
risk: 'safe',
|
|
21
|
+
run: async (signal) => {
|
|
22
|
+
const { runId } = run.historyRun;
|
|
23
|
+
if (runId === undefined)
|
|
24
|
+
return nothingYet;
|
|
25
|
+
const diff = await showChanges(store, runId, {
|
|
26
|
+
maxLines,
|
|
27
|
+
visible: async (path) => {
|
|
28
|
+
try {
|
|
29
|
+
await policy.check(path, 'read', signal);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof AccessDeniedError)
|
|
34
|
+
return false;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
return diff === '' ? nothingYet : diff;
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=showChangesTool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"showChangesTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/showChangesTool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAqB,MAAM,2BAA2B,CAAC;AAEjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEtD,8EAA8E;AAC9E,MAAM,QAAQ,GAAG,GAAG,CAAC;AAErB,MAAM,UAAU,GAAG,4CAA4C,CAAC;AAEhE;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,EACrC,KAAK,EACL,MAAM,GAIN;IACA,OAAO,mBAAmB,CAAC;QAC1B,IAAI,EAAE,aAAa;QACnB,WAAW,EACV,0HAA0H;QAC3H,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;QACnB,SAAS,EAAE,GAAG,EAAE,CAAC,sBAAsB;QACvC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,CAC3B,OAAO,CAAC,OAAO,CAAC;YACf,IAAI,EAAE,MAAM;YACZ,GAAG,EAAE,KAAK,EAAC,MAAM,EAAC,EAAE;gBACnB,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC,UAAU,CAAC;gBACjC,IAAI,KAAK,KAAK,SAAS;oBAAE,OAAO,UAAU,CAAC;gBAC3C,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE;oBAC5C,QAAQ;oBACR,OAAO,EAAE,KAAK,EAAC,IAAI,EAAC,EAAE;wBACrB,IAAI,CAAC;4BACJ,MAAM,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;4BACzC,OAAO,IAAI,CAAC;wBACb,CAAC;wBAAC,OAAO,KAAK,EAAE,CAAC;4BAChB,IAAI,KAAK,YAAY,iBAAiB;gCAAE,OAAO,KAAK,CAAC;4BACrD,MAAM,KAAK,CAAC;wBACb,CAAC;oBACF,CAAC;iBACD,CAAC,CAAC;gBACH,OAAO,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;YACxC,CAAC;SACD,CAAC;KACH,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import { z } from 'zod';\nimport type { RecoveryStore } from '#src/recovery/domain/recoveryStore';\nimport { showChanges } from '#src/recovery/domain/runChanges';\nimport { AccessDeniedError, type AccessPolicy } from '../domain/accessPolicy.ts';\nimport type { PreparingTool } from '../domain/preparedCall.ts';\nimport { definePreparingTool } from './defineTool.ts';\n\n// A reviewer reads a change whole, but not a rewrite of the whole repository.\nconst maxLines = 600;\n\nconst nothingYet = 'Nothing has been changed in this task yet.';\n\n/**\n * `showChanges` for the planner and the reviewer: what the current task has changed so far, as a\n * diff per file, from the run's own record. It shows only what the role may read: a secret the\n * executor was allowed to write is counted, never shown.\n */\nexport function createShowChangesTool({\n\tstore,\n\tpolicy,\n}: {\n\tstore: RecoveryStore;\n\tpolicy: AccessPolicy;\n}): PreparingTool {\n\treturn definePreparingTool({\n\t\tname: 'showChanges',\n\t\tdescription:\n\t\t\t'Shows what this task has changed in the repository so far, as a diff per file, in the order each file was first changed.',\n\t\tinput: z.object({}),\n\t\tsummarize: () => 'changes in this task',\n\t\tprepare: (_, __, { run }) =>\n\t\t\tPromise.resolve({\n\t\t\t\trisk: 'safe',\n\t\t\t\trun: async signal => {\n\t\t\t\t\tconst { runId } = run.historyRun;\n\t\t\t\t\tif (runId === undefined) return nothingYet;\n\t\t\t\t\tconst diff = await showChanges(store, runId, {\n\t\t\t\t\t\tmaxLines,\n\t\t\t\t\t\tvisible: async path => {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tawait policy.check(path, 'read', signal);\n\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t\tif (error instanceof AccessDeniedError) return false;\n\t\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\treturn diff === '' ? nothingYet : diff;\n\t\t\t\t},\n\t\t\t}),\n\t});\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { PreparedCall } from '../domain/preparedCall.ts';
|
|
2
|
+
import type { ToolCallContext } from '../domain/readRegistry.ts';
|
|
3
|
+
import type { WorkspaceWrites } from './workspaceWrites.ts';
|
|
4
|
+
/**
|
|
5
|
+
* One edit, as an edit format asks for it, with the path as the model wrote it. A modification
|
|
6
|
+
* works its new content out from what the file holds now, the version the model read; it throws
|
|
7
|
+
* `EditRefusedError` with what the model needs to fix the call, such as a text that matched
|
|
8
|
+
* twice.
|
|
9
|
+
*/
|
|
10
|
+
export type EditRequest = {
|
|
11
|
+
kind: 'create';
|
|
12
|
+
path: string;
|
|
13
|
+
content: Buffer;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'modify';
|
|
16
|
+
path: string;
|
|
17
|
+
change: (current: Buffer) => Buffer;
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'delete';
|
|
20
|
+
path: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Prepares an edit from the version of the file this conversation read, and only from it:
|
|
24
|
+
*
|
|
25
|
+
* - a file it never read is `READ_REQUIRED`;
|
|
26
|
+
* - a file changed since it read it is `STALE_FILE`;
|
|
27
|
+
* - a new file needs a path that does not exist.
|
|
28
|
+
*
|
|
29
|
+
* Once applied, the conversation knows the file as the edit left it, so it can edit it again
|
|
30
|
+
* without reading it. What it returns to the model is short: the lines around a change, or a
|
|
31
|
+
* confirmation.
|
|
32
|
+
*/
|
|
33
|
+
export declare function prepareTrackedEdit(writes: WorkspaceWrites, request: EditRequest, signal: AbortSignal, { run, reads }: ToolCallContext): Promise<PreparedCall>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { describeCreation, describeDeletion, describeModification, editDiff, } from "../domain/editResponse.js";
|
|
3
|
+
import { EditRefusedError } from "../domain/fileEdits.js";
|
|
4
|
+
import { editRisk } from "./fileEditor.js";
|
|
5
|
+
function hashOf(content) {
|
|
6
|
+
return createHash('sha256').update(content).digest('hex');
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Prepares an edit from the version of the file this conversation read, and only from it:
|
|
10
|
+
*
|
|
11
|
+
* - a file it never read is `READ_REQUIRED`;
|
|
12
|
+
* - a file changed since it read it is `STALE_FILE`;
|
|
13
|
+
* - a new file needs a path that does not exist.
|
|
14
|
+
*
|
|
15
|
+
* Once applied, the conversation knows the file as the edit left it, so it can edit it again
|
|
16
|
+
* without reading it. What it returns to the model is short: the lines around a change, or a
|
|
17
|
+
* confirmation.
|
|
18
|
+
*/
|
|
19
|
+
export async function prepareTrackedEdit(writes, request, signal, { run, reads }) {
|
|
20
|
+
const editor = writes.editorFor(run);
|
|
21
|
+
const { path } = request;
|
|
22
|
+
if (request.kind === 'create') {
|
|
23
|
+
const prepared = await editor.prepare({ kind: 'create', path, content: request.content }, signal);
|
|
24
|
+
return {
|
|
25
|
+
risk: editRisk(prepared),
|
|
26
|
+
diff: editDiff(path, undefined, request.content.toString('utf8')),
|
|
27
|
+
run: async (runSignal) => {
|
|
28
|
+
await editor.apply(prepared, runSignal);
|
|
29
|
+
// It wrote the content itself, so it has seen it.
|
|
30
|
+
reads.record(prepared.target.absolute, hashOf(request.content));
|
|
31
|
+
return describeCreation(path, request.content);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const { target, state, content } = await editor.current(path, signal);
|
|
36
|
+
if (!state.exists || content === undefined) {
|
|
37
|
+
throw new EditRefusedError(`"${path}" does not exist`);
|
|
38
|
+
}
|
|
39
|
+
const seen = reads.versionOf(target.absolute);
|
|
40
|
+
if (seen === undefined) {
|
|
41
|
+
throw new EditRefusedError(`READ_REQUIRED: read "${path}" before changing it`);
|
|
42
|
+
}
|
|
43
|
+
if (seen !== state.hash) {
|
|
44
|
+
throw new EditRefusedError(`STALE_FILE: "${path}" changed since you read it; read it again`);
|
|
45
|
+
}
|
|
46
|
+
if (request.kind === 'delete') {
|
|
47
|
+
const prepared = await editor.prepare({ kind: 'delete', path, expected: seen }, signal);
|
|
48
|
+
return {
|
|
49
|
+
risk: editRisk(prepared),
|
|
50
|
+
diff: editDiff(path, content.toString('utf8'), undefined),
|
|
51
|
+
run: async (runSignal) => {
|
|
52
|
+
await editor.apply(prepared, runSignal);
|
|
53
|
+
reads.forget(target.absolute);
|
|
54
|
+
return describeDeletion(path);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const next = request.change(content);
|
|
59
|
+
const prepared = await editor.prepare({ kind: 'replace', path, content: next, expected: seen }, signal);
|
|
60
|
+
return {
|
|
61
|
+
risk: editRisk(prepared),
|
|
62
|
+
diff: editDiff(path, content.toString('utf8'), next.toString('utf8')),
|
|
63
|
+
run: async (runSignal) => {
|
|
64
|
+
await editor.apply(prepared, runSignal);
|
|
65
|
+
reads.record(target.absolute, hashOf(next));
|
|
66
|
+
return describeModification(path, content.toString('utf8'), next.toString('utf8'));
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=trackedEdits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trackedEdits.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/trackedEdits.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport {\n\tdescribeCreation,\n\tdescribeDeletion,\n\tdescribeModification,\n\teditDiff,\n} from '../domain/editResponse.ts';\nimport { EditRefusedError } from '../domain/fileEdits.ts';\nimport type { PreparedCall } from '../domain/preparedCall.ts';\nimport type { ToolCallContext } from '../domain/readRegistry.ts';\nimport { editRisk } from './fileEditor.ts';\nimport type { WorkspaceWrites } from './workspaceWrites.ts';\n\n/**\n * One edit, as an edit format asks for it, with the path as the model wrote it. A modification\n * works its new content out from what the file holds now, the version the model read; it throws\n * `EditRefusedError` with what the model needs to fix the call, such as a text that matched\n * twice.\n */\nexport type EditRequest =\n\t| { kind: 'create'; path: string; content: Buffer }\n\t| { kind: 'modify'; path: string; change: (current: Buffer) => Buffer }\n\t| { kind: 'delete'; path: string };\n\nfunction hashOf(content: Buffer): string {\n\treturn createHash('sha256').update(content).digest('hex');\n}\n\n/**\n * Prepares an edit from the version of the file this conversation read, and only from it:\n *\n * - a file it never read is `READ_REQUIRED`;\n * - a file changed since it read it is `STALE_FILE`;\n * - a new file needs a path that does not exist.\n *\n * Once applied, the conversation knows the file as the edit left it, so it can edit it again\n * without reading it. What it returns to the model is short: the lines around a change, or a\n * confirmation.\n */\nexport async function prepareTrackedEdit(\n\twrites: WorkspaceWrites,\n\trequest: EditRequest,\n\tsignal: AbortSignal,\n\t{ run, reads }: ToolCallContext,\n): Promise<PreparedCall> {\n\tconst editor = writes.editorFor(run);\n\tconst { path } = request;\n\n\tif (request.kind === 'create') {\n\t\tconst prepared = await editor.prepare(\n\t\t\t{ kind: 'create', path, content: request.content },\n\t\t\tsignal,\n\t\t);\n\t\treturn {\n\t\t\trisk: editRisk(prepared),\n\t\t\tdiff: editDiff(path, undefined, request.content.toString('utf8')),\n\t\t\trun: async runSignal => {\n\t\t\t\tawait editor.apply(prepared, runSignal);\n\t\t\t\t// It wrote the content itself, so it has seen it.\n\t\t\t\treads.record(prepared.target.absolute, hashOf(request.content));\n\t\t\t\treturn describeCreation(path, request.content);\n\t\t\t},\n\t\t};\n\t}\n\n\tconst { target, state, content } = await editor.current(path, signal);\n\tif (!state.exists || content === undefined) {\n\t\tthrow new EditRefusedError(`\"${path}\" does not exist`);\n\t}\n\tconst seen = reads.versionOf(target.absolute);\n\tif (seen === undefined) {\n\t\tthrow new EditRefusedError(`READ_REQUIRED: read \"${path}\" before changing it`);\n\t}\n\tif (seen !== state.hash) {\n\t\tthrow new EditRefusedError(`STALE_FILE: \"${path}\" changed since you read it; read it again`);\n\t}\n\n\tif (request.kind === 'delete') {\n\t\tconst prepared = await editor.prepare({ kind: 'delete', path, expected: seen }, signal);\n\t\treturn {\n\t\t\trisk: editRisk(prepared),\n\t\t\tdiff: editDiff(path, content.toString('utf8'), undefined),\n\t\t\trun: async runSignal => {\n\t\t\t\tawait editor.apply(prepared, runSignal);\n\t\t\t\treads.forget(target.absolute);\n\t\t\t\treturn describeDeletion(path);\n\t\t\t},\n\t\t};\n\t}\n\n\tconst next = request.change(content);\n\tconst prepared = await editor.prepare(\n\t\t{ kind: 'replace', path, content: next, expected: seen },\n\t\tsignal,\n\t);\n\treturn {\n\t\trisk: editRisk(prepared),\n\t\tdiff: editDiff(path, content.toString('utf8'), next.toString('utf8')),\n\t\trun: async runSignal => {\n\t\t\tawait editor.apply(prepared, runSignal);\n\t\t\treads.record(target.absolute, hashOf(next));\n\t\t\treturn describeModification(path, content.toString('utf8'), next.toString('utf8'));\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AccessPolicy, AllowedPath } from '../domain/accessPolicy.ts';
|
|
2
|
+
import type { PreparingTool } from '../domain/preparedCall.ts';
|
|
3
|
+
import type { RunReads } from '../domain/readRegistry.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Reads lines `from` to `from + count - 1` of a file the policy allowed, and records the version
|
|
6
|
+
* of the whole file the model saw. The file is read once, so the version recorded is the content
|
|
7
|
+
* shown. A partial read counts: the model saw this version, and can read the rest.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readTracked({ target, path, from, count, reads, signal, }: {
|
|
10
|
+
target: AllowedPath;
|
|
11
|
+
path: string;
|
|
12
|
+
from: number;
|
|
13
|
+
count: number;
|
|
14
|
+
reads: RunReads;
|
|
15
|
+
signal: AbortSignal;
|
|
16
|
+
}): Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* `readFile` for an agent that edits: the same tool for the model, through the access policy,
|
|
19
|
+
* and it records the version of the file the model saw, so an edit starts only from it.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createTrackedReadFile(policy: AccessPolicy): PreparingTool;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, readFile } from 'node:fs/promises';
|
|
3
|
+
import { isText } from '#src/recovery/domain/runChanges';
|
|
4
|
+
import { definePreparingTool } from "./defineTool.js";
|
|
5
|
+
import { readFileDescription, readFileInput, readFileSummary, readRange, renderLines, } from "./workspaceTools.js";
|
|
6
|
+
// The same bound as an edit: a file too large to change need not be read whole to track it.
|
|
7
|
+
const maxFileBytes = 5 * 1024 * 1024;
|
|
8
|
+
/**
|
|
9
|
+
* The lines of a text, split as the diff an edit returns splits them, so their numbers agree: on
|
|
10
|
+
* `\n`, with a `\r` before it dropped. A final newline ends the last line; it starts no other.
|
|
11
|
+
*/
|
|
12
|
+
function lines(text) {
|
|
13
|
+
if (text === '')
|
|
14
|
+
return [];
|
|
15
|
+
const all = text.split('\n').map(line => (line.endsWith('\r') ? line.slice(0, -1) : line));
|
|
16
|
+
if (all.at(-1) === '')
|
|
17
|
+
all.pop();
|
|
18
|
+
return all;
|
|
19
|
+
}
|
|
20
|
+
function code(error) {
|
|
21
|
+
return error.code ?? 'an unknown error';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Reads lines `from` to `from + count - 1` of a file the policy allowed, and records the version
|
|
25
|
+
* of the whole file the model saw. The file is read once, so the version recorded is the content
|
|
26
|
+
* shown. A partial read counts: the model saw this version, and can read the rest.
|
|
27
|
+
*/
|
|
28
|
+
export async function readTracked({ target, path, from, count, reads, signal, }) {
|
|
29
|
+
signal.throwIfAborted();
|
|
30
|
+
const stats = await lstat(target.absolute).catch((error) => {
|
|
31
|
+
if (code(error) === 'ENOENT')
|
|
32
|
+
return undefined;
|
|
33
|
+
// Its own message would name the host path.
|
|
34
|
+
throw new Error(`Could not read ${path} (${code(error)})`);
|
|
35
|
+
});
|
|
36
|
+
// One answer for every file it cannot show, as the read tools give.
|
|
37
|
+
if (!stats?.isFile())
|
|
38
|
+
throw new Error(`No such file: ${path}`);
|
|
39
|
+
if (stats.size > maxFileBytes) {
|
|
40
|
+
throw new Error(`${path} is larger than ${String(maxFileBytes / 1024 / 1024)} MB, too large to read`);
|
|
41
|
+
}
|
|
42
|
+
const content = await readFile(target.absolute, { signal }).catch((error) => {
|
|
43
|
+
signal.throwIfAborted();
|
|
44
|
+
throw new Error(`Could not read ${path} (${code(error)})`);
|
|
45
|
+
});
|
|
46
|
+
if (!isText(content))
|
|
47
|
+
throw new Error(`Not a text file: ${path}`);
|
|
48
|
+
const all = lines(content.toString('utf8'));
|
|
49
|
+
const shown = all.slice(from - 1, from - 1 + count);
|
|
50
|
+
reads.record(target.absolute, createHash('sha256').update(content).digest('hex'));
|
|
51
|
+
return renderLines({
|
|
52
|
+
path,
|
|
53
|
+
lines: shown,
|
|
54
|
+
from,
|
|
55
|
+
totalLines: all.length,
|
|
56
|
+
truncated: from - 1 + shown.length < all.length,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `readFile` for an agent that edits: the same tool for the model, through the access policy,
|
|
61
|
+
* and it records the version of the file the model saw, so an edit starts only from it.
|
|
62
|
+
*/
|
|
63
|
+
export function createTrackedReadFile(policy) {
|
|
64
|
+
return definePreparingTool({
|
|
65
|
+
name: 'readFile',
|
|
66
|
+
description: readFileDescription,
|
|
67
|
+
input: readFileInput,
|
|
68
|
+
summarize: readFileSummary,
|
|
69
|
+
prepare: async ({ path, fromLine, lineCount }, signal, { reads }) => {
|
|
70
|
+
const { from, count } = readRange({ fromLine, lineCount });
|
|
71
|
+
const target = await policy.check(path, 'read', signal);
|
|
72
|
+
return {
|
|
73
|
+
risk: 'safe',
|
|
74
|
+
run: runSignal => readTracked({ target, path, from, count, reads, signal: runSignal }),
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=trackedReadFile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trackedReadFile.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/trackedReadFile.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport { lstat, readFile } from 'node:fs/promises';\nimport { isText } from '#src/recovery/domain/runChanges';\nimport type { AccessPolicy, AllowedPath } from '../domain/accessPolicy.ts';\nimport type { PreparingTool } from '../domain/preparedCall.ts';\nimport type { RunReads } from '../domain/readRegistry.ts';\nimport { definePreparingTool } from './defineTool.ts';\nimport {\n\treadFileDescription,\n\treadFileInput,\n\treadFileSummary,\n\treadRange,\n\trenderLines,\n} from './workspaceTools.ts';\n\n// The same bound as an edit: a file too large to change need not be read whole to track it.\nconst maxFileBytes = 5 * 1024 * 1024;\n\n/**\n * The lines of a text, split as the diff an edit returns splits them, so their numbers agree: on\n * `\\n`, with a `\\r` before it dropped. A final newline ends the last line; it starts no other.\n */\nfunction lines(text: string): string[] {\n\tif (text === '') return [];\n\tconst all = text.split('\\n').map(line => (line.endsWith('\\r') ? line.slice(0, -1) : line));\n\tif (all.at(-1) === '') all.pop();\n\treturn all;\n}\n\nfunction code(error: unknown): string {\n\treturn (error as NodeJS.ErrnoException).code ?? 'an unknown error';\n}\n\n/**\n * Reads lines `from` to `from + count - 1` of a file the policy allowed, and records the version\n * of the whole file the model saw. The file is read once, so the version recorded is the content\n * shown. A partial read counts: the model saw this version, and can read the rest.\n */\nexport async function readTracked({\n\ttarget,\n\tpath,\n\tfrom,\n\tcount,\n\treads,\n\tsignal,\n}: {\n\ttarget: AllowedPath;\n\tpath: string;\n\tfrom: number;\n\tcount: number;\n\treads: RunReads;\n\tsignal: AbortSignal;\n}): Promise<string> {\n\tsignal.throwIfAborted();\n\tconst stats = await lstat(target.absolute).catch((error: unknown) => {\n\t\tif (code(error) === 'ENOENT') return undefined;\n\t\t// Its own message would name the host path.\n\t\tthrow new Error(`Could not read ${path} (${code(error)})`);\n\t});\n\t// One answer for every file it cannot show, as the read tools give.\n\tif (!stats?.isFile()) throw new Error(`No such file: ${path}`);\n\tif (stats.size > maxFileBytes) {\n\t\tthrow new Error(\n\t\t\t`${path} is larger than ${String(maxFileBytes / 1024 / 1024)} MB, too large to read`,\n\t\t);\n\t}\n\tconst content = await readFile(target.absolute, { signal }).catch((error: unknown) => {\n\t\tsignal.throwIfAborted();\n\t\tthrow new Error(`Could not read ${path} (${code(error)})`);\n\t});\n\tif (!isText(content)) throw new Error(`Not a text file: ${path}`);\n\n\tconst all = lines(content.toString('utf8'));\n\tconst shown = all.slice(from - 1, from - 1 + count);\n\treads.record(target.absolute, createHash('sha256').update(content).digest('hex'));\n\treturn renderLines({\n\t\tpath,\n\t\tlines: shown,\n\t\tfrom,\n\t\ttotalLines: all.length,\n\t\ttruncated: from - 1 + shown.length < all.length,\n\t});\n}\n\n/**\n * `readFile` for an agent that edits: the same tool for the model, through the access policy,\n * and it records the version of the file the model saw, so an edit starts only from it.\n */\nexport function createTrackedReadFile(policy: AccessPolicy): PreparingTool {\n\treturn definePreparingTool({\n\t\tname: 'readFile',\n\t\tdescription: readFileDescription,\n\t\tinput: readFileInput,\n\t\tsummarize: readFileSummary,\n\t\tprepare: async ({ path, fromLine, lineCount }, signal, { reads }) => {\n\t\t\tconst { from, count } = readRange({ fromLine, lineCount });\n\t\t\tconst target = await policy.check(path, 'read', signal);\n\t\t\treturn {\n\t\t\t\trisk: 'safe',\n\t\t\t\trun: runSignal => readTracked({ target, path, from, count, reads, signal: runSignal }),\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
|
|
@@ -1,4 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import type { Tool } from '../domain/tool.ts';
|
|
2
3
|
import type { Workspace } from '../domain/workspace.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Lines of a file as the model reads them: each numbered, a line too long cut, and a notice for
|
|
6
|
+
* what was cut or lies beyond.
|
|
7
|
+
*/
|
|
8
|
+
export declare function renderLines({ path, lines, from, totalLines, truncated, }: {
|
|
9
|
+
/** The file, which decides how long a line may be. */
|
|
10
|
+
path: string;
|
|
11
|
+
lines: string[];
|
|
12
|
+
from: number;
|
|
13
|
+
totalLines: number;
|
|
14
|
+
truncated: boolean;
|
|
15
|
+
}): string;
|
|
16
|
+
/** What `readFile` takes, shared with the harness's own version of it. */
|
|
17
|
+
export declare const readFileDescription = "Reads lines of a file, each prefixed with its number.";
|
|
18
|
+
export declare const readFileInput: z.ZodObject<{
|
|
19
|
+
path: z.ZodString;
|
|
20
|
+
fromLine: z.ZodNullable<z.ZodNumber>;
|
|
21
|
+
lineCount: z.ZodNullable<z.ZodNumber>;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
/** The range a `readFile` call asks for, checked: strict mode allows the schema no minimum. */
|
|
24
|
+
export declare function readRange({ fromLine, lineCount, }: {
|
|
25
|
+
fromLine: number | null;
|
|
26
|
+
lineCount: number | null;
|
|
27
|
+
}): {
|
|
28
|
+
from: number;
|
|
29
|
+
count: number;
|
|
30
|
+
};
|
|
31
|
+
/** A stable description of a read request, using only the range the model supplied. */
|
|
32
|
+
export declare function readFileSummary({ path, fromLine, lineCount, }: {
|
|
33
|
+
path: string;
|
|
34
|
+
fromLine: number | null;
|
|
35
|
+
lineCount: number | null;
|
|
36
|
+
}): string;
|
|
3
37
|
/** The three read-only tools an agent explores a repository with. */
|
|
4
38
|
export declare function createWorkspaceTools(workspace: Workspace): Tool[];
|
|
@@ -1,19 +1,73 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { defineTool } from "./defineTool.js";
|
|
3
|
+
import { cutLine, maxLineLength } from "../domain/lineLength.js";
|
|
3
4
|
// What one call may return. The model can ask for fewer lines, never for more.
|
|
4
5
|
const maxFiles = 200;
|
|
5
6
|
const maxMatches = 100;
|
|
6
7
|
const maxLines = 200;
|
|
7
8
|
// One minified line can be larger than everything else the model has read.
|
|
8
|
-
const maxLineLength = 300;
|
|
9
9
|
// Strict mode has no optional fields: an argument the model may leave out is nullable.
|
|
10
10
|
const scope = {
|
|
11
11
|
path: z.string().nullable().describe('A folder or file to stay inside, relative to the root.'),
|
|
12
|
-
glob: z
|
|
12
|
+
glob: z
|
|
13
|
+
.string()
|
|
14
|
+
.nullable()
|
|
15
|
+
.describe('Only paths matching this glob, relative to the root. "*" stays inside one folder and "**" crosses folders: "**/*.ts" is every TypeScript file, "*.ts" only those at the top.'),
|
|
13
16
|
};
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Lines of a file as the model reads them: each numbered, a line too long cut, and a notice for
|
|
19
|
+
* what was cut or lies beyond.
|
|
20
|
+
*/
|
|
21
|
+
export function renderLines({ path, lines, from, totalLines, truncated, }) {
|
|
22
|
+
// The file's length tells the model where its range went wrong.
|
|
23
|
+
if (lines.length === 0) {
|
|
24
|
+
return `No lines from ${String(from)}: the file has ${String(totalLines)} lines.`;
|
|
25
|
+
}
|
|
26
|
+
const numbered = lines.map((text, index) => `${String(from + index)}: ${cutLine(path, text)}`);
|
|
27
|
+
const max = maxLineLength(path);
|
|
28
|
+
const cut = lines.filter(text => text.length > max).length;
|
|
29
|
+
const notices = [
|
|
30
|
+
...(cut > 0
|
|
31
|
+
? [`[${String(cut)} lines were longer than ${String(max)} characters and were cut.]`]
|
|
32
|
+
: []),
|
|
33
|
+
...(truncated
|
|
34
|
+
? [
|
|
35
|
+
`[The file has ${String(totalLines)} lines. Read from line ${String(from + lines.length)} to continue.]`,
|
|
36
|
+
]
|
|
37
|
+
: []),
|
|
38
|
+
];
|
|
39
|
+
return [...numbered, ...notices].join('\n');
|
|
40
|
+
}
|
|
41
|
+
/** What `readFile` takes, shared with the harness's own version of it. */
|
|
42
|
+
export const readFileDescription = 'Reads lines of a file, each prefixed with its number.';
|
|
43
|
+
export const readFileInput = z.object({
|
|
44
|
+
path: z.string().describe('The file, relative to the root.'),
|
|
45
|
+
fromLine: z.number().nullable().describe('The first line to read, from 1. Defaults to 1.'),
|
|
46
|
+
lineCount: z
|
|
47
|
+
.number()
|
|
48
|
+
.nullable()
|
|
49
|
+
.describe(`How many lines to read, at most ${String(maxLines)}. Defaults to ${String(maxLines)}.`),
|
|
50
|
+
});
|
|
51
|
+
/** The range a `readFile` call asks for, checked: strict mode allows the schema no minimum. */
|
|
52
|
+
export function readRange({ fromLine, lineCount, }) {
|
|
53
|
+
const from = fromLine ?? 1;
|
|
54
|
+
const requested = lineCount ?? maxLines;
|
|
55
|
+
assertPositiveInteger('fromLine', from);
|
|
56
|
+
assertPositiveInteger('lineCount', requested);
|
|
57
|
+
return { from, count: Math.min(requested, maxLines) };
|
|
58
|
+
}
|
|
59
|
+
/** A stable description of a read request, using only the range the model supplied. */
|
|
60
|
+
export function readFileSummary({ path, fromLine, lineCount, }) {
|
|
61
|
+
const suppliedFrom = fromLine ?? 1;
|
|
62
|
+
const from = Number.isInteger(suppliedFrom) && suppliedFrom > 0 ? suppliedFrom : 1;
|
|
63
|
+
const suppliedCount = lineCount ?? maxLines;
|
|
64
|
+
const requested = Number.isInteger(suppliedCount) && suppliedCount > 0 ? suppliedCount : maxLines;
|
|
65
|
+
const count = Math.min(requested, maxLines);
|
|
66
|
+
return `${path}:${String(from)}-${String(from + count - 1)}`;
|
|
67
|
+
}
|
|
68
|
+
function scopeSummary(path, glob) {
|
|
69
|
+
const scopePath = path ?? '.';
|
|
70
|
+
return glob ? `${scopePath === '.' ? '' : `${scopePath}/`}${glob}` : scopePath;
|
|
17
71
|
}
|
|
18
72
|
function assertPositiveInteger(name, value) {
|
|
19
73
|
if (!Number.isInteger(value) || value < 1) {
|
|
@@ -28,6 +82,7 @@ export function createWorkspaceTools(workspace) {
|
|
|
28
82
|
description: 'Lists the files of the repository, sorted. Ignored files such as dependencies are left out.',
|
|
29
83
|
risk: 'safe',
|
|
30
84
|
input: z.object(scope),
|
|
85
|
+
summarize: ({ path, glob }) => scopeSummary(path, glob),
|
|
31
86
|
execute: async ({ path, glob }, signal) => {
|
|
32
87
|
// The port takes "no scope" as a missing field.
|
|
33
88
|
const { files, total, truncated } = await workspace.listFiles({ path: path ?? undefined, glob: glob ?? undefined, limit: maxFiles }, signal);
|
|
@@ -50,6 +105,7 @@ export function createWorkspaceTools(workspace) {
|
|
|
50
105
|
ignoreCase: z.boolean().nullable().describe('Match regardless of case. Defaults to false.'),
|
|
51
106
|
...scope,
|
|
52
107
|
}),
|
|
108
|
+
summarize: ({ path, glob, pattern }) => `"${pattern}" in ${scopeSummary(path, glob)}`,
|
|
53
109
|
execute: async ({ path, glob, ignoreCase, pattern }, signal) => {
|
|
54
110
|
const { matches, total, truncated } = await workspace.searchText({
|
|
55
111
|
path: path ?? undefined,
|
|
@@ -61,7 +117,7 @@ export function createWorkspaceTools(workspace) {
|
|
|
61
117
|
if (matches.length === 0) {
|
|
62
118
|
return 'No matches.';
|
|
63
119
|
}
|
|
64
|
-
const results = matches.map(match => `${match.path}:${String(match.line)}: ${cutLine(match.text)}`);
|
|
120
|
+
const results = matches.map(match => `${match.path}:${String(match.line)}: ${cutLine(match.path, match.text)}`);
|
|
65
121
|
const notice = truncated
|
|
66
122
|
? `\n[Showing ${String(results.length)} of ${String(total)} matches. Narrow the path or glob to see the rest.]`
|
|
67
123
|
: '';
|
|
@@ -70,42 +126,15 @@ export function createWorkspaceTools(workspace) {
|
|
|
70
126
|
}),
|
|
71
127
|
defineTool({
|
|
72
128
|
name: 'readFile',
|
|
73
|
-
description:
|
|
129
|
+
description: readFileDescription,
|
|
74
130
|
risk: 'safe',
|
|
75
|
-
input:
|
|
76
|
-
|
|
77
|
-
fromLine: z.number().nullable().describe('The first line to read, from 1. Defaults to 1.'),
|
|
78
|
-
lineCount: z
|
|
79
|
-
.number()
|
|
80
|
-
.nullable()
|
|
81
|
-
.describe(`How many lines to read, at most ${String(maxLines)}. Defaults to ${String(maxLines)}.`),
|
|
82
|
-
}),
|
|
131
|
+
input: readFileInput,
|
|
132
|
+
summarize: readFileSummary,
|
|
83
133
|
execute: async ({ fromLine, lineCount, path }, signal) => {
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
assertPositiveInteger('lineCount', requested);
|
|
89
|
-
const { lines, totalLines, truncated } = await workspace.readFile({ path, fromLine: from, lineCount: Math.min(requested, maxLines) }, signal);
|
|
90
|
-
// The file's length tells the model where its range went wrong.
|
|
91
|
-
if (lines.length === 0) {
|
|
92
|
-
return `No lines from ${String(from)}: the file has ${String(totalLines)} lines.`;
|
|
93
|
-
}
|
|
94
|
-
const numbered = lines.map((text, index) => `${String(from + index)}: ${cutLine(text)}`);
|
|
95
|
-
const cut = lines.filter(text => text.length > maxLineLength).length;
|
|
96
|
-
const notices = [
|
|
97
|
-
...(cut > 0
|
|
98
|
-
? [
|
|
99
|
-
`[${String(cut)} lines were longer than ${String(maxLineLength)} characters and were cut.]`,
|
|
100
|
-
]
|
|
101
|
-
: []),
|
|
102
|
-
...(truncated
|
|
103
|
-
? [
|
|
104
|
-
`[The file has ${String(totalLines)} lines. Read from line ${String(from + lines.length)} to continue.]`,
|
|
105
|
-
]
|
|
106
|
-
: []),
|
|
107
|
-
];
|
|
108
|
-
return [...numbered, ...notices].join('\n');
|
|
134
|
+
// Checked before reading.
|
|
135
|
+
const { from, count } = readRange({ fromLine, lineCount });
|
|
136
|
+
const { lines, totalLines, truncated } = await workspace.readFile({ path, fromLine: from, lineCount: count }, signal);
|
|
137
|
+
return renderLines({ path, lines, from, totalLines, truncated });
|
|
109
138
|
},
|
|
110
139
|
}),
|
|
111
140
|
];
|