@mikode13/harness 4.0.1 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +278 -16
  2. package/dist/agent/domain/agent.d.ts +12 -0
  3. package/dist/agent/domain/agent.js.map +1 -1
  4. package/dist/agent/domain/approval.d.ts +9 -0
  5. package/dist/agent/domain/approval.js.map +1 -1
  6. package/dist/agent/domain/runContext.d.ts +31 -0
  7. package/dist/agent/domain/runContext.js +47 -0
  8. package/dist/agent/domain/runContext.js.map +1 -0
  9. package/dist/diff/domain/unifiedDiff.d.ts +19 -0
  10. package/dist/diff/domain/unifiedDiff.js +166 -0
  11. package/dist/diff/domain/unifiedDiff.js.map +1 -0
  12. package/dist/engines/domain/model/historyFollower.d.ts +41 -0
  13. package/dist/engines/domain/model/historyFollower.js +158 -0
  14. package/dist/engines/domain/model/historyFollower.js.map +1 -0
  15. package/dist/engines/domain/model/llmAgent.d.ts +42 -4
  16. package/dist/engines/domain/model/llmAgent.js +223 -23
  17. package/dist/engines/domain/model/llmAgent.js.map +1 -1
  18. package/dist/factory/infrastructure/agentFactory.d.ts +14 -0
  19. package/dist/factory/infrastructure/agentFactory.js +20 -2
  20. package/dist/factory/infrastructure/agentFactory.js.map +1 -1
  21. package/dist/factory/infrastructure/agentLLMFactory.d.ts +119 -14
  22. package/dist/factory/infrastructure/agentLLMFactory.js +284 -57
  23. package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
  24. package/dist/index.d.ts +11 -3
  25. package/dist/index.js +5 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/llm/domain/conversation.d.ts +44 -4
  28. package/dist/llm/domain/conversation.js +56 -7
  29. package/dist/llm/domain/conversation.js.map +1 -1
  30. package/dist/llm/domain/llm.d.ts +3 -1
  31. package/dist/llm/domain/llm.js.map +1 -1
  32. package/dist/llm/domain/tool.d.ts +13 -0
  33. package/dist/llm/domain/tool.js +12 -1
  34. package/dist/llm/domain/tool.js.map +1 -1
  35. package/dist/llm/infrastructure/claudeLLMClient.d.ts +3 -2
  36. package/dist/llm/infrastructure/claudeLLMClient.js +38 -10
  37. package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -1
  38. package/dist/llm/infrastructure/openAILLMClient.d.ts +2 -1
  39. package/dist/llm/infrastructure/openAILLMClient.js +66 -16
  40. package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
  41. package/dist/orchestration/domain/model/orchestratorAgent.d.ts +11 -2
  42. package/dist/orchestration/domain/model/orchestratorAgent.js +43 -4
  43. package/dist/orchestration/domain/model/orchestratorAgent.js.map +1 -1
  44. package/dist/recovery/domain/fileChanges.d.ts +31 -0
  45. package/dist/recovery/domain/fileChanges.js +48 -0
  46. package/dist/recovery/domain/fileChanges.js.map +1 -0
  47. package/dist/recovery/domain/history.d.ts +114 -0
  48. package/dist/recovery/domain/history.js +62 -0
  49. package/dist/recovery/domain/history.js.map +1 -0
  50. package/dist/recovery/domain/recoveryStore.d.ts +233 -0
  51. package/dist/recovery/domain/recoveryStore.js +32 -0
  52. package/dist/recovery/domain/recoveryStore.js.map +1 -0
  53. package/dist/recovery/domain/retention.d.ts +33 -0
  54. package/dist/recovery/domain/retention.js +90 -0
  55. package/dist/recovery/domain/retention.js.map +1 -0
  56. package/dist/recovery/domain/runChanges.d.ts +37 -0
  57. package/dist/recovery/domain/runChanges.js +109 -0
  58. package/dist/recovery/domain/runChanges.js.map +1 -0
  59. package/dist/recovery/domain/runTree.d.ts +32 -0
  60. package/dist/recovery/domain/runTree.js +90 -0
  61. package/dist/recovery/domain/runTree.js.map +1 -0
  62. package/dist/recovery/infrastructure/createHistory.d.ts +11 -0
  63. package/dist/recovery/infrastructure/createHistory.js +17 -0
  64. package/dist/recovery/infrastructure/createHistory.js.map +1 -0
  65. package/dist/recovery/infrastructure/fileRecoveryStore.d.ts +133 -0
  66. package/dist/recovery/infrastructure/fileRecoveryStore.js +1043 -0
  67. package/dist/recovery/infrastructure/fileRecoveryStore.js.map +1 -0
  68. package/dist/recovery/infrastructure/workspaceFiles.d.ts +30 -0
  69. package/dist/recovery/infrastructure/workspaceFiles.js +125 -0
  70. package/dist/recovery/infrastructure/workspaceFiles.js.map +1 -0
  71. package/dist/tools/domain/accessPolicy.d.ts +30 -0
  72. package/dist/tools/domain/accessPolicy.js +11 -0
  73. package/dist/tools/domain/accessPolicy.js.map +1 -0
  74. package/dist/tools/domain/commands.d.ts +93 -0
  75. package/dist/tools/domain/commands.js +191 -0
  76. package/dist/tools/domain/commands.js.map +1 -0
  77. package/dist/tools/domain/editResponse.d.ts +14 -0
  78. package/dist/tools/domain/editResponse.js +79 -0
  79. package/dist/tools/domain/editResponse.js.map +1 -0
  80. package/dist/tools/domain/fileEdits.d.ts +64 -0
  81. package/dist/tools/domain/fileEdits.js +21 -0
  82. package/dist/tools/domain/fileEdits.js.map +1 -0
  83. package/dist/tools/domain/lineLength.d.ts +7 -0
  84. package/dist/tools/domain/lineLength.js +26 -0
  85. package/dist/tools/domain/lineLength.js.map +1 -0
  86. package/dist/tools/domain/preparedCall.d.ts +31 -0
  87. package/dist/tools/domain/preparedCall.js +14 -0
  88. package/dist/tools/domain/preparedCall.js.map +1 -0
  89. package/dist/tools/domain/readRegistry.d.ts +46 -0
  90. package/dist/tools/domain/readRegistry.js +62 -0
  91. package/dist/tools/domain/readRegistry.js.map +1 -0
  92. package/dist/tools/domain/textLines.d.ts +26 -0
  93. package/dist/tools/domain/textLines.js +44 -0
  94. package/dist/tools/domain/textLines.js.map +1 -0
  95. package/dist/tools/domain/v4aPatch.d.ts +17 -0
  96. package/dist/tools/domain/v4aPatch.js +236 -0
  97. package/dist/tools/domain/v4aPatch.js.map +1 -0
  98. package/dist/tools/infrastructure/boundedWorkspace.d.ts +11 -2
  99. package/dist/tools/infrastructure/boundedWorkspace.js +9 -1
  100. package/dist/tools/infrastructure/boundedWorkspace.js.map +1 -1
  101. package/dist/tools/infrastructure/commandCheckpoint.d.ts +55 -0
  102. package/dist/tools/infrastructure/commandCheckpoint.js +178 -0
  103. package/dist/tools/infrastructure/commandCheckpoint.js.map +1 -0
  104. package/dist/tools/infrastructure/commandProcess.d.ts +47 -0
  105. package/dist/tools/infrastructure/commandProcess.js +217 -0
  106. package/dist/tools/infrastructure/commandProcess.js.map +1 -0
  107. package/dist/tools/infrastructure/commandTool.d.ts +36 -0
  108. package/dist/tools/infrastructure/commandTool.js +254 -0
  109. package/dist/tools/infrastructure/commandTool.js.map +1 -0
  110. package/dist/tools/infrastructure/createWorkspace.d.ts +10 -0
  111. package/dist/tools/infrastructure/createWorkspace.js +11 -3
  112. package/dist/tools/infrastructure/createWorkspace.js.map +1 -1
  113. package/dist/tools/infrastructure/defineTool.d.ts +14 -0
  114. package/dist/tools/infrastructure/defineTool.js +28 -5
  115. package/dist/tools/infrastructure/defineTool.js.map +1 -1
  116. package/dist/tools/infrastructure/editFormats.d.ts +22 -0
  117. package/dist/tools/infrastructure/editFormats.js +211 -0
  118. package/dist/tools/infrastructure/editFormats.js.map +1 -0
  119. package/dist/tools/infrastructure/fileEditor.d.ts +64 -0
  120. package/dist/tools/infrastructure/fileEditor.js +306 -0
  121. package/dist/tools/infrastructure/fileEditor.js.map +1 -0
  122. package/dist/tools/infrastructure/fileTools.d.ts +81 -0
  123. package/dist/tools/infrastructure/fileTools.js +152 -0
  124. package/dist/tools/infrastructure/fileTools.js.map +1 -0
  125. package/dist/tools/infrastructure/gitIgnoreRules.d.ts +15 -0
  126. package/dist/tools/infrastructure/gitIgnoreRules.js +74 -0
  127. package/dist/tools/infrastructure/gitIgnoreRules.js.map +1 -0
  128. package/dist/tools/infrastructure/gitWorkspace.d.ts +1 -1
  129. package/dist/tools/infrastructure/gitWorkspace.js +1 -1
  130. package/dist/tools/infrastructure/gitWorkspace.js.map +1 -1
  131. package/dist/tools/infrastructure/ignoreFileChanges.d.ts +14 -0
  132. package/dist/tools/infrastructure/ignoreFileChanges.js +62 -0
  133. package/dist/tools/infrastructure/ignoreFileChanges.js.map +1 -0
  134. package/dist/tools/infrastructure/policyWorkspace.d.ts +52 -0
  135. package/dist/tools/infrastructure/policyWorkspace.js +32 -0
  136. package/dist/tools/infrastructure/policyWorkspace.js.map +1 -0
  137. package/dist/tools/infrastructure/ripgrepWorkspace.d.ts +2 -1
  138. package/dist/tools/infrastructure/ripgrepWorkspace.js +1 -1
  139. package/dist/tools/infrastructure/ripgrepWorkspace.js.map +1 -1
  140. package/dist/tools/infrastructure/rootsAccessPolicy.d.ts +100 -0
  141. package/dist/tools/infrastructure/rootsAccessPolicy.js +301 -0
  142. package/dist/tools/infrastructure/rootsAccessPolicy.js.map +1 -0
  143. package/dist/tools/infrastructure/runProcess.d.ts +4 -1
  144. package/dist/tools/infrastructure/runProcess.js +4 -2
  145. package/dist/tools/infrastructure/runProcess.js.map +1 -1
  146. package/dist/tools/infrastructure/secretPaths.d.ts +8 -0
  147. package/dist/tools/infrastructure/secretPaths.js +76 -0
  148. package/dist/tools/infrastructure/secretPaths.js.map +1 -0
  149. package/dist/tools/infrastructure/showChangesTool.d.ts +12 -0
  150. package/dist/tools/infrastructure/showChangesTool.js +43 -0
  151. package/dist/tools/infrastructure/showChangesTool.js.map +1 -0
  152. package/dist/tools/infrastructure/trackedEdits.d.ts +33 -0
  153. package/dist/tools/infrastructure/trackedEdits.js +70 -0
  154. package/dist/tools/infrastructure/trackedEdits.js.map +1 -0
  155. package/dist/tools/infrastructure/trackedReadFile.d.ts +21 -0
  156. package/dist/tools/infrastructure/trackedReadFile.js +78 -0
  157. package/dist/tools/infrastructure/trackedReadFile.js.map +1 -0
  158. package/dist/tools/infrastructure/workspaceTools.d.ts +28 -0
  159. package/dist/tools/infrastructure/workspaceTools.js +53 -40
  160. package/dist/tools/infrastructure/workspaceTools.js.map +1 -1
  161. package/dist/tools/infrastructure/workspaceWrites.d.ts +23 -0
  162. package/dist/tools/infrastructure/workspaceWrites.js +42 -0
  163. package/dist/tools/infrastructure/workspaceWrites.js.map +1 -0
  164. package/dist/tools/infrastructure/writeSession.d.ts +35 -0
  165. package/dist/tools/infrastructure/writeSession.js +81 -0
  166. package/dist/tools/infrastructure/writeSession.js.map +1 -0
  167. 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,43 @@
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
+ prepare: (_, __, { run }) => Promise.resolve({
19
+ risk: 'safe',
20
+ run: async (signal) => {
21
+ const { runId } = run.historyRun;
22
+ if (runId === undefined)
23
+ return nothingYet;
24
+ const diff = await showChanges(store, runId, {
25
+ maxLines,
26
+ visible: async (path) => {
27
+ try {
28
+ await policy.check(path, 'read', signal);
29
+ return true;
30
+ }
31
+ catch (error) {
32
+ if (error instanceof AccessDeniedError)
33
+ return false;
34
+ throw error;
35
+ }
36
+ },
37
+ });
38
+ return diff === '' ? nothingYet : diff;
39
+ },
40
+ }),
41
+ });
42
+ }
43
+ //# 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,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\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,78 @@
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, 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
+ prepare: async ({ path, fromLine, lineCount }, signal, { reads }) => {
69
+ const { from, count } = readRange({ fromLine, lineCount });
70
+ const target = await policy.check(path, 'read', signal);
71
+ return {
72
+ risk: 'safe',
73
+ run: runSignal => readTracked({ target, path, from, count, reads, signal: runSignal }),
74
+ };
75
+ },
76
+ });
77
+ }
78
+ //# 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 { readFileDescription, readFileInput, readRange, renderLines } 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\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,32 @@
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
+ };
3
31
  /** The three read-only tools an agent explores a repository with. */
