@mikode13/harness 6.0.1 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +31 -4
  2. package/dist/agent/domain/agent.d.ts +14 -0
  3. package/dist/agent/domain/agent.js.map +1 -1
  4. package/dist/engines/domain/model/llmAgent.js +31 -3
  5. package/dist/engines/domain/model/llmAgent.js.map +1 -1
  6. package/dist/factory/infrastructure/agentLLMFactory.d.ts +21 -6
  7. package/dist/factory/infrastructure/agentLLMFactory.js +54 -18
  8. package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/tools/domain/commands.d.ts +27 -0
  12. package/dist/tools/domain/commands.js +43 -15
  13. package/dist/tools/domain/commands.js.map +1 -1
  14. package/dist/tools/domain/readRegistry.d.ts +17 -2
  15. package/dist/tools/domain/readRegistry.js.map +1 -1
  16. package/dist/tools/infrastructure/commandTool.d.ts +6 -1
  17. package/dist/tools/infrastructure/commandTool.js +11 -5
  18. package/dist/tools/infrastructure/commandTool.js.map +1 -1
  19. package/dist/tools/infrastructure/exploreTool.d.ts +10 -0
  20. package/dist/tools/infrastructure/exploreTool.js +88 -0
  21. package/dist/tools/infrastructure/exploreTool.js.map +1 -0
  22. package/dist/tools/infrastructure/fileFormatter.d.ts +39 -0
  23. package/dist/tools/infrastructure/fileFormatter.js +114 -0
  24. package/dist/tools/infrastructure/fileFormatter.js.map +1 -0
  25. package/dist/tools/infrastructure/fileTools.d.ts +10 -2
  26. package/dist/tools/infrastructure/fileTools.js +14 -2
  27. package/dist/tools/infrastructure/fileTools.js.map +1 -1
  28. package/dist/tools/infrastructure/trackedEdits.d.ts +4 -4
  29. package/dist/tools/infrastructure/trackedEdits.js +32 -9
  30. package/dist/tools/infrastructure/trackedEdits.js.map +1 -1
  31. package/dist/tools/infrastructure/workspaceWrites.d.ts +5 -1
  32. package/dist/tools/infrastructure/workspaceWrites.js +4 -1
  33. package/dist/tools/infrastructure/workspaceWrites.js.map +1 -1
  34. package/package.json +1 -1
