@mikode13/harness 9.3.0 → 10.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 +66 -8
- package/dist/agent/domain/agent.d.ts +11 -5
- 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/historyFollower.d.ts +1 -1
- package/dist/engines/domain/model/historyFollower.js +14 -3
- package/dist/engines/domain/model/historyFollower.js.map +1 -1
- package/dist/engines/domain/model/llmAgent.d.ts +19 -3
- package/dist/engines/domain/model/llmAgent.js +141 -51
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/factory/infrastructure/agentLLMFactory.d.ts +6 -2
- package/dist/factory/infrastructure/agentLLMFactory.js +4 -4
- 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/recovery/domain/history.d.ts +3 -2
- package/dist/recovery/domain/history.js +6 -2
- package/dist/recovery/domain/history.js.map +1 -1
- package/dist/recovery/domain/recoveryStore.d.ts +47 -5
- package/dist/recovery/domain/recoveryStore.js.map +1 -1
- package/dist/recovery/domain/runChanges.js +11 -2
- package/dist/recovery/domain/runChanges.js.map +1 -1
- package/dist/recovery/infrastructure/fileRecoveryStore.d.ts +41 -9
- package/dist/recovery/infrastructure/fileRecoveryStore.js +217 -65
- package/dist/recovery/infrastructure/fileRecoveryStore.js.map +1 -1
- package/dist/tools/domain/preparedCall.d.ts +14 -0
- package/dist/tools/domain/preparedCall.js +12 -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 +61 -0
- package/dist/tools/infrastructure/agentAsTool.js +144 -0
- package/dist/tools/infrastructure/agentAsTool.js.map +1 -0
- package/dist/tools/infrastructure/commandCheckpoint.d.ts +3 -2
- package/dist/tools/infrastructure/commandCheckpoint.js +21 -9
- package/dist/tools/infrastructure/commandCheckpoint.js.map +1 -1
- package/dist/tools/infrastructure/commandTool.js +7 -3
- package/dist/tools/infrastructure/commandTool.js.map +1 -1
- package/dist/tools/infrastructure/exploreTool.js +9 -22
- 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/fileTools.d.ts +1 -1
- package/dist/tools/infrastructure/fileTools.js +3 -3
- package/dist/tools/infrastructure/fileTools.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/workspaceWrites.d.ts +8 -5
- package/dist/tools/infrastructure/workspaceWrites.js +19 -7
- package/dist/tools/infrastructure/workspaceWrites.js.map +1 -1
- 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 { PreparingTool } from '../domain/preparedCall.ts';\nimport type { ToolCallContext } from '../domain/readRegistry.ts';\nimport { definePreparingTool } 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 * 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\t// Within the run that made the call, so the explorers' reads belong to it.\n\t\t\tconst options = subagentOptions(call, {\n\t\t\t\ttool: 'explore',\n\t\t\t\tname: 'explorer',\n\t\t\t\tsubagent,\n\t\t\t\ttask: question,\n\t\t\t\toptions: { signal },\n\t\t\t});\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${cutAnswer(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"]}
|
|
@@ -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"]}
|
|
@@ -59,7 +59,7 @@ export declare function stateDirectoryOf({ stateDirectory }: WorkspaceOptions):
|
|
|
59
59
|
* @throws {InvalidAgentConfigError} when the workspace has no root.
|
|
60
60
|
* @throws {UnrecoverableError} when the store cannot be opened.
|
|
61
61
|
*/
|
|
62
|
-
export declare function openStore(workspace: WorkspaceOptions, directory: string): Promise<RecoveryStore>;
|
|
62
|
+
export declare function openStore(workspace: WorkspaceOptions, directory: string, logger: ILogger): Promise<RecoveryStore>;
|
|
63
63
|
/**
|
|
64
64
|
* Opens what the file tools of one workspace share. With `formatter`, its edits format each file
|
|
65
65
|
* they write.
|
|
@@ -38,12 +38,12 @@ export async function stateDirectoryOf({ stateDirectory }) {
|
|
|
38
38
|
* @throws {InvalidAgentConfigError} when the workspace has no root.
|
|
39
39
|
* @throws {UnrecoverableError} when the store cannot be opened.
|
|
40
40
|
*/
|
|
41
|
-
export async function openStore(workspace, directory) {
|
|
41
|
+
export async function openStore(workspace, directory, logger) {
|
|
42
42
|
const [first] = workspace.roots;
|
|
43
43
|
if (!first)
|
|
44
44
|
throw new InvalidAgentConfigError('A workspace needs at least one root');
|
|
45
45
|
try {
|
|
46
|
-
return await FileRecoveryStore.open({ root: resolve(first.path), directory });
|
|
46
|
+
return await FileRecoveryStore.open({ root: resolve(first.path), directory, logger });
|
|
47
47
|
}
|
|
48
48
|
catch (error) {
|
|
49
49
|
throw new UnrecoverableError('The workspace history could not be opened', {
|
|
@@ -67,7 +67,7 @@ export async function openWorkspace(workspace, logger, { formatter } = {}) {
|
|
|
67
67
|
// The history holds the source a run replaced: no tool may read or write it.
|
|
68
68
|
protectedPaths: [stateDirectory],
|
|
69
69
|
});
|
|
70
|
-
const store = await openStore(workspace, stateDirectory);
|
|
70
|
+
const store = await openStore(workspace, stateDirectory, logger);
|
|
71
71
|
// Real, as the policy's paths are, so a file's path can be judged by name.
|
|
72
72
|
const root = await realpath(resolve(roots[0]?.path ?? '.'));
|
|
73
73
|
const inner = await chooseWorkspace({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fileTools.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/fileTools.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir, realpath } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport type { ToolDefinition } from '#src/llm/domain/tool';\nimport type { RecoveryStore } from '#src/recovery/domain/recoveryStore';\nimport {\n\tdefaultStateDirectory,\n\tFileRecoveryStore,\n} from '#src/recovery/infrastructure/fileRecoveryStore';\nimport { InvalidAgentConfigError, UnrecoverableError } from '#src/shared/domain/errors';\nimport type { ILogger } from '#src/shared/domain/logger';\nimport { describeFailure } from '#src/shared/domain/providerFailure';\nimport type { AccessPolicy, WorkspaceRoot } from '../domain/accessPolicy.ts';\nimport type { Commands, Formatter } from '../domain/commands.ts';\nimport type { AgentTool } from '../domain/preparedCall.ts';\nimport type { Workspace } from '../domain/workspace.ts';\nimport type { CoveredFiles } from './commandCheckpoint.ts';\nimport { createCommandTool } from './commandTool.ts';\nimport { chooseWorkspace } from './createWorkspace.ts';\nimport { createApplyPatchTool, createDeleteFileTool, createTextEditorTool } from './editFormats.ts';\nimport { FileFormatter } from './fileFormatter.ts';\nimport { GitIgnoreRules } from './gitIgnoreRules.ts';\nimport { PolicyWorkspace } from './policyWorkspace.ts';\nimport { RootsAccessPolicy, type SecretRules } from './rootsAccessPolicy.ts';\nimport { createTrackedReadFile, createTrackedReadFiles } from './trackedReadFile.ts';\nimport { createWorkspaceTools } from './workspaceTools.ts';\nimport { WorkspaceWrites } from './workspaceWrites.ts';\n\n/** The folders an agent works on, and what it may do in each. */\nexport interface WorkspaceOptions {\n\t/**\n\t * Each folder `read` or `write`. The first is where a relative path starts, and the one whose\n\t * history of runs is kept; a write to another root is recorded in it too.\n\t */\n\troots: WorkspaceRoot[];\n\t/** More files to keep closed as secrets, and exact files to open. */\n\tsecrets?: SecretRules;\n\t/**\n\t * Where the history of runs is kept, relative to the working directory if not absolute.\n\t * Defaults to the platform's state directory. No tool may reach it, even inside a root.\n\t */\n\tstateDirectory?: string;\n}\n\ndeclare const fileToolBrand: unique symbol;\n\n/**\n * A tool the harness builds for files, from `createFileTools`. It goes to an agent from\n * `createLLMAgent` beside the consumer's own tools; what it does inside is the harness's.\n */\nexport type FileTool = ToolDefinition & { readonly [fileToolBrand]: true };\n\n/** What the file tools of one workspace share: one policy, one history, one record of writes. */\nexport interface OpenedWorkspace {\n\treadonly roots: WorkspaceRoot[];\n\treadonly policy: AccessPolicy;\n\treadonly store: RecoveryStore;\n\t/** The read side, with the policy's word on every path. */\n\treadonly read: Workspace;\n\treadonly writes: WorkspaceWrites;\n\treadonly writable: boolean;\n\t/** The real path of the first root. */\n\treadonly base: string;\n\t/** What a command's checkpoint captures and compares: see `CoveredFiles`. */\n\treadonly covered: CoveredFiles;\n}\n\n/**\n * Where a workspace's history is kept, as one real path: made if missing, then resolved, links\n * included. The store and the access policy must name the same folder, or a tool could reach the\n * history the policy means to protect. A relative path is from the working directory.\n *\n * @throws {UnrecoverableError} when the folder cannot be made or reached.\n */\nexport async function stateDirectoryOf({ stateDirectory }: WorkspaceOptions): Promise<string> {\n\tconst directory = resolve(stateDirectory ?? defaultStateDirectory());\n\ttry {\n\t\tawait mkdir(directory, { recursive: true, mode: 0o700 });\n\t\treturn await realpath(directory);\n\t} catch (error) {\n\t\tthrow new UnrecoverableError('The folder for the workspace history could not be made', {\n\t\t\tcause: describeFailure(error),\n\t\t});\n\t}\n}\n\n/**\n * The recovery store of a workspace, at its first root.\n *\n * @throws {InvalidAgentConfigError} when the workspace has no root.\n * @throws {UnrecoverableError} when the store cannot be opened.\n */\nexport async function openStore(\n\tworkspace: WorkspaceOptions,\n\tdirectory: string,\n): Promise<RecoveryStore> {\n\tconst [first] = workspace.roots;\n\tif (!first) throw new InvalidAgentConfigError('A workspace needs at least one root');\n\ttry {\n\t\treturn await FileRecoveryStore.open({ root: resolve(first.path), directory });\n\t} catch (error) {\n\t\tthrow new UnrecoverableError('The workspace history could not be opened', {\n\t\t\tcause: describeFailure(error),\n\t\t});\n\t}\n}\n\n/**\n * Opens what the file tools of one workspace share. With `formatter`, its edits format each file\n * they write.\n *\n * @throws {InvalidAgentConfigError} for a workspace with no root, or an invalid formatter.\n */\nexport async function openWorkspace(\n\tworkspace: WorkspaceOptions,\n\tlogger: ILogger,\n\t{ formatter }: { formatter?: Formatter } = {},\n): Promise<OpenedWorkspace> {\n\tconst { roots, secrets } = workspace;\n\tconst stateDirectory = await stateDirectoryOf(workspace);\n\tconst policy = await RootsAccessPolicy.create({\n\t\troots,\n\t\t...(secrets ? { secrets } : {}),\n\t\tignoreRules: new GitIgnoreRules(),\n\t\t// The history holds the source a run replaced: no tool may read or write it.\n\t\tprotectedPaths: [stateDirectory],\n\t});\n\tconst store = await openStore(workspace, stateDirectory);\n\t// Real, as the policy's paths are, so a file's path can be judged by name.\n\tconst root = await realpath(resolve(roots[0]?.path ?? '.'));\n\tconst inner = await chooseWorkspace({\n\t\troot,\n\t\tlogger,\n\t\t// Left out before a search counts or stores a match, so no total, limit or error can tell\n\t\t// that a secret matched. The policy still decides each path after it.\n\t\thidden: path => policy.hidesFromReading(join(root, path)),\n\t});\n\tconst read = new PolicyWorkspace({ inner, policy });\n\tconst fileFormatter =\n\t\tformatter && (await FileFormatter.create({ formatter, policy, base: root }));\n\treturn {\n\t\troots,\n\t\tpolicy,\n\t\tstore,\n\t\tread,\n\t\twrites: new WorkspaceWrites({\n\t\t\tpolicy,\n\t\t\tstore,\n\t\t\t...(fileFormatter ? { formatter: fileFormatter } : {}),\n\t\t}),\n\t\twritable: roots.some(({ access }) => access === 'write'),\n\t\tbase: root,\n\t\tcovered: coveredFiles(roots, policy, logger),\n\t};\n}\n\n/**\n * Lists every file of the `write` roots that `.gitignore` does not exclude, less what the policy\n * closes to writing by name, before anything is read. Each root is listed by its own program,\n * opened on the first listing.\n */\nfunction coveredFiles(\n\troots: WorkspaceRoot[],\n\tpolicy: RootsAccessPolicy,\n\tlogger: ILogger,\n): CoveredFiles {\n\tlet listers: Promise<{ root: string; workspace: Workspace }[]> | undefined;\n\tconst open = () =>\n\t\tPromise.all(\n\t\t\troots\n\t\t\t\t.filter(({ access }) => access === 'write')\n\t\t\t\t.map(async ({ path }) => {\n\t\t\t\t\tconst root = await realpath(resolve(path));\n\t\t\t\t\tconst workspace = await chooseWorkspace({\n\t\t\t\t\t\troot,\n\t\t\t\t\t\tlogger,\n\t\t\t\t\t\thidden: relativePath => policy.closedToWriting(join(root, relativePath)),\n\t\t\t\t\t});\n\t\t\t\t\treturn { root, workspace };\n\t\t\t\t}),\n\t\t);\n\n\treturn async signal => {\n\t\tlisters ??= open().catch((error: unknown) => {\n\t\t\t// Not kept: the next command tries again.\n\t\t\tlisters = undefined;\n\t\t\tthrow error;\n\t\t});\n\t\tconst files = new Set<string>();\n\t\tfor (const { root, workspace } of await listers) {\n\t\t\tconst listed = await workspace.listFiles({ limit: Number.MAX_SAFE_INTEGER }, signal);\n\t\t\tfor (const path of listed.files) files.add(join(root, path));\n\t\t}\n\t\treturn [...files];\n\t};\n}\n\n/**\n * `runCommand` over an opened workspace: see `createCommandTool`.\n *\n * @throws {InvalidAgentConfigError} for an invalid configuration, or on Windows.\n */\nexport function commandToolFor(workspace: OpenedWorkspace, commands: Commands): Promise<AgentTool> {\n\tconst { policy, writes, covered, base } = workspace;\n\treturn createCommandTool({ commands, policy, writes, covered, base });\n}\n\n/**\n * The file tools of a provider's executor, over one workspace. Every one reads through the access\n * policy, and an edit starts only from a version the conversation read.\n *\n * - OpenAI: `listFiles`, `searchText`, `readFile`, `readFiles`, and its native `apply_patch`.\n * - Anthropic: `listFiles`, `searchText`, its native text editor, whose `view` reads, and\n * `delete_file`.\n *\n * Without a `write` root they only read: `readFile` and `readFiles` in place of the editor and\n * the patch.\n */\nexport function fileToolsFor(\n\tprovider: 'anthropic' | 'openai',\n\tworkspace: OpenedWorkspace,\n): AgentTool[] {\n\tconst { policy, read, writes, writable } = workspace;\n\tconst [listFiles, searchText] = createWorkspaceTools(read).filter(\n\t\ttool => tool.name === 'listFiles' || tool.name === 'searchText',\n\t);\n\tconst finding: AgentTool[] = [listFiles, searchText].filter(tool => tool !== undefined);\n\tconst reading = [createTrackedReadFile(policy), createTrackedReadFiles(policy)];\n\tif (!writable) return [...finding, ...reading];\n\tif (provider === 'openai') return [...finding, ...reading, createApplyPatchTool(writes)];\n\treturn [\n\t\t...finding,\n\t\tcreateTextEditorTool({ writes, policy, workspace: read }),\n\t\tcreateDeleteFileTool(writes),\n\t];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fileTools.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/fileTools.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir, realpath } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport type { ToolDefinition } from '#src/llm/domain/tool';\nimport type { RecoveryStore } from '#src/recovery/domain/recoveryStore';\nimport {\n\tdefaultStateDirectory,\n\tFileRecoveryStore,\n} from '#src/recovery/infrastructure/fileRecoveryStore';\nimport { InvalidAgentConfigError, UnrecoverableError } from '#src/shared/domain/errors';\nimport type { ILogger } from '#src/shared/domain/logger';\nimport { describeFailure } from '#src/shared/domain/providerFailure';\nimport type { AccessPolicy, WorkspaceRoot } from '../domain/accessPolicy.ts';\nimport type { Commands, Formatter } from '../domain/commands.ts';\nimport type { AgentTool } from '../domain/preparedCall.ts';\nimport type { Workspace } from '../domain/workspace.ts';\nimport type { CoveredFiles } from './commandCheckpoint.ts';\nimport { createCommandTool } from './commandTool.ts';\nimport { chooseWorkspace } from './createWorkspace.ts';\nimport { createApplyPatchTool, createDeleteFileTool, createTextEditorTool } from './editFormats.ts';\nimport { FileFormatter } from './fileFormatter.ts';\nimport { GitIgnoreRules } from './gitIgnoreRules.ts';\nimport { PolicyWorkspace } from './policyWorkspace.ts';\nimport { RootsAccessPolicy, type SecretRules } from './rootsAccessPolicy.ts';\nimport { createTrackedReadFile, createTrackedReadFiles } from './trackedReadFile.ts';\nimport { createWorkspaceTools } from './workspaceTools.ts';\nimport { WorkspaceWrites } from './workspaceWrites.ts';\n\n/** The folders an agent works on, and what it may do in each. */\nexport interface WorkspaceOptions {\n\t/**\n\t * Each folder `read` or `write`. The first is where a relative path starts, and the one whose\n\t * history of runs is kept; a write to another root is recorded in it too.\n\t */\n\troots: WorkspaceRoot[];\n\t/** More files to keep closed as secrets, and exact files to open. */\n\tsecrets?: SecretRules;\n\t/**\n\t * Where the history of runs is kept, relative to the working directory if not absolute.\n\t * Defaults to the platform's state directory. No tool may reach it, even inside a root.\n\t */\n\tstateDirectory?: string;\n}\n\ndeclare const fileToolBrand: unique symbol;\n\n/**\n * A tool the harness builds for files, from `createFileTools`. It goes to an agent from\n * `createLLMAgent` beside the consumer's own tools; what it does inside is the harness's.\n */\nexport type FileTool = ToolDefinition & { readonly [fileToolBrand]: true };\n\n/** What the file tools of one workspace share: one policy, one history, one record of writes. */\nexport interface OpenedWorkspace {\n\treadonly roots: WorkspaceRoot[];\n\treadonly policy: AccessPolicy;\n\treadonly store: RecoveryStore;\n\t/** The read side, with the policy's word on every path. */\n\treadonly read: Workspace;\n\treadonly writes: WorkspaceWrites;\n\treadonly writable: boolean;\n\t/** The real path of the first root. */\n\treadonly base: string;\n\t/** What a command's checkpoint captures and compares: see `CoveredFiles`. */\n\treadonly covered: CoveredFiles;\n}\n\n/**\n * Where a workspace's history is kept, as one real path: made if missing, then resolved, links\n * included. The store and the access policy must name the same folder, or a tool could reach the\n * history the policy means to protect. A relative path is from the working directory.\n *\n * @throws {UnrecoverableError} when the folder cannot be made or reached.\n */\nexport async function stateDirectoryOf({ stateDirectory }: WorkspaceOptions): Promise<string> {\n\tconst directory = resolve(stateDirectory ?? defaultStateDirectory());\n\ttry {\n\t\tawait mkdir(directory, { recursive: true, mode: 0o700 });\n\t\treturn await realpath(directory);\n\t} catch (error) {\n\t\tthrow new UnrecoverableError('The folder for the workspace history could not be made', {\n\t\t\tcause: describeFailure(error),\n\t\t});\n\t}\n}\n\n/**\n * The recovery store of a workspace, at its first root.\n *\n * @throws {InvalidAgentConfigError} when the workspace has no root.\n * @throws {UnrecoverableError} when the store cannot be opened.\n */\nexport async function openStore(\n\tworkspace: WorkspaceOptions,\n\tdirectory: string,\n\tlogger: ILogger,\n): Promise<RecoveryStore> {\n\tconst [first] = workspace.roots;\n\tif (!first) throw new InvalidAgentConfigError('A workspace needs at least one root');\n\ttry {\n\t\treturn await FileRecoveryStore.open({ root: resolve(first.path), directory, logger });\n\t} catch (error) {\n\t\tthrow new UnrecoverableError('The workspace history could not be opened', {\n\t\t\tcause: describeFailure(error),\n\t\t});\n\t}\n}\n\n/**\n * Opens what the file tools of one workspace share. With `formatter`, its edits format each file\n * they write.\n *\n * @throws {InvalidAgentConfigError} for a workspace with no root, or an invalid formatter.\n */\nexport async function openWorkspace(\n\tworkspace: WorkspaceOptions,\n\tlogger: ILogger,\n\t{ formatter }: { formatter?: Formatter } = {},\n): Promise<OpenedWorkspace> {\n\tconst { roots, secrets } = workspace;\n\tconst stateDirectory = await stateDirectoryOf(workspace);\n\tconst policy = await RootsAccessPolicy.create({\n\t\troots,\n\t\t...(secrets ? { secrets } : {}),\n\t\tignoreRules: new GitIgnoreRules(),\n\t\t// The history holds the source a run replaced: no tool may read or write it.\n\t\tprotectedPaths: [stateDirectory],\n\t});\n\tconst store = await openStore(workspace, stateDirectory, logger);\n\t// Real, as the policy's paths are, so a file's path can be judged by name.\n\tconst root = await realpath(resolve(roots[0]?.path ?? '.'));\n\tconst inner = await chooseWorkspace({\n\t\troot,\n\t\tlogger,\n\t\t// Left out before a search counts or stores a match, so no total, limit or error can tell\n\t\t// that a secret matched. The policy still decides each path after it.\n\t\thidden: path => policy.hidesFromReading(join(root, path)),\n\t});\n\tconst read = new PolicyWorkspace({ inner, policy });\n\tconst fileFormatter =\n\t\tformatter && (await FileFormatter.create({ formatter, policy, base: root }));\n\treturn {\n\t\troots,\n\t\tpolicy,\n\t\tstore,\n\t\tread,\n\t\twrites: new WorkspaceWrites({\n\t\t\tpolicy,\n\t\t\tstore,\n\t\t\t...(fileFormatter ? { formatter: fileFormatter } : {}),\n\t\t}),\n\t\twritable: roots.some(({ access }) => access === 'write'),\n\t\tbase: root,\n\t\tcovered: coveredFiles(roots, policy, logger),\n\t};\n}\n\n/**\n * Lists every file of the `write` roots that `.gitignore` does not exclude, less what the policy\n * closes to writing by name, before anything is read. Each root is listed by its own program,\n * opened on the first listing.\n */\nfunction coveredFiles(\n\troots: WorkspaceRoot[],\n\tpolicy: RootsAccessPolicy,\n\tlogger: ILogger,\n): CoveredFiles {\n\tlet listers: Promise<{ root: string; workspace: Workspace }[]> | undefined;\n\tconst open = () =>\n\t\tPromise.all(\n\t\t\troots\n\t\t\t\t.filter(({ access }) => access === 'write')\n\t\t\t\t.map(async ({ path }) => {\n\t\t\t\t\tconst root = await realpath(resolve(path));\n\t\t\t\t\tconst workspace = await chooseWorkspace({\n\t\t\t\t\t\troot,\n\t\t\t\t\t\tlogger,\n\t\t\t\t\t\thidden: relativePath => policy.closedToWriting(join(root, relativePath)),\n\t\t\t\t\t});\n\t\t\t\t\treturn { root, workspace };\n\t\t\t\t}),\n\t\t);\n\n\treturn async signal => {\n\t\tlisters ??= open().catch((error: unknown) => {\n\t\t\t// Not kept: the next command tries again.\n\t\t\tlisters = undefined;\n\t\t\tthrow error;\n\t\t});\n\t\tconst files = new Set<string>();\n\t\tfor (const { root, workspace } of await listers) {\n\t\t\tconst listed = await workspace.listFiles({ limit: Number.MAX_SAFE_INTEGER }, signal);\n\t\t\tfor (const path of listed.files) files.add(join(root, path));\n\t\t}\n\t\treturn [...files];\n\t};\n}\n\n/**\n * `runCommand` over an opened workspace: see `createCommandTool`.\n *\n * @throws {InvalidAgentConfigError} for an invalid configuration, or on Windows.\n */\nexport function commandToolFor(workspace: OpenedWorkspace, commands: Commands): Promise<AgentTool> {\n\tconst { policy, writes, covered, base } = workspace;\n\treturn createCommandTool({ commands, policy, writes, covered, base });\n}\n\n/**\n * The file tools of a provider's executor, over one workspace. Every one reads through the access\n * policy, and an edit starts only from a version the conversation read.\n *\n * - OpenAI: `listFiles`, `searchText`, `readFile`, `readFiles`, and its native `apply_patch`.\n * - Anthropic: `listFiles`, `searchText`, its native text editor, whose `view` reads, and\n * `delete_file`.\n *\n * Without a `write` root they only read: `readFile` and `readFiles` in place of the editor and\n * the patch.\n */\nexport function fileToolsFor(\n\tprovider: 'anthropic' | 'openai',\n\tworkspace: OpenedWorkspace,\n): AgentTool[] {\n\tconst { policy, read, writes, writable } = workspace;\n\tconst [listFiles, searchText] = createWorkspaceTools(read).filter(\n\t\ttool => tool.name === 'listFiles' || tool.name === 'searchText',\n\t);\n\tconst finding: AgentTool[] = [listFiles, searchText].filter(tool => tool !== undefined);\n\tconst reading = [createTrackedReadFile(policy), createTrackedReadFiles(policy)];\n\tif (!writable) return [...finding, ...reading];\n\tif (provider === 'openai') return [...finding, ...reading, createApplyPatchTool(writes)];\n\treturn [\n\t\t...finding,\n\t\tcreateTextEditorTool({ writes, policy, workspace: read }),\n\t\tcreateDeleteFileTool(writes),\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"]}
|
|
@@ -6,22 +6,25 @@ import type { FileFormatter } from './fileFormatter.ts';
|
|
|
6
6
|
import { WriteSession } from './writeSession.ts';
|
|
7
7
|
/**
|
|
8
8
|
* The writing side of a workspace, shared by every write tool built over it: one policy and
|
|
9
|
-
* one recovery store
|
|
10
|
-
*
|
|
9
|
+
* one recovery store. Every set of tools over the same history shares the run's `WriteSession`,
|
|
10
|
+
* ended with that run.
|
|
11
11
|
*/
|
|
12
12
|
export declare class WorkspaceWrites {
|
|
13
13
|
/** What the edit tools run on each file they create or change, when the consumer gave one. */
|
|
14
14
|
readonly formatter: FileFormatter | undefined;
|
|
15
15
|
private readonly policy;
|
|
16
16
|
private readonly store;
|
|
17
|
-
private readonly sessions;
|
|
18
17
|
constructor({ policy, store, formatter, }: {
|
|
19
18
|
policy: AccessPolicy;
|
|
20
19
|
store: RecoveryStore;
|
|
21
20
|
formatter?: FileFormatter;
|
|
22
21
|
});
|
|
23
|
-
/** An editor over this run's session,
|
|
22
|
+
/** An editor over this run's session, under this set of tools' policy. */
|
|
24
23
|
editorFor(context: RunContext): FileEditor;
|
|
25
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Everything this run writes to this history, its commands' changes included, from any set of
|
|
26
|
+
* tools. Created on its first write, and registered before anything is awaited, so two tools
|
|
27
|
+
* asking at once get the same one.
|
|
28
|
+
*/
|
|
26
29
|
sessionFor(context: RunContext): WriteSession;
|
|
27
30
|
}
|
|
@@ -1,28 +1,40 @@
|
|
|
1
1
|
import { FileEditor } from "./fileEditor.js";
|
|
2
2
|
import { WriteSession } from "./writeSession.js";
|
|
3
|
+
// One session per run and history on disk, whichever set of tools asks for it: two sets built
|
|
4
|
+
// apart over one workspace, such as two stages of a workflow, record one run. Each keeps its own
|
|
5
|
+
// policy. Kept by the run's context, so nothing here outlives a run.
|
|
6
|
+
const sessions = new WeakMap();
|
|
3
7
|
/**
|
|
4
8
|
* The writing side of a workspace, shared by every write tool built over it: one policy and
|
|
5
|
-
* one recovery store
|
|
6
|
-
*
|
|
9
|
+
* one recovery store. Every set of tools over the same history shares the run's `WriteSession`,
|
|
10
|
+
* ended with that run.
|
|
7
11
|
*/
|
|
8
12
|
export class WorkspaceWrites {
|
|
9
13
|
/** What the edit tools run on each file they create or change, when the consumer gave one. */
|
|
10
14
|
formatter;
|
|
11
15
|
policy;
|
|
12
16
|
store;
|
|
13
|
-
sessions = new WeakMap();
|
|
14
17
|
constructor({ policy, store, formatter, }) {
|
|
15
18
|
this.policy = policy;
|
|
16
19
|
this.store = store;
|
|
17
20
|
this.formatter = formatter;
|
|
18
21
|
}
|
|
19
|
-
/** An editor over this run's session,
|
|
22
|
+
/** An editor over this run's session, under this set of tools' policy. */
|
|
20
23
|
editorFor(context) {
|
|
21
24
|
return new FileEditor({ policy: this.policy, session: this.sessionFor(context) });
|
|
22
25
|
}
|
|
23
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Everything this run writes to this history, its commands' changes included, from any set of
|
|
28
|
+
* tools. Created on its first write, and registered before anything is awaited, so two tools
|
|
29
|
+
* asking at once get the same one.
|
|
30
|
+
*/
|
|
24
31
|
sessionFor(context) {
|
|
25
|
-
let
|
|
32
|
+
let ofRun = sessions.get(context);
|
|
33
|
+
if (!ofRun) {
|
|
34
|
+
ofRun = new Map();
|
|
35
|
+
sessions.set(context, ofRun);
|
|
36
|
+
}
|
|
37
|
+
let session = ofRun.get(this.store.workspaceId);
|
|
26
38
|
if (!session) {
|
|
27
39
|
const created = new WriteSession({
|
|
28
40
|
store: this.store,
|
|
@@ -36,7 +48,7 @@ export class WorkspaceWrites {
|
|
|
36
48
|
},
|
|
37
49
|
});
|
|
38
50
|
context.onFinish(end => created.finish(end));
|
|
39
|
-
this.
|
|
51
|
+
ofRun.set(this.store.workspaceId, created);
|
|
40
52
|
session = created;
|
|
41
53
|
}
|
|
42
54
|
return session;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspaceWrites.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/workspaceWrites.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD;;;;GAIG;AACH,MAAM,OAAO,eAAe;IAC3B,8FAA8F;IACrF,SAAS,CAA4B;IAC7B,MAAM,CAAe;IACrB,KAAK,CAAgB;
|
|
1
|
+
{"version":3,"file":"workspaceWrites.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/workspaceWrites.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,8FAA8F;AAC9F,iGAAiG;AACjG,qEAAqE;AACrE,MAAM,QAAQ,GAAG,IAAI,OAAO,EAAyC,CAAC;AAEtE;;;;GAIG;AACH,MAAM,OAAO,eAAe;IAC3B,8FAA8F;IACrF,SAAS,CAA4B;IAC7B,MAAM,CAAe;IACrB,KAAK,CAAgB;IAEtC,YAAY,EACX,MAAM,EACN,KAAK,EACL,SAAS,GAKT;QACA,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC5B,CAAC;IAED,0EAA0E;IAC1E,SAAS,CAAC,OAAmB;QAC5B,OAAO,IAAI,UAAU,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACnF,CAAC;IAED;;;;OAIG;IACH,UAAU,CAAC,OAAmB;QAC7B,IAAI,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAClC,IAAI,CAAC,KAAK,EAAE,CAAC;YACZ,KAAK,GAAG,IAAI,GAAG,EAAE,CAAC;YAClB,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAC9B,CAAC;QACD,IAAI,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QAChD,IAAI,CAAC,OAAO,EAAE,CAAC;YACd,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC;gBAChC,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,yEAAyE;gBACzE,OAAO,EAAE,KAAK,CAAC,EAAE;oBAChB,OAAO,CAAC,UAAU,CAAC,KAAK,GAAG,KAAK,CAAC;gBAClC,CAAC;gBACD,gFAAgF;gBAChF,SAAS,EAAE,GAAG,EAAE;oBACf,OAAO,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC;gBACjC,CAAC;aACD,CAAC,CAAC;YACH,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YAC7C,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;YAC3C,OAAO,GAAG,OAAO,CAAC;QACnB,CAAC;QACD,OAAO,OAAO,CAAC;IAChB,CAAC;CACD","sourcesContent":["import type { RunContext } from '#src/agent/domain/runContext';\nimport type { RecoveryStore } from '#src/recovery/domain/recoveryStore';\nimport type { AccessPolicy } from '../domain/accessPolicy.ts';\nimport { FileEditor } from './fileEditor.ts';\nimport type { FileFormatter } from './fileFormatter.ts';\nimport { WriteSession } from './writeSession.ts';\n\n// One session per run and history on disk, whichever set of tools asks for it: two sets built\n// apart over one workspace, such as two stages of a workflow, record one run. Each keeps its own\n// policy. Kept by the run's context, so nothing here outlives a run.\nconst sessions = new WeakMap<RunContext, Map<string, WriteSession>>();\n\n/**\n * The writing side of a workspace, shared by every write tool built over it: one policy and\n * one recovery store. Every set of tools over the same history shares the run's `WriteSession`,\n * ended with that run.\n */\nexport class WorkspaceWrites {\n\t/** What the edit tools run on each file they create or change, when the consumer gave one. */\n\treadonly formatter: FileFormatter | undefined;\n\tprivate readonly policy: AccessPolicy;\n\tprivate readonly store: RecoveryStore;\n\n\tconstructor({\n\t\tpolicy,\n\t\tstore,\n\t\tformatter,\n\t}: {\n\t\tpolicy: AccessPolicy;\n\t\tstore: RecoveryStore;\n\t\tformatter?: FileFormatter;\n\t}) {\n\t\tthis.policy = policy;\n\t\tthis.store = store;\n\t\tthis.formatter = formatter;\n\t}\n\n\t/** An editor over this run's session, under this set of tools' policy. */\n\teditorFor(context: RunContext): FileEditor {\n\t\treturn new FileEditor({ policy: this.policy, session: this.sessionFor(context) });\n\t}\n\n\t/**\n\t * Everything this run writes to this history, its commands' changes included, from any set of\n\t * tools. Created on its first write, and registered before anything is awaited, so two tools\n\t * asking at once get the same one.\n\t */\n\tsessionFor(context: RunContext): WriteSession {\n\t\tlet ofRun = sessions.get(context);\n\t\tif (!ofRun) {\n\t\t\tofRun = new Map();\n\t\t\tsessions.set(context, ofRun);\n\t\t}\n\t\tlet session = ofRun.get(this.store.workspaceId);\n\t\tif (!session) {\n\t\t\tconst created = new WriteSession({\n\t\t\t\tstore: this.store,\n\t\t\t\t// So every turn of the run, in every role, can be tied to what it wrote.\n\t\t\t\tonStart: runId => {\n\t\t\t\t\tcontext.historyRun.runId = runId;\n\t\t\t\t},\n\t\t\t\t// A run that changed nothing is not in the history, so its turns wrote nothing.\n\t\t\t\tonDiscard: () => {\n\t\t\t\t\tdelete context.historyRun.runId;\n\t\t\t\t},\n\t\t\t});\n\t\t\tcontext.onFinish(end => created.finish(end));\n\t\t\tofRun.set(this.store.workspaceId, created);\n\t\t\tsession = created;\n\t\t}\n\t\treturn session;\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. */
|