@mikode13/harness 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +109 -9
  2. package/dist/agent/domain/agent.d.ts +23 -3
  3. package/dist/agent/domain/agent.js +2 -1
  4. package/dist/agent/domain/agent.js.map +1 -1
  5. package/dist/agent/domain/approval.d.ts +44 -0
  6. package/dist/agent/domain/approval.js +23 -0
  7. package/dist/agent/domain/approval.js.map +1 -0
  8. package/dist/engines/claude/infrastructure/model/claudeAgent.d.ts +2 -2
  9. package/dist/engines/claude/infrastructure/model/claudeAgent.js +4 -3
  10. package/dist/engines/claude/infrastructure/model/claudeAgent.js.map +1 -1
  11. package/dist/engines/codex/infrastructure/model/codexAgent.d.ts +2 -2
  12. package/dist/engines/codex/infrastructure/model/codexAgent.js +4 -4
  13. package/dist/engines/codex/infrastructure/model/codexAgent.js.map +1 -1
  14. package/dist/engines/domain/model/llmAgent.d.ts +15 -5
  15. package/dist/engines/domain/model/llmAgent.js +109 -26
  16. package/dist/engines/domain/model/llmAgent.js.map +1 -1
  17. package/dist/factory/infrastructure/agentFactory.js +1 -1
  18. package/dist/factory/infrastructure/agentFactory.js.map +1 -1
  19. package/dist/factory/infrastructure/agentLLMFactory.d.ts +14 -7
  20. package/dist/factory/infrastructure/agentLLMFactory.js +11 -9
  21. package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
  22. package/dist/index.d.ts +10 -2
  23. package/dist/index.js +6 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/llm/domain/errors.d.ts +1 -1
  26. package/dist/llm/domain/errors.js +1 -1
  27. package/dist/llm/domain/errors.js.map +1 -1
  28. package/dist/llm/infrastructure/claudeLLMClient.js +2 -2
  29. package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -1
  30. package/dist/llm/infrastructure/openAILLMClient.js +2 -2
  31. package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
  32. package/dist/orchestration/domain/model/instructedAgent.d.ts +2 -2
  33. package/dist/orchestration/domain/model/instructedAgent.js +2 -2
  34. package/dist/orchestration/domain/model/instructedAgent.js.map +1 -1
  35. package/dist/orchestration/domain/model/orchestratorAgent.d.ts +2 -2
  36. package/dist/orchestration/domain/model/orchestratorAgent.js +10 -10
  37. package/dist/orchestration/domain/model/orchestratorAgent.js.map +1 -1
  38. package/dist/retry/domain/model/retryingAgent.d.ts +2 -2
  39. package/dist/retry/domain/model/retryingAgent.js +4 -4
  40. package/dist/retry/domain/model/retryingAgent.js.map +1 -1
  41. package/dist/shared/domain/errors.js.map +1 -0
  42. package/dist/shared/domain/providerFailure.js.map +1 -0
  43. package/dist/tools/domain/tool.d.ts +6 -0
  44. package/dist/tools/domain/tool.js.map +1 -1
  45. package/dist/tools/infrastructure/createWorkspace.d.ts +2 -1
  46. package/dist/tools/infrastructure/createWorkspace.js +4 -3
  47. package/dist/tools/infrastructure/createWorkspace.js.map +1 -1
  48. package/dist/tools/infrastructure/defineTool.d.ts +7 -1
  49. package/dist/tools/infrastructure/defineTool.js +12 -2
  50. package/dist/tools/infrastructure/defineTool.js.map +1 -1
  51. package/dist/tools/infrastructure/workspaceTools.js +3 -0
  52. package/dist/tools/infrastructure/workspaceTools.js.map +1 -1
  53. package/package.json +7 -4
  54. package/dist/agent/domain/errors.js.map +0 -1
  55. package/dist/agent/domain/providerFailure.js.map +0 -1
  56. /package/dist/{agent → shared}/domain/errors.d.ts +0 -0
  57. /package/dist/{agent → shared}/domain/errors.js +0 -0
  58. /package/dist/{agent → shared}/domain/providerFailure.d.ts +0 -0
  59. /package/dist/{agent → shared}/domain/providerFailure.js +0 -0
@@ -1,5 +1,6 @@
1
- import { UnrecoverableError } from '#src/agent/domain/errors';
2
- import { classifyHostFailure, treatErrors } from '#src/agent/domain/providerFailure';
1
+ import { UnrecoverableError } from '#src/shared/domain/errors';
2
+ import { classifyHostFailure, treatErrors } from '#src/shared/domain/providerFailure';
3
+ import { Logger } from '#src/shared/infrastructure/logger';
3
4
  import { GitWorkspace } from "./gitWorkspace.js";
4
5
  import { RipgrepWorkspace } from "./ripgrepWorkspace.js";
5
6
  import { runProcess } from "./runProcess.js";