@@ -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 } 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 { GitIgnoreRules } from './gitIgnoreRules.ts';\nimport { PolicyWorkspace } from './policyWorkspace.ts';\nimport { RootsAccessPolicy, type SecretRules } from './rootsAccessPolicy.ts';\nimport { createTrackedReadFile } 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\nexport async function openWorkspace(\n\tworkspace: WorkspaceOptions,\n\tlogger: ILogger,\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\treturn {\n\t\troots,\n\t\tpolicy,\n\t\tstore,\n\t\tread,\n\t\twrites: new WorkspaceWrites({ policy, store }),\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`, 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` in place of the editor and 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 readFile = createTrackedReadFile(policy);\n\tif (!writable) return [...finding, readFile];\n\tif (provider === 'openai') return [...finding, readFile, 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 } 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`, 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` in place of the editor and 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 readFile = createTrackedReadFile(policy);\n\tif (!writable) return [...finding, readFile];\n\tif (provider === 'openai') return [...finding, readFile, createApplyPatchTool(writes)];\n\treturn [\n\t\t...finding,\n\t\tcreateTextEditorTool({ writes, policy, workspace: read }),\n\t\tcreateDeleteFileTool(writes),\n\t];\n}\n"]}
@@ -26,8 +26,8 @@ export type EditRequest = {
26
26
  * - a file changed since it read it is `STALE_FILE`;
27
27
  * - a new file needs a path that does not exist.
28
28
  *
29
- * Once applied, the conversation knows the file as the edit left it, so it can edit it again
30
- * without reading it. What it returns to the model is short: the lines around a change, or a
31
- * confirmation.
29
+ * Once applied, and formatted when the workspace has a formatter, the conversation knows the
30
+ * file as it is then, so it can edit it again without reading it. What it returns to the model
31
+ * is short: the lines around a change, or a confirmation.
32
32
  */
33
- export declare function prepareTrackedEdit(writes: WorkspaceWrites, request: EditRequest, signal: AbortSignal, { run, reads }: ToolCallContext): Promise<PreparedCall>;
33
+ export declare function prepareTrackedEdit(writes: WorkspaceWrites, request: EditRequest, signal: AbortSignal, call: ToolCallContext): Promise<PreparedCall>;
@@ -5,6 +5,26 @@ import { editRisk } from "./fileEditor.js";
5
5
  function hashOf(content) {
6
6
  return createHash('sha256').update(content).digest('hex');
7
7
  }
8
+ /**
9
+ * What the file holds once the formatter, if there is one, ran on what the edit wrote, and what
10
+ * the model needs to know when it may not be formatted. The conversation knows the file as it is
11
+ * now, so it can edit it again without reading it.
12
+ */
13
+ async function afterFormatting(writes, editor, { path, absolute, written }, { run, reads }, signal) {
14
+ const { formatter } = writes;
15
+ if (!formatter) {
16
+ // It wrote the content itself, so it has seen it.
17
+ reads.record(absolute, hashOf(written));
18
+ return { content: written, note: '' };
19
+ }
20
+ const failure = await formatter.format(absolute, writes.sessionFor(run), signal);
21
+ const { content } = await editor.current(path, signal);
22
+ if (content === undefined)
23
+ reads.forget(absolute);
24
+ else
25
+ reads.record(absolute, hashOf(content));
26
+ return { content, note: failure === undefined ? '' : `\n${failure}` };
27
+ }
8
28
  /**
9
29
  * Prepares an edit from the version of the file this conversation read, and only from it:
10
30
  *
@@ -12,11 +32,12 @@ function hashOf(content) {
12
32
  * - a file changed since it read it is `STALE_FILE`;
13
33
  * - a new file needs a path that does not exist.
14
34
  *
15
- * Once applied, the conversation knows the file as the edit left it, so it can edit it again
16
- * without reading it. What it returns to the model is short: the lines around a change, or a
17
- * confirmation.
35
+ * Once applied, and formatted when the workspace has a formatter, the conversation knows the
36
+ * file as it is then, so it can edit it again without reading it. What it returns to the model
37
+ * is short: the lines around a change, or a confirmation.
18
38
  */
19
- export async function prepareTrackedEdit(writes, request, signal, { run, reads }) {
39
+ export async function prepareTrackedEdit(writes, request, signal, call) {
40
+ const { run, reads } = call;
20
41
  const editor = writes.editorFor(run);
21
42
  const { path } = request;
22
43
  if (request.kind === 'create') {
@@ -26,9 +47,8 @@ export async function prepareTrackedEdit(writes, request, signal, { run, reads }
26
47
  diff: editDiff(path, undefined, request.content.toString('utf8')),
27
48
  run: async (runSignal) => {
28
49
  await editor.apply(prepared, runSignal);
29
- // It wrote the content itself, so it has seen it.
30
- reads.record(prepared.target.absolute, hashOf(request.content));
31
- return describeCreation(path, request.content);
50
+ const { content, note } = await afterFormatting(writes, editor, { path, absolute: prepared.target.absolute, written: request.content }, call, runSignal);
51
+ return `${content === undefined ? goneAfterFormatting(path) : describeCreation(path, content)}${note}`;
32
52
  },
33
53
  };
34
54
  }
@@ -62,9 +82,12 @@ export async function prepareTrackedEdit(writes, request, signal, { run, reads }
62
82
  diff: editDiff(path, content.toString('utf8'), next.toString('utf8')),
63
83
  run: async (runSignal) => {
64
84
  await editor.apply(prepared, runSignal);
65
- reads.record(target.absolute, hashOf(next));
66
- return describeModification(path, content.toString('utf8'), next.toString('utf8'));
85
+ const { content: now, note } = await afterFormatting(writes, editor, { path, absolute: target.absolute, written: next }, call, runSignal);
86
+ return `${now === undefined ? goneAfterFormatting(path) : describeModification(path, content.toString('utf8'), now.toString('utf8'))}${note}`;
67
87
  },
68
88
  };
69
89
  }
90
+ function goneAfterFormatting(path) {
91
+ return `Changed ${path}, but it no longer exists once the formatter ran.`;
92
+ }
70
93
  //# sourceMappingURL=trackedEdits.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"trackedEdits.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/trackedEdits.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport {\n\tdescribeCreation,\n\tdescribeDeletion,\n\tdescribeModification,\n\teditDiff,\n} from '../domain/editResponse.ts';\nimport { EditRefusedError } from '../domain/fileEdits.ts';\nimport type { PreparedCall } from '../domain/preparedCall.ts';\nimport type { ToolCallContext } from '../domain/readRegistry.ts';\nimport { editRisk } from './fileEditor.ts';\nimport type { WorkspaceWrites } from './workspaceWrites.ts';\n\n/**\n * One edit, as an edit format asks for it, with the path as the model wrote it. A modification\n * works its new content out from what the file holds now, the version the model read; it throws\n * `EditRefusedError` with what the model needs to fix the call, such as a text that matched\n * twice.\n */\nexport type EditRequest =\n\t| { kind: 'create'; path: string; content: Buffer }\n\t| { kind: 'modify'; path: string; change: (current: Buffer) => Buffer }\n\t| { kind: 'delete'; path: string };\n\nfunction hashOf(content: Buffer): string {\n\treturn createHash('sha256').update(content).digest('hex');\n}\n\n/**\n * Prepares an edit from the version of the file this conversation read, and only from it:\n *\n * - a file it never read is `READ_REQUIRED`;\n * - a file changed since it read it is `STALE_FILE`;\n * - a new file needs a path that does not exist.\n *\n * Once applied, the conversation knows the file as the edit left it, so it can edit it again\n * without reading it. What it returns to the model is short: the lines around a change, or a\n * confirmation.\n */\nexport async function prepareTrackedEdit(\n\twrites: WorkspaceWrites,\n\trequest: EditRequest,\n\tsignal: AbortSignal,\n\t{ run, reads }: ToolCallContext,\n): Promise<PreparedCall> {\n\tconst editor = writes.editorFor(run);\n\tconst { path } = request;\n\n\tif (request.kind === 'create') {\n\t\tconst prepared = await editor.prepare(\n\t\t\t{ kind: 'create', path, content: request.content },\n\t\t\tsignal,\n\t\t);\n\t\treturn {\n\t\t\trisk: editRisk(prepared),\n\t\t\tdiff: editDiff(path, undefined, request.content.toString('utf8')),\n\t\t\trun: async runSignal => {\n\t\t\t\tawait editor.apply(prepared, runSignal);\n\t\t\t\t// It wrote the content itself, so it has seen it.\n\t\t\t\treads.record(prepared.target.absolute, hashOf(request.content));\n\t\t\t\treturn describeCreation(path, request.content);\n\t\t\t},\n\t\t};\n\t}\n\n\tconst { target, state, content } = await editor.current(path, signal);\n\tif (!state.exists || content === undefined) {\n\t\tthrow new EditRefusedError(`\"${path}\" does not exist`);\n\t}\n\tconst seen = reads.versionOf(target.absolute);\n\tif (seen === undefined) {\n\t\tthrow new EditRefusedError(`READ_REQUIRED: read \"${path}\" before changing it`);\n\t}\n\tif (seen !== state.hash) {\n\t\tthrow new EditRefusedError(`STALE_FILE: \"${path}\" changed since you read it; read it again`);\n\t}\n\n\tif (request.kind === 'delete') {\n\t\tconst prepared = await editor.prepare({ kind: 'delete', path, expected: seen }, signal);\n\t\treturn {\n\t\t\trisk: editRisk(prepared),\n\t\t\tdiff: editDiff(path, content.toString('utf8'), undefined),\n\t\t\trun: async runSignal => {\n\t\t\t\tawait editor.apply(prepared, runSignal);\n\t\t\t\treads.forget(target.absolute);\n\t\t\t\treturn describeDeletion(path);\n\t\t\t},\n\t\t};\n\t}\n\n\tconst next = request.change(content);\n\tconst prepared = await editor.prepare(\n\t\t{ kind: 'replace', path, content: next, expected: seen },\n\t\tsignal,\n\t);\n\treturn {\n\t\trisk: editRisk(prepared),\n\t\tdiff: editDiff(path, content.toString('utf8'), next.toString('utf8')),\n\t\trun: async runSignal => {\n\t\t\tawait editor.apply(prepared, runSignal);\n\t\t\treads.record(target.absolute, hashOf(next));\n\t\t\treturn describeModification(path, content.toString('utf8'), next.toString('utf8'));\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"trackedEdits.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/trackedEdits.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport {\n\tdescribeCreation,\n\tdescribeDeletion,\n\tdescribeModification,\n\teditDiff,\n} from '../domain/editResponse.ts';\nimport { EditRefusedError } from '../domain/fileEdits.ts';\nimport type { PreparedCall } from '../domain/preparedCall.ts';\nimport type { ToolCallContext } from '../domain/readRegistry.ts';\nimport { editRisk, type FileEditor } from './fileEditor.ts';\nimport type { WorkspaceWrites } from './workspaceWrites.ts';\n\n/**\n * One edit, as an edit format asks for it, with the path as the model wrote it. A modification\n * works its new content out from what the file holds now, the version the model read; it throws\n * `EditRefusedError` with what the model needs to fix the call, such as a text that matched\n * twice.\n */\nexport type EditRequest =\n\t| { kind: 'create'; path: string; content: Buffer }\n\t| { kind: 'modify'; path: string; change: (current: Buffer) => Buffer }\n\t| { kind: 'delete'; path: string };\n\nfunction hashOf(content: Buffer): string {\n\treturn createHash('sha256').update(content).digest('hex');\n}\n\n/**\n * What the file holds once the formatter, if there is one, ran on what the edit wrote, and what\n * the model needs to know when it may not be formatted. The conversation knows the file as it is\n * now, so it can edit it again without reading it.\n */\nasync function afterFormatting(\n\twrites: WorkspaceWrites,\n\teditor: FileEditor,\n\t{ path, absolute, written }: { path: string; absolute: string; written: Buffer },\n\t{ run, reads }: ToolCallContext,\n\tsignal: AbortSignal,\n): Promise<{ content: Buffer | undefined; note: string }> {\n\tconst { formatter } = writes;\n\tif (!formatter) {\n\t\t// It wrote the content itself, so it has seen it.\n\t\treads.record(absolute, hashOf(written));\n\t\treturn { content: written, note: '' };\n\t}\n\tconst failure = await formatter.format(absolute, writes.sessionFor(run), signal);\n\tconst { content } = await editor.current(path, signal);\n\tif (content === undefined) reads.forget(absolute);\n\telse reads.record(absolute, hashOf(content));\n\treturn { content, note: failure === undefined ? '' : `\\n${failure}` };\n}\n\n/**\n * Prepares an edit from the version of the file this conversation read, and only from it:\n *\n * - a file it never read is `READ_REQUIRED`;\n * - a file changed since it read it is `STALE_FILE`;\n * - a new file needs a path that does not exist.\n *\n * Once applied, and formatted when the workspace has a formatter, the conversation knows the\n * file as it is then, so it can edit it again without reading it. What it returns to the model\n * is short: the lines around a change, or a confirmation.\n */\nexport async function prepareTrackedEdit(\n\twrites: WorkspaceWrites,\n\trequest: EditRequest,\n\tsignal: AbortSignal,\n\tcall: ToolCallContext,\n): Promise<PreparedCall> {\n\tconst { run, reads } = call;\n\tconst editor = writes.editorFor(run);\n\tconst { path } = request;\n\n\tif (request.kind === 'create') {\n\t\tconst prepared = await editor.prepare(\n\t\t\t{ kind: 'create', path, content: request.content },\n\t\t\tsignal,\n\t\t);\n\t\treturn {\n\t\t\trisk: editRisk(prepared),\n\t\t\tdiff: editDiff(path, undefined, request.content.toString('utf8')),\n\t\t\trun: async runSignal => {\n\t\t\t\tawait editor.apply(prepared, runSignal);\n\t\t\t\tconst { content, note } = await afterFormatting(\n\t\t\t\t\twrites,\n\t\t\t\t\teditor,\n\t\t\t\t\t{ path, absolute: prepared.target.absolute, written: request.content },\n\t\t\t\t\tcall,\n\t\t\t\t\trunSignal,\n\t\t\t\t);\n\t\t\t\treturn `${content === undefined ? goneAfterFormatting(path) : describeCreation(path, content)}${note}`;\n\t\t\t},\n\t\t};\n\t}\n\n\tconst { target, state, content } = await editor.current(path, signal);\n\tif (!state.exists || content === undefined) {\n\t\tthrow new EditRefusedError(`\"${path}\" does not exist`);\n\t}\n\tconst seen = reads.versionOf(target.absolute);\n\tif (seen === undefined) {\n\t\tthrow new EditRefusedError(`READ_REQUIRED: read \"${path}\" before changing it`);\n\t}\n\tif (seen !== state.hash) {\n\t\tthrow new EditRefusedError(`STALE_FILE: \"${path}\" changed since you read it; read it again`);\n\t}\n\n\tif (request.kind === 'delete') {\n\t\tconst prepared = await editor.prepare({ kind: 'delete', path, expected: seen }, signal);\n\t\treturn {\n\t\t\trisk: editRisk(prepared),\n\t\t\tdiff: editDiff(path, content.toString('utf8'), undefined),\n\t\t\trun: async runSignal => {\n\t\t\t\tawait editor.apply(prepared, runSignal);\n\t\t\t\treads.forget(target.absolute);\n\t\t\t\treturn describeDeletion(path);\n\t\t\t},\n\t\t};\n\t}\n\n\tconst next = request.change(content);\n\tconst prepared = await editor.prepare(\n\t\t{ kind: 'replace', path, content: next, expected: seen },\n\t\tsignal,\n\t);\n\treturn {\n\t\trisk: editRisk(prepared),\n\t\tdiff: editDiff(path, content.toString('utf8'), next.toString('utf8')),\n\t\trun: async runSignal => {\n\t\t\tawait editor.apply(prepared, runSignal);\n\t\t\tconst { content: now, note } = await afterFormatting(\n\t\t\t\twrites,\n\t\t\t\teditor,\n\t\t\t\t{ path, absolute: target.absolute, written: next },\n\t\t\t\tcall,\n\t\t\t\trunSignal,\n\t\t\t);\n\t\t\treturn `${now === undefined ? goneAfterFormatting(path) : describeModification(path, content.toString('utf8'), now.toString('utf8'))}${note}`;\n\t\t},\n\t};\n}\n\nfunction goneAfterFormatting(path: string): string {\n\treturn `Changed ${path}, but it no longer exists once the formatter ran.`;\n}\n"]}
@@ -2,6 +2,7 @@ import type { RunContext } from '#src/agent/domain/runContext';
2
2
  import type { RecoveryStore } from '#src/recovery/domain/recoveryStore';
3
3
  import type { AccessPolicy } from '../domain/accessPolicy.ts';
4
4
  import { FileEditor } from './fileEditor.ts';
5
+ import type { FileFormatter } from './fileFormatter.ts';
5
6
  import { WriteSession } from './writeSession.ts';
6
7
  /**
7
8
  * The writing side of a workspace, shared by every write tool built over it: one policy and
@@ -9,12 +10,15 @@ import { WriteSession } from './writeSession.ts';
9
10
  * only as long as its run's context, so nothing here outlives a run.
10
11
  */
11
12
  export declare class WorkspaceWrites {
13
+ /** What the edit tools run on each file they create or change, when the consumer gave one. */
14
+ readonly formatter: FileFormatter | undefined;
12
15
  private readonly policy;
13
16
  private readonly store;
14
17
  private readonly sessions;
15
- constructor({ policy, store }: {
18
+ constructor({ policy, store, formatter, }: {
16
19
  policy: AccessPolicy;
17
20
  store: RecoveryStore;
21
+ formatter?: FileFormatter;
18
22
  });
19
23
  /** An editor over this run's session, created on the run's first write tool call. */
20
24
  editorFor(context: RunContext): FileEditor;
@@ -6,12 +6,15 @@ import { WriteSession } from "./writeSession.js";
6
6
  * only as long as its run's context, so nothing here outlives a run.
7
7
  */
8
8
  export class WorkspaceWrites {
9
+ /** What the edit tools run on each file they create or change, when the consumer gave one. */
10
+ formatter;
9
11
  policy;
10
12
  store;
11
13
  sessions = new WeakMap();
12
- constructor({ policy, store }) {
14
+ constructor({ policy, store, formatter, }) {
13
15
  this.policy = policy;
14
16
  this.store = store;
17
+ this.formatter = formatter;
15
18
  }
16
19
  /** An editor over this run's session, created on the run's first write tool call. */
17
20
  editorFor(context) {
@@ -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;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD;;;;GAIG;AACH,MAAM,OAAO,eAAe;IACV,MAAM,CAAe;IACrB,KAAK,CAAgB;IACrB,QAAQ,GAAG,IAAI,OAAO,EAA4B,CAAC;IAEpE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAkD;QAC5E,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACpB,CAAC;IAED,qFAAqF;IACrF,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,8FAA8F;IAC9F,UAAU,CAAC,OAAmB;QAC7B,IAAI,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACzC,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,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YACpC,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 { WriteSession } from './writeSession.ts';\n\n/**\n * The writing side of a workspace, shared by every write tool built over it: one policy and\n * one recovery store, and one `WriteSession` per run, ended with that run. A session lives\n * only as long as its run's context, so nothing here outlives a run.\n */\nexport class WorkspaceWrites {\n\tprivate readonly policy: AccessPolicy;\n\tprivate readonly store: RecoveryStore;\n\tprivate readonly sessions = new WeakMap<RunContext, WriteSession>();\n\n\tconstructor({ policy, store }: { policy: AccessPolicy; store: RecoveryStore }) {\n\t\tthis.policy = policy;\n\t\tthis.store = store;\n\t}\n\n\t/** An editor over this run's session, created on the run's first write tool call. */\n\teditorFor(context: RunContext): FileEditor {\n\t\treturn new FileEditor({ policy: this.policy, session: this.sessionFor(context) });\n\t}\n\n\t/** Everything this run writes, its commands' changes included, created on its first write. */\n\tsessionFor(context: RunContext): WriteSession {\n\t\tlet session = this.sessions.get(context);\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\tthis.sessions.set(context, created);\n\t\t\tsession = created;\n\t\t}\n\t\treturn session;\n\t}\n}\n"]}
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;IACrB,QAAQ,GAAG,IAAI,OAAO,EAA4B,CAAC;IAEpE,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,qFAAqF;IACrF,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,8FAA8F;IAC9F,UAAU,CAAC,OAAmB;QAC7B,IAAI,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACzC,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,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YACpC,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/**\n * The writing side of a workspace, shared by every write tool built over it: one policy and\n * one recovery store, and one `WriteSession` per run, ended with that run. A session lives\n * only as long as its run's context, so nothing here outlives a 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\tprivate readonly sessions = new WeakMap<RunContext, WriteSession>();\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, created on the run's first write tool call. */\n\teditorFor(context: RunContext): FileEditor {\n\t\treturn new FileEditor({ policy: this.policy, session: this.sessionFor(context) });\n\t}\n\n\t/** Everything this run writes, its commands' changes included, created on its first write. */\n\tsessionFor(context: RunContext): WriteSession {\n\t\tlet session = this.sessions.get(context);\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\tthis.sessions.set(context, created);\n\t\t\tsession = created;\n\t\t}\n\t\treturn session;\n\t}\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikode13/harness",
3
- "version": "6.0.1",
3
+ "version": "6.2.0",
4
4
  "description": "Test/agent harness utility",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",