@mikode13/harness 4.0.0 → 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.
- package/README.md +285 -20
- package/dist/agent/domain/agent.d.ts +12 -0
- package/dist/agent/domain/agent.js.map +1 -1
- package/dist/agent/domain/approval.d.ts +14 -0
- package/dist/agent/domain/approval.js +28 -3
- 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 +224 -36
- 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 +11 -3
- 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 +66 -16
- package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
- package/dist/orchestration/domain/model/orchestratorAgent.d.ts +11 -2
- package/dist/orchestration/domain/model/orchestratorAgent.js +43 -4
- 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 +31 -0
- package/dist/tools/domain/preparedCall.js +14 -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/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 +254 -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 +14 -0
- package/dist/tools/infrastructure/defineTool.js +28 -5
- package/dist/tools/infrastructure/defineTool.js.map +1 -1
- package/dist/tools/infrastructure/editFormats.d.ts +22 -0
- package/dist/tools/infrastructure/editFormats.js +211 -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 +43 -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 +78 -0
- package/dist/tools/infrastructure/trackedReadFile.js.map +1 -0
- package/dist/tools/infrastructure/workspaceTools.d.ts +28 -0
- package/dist/tools/infrastructure/workspaceTools.js +53 -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,191 @@
|
|
|
1
|
+
import { InvalidAgentConfigError } from '#src/shared/domain/errors';
|
|
2
|
+
import { isOneOf } from '#src/shared/domain/isOneOf';
|
|
3
|
+
/** The roles of the model-backed orchestrator a command can be given to. */
|
|
4
|
+
export const commandRoles = ['planner', 'executor', 'reviewer'];
|
|
5
|
+
export const defaultCommandTimeoutMs = 10 * 60 * 1000;
|
|
6
|
+
export const defaultCommandOutputBytes = 16 * 1024;
|
|
7
|
+
// A test filter or a snapshot name, not a script.
|
|
8
|
+
const maxTextLength = 1000;
|
|
9
|
+
const maxOutputBytes = 1024 * 1024;
|
|
10
|
+
const maxTimeoutMs = 24 * 60 * 60 * 1000;
|
|
11
|
+
const namePattern = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
12
|
+
const variablePattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
13
|
+
// The tool's input names the command with this key, so no parameter may take it.
|
|
14
|
+
const reservedParameter = 'name';
|
|
15
|
+
/**
|
|
16
|
+
* Checks every command before an agent is built, so a mistake in the configuration fails there
|
|
17
|
+
* and never as a refused call in the middle of a run.
|
|
18
|
+
*
|
|
19
|
+
* @throws {InvalidAgentConfigError} for the first thing that is wrong.
|
|
20
|
+
*/
|
|
21
|
+
export function validateCommands(commands) {
|
|
22
|
+
// Unknown throughout: the configuration may come from a JSON file, whatever its type says.
|
|
23
|
+
if (!isRecord(commands))
|
|
24
|
+
throw new InvalidAgentConfigError('Commands must be an object');
|
|
25
|
+
const entries = Object.entries(commands);
|
|
26
|
+
if (entries.length === 0)
|
|
27
|
+
throw new InvalidAgentConfigError('No command is configured');
|
|
28
|
+
for (const [name, definition] of entries) {
|
|
29
|
+
const fail = (problem) => {
|
|
30
|
+
throw new InvalidAgentConfigError(`Command "${name}" ${problem}`);
|
|
31
|
+
};
|
|
32
|
+
if (!namePattern.test(name)) {
|
|
33
|
+
fail('needs a name of letters, digits, "-" and "_", starting with a letter');
|
|
34
|
+
}
|
|
35
|
+
if (!isRecord(definition))
|
|
36
|
+
fail('must be an object');
|
|
37
|
+
const { description, executable, cwd, timeoutMs, outputBytes, env, args, roles } = definition;
|
|
38
|
+
if (typeof description !== 'string' || description.trim() === '') {
|
|
39
|
+
fail('needs a description');
|
|
40
|
+
}
|
|
41
|
+
if (typeof executable !== 'string' || !isArgument(executable))
|
|
42
|
+
fail('needs an executable');
|
|
43
|
+
if (cwd !== undefined && (typeof cwd !== 'string' || !isArgument(cwd))) {
|
|
44
|
+
fail('has an invalid cwd');
|
|
45
|
+
}
|
|
46
|
+
checkLimit(timeoutMs, maxTimeoutMs, 'timeoutMs', fail);
|
|
47
|
+
checkLimit(outputBytes, maxOutputBytes, 'outputBytes', fail);
|
|
48
|
+
if (env !== undefined && !isRecord(env))
|
|
49
|
+
fail('needs env to map names to values');
|
|
50
|
+
for (const [variable, value] of Object.entries((env ?? {}))) {
|
|
51
|
+
if (!variablePattern.test(variable))
|
|
52
|
+
fail(`sets an invalid variable name "${variable}"`);
|
|
53
|
+
if (typeof value !== 'string' || value.includes('\0')) {
|
|
54
|
+
fail(`sets "${variable}" to a non-string`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (roles !== undefined &&
|
|
58
|
+
(!Array.isArray(roles) ||
|
|
59
|
+
roles.length === 0 ||
|
|
60
|
+
new Set(roles).size !== roles.length ||
|
|
61
|
+
!roles.every(role => typeof role === 'string' && isOneOf(commandRoles, role)))) {
|
|
62
|
+
fail(`needs roles to list some of ${commandRoles.join(', ')}, each once`);
|
|
63
|
+
}
|
|
64
|
+
if (args !== undefined && !Array.isArray(args))
|
|
65
|
+
fail('needs args to be a list');
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
for (const argument of (args ?? [])) {
|
|
68
|
+
if (typeof argument === 'string') {
|
|
69
|
+
if (!isArgument(argument, { allowEmpty: true })) {
|
|
70
|
+
fail('has an argument with a NUL character');
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!isRecord(argument))
|
|
75
|
+
fail('has an argument that is neither text nor a parameter');
|
|
76
|
+
checkParameter(argument, seen, fail);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function isRecord(value) {
|
|
81
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
82
|
+
}
|
|
83
|
+
function checkLimit(value, max, field, fail) {
|
|
84
|
+
if (value === undefined)
|
|
85
|
+
return;
|
|
86
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0 || value > max) {
|
|
87
|
+
fail(`needs ${field} to be a whole number from 1 to ${String(max)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function checkParameter(parameter, seen, fail) {
|
|
91
|
+
// Unknown: the configuration may come from a JSON file, whatever its type says.
|
|
92
|
+
const name = parameter.name;
|
|
93
|
+
if (typeof name !== 'string' || !namePattern.test(name) || name === reservedParameter) {
|
|
94
|
+
fail(`has a parameter with an invalid name ${typeof name === 'string' ? `"${name}"` : 'that is not text'}`);
|
|
95
|
+
}
|
|
96
|
+
if (seen.has(name))
|
|
97
|
+
fail(`uses the parameter "${name}" twice`);
|
|
98
|
+
seen.add(name);
|
|
99
|
+
if (typeof parameter.description !== 'string' || parameter.description.trim() === '') {
|
|
100
|
+
fail(`needs a description for the parameter "${name}"`);
|
|
101
|
+
}
|
|
102
|
+
const { required } = parameter;
|
|
103
|
+
if (required !== undefined && typeof required !== 'boolean') {
|
|
104
|
+
fail(`needs required to be true or false for the parameter "${name}"`);
|
|
105
|
+
}
|
|
106
|
+
switch (parameter.type) {
|
|
107
|
+
case 'paths':
|
|
108
|
+
return;
|
|
109
|
+
case 'choice':
|
|
110
|
+
if (!Array.isArray(parameter.values) ||
|
|
111
|
+
parameter.values.length === 0 ||
|
|
112
|
+
!parameter.values.every(value => typeof value === 'string' && isArgument(value))) {
|
|
113
|
+
fail(`needs the choice "${name}" to list at least one value`);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
case 'text':
|
|
117
|
+
if (typeof parameter.pattern !== 'string')
|
|
118
|
+
fail(`needs a pattern for the text "${name}"`);
|
|
119
|
+
try {
|
|
120
|
+
textMatcher(parameter.pattern);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
fail(`has a pattern for the text "${name}" that is not a valid regular expression`);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
default:
|
|
127
|
+
fail(`has a parameter "${name}" of an unknown type`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function isArgument(value, { allowEmpty = false } = {}) {
|
|
131
|
+
return (allowEmpty || value !== '') && !value.includes('\0');
|
|
132
|
+
}
|
|
133
|
+
function textMatcher(pattern) {
|
|
134
|
+
return new RegExp(`^(?:${pattern})$`, 'u');
|
|
135
|
+
}
|
|
136
|
+
/** The commands `role` may run: those that name it, or, without `roles`, the executor. */
|
|
137
|
+
export function commandsFor(commands, role) {
|
|
138
|
+
return Object.fromEntries(Object.entries(commands).filter(([, definition]) => (definition.roles ?? ['executor']).includes(role)));
|
|
139
|
+
}
|
|
140
|
+
/** The parameters of a command, in the order its arguments use them. */
|
|
141
|
+
export function parametersOf(definition) {
|
|
142
|
+
return (definition.args ?? []).filter(argument => typeof argument !== 'string');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The arguments a call runs with: the fixed ones as declared, and each parameter replaced by
|
|
146
|
+
* the value the model gave, once checked.
|
|
147
|
+
*
|
|
148
|
+
* @throws {Error} worded for the model, when a value is not one the parameter takes.
|
|
149
|
+
*/
|
|
150
|
+
export function buildArguments(definition, values) {
|
|
151
|
+
const args = [];
|
|
152
|
+
for (const argument of definition.args ?? []) {
|
|
153
|
+
if (typeof argument === 'string') {
|
|
154
|
+
args.push(argument);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const value = values[argument.name] ?? null;
|
|
158
|
+
args.push(...valueArguments(argument, value));
|
|
159
|
+
}
|
|
160
|
+
return args;
|
|
161
|
+
}
|
|
162
|
+
function valueArguments(parameter, value) {
|
|
163
|
+
const { name } = parameter;
|
|
164
|
+
if (parameter.type === 'paths') {
|
|
165
|
+
if (!Array.isArray(value))
|
|
166
|
+
throw new Error(`"${name}" takes a list of paths`);
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
if (value === null) {
|
|
170
|
+
if (parameter.required === true)
|
|
171
|
+
throw new Error(`"${name}" is required`);
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
if (typeof value !== 'string')
|
|
175
|
+
throw new Error(`"${name}" takes one value`);
|
|
176
|
+
if (parameter.type === 'choice') {
|
|
177
|
+
if (!parameter.values.includes(value)) {
|
|
178
|
+
throw new Error(`"${name}" takes one of: ${parameter.values.join(', ')}`);
|
|
179
|
+
}
|
|
180
|
+
return [value];
|
|
181
|
+
}
|
|
182
|
+
if (value.startsWith('-'))
|
|
183
|
+
throw new Error(`"${name}" may not start with "-"`);
|
|
184
|
+
if (value.length > maxTextLength ||
|
|
185
|
+
value.includes('\0') ||
|
|
186
|
+
!textMatcher(parameter.pattern).test(value)) {
|
|
187
|
+
throw new Error(`"${name}" must match ${parameter.pattern}`);
|
|
188
|
+
}
|
|
189
|
+
return [value];
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../../src/tools/domain/commands.ts"],"names":[],"mappings":"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","sourcesContent":["import { InvalidAgentConfigError } from '#src/shared/domain/errors';\nimport { isOneOf } from '#src/shared/domain/isOneOf';\n\n/**\n * Something the model fills in a command's arguments. It never writes free arguments: each\n * value is checked against what the consumer declared here, and none can become a flag it did\n * not declare.\n *\n * - `paths`: files or folders inside the roots that the agent may read, each its own argument,\n * relative to the command's working directory. The model may give none.\n * - `choice`: one of `values`, as the consumer wrote them, so a value may be a flag.\n * - `text`: a value that matches `pattern` whole, and never starts with `-`.\n *\n * A `choice` or `text` that is not `required` may be left out, and then adds no argument.\n */\nexport type CommandParameter =\n\t| { type: 'paths'; name: string; description: string }\n\t| { type: 'choice'; name: string; description: string; values: string[]; required?: boolean }\n\t| { type: 'text'; name: string; description: string; pattern: string; required?: boolean };\n\n/** One argument of a command: fixed text, or the place where a parameter goes. */\nexport type CommandArgument = string | CommandParameter;\n\n/** The roles of the model-backed orchestrator a command can be given to. */\nexport const commandRoles = ['planner', 'executor', 'reviewer'] as const;\nexport type CommandRole = (typeof commandRoles)[number];\n\n/**\n * A command the consumer lets the agent run, such as the project's tests or its formatter. It\n * runs without a shell, so nothing in an argument is ever read as shell syntax.\n */\nexport interface CommandDefinition {\n\t/** What it does and when to run it, for the model. */\n\tdescription: string;\n\t/**\n\t * The program: a name looked up in the command's `PATH`, or a path. A relative path is from\n\t * `cwd`.\n\t */\n\texecutable: string;\n\targs?: CommandArgument[];\n\t/**\n\t * Where it runs, inside a `write` root: relative to the first root, or absolute. Defaults to\n\t * the first root.\n\t */\n\tcwd?: string;\n\t/**\n\t * Variables set for it, on top of the few it inherits: `PATH`, `HOME`, `TMPDIR`, `USER`,\n\t * `SHELL`, `LANG` and `LC_*`. Nothing else of this process's environment reaches it, so\n\t * neither do the provider keys.\n\t */\n\tenv?: Record<string, string>;\n\t/** How long it may run before its whole process group is stopped. Defaults to 10 minutes. */\n\ttimeoutMs?: number;\n\t/** How much of the end of its output the model reads. Defaults to 16 KB. */\n\toutputBytes?: number;\n\t/**\n\t * Which roles of `createLLMOrchestrator` may run it, such as the planner for a command that\n\t * reads an issue. Defaults to the executor only. Whoever runs it, the files are captured\n\t * around it, so what it changes is in the history. `createFileTools` offers every command.\n\t */\n\troles?: CommandRole[];\n}\n\n/** The commands an agent may run, by the name the model calls each one. */\nexport type Commands = Record<string, CommandDefinition>;\n\nexport const defaultCommandTimeoutMs = 10 * 60 * 1000;\nexport const defaultCommandOutputBytes = 16 * 1024;\n// A test filter or a snapshot name, not a script.\nconst maxTextLength = 1000;\nconst maxOutputBytes = 1024 * 1024;\nconst maxTimeoutMs = 24 * 60 * 60 * 1000;\n\nconst namePattern = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;\nconst variablePattern = /^[A-Za-z_][A-Za-z0-9_]*$/;\n// The tool's input names the command with this key, so no parameter may take it.\nconst reservedParameter = 'name';\n\n/** A parameter's value as the model sent it, with `paths` already checked and resolved. */\nexport type ParameterValue = string[] | string | null;\n\n/**\n * Checks every command before an agent is built, so a mistake in the configuration fails there\n * and never as a refused call in the middle of a run.\n *\n * @throws {InvalidAgentConfigError} for the first thing that is wrong.\n */\nexport function validateCommands(commands: Commands): void {\n\t// Unknown throughout: the configuration may come from a JSON file, whatever its type says.\n\tif (!isRecord(commands)) throw new InvalidAgentConfigError('Commands must be an object');\n\tconst entries = Object.entries(commands as Record<string, unknown>);\n\tif (entries.length === 0) throw new InvalidAgentConfigError('No command is configured');\n\tfor (const [name, definition] of entries) {\n\t\tconst fail = (problem: string) => {\n\t\t\tthrow new InvalidAgentConfigError(`Command \"${name}\" ${problem}`);\n\t\t};\n\t\tif (!namePattern.test(name)) {\n\t\t\tfail('needs a name of letters, digits, \"-\" and \"_\", starting with a letter');\n\t\t}\n\t\tif (!isRecord(definition)) fail('must be an object');\n\t\tconst { description, executable, cwd, timeoutMs, outputBytes, env, args, roles } =\n\t\t\tdefinition as Record<string, unknown>;\n\t\tif (typeof description !== 'string' || description.trim() === '') {\n\t\t\tfail('needs a description');\n\t\t}\n\t\tif (typeof executable !== 'string' || !isArgument(executable)) fail('needs an executable');\n\t\tif (cwd !== undefined && (typeof cwd !== 'string' || !isArgument(cwd))) {\n\t\t\tfail('has an invalid cwd');\n\t\t}\n\t\tcheckLimit(timeoutMs, maxTimeoutMs, 'timeoutMs', fail);\n\t\tcheckLimit(outputBytes, maxOutputBytes, 'outputBytes', fail);\n\t\tif (env !== undefined && !isRecord(env)) fail('needs env to map names to values');\n\t\tfor (const [variable, value] of Object.entries((env ?? {}) as Record<string, unknown>)) {\n\t\t\tif (!variablePattern.test(variable)) fail(`sets an invalid variable name \"${variable}\"`);\n\t\t\tif (typeof value !== 'string' || value.includes('\\0')) {\n\t\t\t\tfail(`sets \"${variable}\" to a non-string`);\n\t\t\t}\n\t\t}\n\n\t\tif (\n\t\t\troles !== undefined &&\n\t\t\t(!Array.isArray(roles) ||\n\t\t\t\troles.length === 0 ||\n\t\t\t\tnew Set(roles).size !== roles.length ||\n\t\t\t\t!roles.every(role => typeof role === 'string' && isOneOf(commandRoles, role)))\n\t\t) {\n\t\t\tfail(`needs roles to list some of ${commandRoles.join(', ')}, each once`);\n\t\t}\n\t\tif (args !== undefined && !Array.isArray(args)) fail('needs args to be a list');\n\t\tconst seen = new Set<string>();\n\t\tfor (const argument of (args ?? []) as unknown[]) {\n\t\t\tif (typeof argument === 'string') {\n\t\t\t\tif (!isArgument(argument, { allowEmpty: true })) {\n\t\t\t\t\tfail('has an argument with a NUL character');\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!isRecord(argument)) fail('has an argument that is neither text nor a parameter');\n\t\t\tcheckParameter(argument as CommandParameter, seen, fail);\n\t\t}\n\t}\n}\n\nfunction isRecord(value: unknown): boolean {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction checkLimit(\n\tvalue: unknown,\n\tmax: number,\n\tfield: string,\n\tfail: (problem: string) => never,\n): void {\n\tif (value === undefined) return;\n\tif (typeof value !== 'number' || !Number.isInteger(value) || value <= 0 || value > max) {\n\t\tfail(`needs ${field} to be a whole number from 1 to ${String(max)}`);\n\t}\n}\n\nfunction checkParameter(\n\tparameter: CommandParameter,\n\tseen: Set<string>,\n\tfail: (problem: string) => never,\n): void {\n\t// Unknown: the configuration may come from a JSON file, whatever its type says.\n\tconst name: unknown = parameter.name;\n\tif (typeof name !== 'string' || !namePattern.test(name) || name === reservedParameter) {\n\t\tfail(\n\t\t\t`has a parameter with an invalid name ${typeof name === 'string' ? `\"${name}\"` : 'that is not text'}`,\n\t\t);\n\t}\n\tif (seen.has(name)) fail(`uses the parameter \"${name}\" twice`);\n\tseen.add(name);\n\tif (typeof parameter.description !== 'string' || parameter.description.trim() === '') {\n\t\tfail(`needs a description for the parameter \"${name}\"`);\n\t}\n\tconst { required } = parameter as { required?: unknown };\n\tif (required !== undefined && typeof required !== 'boolean') {\n\t\tfail(`needs required to be true or false for the parameter \"${name}\"`);\n\t}\n\n\tswitch (parameter.type) {\n\t\tcase 'paths':\n\t\t\treturn;\n\t\tcase 'choice':\n\t\t\tif (\n\t\t\t\t!Array.isArray(parameter.values) ||\n\t\t\t\tparameter.values.length === 0 ||\n\t\t\t\t!parameter.values.every(value => typeof value === 'string' && isArgument(value))\n\t\t\t) {\n\t\t\t\tfail(`needs the choice \"${name}\" to list at least one value`);\n\t\t\t}\n\t\t\treturn;\n\t\tcase 'text':\n\t\t\tif (typeof parameter.pattern !== 'string') fail(`needs a pattern for the text \"${name}\"`);\n\t\t\ttry {\n\t\t\t\ttextMatcher(parameter.pattern);\n\t\t\t} catch {\n\t\t\t\tfail(`has a pattern for the text \"${name}\" that is not a valid regular expression`);\n\t\t\t}\n\t\t\treturn;\n\t\tdefault:\n\t\t\tfail(`has a parameter \"${name}\" of an unknown type`);\n\t}\n}\n\nfunction isArgument(value: string, { allowEmpty = false } = {}): boolean {\n\treturn (allowEmpty || value !== '') && !value.includes('\\0');\n}\n\nfunction textMatcher(pattern: string): RegExp {\n\treturn new RegExp(`^(?:${pattern})$`, 'u');\n}\n\n/** The commands `role` may run: those that name it, or, without `roles`, the executor. */\nexport function commandsFor(commands: Commands, role: CommandRole): Commands {\n\treturn Object.fromEntries(\n\t\tObject.entries(commands).filter(([, definition]) =>\n\t\t\t(definition.roles ?? ['executor']).includes(role),\n\t\t),\n\t);\n}\n\n/** The parameters of a command, in the order its arguments use them. */\nexport function parametersOf(definition: CommandDefinition): CommandParameter[] {\n\treturn (definition.args ?? []).filter(argument => typeof argument !== 'string');\n}\n\n/**\n * The arguments a call runs with: the fixed ones as declared, and each parameter replaced by\n * the value the model gave, once checked.\n *\n * @throws {Error} worded for the model, when a value is not one the parameter takes.\n */\nexport function buildArguments(\n\tdefinition: CommandDefinition,\n\tvalues: Record<string, ParameterValue>,\n): string[] {\n\tconst args: string[] = [];\n\tfor (const argument of definition.args ?? []) {\n\t\tif (typeof argument === 'string') {\n\t\t\targs.push(argument);\n\t\t\tcontinue;\n\t\t}\n\t\tconst value = values[argument.name] ?? null;\n\t\targs.push(...valueArguments(argument, value));\n\t}\n\treturn args;\n}\n\nfunction valueArguments(parameter: CommandParameter, value: ParameterValue): string[] {\n\tconst { name } = parameter;\n\tif (parameter.type === 'paths') {\n\t\tif (!Array.isArray(value)) throw new Error(`\"${name}\" takes a list of paths`);\n\t\treturn value;\n\t}\n\tif (value === null) {\n\t\tif (parameter.required === true) throw new Error(`\"${name}\" is required`);\n\t\treturn [];\n\t}\n\tif (typeof value !== 'string') throw new Error(`\"${name}\" takes one value`);\n\n\tif (parameter.type === 'choice') {\n\t\tif (!parameter.values.includes(value)) {\n\t\t\tthrow new Error(`\"${name}\" takes one of: ${parameter.values.join(', ')}`);\n\t\t}\n\t\treturn [value];\n\t}\n\n\tif (value.startsWith('-')) throw new Error(`\"${name}\" may not start with \"-\"`);\n\tif (\n\t\tvalue.length > maxTextLength ||\n\t\tvalue.includes('\\0') ||\n\t\t!textMatcher(parameter.pattern).test(value)\n\t) {\n\t\tthrow new Error(`\"${name}\" must match ${parameter.pattern}`);\n\t}\n\treturn [value];\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a modification returns: its path, then the lines around each change as the file now
|
|
3
|
+
* reads them, numbered, with two lines of context. The model can check its edit without reading
|
|
4
|
+
* the file again. What does not fit is counted, not shown.
|
|
5
|
+
*/
|
|
6
|
+
export declare function describeModification(path: string, before: string, after: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* An edit as a git-style unified diff, for the person watching it happen. A missing side is a
|
|
9
|
+
* file that did not exist, or no longer does. Too long a diff is cut, and says so.
|
|
10
|
+
*/
|
|
11
|
+
export declare function editDiff(path: string, before: string | undefined, after: string | undefined): string;
|
|
12
|
+
/** What a creation returns: a confirmation and the size, not the content the model just sent. */
|
|
13
|
+
export declare function describeCreation(path: string, content: Buffer): string;
|
|
14
|
+
export declare function describeDeletion(path: string): string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { unifiedDiff } from '#src/diff/domain/unifiedDiff';
|
|
2
|
+
import { cutLine } from "./lineLength.js";
|
|
3
|
+
// Enough to place a change without reading the file again.
|
|
4
|
+
const contextLines = 2;
|
|
5
|
+
// What one response may show: a large rewrite is named, not repeated.
|
|
6
|
+
const maxShownLines = 60;
|
|
7
|
+
/** A line of `path` as the model reads it: without the `\r` of a CRLF ending, and not too long. */
|
|
8
|
+
function shownLine(path, text) {
|
|
9
|
+
return cutLine(path, text.endsWith('\r') ? text.slice(0, -1) : text);
|
|
10
|
+
}
|
|
11
|
+
function lineCount(text) {
|
|
12
|
+
if (text === '')
|
|
13
|
+
return 0;
|
|
14
|
+
const lines = text.split('\n');
|
|
15
|
+
return lines.at(-1) === '' ? lines.length - 1 : lines.length;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* What a modification returns: its path, then the lines around each change as the file now
|
|
19
|
+
* reads them, numbered, with two lines of context. The model can check its edit without reading
|
|
20
|
+
* the file again. What does not fit is counted, not shown.
|
|
21
|
+
*/
|
|
22
|
+
export function describeModification(path, before, after) {
|
|
23
|
+
if (after === '')
|
|
24
|
+
return `Changed ${path}. The file is now empty.`;
|
|
25
|
+
const shown = [];
|
|
26
|
+
let hidden = 0;
|
|
27
|
+
let line = 0;
|
|
28
|
+
for (const text of unifiedDiff(before, after, { context: contextLines }).split('\n')) {
|
|
29
|
+
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(text);
|
|
30
|
+
if (hunk) {
|
|
31
|
+
if (shown.length > 0 && shown.length < maxShownLines)
|
|
32
|
+
shown.push('…');
|
|
33
|
+
line = Number(hunk[1]);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// Removed lines are gone from the file, and git's marker is not a line of it.
|
|
37
|
+
if (!text.startsWith(' ') && !text.startsWith('+'))
|
|
38
|
+
continue;
|
|
39
|
+
if (shown.length < maxShownLines) {
|
|
40
|
+
shown.push(`${String(line)}: ${shownLine(path, text.slice(1))}`);
|
|
41
|
+
}
|
|
42
|
+
else
|
|
43
|
+
hidden++;
|
|
44
|
+
line++;
|
|
45
|
+
}
|
|
46
|
+
const header = `Changed ${path}. It now reads, around the change:`;
|
|
47
|
+
const notice = hidden > 0
|
|
48
|
+
? [`[${String(hidden)} more lines of the change are not shown. Read the file to see them.]`]
|
|
49
|
+
: [];
|
|
50
|
+
return [header, ...shown, ...notice].join('\n');
|
|
51
|
+
}
|
|
52
|
+
// What one edit's diff may hold for the person watching; the history keeps the whole change.
|
|
53
|
+
const maxDiffLines = 400;
|
|
54
|
+
/**
|
|
55
|
+
* An edit as a git-style unified diff, for the person watching it happen. A missing side is a
|
|
56
|
+
* file that did not exist, or no longer does. Too long a diff is cut, and says so.
|
|
57
|
+
*/
|
|
58
|
+
export function editDiff(path, before, after) {
|
|
59
|
+
const lines = [
|
|
60
|
+
`--- ${before === undefined ? '/dev/null' : `a/${path}`}`,
|
|
61
|
+
`+++ ${after === undefined ? '/dev/null' : `b/${path}`}`,
|
|
62
|
+
...unifiedDiff(before ?? '', after ?? '', { context: contextLines }).split('\n'),
|
|
63
|
+
];
|
|
64
|
+
if (lines.length <= maxDiffLines)
|
|
65
|
+
return lines.join('\n');
|
|
66
|
+
return [
|
|
67
|
+
...lines.slice(0, maxDiffLines),
|
|
68
|
+
`... ${String(lines.length - maxDiffLines)} more lines of the diff not shown`,
|
|
69
|
+
].join('\n');
|
|
70
|
+
}
|
|
71
|
+
/** What a creation returns: a confirmation and the size, not the content the model just sent. */
|
|
72
|
+
export function describeCreation(path, content) {
|
|
73
|
+
const lines = lineCount(content.toString('utf8'));
|
|
74
|
+
return `Created ${path} (${String(lines)} ${lines === 1 ? 'line' : 'lines'}, ${String(content.length)} bytes).`;
|
|
75
|
+
}
|
|
76
|
+
export function describeDeletion(path) {
|
|
77
|
+
return `Deleted ${path}.`;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=editResponse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editResponse.js","sourceRoot":"","sources":["../../../src/tools/domain/editResponse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE1C,2DAA2D;AAC3D,MAAM,YAAY,GAAG,CAAC,CAAC;AACvB,sEAAsE;AACtE,MAAM,aAAa,GAAG,EAAE,CAAC;AACzB,mGAAmG;AACnG,SAAS,SAAS,CAAC,IAAY,EAAE,IAAY;IAC5C,OAAO,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AACtE,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC9B,IAAI,IAAI,KAAK,EAAE;QAAE,OAAO,CAAC,CAAC;IAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/B,OAAO,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC;AAC9D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY,EAAE,MAAc,EAAE,KAAa;IAC/E,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,WAAW,IAAI,0BAA0B,CAAC;IACnE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACtF,MAAM,IAAI,GAAG,uCAAuC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChE,IAAI,IAAI,EAAE,CAAC;YACV,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,aAAa;gBAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACtE,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YACvB,SAAS;QACV,CAAC;QACD,8EAA8E;QAC9E,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAC7D,IAAI,KAAK,CAAC,MAAM,GAAG,aAAa,EAAE,CAAC;YAClC,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAClE,CAAC;;YAAM,MAAM,EAAE,CAAC;QAChB,IAAI,EAAE,CAAC;IACR,CAAC;IAED,MAAM,MAAM,GAAG,WAAW,IAAI,oCAAoC,CAAC;IACnE,MAAM,MAAM,GACX,MAAM,GAAG,CAAC;QACT,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,sEAAsE,CAAC;QAC5F,CAAC,CAAC,EAAE,CAAC;IACP,OAAO,CAAC,MAAM,EAAE,GAAG,KAAK,EAAE,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACjD,CAAC;AAED,6FAA6F;AAC7F,MAAM,YAAY,GAAG,GAAG,CAAC;AAEzB;;;GAGG;AACH,MAAM,UAAU,QAAQ,CACvB,IAAY,EACZ,MAA0B,EAC1B,KAAyB;IAEzB,MAAM,KAAK,GAAG;QACb,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,EAAE;QACzD,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,EAAE;QACxD,GAAG,WAAW,CAAC,MAAM,IAAI,EAAE,EAAE,KAAK,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;KAChF,CAAC;IACF,IAAI,KAAK,CAAC,MAAM,IAAI,YAAY;QAAE,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1D,OAAO;QACN,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC;QAC/B,OAAO,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,YAAY,CAAC,mCAAmC;KAC7E,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,OAAe;IAC7D,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IAClD,OAAO,WAAW,IAAI,KAAK,MAAM,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,KAAK,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC;AACjH,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC5C,OAAO,WAAW,IAAI,GAAG,CAAC;AAC3B,CAAC","sourcesContent":["import { unifiedDiff } from '#src/diff/domain/unifiedDiff';\nimport { cutLine } from './lineLength.ts';\n\n// Enough to place a change without reading the file again.\nconst contextLines = 2;\n// What one response may show: a large rewrite is named, not repeated.\nconst maxShownLines = 60;\n/** A line of `path` as the model reads it: without the `\\r` of a CRLF ending, and not too long. */\nfunction shownLine(path: string, text: string): string {\n\treturn cutLine(path, text.endsWith('\\r') ? text.slice(0, -1) : text);\n}\n\nfunction lineCount(text: string): number {\n\tif (text === '') return 0;\n\tconst lines = text.split('\\n');\n\treturn lines.at(-1) === '' ? lines.length - 1 : lines.length;\n}\n\n/**\n * What a modification returns: its path, then the lines around each change as the file now\n * reads them, numbered, with two lines of context. The model can check its edit without reading\n * the file again. What does not fit is counted, not shown.\n */\nexport function describeModification(path: string, before: string, after: string): string {\n\tif (after === '') return `Changed ${path}. The file is now empty.`;\n\tconst shown: string[] = [];\n\tlet hidden = 0;\n\tlet line = 0;\n\tfor (const text of unifiedDiff(before, after, { context: contextLines }).split('\\n')) {\n\t\tconst hunk = /^@@ -\\d+(?:,\\d+)? \\+(\\d+)(?:,\\d+)? @@/.exec(text);\n\t\tif (hunk) {\n\t\t\tif (shown.length > 0 && shown.length < maxShownLines) shown.push('…');\n\t\t\tline = Number(hunk[1]);\n\t\t\tcontinue;\n\t\t}\n\t\t// Removed lines are gone from the file, and git's marker is not a line of it.\n\t\tif (!text.startsWith(' ') && !text.startsWith('+')) continue;\n\t\tif (shown.length < maxShownLines) {\n\t\t\tshown.push(`${String(line)}: ${shownLine(path, text.slice(1))}`);\n\t\t} else hidden++;\n\t\tline++;\n\t}\n\n\tconst header = `Changed ${path}. It now reads, around the change:`;\n\tconst notice =\n\t\thidden > 0\n\t\t\t? [`[${String(hidden)} more lines of the change are not shown. Read the file to see them.]`]\n\t\t\t: [];\n\treturn [header, ...shown, ...notice].join('\\n');\n}\n\n// What one edit's diff may hold for the person watching; the history keeps the whole change.\nconst maxDiffLines = 400;\n\n/**\n * An edit as a git-style unified diff, for the person watching it happen. A missing side is a\n * file that did not exist, or no longer does. Too long a diff is cut, and says so.\n */\nexport function editDiff(\n\tpath: string,\n\tbefore: string | undefined,\n\tafter: string | undefined,\n): string {\n\tconst lines = [\n\t\t`--- ${before === undefined ? '/dev/null' : `a/${path}`}`,\n\t\t`+++ ${after === undefined ? '/dev/null' : `b/${path}`}`,\n\t\t...unifiedDiff(before ?? '', after ?? '', { context: contextLines }).split('\\n'),\n\t];\n\tif (lines.length <= maxDiffLines) return lines.join('\\n');\n\treturn [\n\t\t...lines.slice(0, maxDiffLines),\n\t\t`... ${String(lines.length - maxDiffLines)} more lines of the diff not shown`,\n\t].join('\\n');\n}\n\n/** What a creation returns: a confirmation and the size, not the content the model just sent. */\nexport function describeCreation(path: string, content: Buffer): string {\n\tconst lines = lineCount(content.toString('utf8'));\n\treturn `Created ${path} (${String(lines)} ${lines === 1 ? 'line' : 'lines'}, ${String(content.length)} bytes).`;\n}\n\nexport function describeDeletion(path: string): string {\n\treturn `Deleted ${path}.`;\n}\n"]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { FileState } from '#src/recovery/domain/recoveryStore';
|
|
2
|
+
import type { AllowedPath } from './accessPolicy.ts';
|
|
3
|
+
/**
|
|
4
|
+
* One change to one file, with the path as the model wrote it. Each edit format, such as a
|
|
5
|
+
* patch or a text replacement, works out the whole new content and asks for one of these.
|
|
6
|
+
*
|
|
7
|
+
* `expected` is the hash of the content the change was worked out from. It is refused if the
|
|
8
|
+
* file no longer holds it, so a change is never applied to a version nobody looked at.
|
|
9
|
+
*/
|
|
10
|
+
export type FileChange = {
|
|
11
|
+
kind: 'create';
|
|
12
|
+
path: string;
|
|
13
|
+
content: Buffer;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'replace';
|
|
16
|
+
path: string;
|
|
17
|
+
content: Buffer;
|
|
18
|
+
expected?: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'delete';
|
|
21
|
+
path: string;
|
|
22
|
+
expected?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A change checked and ready to apply, and the only thing that can be applied: what is
|
|
26
|
+
* approved is exactly what is written. It is applied only if the file still holds `before`.
|
|
27
|
+
*/
|
|
28
|
+
export interface PreparedEdit {
|
|
29
|
+
readonly kind: FileChange['kind'];
|
|
30
|
+
/** As the model wrote it, for every message about it. */
|
|
31
|
+
readonly path: string;
|
|
32
|
+
readonly target: AllowedPath;
|
|
33
|
+
readonly before: FileState;
|
|
34
|
+
/**
|
|
35
|
+
* The editor's own copy, taken in `prepare`, for showing what will be written. Its bytes can
|
|
36
|
+
* still be changed, so `apply` writes them only while they match `contentHash`.
|
|
37
|
+
*/
|
|
38
|
+
readonly content: Buffer | undefined;
|
|
39
|
+
/** The SHA-256 of `content` as prepared. */
|
|
40
|
+
readonly contentHash: string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* The change can make `.gitignore` hide less, which would open paths to every tool. It
|
|
43
|
+
* takes effect only if the user allows it: a tool asks for it as `destructive`.
|
|
44
|
+
*/
|
|
45
|
+
readonly widensIgnoreRules: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Why a change was not made, worded for the model. Nothing was written: the file is as it was.
|
|
49
|
+
*/
|
|
50
|
+
export declare class EditRefusedError extends Error {
|
|
51
|
+
constructor(message: string, options?: {
|
|
52
|
+
cause?: unknown;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A change the file may hold but the run's record does not confirm, worded for the model: it was
|
|
57
|
+
* made and could not be recorded as made, or whether it was made cannot be told. The record keeps
|
|
58
|
+
* the change as prepared, so an undo can still check the file, and writing stops for the run.
|
|
59
|
+
*/
|
|
60
|
+
export declare class EditUnconfirmedError extends Error {
|
|
61
|
+
constructor(message: string, options?: {
|
|
62
|
+
cause?: unknown;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a change was not made, worded for the model. Nothing was written: the file is as it was.
|
|
3
|
+
*/
|
|
4
|
+
export class EditRefusedError extends Error {
|
|
5
|
+
constructor(message, options) {
|
|
6
|
+
super(message, options);
|
|
7
|
+
this.name = 'EditRefusedError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A change the file may hold but the run's record does not confirm, worded for the model: it was
|
|
12
|
+
* made and could not be recorded as made, or whether it was made cannot be told. The record keeps
|
|
13
|
+
* the change as prepared, so an undo can still check the file, and writing stops for the run.
|
|
14
|
+
*/
|
|
15
|
+
export class EditUnconfirmedError extends Error {
|
|
16
|
+
constructor(message, options) {
|
|
17
|
+
super(message, options);
|
|
18
|
+
this.name = 'EditUnconfirmedError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=fileEdits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileEdits.js","sourceRoot":"","sources":["../../../src/tools/domain/fileEdits.ts"],"names":[],"mappings":"AAuCA;;GAEG;AACH,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAC1C,YAAY,OAAe,EAAE,OAA6B;QACzD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IAChC,CAAC;CACD;AAED;;;;GAIG;AACH,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC9C,YAAY,OAAe,EAAE,OAA6B;QACzD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACpC,CAAC;CACD","sourcesContent":["import type { FileState } from '#src/recovery/domain/recoveryStore';\nimport type { AllowedPath } from './accessPolicy.ts';\n\n/**\n * One change to one file, with the path as the model wrote it. Each edit format, such as a\n * patch or a text replacement, works out the whole new content and asks for one of these.\n *\n * `expected` is the hash of the content the change was worked out from. It is refused if the\n * file no longer holds it, so a change is never applied to a version nobody looked at.\n */\nexport type FileChange =\n\t| { kind: 'create'; path: string; content: Buffer }\n\t| { kind: 'replace'; path: string; content: Buffer; expected?: string }\n\t| { kind: 'delete'; path: string; expected?: string };\n\n/**\n * A change checked and ready to apply, and the only thing that can be applied: what is\n * approved is exactly what is written. It is applied only if the file still holds `before`.\n */\nexport interface PreparedEdit {\n\treadonly kind: FileChange['kind'];\n\t/** As the model wrote it, for every message about it. */\n\treadonly path: string;\n\treadonly target: AllowedPath;\n\treadonly before: FileState;\n\t/**\n\t * The editor's own copy, taken in `prepare`, for showing what will be written. Its bytes can\n\t * still be changed, so `apply` writes them only while they match `contentHash`.\n\t */\n\treadonly content: Buffer | undefined;\n\t/** The SHA-256 of `content` as prepared. */\n\treadonly contentHash: string | undefined;\n\t/**\n\t * The change can make `.gitignore` hide less, which would open paths to every tool. It\n\t * takes effect only if the user allows it: a tool asks for it as `destructive`.\n\t */\n\treadonly widensIgnoreRules: boolean;\n}\n\n/**\n * Why a change was not made, worded for the model. Nothing was written: the file is as it was.\n */\nexport class EditRefusedError extends Error {\n\tconstructor(message: string, options?: { cause?: unknown }) {\n\t\tsuper(message, options);\n\t\tthis.name = 'EditRefusedError';\n\t}\n}\n\n/**\n * A change the file may hold but the run's record does not confirm, worded for the model: it was\n * made and could not be recorded as made, or whether it was made cannot be told. The record keeps\n * the change as prepared, so an undo can still check the file, and writing stops for the run.\n */\nexport class EditUnconfirmedError extends Error {\n\tconstructor(message: string, options?: { cause?: unknown }) {\n\t\tsuper(message, options);\n\t\tthis.name = 'EditUnconfirmedError';\n\t}\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long a line of `path` may be before the model sees it cut: long for Markdown and plain
|
|
3
|
+
* text, short for everything else. See "Prose files show their long lines whole" in decisions.md.
|
|
4
|
+
*/
|
|
5
|
+
export declare function maxLineLength(path: string): number;
|
|
6
|
+
/** `text` as the model reads it from `path`: cut, with a mark, when longer than the file's limit. */
|
|
7
|
+
export declare function cutLine(path: string, text: string): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Code is where one line can be a whole minified bundle, larger than everything else the model
|
|
2
|
+
// reads; a line of hand-written code fits well within this.
|
|
3
|
+
const codeLineLength = 300;
|
|
4
|
+
// Prose keeps a paragraph, or a table row, on one line. Cut, such a line cannot be copied into
|
|
5
|
+
// an edit's context, so it is shown whole up to this, which also bounds a generated one.
|
|
6
|
+
const proseLineLength = 5000;
|
|
7
|
+
const proseExtensions = new Set(['.md', '.markdown', '.mdx', '.txt']);
|
|
8
|
+
/**
|
|
9
|
+
* How long a line of `path` may be before the model sees it cut: long for Markdown and plain
|
|
10
|
+
* text, short for everything else. See "Prose files show their long lines whole" in decisions.md.
|
|
11
|
+
*/
|
|
12
|
+
export function maxLineLength(path) {
|
|
13
|
+
return proseExtensions.has(extensionOf(path)) ? proseLineLength : codeLineLength;
|
|
14
|
+
}
|
|
15
|
+
/** The extension of the file's own name, lower case: `.md` for `docs/A.MD`, none for `.md`. */
|
|
16
|
+
function extensionOf(path) {
|
|
17
|
+
const name = path.slice(Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + 1);
|
|
18
|
+
const dot = name.lastIndexOf('.');
|
|
19
|
+
return dot > 0 ? name.slice(dot).toLowerCase() : '';
|
|
20
|
+
}
|
|
21
|
+
/** `text` as the model reads it from `path`: cut, with a mark, when longer than the file's limit. */
|
|
22
|
+
export function cutLine(path, text) {
|
|
23
|
+
const max = maxLineLength(path);
|
|
24
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=lineLength.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lineLength.js","sourceRoot":"","sources":["../../../src/tools/domain/lineLength.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,4DAA4D;AAC5D,MAAM,cAAc,GAAG,GAAG,CAAC;AAC3B,+FAA+F;AAC/F,yFAAyF;AACzF,MAAM,eAAe,GAAG,IAAI,CAAC;AAC7B,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAEtE;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACzC,OAAO,eAAe,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,cAAc,CAAC;AAClF,CAAC;AAED,+FAA+F;AAC/F,SAAS,WAAW,CAAC,IAAY;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACrF,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AACrD,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,OAAO,CAAC,IAAY,EAAE,IAAY;IACjD,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IAChC,OAAO,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5D,CAAC","sourcesContent":["// Code is where one line can be a whole minified bundle, larger than everything else the model\n// reads; a line of hand-written code fits well within this.\nconst codeLineLength = 300;\n// Prose keeps a paragraph, or a table row, on one line. Cut, such a line cannot be copied into\n// an edit's context, so it is shown whole up to this, which also bounds a generated one.\nconst proseLineLength = 5000;\nconst proseExtensions = new Set(['.md', '.markdown', '.mdx', '.txt']);\n\n/**\n * How long a line of `path` may be before the model sees it cut: long for Markdown and plain\n * text, short for everything else. See \"Prose files show their long lines whole\" in decisions.md.\n */\nexport function maxLineLength(path: string): number {\n\treturn proseExtensions.has(extensionOf(path)) ? proseLineLength : codeLineLength;\n}\n\n/** The extension of the file's own name, lower case: `.md` for `docs/A.MD`, none for `.md`. */\nfunction extensionOf(path: string): string {\n\tconst name = path.slice(Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\\\')) + 1);\n\tconst dot = name.lastIndexOf('.');\n\treturn dot > 0 ? name.slice(dot).toLowerCase() : '';\n}\n\n/** `text` as the model reads it from `path`: cut, with a mark, when longer than the file's limit. */\nexport function cutLine(path: string, text: string): string {\n\tconst max = maxLineLength(path);\n\treturn text.length > max ? `${text.slice(0, max)}…` : text;\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CommandLine, ToolRisk } from '#src/agent/domain/approval';
|
|
2
|
+
import type { ToolDefinition } from '#src/llm/domain/tool';
|
|
3
|
+
import type { ToolCallContext } from './readRegistry.ts';
|
|
4
|
+
import type { Tool } from './tool.ts';
|
|
5
|
+
/**
|
|
6
|
+
* A tool call checked and fixed before anyone approves it: its risk is judged on what will
|
|
7
|
+
* really happen, and `run` does exactly that, nothing worked out again.
|
|
8
|
+
*/
|
|
9
|
+
export interface PreparedCall {
|
|
10
|
+
readonly risk: ToolRisk;
|
|
11
|
+
/** For a change to a file, what it will change as a unified diff, fixed when prepared. */
|
|
12
|
+
readonly diff?: string;
|
|
13
|
+
/** For a call that runs a configured command, what it will run, fixed when prepared. */
|
|
14
|
+
readonly command?: CommandLine;
|
|
15
|
+
run(signal: AbortSignal): Promise<string>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A tool built by the harness, such as a write tool, which prepares each call against the
|
|
19
|
+
* state of the run before it is approved. Internal: consumers build a `Tool`.
|
|
20
|
+
*/
|
|
21
|
+
export interface PreparingTool extends ToolDefinition {
|
|
22
|
+
/** Rejects with a message for the model when the call cannot run. */
|
|
23
|
+
prepare(input: unknown, signal: AbortSignal, call: ToolCallContext): Promise<PreparedCall>;
|
|
24
|
+
}
|
|
25
|
+
/** What an agent can run: a consumer's `Tool`, or one the harness built. */
|
|
26
|
+
export type AgentTool = Tool | PreparingTool;
|
|
27
|
+
/**
|
|
28
|
+
* Brings any tool into the one lifecycle of prepare, approve, run. A consumer's `Tool` has its
|
|
29
|
+
* risk judged now, and later runs on the input as the model sent it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function prepareCall(tool: AgentTool, input: unknown, signal: AbortSignal, call: ToolCallContext): Promise<PreparedCall>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function isPreparing(tool) {
|
|
2
|
+
return 'prepare' in tool;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Brings any tool into the one lifecycle of prepare, approve, run. A consumer's `Tool` has its
|
|
6
|
+
* risk judged now, and later runs on the input as the model sent it.
|
|
7
|
+
*/
|
|
8
|
+
export async function prepareCall(tool, input, signal, call) {
|
|
9
|
+
if (isPreparing(tool))
|
|
10
|
+
return tool.prepare(input, signal, call);
|
|
11
|
+
const risk = tool.risk(input);
|
|
12
|
+
return { risk, run: runSignal => tool.execute(input, runSignal) };
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=preparedCall.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preparedCall.js","sourceRoot":"","sources":["../../../src/tools/domain/preparedCall.ts"],"names":[],"mappings":"AA8BA,SAAS,WAAW,CAAC,IAAe;IACnC,OAAO,SAAS,IAAI,IAAI,CAAC;AAC1B,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAChC,IAAe,EACf,KAAc,EACd,MAAmB,EACnB,IAAqB;IAErB,IAAI,WAAW,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IAEhE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9B,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;AACnE,CAAC","sourcesContent":["import type { CommandLine, ToolRisk } from '#src/agent/domain/approval';\nimport type { ToolDefinition } from '#src/llm/domain/tool';\nimport type { ToolCallContext } from './readRegistry.ts';\nimport type { Tool } from './tool.ts';\n\n/**\n * A tool call checked and fixed before anyone approves it: its risk is judged on what will\n * really happen, and `run` does exactly that, nothing worked out again.\n */\nexport interface PreparedCall {\n\treadonly risk: ToolRisk;\n\t/** For a change to a file, what it will change as a unified diff, fixed when prepared. */\n\treadonly diff?: string;\n\t/** For a call that runs a configured command, what it will run, fixed when prepared. */\n\treadonly command?: CommandLine;\n\trun(signal: AbortSignal): Promise<string>;\n}\n\n/**\n * A tool built by the harness, such as a write tool, which prepares each call against the\n * state of the run before it is approved. Internal: consumers build a `Tool`.\n */\nexport interface PreparingTool extends ToolDefinition {\n\t/** Rejects with a message for the model when the call cannot run. */\n\tprepare(input: unknown, signal: AbortSignal, call: ToolCallContext): Promise<PreparedCall>;\n}\n\n/** What an agent can run: a consumer's `Tool`, or one the harness built. */\nexport type AgentTool = Tool | PreparingTool;\n\nfunction isPreparing(tool: AgentTool): tool is PreparingTool {\n\treturn 'prepare' in tool;\n}\n\n/**\n * Brings any tool into the one lifecycle of prepare, approve, run. A consumer's `Tool` has its\n * risk judged now, and later runs on the input as the model sent it.\n */\nexport async function prepareCall(\n\ttool: AgentTool,\n\tinput: unknown,\n\tsignal: AbortSignal,\n\tcall: ToolCallContext,\n): Promise<PreparedCall> {\n\tif (isPreparing(tool)) return tool.prepare(input, signal, call);\n\n\tconst risk = tool.risk(input);\n\treturn { risk, run: runSignal => tool.execute(input, runSignal) };\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { RunContext } from '#src/agent/domain/runContext';
|
|
2
|
+
/**
|
|
3
|
+
* What one conversation has seen of each file: the hash of the content a read showed it, or an
|
|
4
|
+
* edit of its own left. An edit is made only from that version, so the model never changes a
|
|
5
|
+
* file it has not read, or one that changed since.
|
|
6
|
+
*
|
|
7
|
+
* Keyed by the file's real path, so two spellings of one file are one entry. It belongs to the
|
|
8
|
+
* conversation, not to the run: each role, with its own conversation, has its own.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ReadRegistry {
|
|
11
|
+
private seen;
|
|
12
|
+
/** The reads of one run, which count for the conversation only once the run succeeded. */
|
|
13
|
+
begin(): RunReads;
|
|
14
|
+
/**
|
|
15
|
+
* Forgets every file: the workspace moved through its history, so what the conversation saw
|
|
16
|
+
* may no longer be what the files hold.
|
|
17
|
+
*/
|
|
18
|
+
clear(): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The versions one run has seen, on top of what its conversation saw before it. A failed run
|
|
22
|
+
* records nothing in its conversation, so the model does not remember what it read there: its
|
|
23
|
+
* reads are kept only by `commit`.
|
|
24
|
+
*/
|
|
25
|
+
export declare class RunReads {
|
|
26
|
+
private readonly base;
|
|
27
|
+
private readonly own;
|
|
28
|
+
private readonly save;
|
|
29
|
+
constructor(base: ReadonlyMap<string, string>, save: (seen: Map<string, string>) => void);
|
|
30
|
+
/** The version of `absolute` the conversation has seen, if any. */
|
|
31
|
+
versionOf(absolute: string): string | undefined;
|
|
32
|
+
/** The conversation now knows `absolute` as holding the content `hash` names. */
|
|
33
|
+
record(absolute: string, hash: string): void;
|
|
34
|
+
/** The file is gone, as the conversation knows it. */
|
|
35
|
+
forget(absolute: string): void;
|
|
36
|
+
/** Keeps this run's reads for the conversation's next runs. */
|
|
37
|
+
commit(): void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* What a harness-built tool gets for one call: the run it belongs to, shared by every role, and
|
|
41
|
+
* the reads of the conversation that made the call.
|
|
42
|
+
*/
|
|
43
|
+
export interface ToolCallContext {
|
|
44
|
+
readonly run: RunContext;
|
|
45
|
+
readonly reads: RunReads;
|
|
46
|
+
}
|