@mikode13/harness 6.0.1 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -4
- package/dist/agent/domain/agent.d.ts +14 -0
- package/dist/agent/domain/agent.js.map +1 -1
- package/dist/engines/domain/model/llmAgent.js +31 -3
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/factory/infrastructure/agentLLMFactory.d.ts +21 -6
- package/dist/factory/infrastructure/agentLLMFactory.js +54 -18
- package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/domain/commands.d.ts +27 -0
- package/dist/tools/domain/commands.js +43 -15
- package/dist/tools/domain/commands.js.map +1 -1
- package/dist/tools/domain/readRegistry.d.ts +17 -2
- package/dist/tools/domain/readRegistry.js.map +1 -1
- package/dist/tools/infrastructure/commandTool.d.ts +6 -1
- package/dist/tools/infrastructure/commandTool.js +11 -5
- package/dist/tools/infrastructure/commandTool.js.map +1 -1
- package/dist/tools/infrastructure/exploreTool.d.ts +10 -0
- package/dist/tools/infrastructure/exploreTool.js +88 -0
- package/dist/tools/infrastructure/exploreTool.js.map +1 -0
- package/dist/tools/infrastructure/fileFormatter.d.ts +39 -0
- package/dist/tools/infrastructure/fileFormatter.js +114 -0
- package/dist/tools/infrastructure/fileFormatter.js.map +1 -0
- package/dist/tools/infrastructure/fileTools.d.ts +10 -2
- package/dist/tools/infrastructure/fileTools.js +14 -2
- package/dist/tools/infrastructure/fileTools.js.map +1 -1
- package/dist/tools/infrastructure/trackedEdits.d.ts +4 -4
- package/dist/tools/infrastructure/trackedEdits.js +32 -9
- package/dist/tools/infrastructure/trackedEdits.js.map +1 -1
- package/dist/tools/infrastructure/workspaceWrites.d.ts +5 -1
- package/dist/tools/infrastructure/workspaceWrites.js +4 -1
- package/dist/tools/infrastructure/workspaceWrites.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { ProgressEvent } from '#src/agent/domain/agent';
|
|
1
2
|
import type { RunContext } from '#src/agent/domain/runContext';
|
|
3
|
+
import type { Tokens } from '#src/shared/domain/tokens';
|
|
2
4
|
/**
|
|
3
5
|
* What one conversation has seen of each file: the hash of the content a read showed it, or an
|
|
4
6
|
* edit of its own left. An edit is made only from that version, so the model never changes a
|
|
@@ -37,10 +39,23 @@ export declare class RunReads {
|
|
|
37
39
|
commit(): void;
|
|
38
40
|
}
|
|
39
41
|
/**
|
|
40
|
-
* What a harness-built tool gets for one call: the run it belongs to, shared by every role,
|
|
41
|
-
*
|
|
42
|
+
* What a harness-built tool gets for one call: the run it belongs to, shared by every role, the
|
|
43
|
+
* reads of the conversation that made the call, and the run's narration and usage, for a call
|
|
44
|
+
* that does its own work on the model, such as running subagents.
|
|
42
45
|
*/
|
|
43
46
|
export interface ToolCallContext {
|
|
44
47
|
readonly run: RunContext;
|
|
45
48
|
readonly reads: RunReads;
|
|
49
|
+
/** The id of the call, which its `tool` events carry. */
|
|
50
|
+
readonly callId: string;
|
|
51
|
+
/**
|
|
52
|
+
* Narrates an event in the run's progress. It never throws: a failing callback ends the run
|
|
53
|
+
* once the call returns.
|
|
54
|
+
*/
|
|
55
|
+
readonly onProgress: (event: ProgressEvent) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Adds what the call spent on the model to the run's usage. `unreported` when a call it made
|
|
58
|
+
* came back without usage, which leaves the run's total unknown.
|
|
59
|
+
*/
|
|
60
|
+
readonly spend: (tokens: Tokens | undefined, unreported: boolean) => void;
|
|
46
61
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"readRegistry.js","sourceRoot":"","sources":["../../../src/tools/domain/readRegistry.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"readRegistry.js","sourceRoot":"","sources":["../../../src/tools/domain/readRegistry.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AACH,MAAM,OAAO,YAAY;IAChB,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEzC,0FAA0F;IAC1F,KAAK;QACJ,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE;YACrC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QAClB,CAAC,CAAC,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,KAAK;QACJ,IAAI,CAAC,IAAI,GAAG,IAAI,GAAG,EAAE,CAAC;IACvB,CAAC;CACD;AAED;;;;GAIG;AACH,MAAM,OAAO,QAAQ;IACH,IAAI,CAA8B;IAClC,GAAG,GAAG,IAAI,GAAG,EAA8B,CAAC;IAC5C,IAAI,CAAsC;IAE3D,YAAY,IAAiC,EAAE,IAAyC;QACvF,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAClB,CAAC;IAED,mEAAmE;IACnE,SAAS,CAAC,QAAgB;QACzB,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAClF,CAAC;IAED,iFAAiF;IACjF,MAAM,CAAC,QAAgB,EAAE,IAAY;QACpC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,sDAAsD;IACtD,MAAM,CAAC,QAAgB;QACtB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IACnC,CAAC;IAED,+DAA+D;IAC/D,MAAM;QACL,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChC,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;YACrC,IAAI,IAAI,KAAK,SAAS;gBAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;;gBACrC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC3B,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;CACD","sourcesContent":["import type { ProgressEvent } from '#src/agent/domain/agent';\nimport type { RunContext } from '#src/agent/domain/runContext';\nimport type { Tokens } from '#src/shared/domain/tokens';\n\n/**\n * What one conversation has seen of each file: the hash of the content a read showed it, or an\n * edit of its own left. An edit is made only from that version, so the model never changes a\n * file it has not read, or one that changed since.\n *\n * Keyed by the file's real path, so two spellings of one file are one entry. It belongs to the\n * conversation, not to the run: each role, with its own conversation, has its own.\n */\nexport class ReadRegistry {\n\tprivate seen = new Map<string, string>();\n\n\t/** The reads of one run, which count for the conversation only once the run succeeded. */\n\tbegin(): RunReads {\n\t\treturn new RunReads(this.seen, seen => {\n\t\t\tthis.seen = seen;\n\t\t});\n\t}\n\n\t/**\n\t * Forgets every file: the workspace moved through its history, so what the conversation saw\n\t * may no longer be what the files hold.\n\t */\n\tclear(): void {\n\t\tthis.seen = new Map();\n\t}\n}\n\n/**\n * The versions one run has seen, on top of what its conversation saw before it. A failed run\n * records nothing in its conversation, so the model does not remember what it read there: its\n * reads are kept only by `commit`.\n */\nexport class RunReads {\n\tprivate readonly base: ReadonlyMap<string, string>;\n\tprivate readonly own = new Map<string, string | undefined>();\n\tprivate readonly save: (seen: Map<string, string>) => void;\n\n\tconstructor(base: ReadonlyMap<string, string>, save: (seen: Map<string, string>) => void) {\n\t\tthis.base = base;\n\t\tthis.save = save;\n\t}\n\n\t/** The version of `absolute` the conversation has seen, if any. */\n\tversionOf(absolute: string): string | undefined {\n\t\treturn this.own.has(absolute) ? this.own.get(absolute) : this.base.get(absolute);\n\t}\n\n\t/** The conversation now knows `absolute` as holding the content `hash` names. */\n\trecord(absolute: string, hash: string): void {\n\t\tthis.own.set(absolute, hash);\n\t}\n\n\t/** The file is gone, as the conversation knows it. */\n\tforget(absolute: string): void {\n\t\tthis.own.set(absolute, undefined);\n\t}\n\n\t/** Keeps this run's reads for the conversation's next runs. */\n\tcommit(): void {\n\t\tconst seen = new Map(this.base);\n\t\tfor (const [path, hash] of this.own) {\n\t\t\tif (hash === undefined) seen.delete(path);\n\t\t\telse seen.set(path, hash);\n\t\t}\n\t\tthis.save(seen);\n\t}\n}\n\n/**\n * What a harness-built tool gets for one call: the run it belongs to, shared by every role, the\n * reads of the conversation that made the call, and the run's narration and usage, for a call\n * that does its own work on the model, such as running subagents.\n */\nexport interface ToolCallContext {\n\treadonly run: RunContext;\n\treadonly reads: RunReads;\n\t/** The id of the call, which its `tool` events carry. */\n\treadonly callId: string;\n\t/**\n\t * Narrates an event in the run's progress. It never throws: a failing callback ends the run\n\t * once the call returns.\n\t */\n\treadonly onProgress: (event: ProgressEvent) => void;\n\t/**\n\t * Adds what the call spent on the model to the run's usage. `unreported` when a call it made\n\t * came back without usage, which leaves the run's total unknown.\n\t */\n\treadonly spend: (tokens: Tokens | undefined, unreported: boolean) => void;\n}\n"]}
|
|
@@ -32,5 +32,10 @@ export declare function createCommandTool({ commands, policy, writes, covered, b
|
|
|
32
32
|
/** Runs the program. Replaced in tests, to reach what a real process rarely does. */
|
|
33
33
|
runProcess?: typeof runCommandProcess;
|
|
34
34
|
}): Promise<PreparingTool>;
|
|
35
|
+
/**
|
|
36
|
+
* Where a configured program runs, as a real path inside a `write` root. `program` names it in
|
|
37
|
+
* the error, as `Command "test"`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveCwd(program: string, cwd: string | undefined, policy: AccessPolicy, base: string): Promise<string>;
|
|
35
40
|
/** A small environment of its own: the inherited few, then what the consumer set. */
|
|
36
|
-
export declare function environmentFor(definition: CommandDefinition, env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
41
|
+
export declare function environmentFor(definition: Pick<CommandDefinition, 'env'>, env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
@@ -38,7 +38,10 @@ export async function createCommandTool({ commands, policy, writes, covered, bas
|
|
|
38
38
|
validateCommands(commands);
|
|
39
39
|
const resolved = new Map();
|
|
40
40
|
for (const [name, definition] of Object.entries(commands)) {
|
|
41
|
-
resolved.set(name, {
|
|
41
|
+
resolved.set(name, {
|
|
42
|
+
definition,
|
|
43
|
+
cwd: await resolveCwd(`Command "${name}"`, definition.cwd, policy, base),
|
|
44
|
+
});
|
|
42
45
|
}
|
|
43
46
|
return definePreparingTool({
|
|
44
47
|
name: 'runCommand',
|
|
@@ -195,16 +198,19 @@ function visibleCommand(definition, values) {
|
|
|
195
198
|
}
|
|
196
199
|
return [definition.executable, ...args].join(' ');
|
|
197
200
|
}
|
|
198
|
-
/**
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Where a configured program runs, as a real path inside a `write` root. `program` names it in
|
|
203
|
+
* the error, as `Command "test"`.
|
|
204
|
+
*/
|
|
205
|
+
export async function resolveCwd(program, cwd, policy, base) {
|
|
206
|
+
const named = resolve(base, cwd ?? '.');
|
|
201
207
|
let real;
|
|
202
208
|
try {
|
|
203
209
|
real = await realpath(named);
|
|
204
210
|
await policy.check(real, 'write', AbortSignal.timeout(cwdCheckMs));
|
|
205
211
|
}
|
|
206
212
|
catch (error) {
|
|
207
|
-
throw new InvalidAgentConfigError(
|
|
213
|
+
throw new InvalidAgentConfigError(`${program} needs a cwd inside a write root of the workspace`, { cause: error });
|
|
208
214
|
}
|
|
209
215
|
return real;
|
|
210
216
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commandTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/commandTool.ts"],"names":[],"mappings":"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","sourcesContent":["import { realpath } from 'node:fs/promises';\nimport { isAbsolute, relative, resolve } from 'node:path';\nimport { z } from 'zod';\nimport type { CommandLine } from '#src/agent/domain/approval';\nimport type { RunContext } from '#src/agent/domain/runContext';\nimport { displayPath } from '#src/recovery/domain/runChanges';\nimport { InvalidAgentConfigError } from '#src/shared/domain/errors';\nimport type { AccessPolicy } from '../domain/accessPolicy.ts';\nimport {\n\tbuildArguments,\n\ttype CommandDefinition,\n\ttype CommandParameter,\n\ttype Commands,\n\tdefaultCommandOutputBytes,\n\tdefaultCommandTimeoutMs,\n\ttype ParameterValue,\n\tparametersOf,\n\tvalidateCommands,\n} from '../domain/commands.ts';\nimport { EditRefusedError, EditUnconfirmedError } from '../domain/fileEdits.ts';\nimport { ToolExecutionError, type PreparingTool } from '../domain/preparedCall.ts';\nimport { CommandCheckpoint, type CoveredFiles } from './commandCheckpoint.ts';\nimport { type CommandProcessResult, runCommandProcess } from './commandProcess.ts';\nimport { definePreparingTool } from './defineTool.ts';\nimport type { WorkspaceWrites } from './workspaceWrites.ts';\n\n// Only what a toolchain needs to find itself and its locale. Everything else, the provider\n// keys included, stays in this process.\nconst inheritedVariables = ['PATH', 'HOME', 'TMPDIR', 'USER', 'SHELL', 'LANG'];\n// Long enough for a slow disk, short enough not to hold up building an agent.\nconst cwdCheckMs = 10_000;\n\n/** A command ready to run: its definition, and where it runs as a real path. */\ninterface ResolvedCommand {\n\tdefinition: CommandDefinition;\n\tcwd: string;\n}\n\n/**\n * `runCommand`, which runs one of the commands the consumer configured, by name, with the\n * parameters each one declares. Every call asks the approver, showing the program exactly as\n * it will run; `autoApprove` skips that.\n *\n * Around each command, the covered files are captured before and compared after, so what it\n * changed is in the run's history and can be undone: see `CommandCheckpoint`. If the capture\n * fails, the command does not run. A timeout or a cancellation stops its whole process group,\n * and is never reported as a completed command. If the group cannot be confirmed stopped, what\n * changed so far is recorded, the observation stays open, and writing stops for the run.\n *\n * This is not a sandbox: a command runs the repository's code, with everything this user can\n * reach. See \"Configured commands are trusted, not sandboxed\" in decisions.md.\n *\n * @throws {InvalidAgentConfigError} for an invalid configuration, a working directory outside\n * every `write` root, or on Windows, where a process group cannot be stopped.\n */\nexport async function createCommandTool({\n\tcommands,\n\tpolicy,\n\twrites,\n\tcovered,\n\tbase,\n\tplatform = process.platform,\n\trunProcess = runCommandProcess,\n}: {\n\tcommands: Commands;\n\tpolicy: AccessPolicy;\n\twrites: WorkspaceWrites;\n\tcovered: CoveredFiles;\n\t/** The real path of the first root, where a relative `cwd` starts. */\n\tbase: string;\n\tplatform?: NodeJS.Platform;\n\t/** Runs the program. Replaced in tests, to reach what a real process rarely does. */\n\trunProcess?: typeof runCommandProcess;\n}): Promise<PreparingTool> {\n\tif (platform === 'win32') {\n\t\tthrow new InvalidAgentConfigError(\n\t\t\t'Configured commands are not supported on Windows yet: the harness cannot stop a command and everything it started there',\n\t\t);\n\t}\n\tvalidateCommands(commands);\n\tconst resolved = new Map<string, ResolvedCommand>();\n\tfor (const [name, definition] of Object.entries(commands)) {\n\t\tresolved.set(name, { definition, cwd: await resolveCwd(name, definition, policy, base) });\n\t}\n\n\treturn definePreparingTool({\n\t\tname: 'runCommand',\n\t\tdescription: describeTool(commands),\n\t\tinput: inputSchema(commands),\n\t\tsummarize: ({ command: { name, ...values } }) => {\n\t\t\tconst definition = commands[name];\n\t\t\t// The schema only permits configured names, but keep this defensive for callers that\n\t\t\t// use a tool outside LLMAgent. This description never uses the resolved cwd.\n\t\t\treturn definition ? visibleCommand(definition, values) : name;\n\t\t},\n\t\tprepare: async ({ command: { name, ...values } }, signal, { run }) => {\n\t\t\tconst command = resolved.get(name);\n\t\t\t// The schema allows only the configured names.\n\t\t\tif (!command) throw new Error(`There is no command named \"${name}\"`);\n\t\t\tconst paramValues: Record<string, ParameterValue> = {};\n\t\t\tfor (const parameter of parametersOf(command.definition)) {\n\t\t\t\tconst value =\n\t\t\t\t\t(values as Record<string, ParameterValue | undefined>)[parameter.name] ?? null;\n\t\t\t\tparamValues[parameter.name] =\n\t\t\t\t\tparameter.type === 'paths' && Array.isArray(value)\n\t\t\t\t\t\t? await resolvePaths(value, command.cwd, policy, signal)\n\t\t\t\t\t\t: value;\n\t\t\t}\n\t\t\tconst line: CommandLine = Object.freeze({\n\t\t\t\texecutable: command.definition.executable,\n\t\t\t\targs: Object.freeze(buildArguments(command.definition, paramValues)) as string[],\n\t\t\t\tcwd: command.cwd,\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\t// A command can change anything its user can: it is always asked about.\n\t\t\t\trisk: 'destructive',\n\t\t\t\tcommand: line,\n\t\t\t\trun: runSignal =>\n\t\t\t\t\trunCommand({\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tline,\n\t\t\t\t\t\tdefinition: command.definition,\n\t\t\t\t\t\tcontext: run,\n\t\t\t\t\t\tsignal: runSignal,\n\t\t\t\t\t}),\n\t\t\t};\n\t\t},\n\t});\n\n\tasync function runCommand({\n\t\tname,\n\t\tline,\n\t\tdefinition,\n\t\tcontext,\n\t\tsignal,\n\t}: {\n\t\tname: string;\n\t\tline: CommandLine;\n\t\tdefinition: CommandDefinition;\n\t\tcontext: RunContext;\n\t\tsignal: AbortSignal;\n\t}): Promise<string> {\n\t\tsignal.throwIfAborted();\n\t\tconst session = writes.sessionFor(context);\n\t\tconst journal = await session.journalForChange();\n\t\tlet checkpoint: CommandCheckpoint;\n\t\ttry {\n\t\t\tcheckpoint = await CommandCheckpoint.begin({ journal, covered, by: name, signal });\n\t\t} catch (error) {\n\t\t\tsignal.throwIfAborted();\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`The harness could not capture the files before \"${name}\", so it did not run`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\n\t\tlet result: CommandProcessResult | undefined;\n\t\tlet startFailure: unknown;\n\t\ttry {\n\t\t\tresult = await runProcess(line.executable, line.args, {\n\t\t\t\tcwd: line.cwd,\n\t\t\t\tenv: environmentFor(definition),\n\t\t\t\ttimeoutMs: definition.timeoutMs ?? defaultCommandTimeoutMs,\n\t\t\t\toutputBytes: definition.outputBytes ?? defaultCommandOutputBytes,\n\t\t\t\tsignal,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tstartFailure = error;\n\t\t}\n\n\t\tconst complete = result?.stopped ?? true;\n\t\tlet observed: Awaited<ReturnType<CommandCheckpoint['end']>>;\n\t\ttry {\n\t\t\tobserved = await checkpoint.end({ complete });\n\t\t} catch (error) {\n\t\t\tsession.stop('Writing stopped: the harness could not record what a command changed');\n\t\t\tthrow new EditUnconfirmedError(\n\t\t\t\t`\"${name}\" ran, but the harness could not record what it changed; writing has stopped for this run`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\n\t\tif (!result) {\n\t\t\tsignal.throwIfAborted();\n\t\t\tthrow new EditRefusedError(`\"${name}\" could not be started`, { cause: startFailure });\n\t\t}\n\t\tconst report = describeRun(name, definition, result, observed);\n\t\tif (!complete) {\n\t\t\tsession.stop('Writing stopped: a command could not be stopped');\n\t\t\tthrow new EditUnconfirmedError(\n\t\t\t\t`${report}\\n\"${name}\" could not be stopped: something it started may still be changing files, and the history may not hold all of it. Writing has stopped for this run.`,\n\t\t\t);\n\t\t}\n\t\t// After its changes are recorded: a cancelled command is never reported as finished.\n\t\tif (result.outcome === 'cancelled') signal.throwIfAborted();\n\t\tif (result.outcome !== 'exited') throw new ToolExecutionError(report);\n\t\tif (result.exitCode !== 0) {\n\t\t\tthrow new ToolExecutionError(report, {\n\t\t\t\t...(result.exitCode === null ? {} : { exitCode: result.exitCode }),\n\t\t\t});\n\t\t}\n\t\treturn report;\n\t}\n\n\tfunction describeRun(\n\t\tname: string,\n\t\tdefinition: CommandDefinition,\n\t\tresult: CommandProcessResult,\n\t\t{ changes, unrecorded }: Awaited<ReturnType<CommandCheckpoint['end']>>,\n\t): string {\n\t\tconst seconds = (result.durationMs / 1000).toFixed(1);\n\t\tconst lines: string[] = [];\n\t\tif (result.outcome === 'timedOut') {\n\t\t\tconst limit = (definition.timeoutMs ?? defaultCommandTimeoutMs) / 1000;\n\t\t\tlines.push(`\"${name}\" ran past its limit of ${String(limit)} s and was stopped.`);\n\t\t} else if (result.outcome === 'cancelled') {\n\t\t\tlines.push(`\"${name}\" was stopped: the run was cancelled.`);\n\t\t} else if (result.exitCode !== null) {\n\t\t\tlines.push(`\"${name}\" exited with code ${String(result.exitCode)} after ${seconds} s.`);\n\t\t} else {\n\t\t\tlines.push(`\"${name}\" was killed by ${result.signal ?? 'a signal'} after ${seconds} s.`);\n\t\t}\n\n\t\tconst shown = (path: string) => displayPath(base, path);\n\t\tlines.push(\n\t\t\tchanges.length === 0\n\t\t\t\t? 'It changed no file the history covers.'\n\t\t\t\t: `Files it changed, recorded in the history: ${changes.map(change => shown(change.path)).join(', ')}.`,\n\t\t);\n\t\tif (unrecorded.length > 0) {\n\t\t\tlines.push(\n\t\t\t\t`Not recorded, because the history cannot copy them (a folder, a link, or a file over 64 MB): ${unrecorded.map(shown).join(', ')}.`,\n\t\t\t);\n\t\t}\n\n\t\tconst limit = definition.outputBytes ?? defaultCommandOutputBytes;\n\t\tif (result.printedBytes === 0) {\n\t\t\tlines.push('It printed nothing.');\n\t\t} else {\n\t\t\tlines.push(\n\t\t\t\tresult.printedBytes > limit\n\t\t\t\t\t? `Output, the last ${kilobytes(limit)} of ${kilobytes(result.printedBytes)}:`\n\t\t\t\t\t: 'Output:',\n\t\t\t\tresult.output,\n\t\t\t);\n\t\t}\n\t\treturn lines.join('\\n');\n\t}\n}\n\nfunction kilobytes(bytes: number): string {\n\treturn `${String(Math.ceil(bytes / 1024))} KB`;\n}\n\n/** The command line the model can see, before paths are resolved against the host workspace. */\nfunction visibleCommand(\n\tdefinition: CommandDefinition,\n\tvalues: Record<string, ParameterValue | undefined>,\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\n\t\tconst value = values[argument.name] ?? null;\n\t\tif (argument.type === 'paths') {\n\t\t\tif (Array.isArray(value)) args.push(...value);\n\t\t} else if (value !== null && typeof value === 'string') {\n\t\t\targs.push(value);\n\t\t}\n\t}\n\n\treturn [definition.executable, ...args].join(' ');\n}\n\n/** Its working directory, as a real path inside a `write` root. */\nasync function resolveCwd(\n\tname: string,\n\tdefinition: CommandDefinition,\n\tpolicy: AccessPolicy,\n\tbase: string,\n): Promise<string> {\n\tconst named = resolve(base, definition.cwd ?? '.');\n\tlet real: string;\n\ttry {\n\t\treal = await realpath(named);\n\t\tawait policy.check(real, 'write', AbortSignal.timeout(cwdCheckMs));\n\t} catch (error) {\n\t\tthrow new InvalidAgentConfigError(\n\t\t\t`Command \"${name}\" needs a cwd inside a write root of the workspace`,\n\t\t\t{ cause: error },\n\t\t);\n\t}\n\treturn real;\n}\n\n/**\n * Each path, once the policy lets the agent read it, relative to where the command runs. One\n * that would read as a flag is given as `./` and its name.\n */\nasync function resolvePaths(\n\tpaths: string[],\n\tcwd: string,\n\tpolicy: AccessPolicy,\n\tsignal: AbortSignal,\n): Promise<string[]> {\n\tconst resolved: string[] = [];\n\tfor (const path of paths) {\n\t\tconst { absolute } = await policy.check(path, 'read', signal);\n\t\tconst fromCwd = relative(cwd, absolute) || '.';\n\t\tresolved.push(fromCwd.startsWith('-') && !isAbsolute(fromCwd) ? `./${fromCwd}` : fromCwd);\n\t}\n\treturn resolved;\n}\n\n/** A small environment of its own: the inherited few, then what the consumer set. */\nexport function environmentFor(\n\tdefinition: CommandDefinition,\n\tenv: NodeJS.ProcessEnv = process.env,\n): NodeJS.ProcessEnv {\n\tconst inherited: NodeJS.ProcessEnv = {};\n\tfor (const [variable, value] of Object.entries(env)) {\n\t\tif (value === undefined) continue;\n\t\tif (inheritedVariables.includes(variable) || variable.startsWith('LC_')) {\n\t\t\tinherited[variable] = value;\n\t\t}\n\t}\n\treturn { ...inherited, ...definition.env };\n}\n\nfunction describeTool(commands: Commands): string {\n\tconst list = Object.entries(commands)\n\t\t.map(([name, { description }]) => `- ${name}: ${description}`)\n\t\t.join('\\n');\n\treturn [\n\t\t\"Runs one of the project's configured commands, by name, such as its tests or its formatter.\",\n\t\t'Files it changes are recorded in the history, so they can be undone. Its output is cut to its end.',\n\t\t'Commands:',\n\t\tlist,\n\t].join('\\n');\n}\n\n/**\n * One object per command, told apart by `name`, each with the parameters that command takes.\n * Strict mode needs every key present, so a parameter that may be left out takes `null`.\n */\nfunction inputSchema(commands: Commands) {\n\tconst variants = Object.entries(commands).map(([name, definition]) =>\n\t\tz.object({\n\t\t\tname: z.enum([name]),\n\t\t\t...Object.fromEntries(\n\t\t\t\tparametersOf(definition).map(parameter => [parameter.name, parameterSchema(parameter)]),\n\t\t\t),\n\t\t}),\n\t);\n\tconst [first, ...rest] = variants;\n\t// `validateCommands` refuses an empty configuration.\n\tif (!first) throw new InvalidAgentConfigError('No command is configured');\n\treturn z.object({\n\t\tcommand: (rest.length === 0 ? first : z.union([first, ...rest])).describe(\n\t\t\t'The command to run, and its parameters.',\n\t\t),\n\t});\n}\n\nfunction parameterSchema(parameter: CommandParameter): z.ZodType {\n\tswitch (parameter.type) {\n\t\tcase 'paths':\n\t\t\treturn z\n\t\t\t\t.array(z.string())\n\t\t\t\t.describe(`${parameter.description} Paths inside the workspace; [] for none.`);\n\t\tcase 'choice': {\n\t\t\tconst values = z.enum(parameter.values as [string, ...string[]]);\n\t\t\treturn parameter.required === true\n\t\t\t\t? values.describe(parameter.description)\n\t\t\t\t: values.nullable().describe(`${parameter.description} null to leave it out.`);\n\t\t}\n\t\tcase 'text': {\n\t\t\tconst text = z.string();\n\t\t\tconst description = `${parameter.description} Must match the regular expression ${parameter.pattern}, and not start with \"-\".`;\n\t\t\treturn parameter.required === true\n\t\t\t\t? text.describe(description)\n\t\t\t\t: text.nullable().describe(`${description} null to leave it out.`);\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"commandTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/commandTool.ts"],"names":[],"mappings":"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","sourcesContent":["import { realpath } from 'node:fs/promises';\nimport { isAbsolute, relative, resolve } from 'node:path';\nimport { z } from 'zod';\nimport type { CommandLine } from '#src/agent/domain/approval';\nimport type { RunContext } from '#src/agent/domain/runContext';\nimport { displayPath } from '#src/recovery/domain/runChanges';\nimport { InvalidAgentConfigError } from '#src/shared/domain/errors';\nimport type { AccessPolicy } from '../domain/accessPolicy.ts';\nimport {\n\tbuildArguments,\n\ttype CommandDefinition,\n\ttype CommandParameter,\n\ttype Commands,\n\tdefaultCommandOutputBytes,\n\tdefaultCommandTimeoutMs,\n\ttype ParameterValue,\n\tparametersOf,\n\tvalidateCommands,\n} from '../domain/commands.ts';\nimport { EditRefusedError, EditUnconfirmedError } from '../domain/fileEdits.ts';\nimport { ToolExecutionError, type PreparingTool } from '../domain/preparedCall.ts';\nimport { CommandCheckpoint, type CoveredFiles } from './commandCheckpoint.ts';\nimport { type CommandProcessResult, runCommandProcess } from './commandProcess.ts';\nimport { definePreparingTool } from './defineTool.ts';\nimport type { WorkspaceWrites } from './workspaceWrites.ts';\n\n// Only what a toolchain needs to find itself and its locale. Everything else, the provider\n// keys included, stays in this process.\nconst inheritedVariables = ['PATH', 'HOME', 'TMPDIR', 'USER', 'SHELL', 'LANG'];\n// Long enough for a slow disk, short enough not to hold up building an agent.\nconst cwdCheckMs = 10_000;\n\n/** A command ready to run: its definition, and where it runs as a real path. */\ninterface ResolvedCommand {\n\tdefinition: CommandDefinition;\n\tcwd: string;\n}\n\n/**\n * `runCommand`, which runs one of the commands the consumer configured, by name, with the\n * parameters each one declares. Every call asks the approver, showing the program exactly as\n * it will run; `autoApprove` skips that.\n *\n * Around each command, the covered files are captured before and compared after, so what it\n * changed is in the run's history and can be undone: see `CommandCheckpoint`. If the capture\n * fails, the command does not run. A timeout or a cancellation stops its whole process group,\n * and is never reported as a completed command. If the group cannot be confirmed stopped, what\n * changed so far is recorded, the observation stays open, and writing stops for the run.\n *\n * This is not a sandbox: a command runs the repository's code, with everything this user can\n * reach. See \"Configured commands are trusted, not sandboxed\" in decisions.md.\n *\n * @throws {InvalidAgentConfigError} for an invalid configuration, a working directory outside\n * every `write` root, or on Windows, where a process group cannot be stopped.\n */\nexport async function createCommandTool({\n\tcommands,\n\tpolicy,\n\twrites,\n\tcovered,\n\tbase,\n\tplatform = process.platform,\n\trunProcess = runCommandProcess,\n}: {\n\tcommands: Commands;\n\tpolicy: AccessPolicy;\n\twrites: WorkspaceWrites;\n\tcovered: CoveredFiles;\n\t/** The real path of the first root, where a relative `cwd` starts. */\n\tbase: string;\n\tplatform?: NodeJS.Platform;\n\t/** Runs the program. Replaced in tests, to reach what a real process rarely does. */\n\trunProcess?: typeof runCommandProcess;\n}): Promise<PreparingTool> {\n\tif (platform === 'win32') {\n\t\tthrow new InvalidAgentConfigError(\n\t\t\t'Configured commands are not supported on Windows yet: the harness cannot stop a command and everything it started there',\n\t\t);\n\t}\n\tvalidateCommands(commands);\n\tconst resolved = new Map<string, ResolvedCommand>();\n\tfor (const [name, definition] of Object.entries(commands)) {\n\t\tresolved.set(name, {\n\t\t\tdefinition,\n\t\t\tcwd: await resolveCwd(`Command \"${name}\"`, definition.cwd, policy, base),\n\t\t});\n\t}\n\n\treturn definePreparingTool({\n\t\tname: 'runCommand',\n\t\tdescription: describeTool(commands),\n\t\tinput: inputSchema(commands),\n\t\tsummarize: ({ command: { name, ...values } }) => {\n\t\t\tconst definition = commands[name];\n\t\t\t// The schema only permits configured names, but keep this defensive for callers that\n\t\t\t// use a tool outside LLMAgent. This description never uses the resolved cwd.\n\t\t\treturn definition ? visibleCommand(definition, values) : name;\n\t\t},\n\t\tprepare: async ({ command: { name, ...values } }, signal, { run }) => {\n\t\t\tconst command = resolved.get(name);\n\t\t\t// The schema allows only the configured names.\n\t\t\tif (!command) throw new Error(`There is no command named \"${name}\"`);\n\t\t\tconst paramValues: Record<string, ParameterValue> = {};\n\t\t\tfor (const parameter of parametersOf(command.definition)) {\n\t\t\t\tconst value =\n\t\t\t\t\t(values as Record<string, ParameterValue | undefined>)[parameter.name] ?? null;\n\t\t\t\tparamValues[parameter.name] =\n\t\t\t\t\tparameter.type === 'paths' && Array.isArray(value)\n\t\t\t\t\t\t? await resolvePaths(value, command.cwd, policy, signal)\n\t\t\t\t\t\t: value;\n\t\t\t}\n\t\t\tconst line: CommandLine = Object.freeze({\n\t\t\t\texecutable: command.definition.executable,\n\t\t\t\targs: Object.freeze(buildArguments(command.definition, paramValues)) as string[],\n\t\t\t\tcwd: command.cwd,\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\t// A command can change anything its user can: it is always asked about.\n\t\t\t\trisk: 'destructive',\n\t\t\t\tcommand: line,\n\t\t\t\trun: runSignal =>\n\t\t\t\t\trunCommand({\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tline,\n\t\t\t\t\t\tdefinition: command.definition,\n\t\t\t\t\t\tcontext: run,\n\t\t\t\t\t\tsignal: runSignal,\n\t\t\t\t\t}),\n\t\t\t};\n\t\t},\n\t});\n\n\tasync function runCommand({\n\t\tname,\n\t\tline,\n\t\tdefinition,\n\t\tcontext,\n\t\tsignal,\n\t}: {\n\t\tname: string;\n\t\tline: CommandLine;\n\t\tdefinition: CommandDefinition;\n\t\tcontext: RunContext;\n\t\tsignal: AbortSignal;\n\t}): Promise<string> {\n\t\tsignal.throwIfAborted();\n\t\tconst session = writes.sessionFor(context);\n\t\tconst journal = await session.journalForChange();\n\t\tlet checkpoint: CommandCheckpoint;\n\t\ttry {\n\t\t\tcheckpoint = await CommandCheckpoint.begin({ journal, covered, by: name, signal });\n\t\t} catch (error) {\n\t\t\tsignal.throwIfAborted();\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`The harness could not capture the files before \"${name}\", so it did not run`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\n\t\tlet result: CommandProcessResult | undefined;\n\t\tlet startFailure: unknown;\n\t\ttry {\n\t\t\tresult = await runProcess(line.executable, line.args, {\n\t\t\t\tcwd: line.cwd,\n\t\t\t\tenv: environmentFor(definition),\n\t\t\t\ttimeoutMs: definition.timeoutMs ?? defaultCommandTimeoutMs,\n\t\t\t\toutputBytes: definition.outputBytes ?? defaultCommandOutputBytes,\n\t\t\t\tsignal,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tstartFailure = error;\n\t\t}\n\n\t\tconst complete = result?.stopped ?? true;\n\t\tlet observed: Awaited<ReturnType<CommandCheckpoint['end']>>;\n\t\ttry {\n\t\t\tobserved = await checkpoint.end({ complete });\n\t\t} catch (error) {\n\t\t\tsession.stop('Writing stopped: the harness could not record what a command changed');\n\t\t\tthrow new EditUnconfirmedError(\n\t\t\t\t`\"${name}\" ran, but the harness could not record what it changed; writing has stopped for this run`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\n\t\tif (!result) {\n\t\t\tsignal.throwIfAborted();\n\t\t\tthrow new EditRefusedError(`\"${name}\" could not be started`, { cause: startFailure });\n\t\t}\n\t\tconst report = describeRun(name, definition, result, observed);\n\t\tif (!complete) {\n\t\t\tsession.stop('Writing stopped: a command could not be stopped');\n\t\t\tthrow new EditUnconfirmedError(\n\t\t\t\t`${report}\\n\"${name}\" could not be stopped: something it started may still be changing files, and the history may not hold all of it. Writing has stopped for this run.`,\n\t\t\t);\n\t\t}\n\t\t// After its changes are recorded: a cancelled command is never reported as finished.\n\t\tif (result.outcome === 'cancelled') signal.throwIfAborted();\n\t\tif (result.outcome !== 'exited') throw new ToolExecutionError(report);\n\t\tif (result.exitCode !== 0) {\n\t\t\tthrow new ToolExecutionError(report, {\n\t\t\t\t...(result.exitCode === null ? {} : { exitCode: result.exitCode }),\n\t\t\t});\n\t\t}\n\t\treturn report;\n\t}\n\n\tfunction describeRun(\n\t\tname: string,\n\t\tdefinition: CommandDefinition,\n\t\tresult: CommandProcessResult,\n\t\t{ changes, unrecorded }: Awaited<ReturnType<CommandCheckpoint['end']>>,\n\t): string {\n\t\tconst seconds = (result.durationMs / 1000).toFixed(1);\n\t\tconst lines: string[] = [];\n\t\tif (result.outcome === 'timedOut') {\n\t\t\tconst limit = (definition.timeoutMs ?? defaultCommandTimeoutMs) / 1000;\n\t\t\tlines.push(`\"${name}\" ran past its limit of ${String(limit)} s and was stopped.`);\n\t\t} else if (result.outcome === 'cancelled') {\n\t\t\tlines.push(`\"${name}\" was stopped: the run was cancelled.`);\n\t\t} else if (result.exitCode !== null) {\n\t\t\tlines.push(`\"${name}\" exited with code ${String(result.exitCode)} after ${seconds} s.`);\n\t\t} else {\n\t\t\tlines.push(`\"${name}\" was killed by ${result.signal ?? 'a signal'} after ${seconds} s.`);\n\t\t}\n\n\t\tconst shown = (path: string) => displayPath(base, path);\n\t\tlines.push(\n\t\t\tchanges.length === 0\n\t\t\t\t? 'It changed no file the history covers.'\n\t\t\t\t: `Files it changed, recorded in the history: ${changes.map(change => shown(change.path)).join(', ')}.`,\n\t\t);\n\t\tif (unrecorded.length > 0) {\n\t\t\tlines.push(\n\t\t\t\t`Not recorded, because the history cannot copy them (a folder, a link, or a file over 64 MB): ${unrecorded.map(shown).join(', ')}.`,\n\t\t\t);\n\t\t}\n\n\t\tconst limit = definition.outputBytes ?? defaultCommandOutputBytes;\n\t\tif (result.printedBytes === 0) {\n\t\t\tlines.push('It printed nothing.');\n\t\t} else {\n\t\t\tlines.push(\n\t\t\t\tresult.printedBytes > limit\n\t\t\t\t\t? `Output, the last ${kilobytes(limit)} of ${kilobytes(result.printedBytes)}:`\n\t\t\t\t\t: 'Output:',\n\t\t\t\tresult.output,\n\t\t\t);\n\t\t}\n\t\treturn lines.join('\\n');\n\t}\n}\n\nfunction kilobytes(bytes: number): string {\n\treturn `${String(Math.ceil(bytes / 1024))} KB`;\n}\n\n/** The command line the model can see, before paths are resolved against the host workspace. */\nfunction visibleCommand(\n\tdefinition: CommandDefinition,\n\tvalues: Record<string, ParameterValue | undefined>,\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\n\t\tconst value = values[argument.name] ?? null;\n\t\tif (argument.type === 'paths') {\n\t\t\tif (Array.isArray(value)) args.push(...value);\n\t\t} else if (value !== null && typeof value === 'string') {\n\t\t\targs.push(value);\n\t\t}\n\t}\n\n\treturn [definition.executable, ...args].join(' ');\n}\n\n/**\n * Where a configured program runs, as a real path inside a `write` root. `program` names it in\n * the error, as `Command \"test\"`.\n */\nexport async function resolveCwd(\n\tprogram: string,\n\tcwd: string | undefined,\n\tpolicy: AccessPolicy,\n\tbase: string,\n): Promise<string> {\n\tconst named = resolve(base, cwd ?? '.');\n\tlet real: string;\n\ttry {\n\t\treal = await realpath(named);\n\t\tawait policy.check(real, 'write', AbortSignal.timeout(cwdCheckMs));\n\t} catch (error) {\n\t\tthrow new InvalidAgentConfigError(\n\t\t\t`${program} needs a cwd inside a write root of the workspace`,\n\t\t\t{ cause: error },\n\t\t);\n\t}\n\treturn real;\n}\n\n/**\n * Each path, once the policy lets the agent read it, relative to where the command runs. One\n * that would read as a flag is given as `./` and its name.\n */\nasync function resolvePaths(\n\tpaths: string[],\n\tcwd: string,\n\tpolicy: AccessPolicy,\n\tsignal: AbortSignal,\n): Promise<string[]> {\n\tconst resolved: string[] = [];\n\tfor (const path of paths) {\n\t\tconst { absolute } = await policy.check(path, 'read', signal);\n\t\tconst fromCwd = relative(cwd, absolute) || '.';\n\t\tresolved.push(fromCwd.startsWith('-') && !isAbsolute(fromCwd) ? `./${fromCwd}` : fromCwd);\n\t}\n\treturn resolved;\n}\n\n/** A small environment of its own: the inherited few, then what the consumer set. */\nexport function environmentFor(\n\tdefinition: Pick<CommandDefinition, 'env'>,\n\tenv: NodeJS.ProcessEnv = process.env,\n): NodeJS.ProcessEnv {\n\tconst inherited: NodeJS.ProcessEnv = {};\n\tfor (const [variable, value] of Object.entries(env)) {\n\t\tif (value === undefined) continue;\n\t\tif (inheritedVariables.includes(variable) || variable.startsWith('LC_')) {\n\t\t\tinherited[variable] = value;\n\t\t}\n\t}\n\treturn { ...inherited, ...definition.env };\n}\n\nfunction describeTool(commands: Commands): string {\n\tconst list = Object.entries(commands)\n\t\t.map(([name, { description }]) => `- ${name}: ${description}`)\n\t\t.join('\\n');\n\treturn [\n\t\t\"Runs one of the project's configured commands, by name, such as its tests or its formatter.\",\n\t\t'Files it changes are recorded in the history, so they can be undone. Its output is cut to its end.',\n\t\t'Commands:',\n\t\tlist,\n\t].join('\\n');\n}\n\n/**\n * One object per command, told apart by `name`, each with the parameters that command takes.\n * Strict mode needs every key present, so a parameter that may be left out takes `null`.\n */\nfunction inputSchema(commands: Commands) {\n\tconst variants = Object.entries(commands).map(([name, definition]) =>\n\t\tz.object({\n\t\t\tname: z.enum([name]),\n\t\t\t...Object.fromEntries(\n\t\t\t\tparametersOf(definition).map(parameter => [parameter.name, parameterSchema(parameter)]),\n\t\t\t),\n\t\t}),\n\t);\n\tconst [first, ...rest] = variants;\n\t// `validateCommands` refuses an empty configuration.\n\tif (!first) throw new InvalidAgentConfigError('No command is configured');\n\treturn z.object({\n\t\tcommand: (rest.length === 0 ? first : z.union([first, ...rest])).describe(\n\t\t\t'The command to run, and its parameters.',\n\t\t),\n\t});\n}\n\nfunction parameterSchema(parameter: CommandParameter): z.ZodType {\n\tswitch (parameter.type) {\n\t\tcase 'paths':\n\t\t\treturn z\n\t\t\t\t.array(z.string())\n\t\t\t\t.describe(`${parameter.description} Paths inside the workspace; [] for none.`);\n\t\tcase 'choice': {\n\t\t\tconst values = z.enum(parameter.values as [string, ...string[]]);\n\t\t\treturn parameter.required === true\n\t\t\t\t? values.describe(parameter.description)\n\t\t\t\t: values.nullable().describe(`${parameter.description} null to leave it out.`);\n\t\t}\n\t\tcase 'text': {\n\t\t\tconst text = z.string();\n\t\t\tconst description = `${parameter.description} Must match the regular expression ${parameter.pattern}, and not start with \"-\".`;\n\t\t\treturn parameter.required === true\n\t\t\t\t? text.describe(description)\n\t\t\t\t: text.nullable().describe(`${description} null to leave it out.`);\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Agent } from '#src/agent/domain/agent';
|
|
2
|
+
import type { PreparingTool } from '../domain/preparedCall.ts';
|
|
3
|
+
/** The most explorers one call sends, so a single call cannot fan out without bound. */
|
|
4
|
+
export declare const maxExplorers = 4;
|
|
5
|
+
/**
|
|
6
|
+
* `explore` for the planner and the reviewer: it sends cheaper, read-only explorers, created by
|
|
7
|
+
* `explorer`, to find what an agent needs in the repository, so the agent that called it reads
|
|
8
|
+
* their reports instead of filling its own context with the files.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createExploreTool(explorer: () => Agent): PreparingTool;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { withRunContext } from '#src/agent/domain/runContext';
|
|
3
|
+
import { RecoverableError, UnrecoverableError } from '#src/shared/domain/errors';
|
|
4
|
+
import { definePreparingTool } from "./defineTool.js";
|
|
5
|
+
/** The most explorers one call sends, so a single call cannot fan out without bound. */
|
|
6
|
+
export const maxExplorers = 4;
|
|
7
|
+
// An answer is read whole, like a read, which returns at most 16 KB.
|
|
8
|
+
const maxAnswerCharacters = 16_000;
|
|
9
|
+
function cut(answer) {
|
|
10
|
+
if (answer.length <= maxAnswerCharacters)
|
|
11
|
+
return answer;
|
|
12
|
+
return `${answer.slice(0, maxAnswerCharacters)}\n[The answer was cut at ${String(maxAnswerCharacters)} characters.]`;
|
|
13
|
+
}
|
|
14
|
+
function failureOf(error) {
|
|
15
|
+
return typeof error.cause === 'string' ? `${error.message}: ${error.cause}` : error.message;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Runs one explorer per question, all at once, each a new agent that starts from its question
|
|
19
|
+
* alone. Every explorer's usage counts in the run's, whether it answered or failed; a failure
|
|
20
|
+
* becomes that explorer's answer, so the others' are not lost. A cancellation escapes.
|
|
21
|
+
*/
|
|
22
|
+
async function explore(questions, explorer, signal, call) {
|
|
23
|
+
const answers = await Promise.allSettled(questions.map(async (question, index) => {
|
|
24
|
+
const subagent = index + 1;
|
|
25
|
+
const heading = `Explorer ${String(subagent)}: ${question}`;
|
|
26
|
+
const onProgress = (event) => {
|
|
27
|
+
call.onProgress({
|
|
28
|
+
type: 'subagent',
|
|
29
|
+
callId: call.callId,
|
|
30
|
+
subagent,
|
|
31
|
+
task: question,
|
|
32
|
+
event,
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
// Within the run that made the call, so the explorers' reads belong to it.
|
|
36
|
+
const options = withRunContext({ signal, onProgress }, call.run);
|
|
37
|
+
try {
|
|
38
|
+
const { response, tokens } = await explorer().run(question, options);
|
|
39
|
+
call.spend(tokens, tokens === undefined);
|
|
40
|
+
return `${heading}\n${cut(response || 'It found nothing to report.')}`;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (!(error instanceof RecoverableError || error instanceof UnrecoverableError)) {
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
call.spend(error.tokens, error.usageUnreported);
|
|
47
|
+
return `${heading}\nThe explorer failed: ${failureOf(error)}`;
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
50
|
+
// Whatever each explorer threw once the run was cancelled, the call ends as cancelled.
|
|
51
|
+
signal.throwIfAborted();
|
|
52
|
+
return answers
|
|
53
|
+
.map(answer => {
|
|
54
|
+
if (answer.status === 'rejected')
|
|
55
|
+
throw answer.reason;
|
|
56
|
+
return answer.value;
|
|
57
|
+
})
|
|
58
|
+
.join('\n\n');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* `explore` for the planner and the reviewer: it sends cheaper, read-only explorers, created by
|
|
62
|
+
* `explorer`, to find what an agent needs in the repository, so the agent that called it reads
|
|
63
|
+
* their reports instead of filling its own context with the files.
|
|
64
|
+
*/
|
|
65
|
+
export function createExploreTool(explorer) {
|
|
66
|
+
return definePreparingTool({
|
|
67
|
+
name: 'explore',
|
|
68
|
+
description: `Sends read-only explorers to find things in the repository for you, one for each question, all at once. Each explorer starts from nothing but its question, so make each one complete: what to find, and what to report about it, such as which files and lines implement something and how. Ask up to ${String(maxExplorers)} independent questions in one call. Each answer is an explorer's report, not the files: read a file yourself when you need its exact text.`,
|
|
69
|
+
input: z.object({
|
|
70
|
+
questions: z.array(z.string()).describe('One complete, independent question per explorer.'),
|
|
71
|
+
}),
|
|
72
|
+
summarize: ({ questions }) => questions.length === 1 ? (questions[0] ?? '') : `${String(questions.length)} questions`,
|
|
73
|
+
prepare: ({ questions }, _, call) => {
|
|
74
|
+
const asked = questions.map(question => question.trim());
|
|
75
|
+
if (asked.length === 0 || asked.length > maxExplorers) {
|
|
76
|
+
return Promise.reject(new Error(`Ask between 1 and ${String(maxExplorers)} questions in one call.`));
|
|
77
|
+
}
|
|
78
|
+
if (asked.some(question => question === '')) {
|
|
79
|
+
return Promise.reject(new Error('Every question needs some text.'));
|
|
80
|
+
}
|
|
81
|
+
return Promise.resolve({
|
|
82
|
+
risk: 'safe',
|
|
83
|
+
run: signal => explore(asked, explorer, signal, call),
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=exploreTool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exploreTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/exploreTool.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod';\nimport type { Agent, ProgressEvent } from '#src/agent/domain/agent';\nimport { withRunContext } from '#src/agent/domain/runContext';\nimport { RecoverableError, UnrecoverableError } from '#src/shared/domain/errors';\nimport type { PreparingTool } from '../domain/preparedCall.ts';\nimport type { ToolCallContext } from '../domain/readRegistry.ts';\nimport { definePreparingTool } from './defineTool.ts';\n\n/** The most explorers one call sends, so a single call cannot fan out without bound. */\nexport const maxExplorers = 4;\n\n// An answer is read whole, like a read, which returns at most 16 KB.\nconst maxAnswerCharacters = 16_000;\n\nfunction cut(answer: string): string {\n\tif (answer.length <= maxAnswerCharacters) return answer;\n\treturn `${answer.slice(0, maxAnswerCharacters)}\\n[The answer was cut at ${String(maxAnswerCharacters)} characters.]`;\n}\n\nfunction failureOf(error: RecoverableError | UnrecoverableError): string {\n\treturn typeof error.cause === 'string' ? `${error.message}: ${error.cause}` : error.message;\n}\n\n/**\n * Runs one explorer per question, all at once, each a new agent that starts from its question\n * alone. Every explorer's usage counts in the run's, whether it answered or failed; a failure\n * becomes that explorer's answer, so the others' are not lost. A cancellation escapes.\n */\nasync function explore(\n\tquestions: readonly string[],\n\texplorer: () => Agent,\n\tsignal: AbortSignal,\n\tcall: ToolCallContext,\n): Promise<string> {\n\tconst answers = await Promise.allSettled(\n\t\tquestions.map(async (question, index) => {\n\t\t\tconst subagent = index + 1;\n\t\t\tconst heading = `Explorer ${String(subagent)}: ${question}`;\n\t\t\tconst onProgress = (event: ProgressEvent) => {\n\t\t\t\tcall.onProgress({\n\t\t\t\t\ttype: 'subagent',\n\t\t\t\t\tcallId: call.callId,\n\t\t\t\t\tsubagent,\n\t\t\t\t\ttask: question,\n\t\t\t\t\tevent,\n\t\t\t\t});\n\t\t\t};\n\t\t\t// Within the run that made the call, so the explorers' reads belong to it.\n\t\t\tconst options = withRunContext({ signal, onProgress }, call.run);\n\t\t\ttry {\n\t\t\t\tconst { response, tokens } = await explorer().run(question, options);\n\t\t\t\tcall.spend(tokens, tokens === undefined);\n\t\t\t\treturn `${heading}\\n${cut(response || 'It found nothing to report.')}`;\n\t\t\t} catch (error) {\n\t\t\t\tif (!(error instanceof RecoverableError || error instanceof UnrecoverableError)) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tcall.spend(error.tokens, error.usageUnreported);\n\t\t\t\treturn `${heading}\\nThe explorer failed: ${failureOf(error)}`;\n\t\t\t}\n\t\t}),\n\t);\n\t// Whatever each explorer threw once the run was cancelled, the call ends as cancelled.\n\tsignal.throwIfAborted();\n\treturn answers\n\t\t.map(answer => {\n\t\t\tif (answer.status === 'rejected') throw answer.reason;\n\t\t\treturn answer.value;\n\t\t})\n\t\t.join('\\n\\n');\n}\n\n/**\n * `explore` for the planner and the reviewer: it sends cheaper, read-only explorers, created by\n * `explorer`, to find what an agent needs in the repository, so the agent that called it reads\n * their reports instead of filling its own context with the files.\n */\nexport function createExploreTool(explorer: () => Agent): PreparingTool {\n\treturn definePreparingTool({\n\t\tname: 'explore',\n\t\tdescription: `Sends read-only explorers to find things in the repository for you, one for each question, all at once. Each explorer starts from nothing but its question, so make each one complete: what to find, and what to report about it, such as which files and lines implement something and how. Ask up to ${String(maxExplorers)} independent questions in one call. Each answer is an explorer's report, not the files: read a file yourself when you need its exact text.`,\n\t\tinput: z.object({\n\t\t\tquestions: z.array(z.string()).describe('One complete, independent question per explorer.'),\n\t\t}),\n\t\tsummarize: ({ questions }) =>\n\t\t\tquestions.length === 1 ? (questions[0] ?? '') : `${String(questions.length)} questions`,\n\t\tprepare: ({ questions }, _, call) => {\n\t\t\tconst asked = questions.map(question => question.trim());\n\t\t\tif (asked.length === 0 || asked.length > maxExplorers) {\n\t\t\t\treturn Promise.reject(\n\t\t\t\t\tnew Error(`Ask between 1 and ${String(maxExplorers)} questions in one call.`),\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (asked.some(question => question === '')) {\n\t\t\t\treturn Promise.reject(new Error('Every question needs some text.'));\n\t\t\t}\n\t\t\treturn Promise.resolve({\n\t\t\t\trisk: 'safe',\n\t\t\t\trun: signal => explore(asked, explorer, signal, call),\n\t\t\t});\n\t\t},\n\t});\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AccessPolicy } from '../domain/accessPolicy.ts';
|
|
2
|
+
import { type Formatter } from '../domain/commands.ts';
|
|
3
|
+
import { runCommandProcess } from './commandProcess.ts';
|
|
4
|
+
import type { WriteSession } from './writeSession.ts';
|
|
5
|
+
/**
|
|
6
|
+
* Runs the consumer's formatter on a file an edit has just written. Only that file is captured
|
|
7
|
+
* around it, so what it changed there is in the run's history and can be undone; a formatter
|
|
8
|
+
* that changes other files leaves those changes out of the history.
|
|
9
|
+
*/
|
|
10
|
+
export declare class FileFormatter {
|
|
11
|
+
private readonly formatter;
|
|
12
|
+
private readonly cwd;
|
|
13
|
+
private readonly runProcess;
|
|
14
|
+
private constructor();
|
|
15
|
+
/**
|
|
16
|
+
* @throws {InvalidAgentConfigError} for an invalid formatter, a working directory outside every
|
|
17
|
+
* `write` root, or on Windows, where a process group cannot be stopped.
|
|
18
|
+
*/
|
|
19
|
+
static create({ formatter, policy, base, platform, runProcess, }: {
|
|
20
|
+
formatter: Formatter;
|
|
21
|
+
policy: AccessPolicy;
|
|
22
|
+
/** The real path of the first root, where a relative `cwd` starts. */
|
|
23
|
+
base: string;
|
|
24
|
+
platform?: NodeJS.Platform;
|
|
25
|
+
/** Runs the program. Replaced in tests, to reach what a real process rarely does. */
|
|
26
|
+
runProcess?: typeof runCommandProcess;
|
|
27
|
+
}): Promise<FileFormatter>;
|
|
28
|
+
/**
|
|
29
|
+
* Formats the file at `absolute` and records in `session` what it changed.
|
|
30
|
+
*
|
|
31
|
+
* @returns what the model needs to know when the file may not be formatted, or `undefined`
|
|
32
|
+
* once the formatter exited with 0.
|
|
33
|
+
* @throws {EditUnconfirmedError} when what it changed could not be recorded, or it could not be
|
|
34
|
+
* stopped. Writing stops for the run then, as after such a command.
|
|
35
|
+
*/
|
|
36
|
+
format(absolute: string, session: WriteSession, signal: AbortSignal): Promise<string | undefined>;
|
|
37
|
+
/** The file as the formatter's last argument: relative to where it runs, never read as a flag. */
|
|
38
|
+
private argumentFor;
|
|
39
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { isAbsolute, relative } from 'node:path';
|
|
2
|
+
import { InvalidAgentConfigError } from '#src/shared/domain/errors';
|
|
3
|
+
import { describeFailure } from '#src/shared/domain/providerFailure';
|
|
4
|
+
import { defaultFormatterTimeoutMs, validateFormatter, } from "../domain/commands.js";
|
|
5
|
+
import { EditUnconfirmedError } from "../domain/fileEdits.js";
|
|
6
|
+
import { CommandCheckpoint } from "./commandCheckpoint.js";
|
|
7
|
+
import { runCommandProcess } from "./commandProcess.js";
|
|
8
|
+
import { environmentFor, resolveCwd } from "./commandTool.js";
|
|
9
|
+
// What the model reads of a formatter that failed: enough for a syntax error, and short, since
|
|
10
|
+
// it comes with an edit.
|
|
11
|
+
const failureOutputBytes = 4 * 1024;
|
|
12
|
+
// How the history names what the formatter changed.
|
|
13
|
+
const observer = 'formatter';
|
|
14
|
+
/**
|
|
15
|
+
* Runs the consumer's formatter on a file an edit has just written. Only that file is captured
|
|
16
|
+
* around it, so what it changed there is in the run's history and can be undone; a formatter
|
|
17
|
+
* that changes other files leaves those changes out of the history.
|
|
18
|
+
*/
|
|
19
|
+
export class FileFormatter {
|
|
20
|
+
formatter;
|
|
21
|
+
cwd;
|
|
22
|
+
runProcess;
|
|
23
|
+
constructor(formatter, cwd, runProcess) {
|
|
24
|
+
this.formatter = formatter;
|
|
25
|
+
this.cwd = cwd;
|
|
26
|
+
this.runProcess = runProcess;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* @throws {InvalidAgentConfigError} for an invalid formatter, a working directory outside every
|
|
30
|
+
* `write` root, or on Windows, where a process group cannot be stopped.
|
|
31
|
+
*/
|
|
32
|
+
static async create({ formatter, policy, base, platform = process.platform, runProcess = runCommandProcess, }) {
|
|
33
|
+
if (platform === 'win32') {
|
|
34
|
+
throw new InvalidAgentConfigError('A formatter is not supported on Windows yet: the harness cannot stop it and everything it started there');
|
|
35
|
+
}
|
|
36
|
+
validateFormatter(formatter);
|
|
37
|
+
const cwd = await resolveCwd('The formatter', formatter.cwd, policy, base);
|
|
38
|
+
return new FileFormatter(formatter, cwd, runProcess);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Formats the file at `absolute` and records in `session` what it changed.
|
|
42
|
+
*
|
|
43
|
+
* @returns what the model needs to know when the file may not be formatted, or `undefined`
|
|
44
|
+
* once the formatter exited with 0.
|
|
45
|
+
* @throws {EditUnconfirmedError} when what it changed could not be recorded, or it could not be
|
|
46
|
+
* stopped. Writing stops for the run then, as after such a command.
|
|
47
|
+
*/
|
|
48
|
+
async format(absolute, session, signal) {
|
|
49
|
+
signal.throwIfAborted();
|
|
50
|
+
let checkpoint;
|
|
51
|
+
try {
|
|
52
|
+
const journal = await session.journalForChange();
|
|
53
|
+
checkpoint = await CommandCheckpoint.begin({
|
|
54
|
+
journal,
|
|
55
|
+
covered: () => Promise.resolve([absolute]),
|
|
56
|
+
by: observer,
|
|
57
|
+
signal,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
signal.throwIfAborted();
|
|
62
|
+
return `The formatter did not run, so the file is as you wrote it: ${describeFailure(error)}.`;
|
|
63
|
+
}
|
|
64
|
+
const { executable, args = [], timeoutMs = defaultFormatterTimeoutMs } = this.formatter;
|
|
65
|
+
let result;
|
|
66
|
+
let startFailure;
|
|
67
|
+
try {
|
|
68
|
+
result = await this.runProcess(executable, [...args, this.argumentFor(absolute)], {
|
|
69
|
+
cwd: this.cwd,
|
|
70
|
+
env: environmentFor(this.formatter),
|
|
71
|
+
timeoutMs,
|
|
72
|
+
outputBytes: failureOutputBytes,
|
|
73
|
+
signal,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
startFailure = error;
|
|
78
|
+
}
|
|
79
|
+
const complete = result?.stopped ?? true;
|
|
80
|
+
try {
|
|
81
|
+
await checkpoint.end({ complete });
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
session.stop('Writing stopped: the harness could not record what the formatter changed');
|
|
85
|
+
throw new EditUnconfirmedError('The formatter ran, but the harness could not record what it changed; writing has stopped for this run', { cause: error });
|
|
86
|
+
}
|
|
87
|
+
if (!complete) {
|
|
88
|
+
session.stop('Writing stopped: the formatter could not be stopped');
|
|
89
|
+
throw new EditUnconfirmedError('The formatter could not be stopped: something it started may still be changing files, and the history may not hold all of it. Writing has stopped for this run.');
|
|
90
|
+
}
|
|
91
|
+
if (!result) {
|
|
92
|
+
signal.throwIfAborted();
|
|
93
|
+
return `The formatter could not be started, so the file is as you wrote it: ${describeFailure(startFailure)}.`;
|
|
94
|
+
}
|
|
95
|
+
// After its changes are recorded: a cancelled formatter is never reported as finished.
|
|
96
|
+
if (result.outcome === 'cancelled')
|
|
97
|
+
signal.throwIfAborted();
|
|
98
|
+
if (result.outcome === 'exited' && result.exitCode === 0)
|
|
99
|
+
return undefined;
|
|
100
|
+
const ending = result.outcome === 'timedOut'
|
|
101
|
+
? `ran past its limit of ${String(timeoutMs / 1000)} s and was stopped`
|
|
102
|
+
: result.exitCode !== null
|
|
103
|
+
? `exited with code ${String(result.exitCode)}`
|
|
104
|
+
: `was killed by ${result.signal ?? 'a signal'}`;
|
|
105
|
+
const output = result.printedBytes === 0 ? '' : `\nIts output:\n${result.output}`;
|
|
106
|
+
return `The formatter ${ending}, so the file may not be formatted.${output}`;
|
|
107
|
+
}
|
|
108
|
+
/** The file as the formatter's last argument: relative to where it runs, never read as a flag. */
|
|
109
|
+
argumentFor(absolute) {
|
|
110
|
+
const fromCwd = relative(this.cwd, absolute);
|
|
111
|
+
return fromCwd.startsWith('-') && !isAbsolute(fromCwd) ? `./${fromCwd}` : fromCwd;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=fileFormatter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileFormatter.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/fileFormatter.ts"],"names":[],"mappings":"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","sourcesContent":["import { isAbsolute, relative } from 'node:path';\nimport { InvalidAgentConfigError } from '#src/shared/domain/errors';\nimport { describeFailure } from '#src/shared/domain/providerFailure';\nimport type { AccessPolicy } from '../domain/accessPolicy.ts';\nimport {\n\tdefaultFormatterTimeoutMs,\n\ttype Formatter,\n\tvalidateFormatter,\n} from '../domain/commands.ts';\nimport { EditUnconfirmedError } from '../domain/fileEdits.ts';\nimport { CommandCheckpoint } from './commandCheckpoint.ts';\nimport { type CommandProcessResult, runCommandProcess } from './commandProcess.ts';\nimport { environmentFor, resolveCwd } from './commandTool.ts';\nimport type { WriteSession } from './writeSession.ts';\n\n// What the model reads of a formatter that failed: enough for a syntax error, and short, since\n// it comes with an edit.\nconst failureOutputBytes = 4 * 1024;\n// How the history names what the formatter changed.\nconst observer = 'formatter';\n\n/**\n * Runs the consumer's formatter on a file an edit has just written. Only that file is captured\n * around it, so what it changed there is in the run's history and can be undone; a formatter\n * that changes other files leaves those changes out of the history.\n */\nexport class FileFormatter {\n\tprivate readonly formatter: Formatter;\n\tprivate readonly cwd: string;\n\tprivate readonly runProcess: typeof runCommandProcess;\n\n\tprivate constructor(formatter: Formatter, cwd: string, runProcess: typeof runCommandProcess) {\n\t\tthis.formatter = formatter;\n\t\tthis.cwd = cwd;\n\t\tthis.runProcess = runProcess;\n\t}\n\n\t/**\n\t * @throws {InvalidAgentConfigError} for an invalid formatter, a working directory outside every\n\t * `write` root, or on Windows, where a process group cannot be stopped.\n\t */\n\tstatic async create({\n\t\tformatter,\n\t\tpolicy,\n\t\tbase,\n\t\tplatform = process.platform,\n\t\trunProcess = runCommandProcess,\n\t}: {\n\t\tformatter: Formatter;\n\t\tpolicy: AccessPolicy;\n\t\t/** The real path of the first root, where a relative `cwd` starts. */\n\t\tbase: string;\n\t\tplatform?: NodeJS.Platform;\n\t\t/** Runs the program. Replaced in tests, to reach what a real process rarely does. */\n\t\trunProcess?: typeof runCommandProcess;\n\t}): Promise<FileFormatter> {\n\t\tif (platform === 'win32') {\n\t\t\tthrow new InvalidAgentConfigError(\n\t\t\t\t'A formatter is not supported on Windows yet: the harness cannot stop it and everything it started there',\n\t\t\t);\n\t\t}\n\t\tvalidateFormatter(formatter);\n\t\tconst cwd = await resolveCwd('The formatter', formatter.cwd, policy, base);\n\t\treturn new FileFormatter(formatter, cwd, runProcess);\n\t}\n\n\t/**\n\t * Formats the file at `absolute` and records in `session` what it changed.\n\t *\n\t * @returns what the model needs to know when the file may not be formatted, or `undefined`\n\t * once the formatter exited with 0.\n\t * @throws {EditUnconfirmedError} when what it changed could not be recorded, or it could not be\n\t * stopped. Writing stops for the run then, as after such a command.\n\t */\n\tasync format(\n\t\tabsolute: string,\n\t\tsession: WriteSession,\n\t\tsignal: AbortSignal,\n\t): Promise<string | undefined> {\n\t\tsignal.throwIfAborted();\n\t\tlet checkpoint: CommandCheckpoint;\n\t\ttry {\n\t\t\tconst journal = await session.journalForChange();\n\t\t\tcheckpoint = await CommandCheckpoint.begin({\n\t\t\t\tjournal,\n\t\t\t\tcovered: () => Promise.resolve([absolute]),\n\t\t\t\tby: observer,\n\t\t\t\tsignal,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tsignal.throwIfAborted();\n\t\t\treturn `The formatter did not run, so the file is as you wrote it: ${describeFailure(error)}.`;\n\t\t}\n\n\t\tconst { executable, args = [], timeoutMs = defaultFormatterTimeoutMs } = this.formatter;\n\t\tlet result: CommandProcessResult | undefined;\n\t\tlet startFailure: unknown;\n\t\ttry {\n\t\t\tresult = await this.runProcess(executable, [...args, this.argumentFor(absolute)], {\n\t\t\t\tcwd: this.cwd,\n\t\t\t\tenv: environmentFor(this.formatter),\n\t\t\t\ttimeoutMs,\n\t\t\t\toutputBytes: failureOutputBytes,\n\t\t\t\tsignal,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tstartFailure = error;\n\t\t}\n\n\t\tconst complete = result?.stopped ?? true;\n\t\ttry {\n\t\t\tawait checkpoint.end({ complete });\n\t\t} catch (error) {\n\t\t\tsession.stop('Writing stopped: the harness could not record what the formatter changed');\n\t\t\tthrow new EditUnconfirmedError(\n\t\t\t\t'The formatter ran, but the harness could not record what it changed; writing has stopped for this run',\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\t\tif (!complete) {\n\t\t\tsession.stop('Writing stopped: the formatter could not be stopped');\n\t\t\tthrow new EditUnconfirmedError(\n\t\t\t\t'The formatter could not be stopped: something it started may still be changing files, and the history may not hold all of it. Writing has stopped for this run.',\n\t\t\t);\n\t\t}\n\n\t\tif (!result) {\n\t\t\tsignal.throwIfAborted();\n\t\t\treturn `The formatter could not be started, so the file is as you wrote it: ${describeFailure(startFailure)}.`;\n\t\t}\n\t\t// After its changes are recorded: a cancelled formatter is never reported as finished.\n\t\tif (result.outcome === 'cancelled') signal.throwIfAborted();\n\t\tif (result.outcome === 'exited' && result.exitCode === 0) return undefined;\n\t\tconst ending =\n\t\t\tresult.outcome === 'timedOut'\n\t\t\t\t? `ran past its limit of ${String(timeoutMs / 1000)} s and was stopped`\n\t\t\t\t: result.exitCode !== null\n\t\t\t\t\t? `exited with code ${String(result.exitCode)}`\n\t\t\t\t\t: `was killed by ${result.signal ?? 'a signal'}`;\n\t\tconst output = result.printedBytes === 0 ? '' : `\\nIts output:\\n${result.output}`;\n\t\treturn `The formatter ${ending}, so the file may not be formatted.${output}`;\n\t}\n\n\t/** The file as the formatter's last argument: relative to where it runs, never read as a flag. */\n\tprivate argumentFor(absolute: string): string {\n\t\tconst fromCwd = relative(this.cwd, absolute);\n\t\treturn fromCwd.startsWith('-') && !isAbsolute(fromCwd) ? `./${fromCwd}` : fromCwd;\n\t}\n}\n"]}
|
|
@@ -2,7 +2,7 @@ import type { ToolDefinition } from '#src/llm/domain/tool';
|
|
|
2
2
|
import type { RecoveryStore } from '#src/recovery/domain/recoveryStore';
|
|
3
3
|
import type { ILogger } from '#src/shared/domain/logger';
|
|
4
4
|
import type { AccessPolicy, WorkspaceRoot } from '../domain/accessPolicy.ts';
|
|
5
|
-
import type { Commands } from '../domain/commands.ts';
|
|
5
|
+
import type { Commands, Formatter } from '../domain/commands.ts';
|
|
6
6
|
import type { AgentTool } from '../domain/preparedCall.ts';
|
|
7
7
|
import type { Workspace } from '../domain/workspace.ts';
|
|
8
8
|
import type { CoveredFiles } from './commandCheckpoint.ts';
|
|
@@ -60,7 +60,15 @@ export declare function stateDirectoryOf({ stateDirectory }: WorkspaceOptions):
|
|
|
60
60
|
* @throws {UnrecoverableError} when the store cannot be opened.
|
|
61
61
|
*/
|
|
62
62
|
export declare function openStore(workspace: WorkspaceOptions, directory: string): Promise<RecoveryStore>;
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Opens what the file tools of one workspace share. With `formatter`, its edits format each file
|
|
65
|
+
* they write.
|
|
66
|
+
*
|
|
67
|
+
* @throws {InvalidAgentConfigError} for a workspace with no root, or an invalid formatter.
|
|
68
|
+
*/
|
|
69
|
+
export declare function openWorkspace(workspace: WorkspaceOptions, logger: ILogger, { formatter }?: {
|
|
70
|
+
formatter?: Formatter;
|
|
71
|
+
}): Promise<OpenedWorkspace>;
|
|
64
72
|
/**
|
|
65
73
|
* `runCommand` over an opened workspace: see `createCommandTool`.
|
|
66
74
|
*
|
|
@@ -6,6 +6,7 @@ import { describeFailure } from '#src/shared/domain/providerFailure';
|
|
|
6
6
|
import { createCommandTool } from "./commandTool.js";
|
|
7
7
|
import { chooseWorkspace } from "./createWorkspace.js";
|
|
8
8
|
import { createApplyPatchTool, createDeleteFileTool, createTextEditorTool } from "./editFormats.js";
|
|
9
|
+
import { FileFormatter } from "./fileFormatter.js";
|
|
9
10
|
import { GitIgnoreRules } from "./gitIgnoreRules.js";
|
|
10
11
|
import { PolicyWorkspace } from "./policyWorkspace.js";
|
|
11
12
|
import { RootsAccessPolicy } from "./rootsAccessPolicy.js";
|
|
@@ -50,7 +51,13 @@ export async function openStore(workspace, directory) {
|
|
|
50
51
|
});
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Opens what the file tools of one workspace share. With `formatter`, its edits format each file
|
|
56
|
+
* they write.
|
|
57
|
+
*
|
|
58
|
+
* @throws {InvalidAgentConfigError} for a workspace with no root, or an invalid formatter.
|
|
59
|
+
*/
|
|
60
|
+
export async function openWorkspace(workspace, logger, { formatter } = {}) {
|
|
54
61
|
const { roots, secrets } = workspace;
|
|
55
62
|
const stateDirectory = await stateDirectoryOf(workspace);
|
|
56
63
|
const policy = await RootsAccessPolicy.create({
|
|
@@ -71,12 +78,17 @@ export async function openWorkspace(workspace, logger) {
|
|
|
71
78
|
hidden: path => policy.hidesFromReading(join(root, path)),
|
|
72
79
|
});
|
|
73
80
|
const read = new PolicyWorkspace({ inner, policy });
|
|
81
|
+
const fileFormatter = formatter && (await FileFormatter.create({ formatter, policy, base: root }));
|
|
74
82
|
return {
|
|
75
83
|
roots,
|
|
76
84
|
policy,
|
|
77
85
|
store,
|
|
78
86
|
read,
|
|
79
|
-
writes: new WorkspaceWrites({
|
|
87
|
+
writes: new WorkspaceWrites({
|
|
88
|
+
policy,
|
|
89
|
+
store,
|
|
90
|
+
...(fileFormatter ? { formatter: fileFormatter } : {}),
|
|
91
|
+
}),
|
|
80
92
|
writable: roots.some(({ access }) => access === 'write'),
|
|
81
93
|
base: root,
|
|
82
94
|
covered: coveredFiles(roots, policy, logger),
|