4
32
  export declare function createWorkspaceTools(workspace: Workspace): Tool[];
@@ -1,19 +1,60 @@
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.string().nullable().describe('Only paths matching this glob, such as "**/*.ts".'),
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
- /** One line as the model reads it: a minified bundle on a single line would flood its context. */
15
- function cutLine(text) {
16
- return text.length > maxLineLength ? `${text.slice(0, maxLineLength)}…` : text;
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) };
17
58
  }
18
59
  function assertPositiveInteger(name, value) {
19
60
  if (!Number.isInteger(value) || value < 1) {
@@ -61,7 +102,7 @@ export function createWorkspaceTools(workspace) {
61
102
  if (matches.length === 0) {
62
103
  return 'No matches.';
63
104
  }
64
- const results = matches.map(match => `${match.path}:${String(match.line)}: ${cutLine(match.text)}`);
105
+ const results = matches.map(match => `${match.path}:${String(match.line)}: ${cutLine(match.path, match.text)}`);
65
106
  const notice = truncated
66
107
  ? `\n[Showing ${String(results.length)} of ${String(total)} matches. Narrow the path or glob to see the rest.]`
67
108
  : '';
@@ -70,42 +111,14 @@ export function createWorkspaceTools(workspace) {
70
111
  }),
71
112
  defineTool({
72
113
  name: 'readFile',
73
- description: 'Reads lines of a file, each prefixed with its number.',
114
+ description: readFileDescription,
74
115
  risk: 'safe',
75
- input: z.object({
76
- path: z.string().describe('The file, relative to the root.'),
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
- }),
116
+ input: readFileInput,
83
117
  execute: async ({ fromLine, lineCount, path }, signal) => {
84
- const from = fromLine ?? 1;
85
- const requested = lineCount ?? maxLines;
86
- // Checked here, before reading, because strict mode allows the schema no minimum.
87
- assertPositiveInteger('fromLine', from);
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');
118
+ // Checked before reading.
119
+ const { from, count } = readRange({ fromLine, lineCount });
120
+ const { lines, totalLines, truncated } = await workspace.readFile({ path, fromLine: from, lineCount: count }, signal);
121
+ return renderLines({ path, lines, from, totalLines, truncated });
109
122
  },
110
123
  }),
111
124
  ];
@@ -1 +1 @@
1
- {"version":3,"file":"workspaceTools.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/workspaceTools.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod';\nimport type { Tool } from '../domain/tool.ts';\nimport type { Workspace } from '../domain/workspace.ts';\nimport { defineTool } from './defineTool.ts';\n\n// What one call may return. The model can ask for fewer lines, never for more.\nconst maxFiles = 200;\nconst maxMatches = 100;\nconst maxLines = 200;\n// One minified line can be larger than everything else the model has read.\nconst maxLineLength = 300;\n\n// Strict mode has no optional fields: an argument the model may leave out is nullable.\nconst scope = {\n\tpath: z.string().nullable().describe('A folder or file to stay inside, relative to the root.'),\n\tglob: z.string().nullable().describe('Only paths matching this glob, such as \"**/*.ts\".'),\n};\n\n/** One line as the model reads it: a minified bundle on a single line would flood its context. */\nfunction cutLine(text: string): string {\n\treturn text.length > maxLineLength ? `${text.slice(0, maxLineLength)}…` : text;\n}\n\nfunction assertPositiveInteger(name: string, value: number): void {\n\tif (!Number.isInteger(value) || value < 1) {\n\t\tthrow new Error(`${name} must be a whole number of at least 1; got ${String(value)}`);\n\t}\n}\n\n/** The three read-only tools an agent explores a repository with. */\nexport function createWorkspaceTools(workspace: Workspace): Tool[] {\n\treturn [\n\t\tdefineTool({\n\t\t\tname: 'listFiles',\n\t\t\tdescription:\n\t\t\t\t'Lists the files of the repository, sorted. Ignored files such as dependencies are left out.',\n\t\t\trisk: 'safe',\n\t\t\tinput: z.object(scope),\n\t\t\texecute: async ({ path, glob }, signal) => {\n\t\t\t\t// The port takes \"no scope\" as a missing field.\n\t\t\t\tconst { files, total, truncated } = await workspace.listFiles(\n\t\t\t\t\t{ path: path ?? undefined, glob: glob ?? undefined, limit: maxFiles },\n\t\t\t\t\tsignal,\n\t\t\t\t);\n\n\t\t\t\t// Nothing found is an answer too, not an empty string.\n\t\t\t\tif (files.length === 0) {\n\t\t\t\t\treturn 'No files.';\n\t\t\t\t}\n\n\t\t\t\tconst notice = truncated\n\t\t\t\t\t? `\\n[Showing ${String(files.length)} of ${String(total)} files. Narrow the path or glob to see the rest.]`\n\t\t\t\t\t: '';\n\t\t\t\treturn files.join('\\n') + notice;\n\t\t\t},\n\t\t}),\n\n\t\tdefineTool({\n\t\t\tname: 'searchText',\n\t\t\tdescription:\n\t\t\t\t'Searches the repository for a regular expression and returns each matching line as path:line: text.',\n\t\t\trisk: 'safe',\n\t\t\tinput: z.object({\n\t\t\t\tpattern: z.string().describe('A regular expression.'),\n\t\t\t\tignoreCase: z.boolean().nullable().describe('Match regardless of case. Defaults to false.'),\n\t\t\t\t...scope,\n\t\t\t}),\n\t\t\texecute: async ({ path, glob, ignoreCase, pattern }, signal) => {\n\t\t\t\tconst { matches, total, truncated } = await workspace.searchText(\n\t\t\t\t\t{\n\t\t\t\t\t\tpath: path ?? undefined,\n\t\t\t\t\t\tglob: glob ?? undefined,\n\t\t\t\t\t\tignoreCase: ignoreCase ?? false,\n\t\t\t\t\t\tpattern,\n\t\t\t\t\t\tlimit: maxMatches,\n\t\t\t\t\t},\n\t\t\t\t\tsignal,\n\t\t\t\t);\n\n\t\t\t\tif (matches.length === 0) {\n\t\t\t\t\treturn 'No matches.';\n\t\t\t\t}\n\n\t\t\t\tconst results = matches.map(\n\t\t\t\t\tmatch => `${match.path}:${String(match.line)}: ${cutLine(match.text)}`,\n\t\t\t\t);\n\n\t\t\t\tconst notice = truncated\n\t\t\t\t\t? `\\n[Showing ${String(results.length)} of ${String(total)} matches. Narrow the path or glob to see the rest.]`\n\t\t\t\t\t: '';\n\t\t\t\treturn results.join('\\n') + notice;\n\t\t\t},\n\t\t}),\n\n\t\tdefineTool({\n\t\t\tname: 'readFile',\n\t\t\tdescription: 'Reads lines of a file, each prefixed with its number.',\n\t\t\trisk: 'safe',\n\t\t\tinput: z.object({\n\t\t\t\tpath: z.string().describe('The file, relative to the root.'),\n\t\t\t\tfromLine: z.number().nullable().describe('The first line to read, from 1. Defaults to 1.'),\n\t\t\t\tlineCount: z\n\t\t\t\t\t.number()\n\t\t\t\t\t.nullable()\n\t\t\t\t\t.describe(\n\t\t\t\t\t\t`How many lines to read, at most ${String(maxLines)}. Defaults to ${String(maxLines)}.`,\n\t\t\t\t\t),\n\t\t\t}),\n\t\t\texecute: async ({ fromLine, lineCount, path }, signal) => {\n\t\t\t\tconst from = fromLine ?? 1;\n\t\t\t\tconst requested = lineCount ?? maxLines;\n\t\t\t\t// Checked here, before reading, because strict mode allows the schema no minimum.\n\t\t\t\tassertPositiveInteger('fromLine', from);\n\t\t\t\tassertPositiveInteger('lineCount', requested);\n\n\t\t\t\tconst { lines, totalLines, truncated } = await workspace.readFile(\n\t\t\t\t\t{ path, fromLine: from, lineCount: Math.min(requested, maxLines) },\n\t\t\t\t\tsignal,\n\t\t\t\t);\n\n\t\t\t\t// The file's length tells the model where its range went wrong.\n\t\t\t\tif (lines.length === 0) {\n\t\t\t\t\treturn `No lines from ${String(from)}: the file has ${String(totalLines)} lines.`;\n\t\t\t\t}\n\n\t\t\t\tconst numbered = lines.map((text, index) => `${String(from + index)}: ${cutLine(text)}`);\n\t\t\t\tconst cut = lines.filter(text => text.length > maxLineLength).length;\n\t\t\t\tconst notices = [\n\t\t\t\t\t...(cut > 0\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t`[${String(cut)} lines were longer than ${String(maxLineLength)} characters and were cut.]`,\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: []),\n\t\t\t\t\t...(truncated\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t`[The file has ${String(totalLines)} lines. Read from line ${String(from + lines.length)} to continue.]`,\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: []),\n\t\t\t\t];\n\t\t\t\treturn [...numbered, ...notices].join('\\n');\n\t\t\t},\n\t\t}),\n\t];\n}\n"]}
1
+ {"version":3,"file":"workspaceTools.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/workspaceTools.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod';\nimport type { Tool } from '../domain/tool.ts';\nimport type { Workspace } from '../domain/workspace.ts';\nimport { defineTool } from './defineTool.ts';\nimport { cutLine, maxLineLength } from '../domain/lineLength.ts';\n\n// What one call may return. The model can ask for fewer lines, never for more.\nconst maxFiles = 200;\nconst maxMatches = 100;\nconst maxLines = 200;\n// One minified line can be larger than everything else the model has read.\n\n// Strict mode has no optional fields: an argument the model may leave out is nullable.\nconst scope = {\n\tpath: z.string().nullable().describe('A folder or file to stay inside, relative to the root.'),\n\tglob: z\n\t\t.string()\n\t\t.nullable()\n\t\t.describe(\n\t\t\t'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.',\n\t\t),\n};\n\n/**\n * Lines of a file as the model reads them: each numbered, a line too long cut, and a notice for\n * what was cut or lies beyond.\n */\nexport function renderLines({\n\tpath,\n\tlines,\n\tfrom,\n\ttotalLines,\n\ttruncated,\n}: {\n\t/** The file, which decides how long a line may be. */\n\tpath: string;\n\tlines: string[];\n\tfrom: number;\n\ttotalLines: number;\n\ttruncated: boolean;\n}): string {\n\t// The file's length tells the model where its range went wrong.\n\tif (lines.length === 0) {\n\t\treturn `No lines from ${String(from)}: the file has ${String(totalLines)} lines.`;\n\t}\n\n\tconst numbered = lines.map((text, index) => `${String(from + index)}: ${cutLine(path, text)}`);\n\tconst max = maxLineLength(path);\n\tconst cut = lines.filter(text => text.length > max).length;\n\tconst notices = [\n\t\t...(cut > 0\n\t\t\t? [`[${String(cut)} lines were longer than ${String(max)} characters and were cut.]`]\n\t\t\t: []),\n\t\t...(truncated\n\t\t\t? [\n\t\t\t\t\t`[The file has ${String(totalLines)} lines. Read from line ${String(from + lines.length)} to continue.]`,\n\t\t\t\t]\n\t\t\t: []),\n\t];\n\treturn [...numbered, ...notices].join('\\n');\n}\n\n/** What `readFile` takes, shared with the harness's own version of it. */\nexport const readFileDescription = 'Reads lines of a file, each prefixed with its number.';\nexport const readFileInput = z.object({\n\tpath: z.string().describe('The file, relative to the root.'),\n\tfromLine: z.number().nullable().describe('The first line to read, from 1. Defaults to 1.'),\n\tlineCount: z\n\t\t.number()\n\t\t.nullable()\n\t\t.describe(\n\t\t\t`How many lines to read, at most ${String(maxLines)}. Defaults to ${String(maxLines)}.`,\n\t\t),\n});\n\n/** The range a `readFile` call asks for, checked: strict mode allows the schema no minimum. */\nexport function readRange({\n\tfromLine,\n\tlineCount,\n}: {\n\tfromLine: number | null;\n\tlineCount: number | null;\n}): { from: number; count: number } {\n\tconst from = fromLine ?? 1;\n\tconst requested = lineCount ?? maxLines;\n\tassertPositiveInteger('fromLine', from);\n\tassertPositiveInteger('lineCount', requested);\n\treturn { from, count: Math.min(requested, maxLines) };\n}\n\nfunction assertPositiveInteger(name: string, value: number): void {\n\tif (!Number.isInteger(value) || value < 1) {\n\t\tthrow new Error(`${name} must be a whole number of at least 1; got ${String(value)}`);\n\t}\n}\n\n/** The three read-only tools an agent explores a repository with. */\nexport function createWorkspaceTools(workspace: Workspace): Tool[] {\n\treturn [\n\t\tdefineTool({\n\t\t\tname: 'listFiles',\n\t\t\tdescription:\n\t\t\t\t'Lists the files of the repository, sorted. Ignored files such as dependencies are left out.',\n\t\t\trisk: 'safe',\n\t\t\tinput: z.object(scope),\n\t\t\texecute: async ({ path, glob }, signal) => {\n\t\t\t\t// The port takes \"no scope\" as a missing field.\n\t\t\t\tconst { files, total, truncated } = await workspace.listFiles(\n\t\t\t\t\t{ path: path ?? undefined, glob: glob ?? undefined, limit: maxFiles },\n\t\t\t\t\tsignal,\n\t\t\t\t);\n\n\t\t\t\t// Nothing found is an answer too, not an empty string.\n\t\t\t\tif (files.length === 0) {\n\t\t\t\t\treturn 'No files.';\n\t\t\t\t}\n\n\t\t\t\tconst notice = truncated\n\t\t\t\t\t? `\\n[Showing ${String(files.length)} of ${String(total)} files. Narrow the path or glob to see the rest.]`\n\t\t\t\t\t: '';\n\t\t\t\treturn files.join('\\n') + notice;\n\t\t\t},\n\t\t}),\n\n\t\tdefineTool({\n\t\t\tname: 'searchText',\n\t\t\tdescription:\n\t\t\t\t'Searches the repository for a regular expression and returns each matching line as path:line: text.',\n\t\t\trisk: 'safe',\n\t\t\tinput: z.object({\n\t\t\t\tpattern: z.string().describe('A regular expression.'),\n\t\t\t\tignoreCase: z.boolean().nullable().describe('Match regardless of case. Defaults to false.'),\n\t\t\t\t...scope,\n\t\t\t}),\n\t\t\texecute: async ({ path, glob, ignoreCase, pattern }, signal) => {\n\t\t\t\tconst { matches, total, truncated } = await workspace.searchText(\n\t\t\t\t\t{\n\t\t\t\t\t\tpath: path ?? undefined,\n\t\t\t\t\t\tglob: glob ?? undefined,\n\t\t\t\t\t\tignoreCase: ignoreCase ?? false,\n\t\t\t\t\t\tpattern,\n\t\t\t\t\t\tlimit: maxMatches,\n\t\t\t\t\t},\n\t\t\t\t\tsignal,\n\t\t\t\t);\n\n\t\t\t\tif (matches.length === 0) {\n\t\t\t\t\treturn 'No matches.';\n\t\t\t\t}\n\n\t\t\t\tconst results = matches.map(\n\t\t\t\t\tmatch => `${match.path}:${String(match.line)}: ${cutLine(match.path, match.text)}`,\n\t\t\t\t);\n\n\t\t\t\tconst notice = truncated\n\t\t\t\t\t? `\\n[Showing ${String(results.length)} of ${String(total)} matches. Narrow the path or glob to see the rest.]`\n\t\t\t\t\t: '';\n\t\t\t\treturn results.join('\\n') + notice;\n\t\t\t},\n\t\t}),\n\n\t\tdefineTool({\n\t\t\tname: 'readFile',\n\t\t\tdescription: readFileDescription,\n\t\t\trisk: 'safe',\n\t\t\tinput: readFileInput,\n\t\t\texecute: async ({ fromLine, lineCount, path }, signal) => {\n\t\t\t\t// Checked before reading.\n\t\t\t\tconst { from, count } = readRange({ fromLine, lineCount });\n\t\t\t\tconst { lines, totalLines, truncated } = await workspace.readFile(\n\t\t\t\t\t{ path, fromLine: from, lineCount: count },\n\t\t\t\t\tsignal,\n\t\t\t\t);\n\t\t\t\treturn renderLines({ path, lines, from, totalLines, truncated });\n\t\t\t},\n\t\t}),\n\t];\n}\n"]}