@mikode13/harness 9.3.1 → 11.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 +81 -10
- package/dist/agent/domain/agent.d.ts +31 -7
- package/dist/agent/domain/agent.js.map +1 -1
- package/dist/agent/domain/approval.d.ts +2 -1
- package/dist/agent/domain/approval.js +2 -1
- package/dist/agent/domain/approval.js.map +1 -1
- package/dist/agent/domain/question.d.ts +5 -0
- package/dist/agent/domain/question.js +11 -0
- package/dist/agent/domain/question.js.map +1 -1
- package/dist/agent/domain/runContext.d.ts +17 -0
- package/dist/agent/domain/runContext.js +9 -0
- package/dist/agent/domain/runContext.js.map +1 -1
- package/dist/agent/domain/transcript.d.ts +10 -0
- package/dist/agent/domain/transcript.js.map +1 -1
- package/dist/engines/domain/model/askUser.d.ts +0 -5
- package/dist/engines/domain/model/askUser.js +0 -9
- package/dist/engines/domain/model/askUser.js.map +1 -1
- package/dist/engines/domain/model/backgroundAgents.d.ts +60 -0
- package/dist/engines/domain/model/backgroundAgents.js +165 -0
- package/dist/engines/domain/model/backgroundAgents.js.map +1 -0
- package/dist/engines/domain/model/llmAgent.d.ts +12 -3
- package/dist/engines/domain/model/llmAgent.js +184 -74
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/factory/infrastructure/agentLLMFactory.d.ts +7 -3
- package/dist/factory/infrastructure/agentLLMFactory.js +3 -3
- package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/tools/domain/preparedCall.d.ts +52 -2
- package/dist/tools/domain/preparedCall.js +15 -0
- package/dist/tools/domain/preparedCall.js.map +1 -1
- package/dist/tools/domain/readRegistry.d.ts +7 -0
- package/dist/tools/domain/readRegistry.js.map +1 -1
- package/dist/tools/infrastructure/agentAsTool.d.ts +63 -0
- package/dist/tools/infrastructure/agentAsTool.js +143 -0
- package/dist/tools/infrastructure/agentAsTool.js.map +1 -0
- package/dist/tools/infrastructure/commandTool.js +6 -2
- package/dist/tools/infrastructure/commandTool.js.map +1 -1
- package/dist/tools/infrastructure/defineTool.d.ts +14 -1
- package/dist/tools/infrastructure/defineTool.js +24 -0
- package/dist/tools/infrastructure/defineTool.js.map +1 -1
- package/dist/tools/infrastructure/exploreTool.d.ts +2 -2
- package/dist/tools/infrastructure/exploreTool.js +36 -56
- package/dist/tools/infrastructure/exploreTool.js.map +1 -1
- package/dist/tools/infrastructure/fileEditor.d.ts +2 -0
- package/dist/tools/infrastructure/fileEditor.js +5 -1
- package/dist/tools/infrastructure/fileEditor.js.map +1 -1
- package/dist/tools/infrastructure/fileFormatter.d.ts +1 -0
- package/dist/tools/infrastructure/fileFormatter.js +5 -1
- package/dist/tools/infrastructure/fileFormatter.js.map +1 -1
- package/dist/tools/infrastructure/subagents.d.ts +19 -0
- package/dist/tools/infrastructure/subagents.js +29 -0
- package/dist/tools/infrastructure/subagents.js.map +1 -0
- package/dist/tools/infrastructure/writeSession.d.ts +8 -0
- package/dist/tools/infrastructure/writeSession.js +37 -0
- package/dist/tools/infrastructure/writeSession.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exploreTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/exploreTool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,
|
|
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 } from '#src/agent/domain/agent';\nimport { RecoverableError, UnrecoverableError } from '#src/shared/domain/errors';\nimport type { DelegatedAnswer, DelegatingTool } from '../domain/preparedCall.ts';\nimport type { ToolCallContext } from '../domain/readRegistry.ts';\nimport { defineDelegatingTool } from './defineTool.ts';\nimport { cutAnswer, failureOf, subagentOptions } from './subagents.ts';\n\n/** The most explorers one call sends, so a single call cannot fan out without bound. */\nexport const maxExplorers = 4;\n\n/**\n * Sends one explorer, a new agent that starts from its question alone. What it spends counts in\n * the run's usage, whether it answered or failed; a failure becomes its answer. A cancellation\n * escapes.\n */\nasync function explore(\n\tquestion: string,\n\tsubagent: number,\n\texplorer: () => Agent,\n\tsignal: AbortSignal,\n\tcall: ToolCallContext,\n): Promise<DelegatedAnswer> {\n\t// Within the run that made the call, so the explorer's reads belong to it.\n\tconst options = subagentOptions(call, {\n\t\ttool: 'explore',\n\t\tname: 'explorer',\n\t\tsubagent,\n\t\ttask: question,\n\t\toptions: { signal },\n\t});\n\ttry {\n\t\tconst { response, tokens } = await explorer().run(question, options);\n\t\tcall.spend(tokens, tokens === undefined);\n\t\treturn { text: cutAnswer(response || 'It found nothing to report.') };\n\t} catch (error) {\n\t\t// Whatever it threw once the run was cancelled, it ends as cancelled.\n\t\tsignal.throwIfAborted();\n\t\tif (!(error instanceof RecoverableError || error instanceof UnrecoverableError)) throw error;\n\t\tcall.spend(error.tokens, error.usageUnreported);\n\t\treturn { text: `The explorer failed: ${failureOf(error)}`, failed: true };\n\t}\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): DelegatingTool {\n\treturn defineDelegatingTool({\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. They work in the background: keep working meanwhile, and each explorer's report reaches you in a message of its own once it finishes. 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\tdelegate: ({ 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\tstart: signal =>\n\t\t\t\t\tasked.map((question, index) => ({\n\t\t\t\t\t\tname: 'explorer',\n\t\t\t\t\t\tsubagent: index + 1,\n\t\t\t\t\t\ttask: question,\n\t\t\t\t\t\tanswer: explore(question, index + 1, explorer, signal, call),\n\t\t\t\t\t})),\n\t\t\t});\n\t\t},\n\t});\n}\n"]}
|
|
@@ -43,7 +43,9 @@ export declare class FileEditor {
|
|
|
43
43
|
content?: Buffer;
|
|
44
44
|
}>;
|
|
45
45
|
prepare(change: FileChange, signal: AbortSignal): Promise<PreparedEdit>;
|
|
46
|
+
/** Makes the change once no other write of the run is under way, so none can come between. */
|
|
46
47
|
apply(prepared: PreparedEdit, signal: AbortSignal): Promise<void>;
|
|
48
|
+
private applyAlone;
|
|
47
49
|
/**
|
|
48
50
|
* Records a change that was made. If that fails, its prepared line still holds both states, so
|
|
49
51
|
* an undo can tell; but the record cannot be trusted to continue, and the caller must know.
|
|
@@ -133,7 +133,11 @@ export class FileEditor {
|
|
|
133
133
|
widensIgnoreRules: isIgnoreFile(target.absolute) && widensIgnoreRules(previous, content),
|
|
134
134
|
});
|
|
135
135
|
}
|
|
136
|
-
|
|
136
|
+
/** Makes the change once no other write of the run is under way, so none can come between. */
|
|
137
|
+
apply(prepared, signal) {
|
|
138
|
+
return this.session.exclusive(() => this.applyAlone(prepared, signal), signal);
|
|
139
|
+
}
|
|
140
|
+
async applyAlone(prepared, signal) {
|
|
137
141
|
const { path } = prepared;
|
|
138
142
|
signal.throwIfAborted();
|
|
139
143
|
// A Buffer's bytes cannot be frozen: copied, then checked, so what is written is what was
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fileEditor.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/fileEditor.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomBytes } from 'node:crypto';\nimport {\n\tchmod,\n\tlink,\n\tlstat,\n\tmkdir,\n\tmkdtemp,\n\topen,\n\treadFile,\n\trename,\n\trm,\n\trmdir,\n\tstat,\n\tunlink,\n} from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { basename, dirname, join } from 'node:path';\nimport type { FileState, RunJournal } from '#src/recovery/domain/recoveryStore';\nimport type { ToolRisk } from '#src/agent/domain/approval';\nimport type { AccessPolicy, AllowedPath } from '../domain/accessPolicy.ts';\nimport {\n\tEditRefusedError,\n\tEditUnconfirmedError,\n\ttype FileChange,\n\ttype PreparedEdit,\n} from '../domain/fileEdits.ts';\nimport { isIgnoreFile, widensIgnoreRules } from './ignoreFileChanges.ts';\nimport type { WriteSession } from './writeSession.ts';\n\n// Larger than any source file a model should rewrite whole, and small enough to keep a copy.\nconst defaultMaxFileBytes = 5 * 1024 * 1024;\n\nfunction hashOf(content: Buffer): string {\n\treturn createHash('sha256').update(content).digest('hex');\n}\n\nfunction code(error: unknown): string {\n\treturn (error as NodeJS.ErrnoException).code ?? 'an unknown error';\n}\n\nfunction sameState(a: FileState, b: FileState): boolean {\n\tif (!a.exists || !b.exists) return a.exists === b.exists;\n\treturn a.hash === b.hash && a.mode === b.mode;\n}\n\nasync function syncDirectory(path: string): Promise<void> {\n\tconst directory = await open(path, 'r');\n\ttry {\n\t\tawait directory.sync();\n\t} finally {\n\t\tawait directory.close();\n\t}\n}\n\nlet newFileMode: Promise<number> | undefined;\n\n/**\n * The mode a new file gets in this process: 0o666 less the umask. Measured once by creating\n * one, because reading the umask directly briefly changes it for every thread.\n */\nfunction getNewFileMode(): Promise<number> {\n\tnewFileMode ??= (async () => {\n\t\tconst folder = await mkdtemp(join(tmpdir(), 'mikode-harness-mode-'));\n\t\ttry {\n\t\t\tconst probe = join(folder, 'probe');\n\t\t\tawait (await open(probe, 'wx', 0o666)).close();\n\t\t\treturn (await stat(probe)).mode & 0o7777;\n\t\t} finally {\n\t\t\tawait rm(folder, { recursive: true, force: true });\n\t\t}\n\t})();\n\treturn newFileMode;\n}\n\n/** The folders between the nearest existing one and `folder`, outermost first. */\nasync function missingFolders(folder: string): Promise<string[]> {\n\tconst missing: string[] = [];\n\tfor (let current = folder; ; current = dirname(current)) {\n\t\tconst exists = await lstat(current).then(\n\t\t\t() => true,\n\t\t\t(error: unknown) => {\n\t\t\t\tif (code(error) === 'ENOENT') return false;\n\t\t\t\tthrow error;\n\t\t\t},\n\t\t);\n\t\tif (exists || dirname(current) === current) return missing;\n\t\tmissing.unshift(current);\n\t}\n}\n\n/**\n * The risk of a prepared change. A change git or the run's record can undo is `mutating`; one\n * that widens `.gitignore` is `destructive`, so it waits for the user, because it would open\n * paths that every tool keeps closed.\n */\nexport function editRisk(prepared: PreparedEdit): ToolRisk {\n\treturn prepared.widensIgnoreRules ? 'destructive' : 'mutating';\n}\n\n/**\n * Makes the changes the edit tools ask for, under the access policy and the run's recovery\n * journal. `prepare` checks a change and fixes what it will do; `apply` makes it, in this order:\n *\n * 1. Check the policy again, and that the file still holds what was prepared.\n * 2. Store the content it replaces and the content it writes, and record the change as\n * prepared. If either fails, nothing is touched.\n * 3. Write: a new file appears whole and only if nothing took its place meanwhile; a replaced\n * one is swapped for its new version in one rename; a deleted one is unlinked.\n * 4. Record the change as applied, or as abandoned when the write failed and left the file as\n * it was. A change the record cannot confirm stops the session and throws\n * `EditUnconfirmedError`, never a success or a refusal.\n *\n * Every message names the file as the model wrote it, never by its place on the host.\n */\nexport class FileEditor {\n\tprivate readonly policy: AccessPolicy;\n\tprivate readonly session: WriteSession;\n\tprivate readonly maxFileBytes: number;\n\n\tconstructor({\n\t\tpolicy,\n\t\tsession,\n\t\tmaxFileBytes = defaultMaxFileBytes,\n\t}: {\n\t\tpolicy: AccessPolicy;\n\t\tsession: WriteSession;\n\t\tmaxFileBytes?: number;\n\t}) {\n\t\tthis.policy = policy;\n\t\tthis.session = session;\n\t\tthis.maxFileBytes = maxFileBytes;\n\t}\n\n\t/**\n\t * What `path` holds now, checked as a change to it would be: an edit format works its new\n\t * content out from this, and an edit tool compares it with the version the model read.\n\t */\n\tasync current(\n\t\tpath: string,\n\t\tsignal: AbortSignal,\n\t): Promise<{ target: AllowedPath; state: FileState; content?: Buffer }> {\n\t\tconst target = await this.policy.check(path, 'write', signal);\n\t\treturn { target, ...(await this.read(target.absolute, path)) };\n\t}\n\n\tasync prepare(change: FileChange, signal: AbortSignal): Promise<PreparedEdit> {\n\t\tconst { path } = change;\n\t\tconst target = await this.policy.check(path, 'write', signal);\n\t\tconst { state, content: previous } = await this.read(target.absolute, path);\n\n\t\tif (change.kind === 'create') {\n\t\t\tif (state.exists) {\n\t\t\t\tthrow new EditRefusedError(`\"${path}\" already exists; read it and change it instead`);\n\t\t\t}\n\t\t} else {\n\t\t\tif (!state.exists) throw new EditRefusedError(`\"${path}\" does not exist`);\n\t\t\tif (change.expected !== undefined && change.expected !== state.hash) {\n\t\t\t\tthrow new EditRefusedError(`\"${path}\" changed since it was read; read it again`);\n\t\t\t}\n\t\t}\n\n\t\tconst content = change.kind === 'delete' ? undefined : change.content;\n\t\tif (content && content.length > this.maxFileBytes) {\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`The new content of \"${path}\" is larger than ${String(this.maxFileBytes / 1024 / 1024)} MB`,\n\t\t\t);\n\t\t}\n\t\tif (content && state.exists && hashOf(content) === state.hash) {\n\t\t\tthrow new EditRefusedError(`\"${path}\" already holds that content; nothing to change`);\n\t\t}\n\n\t\t// Copies all the way down: what is approved must not change while the approval waits.\n\t\treturn Object.freeze({\n\t\t\tkind: change.kind,\n\t\t\tpath,\n\t\t\ttarget: Object.freeze({ ...target }),\n\t\t\tbefore: Object.freeze({ ...state }),\n\t\t\tcontent: content && Buffer.from(content),\n\t\t\tcontentHash: content && hashOf(content),\n\t\t\twidensIgnoreRules: isIgnoreFile(target.absolute) && widensIgnoreRules(previous, content),\n\t\t});\n\t}\n\n\tasync apply(prepared: PreparedEdit, signal: AbortSignal): Promise<void> {\n\t\tconst { path } = prepared;\n\t\tsignal.throwIfAborted();\n\t\t// A Buffer's bytes cannot be frozen: copied, then checked, so what is written is what was\n\t\t// approved, whatever happens to `prepared.content` from here on.\n\t\tconst content = prepared.content && Buffer.from(prepared.content);\n\t\tif ((content && hashOf(content)) !== prepared.contentHash) {\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`The content prepared for \"${path}\" changed after it was prepared; prepare the change again`,\n\t\t\t);\n\t\t}\n\t\tconst target = await this.policy.check(path, 'write', signal);\n\t\tif (target.absolute !== prepared.target.absolute) {\n\t\t\tthrow new EditRefusedError(`\"${path}\" now leads somewhere else; prepare the change again`);\n\t\t}\n\t\tconst current = await this.read(target.absolute, path);\n\t\tif (!sameState(current.state, prepared.before)) {\n\t\t\tthrow new EditRefusedError(`\"${path}\" changed after this change was prepared; read it again`);\n\t\t}\n\n\t\t// Checked again before the first effect: starting the journal takes the workspace.\n\t\tsignal.throwIfAborted();\n\t\tconst journal = await this.session.journalForChange();\n\t\tconst after = await this.record(journal, path, current.content, content, prepared.before);\n\t\tconst createdFolders =\n\t\t\tprepared.kind === 'create' ? await missingFolders(dirname(target.absolute)) : [];\n\t\tconst sequence = await this.journal(path, () =>\n\t\t\tjournal.prepare({\n\t\t\t\tpath: target.absolute,\n\t\t\t\tbefore: current.state,\n\t\t\t\tafter,\n\t\t\t\t...(createdFolders.length > 0 ? { createdFolders } : {}),\n\t\t\t}),\n\t\t);\n\n\t\t// The last moment to stop: past here the file changes.\n\t\tif (signal.aborted) {\n\t\t\tawait journal.abandoned(sequence);\n\t\t\tsignal.throwIfAborted();\n\t\t}\n\n\t\ttry {\n\t\t\tawait this.write(prepared, content, target.absolute);\n\t\t} catch (error) {\n\t\t\tawait this.settleFailedWrite({\n\t\t\t\tjournal,\n\t\t\t\tsequence,\n\t\t\t\tkind: prepared.kind,\n\t\t\t\tpath,\n\t\t\t\ttarget: target.absolute,\n\t\t\t\tcurrent,\n\t\t\t\tafter,\n\t\t\t\tcreatedFolders,\n\t\t\t\terror,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tawait this.markApplied(journal, sequence, path);\n\t}\n\n\t/**\n\t * Records a change that was made. If that fails, its prepared line still holds both states, so\n\t * an undo can tell; but the record cannot be trusted to continue, and the caller must know.\n\t */\n\tprivate async markApplied(journal: RunJournal, sequence: number, path: string): Promise<void> {\n\t\ttry {\n\t\t\tawait journal.applied(sequence);\n\t\t} catch (error) {\n\t\t\tthis.session.stop('Writing stopped: the harness could not record a change it made');\n\t\t\tthrow new EditUnconfirmedError(\n\t\t\t\t`\"${path}\" was changed, but the harness could not record it; writing has stopped for this run`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\t}\n\n\t/** What `path` holds now. Only a regular file within the size limit can be changed. */\n\tprivate async read(\n\t\tabsolute: string,\n\t\tpath: string,\n\t): Promise<{ state: FileState; content?: Buffer }> {\n\t\tlet stats;\n\t\ttry {\n\t\t\tstats = await lstat(absolute);\n\t\t} catch (error) {\n\t\t\tif (code(error) === 'ENOENT') return { state: { exists: false } };\n\t\t\tthrow new EditRefusedError(`Could not read \"${path}\" (${code(error)})`, { cause: error });\n\t\t}\n\t\tif (!stats.isFile()) throw new EditRefusedError(`\"${path}\" is not a regular file`);\n\t\tif (stats.size > this.maxFileBytes) {\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`\"${path}\" is larger than ${String(this.maxFileBytes / 1024 / 1024)} MB, too large to change`,\n\t\t\t);\n\t\t}\n\n\t\tlet content: Buffer;\n\t\ttry {\n\t\t\tcontent = await readFile(absolute);\n\t\t} catch (error) {\n\t\t\tthrow new EditRefusedError(`Could not read \"${path}\" (${code(error)})`, { cause: error });\n\t\t}\n\t\treturn {\n\t\t\tstate: { exists: true, hash: hashOf(content), mode: stats.mode & 0o7777 },\n\t\t\tcontent,\n\t\t};\n\t}\n\n\t/** Stores what the change replaces and what it writes, and returns the state it leaves. */\n\tprivate async record(\n\t\tjournal: RunJournal,\n\t\tpath: string,\n\t\tprevious: Buffer | undefined,\n\t\tcontent: Buffer | undefined,\n\t\tbefore: FileState,\n\t): Promise<FileState> {\n\t\tif (previous) await this.journal(path, () => journal.saveContent(previous));\n\t\tif (!content) return { exists: false };\n\n\t\tconst hash = await this.journal(path, () => journal.saveContent(content));\n\t\treturn { exists: true, hash, mode: before.exists ? before.mode : await getNewFileMode() };\n\t}\n\n\tprivate async journal<Result>(path: string, step: () => Promise<Result>): Promise<Result> {\n\t\ttry {\n\t\t\treturn await step();\n\t\t} catch (error) {\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`The harness could not record how to undo the change to \"${path}\", so it was not changed`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate async write(\n\t\t{ kind, before }: PreparedEdit,\n\t\tcontent: Buffer | undefined,\n\t\tabsolute: string,\n\t): Promise<void> {\n\t\tif (kind === 'delete') {\n\t\t\tawait unlink(absolute);\n\t\t\tawait syncDirectory(dirname(absolute));\n\t\t\treturn;\n\t\t}\n\n\t\tawait mkdir(dirname(absolute), { recursive: true });\n\t\t// Written beside the file, so the final step is one rename or link on the same disk.\n\t\tconst temporary = join(\n\t\t\tdirname(absolute),\n\t\t\t`.${basename(absolute)}.${randomBytes(6).toString('hex')}.mikode-tmp`,\n\t\t);\n\t\ttry {\n\t\t\tconst handle = await open(temporary, 'wx');\n\t\t\ttry {\n\t\t\t\tawait handle.writeFile(content ?? Buffer.alloc(0));\n\t\t\t\tawait handle.sync();\n\t\t\t} finally {\n\t\t\t\tawait handle.close();\n\t\t\t}\n\t\t\tif (before.exists) await chmod(temporary, before.mode);\n\n\t\t\tif (kind === 'create') {\n\t\t\t\t// A link fails if something appeared at the path meanwhile; a rename would replace it.\n\t\t\t\tawait link(temporary, absolute);\n\t\t\t} else {\n\t\t\t\tawait rename(temporary, absolute);\n\t\t\t}\n\t\t} finally {\n\t\t\tawait rm(temporary, { force: true });\n\t\t}\n\t\tawait syncDirectory(dirname(absolute));\n\t}\n\n\t/**\n\t * A write threw. If the file still holds what it held, nothing happened and the change is\n\t * abandoned; if it holds the new state, the change happened anyway. Anything else cannot be\n\t * explained, so the session stops writing.\n\t */\n\tprivate async settleFailedWrite({\n\t\tjournal,\n\t\tsequence,\n\t\tkind,\n\t\tpath,\n\t\ttarget,\n\t\tcurrent,\n\t\tafter,\n\t\tcreatedFolders,\n\t\terror,\n\t}: {\n\t\tjournal: RunJournal;\n\t\tsequence: number;\n\t\tkind: PreparedEdit['kind'];\n\t\tpath: string;\n\t\ttarget: string;\n\t\tcurrent: { state: FileState };\n\t\tafter: FileState;\n\t\tcreatedFolders: string[];\n\t\terror: unknown;\n\t}): Promise<void> {\n\t\tconst now = await this.read(target, path).catch(() => undefined);\n\t\tif (now && sameState(now.state, after)) {\n\t\t\tawait this.markApplied(journal, sequence, path);\n\t\t\treturn;\n\t\t}\n\t\t// A link that found the path taken made no change: what is there now is someone else's.\n\t\tconst appeared = kind === 'create' && code(error) === 'EEXIST';\n\t\tif (appeared || (now && sameState(now.state, current.state))) {\n\t\t\t// Innermost first, and only while empty: anything else in them is not this run's.\n\t\t\tfor (const folder of [...createdFolders].reverse()) {\n\t\t\t\tawait rmdir(folder).catch(() => undefined);\n\t\t\t}\n\t\t\tawait journal.abandoned(sequence).catch(() => undefined);\n\t\t\tthrow new EditRefusedError(\n\t\t\t\tappeared\n\t\t\t\t\t? `\"${path}\" appeared while it was being created; read it before changing it`\n\t\t\t\t\t: `Could not write \"${path}\" (${code(error)}); it was not changed`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\n\t\tthis.session.stop(\n\t\t\t`Writing stopped: the harness could not tell whether \"${path}\" was changed; ask the user to check it`,\n\t\t);\n\t\tthrow new EditUnconfirmedError(\n\t\t\t`Could not tell whether \"${path}\" was changed (${code(error)}); writing has stopped for this run`,\n\t\t\t{ cause: error },\n\t\t);\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fileEditor.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/fileEditor.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomBytes } from 'node:crypto';\nimport {\n\tchmod,\n\tlink,\n\tlstat,\n\tmkdir,\n\tmkdtemp,\n\topen,\n\treadFile,\n\trename,\n\trm,\n\trmdir,\n\tstat,\n\tunlink,\n} from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { basename, dirname, join } from 'node:path';\nimport type { FileState, RunJournal } from '#src/recovery/domain/recoveryStore';\nimport type { ToolRisk } from '#src/agent/domain/approval';\nimport type { AccessPolicy, AllowedPath } from '../domain/accessPolicy.ts';\nimport {\n\tEditRefusedError,\n\tEditUnconfirmedError,\n\ttype FileChange,\n\ttype PreparedEdit,\n} from '../domain/fileEdits.ts';\nimport { isIgnoreFile, widensIgnoreRules } from './ignoreFileChanges.ts';\nimport type { WriteSession } from './writeSession.ts';\n\n// Larger than any source file a model should rewrite whole, and small enough to keep a copy.\nconst defaultMaxFileBytes = 5 * 1024 * 1024;\n\nfunction hashOf(content: Buffer): string {\n\treturn createHash('sha256').update(content).digest('hex');\n}\n\nfunction code(error: unknown): string {\n\treturn (error as NodeJS.ErrnoException).code ?? 'an unknown error';\n}\n\nfunction sameState(a: FileState, b: FileState): boolean {\n\tif (!a.exists || !b.exists) return a.exists === b.exists;\n\treturn a.hash === b.hash && a.mode === b.mode;\n}\n\nasync function syncDirectory(path: string): Promise<void> {\n\tconst directory = await open(path, 'r');\n\ttry {\n\t\tawait directory.sync();\n\t} finally {\n\t\tawait directory.close();\n\t}\n}\n\nlet newFileMode: Promise<number> | undefined;\n\n/**\n * The mode a new file gets in this process: 0o666 less the umask. Measured once by creating\n * one, because reading the umask directly briefly changes it for every thread.\n */\nfunction getNewFileMode(): Promise<number> {\n\tnewFileMode ??= (async () => {\n\t\tconst folder = await mkdtemp(join(tmpdir(), 'mikode-harness-mode-'));\n\t\ttry {\n\t\t\tconst probe = join(folder, 'probe');\n\t\t\tawait (await open(probe, 'wx', 0o666)).close();\n\t\t\treturn (await stat(probe)).mode & 0o7777;\n\t\t} finally {\n\t\t\tawait rm(folder, { recursive: true, force: true });\n\t\t}\n\t})();\n\treturn newFileMode;\n}\n\n/** The folders between the nearest existing one and `folder`, outermost first. */\nasync function missingFolders(folder: string): Promise<string[]> {\n\tconst missing: string[] = [];\n\tfor (let current = folder; ; current = dirname(current)) {\n\t\tconst exists = await lstat(current).then(\n\t\t\t() => true,\n\t\t\t(error: unknown) => {\n\t\t\t\tif (code(error) === 'ENOENT') return false;\n\t\t\t\tthrow error;\n\t\t\t},\n\t\t);\n\t\tif (exists || dirname(current) === current) return missing;\n\t\tmissing.unshift(current);\n\t}\n}\n\n/**\n * The risk of a prepared change. A change git or the run's record can undo is `mutating`; one\n * that widens `.gitignore` is `destructive`, so it waits for the user, because it would open\n * paths that every tool keeps closed.\n */\nexport function editRisk(prepared: PreparedEdit): ToolRisk {\n\treturn prepared.widensIgnoreRules ? 'destructive' : 'mutating';\n}\n\n/**\n * Makes the changes the edit tools ask for, under the access policy and the run's recovery\n * journal. `prepare` checks a change and fixes what it will do; `apply` makes it, in this order:\n *\n * 1. Check the policy again, and that the file still holds what was prepared.\n * 2. Store the content it replaces and the content it writes, and record the change as\n * prepared. If either fails, nothing is touched.\n * 3. Write: a new file appears whole and only if nothing took its place meanwhile; a replaced\n * one is swapped for its new version in one rename; a deleted one is unlinked.\n * 4. Record the change as applied, or as abandoned when the write failed and left the file as\n * it was. A change the record cannot confirm stops the session and throws\n * `EditUnconfirmedError`, never a success or a refusal.\n *\n * Every message names the file as the model wrote it, never by its place on the host.\n */\nexport class FileEditor {\n\tprivate readonly policy: AccessPolicy;\n\tprivate readonly session: WriteSession;\n\tprivate readonly maxFileBytes: number;\n\n\tconstructor({\n\t\tpolicy,\n\t\tsession,\n\t\tmaxFileBytes = defaultMaxFileBytes,\n\t}: {\n\t\tpolicy: AccessPolicy;\n\t\tsession: WriteSession;\n\t\tmaxFileBytes?: number;\n\t}) {\n\t\tthis.policy = policy;\n\t\tthis.session = session;\n\t\tthis.maxFileBytes = maxFileBytes;\n\t}\n\n\t/**\n\t * What `path` holds now, checked as a change to it would be: an edit format works its new\n\t * content out from this, and an edit tool compares it with the version the model read.\n\t */\n\tasync current(\n\t\tpath: string,\n\t\tsignal: AbortSignal,\n\t): Promise<{ target: AllowedPath; state: FileState; content?: Buffer }> {\n\t\tconst target = await this.policy.check(path, 'write', signal);\n\t\treturn { target, ...(await this.read(target.absolute, path)) };\n\t}\n\n\tasync prepare(change: FileChange, signal: AbortSignal): Promise<PreparedEdit> {\n\t\tconst { path } = change;\n\t\tconst target = await this.policy.check(path, 'write', signal);\n\t\tconst { state, content: previous } = await this.read(target.absolute, path);\n\n\t\tif (change.kind === 'create') {\n\t\t\tif (state.exists) {\n\t\t\t\tthrow new EditRefusedError(`\"${path}\" already exists; read it and change it instead`);\n\t\t\t}\n\t\t} else {\n\t\t\tif (!state.exists) throw new EditRefusedError(`\"${path}\" does not exist`);\n\t\t\tif (change.expected !== undefined && change.expected !== state.hash) {\n\t\t\t\tthrow new EditRefusedError(`\"${path}\" changed since it was read; read it again`);\n\t\t\t}\n\t\t}\n\n\t\tconst content = change.kind === 'delete' ? undefined : change.content;\n\t\tif (content && content.length > this.maxFileBytes) {\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`The new content of \"${path}\" is larger than ${String(this.maxFileBytes / 1024 / 1024)} MB`,\n\t\t\t);\n\t\t}\n\t\tif (content && state.exists && hashOf(content) === state.hash) {\n\t\t\tthrow new EditRefusedError(`\"${path}\" already holds that content; nothing to change`);\n\t\t}\n\n\t\t// Copies all the way down: what is approved must not change while the approval waits.\n\t\treturn Object.freeze({\n\t\t\tkind: change.kind,\n\t\t\tpath,\n\t\t\ttarget: Object.freeze({ ...target }),\n\t\t\tbefore: Object.freeze({ ...state }),\n\t\t\tcontent: content && Buffer.from(content),\n\t\t\tcontentHash: content && hashOf(content),\n\t\t\twidensIgnoreRules: isIgnoreFile(target.absolute) && widensIgnoreRules(previous, content),\n\t\t});\n\t}\n\n\t/** Makes the change once no other write of the run is under way, so none can come between. */\n\tapply(prepared: PreparedEdit, signal: AbortSignal): Promise<void> {\n\t\treturn this.session.exclusive(() => this.applyAlone(prepared, signal), signal);\n\t}\n\n\tprivate async applyAlone(prepared: PreparedEdit, signal: AbortSignal): Promise<void> {\n\t\tconst { path } = prepared;\n\t\tsignal.throwIfAborted();\n\t\t// A Buffer's bytes cannot be frozen: copied, then checked, so what is written is what was\n\t\t// approved, whatever happens to `prepared.content` from here on.\n\t\tconst content = prepared.content && Buffer.from(prepared.content);\n\t\tif ((content && hashOf(content)) !== prepared.contentHash) {\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`The content prepared for \"${path}\" changed after it was prepared; prepare the change again`,\n\t\t\t);\n\t\t}\n\t\tconst target = await this.policy.check(path, 'write', signal);\n\t\tif (target.absolute !== prepared.target.absolute) {\n\t\t\tthrow new EditRefusedError(`\"${path}\" now leads somewhere else; prepare the change again`);\n\t\t}\n\t\tconst current = await this.read(target.absolute, path);\n\t\tif (!sameState(current.state, prepared.before)) {\n\t\t\tthrow new EditRefusedError(`\"${path}\" changed after this change was prepared; read it again`);\n\t\t}\n\n\t\t// Checked again before the first effect: starting the journal takes the workspace.\n\t\tsignal.throwIfAborted();\n\t\tconst journal = await this.session.journalForChange();\n\t\tconst after = await this.record(journal, path, current.content, content, prepared.before);\n\t\tconst createdFolders =\n\t\t\tprepared.kind === 'create' ? await missingFolders(dirname(target.absolute)) : [];\n\t\tconst sequence = await this.journal(path, () =>\n\t\t\tjournal.prepare({\n\t\t\t\tpath: target.absolute,\n\t\t\t\tbefore: current.state,\n\t\t\t\tafter,\n\t\t\t\t...(createdFolders.length > 0 ? { createdFolders } : {}),\n\t\t\t}),\n\t\t);\n\n\t\t// The last moment to stop: past here the file changes.\n\t\tif (signal.aborted) {\n\t\t\tawait journal.abandoned(sequence);\n\t\t\tsignal.throwIfAborted();\n\t\t}\n\n\t\ttry {\n\t\t\tawait this.write(prepared, content, target.absolute);\n\t\t} catch (error) {\n\t\t\tawait this.settleFailedWrite({\n\t\t\t\tjournal,\n\t\t\t\tsequence,\n\t\t\t\tkind: prepared.kind,\n\t\t\t\tpath,\n\t\t\t\ttarget: target.absolute,\n\t\t\t\tcurrent,\n\t\t\t\tafter,\n\t\t\t\tcreatedFolders,\n\t\t\t\terror,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tawait this.markApplied(journal, sequence, path);\n\t}\n\n\t/**\n\t * Records a change that was made. If that fails, its prepared line still holds both states, so\n\t * an undo can tell; but the record cannot be trusted to continue, and the caller must know.\n\t */\n\tprivate async markApplied(journal: RunJournal, sequence: number, path: string): Promise<void> {\n\t\ttry {\n\t\t\tawait journal.applied(sequence);\n\t\t} catch (error) {\n\t\t\tthis.session.stop('Writing stopped: the harness could not record a change it made');\n\t\t\tthrow new EditUnconfirmedError(\n\t\t\t\t`\"${path}\" was changed, but the harness could not record it; writing has stopped for this run`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\t}\n\n\t/** What `path` holds now. Only a regular file within the size limit can be changed. */\n\tprivate async read(\n\t\tabsolute: string,\n\t\tpath: string,\n\t): Promise<{ state: FileState; content?: Buffer }> {\n\t\tlet stats;\n\t\ttry {\n\t\t\tstats = await lstat(absolute);\n\t\t} catch (error) {\n\t\t\tif (code(error) === 'ENOENT') return { state: { exists: false } };\n\t\t\tthrow new EditRefusedError(`Could not read \"${path}\" (${code(error)})`, { cause: error });\n\t\t}\n\t\tif (!stats.isFile()) throw new EditRefusedError(`\"${path}\" is not a regular file`);\n\t\tif (stats.size > this.maxFileBytes) {\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`\"${path}\" is larger than ${String(this.maxFileBytes / 1024 / 1024)} MB, too large to change`,\n\t\t\t);\n\t\t}\n\n\t\tlet content: Buffer;\n\t\ttry {\n\t\t\tcontent = await readFile(absolute);\n\t\t} catch (error) {\n\t\t\tthrow new EditRefusedError(`Could not read \"${path}\" (${code(error)})`, { cause: error });\n\t\t}\n\t\treturn {\n\t\t\tstate: { exists: true, hash: hashOf(content), mode: stats.mode & 0o7777 },\n\t\t\tcontent,\n\t\t};\n\t}\n\n\t/** Stores what the change replaces and what it writes, and returns the state it leaves. */\n\tprivate async record(\n\t\tjournal: RunJournal,\n\t\tpath: string,\n\t\tprevious: Buffer | undefined,\n\t\tcontent: Buffer | undefined,\n\t\tbefore: FileState,\n\t): Promise<FileState> {\n\t\tif (previous) await this.journal(path, () => journal.saveContent(previous));\n\t\tif (!content) return { exists: false };\n\n\t\tconst hash = await this.journal(path, () => journal.saveContent(content));\n\t\treturn { exists: true, hash, mode: before.exists ? before.mode : await getNewFileMode() };\n\t}\n\n\tprivate async journal<Result>(path: string, step: () => Promise<Result>): Promise<Result> {\n\t\ttry {\n\t\t\treturn await step();\n\t\t} catch (error) {\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`The harness could not record how to undo the change to \"${path}\", so it was not changed`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate async write(\n\t\t{ kind, before }: PreparedEdit,\n\t\tcontent: Buffer | undefined,\n\t\tabsolute: string,\n\t): Promise<void> {\n\t\tif (kind === 'delete') {\n\t\t\tawait unlink(absolute);\n\t\t\tawait syncDirectory(dirname(absolute));\n\t\t\treturn;\n\t\t}\n\n\t\tawait mkdir(dirname(absolute), { recursive: true });\n\t\t// Written beside the file, so the final step is one rename or link on the same disk.\n\t\tconst temporary = join(\n\t\t\tdirname(absolute),\n\t\t\t`.${basename(absolute)}.${randomBytes(6).toString('hex')}.mikode-tmp`,\n\t\t);\n\t\ttry {\n\t\t\tconst handle = await open(temporary, 'wx');\n\t\t\ttry {\n\t\t\t\tawait handle.writeFile(content ?? Buffer.alloc(0));\n\t\t\t\tawait handle.sync();\n\t\t\t} finally {\n\t\t\t\tawait handle.close();\n\t\t\t}\n\t\t\tif (before.exists) await chmod(temporary, before.mode);\n\n\t\t\tif (kind === 'create') {\n\t\t\t\t// A link fails if something appeared at the path meanwhile; a rename would replace it.\n\t\t\t\tawait link(temporary, absolute);\n\t\t\t} else {\n\t\t\t\tawait rename(temporary, absolute);\n\t\t\t}\n\t\t} finally {\n\t\t\tawait rm(temporary, { force: true });\n\t\t}\n\t\tawait syncDirectory(dirname(absolute));\n\t}\n\n\t/**\n\t * A write threw. If the file still holds what it held, nothing happened and the change is\n\t * abandoned; if it holds the new state, the change happened anyway. Anything else cannot be\n\t * explained, so the session stops writing.\n\t */\n\tprivate async settleFailedWrite({\n\t\tjournal,\n\t\tsequence,\n\t\tkind,\n\t\tpath,\n\t\ttarget,\n\t\tcurrent,\n\t\tafter,\n\t\tcreatedFolders,\n\t\terror,\n\t}: {\n\t\tjournal: RunJournal;\n\t\tsequence: number;\n\t\tkind: PreparedEdit['kind'];\n\t\tpath: string;\n\t\ttarget: string;\n\t\tcurrent: { state: FileState };\n\t\tafter: FileState;\n\t\tcreatedFolders: string[];\n\t\terror: unknown;\n\t}): Promise<void> {\n\t\tconst now = await this.read(target, path).catch(() => undefined);\n\t\tif (now && sameState(now.state, after)) {\n\t\t\tawait this.markApplied(journal, sequence, path);\n\t\t\treturn;\n\t\t}\n\t\t// A link that found the path taken made no change: what is there now is someone else's.\n\t\tconst appeared = kind === 'create' && code(error) === 'EEXIST';\n\t\tif (appeared || (now && sameState(now.state, current.state))) {\n\t\t\t// Innermost first, and only while empty: anything else in them is not this run's.\n\t\t\tfor (const folder of [...createdFolders].reverse()) {\n\t\t\t\tawait rmdir(folder).catch(() => undefined);\n\t\t\t}\n\t\t\tawait journal.abandoned(sequence).catch(() => undefined);\n\t\t\tthrow new EditRefusedError(\n\t\t\t\tappeared\n\t\t\t\t\t? `\"${path}\" appeared while it was being created; read it before changing it`\n\t\t\t\t\t: `Could not write \"${path}\" (${code(error)}); it was not changed`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\n\t\tthis.session.stop(\n\t\t\t`Writing stopped: the harness could not tell whether \"${path}\" was changed; ask the user to check it`,\n\t\t);\n\t\tthrow new EditUnconfirmedError(\n\t\t\t`Could not tell whether \"${path}\" was changed (${code(error)}); writing has stopped for this run`,\n\t\t\t{ cause: error },\n\t\t);\n\t}\n}\n"]}
|
|
@@ -34,6 +34,7 @@ export declare class FileFormatter {
|
|
|
34
34
|
* stopped. Writing stops for the run then, as after such a command.
|
|
35
35
|
*/
|
|
36
36
|
format(absolute: string, session: WriteSession, signal: AbortSignal): Promise<string | undefined>;
|
|
37
|
+
private formatAlone;
|
|
37
38
|
/** The file as the formatter's last argument: relative to where it runs, never read as a flag. */
|
|
38
39
|
private argumentFor;
|
|
39
40
|
}
|
|
@@ -45,7 +45,11 @@ export class FileFormatter {
|
|
|
45
45
|
* @throws {EditUnconfirmedError} when what it changed could not be recorded, or it could not be
|
|
46
46
|
* stopped. Writing stops for the run then, as after such a command.
|
|
47
47
|
*/
|
|
48
|
-
|
|
48
|
+
format(absolute, session, signal) {
|
|
49
|
+
// Alone, as a command runs, so no other write of the run comes between its captures.
|
|
50
|
+
return session.exclusive(() => this.formatAlone(absolute, session, signal), signal);
|
|
51
|
+
}
|
|
52
|
+
async formatAlone(absolute, session, signal) {
|
|
49
53
|
signal.throwIfAborted();
|
|
50
54
|
let checkpoint;
|
|
51
55
|
try {
|
|
@@ -1 +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"]}
|
|
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\tformat(\n\t\tabsolute: string,\n\t\tsession: WriteSession,\n\t\tsignal: AbortSignal,\n\t): Promise<string | undefined> {\n\t\t// Alone, as a command runs, so no other write of the run comes between its captures.\n\t\treturn session.exclusive(() => this.formatAlone(absolute, session, signal), signal);\n\t}\n\n\tprivate async formatAlone(\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"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RunOptions } from '#src/agent/domain/agent';
|
|
2
|
+
import type { RecoverableError, UnrecoverableError } from '#src/shared/domain/errors';
|
|
3
|
+
import type { ToolCallContext } from '../domain/readRegistry.ts';
|
|
4
|
+
/** A subagent's answer as the agent that called it reads it, cut where a read would be. */
|
|
5
|
+
export declare function cutAnswer(answer: string): string;
|
|
6
|
+
/** Why a subagent failed, for the agent that called it. */
|
|
7
|
+
export declare function failureOf(error: RecoverableError | UnrecoverableError): string;
|
|
8
|
+
/**
|
|
9
|
+
* The options of a subagent a call starts: within the run that made the call, one delegation
|
|
10
|
+
* deeper, and narrated as that call's `subagent`, so the person watching can tell it apart.
|
|
11
|
+
*/
|
|
12
|
+
export declare function subagentOptions(call: ToolCallContext, { tool, name, subagent, task, options, }: {
|
|
13
|
+
tool: string;
|
|
14
|
+
name: string;
|
|
15
|
+
subagent: number;
|
|
16
|
+
task: string;
|
|
17
|
+
/** What else it runs with, such as its signal. */
|
|
18
|
+
options: RunOptions;
|
|
19
|
+
}): RunOptions;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { withDelegation, withRunContext } from '#src/agent/domain/runContext';
|
|
2
|
+
// An answer is read whole, like a read, which returns at most 16 KB.
|
|
3
|
+
const maxAnswerCharacters = 16_000;
|
|
4
|
+
/** A subagent's answer as the agent that called it reads it, cut where a read would be. */
|
|
5
|
+
export function cutAnswer(answer) {
|
|
6
|
+
if (answer.length <= maxAnswerCharacters)
|
|
7
|
+
return answer;
|
|
8
|
+
return `${answer.slice(0, maxAnswerCharacters)}\n[The answer was cut at ${String(maxAnswerCharacters)} characters.]`;
|
|
9
|
+
}
|
|
10
|
+
/** Why a subagent failed, for the agent that called it. */
|
|
11
|
+
export function failureOf(error) {
|
|
12
|
+
return typeof error.cause === 'string' ? `${error.message}: ${error.cause}` : error.message;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The options of a subagent a call starts: within the run that made the call, one delegation
|
|
16
|
+
* deeper, and narrated as that call's `subagent`, so the person watching can tell it apart.
|
|
17
|
+
*/
|
|
18
|
+
export function subagentOptions(call, { tool, name, subagent, task, options, }) {
|
|
19
|
+
const onProgress = (event) => {
|
|
20
|
+
call.onProgress({ type: 'subagent', callId: call.callId, subagent, name, task, event });
|
|
21
|
+
};
|
|
22
|
+
return withDelegation(withRunContext({ ...options, onProgress }, call.run), {
|
|
23
|
+
tool,
|
|
24
|
+
callId: call.callId,
|
|
25
|
+
subagent,
|
|
26
|
+
depth: call.depth + 1,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=subagents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subagents.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/subagents.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAI9E,qEAAqE;AACrE,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAEnC,2FAA2F;AAC3F,MAAM,UAAU,SAAS,CAAC,MAAc;IACvC,IAAI,MAAM,CAAC,MAAM,IAAI,mBAAmB;QAAE,OAAO,MAAM,CAAC;IACxD,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,mBAAmB,CAAC,4BAA4B,MAAM,CAAC,mBAAmB,CAAC,eAAe,CAAC;AACtH,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,SAAS,CAAC,KAA4C;IACrE,OAAO,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,OAAO,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;AAC7F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC9B,IAAqB,EACrB,EACC,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,IAAI,EACJ,OAAO,GAQP;IAED,MAAM,UAAU,GAAG,CAAC,KAAoB,EAAE,EAAE;QAC3C,IAAI,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzF,CAAC,CAAC;IACF,OAAO,cAAc,CAAC,cAAc,CAAC,EAAE,GAAG,OAAO,EAAE,UAAU,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE;QAC3E,IAAI;QACJ,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,QAAQ;QACR,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC;KACrB,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import type { ProgressEvent, RunOptions } from '#src/agent/domain/agent';\nimport { withDelegation, withRunContext } from '#src/agent/domain/runContext';\nimport type { RecoverableError, UnrecoverableError } from '#src/shared/domain/errors';\nimport type { ToolCallContext } from '../domain/readRegistry.ts';\n\n// An answer is read whole, like a read, which returns at most 16 KB.\nconst maxAnswerCharacters = 16_000;\n\n/** A subagent's answer as the agent that called it reads it, cut where a read would be. */\nexport function cutAnswer(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\n/** Why a subagent failed, for the agent that called it. */\nexport function failureOf(error: RecoverableError | UnrecoverableError): string {\n\treturn typeof error.cause === 'string' ? `${error.message}: ${error.cause}` : error.message;\n}\n\n/**\n * The options of a subagent a call starts: within the run that made the call, one delegation\n * deeper, and narrated as that call's `subagent`, so the person watching can tell it apart.\n */\nexport function subagentOptions(\n\tcall: ToolCallContext,\n\t{\n\t\ttool,\n\t\tname,\n\t\tsubagent,\n\t\ttask,\n\t\toptions,\n\t}: {\n\t\ttool: string;\n\t\tname: string;\n\t\tsubagent: number;\n\t\ttask: string;\n\t\t/** What else it runs with, such as its signal. */\n\t\toptions: RunOptions;\n\t},\n): RunOptions {\n\tconst onProgress = (event: ProgressEvent) => {\n\t\tcall.onProgress({ type: 'subagent', callId: call.callId, subagent, name, task, event });\n\t};\n\treturn withDelegation(withRunContext({ ...options, onProgress }, call.run), {\n\t\ttool,\n\t\tcallId: call.callId,\n\t\tsubagent,\n\t\tdepth: call.depth + 1,\n\t});\n}\n"]}
|
|
@@ -13,6 +13,7 @@ export declare class WriteSession {
|
|
|
13
13
|
private journal;
|
|
14
14
|
private changes;
|
|
15
15
|
private stopped;
|
|
16
|
+
private writing;
|
|
16
17
|
constructor({ store, maxChanges, onStart, onDiscard, }: {
|
|
17
18
|
store: RecoveryStore;
|
|
18
19
|
maxChanges?: number;
|
|
@@ -23,6 +24,13 @@ export declare class WriteSession {
|
|
|
23
24
|
});
|
|
24
25
|
/** The journal for one more change, counted against the run's limit. */
|
|
25
26
|
journalForChange(): Promise<RunJournal>;
|
|
27
|
+
/**
|
|
28
|
+
* Runs `write` once every write before it has ended, so agents of one run that work at once
|
|
29
|
+
* never change files together: each change is read, recorded and made alone, and a command
|
|
30
|
+
* runs with no other write while the files are captured around it. A cancellation stops the
|
|
31
|
+
* wait.
|
|
32
|
+
*/
|
|
33
|
+
exclusive<Result>(write: () => Promise<Result>, signal: AbortSignal): Promise<Result>;
|
|
26
34
|
/** Refuses every further write, for this reason. The first reason given is kept. */
|
|
27
35
|
stop(reason: string): void;
|
|
28
36
|
/** The run's id once it has written, which is what an undo needs. */
|
|
@@ -16,6 +16,8 @@ export class WriteSession {
|
|
|
16
16
|
journal;
|
|
17
17
|
changes = 0;
|
|
18
18
|
stopped;
|
|
19
|
+
// The end of the last write waiting or running, which the next one starts after.
|
|
20
|
+
writing = Promise.resolve();
|
|
19
21
|
constructor({ store, maxChanges = defaultMaxChanges, onStart, onDiscard, }) {
|
|
20
22
|
this.store = store;
|
|
21
23
|
this.maxChanges = maxChanges;
|
|
@@ -50,6 +52,28 @@ export class WriteSession {
|
|
|
50
52
|
this.changes++;
|
|
51
53
|
return journal;
|
|
52
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Runs `write` once every write before it has ended, so agents of one run that work at once
|
|
57
|
+
* never change files together: each change is read, recorded and made alone, and a command
|
|
58
|
+
* runs with no other write while the files are captured around it. A cancellation stops the
|
|
59
|
+
* wait.
|
|
60
|
+
*/
|
|
61
|
+
async exclusive(write, signal) {
|
|
62
|
+
const before = this.writing;
|
|
63
|
+
let release;
|
|
64
|
+
this.writing = new Promise(resolve => {
|
|
65
|
+
release = resolve;
|
|
66
|
+
});
|
|
67
|
+
try {
|
|
68
|
+
await untilAborted(before, signal);
|
|
69
|
+
signal.throwIfAborted();
|
|
70
|
+
return await write();
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
// Released even when cancelled while waiting: the writes after it wait for those before.
|
|
74
|
+
void before.then(release, release);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
53
77
|
/** Refuses every further write, for this reason. The first reason given is kept. */
|
|
54
78
|
stop(reason) {
|
|
55
79
|
this.stopped ??= reason;
|
|
@@ -78,4 +102,17 @@ export class WriteSession {
|
|
|
78
102
|
}
|
|
79
103
|
}
|
|
80
104
|
}
|
|
105
|
+
/** Waits for `promise`, settled either way, or until `signal` aborts, whichever comes first. */
|
|
106
|
+
function untilAborted(promise, signal) {
|
|
107
|
+
if (signal.aborted)
|
|
108
|
+
return Promise.resolve();
|
|
109
|
+
return new Promise(resolve => {
|
|
110
|
+
const done = () => {
|
|
111
|
+
signal.removeEventListener('abort', done);
|
|
112
|
+
resolve();
|
|
113
|
+
};
|
|
114
|
+
signal.addEventListener('abort', done, { once: true });
|
|
115
|
+
promise.then(done, done);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
81
118
|
//# sourceMappingURL=writeSession.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writeSession.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/writeSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAIN,kBAAkB,GAClB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1D,iFAAiF;AACjF,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,OAAO,YAAY;IACP,KAAK,CAAgB;IACrB,UAAU,CAAS;IACnB,OAAO,CAAwC;IAC/C,SAAS,CAA2B;IAC7C,OAAO,CAAkC;IACzC,OAAO,GAAG,CAAC,CAAC;IACZ,OAAO,CAAqB;
|
|
1
|
+
{"version":3,"file":"writeSession.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/writeSession.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype RecoveryStore,\n\ttype RunJournal,\n\ttype RunStatus,\n\tWorkspaceBusyError,\n} from '#src/recovery/domain/recoveryStore';\nimport { EditRefusedError } from '../domain/fileEdits.ts';\n\n// Far more than a task needs; a run past it is more likely looping than working.\nconst defaultMaxChanges = 500;\n\n/**\n * Everything one top-level run writes, across all its roles and rounds. Its journal starts on\n * the first write, so a run that only reads never holds the workspace, and ends with the run.\n * Once a change could not be confirmed, the session refuses every further write: the record of\n * what the run did must stay complete.\n */\nexport class WriteSession {\n\tprivate readonly store: RecoveryStore;\n\tprivate readonly maxChanges: number;\n\tprivate readonly onStart: ((runId: string) => void) | undefined;\n\tprivate readonly onDiscard: (() => void) | undefined;\n\tprivate journal: Promise<RunJournal> | undefined;\n\tprivate changes = 0;\n\tprivate stopped: string | undefined;\n\t// The end of the last write waiting or running, which the next one starts after.\n\tprivate writing: Promise<unknown> = Promise.resolve();\n\n\tconstructor({\n\t\tstore,\n\t\tmaxChanges = defaultMaxChanges,\n\t\tonStart,\n\t\tonDiscard,\n\t}: {\n\t\tstore: RecoveryStore;\n\t\tmaxChanges?: number;\n\t\t/** Told the run's id once its journal has started, before its first change is recorded. */\n\t\tonStart?: (runId: string) => void;\n\t\t/** Told when the run ended having changed nothing, so the history no longer has it. */\n\t\tonDiscard?: () => void;\n\t}) {\n\t\tthis.store = store;\n\t\tthis.maxChanges = maxChanges;\n\t\tthis.onStart = onStart;\n\t\tthis.onDiscard = onDiscard;\n\t}\n\n\t/** The journal for one more change, counted against the run's limit. */\n\tasync journalForChange(): Promise<RunJournal> {\n\t\tif (this.stopped !== undefined) throw new EditRefusedError(this.stopped);\n\t\tif (this.changes >= this.maxChanges) {\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`This run has made ${String(this.maxChanges)} changes, the most one run may make`,\n\t\t\t);\n\t\t}\n\n\t\tthis.journal ??= this.store.startRun().then(started => {\n\t\t\tthis.onStart?.(started.runId);\n\t\t\treturn started;\n\t\t});\n\t\tlet journal: RunJournal;\n\t\ttry {\n\t\t\tjournal = await this.journal;\n\t\t} catch (error) {\n\t\t\t// A failed start is not kept, so the next change tries again.\n\t\t\tthis.journal = undefined;\n\t\t\tif (error instanceof WorkspaceBusyError) {\n\t\t\t\tthrow new EditRefusedError(\n\t\t\t\t\t'Another run, or a move through its history, holds this workspace, so nothing was changed',\n\t\t\t\t\t{\n\t\t\t\t\t\tcause: error,\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t'The harness could not start recording this run, so nothing was changed',\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\t\tthis.changes++;\n\t\treturn journal;\n\t}\n\n\t/**\n\t * Runs `write` once every write before it has ended, so agents of one run that work at once\n\t * never change files together: each change is read, recorded and made alone, and a command\n\t * runs with no other write while the files are captured around it. A cancellation stops the\n\t * wait.\n\t */\n\tasync exclusive<Result>(write: () => Promise<Result>, signal: AbortSignal): Promise<Result> {\n\t\tconst before = this.writing;\n\t\tlet release!: () => void;\n\t\tthis.writing = new Promise<void>(resolve => {\n\t\t\trelease = resolve;\n\t\t});\n\t\ttry {\n\t\t\tawait untilAborted(before, signal);\n\t\t\tsignal.throwIfAborted();\n\t\t\treturn await write();\n\t\t} finally {\n\t\t\t// Released even when cancelled while waiting: the writes after it wait for those before.\n\t\t\tvoid before.then(release, release);\n\t\t}\n\t}\n\n\t/** Refuses every further write, for this reason. The first reason given is kept. */\n\tstop(reason: string): void {\n\t\tthis.stopped ??= reason;\n\t}\n\n\t/** The run's id once it has written, which is what an undo needs. */\n\tasync runId(): Promise<string | undefined> {\n\t\tconst journal = await this.journal?.catch(() => undefined);\n\t\treturn journal?.runId;\n\t}\n\n\t/**\n\t * Ends the run's record and releases the workspace. A run that never wrote has nothing to end,\n\t * and one whose every change was abandoned leaves the history.\n\t */\n\tasync finish(status: Exclude<RunStatus, 'running' | 'interrupted'>): Promise<void> {\n\t\tconst journal = await this.journal?.catch(() => undefined);\n\t\tthis.journal = undefined;\n\t\tif (!journal) return;\n\t\ttry {\n\t\t\tawait journal.finish(status);\n\t\t} finally {\n\t\t\t// Even when finishing failed: a run that changed nothing is out of the history either way.\n\t\t\tif (!journal.changed) this.onDiscard?.();\n\t\t}\n\t}\n}\n\n/** Waits for `promise`, settled either way, or until `signal` aborts, whichever comes first. */\nfunction untilAborted(promise: Promise<unknown>, signal: AbortSignal): Promise<void> {\n\tif (signal.aborted) return Promise.resolve();\n\treturn new Promise(resolve => {\n\t\tconst done = () => {\n\t\t\tsignal.removeEventListener('abort', done);\n\t\t\tresolve();\n\t\t};\n\t\tsignal.addEventListener('abort', done, { once: true });\n\t\tpromise.then(done, done);\n\t});\n}\n"]}
|