@mikode13/harness 11.1.0 → 11.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -10
- package/dist/agent/domain/agent.d.ts +17 -0
- package/dist/agent/domain/agent.js.map +1 -1
- package/dist/agent/domain/runContext.d.ts +5 -0
- package/dist/agent/domain/runContext.js.map +1 -1
- package/dist/engines/domain/model/compactor.d.ts +40 -0
- package/dist/engines/domain/model/compactor.js +5 -0
- package/dist/engines/domain/model/compactor.js.map +1 -0
- package/dist/engines/domain/model/llmAgent.d.ts +48 -2
- package/dist/engines/domain/model/llmAgent.js +351 -14
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/engines/domain/model/recentCompaction.d.ts +39 -0
- package/dist/engines/domain/model/recentCompaction.js +119 -0
- package/dist/engines/domain/model/recentCompaction.js.map +1 -0
- package/dist/engines/domain/model/stepContext.js +3 -0
- package/dist/engines/domain/model/stepContext.js.map +1 -1
- package/dist/engines/domain/model/triagedCompaction.d.ts +27 -0
- package/dist/engines/domain/model/triagedCompaction.js +60 -0
- package/dist/engines/domain/model/triagedCompaction.js.map +1 -0
- package/dist/factory/infrastructure/agentLLMFactory.d.ts +40 -12
- package/dist/factory/infrastructure/agentLLMFactory.js +114 -18
- package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/llm/domain/contextTriage.d.ts +26 -0
- package/dist/llm/domain/contextTriage.js +3 -0
- package/dist/llm/domain/contextTriage.js.map +1 -0
- package/dist/llm/domain/llm.d.ts +18 -1
- package/dist/llm/domain/llm.js.map +1 -1
- package/dist/llm/domain/message.d.ts +18 -1
- package/dist/llm/domain/message.js +20 -0
- package/dist/llm/domain/message.js.map +1 -1
- package/dist/llm/infrastructure/claudeLLMClient.d.ts +4 -5
- package/dist/llm/infrastructure/claudeLLMClient.js +39 -13
- package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -1
- package/dist/llm/infrastructure/jevContextTriage.d.ts +18 -0
- package/dist/llm/infrastructure/jevContextTriage.js +92 -0
- package/dist/llm/infrastructure/jevContextTriage.js.map +1 -0
- package/dist/llm/infrastructure/openAILLMClient.d.ts +4 -5
- package/dist/llm/infrastructure/openAILLMClient.js +20 -9
- package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
- package/dist/tools/domain/preparedCall.d.ts +2 -1
- package/dist/tools/domain/preparedCall.js +7 -1
- package/dist/tools/domain/preparedCall.js.map +1 -1
- package/dist/tools/domain/readRegistry.d.ts +11 -0
- package/dist/tools/domain/readRegistry.js.map +1 -1
- package/dist/tools/infrastructure/agentAsTool.d.ts +6 -0
- package/dist/tools/infrastructure/agentAsTool.js +9 -0
- package/dist/tools/infrastructure/agentAsTool.js.map +1 -1
- package/dist/tools/infrastructure/commandTool.js +4 -1
- package/dist/tools/infrastructure/commandTool.js.map +1 -1
- package/dist/tools/infrastructure/exploreTool.js +2 -0
- package/dist/tools/infrastructure/exploreTool.js.map +1 -1
- package/dist/tools/infrastructure/fileTools.js +1 -0
- package/dist/tools/infrastructure/fileTools.js.map +1 -1
- package/dist/tools/infrastructure/removeWorktreeTool.js +7 -1
- package/dist/tools/infrastructure/removeWorktreeTool.js.map +1 -1
- package/dist/tools/infrastructure/subagents.d.ts +3 -1
- package/dist/tools/infrastructure/subagents.js +2 -1
- package/dist/tools/infrastructure/subagents.js.map +1 -1
- package/dist/tools/infrastructure/trackedEdits.js +1 -0
- package/dist/tools/infrastructure/trackedEdits.js.map +1 -1
- package/dist/tools/infrastructure/workspaceWrites.d.ts +13 -1
- package/dist/tools/infrastructure/workspaceWrites.js +18 -1
- package/dist/tools/infrastructure/workspaceWrites.js.map +1 -1
- package/dist/tools/infrastructure/writeCeiling.d.ts +16 -0
- package/dist/tools/infrastructure/writeCeiling.js +49 -0
- package/dist/tools/infrastructure/writeCeiling.js.map +1 -0
- package/package.json +2 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"removeWorktreeTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/removeWorktreeTool.ts"],"names":[],"mappings":"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","sourcesContent":["import { lstat, realpath } from 'node:fs/promises';\nimport { join, resolve, sep } from 'node:path';\nimport { z } from 'zod';\nimport type { WorktreeRemoval } from '#src/agent/domain/approval';\nimport { WorkspaceBusyError } from '#src/recovery/domain/recoveryStore';\nimport { FolderClaims } from '#src/recovery/infrastructure/folderClaims';\nimport type { WorkspaceRoot } from '../domain/accessPolicy.ts';\nimport type { PreparingTool } from '../domain/preparedCall.ts';\nimport { definePreparingTool } from './defineTool.ts';\nimport { git, listWorktrees } from './worktrees.ts';\n\nfunction within(path: string, folder: string): boolean {\n\treturn path === folder || path.startsWith(`${folder}${sep}`);\n}\n\n/**\n * The paths `git status --porcelain -z` names, each with its two-letter code. A rename or copy\n * names its source after it, which is skipped.\n */\nfunction statusEntries(status: string): { code: string; path: string }[] {\n\tconst fields = status.split('\\0');\n\tconst entries: { code: string; path: string }[] = [];\n\tfor (let index = 0; index < fields.length; index++) {\n\t\tconst field = fields[index] ?? '';\n\t\tif (field.length < 4) continue;\n\t\tconst code = field.slice(0, 2);\n\t\tentries.push({ code, path: field.slice(3) });\n\t\tif (code.startsWith('R') || code.startsWith('C')) index++;\n\t}\n\treturn entries;\n}\n\nasync function gitOutput(args: string[], cwd: string, signal: AbortSignal): Promise<string> {\n\tconst { stdout, stderr, exitCode } = await git(args, cwd, signal);\n\tif (exitCode !== 0) {\n\t\tthrow new Error(\n\t\t\t`git could not tell what removing ${cwd} would lose, so nothing was removed: ${stderr.trim()}`,\n\t\t);\n\t}\n\treturn stdout;\n}\n\n/**\n * What removing the worktree at `path` would lose, and a fingerprint of all it rests on: its\n * HEAD, its branch, the commits at risk, and each file `git status` names, by size and time of\n * change. Throws when\n * any of it cannot be read, since a removal must never be asked about with less than it loses.\n */\nasync function inspect(\n\tpath: string,\n\tbranch: string | undefined,\n\tsignal: AbortSignal,\n): Promise<{ removal: WorktreeRemoval; fingerprint: string }> {\n\tconst head = (await gitOutput(['rev-parse', '--verify', 'HEAD'], path, signal)).trim();\n\tconst changed = statusEntries(\n\t\tawait gitOutput(['status', '--porcelain=v1', '-z', '--untracked-files=all'], path, signal),\n\t);\n\t// A folder `.gitignore` excludes whole is named once, however much it holds.\n\tconst ignored = statusEntries(\n\t\tawait gitOutput(\n\t\t\t['status', '--porcelain=v1', '-z', '--untracked-files=normal', '--ignored=matching'],\n\t\t\tpath,\n\t\t\tsignal,\n\t\t),\n\t).filter(({ code }) => code === '!!');\n\t// The commits only a detached HEAD holds, which are lost with it.\n\tconst atRisk: string[] = [];\n\tif (branch === undefined) {\n\t\tconst listed = (\n\t\t\tawait gitOutput(\n\t\t\t\t['rev-list', 'HEAD', '--not', '--branches', '--tags', '--remotes'],\n\t\t\t\tpath,\n\t\t\t\tsignal,\n\t\t\t)\n\t\t)\n\t\t\t.split('\\n')\n\t\t\t.filter(line => line !== '');\n\t\tif (!listed.every(line => /^[0-9a-f]{40,64}$/.test(line))) {\n\t\t\tthrow new Error(\n\t\t\t\t`git could not list the commits only ${path} holds, so nothing was removed: it answered \"${listed.join(' ')}\".`,\n\t\t\t);\n\t\t}\n\t\tatRisk.push(...listed);\n\t}\n\tconst files = await Promise.all(\n\t\t[...changed, ...ignored].map(async ({ code, path: file }) => {\n\t\t\tconst stats = await lstat(join(path, file)).catch(() => undefined);\n\t\t\treturn `${code} ${file} ${String(stats?.size)} ${String(stats?.mtimeMs)}`;\n\t\t}),\n\t);\n\treturn {\n\t\tremoval: {\n\t\t\tpath,\n\t\t\t...(branch === undefined ? {} : { branch }),\n\t\t\tuncommitted: changed.length,\n\t\t\tignored: ignored.length,\n\t\t\tcommitsOnlyHere: atRisk.length,\n\t\t},\n\t\tfingerprint: JSON.stringify([head, branch ?? null, atRisk, files.sort()]),\n\t};\n}\n\n/**\n * The worktree of this repository at `target`, checked as removable: not the main folder, not\n * one the agent works in, and not locked. Throws a message for the model otherwise.\n */\nasync function removableWorktree(\n\ttarget: string,\n\t{ base, roots }: { base: string; roots: readonly WorkspaceRoot[] },\n\tsignal: AbortSignal,\n) {\n\tconst worktrees = await listWorktrees(base, signal);\n\tconst [main] = worktrees;\n\tif (main?.path === target) {\n\t\tthrow new Error(\n\t\t\t`${target} is the repository's main folder, not a worktree. Only a worktree can be removed.`,\n\t\t);\n\t}\n\tconst worktree = worktrees.find(listed => listed.path === target);\n\tif (!worktree) {\n\t\tconst others = worktrees.slice(1).map(listed => listed.path);\n\t\tthrow new Error(\n\t\t\t`${target} is not a worktree of this repository. Its worktrees: ${others.join(', ') || 'none'}.`,\n\t\t);\n\t}\n\tconst own = await Promise.all(\n\t\t// From the working directory, as the workspace reads them.\n\t\troots.map(root => realpath(resolve(root.path)).catch(() => resolve(root.path))),\n\t);\n\tif (own.some(root => within(root, target) || within(target, root))) {\n\t\tthrow new Error(`You work in ${target}, so it cannot be removed from this run.`);\n\t}\n\tif (worktree.locked) {\n\t\tthrow new Error(`${target} is locked. The user must unlock it to remove it.`);\n\t}\n\treturn worktree;\n}\n\n/**\n * `removeWorktree`: removes a git worktree of the repository the first root is in, its folder\n * and every file in it, never its branch, which keeps its commits. Each call is `destructive`,\n * so it is asked about, and the request names what is lost: changes no commit has, files\n * `.gitignore` excludes, and with a detached HEAD the commits no branch, tag or remote holds. It\n * refuses the repository's main folder, a worktree the agent works in, which holds one of its\n * roots or lies inside one, and a locked one.\n *\n * Once approved it claims the worktree (`FolderClaims`), so no run that may write in it starts,\n * and removes nothing while one works; it checks again that nothing it rests on changed while\n * the user was asked, and otherwise removes nothing, so the model calls it again to have the\n * user asked anew. A folder `.gitignore` excludes whole is checked as a folder, never looked\n * into, and its result tells the model so.\n */\nexport function createRemoveWorktreeTool({\n\tbase,\n\troots,\n\tstateDirectory,\n}: {\n\t/** The real path of the first root. */\n\tbase: string;\n\troots: readonly WorkspaceRoot[];\n\t/** Where the histories are kept, as a real path: their runs' claims are there. */\n\tstateDirectory: string;\n}): PreparingTool {\n\treturn definePreparingTool({\n\t\tname: 'removeWorktree',\n\t\tdescription:\n\t\t\t'Removes a git worktree of this repository: its folder and every file in it, committed or not. Its branch stays, with its commits. Only when the user asks for it.',\n\t\tinput: z.object({\n\t\t\tpath: z\n\t\t\t\t.string()\n\t\t\t\t.describe('The folder of the worktree, absolute or relative to the first root.'),\n\t\t}),\n\t\tsummarize: ({ path }) => path,\n\t\tprepare: async ({ path }, signal) => {\n\t\t\tconst asked = resolve(base, path);\n\t\t\tconst target = await realpath(asked).catch(() => {\n\t\t\t\tthrow new Error(`There is no folder at ${asked}.`);\n\t\t\t});\n\t\t\tconst worktree = await removableWorktree(target, { base, roots }, signal);\n\t\t\tconst { removal, fingerprint } = await inspect(target, worktree.branch, signal);\n\t\t\treturn {\n\t\t\t\trisk: 'destructive',\n\t\t\t\tworktree: removal,\n\t\t\t\trun: async runSignal => {\n\t\t\t\t\tlet release: () => Promise<void>;\n\t\t\t\t\ttry {\n\t\t\t\t\t\trelease = await new FolderClaims(stateDirectory).claimRemoval(target);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tif (!(error instanceof WorkspaceBusyError)) throw error;\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`A run, or a move through a history, may write in ${target}, so nothing was removed. The harness removes no worktree a run may write in; tell the user, and try again once it ends.`,\n\t\t\t\t\t\t\t{ cause: error },\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst now = await removableWorktree(target, { base, roots }, runSignal);\n\t\t\t\t\t\tconst current = await inspect(target, now.branch, runSignal);\n\t\t\t\t\t\tif (current.fingerprint !== fingerprint) {\n\t\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t\t`${target} changed since the user was asked, so nothing was removed. Call removeWorktree again to have them asked with what it holds now.`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Forced: what it holds unsaved is what the user approved losing.\n\t\t\t\t\t\tconst removed = await git(\n\t\t\t\t\t\t\t['worktree', 'remove', '--force', '--', target],\n\t\t\t\t\t\t\tbase,\n\t\t\t\t\t\t\trunSignal,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (removed.exitCode !== 0) {\n\t\t\t\t\t\t\tthrow new Error(`git could not remove ${target}: ${removed.stderr.trim()}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tawait release();\n\t\t\t\t\t}\n\t\t\t\t\tconst done = `Removed the worktree at ${target}.`;\n\t\t\t\t\tconst kept =\n\t\t\t\t\t\tremoval.branch === undefined\n\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t: ` Its branch ${removal.branch} stays, with its commits.`;\n\t\t\t\t\t// Told to the model, which may have to mention it: an ignored folder went unchecked.\n\t\t\t\t\tconst ignored =\n\t\t\t\t\t\tremoval.ignored === 0\n\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t: ` It held ${String(removal.ignored)} ${removal.ignored === 1 ? 'path' : 'paths'} .gitignore excludes, removed whole without looking into them, so anything written in them while the user was asked went too.`;\n\t\t\t\t\treturn `${done}${kept}${ignored}`;\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"removeWorktreeTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/removeWorktreeTool.ts"],"names":[],"mappings":"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","sourcesContent":["import { lstat, realpath } from 'node:fs/promises';\nimport { join, resolve, sep } from 'node:path';\nimport { z } from 'zod';\nimport type { WorktreeRemoval } from '#src/agent/domain/approval';\nimport { WorkspaceBusyError } from '#src/recovery/domain/recoveryStore';\nimport { FolderClaims } from '#src/recovery/infrastructure/folderClaims';\nimport type { WorkspaceRoot } from '../domain/accessPolicy.ts';\nimport type { PreparingTool } from '../domain/preparedCall.ts';\nimport { definePreparingTool } from './defineTool.ts';\nimport { ceilingRefusal } from './writeCeiling.ts';\nimport { git, listWorktrees } from './worktrees.ts';\n\nfunction within(path: string, folder: string): boolean {\n\treturn path === folder || path.startsWith(`${folder}${sep}`);\n}\n\n/**\n * The paths `git status --porcelain -z` names, each with its two-letter code. A rename or copy\n * names its source after it, which is skipped.\n */\nfunction statusEntries(status: string): { code: string; path: string }[] {\n\tconst fields = status.split('\\0');\n\tconst entries: { code: string; path: string }[] = [];\n\tfor (let index = 0; index < fields.length; index++) {\n\t\tconst field = fields[index] ?? '';\n\t\tif (field.length < 4) continue;\n\t\tconst code = field.slice(0, 2);\n\t\tentries.push({ code, path: field.slice(3) });\n\t\tif (code.startsWith('R') || code.startsWith('C')) index++;\n\t}\n\treturn entries;\n}\n\nasync function gitOutput(args: string[], cwd: string, signal: AbortSignal): Promise<string> {\n\tconst { stdout, stderr, exitCode } = await git(args, cwd, signal);\n\tif (exitCode !== 0) {\n\t\tthrow new Error(\n\t\t\t`git could not tell what removing ${cwd} would lose, so nothing was removed: ${stderr.trim()}`,\n\t\t);\n\t}\n\treturn stdout;\n}\n\n/**\n * What removing the worktree at `path` would lose, and a fingerprint of all it rests on: its\n * HEAD, its branch, the commits at risk, and each file `git status` names, by size and time of\n * change. Throws when\n * any of it cannot be read, since a removal must never be asked about with less than it loses.\n */\nasync function inspect(\n\tpath: string,\n\tbranch: string | undefined,\n\tsignal: AbortSignal,\n): Promise<{ removal: WorktreeRemoval; fingerprint: string }> {\n\tconst head = (await gitOutput(['rev-parse', '--verify', 'HEAD'], path, signal)).trim();\n\tconst changed = statusEntries(\n\t\tawait gitOutput(['status', '--porcelain=v1', '-z', '--untracked-files=all'], path, signal),\n\t);\n\t// A folder `.gitignore` excludes whole is named once, however much it holds.\n\tconst ignored = statusEntries(\n\t\tawait gitOutput(\n\t\t\t['status', '--porcelain=v1', '-z', '--untracked-files=normal', '--ignored=matching'],\n\t\t\tpath,\n\t\t\tsignal,\n\t\t),\n\t).filter(({ code }) => code === '!!');\n\t// The commits only a detached HEAD holds, which are lost with it.\n\tconst atRisk: string[] = [];\n\tif (branch === undefined) {\n\t\tconst listed = (\n\t\t\tawait gitOutput(\n\t\t\t\t['rev-list', 'HEAD', '--not', '--branches', '--tags', '--remotes'],\n\t\t\t\tpath,\n\t\t\t\tsignal,\n\t\t\t)\n\t\t)\n\t\t\t.split('\\n')\n\t\t\t.filter(line => line !== '');\n\t\tif (!listed.every(line => /^[0-9a-f]{40,64}$/.test(line))) {\n\t\t\tthrow new Error(\n\t\t\t\t`git could not list the commits only ${path} holds, so nothing was removed: it answered \"${listed.join(' ')}\".`,\n\t\t\t);\n\t\t}\n\t\tatRisk.push(...listed);\n\t}\n\tconst files = await Promise.all(\n\t\t[...changed, ...ignored].map(async ({ code, path: file }) => {\n\t\t\tconst stats = await lstat(join(path, file)).catch(() => undefined);\n\t\t\treturn `${code} ${file} ${String(stats?.size)} ${String(stats?.mtimeMs)}`;\n\t\t}),\n\t);\n\treturn {\n\t\tremoval: {\n\t\t\tpath,\n\t\t\t...(branch === undefined ? {} : { branch }),\n\t\t\tuncommitted: changed.length,\n\t\t\tignored: ignored.length,\n\t\t\tcommitsOnlyHere: atRisk.length,\n\t\t},\n\t\tfingerprint: JSON.stringify([head, branch ?? null, atRisk, files.sort()]),\n\t};\n}\n\n/**\n * The worktree of this repository at `target`, checked as removable: not the main folder, not\n * one the agent works in, and not locked. Throws a message for the model otherwise.\n */\nasync function removableWorktree(\n\ttarget: string,\n\t{ base, roots }: { base: string; roots: readonly WorkspaceRoot[] },\n\tsignal: AbortSignal,\n) {\n\tconst worktrees = await listWorktrees(base, signal);\n\tconst [main] = worktrees;\n\tif (main?.path === target) {\n\t\tthrow new Error(\n\t\t\t`${target} is the repository's main folder, not a worktree. Only a worktree can be removed.`,\n\t\t);\n\t}\n\tconst worktree = worktrees.find(listed => listed.path === target);\n\tif (!worktree) {\n\t\tconst others = worktrees.slice(1).map(listed => listed.path);\n\t\tthrow new Error(\n\t\t\t`${target} is not a worktree of this repository. Its worktrees: ${others.join(', ') || 'none'}.`,\n\t\t);\n\t}\n\tconst own = await Promise.all(\n\t\t// From the working directory, as the workspace reads them.\n\t\troots.map(root => realpath(resolve(root.path)).catch(() => resolve(root.path))),\n\t);\n\tif (own.some(root => within(root, target) || within(target, root))) {\n\t\tthrow new Error(`You work in ${target}, so it cannot be removed from this run.`);\n\t}\n\tif (worktree.locked) {\n\t\tthrow new Error(`${target} is locked. The user must unlock it to remove it.`);\n\t}\n\treturn worktree;\n}\n\n/**\n * `removeWorktree`: removes a git worktree of the repository the first root is in, its folder\n * and every file in it, never its branch, which keeps its commits. Each call is `destructive`,\n * so it is asked about, and the request names what is lost: changes no commit has, files\n * `.gitignore` excludes, and with a detached HEAD the commits no branch, tag or remote holds. It\n * refuses the repository's main folder, a worktree the agent works in, which holds one of its\n * roots or lies inside one, and a locked one.\n *\n * Once approved it claims the worktree (`FolderClaims`), so no run that may write in it starts,\n * and removes nothing while one works; it checks again that nothing it rests on changed while\n * the user was asked, and otherwise removes nothing, so the model calls it again to have the\n * user asked anew. A folder `.gitignore` excludes whole is checked as a folder, never looked\n * into, and its result tells the model so.\n */\nexport function createRemoveWorktreeTool({\n\tbase,\n\troots,\n\tstateDirectory,\n}: {\n\t/** The real path of the first root. */\n\tbase: string;\n\troots: readonly WorkspaceRoot[];\n\t/** Where the histories are kept, as a real path: their runs' claims are there. */\n\tstateDirectory: string;\n}): PreparingTool {\n\treturn definePreparingTool({\n\t\tname: 'removeWorktree',\n\t\tdescription:\n\t\t\t'Removes a git worktree of this repository: its folder and every file in it, committed or not. Its branch stays, with its commits. Only when the user asks for it.',\n\t\tinput: z.object({\n\t\t\tpath: z\n\t\t\t\t.string()\n\t\t\t\t.describe('The folder of the worktree, absolute or relative to the first root.'),\n\t\t}),\n\t\tsummarize: ({ path }) => path,\n\t\tprepare: async ({ path }, signal, { writeCeiling }) => {\n\t\t\tconst asked = resolve(base, path);\n\t\t\tconst target = await realpath(asked).catch(() => {\n\t\t\t\tthrow new Error(`There is no folder at ${asked}.`);\n\t\t\t});\n\t\t\t// Removing it writes there, which an agent another agent started may do only where that\n\t\t\t// agent may.\n\t\t\tconst refusal = ceilingRefusal([target], writeCeiling);\n\t\t\tif (refusal !== undefined) throw new Error(refusal);\n\t\t\tconst worktree = await removableWorktree(target, { base, roots }, signal);\n\t\t\tconst { removal, fingerprint } = await inspect(target, worktree.branch, signal);\n\t\t\treturn {\n\t\t\t\trisk: 'destructive',\n\t\t\t\tworktree: removal,\n\t\t\t\trun: async runSignal => {\n\t\t\t\t\tlet release: () => Promise<void>;\n\t\t\t\t\ttry {\n\t\t\t\t\t\trelease = await new FolderClaims(stateDirectory).claimRemoval(target);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tif (!(error instanceof WorkspaceBusyError)) throw error;\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`A run, or a move through a history, may write in ${target}, so nothing was removed. The harness removes no worktree a run may write in; tell the user, and try again once it ends.`,\n\t\t\t\t\t\t\t{ cause: error },\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst now = await removableWorktree(target, { base, roots }, runSignal);\n\t\t\t\t\t\tconst current = await inspect(target, now.branch, runSignal);\n\t\t\t\t\t\tif (current.fingerprint !== fingerprint) {\n\t\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t\t`${target} changed since the user was asked, so nothing was removed. Call removeWorktree again to have them asked with what it holds now.`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Forced: what it holds unsaved is what the user approved losing.\n\t\t\t\t\t\tconst removed = await git(\n\t\t\t\t\t\t\t['worktree', 'remove', '--force', '--', target],\n\t\t\t\t\t\t\tbase,\n\t\t\t\t\t\t\trunSignal,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (removed.exitCode !== 0) {\n\t\t\t\t\t\t\tthrow new Error(`git could not remove ${target}: ${removed.stderr.trim()}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tawait release();\n\t\t\t\t\t}\n\t\t\t\t\tconst done = `Removed the worktree at ${target}.`;\n\t\t\t\t\tconst kept =\n\t\t\t\t\t\tremoval.branch === undefined\n\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t: ` Its branch ${removal.branch} stays, with its commits.`;\n\t\t\t\t\t// Told to the model, which may have to mention it: an ignored folder went unchecked.\n\t\t\t\t\tconst ignored =\n\t\t\t\t\t\tremoval.ignored === 0\n\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t: ` It held ${String(removal.ignored)} ${removal.ignored === 1 ? 'path' : 'paths'} .gitignore excludes, removed whole without looking into them, so anything written in them while the user was asked went too.`;\n\t\t\t\t\treturn `${done}${kept}${ignored}`;\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
|
|
@@ -9,11 +9,13 @@ export declare function failureOf(error: RecoverableError | UnrecoverableError):
|
|
|
9
9
|
* The options of a subagent a call starts: within the run that made the call, one delegation
|
|
10
10
|
* deeper, and narrated as that call's `subagent`, so the person watching can tell it apart.
|
|
11
11
|
*/
|
|
12
|
-
export declare function subagentOptions(call: ToolCallContext, { tool, name, subagent, task, options, }: {
|
|
12
|
+
export declare function subagentOptions(call: ToolCallContext, { tool, name, subagent, task, options, writeCeiling, }: {
|
|
13
13
|
tool: string;
|
|
14
14
|
name: string;
|
|
15
15
|
subagent: number;
|
|
16
16
|
task: string;
|
|
17
17
|
/** What else it runs with, such as its signal. */
|
|
18
18
|
options: RunOptions;
|
|
19
|
+
/** Where it may write, as real paths; see `delegateCeiling`. */
|
|
20
|
+
writeCeiling: readonly string[] | undefined;
|
|
19
21
|
}): RunOptions;
|
|
@@ -15,7 +15,7 @@ export function failureOf(error) {
|
|
|
15
15
|
* The options of a subagent a call starts: within the run that made the call, one delegation
|
|
16
16
|
* deeper, and narrated as that call's `subagent`, so the person watching can tell it apart.
|
|
17
17
|
*/
|
|
18
|
-
export function subagentOptions(call, { tool, name, subagent, task, options, }) {
|
|
18
|
+
export function subagentOptions(call, { tool, name, subagent, task, options, writeCeiling, }) {
|
|
19
19
|
const onProgress = (event) => {
|
|
20
20
|
call.onProgress({ type: 'subagent', callId: call.callId, subagent, name, task, event });
|
|
21
21
|
};
|
|
@@ -24,6 +24,7 @@ export function subagentOptions(call, { tool, name, subagent, task, options, })
|
|
|
24
24
|
callId: call.callId,
|
|
25
25
|
subagent,
|
|
26
26
|
depth: call.depth + 1,
|
|
27
|
+
...(writeCeiling ? { writeCeiling } : {}),
|
|
27
28
|
});
|
|
28
29
|
}
|
|
29
30
|
//# sourceMappingURL=subagents.js.map
|
|
@@ -1 +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,
|
|
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,EACP,YAAY,GAUZ;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;QACrB,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACzC,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\twriteCeiling,\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\t/** Where it may write, as real paths; see `delegateCeiling`. */\n\t\twriteCeiling: readonly string[] | undefined;\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\t...(writeCeiling ? { writeCeiling } : {}),\n\t});\n}\n"]}
|
|
@@ -39,6 +39,7 @@ async function afterFormatting(writes, editor, { path, absolute, written }, { ru
|
|
|
39
39
|
*/
|
|
40
40
|
export async function prepareTrackedEdit(writes, request, signal, call) {
|
|
41
41
|
const { run, reads } = call;
|
|
42
|
+
await writes.checkCeiling(call);
|
|
42
43
|
const editor = writes.editorFor(run);
|
|
43
44
|
const { path } = request;
|
|
44
45
|
if (request.kind === 'create') {
|
|
@@ -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\tdescribeFormatting,\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. A file as the edit wrote it is known to\n * the conversation, so it can edit it again without reading it. One the formatter rewrote is not:\n * the model would edit on from the lines it sent, so it must read the file again.\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?.equals(written)) reads.record(absolute, hashOf(content));\n\telse reads.forget(absolute);\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, the conversation knows the file as it wrote it, so it can edit it again without\n * reading it, unless the formatter rewrote it. What it returns to the model is short: the lines\n * around a change, or a confirmation, and the lines the formatter rewrote.\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\n\t\t\t\t\t? `${goneAfterFormatting(path)}${note}`\n\t\t\t\t\t: `${describeCreation(path, content)}${describeFormatting(path, request.content.toString('utf8'), content.toString('utf8'))}${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\tif (now === undefined) return `${goneAfterFormatting(path)}${note}`;\n\t\t\tconst formatted = now.toString('utf8');\n\t\t\treturn `${describeModification(path, content.toString('utf8'), formatted)}${describeFormatting(path, next.toString('utf8'), formatted)}${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"]}
|
|
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\tdescribeFormatting,\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. A file as the edit wrote it is known to\n * the conversation, so it can edit it again without reading it. One the formatter rewrote is not:\n * the model would edit on from the lines it sent, so it must read the file again.\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?.equals(written)) reads.record(absolute, hashOf(content));\n\telse reads.forget(absolute);\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, the conversation knows the file as it wrote it, so it can edit it again without\n * reading it, unless the formatter rewrote it. What it returns to the model is short: the lines\n * around a change, or a confirmation, and the lines the formatter rewrote.\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\tawait writes.checkCeiling(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\n\t\t\t\t\t? `${goneAfterFormatting(path)}${note}`\n\t\t\t\t\t: `${describeCreation(path, content)}${describeFormatting(path, request.content.toString('utf8'), content.toString('utf8'))}${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\tif (now === undefined) return `${goneAfterFormatting(path)}${note}`;\n\t\t\tconst formatted = now.toString('utf8');\n\t\t\treturn `${describeModification(path, content.toString('utf8'), formatted)}${describeFormatting(path, next.toString('utf8'), formatted)}${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"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import type { ToolCallContext } from '../domain/readRegistry.ts';
|
|
4
5
|
import { FileEditor } from './fileEditor.ts';
|
|
5
6
|
import type { FileFormatter } from './fileFormatter.ts';
|
|
6
7
|
import { WriteSession } from './writeSession.ts';
|
|
@@ -14,11 +15,22 @@ export declare class WorkspaceWrites {
|
|
|
14
15
|
readonly formatter: FileFormatter | undefined;
|
|
15
16
|
private readonly policy;
|
|
16
17
|
private readonly store;
|
|
17
|
-
|
|
18
|
+
private readonly writeRoots;
|
|
19
|
+
constructor({ policy, store, formatter, writeRoots, }: {
|
|
18
20
|
policy: AccessPolicy;
|
|
19
21
|
store: RecoveryStore;
|
|
20
22
|
formatter?: FileFormatter;
|
|
23
|
+
/** The `write` roots of the tools' workspace. Defaults to the folders the store claims. */
|
|
24
|
+
writeRoots?: readonly string[];
|
|
21
25
|
});
|
|
26
|
+
/**
|
|
27
|
+
* Checks, before anything is written or anyone is asked, that the agent that made the call
|
|
28
|
+
* may write wherever these tools may: an agent another agent started never writes more than
|
|
29
|
+
* that agent may.
|
|
30
|
+
*
|
|
31
|
+
* @throws {EditRefusedError} when these tools may write outside the call's `writeCeiling`.
|
|
32
|
+
*/
|
|
33
|
+
checkCeiling({ writeCeiling }: Pick<ToolCallContext, 'writeCeiling'>): Promise<void>;
|
|
22
34
|
/** An editor over this run's session, under this set of tools' policy. */
|
|
23
35
|
editorFor(context: RunContext): FileEditor;
|
|
24
36
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sep } from 'node:path';
|
|
2
2
|
import { EditRefusedError } from "../domain/fileEdits.js";
|
|
3
3
|
import { FileEditor } from "./fileEditor.js";
|
|
4
|
+
import { ceilingRefusal, realFolders } from "./writeCeiling.js";
|
|
4
5
|
import { WriteSession } from "./writeSession.js";
|
|
5
6
|
// One session per run and history on disk, whichever set of tools asks for it: two sets built
|
|
6
7
|
// apart over one workspace, such as two stages of a workflow, record one run. Each keeps its own
|
|
@@ -19,10 +20,26 @@ export class WorkspaceWrites {
|
|
|
19
20
|
formatter;
|
|
20
21
|
policy;
|
|
21
22
|
store;
|
|
22
|
-
|
|
23
|
+
writeRoots;
|
|
24
|
+
constructor({ policy, store, formatter, writeRoots = store.writeFolders, }) {
|
|
23
25
|
this.policy = policy;
|
|
24
26
|
this.store = store;
|
|
25
27
|
this.formatter = formatter;
|
|
28
|
+
this.writeRoots = realFolders(writeRoots);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Checks, before anything is written or anyone is asked, that the agent that made the call
|
|
32
|
+
* may write wherever these tools may: an agent another agent started never writes more than
|
|
33
|
+
* that agent may.
|
|
34
|
+
*
|
|
35
|
+
* @throws {EditRefusedError} when these tools may write outside the call's `writeCeiling`.
|
|
36
|
+
*/
|
|
37
|
+
async checkCeiling({ writeCeiling }) {
|
|
38
|
+
if (writeCeiling === undefined)
|
|
39
|
+
return;
|
|
40
|
+
const refusal = ceilingRefusal(await this.writeRoots, writeCeiling);
|
|
41
|
+
if (refusal !== undefined)
|
|
42
|
+
throw new EditRefusedError(refusal);
|
|
26
43
|
}
|
|
27
44
|
/** An editor over this run's session, under this set of tools' policy. */
|
|
28
45
|
editorFor(context) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspaceWrites.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/workspaceWrites.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"workspaceWrites.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/workspaceWrites.ts"],"names":[],"mappings":"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","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 { sep } from 'node:path';\nimport { EditRefusedError } from '../domain/fileEdits.ts';\nimport type { ToolCallContext } from '../domain/readRegistry.ts';\nimport { FileEditor } from './fileEditor.ts';\nimport type { FileFormatter } from './fileFormatter.ts';\nimport { ceilingRefusal, realFolders } from './writeCeiling.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<\n\tRunContext,\n\tMap<string, { session: WriteSession; folders: readonly string[] }>\n>();\n\nfunction within(path: string, folder: string): boolean {\n\treturn path === folder || path.startsWith(`${folder}${sep}`);\n}\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\tprivate readonly writeRoots: Promise<string[]>;\n\n\tconstructor({\n\t\tpolicy,\n\t\tstore,\n\t\tformatter,\n\t\twriteRoots = store.writeFolders,\n\t}: {\n\t\tpolicy: AccessPolicy;\n\t\tstore: RecoveryStore;\n\t\tformatter?: FileFormatter;\n\t\t/** The `write` roots of the tools' workspace. Defaults to the folders the store claims. */\n\t\twriteRoots?: readonly string[];\n\t}) {\n\t\tthis.policy = policy;\n\t\tthis.store = store;\n\t\tthis.formatter = formatter;\n\t\tthis.writeRoots = realFolders(writeRoots);\n\t}\n\n\t/**\n\t * Checks, before anything is written or anyone is asked, that the agent that made the call\n\t * may write wherever these tools may: an agent another agent started never writes more than\n\t * that agent may.\n\t *\n\t * @throws {EditRefusedError} when these tools may write outside the call's `writeCeiling`.\n\t */\n\tasync checkCeiling({ writeCeiling }: Pick<ToolCallContext, 'writeCeiling'>): Promise<void> {\n\t\tif (writeCeiling === undefined) return;\n\t\tconst refusal = ceilingRefusal(await this.writeRoots, writeCeiling);\n\t\tif (refusal !== undefined) throw new EditRefusedError(refusal);\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\t * @throws {EditRefusedError} for a set of tools that may write outside the folders of the set\n\t * that started the session: the run claims those alone, so a worktree removed meanwhile could\n\t * lose what it writes there.\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\tconst started = ofRun.get(this.store.workspaceId);\n\t\tif (started) {\n\t\t\tconst outside = this.store.writeFolders.filter(\n\t\t\t\tfolder => !started.folders.some(claimed => within(folder, claimed)),\n\t\t\t);\n\t\t\tif (outside.length > 0) {\n\t\t\t\tthrow new EditRefusedError(\n\t\t\t\t\t`This run already writes to this workspace's history with tools that may write only in ${started.folders.join(', ')}; tools that may also write in ${outside.join(', ')} cannot join it, so nothing was changed. It is a limit of the harness: in one run, every set of tools that writes to a history may write only where the first one may. Tell the user.`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn started.session;\n\t\t}\n\t\tconst session = new WriteSession({\n\t\t\tstore: this.store,\n\t\t\tsession: context.session,\n\t\t\t// So every turn of the run, in every role, can be tied to what it wrote.\n\t\t\tonStart: runId => {\n\t\t\t\tcontext.historyRun.runId = runId;\n\t\t\t},\n\t\t\t// A run that changed nothing is not in the history, so its turns wrote nothing.\n\t\t\tonDiscard: () => {\n\t\t\t\tdelete context.historyRun.runId;\n\t\t\t},\n\t\t});\n\t\tcontext.onFinish(end => session.finish(end));\n\t\tofRun.set(this.store.workspaceId, { session, folders: this.store.writeFolders });\n\t\treturn session;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ToolCallContext } from '../domain/readRegistry.ts';
|
|
2
|
+
/** Each folder as a real path, or resolved when it does not exist. */
|
|
3
|
+
export declare function realFolders(folders: readonly string[]): Promise<string[]>;
|
|
4
|
+
/** The folders of `folders` that lie in none of `ceiling`'s. */
|
|
5
|
+
export declare function outsideCeiling(folders: readonly string[], ceiling: readonly string[]): string[];
|
|
6
|
+
/**
|
|
7
|
+
* Where an agent the call starts may write, as real paths: within the call's own ceiling, and
|
|
8
|
+
* within the `write` roots of the agent that made it. Absent only when neither sets a limit, as
|
|
9
|
+
* for an agent with no workspace that no other agent started.
|
|
10
|
+
*/
|
|
11
|
+
export declare function delegateCeiling(call: Pick<ToolCallContext, 'writeCeiling' | 'agentWriteRoots'>): Promise<readonly string[] | undefined>;
|
|
12
|
+
/**
|
|
13
|
+
* Why a set of tools that may write in `folders` cannot write under `ceiling`, for the model, or
|
|
14
|
+
* `undefined` when it can.
|
|
15
|
+
*/
|
|
16
|
+
export declare function ceilingRefusal(folders: readonly string[], ceiling: readonly string[] | undefined): string | undefined;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises';
|
|
2
|
+
import { resolve, sep } from 'node:path';
|
|
3
|
+
function within(path, folder) {
|
|
4
|
+
return path === folder || path.startsWith(`${folder}${sep}`);
|
|
5
|
+
}
|
|
6
|
+
/** Each folder as a real path, or resolved when it does not exist. */
|
|
7
|
+
export function realFolders(folders) {
|
|
8
|
+
return Promise.all(folders.map(folder => realpath(folder).catch(() => resolve(folder))));
|
|
9
|
+
}
|
|
10
|
+
/** The folders of `folders` that lie in none of `ceiling`'s. */
|
|
11
|
+
export function outsideCeiling(folders, ceiling) {
|
|
12
|
+
return folders.filter(folder => !ceiling.some(allowed => within(folder, allowed)));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Where an agent the call starts may write, as real paths: within the call's own ceiling, and
|
|
16
|
+
* within the `write` roots of the agent that made it. Absent only when neither sets a limit, as
|
|
17
|
+
* for an agent with no workspace that no other agent started.
|
|
18
|
+
*/
|
|
19
|
+
export async function delegateCeiling(call) {
|
|
20
|
+
const { writeCeiling, agentWriteRoots } = call;
|
|
21
|
+
if (agentWriteRoots === undefined)
|
|
22
|
+
return writeCeiling;
|
|
23
|
+
const own = await realFolders(agentWriteRoots);
|
|
24
|
+
if (writeCeiling === undefined)
|
|
25
|
+
return own;
|
|
26
|
+
// Each folder both allow: the narrower one wherever one lies in the other.
|
|
27
|
+
return [
|
|
28
|
+
...new Set([
|
|
29
|
+
...own.filter(folder => writeCeiling.some(allowed => within(folder, allowed))),
|
|
30
|
+
...writeCeiling.filter(folder => own.some(allowed => within(folder, allowed))),
|
|
31
|
+
]),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Why a set of tools that may write in `folders` cannot write under `ceiling`, for the model, or
|
|
36
|
+
* `undefined` when it can.
|
|
37
|
+
*/
|
|
38
|
+
export function ceilingRefusal(folders, ceiling) {
|
|
39
|
+
if (ceiling === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
const outside = outsideCeiling(folders, ceiling);
|
|
42
|
+
if (outside.length === 0)
|
|
43
|
+
return undefined;
|
|
44
|
+
const allowed = ceiling.length === 0
|
|
45
|
+
? 'the agent that handed you this task only reads, and your tools may write in'
|
|
46
|
+
: `the agent that handed you this task may write only in ${ceiling.join(', ')}, and your tools may also write in`;
|
|
47
|
+
return `Nothing was changed: ${allowed} ${outside.join(', ')}. An agent another agent started never writes more than that agent may. Answer with what you found, and say what you would have changed.`;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=writeCeiling.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writeCeiling.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/writeCeiling.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAGzC,SAAS,MAAM,CAAC,IAAY,EAAE,MAAc;IAC3C,OAAO,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,GAAG,EAAE,CAAC,CAAC;AAC9D,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,WAAW,CAAC,OAA0B;IACrD,OAAO,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1F,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,cAAc,CAAC,OAA0B,EAAE,OAA0B;IACpF,OAAO,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;AACpF,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACpC,IAA+D;IAE/D,MAAM,EAAE,YAAY,EAAE,eAAe,EAAE,GAAG,IAAI,CAAC;IAC/C,IAAI,eAAe,KAAK,SAAS;QAAE,OAAO,YAAY,CAAC;IACvD,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,eAAe,CAAC,CAAC;IAC/C,IAAI,YAAY,KAAK,SAAS;QAAE,OAAO,GAAG,CAAC;IAC3C,2EAA2E;IAC3E,OAAO;QACN,GAAG,IAAI,GAAG,CAAC;YACV,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;YAC9E,GAAG,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;SAC9E,CAAC;KACF,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC7B,OAA0B,EAC1B,OAAsC;IAEtC,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC5C,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACjD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC3C,MAAM,OAAO,GACZ,OAAO,CAAC,MAAM,KAAK,CAAC;QACnB,CAAC,CAAC,6EAA6E;QAC/E,CAAC,CAAC,yDAAyD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,oCAAoC,CAAC;IACpH,OAAO,wBAAwB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,0IAA0I,CAAC;AACxM,CAAC","sourcesContent":["import { realpath } from 'node:fs/promises';\nimport { resolve, sep } from 'node:path';\nimport type { ToolCallContext } from '../domain/readRegistry.ts';\n\nfunction within(path: string, folder: string): boolean {\n\treturn path === folder || path.startsWith(`${folder}${sep}`);\n}\n\n/** Each folder as a real path, or resolved when it does not exist. */\nexport function realFolders(folders: readonly string[]): Promise<string[]> {\n\treturn Promise.all(folders.map(folder => realpath(folder).catch(() => resolve(folder))));\n}\n\n/** The folders of `folders` that lie in none of `ceiling`'s. */\nexport function outsideCeiling(folders: readonly string[], ceiling: readonly string[]): string[] {\n\treturn folders.filter(folder => !ceiling.some(allowed => within(folder, allowed)));\n}\n\n/**\n * Where an agent the call starts may write, as real paths: within the call's own ceiling, and\n * within the `write` roots of the agent that made it. Absent only when neither sets a limit, as\n * for an agent with no workspace that no other agent started.\n */\nexport async function delegateCeiling(\n\tcall: Pick<ToolCallContext, 'writeCeiling' | 'agentWriteRoots'>,\n): Promise<readonly string[] | undefined> {\n\tconst { writeCeiling, agentWriteRoots } = call;\n\tif (agentWriteRoots === undefined) return writeCeiling;\n\tconst own = await realFolders(agentWriteRoots);\n\tif (writeCeiling === undefined) return own;\n\t// Each folder both allow: the narrower one wherever one lies in the other.\n\treturn [\n\t\t...new Set([\n\t\t\t...own.filter(folder => writeCeiling.some(allowed => within(folder, allowed))),\n\t\t\t...writeCeiling.filter(folder => own.some(allowed => within(folder, allowed))),\n\t\t]),\n\t];\n}\n\n/**\n * Why a set of tools that may write in `folders` cannot write under `ceiling`, for the model, or\n * `undefined` when it can.\n */\nexport function ceilingRefusal(\n\tfolders: readonly string[],\n\tceiling: readonly string[] | undefined,\n): string | undefined {\n\tif (ceiling === undefined) return undefined;\n\tconst outside = outsideCeiling(folders, ceiling);\n\tif (outside.length === 0) return undefined;\n\tconst allowed =\n\t\tceiling.length === 0\n\t\t\t? 'the agent that handed you this task only reads, and your tools may write in'\n\t\t\t: `the agent that handed you this task may write only in ${ceiling.join(', ')}, and your tools may also write in`;\n\treturn `Nothing was changed: ${allowed} ${outside.join(', ')}. An agent another agent started never writes more than that agent may. Answer with what you found, and say what you would have changed.`;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikode13/harness",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.2.0",
|
|
4
4
|
"description": "Test/agent harness utility",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"@anthropic-ai/claude-agent-sdk": "^0.3.220",
|
|
65
65
|
"@anthropic-ai/sdk": "^0.115.0",
|
|
66
66
|
"@openai/codex-sdk": "^0.154.0",
|
|
67
|
+
"@typesafe-ai/sdk": "0.7.0",
|
|
67
68
|
"@vscode/ripgrep": "^1.18.0",
|
|
68
69
|
"openai": "^7.23.0"
|
|
69
70
|
},
|