@@ -36,7 +37,7 @@ async function isGitWorkTree(root) {
36
37
  *
37
38
  * @throws {UnrecoverableError} when neither ripgrep nor a git repository is available.
38
39
  */
39
- export async function createWorkspace({ root, logger, }) {
40
+ export async function createWorkspace({ root, logger = new Logger(), }) {
40
41
  const ripgrepPath = await loadRipgrep();
41
42
  if (ripgrepPath !== undefined) {
42
43
  return new RipgrepWorkspace({ root, ripgrepPath });
@@ -1 +1 @@
1
- {"version":3,"file":"createWorkspace.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/createWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAGrF,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,gGAAgG;AAChG,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEhC,KAAK,UAAU,WAAW;IACzB,IAAI,CAAC;QACJ,yEAAyE;QACzE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;QACnD,OAAO,MAAM,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;AACF,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC;QACJ,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,UAAU,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,uBAAuB,CAAC,EAAE;YAC5F,GAAG,EAAE,IAAI;YACT,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,iBAAiB,CAAC;SAC9C,CAAC,CAAC;QACH,OAAO,QAAQ,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACR,6CAA6C;QAC7C,OAAO,KAAK,CAAC;IACd,CAAC;AACF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,EACrC,IAAI,EACJ,MAAM,GAIN;IACA,MAAM,WAAW,GAAG,MAAM,WAAW,EAAE,CAAC;IACxC,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,IAAI,gBAAgB,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;IACpD,CAAC;IAED,IAAI,MAAM,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,WAAW,CACV,GAAG,EAAE;YACJ,MAAM,CAAC,IAAI,CACV,+EAA+E,CAC/E,CAAC;QACH,CAAC,EACD,mBAAmB,EACnB,0DAA0D,CAC1D,CAAC;QACF,OAAO,IAAI,YAAY,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;IACnC,CAAC;IAED,MAAM,IAAI,kBAAkB,CAAC,mCAAmC,EAAE;QACjE,KAAK,EAAE,kDAAkD,IAAI,2BAA2B;KACxF,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import { UnrecoverableError } from '#src/agent/domain/errors';\nimport { classifyHostFailure, treatErrors } from '#src/agent/domain/providerFailure';\nimport type { ILogger } from '#src/shared/domain/logger';\nimport type { Workspace } from '../domain/workspace.ts';\nimport { GitWorkspace } from './gitWorkspace.ts';\nimport { RipgrepWorkspace } from './ripgrepWorkspace.ts';\nimport { runProcess } from './runProcess.ts';\n\n// Long enough for a slow disk; short enough that a hung git does not hold up building an agent.\nconst gitCheckTimeoutMs = 5_000;\n\nasync function loadRipgrep(): Promise<string | undefined> {\n\ttry {\n\t\t// Dynamic: the package throws on import when the platform has no binary.\n\t\tconst { rgPath } = await import('@vscode/ripgrep');\n\t\treturn rgPath;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nasync function isGitWorkTree(root: string): Promise<boolean> {\n\ttry {\n\t\tconst { stdout, exitCode } = await runProcess('git', ['rev-parse', '--is-inside-work-tree'], {\n\t\t\tcwd: root,\n\t\t\tsignal: AbortSignal.timeout(gitCheckTimeoutMs),\n\t\t});\n\t\treturn exitCode === 0 && stdout.trim() === 'true';\n\t} catch {\n\t\t// No git at all, or one that never answered.\n\t\treturn false;\n\t}\n}\n\n/**\n * Picks how to read `root`: the ripgrep that ships with the package, or git when this platform\n * has no ripgrep binary, which is logged because it is a degraded path. Plain `grep` is never a\n * fallback: it knows nothing of `.gitignore`. See \"`.gitignore` is the boundary of what an agent\n * reads\" in decisions.md.\n *\n * @throws {UnrecoverableError} when neither ripgrep nor a git repository is available.\n */\nexport async function createWorkspace({\n\troot,\n\tlogger,\n}: {\n\troot: string;\n\tlogger: ILogger;\n}): Promise<Workspace> {\n\tconst ripgrepPath = await loadRipgrep();\n\tif (ripgrepPath !== undefined) {\n\t\treturn new RipgrepWorkspace({ root, ripgrepPath });\n\t}\n\n\tif (await isGitWorkTree(root)) {\n\t\ttreatErrors(\n\t\t\t() => {\n\t\t\t\tlogger.warn(\n\t\t\t\t\t'ripgrep is not available on this platform; reading the repository through git',\n\t\t\t\t);\n\t\t\t},\n\t\t\tclassifyHostFailure,\n\t\t\t'Workspace logger failed while choosing an implementation',\n\t\t);\n\t\treturn new GitWorkspace({ root });\n\t}\n\n\tthrow new UnrecoverableError('No program can read the workspace', {\n\t\tcause: `ripgrep is not available on this platform, and ${root} is not a git repository.`,\n\t});\n}\n"]}
1
+ {"version":3,"file":"createWorkspace.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/createWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAEtF,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,gGAAgG;AAChG,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEhC,KAAK,UAAU,WAAW;IACzB,IAAI,CAAC;QACJ,yEAAyE;QACzE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;QACnD,OAAO,MAAM,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;AACF,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC;QACJ,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,UAAU,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,uBAAuB,CAAC,EAAE;YAC5F,GAAG,EAAE,IAAI;YACT,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,iBAAiB,CAAC;SAC9C,CAAC,CAAC;QACH,OAAO,QAAQ,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACR,6CAA6C;QAC7C,OAAO,KAAK,CAAC;IACd,CAAC;AACF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,EACrC,IAAI,EACJ,MAAM,GAAG,IAAI,MAAM,EAAE,GAKrB;IACA,MAAM,WAAW,GAAG,MAAM,WAAW,EAAE,CAAC;IACxC,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,IAAI,gBAAgB,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;IACpD,CAAC;IAED,IAAI,MAAM,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,WAAW,CACV,GAAG,EAAE;YACJ,MAAM,CAAC,IAAI,CACV,+EAA+E,CAC/E,CAAC;QACH,CAAC,EACD,mBAAmB,EACnB,0DAA0D,CAC1D,CAAC;QACF,OAAO,IAAI,YAAY,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;IACnC,CAAC;IAED,MAAM,IAAI,kBAAkB,CAAC,mCAAmC,EAAE;QACjE,KAAK,EAAE,kDAAkD,IAAI,2BAA2B;KACxF,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import { UnrecoverableError } from '#src/shared/domain/errors';\nimport { classifyHostFailure, treatErrors } from '#src/shared/domain/providerFailure';\nimport type { ILogger } from '#src/shared/domain/logger';\nimport { Logger } from '#src/shared/infrastructure/logger';\nimport type { Workspace } from '../domain/workspace.ts';\nimport { GitWorkspace } from './gitWorkspace.ts';\nimport { RipgrepWorkspace } from './ripgrepWorkspace.ts';\nimport { runProcess } from './runProcess.ts';\n\n// Long enough for a slow disk; short enough that a hung git does not hold up building an agent.\nconst gitCheckTimeoutMs = 5_000;\n\nasync function loadRipgrep(): Promise<string | undefined> {\n\ttry {\n\t\t// Dynamic: the package throws on import when the platform has no binary.\n\t\tconst { rgPath } = await import('@vscode/ripgrep');\n\t\treturn rgPath;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nasync function isGitWorkTree(root: string): Promise<boolean> {\n\ttry {\n\t\tconst { stdout, exitCode } = await runProcess('git', ['rev-parse', '--is-inside-work-tree'], {\n\t\t\tcwd: root,\n\t\t\tsignal: AbortSignal.timeout(gitCheckTimeoutMs),\n\t\t});\n\t\treturn exitCode === 0 && stdout.trim() === 'true';\n\t} catch {\n\t\t// No git at all, or one that never answered.\n\t\treturn false;\n\t}\n}\n\n/**\n * Picks how to read `root`: the ripgrep that ships with the package, or git when this platform\n * has no ripgrep binary, which is logged because it is a degraded path. Plain `grep` is never a\n * fallback: it knows nothing of `.gitignore`. See \"`.gitignore` is the boundary of what an agent\n * reads\" in decisions.md.\n *\n * @throws {UnrecoverableError} when neither ripgrep nor a git repository is available.\n */\nexport async function createWorkspace({\n\troot,\n\tlogger = new Logger(),\n}: {\n\troot: string;\n\t/** Defaults to warnings on stderr. */\n\tlogger?: ILogger;\n}): Promise<Workspace> {\n\tconst ripgrepPath = await loadRipgrep();\n\tif (ripgrepPath !== undefined) {\n\t\treturn new RipgrepWorkspace({ root, ripgrepPath });\n\t}\n\n\tif (await isGitWorkTree(root)) {\n\t\ttreatErrors(\n\t\t\t() => {\n\t\t\t\tlogger.warn(\n\t\t\t\t\t'ripgrep is not available on this platform; reading the repository through git',\n\t\t\t\t);\n\t\t\t},\n\t\t\tclassifyHostFailure,\n\t\t\t'Workspace logger failed while choosing an implementation',\n\t\t);\n\t\treturn new GitWorkspace({ root });\n\t}\n\n\tthrow new UnrecoverableError('No program can read the workspace', {\n\t\tcause: `ripgrep is not available on this platform, and ${root} is not a git repository.`,\n\t});\n}\n"]}
@@ -1,15 +1,21 @@
1
1
  import { z } from 'zod';
2
+ import type { ToolRisk } from '#src/agent/domain/approval';
2
3
  import type { Tool } from '../domain/tool.ts';
3
4
  /**
4
5
  * Builds a `Tool` from one Zod schema, which gives the model its JSON Schema, validates what the
5
6
  * model sends, and types the input `execute` receives. Invalid input is rejected with a message
6
7
  * naming what is wrong, which the agent hands back to the model as an error result.
7
8
  *
9
+ * `risk` is one level for every call, or a function that judges each call from its validated
10
+ * input, such as `destructive` only when a file already exists. Input that fails validation
11
+ * counts as `safe`, because `execute` rejects it before doing anything.
12
+ *
8
13
  * @throws {InvalidAgentConfigError} when the schema falls outside what strict mode supports.
9
14
  */
10
- export declare function defineTool<Schema extends z.ZodObject>({ name, description, input, execute, }: {
15
+ export declare function defineTool<Schema extends z.ZodObject>({ name, description, input, risk, execute, }: {
11
16
  name: string;
12
17
  description: string;
13
18
  input: Schema;
19
+ risk: ToolRisk | ((input: z.infer<Schema>) => ToolRisk);
14
20
  execute: (input: z.infer<Schema>, signal: AbortSignal) => Promise<string>;
15
21
  }): Tool;
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { InvalidAgentConfigError } from '#src/agent/domain/errors';
2
+ import { InvalidAgentConfigError } from '#src/shared/domain/errors';
3
3
  // Both providers take tools in strict mode, which supports only part of JSON Schema. Claude
4
4
  // rejects these bounds; a tool that needs one checks it in its own code instead.
5
5
  const unsupportedKeywords = [
@@ -64,9 +64,13 @@ function assertStrict(schema, toolName) {
64
64
  * model sends, and types the input `execute` receives. Invalid input is rejected with a message
65
65
  * naming what is wrong, which the agent hands back to the model as an error result.
66
66
  *
67
+ * `risk` is one level for every call, or a function that judges each call from its validated
68
+ * input, such as `destructive` only when a file already exists. Input that fails validation
69
+ * counts as `safe`, because `execute` rejects it before doing anything.
70
+ *
67
71
  * @throws {InvalidAgentConfigError} when the schema falls outside what strict mode supports.
68
72
  */
69
- export function defineTool({ name, description, input, execute, }) {
73
+ export function defineTool({ name, description, input, risk, execute, }) {
70
74
  const inputSchema = { ...z.toJSONSchema(input) };
71
75
  // The dialect marker is noise to both APIs.
72
76
  delete inputSchema.$schema;
@@ -75,6 +79,12 @@ export function defineTool({ name, description, input, execute, }) {
75
79
  name,
76
80
  description,
77
81
  inputSchema: inputSchema,
82
+ risk: raw => {
83
+ if (typeof risk === 'string')
84
+ return risk;
85
+ const parsed = input.safeParse(raw);
86
+ return parsed.success ? risk(parsed.data) : 'safe';
87
+ },
78
88
  execute: (raw, signal) => {
79
89
  const parsed = input.safeParse(raw);
80
90
  if (!parsed.success) {
@@ -1 +1 @@
1
- {"version":3,"file":"defineTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/defineTool.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod';\nimport { InvalidAgentConfigError } from '#src/agent/domain/errors';\nimport type { JSONSchema } from '#src/llm/domain/tool';\nimport type { Tool } from '../domain/tool.ts';\n\n// Both providers take tools in strict mode, which supports only part of JSON Schema. Claude\n// rejects these bounds; a tool that needs one checks it in its own code instead.\nconst unsupportedKeywords = [\n\t'minimum',\n\t'maximum',\n\t'exclusiveMinimum',\n\t'exclusiveMaximum',\n\t'multipleOf',\n\t'minLength',\n\t'maxLength',\n];\n\n// Keywords whose value is one schema, or a list or map of them. Anything else, such as the\n// keys of `properties`, is a name or a value, never a schema to check.\nconst schemaKeywords = ['items', 'not', 'additionalProperties'];\nconst schemaListKeywords = ['anyOf', 'oneOf', 'allOf', 'prefixItems'];\nconst schemaMapKeywords = ['properties', '$defs', 'definitions'];\n\nfunction isSchema(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Walks the whole schema, nested objects included, and visits only schemas: a property named\n * `maxLength` is a name, not the keyword. A schema the providers reject would fail every\n * request with a 400, so it fails here, when the tool is defined.\n */\nfunction assertStrict(schema: unknown, toolName: string): void {\n\tif (!isSchema(schema)) {\n\t\treturn;\n\t}\n\n\tconst keyword = unsupportedKeywords.find(candidate => candidate in schema);\n\tif (keyword) {\n\t\tthrow new InvalidAgentConfigError(\n\t\t\t`Tool \"${toolName}\" uses \"${keyword}\", which strict mode does not support; check it in the tool's code`,\n\t\t);\n\t}\n\n\tif (schema.type === 'object') {\n\t\t// A record or a loose object takes keys strict mode cannot know in advance.\n\t\tif (schema.additionalProperties !== false) {\n\t\t\tthrow new InvalidAgentConfigError(\n\t\t\t\t`Tool \"${toolName}\" has an object open to any key; strict mode needs every key declared`,\n\t\t\t);\n\t\t}\n\n\t\tconst required = (schema.required ?? []) as string[];\n\t\t// OpenAI requires every property: an optional one is a union with `null`.\n\t\tconst optional = Object.keys(schema.properties ?? {}).filter(\n\t\t\tproperty => !required.includes(property),\n\t\t);\n\t\tif (optional.length > 0) {\n\t\t\tthrow new InvalidAgentConfigError(\n\t\t\t\t`Tool \"${toolName}\" has optional fields (${optional.join(', ')}); make them .nullable() instead`,\n\t\t\t);\n\t\t}\n\t}\n\n\tfor (const key of schemaKeywords) assertStrict(schema[key], toolName);\n\tfor (const key of schemaListKeywords) {\n\t\tconst list = schema[key];\n\t\tif (Array.isArray(list)) for (const item of list) assertStrict(item, toolName);\n\t}\n\tfor (const key of schemaMapKeywords) {\n\t\tconst map = schema[key];\n\t\tif (isSchema(map)) for (const item of Object.values(map)) assertStrict(item, toolName);\n\t}\n}\n\n/**\n * Builds a `Tool` from one Zod schema, which gives the model its JSON Schema, validates what the\n * model sends, and types the input `execute` receives. Invalid input is rejected with a message\n * naming what is wrong, which the agent hands back to the model as an error result.\n *\n * @throws {InvalidAgentConfigError} when the schema falls outside what strict mode supports.\n */\nexport function defineTool<Schema extends z.ZodObject>({\n\tname,\n\tdescription,\n\tinput,\n\texecute,\n}: {\n\tname: string;\n\tdescription: string;\n\tinput: Schema;\n\texecute: (input: z.infer<Schema>, signal: AbortSignal) => Promise<string>;\n}): Tool {\n\tconst inputSchema: Record<string, unknown> = { ...z.toJSONSchema(input) };\n\t// The dialect marker is noise to both APIs.\n\tdelete inputSchema.$schema;\n\tassertStrict(inputSchema, name);\n\n\treturn {\n\t\tname,\n\t\tdescription,\n\t\tinputSchema: inputSchema as JSONSchema,\n\t\texecute: (raw, signal) => {\n\t\t\tconst parsed = input.safeParse(raw);\n\t\t\tif (!parsed.success) {\n\t\t\t\treturn Promise.reject(new Error(z.prettifyError(parsed.error)));\n\t\t\t}\n\n\t\t\treturn execute(parsed.data, signal);\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"defineTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/defineTool.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod';\nimport type { ToolRisk } from '#src/agent/domain/approval';\nimport { InvalidAgentConfigError } from '#src/shared/domain/errors';\nimport type { JSONSchema } from '#src/llm/domain/tool';\nimport type { Tool } from '../domain/tool.ts';\n\n// Both providers take tools in strict mode, which supports only part of JSON Schema. Claude\n// rejects these bounds; a tool that needs one checks it in its own code instead.\nconst unsupportedKeywords = [\n\t'minimum',\n\t'maximum',\n\t'exclusiveMinimum',\n\t'exclusiveMaximum',\n\t'multipleOf',\n\t'minLength',\n\t'maxLength',\n];\n\n// Keywords whose value is one schema, or a list or map of them. Anything else, such as the\n// keys of `properties`, is a name or a value, never a schema to check.\nconst schemaKeywords = ['items', 'not', 'additionalProperties'];\nconst schemaListKeywords = ['anyOf', 'oneOf', 'allOf', 'prefixItems'];\nconst schemaMapKeywords = ['properties', '$defs', 'definitions'];\n\nfunction isSchema(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Walks the whole schema, nested objects included, and visits only schemas: a property named\n * `maxLength` is a name, not the keyword. A schema the providers reject would fail every\n * request with a 400, so it fails here, when the tool is defined.\n */\nfunction assertStrict(schema: unknown, toolName: string): void {\n\tif (!isSchema(schema)) {\n\t\treturn;\n\t}\n\n\tconst keyword = unsupportedKeywords.find(candidate => candidate in schema);\n\tif (keyword) {\n\t\tthrow new InvalidAgentConfigError(\n\t\t\t`Tool \"${toolName}\" uses \"${keyword}\", which strict mode does not support; check it in the tool's code`,\n\t\t);\n\t}\n\n\tif (schema.type === 'object') {\n\t\t// A record or a loose object takes keys strict mode cannot know in advance.\n\t\tif (schema.additionalProperties !== false) {\n\t\t\tthrow new InvalidAgentConfigError(\n\t\t\t\t`Tool \"${toolName}\" has an object open to any key; strict mode needs every key declared`,\n\t\t\t);\n\t\t}\n\n\t\tconst required = (schema.required ?? []) as string[];\n\t\t// OpenAI requires every property: an optional one is a union with `null`.\n\t\tconst optional = Object.keys(schema.properties ?? {}).filter(\n\t\t\tproperty => !required.includes(property),\n\t\t);\n\t\tif (optional.length > 0) {\n\t\t\tthrow new InvalidAgentConfigError(\n\t\t\t\t`Tool \"${toolName}\" has optional fields (${optional.join(', ')}); make them .nullable() instead`,\n\t\t\t);\n\t\t}\n\t}\n\n\tfor (const key of schemaKeywords) assertStrict(schema[key], toolName);\n\tfor (const key of schemaListKeywords) {\n\t\tconst list = schema[key];\n\t\tif (Array.isArray(list)) for (const item of list) assertStrict(item, toolName);\n\t}\n\tfor (const key of schemaMapKeywords) {\n\t\tconst map = schema[key];\n\t\tif (isSchema(map)) for (const item of Object.values(map)) assertStrict(item, toolName);\n\t}\n}\n\n/**\n * Builds a `Tool` from one Zod schema, which gives the model its JSON Schema, validates what the\n * model sends, and types the input `execute` receives. Invalid input is rejected with a message\n * naming what is wrong, which the agent hands back to the model as an error result.\n *\n * `risk` is one level for every call, or a function that judges each call from its validated\n * input, such as `destructive` only when a file already exists. Input that fails validation\n * counts as `safe`, because `execute` rejects it before doing anything.\n *\n * @throws {InvalidAgentConfigError} when the schema falls outside what strict mode supports.\n */\nexport function defineTool<Schema extends z.ZodObject>({\n\tname,\n\tdescription,\n\tinput,\n\trisk,\n\texecute,\n}: {\n\tname: string;\n\tdescription: string;\n\tinput: Schema;\n\trisk: ToolRisk | ((input: z.infer<Schema>) => ToolRisk);\n\texecute: (input: z.infer<Schema>, signal: AbortSignal) => Promise<string>;\n}): Tool {\n\tconst inputSchema: Record<string, unknown> = { ...z.toJSONSchema(input) };\n\t// The dialect marker is noise to both APIs.\n\tdelete inputSchema.$schema;\n\tassertStrict(inputSchema, name);\n\n\treturn {\n\t\tname,\n\t\tdescription,\n\t\tinputSchema: inputSchema as JSONSchema,\n\t\trisk: raw => {\n\t\t\tif (typeof risk === 'string') return risk;\n\t\t\tconst parsed = input.safeParse(raw);\n\t\t\treturn parsed.success ? risk(parsed.data) : 'safe';\n\t\t},\n\t\texecute: (raw, signal) => {\n\t\t\tconst parsed = input.safeParse(raw);\n\t\t\tif (!parsed.success) {\n\t\t\t\treturn Promise.reject(new Error(z.prettifyError(parsed.error)));\n\t\t\t}\n\n\t\t\treturn execute(parsed.data, signal);\n\t\t},\n\t};\n}\n"]}
@@ -26,6 +26,7 @@ export function createWorkspaceTools(workspace) {
26
26
  defineTool({
27
27
  name: 'listFiles',
28
28
  description: 'Lists the files of the repository, sorted. Ignored files such as dependencies are left out.',
29
+ risk: 'safe',
29
30
  input: z.object(scope),
30
31
  execute: async ({ path, glob }, signal) => {
31
32
  // The port takes "no scope" as a missing field.
@@ -43,6 +44,7 @@ export function createWorkspaceTools(workspace) {
43
44
  defineTool({
44
45
  name: 'searchText',
45
46
  description: 'Searches the repository for a regular expression and returns each matching line as path:line: text.',
47
+ risk: 'safe',
46
48
  input: z.object({
47
49
  pattern: z.string().describe('A regular expression.'),
48
50
  ignoreCase: z.boolean().nullable().describe('Match regardless of case. Defaults to false.'),
@@ -69,6 +71,7 @@ export function createWorkspaceTools(workspace) {
69
71
  defineTool({
70
72
  name: 'readFile',
71
73
  description: 'Reads lines of a file, each prefixed with its number.',
74
+ risk: 'safe',
72
75
  input: z.object({
73
76
  path: z.string().describe('The file, relative to the root.'),
74
77
  fromLine: z.number().nullable().describe('The first line to read, from 1. Defaults to 1.'),
@@ -1 +1 @@
1
- {"version":3,"file":"workspaceTools.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/workspaceTools.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod';\nimport type { Tool } from '../domain/tool.ts';\nimport type { Workspace } from '../domain/workspace.ts';\nimport { defineTool } from './defineTool.ts';\n\n// What one call may return. The model can ask for fewer lines, never for more.\nconst maxFiles = 200;\nconst maxMatches = 100;\nconst maxLines = 200;\n// One minified line can be larger than everything else the model has read.\nconst maxLineLength = 300;\n\n// Strict mode has no optional fields: an argument the model may leave out is nullable.\nconst scope = {\n\tpath: z.string().nullable().describe('A folder or file to stay inside, relative to the root.'),\n\tglob: z.string().nullable().describe('Only paths matching this glob, such as \"**/*.ts\".'),\n};\n\n/** One line as the model reads it: a minified bundle on a single line would flood its context. */\nfunction cutLine(text: string): string {\n\treturn text.length > maxLineLength ? `${text.slice(0, maxLineLength)}…` : text;\n}\n\nfunction assertPositiveInteger(name: string, value: number): void {\n\tif (!Number.isInteger(value) || value < 1) {\n\t\tthrow new Error(`${name} must be a whole number of at least 1; got ${String(value)}`);\n\t}\n}\n\n/** The three read-only tools an agent explores a repository with. */\nexport function createWorkspaceTools(workspace: Workspace): Tool[] {\n\treturn [\n\t\tdefineTool({\n\t\t\tname: 'listFiles',\n\t\t\tdescription:\n\t\t\t\t'Lists the files of the repository, sorted. Ignored files such as dependencies are left out.',\n\t\t\tinput: z.object(scope),\n\t\t\texecute: async ({ path, glob }, signal) => {\n\t\t\t\t// The port takes \"no scope\" as a missing field.\n\t\t\t\tconst { files, total, truncated } = await workspace.listFiles(\n\t\t\t\t\t{ path: path ?? undefined, glob: glob ?? undefined, limit: maxFiles },\n\t\t\t\t\tsignal,\n\t\t\t\t);\n\n\t\t\t\t// Nothing found is an answer too, not an empty string.\n\t\t\t\tif (files.length === 0) {\n\t\t\t\t\treturn 'No files.';\n\t\t\t\t}\n\n\t\t\t\tconst notice = truncated\n\t\t\t\t\t? `\\n[Showing ${String(files.length)} of ${String(total)} files. Narrow the path or glob to see the rest.]`\n\t\t\t\t\t: '';\n\t\t\t\treturn files.join('\\n') + notice;\n\t\t\t},\n\t\t}),\n\n\t\tdefineTool({\n\t\t\tname: 'searchText',\n\t\t\tdescription:\n\t\t\t\t'Searches the repository for a regular expression and returns each matching line as path:line: text.',\n\t\t\tinput: z.object({\n\t\t\t\tpattern: z.string().describe('A regular expression.'),\n\t\t\t\tignoreCase: z.boolean().nullable().describe('Match regardless of case. Defaults to false.'),\n\t\t\t\t...scope,\n\t\t\t}),\n\t\t\texecute: async ({ path, glob, ignoreCase, pattern }, signal) => {\n\t\t\t\tconst { matches, total, truncated } = await workspace.searchText(\n\t\t\t\t\t{\n\t\t\t\t\t\tpath: path ?? undefined,\n\t\t\t\t\t\tglob: glob ?? undefined,\n\t\t\t\t\t\tignoreCase: ignoreCase ?? false,\n\t\t\t\t\t\tpattern,\n\t\t\t\t\t\tlimit: maxMatches,\n\t\t\t\t\t},\n\t\t\t\t\tsignal,\n\t\t\t\t);\n\n\t\t\t\tif (matches.length === 0) {\n\t\t\t\t\treturn 'No matches.';\n\t\t\t\t}\n\n\t\t\t\tconst results = matches.map(\n\t\t\t\t\tmatch => `${match.path}:${String(match.line)}: ${cutLine(match.text)}`,\n\t\t\t\t);\n\n\t\t\t\tconst notice = truncated\n\t\t\t\t\t? `\\n[Showing ${String(results.length)} of ${String(total)} matches. Narrow the path or glob to see the rest.]`\n\t\t\t\t\t: '';\n\t\t\t\treturn results.join('\\n') + notice;\n\t\t\t},\n\t\t}),\n\n\t\tdefineTool({\n\t\t\tname: 'readFile',\n\t\t\tdescription: 'Reads lines of a file, each prefixed with its number.',\n\t\t\tinput: z.object({\n\t\t\t\tpath: z.string().describe('The file, relative to the root.'),\n\t\t\t\tfromLine: z.number().nullable().describe('The first line to read, from 1. Defaults to 1.'),\n\t\t\t\tlineCount: z\n\t\t\t\t\t.number()\n\t\t\t\t\t.nullable()\n\t\t\t\t\t.describe(\n\t\t\t\t\t\t`How many lines to read, at most ${String(maxLines)}. Defaults to ${String(maxLines)}.`,\n\t\t\t\t\t),\n\t\t\t}),\n\t\t\texecute: async ({ fromLine, lineCount, path }, signal) => {\n\t\t\t\tconst from = fromLine ?? 1;\n\t\t\t\tconst requested = lineCount ?? maxLines;\n\t\t\t\t// Checked here, before reading, because strict mode allows the schema no minimum.\n\t\t\t\tassertPositiveInteger('fromLine', from);\n\t\t\t\tassertPositiveInteger('lineCount', requested);\n\n\t\t\t\tconst { lines, totalLines, truncated } = await workspace.readFile(\n\t\t\t\t\t{ path, fromLine: from, lineCount: Math.min(requested, maxLines) },\n\t\t\t\t\tsignal,\n\t\t\t\t);\n\n\t\t\t\t// The file's length tells the model where its range went wrong.\n\t\t\t\tif (lines.length === 0) {\n\t\t\t\t\treturn `No lines from ${String(from)}: the file has ${String(totalLines)} lines.`;\n\t\t\t\t}\n\n\t\t\t\tconst numbered = lines.map((text, index) => `${String(from + index)}: ${cutLine(text)}`);\n\t\t\t\tconst cut = lines.filter(text => text.length > maxLineLength).length;\n\t\t\t\tconst notices = [\n\t\t\t\t\t...(cut > 0\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t`[${String(cut)} lines were longer than ${String(maxLineLength)} characters and were cut.]`,\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: []),\n\t\t\t\t\t...(truncated\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t`[The file has ${String(totalLines)} lines. Read from line ${String(from + lines.length)} to continue.]`,\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: []),\n\t\t\t\t];\n\t\t\t\treturn [...numbered, ...notices].join('\\n');\n\t\t\t},\n\t\t}),\n\t];\n}\n"]}
1
+ {"version":3,"file":"workspaceTools.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/workspaceTools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,+EAA+E;AAC/E,MAAM,QAAQ,GAAG,GAAG,CAAC;AACrB,MAAM,UAAU,GAAG,GAAG,CAAC;AACvB,MAAM,QAAQ,GAAG,GAAG,CAAC;AACrB,2EAA2E;AAC3E,MAAM,aAAa,GAAG,GAAG,CAAC;AAE1B,uFAAuF;AACvF,MAAM,KAAK,GAAG;IACb,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wDAAwD,CAAC;IAC9F,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,mDAAmD,CAAC;CACzF,CAAC;AAEF,kGAAkG;AAClG,SAAS,OAAO,CAAC,IAAY;IAC5B,OAAO,IAAI,CAAC,MAAM,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAChF,CAAC;AAED,SAAS,qBAAqB,CAAC,IAAY,EAAE,KAAa;IACzD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,8CAA8C,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACvF,CAAC;AACF,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,oBAAoB,CAAC,SAAoB;IACxD,OAAO;QACN,UAAU,CAAC;YACV,IAAI,EAAE,WAAW;YACjB,WAAW,EACV,6FAA6F;YAC9F,IAAI,EAAE,MAAM;YACZ,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;YACtB,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,EAAE;gBACzC,gDAAgD;gBAChD,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,SAAS,CAAC,SAAS,CAC5D,EAAE,IAAI,EAAE,IAAI,IAAI,SAAS,EAAE,IAAI,EAAE,IAAI,IAAI,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,EACrE,MAAM,CACN,CAAC;gBAEF,uDAAuD;gBACvD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBACxB,OAAO,WAAW,CAAC;gBACpB,CAAC;gBAED,MAAM,MAAM,GAAG,SAAS;oBACvB,CAAC,CAAC,cAAc,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,MAAM,CAAC,KAAK,CAAC,mDAAmD;oBAC3G,CAAC,CAAC,EAAE,CAAC;gBACN,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC;YAClC,CAAC;SACD,CAAC;QAEF,UAAU,CAAC;YACV,IAAI,EAAE,YAAY;YAClB,WAAW,EACV,qGAAqG;YACtG,IAAI,EAAE,MAAM;YACZ,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC;gBACf,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,uBAAuB,CAAC;gBACrD,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,8CAA8C,CAAC;gBAC3F,GAAG,KAAK;aACR,CAAC;YACF,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE;gBAC9D,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,SAAS,CAAC,UAAU,CAC/D;oBACC,IAAI,EAAE,IAAI,IAAI,SAAS;oBACvB,IAAI,EAAE,IAAI,IAAI,SAAS;oBACvB,UAAU,EAAE,UAAU,IAAI,KAAK;oBAC/B,OAAO;oBACP,KAAK,EAAE,UAAU;iBACjB,EACD,MAAM,CACN,CAAC;gBAEF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBAC1B,OAAO,aAAa,CAAC;gBACtB,CAAC;gBAED,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAC1B,KAAK,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CACtE,CAAC;gBAEF,MAAM,MAAM,GAAG,SAAS;oBACvB,CAAC,CAAC,cAAc,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,MAAM,CAAC,KAAK,CAAC,qDAAqD;oBAC/G,CAAC,CAAC,EAAE,CAAC;gBACN,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC;YACpC,CAAC;SACD,CAAC;QAEF,UAAU,CAAC;YACV,IAAI,EAAE,UAAU;YAChB,WAAW,EAAE,uDAAuD;YACpE,IAAI,EAAE,MAAM;YACZ,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC;gBACf,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,iCAAiC,CAAC;gBAC5D,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gDAAgD,CAAC;gBAC1F,SAAS,EAAE,CAAC;qBACV,MAAM,EAAE;qBACR,QAAQ,EAAE;qBACV,QAAQ,CACR,mCAAmC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,MAAM,CAAC,QAAQ,CAAC,GAAG,CACvF;aACF,CAAC;YACF,OAAO,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,EAAE;gBACxD,MAAM,IAAI,GAAG,QAAQ,IAAI,CAAC,CAAC;gBAC3B,MAAM,SAAS,GAAG,SAAS,IAAI,QAAQ,CAAC;gBACxC,kFAAkF;gBAClF,qBAAqB,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;gBACxC,qBAAqB,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;gBAE9C,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,MAAM,SAAS,CAAC,QAAQ,CAChE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,EAClE,MAAM,CACN,CAAC;gBAEF,gEAAgE;gBAChE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBACxB,OAAO,iBAAiB,MAAM,CAAC,IAAI,CAAC,kBAAkB,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC;gBACnF,CAAC;gBAED,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,IAAI,GAAG,KAAK,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBACzF,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,aAAa,CAAC,CAAC,MAAM,CAAC;gBACrE,MAAM,OAAO,GAAG;oBACf,GAAG,CAAC,GAAG,GAAG,CAAC;wBACV,CAAC,CAAC;4BACA,IAAI,MAAM,CAAC,GAAG,CAAC,2BAA2B,MAAM,CAAC,aAAa,CAAC,4BAA4B;yBAC3F;wBACF,CAAC,CAAC,EAAE,CAAC;oBACN,GAAG,CAAC,SAAS;wBACZ,CAAC,CAAC;4BACA,iBAAiB,MAAM,CAAC,UAAU,CAAC,0BAA0B,MAAM,CAAC,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,gBAAgB;yBACxG;wBACF,CAAC,CAAC,EAAE,CAAC;iBACN,CAAC;gBACF,OAAO,CAAC,GAAG,QAAQ,EAAE,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC7C,CAAC;SACD,CAAC;KACF,CAAC;AACH,CAAC","sourcesContent":["import { z } from 'zod';\nimport type { Tool } from '../domain/tool.ts';\nimport type { Workspace } from '../domain/workspace.ts';\nimport { defineTool } from './defineTool.ts';\n\n// What one call may return. The model can ask for fewer lines, never for more.\nconst maxFiles = 200;\nconst maxMatches = 100;\nconst maxLines = 200;\n// One minified line can be larger than everything else the model has read.\nconst maxLineLength = 300;\n\n// Strict mode has no optional fields: an argument the model may leave out is nullable.\nconst scope = {\n\tpath: z.string().nullable().describe('A folder or file to stay inside, relative to the root.'),\n\tglob: z.string().nullable().describe('Only paths matching this glob, such as \"**/*.ts\".'),\n};\n\n/** One line as the model reads it: a minified bundle on a single line would flood its context. */\nfunction cutLine(text: string): string {\n\treturn text.length > maxLineLength ? `${text.slice(0, maxLineLength)}…` : text;\n}\n\nfunction assertPositiveInteger(name: string, value: number): void {\n\tif (!Number.isInteger(value) || value < 1) {\n\t\tthrow new Error(`${name} must be a whole number of at least 1; got ${String(value)}`);\n\t}\n}\n\n/** The three read-only tools an agent explores a repository with. */\nexport function createWorkspaceTools(workspace: Workspace): Tool[] {\n\treturn [\n\t\tdefineTool({\n\t\t\tname: 'listFiles',\n\t\t\tdescription:\n\t\t\t\t'Lists the files of the repository, sorted. Ignored files such as dependencies are left out.',\n\t\t\trisk: 'safe',\n\t\t\tinput: z.object(scope),\n\t\t\texecute: async ({ path, glob }, signal) => {\n\t\t\t\t// The port takes \"no scope\" as a missing field.\n\t\t\t\tconst { files, total, truncated } = await workspace.listFiles(\n\t\t\t\t\t{ path: path ?? undefined, glob: glob ?? undefined, limit: maxFiles },\n\t\t\t\t\tsignal,\n\t\t\t\t);\n\n\t\t\t\t// Nothing found is an answer too, not an empty string.\n\t\t\t\tif (files.length === 0) {\n\t\t\t\t\treturn 'No files.';\n\t\t\t\t}\n\n\t\t\t\tconst notice = truncated\n\t\t\t\t\t? `\\n[Showing ${String(files.length)} of ${String(total)} files. Narrow the path or glob to see the rest.]`\n\t\t\t\t\t: '';\n\t\t\t\treturn files.join('\\n') + notice;\n\t\t\t},\n\t\t}),\n\n\t\tdefineTool({\n\t\t\tname: 'searchText',\n\t\t\tdescription:\n\t\t\t\t'Searches the repository for a regular expression and returns each matching line as path:line: text.',\n\t\t\trisk: 'safe',\n\t\t\tinput: z.object({\n\t\t\t\tpattern: z.string().describe('A regular expression.'),\n\t\t\t\tignoreCase: z.boolean().nullable().describe('Match regardless of case. Defaults to false.'),\n\t\t\t\t...scope,\n\t\t\t}),\n\t\t\texecute: async ({ path, glob, ignoreCase, pattern }, signal) => {\n\t\t\t\tconst { matches, total, truncated } = await workspace.searchText(\n\t\t\t\t\t{\n\t\t\t\t\t\tpath: path ?? undefined,\n\t\t\t\t\t\tglob: glob ?? undefined,\n\t\t\t\t\t\tignoreCase: ignoreCase ?? false,\n\t\t\t\t\t\tpattern,\n\t\t\t\t\t\tlimit: maxMatches,\n\t\t\t\t\t},\n\t\t\t\t\tsignal,\n\t\t\t\t);\n\n\t\t\t\tif (matches.length === 0) {\n\t\t\t\t\treturn 'No matches.';\n\t\t\t\t}\n\n\t\t\t\tconst results = matches.map(\n\t\t\t\t\tmatch => `${match.path}:${String(match.line)}: ${cutLine(match.text)}`,\n\t\t\t\t);\n\n\t\t\t\tconst notice = truncated\n\t\t\t\t\t? `\\n[Showing ${String(results.length)} of ${String(total)} matches. Narrow the path or glob to see the rest.]`\n\t\t\t\t\t: '';\n\t\t\t\treturn results.join('\\n') + notice;\n\t\t\t},\n\t\t}),\n\n\t\tdefineTool({\n\t\t\tname: 'readFile',\n\t\t\tdescription: 'Reads lines of a file, each prefixed with its number.',\n\t\t\trisk: 'safe',\n\t\t\tinput: z.object({\n\t\t\t\tpath: z.string().describe('The file, relative to the root.'),\n\t\t\t\tfromLine: z.number().nullable().describe('The first line to read, from 1. Defaults to 1.'),\n\t\t\t\tlineCount: z\n\t\t\t\t\t.number()\n\t\t\t\t\t.nullable()\n\t\t\t\t\t.describe(\n\t\t\t\t\t\t`How many lines to read, at most ${String(maxLines)}. Defaults to ${String(maxLines)}.`,\n\t\t\t\t\t),\n\t\t\t}),\n\t\t\texecute: async ({ fromLine, lineCount, path }, signal) => {\n\t\t\t\tconst from = fromLine ?? 1;\n\t\t\t\tconst requested = lineCount ?? maxLines;\n\t\t\t\t// Checked here, before reading, because strict mode allows the schema no minimum.\n\t\t\t\tassertPositiveInteger('fromLine', from);\n\t\t\t\tassertPositiveInteger('lineCount', requested);\n\n\t\t\t\tconst { lines, totalLines, truncated } = await workspace.readFile(\n\t\t\t\t\t{ path, fromLine: from, lineCount: Math.min(requested, maxLines) },\n\t\t\t\t\tsignal,\n\t\t\t\t);\n\n\t\t\t\t// The file's length tells the model where its range went wrong.\n\t\t\t\tif (lines.length === 0) {\n\t\t\t\t\treturn `No lines from ${String(from)}: the file has ${String(totalLines)} lines.`;\n\t\t\t\t}\n\n\t\t\t\tconst numbered = lines.map((text, index) => `${String(from + index)}: ${cutLine(text)}`);\n\t\t\t\tconst cut = lines.filter(text => text.length > maxLineLength).length;\n\t\t\t\tconst notices = [\n\t\t\t\t\t...(cut > 0\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t`[${String(cut)} lines were longer than ${String(maxLineLength)} characters and were cut.]`,\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: []),\n\t\t\t\t\t...(truncated\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t`[The file has ${String(totalLines)} lines. Read from line ${String(from + lines.length)} to continue.]`,\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: []),\n\t\t\t\t];\n\t\t\t\treturn [...numbered, ...notices].join('\\n');\n\t\t\t},\n\t\t}),\n\t];\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikode13/harness",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "Test/agent harness utility",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -38,7 +38,7 @@
38
38
  "format:check": "prettier . --check",
39
39
  "lint": "eslint . --max-warnings 0",
40
40
  "lint:fix": "eslint . --fix",
41
- "dev": "pnpm --filter @mikode13/harness-cli run dev",
41
+ "dev": "node --conditions=mikode-harness-source cli/cli.ts",
42
42
  "test": "vitest run --project unit --project cli",
43
43
  "test:unit": "vitest run --project unit --project cli",
44
44
  "test:unit:watch": "vitest --project unit --project cli",
@@ -57,14 +57,17 @@
57
57
  "eslint": "^10.8.0",
58
58
  "prettier": "^3.9.6",
59
59
  "typescript": "^6.0.3",
60
- "vitest": "^4.1.11"
60
+ "vitest": "^4.1.11",
61
+ "zod": "^4.4.3"
61
62
  },
62
63
  "dependencies": {
63
64
  "@anthropic-ai/claude-agent-sdk": "^0.3.220",
64
65
  "@anthropic-ai/sdk": "^0.115.0",
65
66
  "@openai/codex-sdk": "^0.154.0",
66
67
  "@vscode/ripgrep": "^1.18.0",
67
- "openai": "^7.23.0",
68
+ "openai": "^7.23.0"
69
+ },
70
+ "peerDependencies": {
68
71
  "zod": "^4.4.3"
69
72
  }
70
73
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/agent/domain/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAe,MAAM,2BAA2B,CAAC;AAcnE,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IACjC,KAAK,CAAS;IACvB,2FAA2F;IAClF,MAAM,CAAqB;IAC3B,eAAe,CAAU;IAElC,YAAY,OAAe,EAAE,OAA+B;QAC3D,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,eAAe,IAAI,KAAK,CAAC;QACxD,gDAAgD;QAChD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IACjE,CAAC;CACD;AACD,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IACnC,KAAK,CAAS;IACvB,2FAA2F;IAClF,MAAM,CAAqB;IAC3B,eAAe,CAAU;IAElC,YAAY,OAAe,EAAE,OAA+B;QAC3D,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,eAAe,IAAI,KAAK,CAAC;QACxD,gDAAgD;QAChD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IACjE,CAAC;CACD;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC9B,KAAc,EACd,KAAyB,EACzB,UAAU,GAAG,KAAK;IAElB,IAAI,CAAC,CAAC,KAAK,YAAY,gBAAgB,IAAI,KAAK,YAAY,kBAAkB,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9F,IAAI,CAAC,KAAK,IAAI,CAAC,UAAU;QAAE,OAAO,KAAK,CAAC;IAExC,MAAM,UAAU,GAAG,KAAK,CAAC,WAGiB,CAAC;IAC3C,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,OAAO,EAAE;QAC1C,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC;QACtC,eAAe,EAAE,KAAK,CAAC,eAAe,IAAI,UAAU;KACpD,CAAC,CAAC;IACH,0FAA0F;IAC1F,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IACzB,OAAO,IAAI,CAAC;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,OAAO,uBAAwB,SAAQ,KAAK;CAAG","sourcesContent":["import { addTokens, type Tokens } from '#src/shared/domain/tokens';\n\ninterface ClassifiedErrorOptions {\n\tcause: string;\n\t/** What the run spent before it failed, as far as it was reported. */\n\ttokens?: Tokens | undefined;\n\t/**\n\t * A provider answered at least one call of the run without reporting its usage, so\n\t * `tokens` would be a subtotal. A failure that never got an answer, such as a dropped\n\t * connection, leaves it unset: it adds nothing that could be missing.\n\t */\n\tusageUnreported?: boolean | undefined;\n}\n\nexport class RecoverableError extends Error {\n\toverride cause: string;\n\t/** Missing when nothing was reported, and always missing when `usageUnreported` is set. */\n\treadonly tokens: Tokens | undefined;\n\treadonly usageUnreported: boolean;\n\n\tconstructor(message: string, options: ClassifiedErrorOptions) {\n\t\tsuper(message, options);\n\t\tthis.cause = options.cause;\n\t\tthis.usageUnreported = options.usageUnreported ?? false;\n\t\t// A subtotal must not pass for the run's total.\n\t\tthis.tokens = this.usageUnreported ? undefined : options.tokens;\n\t}\n}\nexport class UnrecoverableError extends Error {\n\toverride cause: string;\n\t/** Missing when nothing was reported, and always missing when `usageUnreported` is set. */\n\treadonly tokens: Tokens | undefined;\n\treadonly usageUnreported: boolean;\n\n\tconstructor(message: string, options: ClassifiedErrorOptions) {\n\t\tsuper(message, options);\n\t\tthis.cause = options.cause;\n\t\tthis.usageUnreported = options.usageUnreported ?? false;\n\t\t// A subtotal must not pass for the run's total.\n\t\tthis.tokens = this.usageUnreported ? undefined : options.tokens;\n\t}\n}\n\n/**\n * The same failure, also carrying what an outer layer spent before the failure reached it,\n * such as earlier attempts or earlier roles, or the call the failure came from. `unreported`\n * says that some of it was answered without usage. It is a new instance of the same class, so\n * a subclass such as `MaxContextError` keeps its meaning. A cancellation or an unclassified\n * failure is returned unchanged: neither can carry tokens.\n */\nexport function withSpentTokens(\n\terror: unknown,\n\tspent: Tokens | undefined,\n\tunreported = false,\n): unknown {\n\tif (!(error instanceof RecoverableError || error instanceof UnrecoverableError)) return error;\n\tif (!spent && !unreported) return error;\n\n\tconst ErrorClass = error.constructor as new (\n\t\tmessage: string,\n\t\toptions: ClassifiedErrorOptions,\n\t) => RecoverableError | UnrecoverableError;\n\tconst copy = new ErrorClass(error.message, {\n\t\tcause: error.cause,\n\t\ttokens: addTokens(error.tokens, spent),\n\t\tusageUnreported: error.usageUnreported || unreported,\n\t});\n\t// The trace should point at where the failure happened, not at the layer that counted it.\n\tcopy.stack = error.stack;\n\treturn copy;\n}\n\n/**\n * An agent was requested with something its provider does not support, such as another\n * provider's model. Thrown while the agent is built, never by `run()`.\n */\nexport class InvalidAgentConfigError extends Error {}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"providerFailure.js","sourceRoot":"","sources":["../../../src/agent/domain/providerFailure.ts"],"names":[],"mappings":"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","sourcesContent":["import { isAbortError } from '#src/shared/domain/isAbortError';\nimport { RecoverableError, UnrecoverableError } from './errors.ts';\n\n/**\n * Unclassified failures are transport-shaped, so recoverable is the useful default:\n * `RetryingAgent` turns a persistent one into an `UnrecoverableError` on exhaustion, while\n * the opposite default would make every transient blip fatal.\n */\nexport function classifyProviderFailure(error: unknown, context: string): Error {\n\tif (isAbortError(error)) return error;\n\tif (error instanceof RecoverableError || error instanceof UnrecoverableError) return error;\n\n\treturn new RecoverableError(context, { cause: describeFailure(error) });\n}\n\n/** A host failure cannot be retried safely because the provider turn may already have side effects. */\nexport function classifyHostFailure(error: unknown, context: string): Error {\n\tif (isAbortError(error)) return error;\n\tif (error instanceof UnrecoverableError) return error;\n\n\treturn new UnrecoverableError(context, { cause: describeFailure(error) });\n}\n\n/**\n * Runs host code — a logger, for example — and classifies whatever it throws with the\n * classifier the caller chooses. The host code stays unaware of classification, and the\n * failure cannot escape the `Agent` unclassified.\n */\nexport function treatErrors<T>(\n\toperation: () => T,\n\tclassify: (error: unknown, context: string) => Error,\n\tcontext: string,\n): T {\n\ttry {\n\t\treturn operation();\n\t} catch (error) {\n\t\tthrow classify(error, context);\n\t}\n}\n\n/** Preserves deliberate domain errors while making unexpected local failures fatal. */\nexport function classifyLocalFailure(error: unknown, context: string): Error {\n\tif (isAbortError(error)) return error;\n\tif (error instanceof RecoverableError || error instanceof UnrecoverableError) return error;\n\n\treturn new UnrecoverableError(context, { cause: describeFailure(error) });\n}\n\n/** The message alone: a stack or serialized object could expose unstable implementation details. */\nexport function describeFailure(error: unknown): string {\n\tif (error instanceof RecoverableError || error instanceof UnrecoverableError) return error.cause;\n\tif (error instanceof Error && error.message) return error.message;\n\tif (typeof error === 'string' && error) return error;\n\n\treturn 'The provider failed without a description.';\n}\n\n/**\n * Classifies the stream itself and nothing else. Consuming an SDK stream inside a\n * `try/catch` puts the loop body — item mapping, logging, the consumer callback — inside\n * the provider boundary too, so a host failure gets reported as a recoverable provider\n * failure and `RetryingAgent` replays a turn that already ran its side effects.\n */\nexport async function* classifiedProviderStream<T>(\n\tstream: AsyncIterable<T>,\n\tcontext: string,\n): AsyncGenerator<T> {\n\tlet events: AsyncIterator<T>;\n\n\ttry {\n\t\tevents = stream[Symbol.asyncIterator]();\n\t} catch (error) {\n\t\tthrow classifyProviderFailure(error, context);\n\t}\n\n\tlet completed = false;\n\n\ttry {\n\t\tfor (;;) {\n\t\t\tlet next: IteratorResult<T>;\n\n\t\t\ttry {\n\t\t\t\tnext = await events.next();\n\t\t\t} catch (error) {\n\t\t\t\tthrow classifyProviderFailure(error, context);\n\t\t\t}\n\n\t\t\tif (next.done) {\n\t\t\t\tcompleted = true;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tyield next.value;\n\t\t}\n\t} finally {\n\t\tif (!completed) {\n\t\t\ttry {\n\t\t\t\tawait events.return?.();\n\t\t\t} catch {\n\t\t\t\t// Cleanup is secondary to the failure that interrupted consumption. It must\n\t\t\t\t// never replace a classified provider error or an unrecoverable host error.\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
File without changes
File without changes