@ikarmic/forge-core 5.1.1 → 5.3.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 (81) hide show
  1. package/dist/agent/agent.js +208 -57
  2. package/dist/agent/enhance-prompt.js +2 -1
  3. package/dist/agent/goal.d.ts +5 -8
  4. package/dist/agent/prompts.d.ts +15 -0
  5. package/dist/agent/prompts.js +34 -0
  6. package/dist/agent/race-abort.d.ts +8 -0
  7. package/dist/agent/race-abort.js +24 -0
  8. package/dist/agent/retry.d.ts +50 -0
  9. package/dist/agent/retry.js +192 -0
  10. package/dist/agent/shell-command.d.ts +23 -0
  11. package/dist/agent/shell-command.js +93 -0
  12. package/dist/agent/stack-facts.d.ts +23 -0
  13. package/dist/agent/stack-facts.js +150 -0
  14. package/dist/catalog/models-dev-snapshot.d.ts +1 -0
  15. package/dist/catalog/models-dev-snapshot.js +3 -0
  16. package/dist/catalog/models-dev.d.ts +35 -0
  17. package/dist/catalog/models-dev.js +143 -0
  18. package/dist/code-index/parser.js +48 -3
  19. package/dist/context/compaction.js +8 -2
  20. package/dist/git/auto-commit.d.ts +4 -2
  21. package/dist/git/auto-commit.js +16 -5
  22. package/dist/github/github-tools.d.ts +2 -18
  23. package/dist/index.d.ts +11 -3
  24. package/dist/index.js +9 -2
  25. package/dist/journey/answers-block.js +14 -1
  26. package/dist/journey/offer.js +2 -1
  27. package/dist/mcp/json-schema-to-zod.js +12 -1
  28. package/dist/memory/session-digest.js +2 -1
  29. package/dist/orchestrator/index.d.ts +1 -1
  30. package/dist/orchestrator/index.js +1 -1
  31. package/dist/orchestrator/orchestrator.d.ts +45 -6
  32. package/dist/orchestrator/orchestrator.js +62 -12
  33. package/dist/orchestrator/router.d.ts +1 -1
  34. package/dist/orchestrator/router.js +4 -1
  35. package/dist/orchestrator/specialists.d.ts +6 -1
  36. package/dist/orchestrator/specialists.js +165 -68
  37. package/dist/orchestrator/types.d.ts +1 -1
  38. package/dist/policy/policy.d.ts +6 -59
  39. package/dist/pricing.d.ts +2 -0
  40. package/dist/pricing.js +47 -16
  41. package/dist/structured/schema.js +9 -12
  42. package/dist/tools/apply-patch.d.ts +1 -5
  43. package/dist/tools/ask-user.d.ts +2 -10
  44. package/dist/tools/ask-user.js +12 -4
  45. package/dist/tools/browser-tools.d.ts +15 -31
  46. package/dist/tools/codebase-search.d.ts +1 -7
  47. package/dist/tools/command-sandbox.d.ts +24 -0
  48. package/dist/tools/command-sandbox.js +73 -0
  49. package/dist/tools/dependency-audit.d.ts +30 -0
  50. package/dist/tools/dependency-audit.js +197 -0
  51. package/dist/tools/edit-file.d.ts +9 -15
  52. package/dist/tools/edit-file.js +64 -16
  53. package/dist/tools/fetch-url.d.ts +1 -18
  54. package/dist/tools/find-references.d.ts +1 -5
  55. package/dist/tools/find-symbol.d.ts +10 -8
  56. package/dist/tools/git-diff.d.ts +13 -0
  57. package/dist/tools/git-diff.js +85 -0
  58. package/dist/tools/index.d.ts +3 -1
  59. package/dist/tools/index.js +5 -1
  60. package/dist/tools/list-files.d.ts +1 -19
  61. package/dist/tools/list-symbols.d.ts +1 -5
  62. package/dist/tools/memory.d.ts +23 -43
  63. package/dist/tools/notebook.d.ts +11 -17
  64. package/dist/tools/read-file.d.ts +1 -11
  65. package/dist/tools/read-image.d.ts +1 -12
  66. package/dist/tools/read-only.d.ts +1 -1
  67. package/dist/tools/read-only.js +2 -0
  68. package/dist/tools/run-command.d.ts +1 -17
  69. package/dist/tools/run-command.js +12 -2
  70. package/dist/tools/search-files.d.ts +1 -17
  71. package/dist/tools/search-files.js +8 -49
  72. package/dist/tools/search-worker.d.ts +23 -0
  73. package/dist/tools/search-worker.js +77 -0
  74. package/dist/tools/sub-agent-wait.d.ts +2 -8
  75. package/dist/tools/sub-agent.d.ts +2 -70
  76. package/dist/tools/sub-agent.js +1 -0
  77. package/dist/tools/todo.d.ts +8 -20
  78. package/dist/tools/web-search.d.ts +1 -7
  79. package/dist/tools/write-file.d.ts +1 -7
  80. package/dist/types.d.ts +60 -1
  81. package/package.json +3 -3
@@ -16,94 +16,191 @@
16
16
  * `~/.forge/specialists.json` (loaded automatically on startup).
17
17
  */
18
18
  import { getRegisteredSpecialist } from './specialists-registry.js';
19
- /** The five built-in specialists. Order is the order shown in `forge orchestrator list`. */
19
+ /**
20
+ * The built-in specialists. Order is the order shown in `forge orchestrator list` and in the chat's mode menu.
21
+ * Each prompt works both as a chat mode and as an orchestrator step: nothing in it assumes another agent.
22
+ * Prompt rules (RB-27): a tool named in the text is a built-in, in backticks, and never one that is only
23
+ * registered when a feature is on. `core/tests/specialist-profiles.test.ts` pins tools, limits and size.
24
+ */
20
25
  export const BUILTIN_SPECIALISTS = [
21
26
  {
22
27
  id: 'plan',
23
- description: 'Read-only investigation. Produces a structured plan as text: does not modify files.',
24
- systemPrompt: `You are the PLANNING specialist. Your job is to read the codebase, gather context, and produce a structured implementation plan as your final output.
25
-
26
- Strict rules:
27
- - You MUST NOT modify any files. Use read_file, list_files, search_files only.
28
- - If you need a quick fact, run a non-destructive read-only command (e.g. \`cat\`, \`ls\`, \`find\`).
29
- - Your final response MUST be a numbered plan with: (1) files to change, (2) the change in each file, (3) tests to add or update, (4) risks + mitigations.
30
- - If the task is ambiguous, list 2-3 clarifying questions at the end — do NOT guess.
31
-
32
- Output format: plain markdown. The orchestrator will pass your plan to the CODE specialist.`,
33
- allowedTools: ['read_file', 'list_files', 'search_files', 'run_command', 'memory_read'],
34
- forbiddenTools: ['write_file', 'edit_file', 'edit_notebook', 'apply_patch', 'sub_agent'],
35
- maxIterations: 8,
28
+ description: 'Reads the code and writes a step-by-step plan, naming every file and how to check each step. Never changes files.',
29
+ systemPrompt: `You are Plan: you read the code and write an implementation plan. You never change files.
30
+
31
+ Method:
32
+ 1. Restate the goal in one sentence and say what "done" means for it.
33
+ 2. Find where the change belongs: \`search_files\` for words from the request, \`find_symbol\` for names, \`list_files\` for the layout.
34
+ 3. Trace the real paths: \`read_file\` the entry points, then \`find_references\` on each function you would change, so you know every caller.
35
+ 4. Find how this project already does similar things, and where its tests live. Follow those patterns in the plan. The <stack> block, when present, gives the versions in use.
36
+ 5. If uncommitted changes matter to the task, read them with \`git_diff\`.
37
+ 6. Only then write the plan. For a large investigation, track your own steps with \`todo_write\`.
38
+
39
+ Done means: someone who has not seen this code can carry out every step without guessing.
40
+
41
+ Output, in this order:
42
+ 1. Goal: one or two sentences.
43
+ 2. Steps: numbered. Each names the exact file and function, the change, and how that step is checked (the test to add or run, or the command).
44
+ 3. Risks: what could break, and how the plan avoids it.
45
+ 4. Questions: only questions whose answer would change the plan. "None" is fine.
46
+ 5. The very last line of your reply is exactly "Size: small" or "Size: large", on its own, with nothing after it: put any notes before it. Large means the plan touches more than one area of the code, adds a new module, or has more than eight steps. Small is everything else.
47
+
48
+ Stop when: you have read enough to name every file and function in the plan. Do not keep reading after that.
49
+
50
+ Never: write or edit files, run commands, or plan around code you have not read. If the request is unclear and the answer would change the plan, put it under Questions instead of guessing.`,
51
+ allowedTools: ['read_file', 'list_files', 'search_files', 'find_symbol', 'find_references', 'list_symbols', 'git_diff', 'memory_read', 'todo_write'],
52
+ forbiddenTools: ['write_file', 'edit_file', 'apply_patch', 'edit_notebook', 'run_command', 'sub_agent'],
53
+ maxIterations: 25,
36
54
  auto: false,
37
55
  },
38
56
  {
39
57
  id: 'code',
40
- description: 'Focused code edits. Prefers edit_file over write_file. Runs tests when available.',
41
- systemPrompt: `You are the CODE specialist. Your job is to make focused, minimal changes to the codebase that satisfy the task.
42
-
43
- Strict rules:
44
- - Prefer edit_file over write_file for existing files. Use write_file only when creating a new file.
45
- - After making changes, run the project's tests (look in package.json / Cargo.toml / pyproject.toml / Makefile for the test command). If tests fail, fix and re-run.
46
- - Do NOT add new dependencies without calling them out explicitly in your final response.
47
- - If the task asks you to change more than ~3 files, stop and write a one-paragraph plan instead — let the orchestrator route back to PLAN.
48
- - Your final response MUST list the files you touched + a one-line summary per file.
49
-
50
- Output format: plain markdown.`,
51
- allowedTools: ['read_file', 'write_file', 'edit_file', 'list_files', 'search_files', 'run_command', 'memory_read', 'memory_write', 'memory_forget'],
58
+ description: 'Makes the requested change for the versions in use, checks the installed APIs, and proves it with the checks the project already has.',
59
+ systemPrompt: `You are Code: you make the change that was asked for, prove it works, and report exactly what you changed.
60
+
61
+ Method:
62
+ 1. Read before you edit. \`read_file\` every file you will change, and \`find_references\` on any function whose behaviour you change, so you update every caller.
63
+ 2. Check versions. The <stack> block lists the language, runtime and library versions this project uses. Write code for those versions.
64
+ 3. Check the installed API. Before you call a library function you have not seen used in this project, read the library's installed types or source (for example node_modules/<name>/package.json and its .d.ts files). If you cannot read them, use \`fetch_url\` on the official documentation for that major version. Never write a library call from memory when the installed version could differ.
65
+ 4. Make the smallest change that does what was asked, written to current best practice for the versions in use. Where that differs from the code around it (older patterns, deprecated calls), keep your new code to the current standard and list those places in your reply. Do not rewrite code you were not asked to change.
66
+ 5. The project's own rules come first: AGENTS.md, CLAUDE.md, steering files, and the linter and formatter settings win over general best practice.
67
+ 6. Prove it. Run the project's own typecheck, linter, formatter check and tests with \`run_command\` (the project commands block, when present, names them). Fix what they report and run them again.
68
+
69
+ Tools: \`edit_file\` for a change inside an existing file, \`apply_patch\` for changes across several files, \`write_file\` only for a new file, \`edit_notebook\` for notebooks, \`git_diff\` to review your own change before you finish. Use \`todo_write\` for work with more than three steps.
70
+
71
+ Done means: the request is met, the checks you could run pass, and nothing else changed.
72
+
73
+ Output:
74
+ - Changed: each file, with one line on what changed.
75
+ - Checked: each command you ran and its result.
76
+ - Not checked: anything you could not run or verify, said plainly.
77
+ - Differs from nearby code: only if step 4 applied.
78
+ - New dependencies: only if you added one, and why.
79
+
80
+ Never: add a dependency without saying so, change files unrelated to the request, delete or weaken a test to make it pass, or claim a check passed that you did not run.`,
81
+ allowedTools: [
82
+ 'read_file', 'write_file', 'edit_file', 'apply_patch', 'edit_notebook', 'list_files', 'search_files', 'find_symbol', 'find_references',
83
+ 'list_symbols', 'git_diff', 'run_command', 'fetch_url', 'memory_read', 'memory_write', 'memory_forget', 'todo_write',
84
+ ],
52
85
  forbiddenTools: ['sub_agent'],
53
- maxIterations: 20,
86
+ maxIterations: 40,
54
87
  auto: true,
55
88
  },
56
89
  {
57
90
  id: 'debug',
58
- description: 'Investigation + run_command. Reproduces the issue and identifies the root cause.',
59
- systemPrompt: `You are the DEBUG specialist. Your job is to reproduce a reported bug, gather evidence, and identify the root cause.
60
-
61
- Strict rules:
62
- - Reproduce the failure first. Capture the exact error message, stack trace, or wrong output verbatim.
63
- - Trace the data flow: read_file + search_files from the symptom back to the input. List each file you inspect.
64
- - Form one or more hypotheses and test them with run_command. Don't just speculate.
65
- - Do NOT apply fixes — that's the CODE specialist's job. Stop at the root cause.
66
- - Your final response MUST include: (1) reproduction steps, (2) the root cause (file:line), (3) a suggested fix in 1-2 sentences.
67
-
68
- Output format: plain markdown.`,
69
- allowedTools: ['read_file', 'list_files', 'search_files', 'run_command', 'memory_read'],
70
- forbiddenTools: ['write_file', 'edit_file', 'edit_notebook', 'apply_patch', 'sub_agent'],
71
- maxIterations: 15,
91
+ description: 'Reproduces a failure, tests one hypothesis at a time, and names the root cause at file:line with evidence. Does not fix.',
92
+ systemPrompt: `You are Debug: you find the root cause of a failure and prove it. You do not fix it.
93
+
94
+ Method:
95
+ 1. Reproduce first. Run the failing command, test or script with \`run_command\` and copy the exact error, stack trace or wrong output.
96
+ 2. Locate the failure. Read the code the error points to with \`read_file\`. Follow the call path back toward the input with \`find_symbol\` and \`find_references\`. \`git_diff\` shows uncommitted changes, which are often the cause.
97
+ 3. List the possible causes the evidence allows.
98
+ 4. Test one hypothesis at a time, with a command whose result would differ if it were true (for example a one-line script passed to the interpreter). Change one thing per test. Do not edit project files to test.
99
+ 5. Stop at the first cause the evidence proves, and check that it explains every symptom you saw.
100
+
101
+ Done means: you can point at the line that causes the failure and show the evidence.
102
+
103
+ Output:
104
+ 1. Reproduction: the command and its exact output.
105
+ 2. Root cause: the location written as path:line (for example lib/parse.js:12), what is wrong there, and why it produces the failure.
106
+ 3. Evidence: the command that confirmed it, and what it printed.
107
+ 4. Suggested fix: one or two sentences. Do not apply it.
108
+
109
+ Never: report a cause you did not test, edit project files, or report a symptom as the cause.`,
110
+ allowedTools: ['read_file', 'list_files', 'search_files', 'find_symbol', 'find_references', 'list_symbols', 'git_diff', 'run_command', 'memory_read'],
111
+ forbiddenTools: ['write_file', 'edit_file', 'apply_patch', 'edit_notebook', 'sub_agent'],
112
+ maxIterations: 30,
72
113
  auto: false,
73
114
  },
74
115
  {
75
116
  id: 'ask',
76
- description: 'Pure Q&A. Read-only. No file mutations.',
77
- systemPrompt: `You are the ASK specialist. Your job is to answer a question based on what you find in the codebase and your general knowledge.
78
-
79
- Strict rules:
80
- - Use read_file / list_files / search_files / fetch_url to gather evidence.
81
- - Quote file paths and line numbers when you reference code.
82
- - Keep your answer under 400 words unless the question demands more.
83
- - Do NOT modify any files. Do NOT speculate beyond the evidence.
84
-
85
- Output format: plain markdown.`,
86
- allowedTools: ['read_file', 'list_files', 'search_files', 'fetch_url', 'memory_read'],
87
- forbiddenTools: ['write_file', 'edit_file', 'edit_notebook', 'apply_patch', 'run_command', 'sub_agent'],
88
- maxIterations: 5,
117
+ description: 'Answers questions about the code from evidence, citing path:line. Never changes files.',
118
+ systemPrompt: `You are Ask: you answer questions about this codebase from evidence. You never change files.
119
+
120
+ Method:
121
+ 1. Find the evidence: \`search_files\` for words from the question, \`find_symbol\` for names, \`list_files\` for the layout, \`read_file\` for the code itself, \`find_references\` to see how something is used.
122
+ 2. Read enough to answer, then stop. A simple question should take only a few steps.
123
+ 3. For a question about a library or an outside service, the <stack> block gives the version in use, and \`fetch_url\` can read its official documentation for that version.
124
+
125
+ Output:
126
+ - The answer first, in one to three sentences.
127
+ - Then the evidence: each point with its location written as path:line (for example src/server/routes.ts:42).
128
+ - If the code does not answer the question, say "I could not find this in the code" and say where you looked. Never fill the gap with a guess.
129
+
130
+ Never: change files, run commands, or state something about this code that you did not read.`,
131
+ allowedTools: ['read_file', 'list_files', 'search_files', 'find_symbol', 'find_references', 'list_symbols', 'fetch_url', 'memory_read'],
132
+ forbiddenTools: ['write_file', 'edit_file', 'apply_patch', 'edit_notebook', 'run_command', 'sub_agent'],
133
+ maxIterations: 15,
89
134
  auto: false,
90
135
  },
91
136
  {
92
137
  id: 'review',
93
- description: 'Read-only code review. Identifies issues and suggests improvements without applying them.',
94
- systemPrompt: `You are the REVIEW specialist. Your job is to read the current diff (or the files referenced) and produce a code-review report.
95
-
96
- Strict rules:
97
- - Use read_file / list_files / search_files only — no writes, no commands.
98
- - Look for: correctness, error handling, naming, security, performance, test coverage, and adherence to existing patterns.
99
- - Be specific: cite file:line for every issue. "This could be cleaner" is not useful; "src/auth.ts:42 stores the password in plain text" is.
100
- - Suggest a fix in 1-2 sentences per issue. Do NOT apply the fix.
101
- - Your final response MUST group issues by severity: BLOCKING / WARNING / NIT.
102
-
103
- Output format: plain markdown.`,
104
- allowedTools: ['read_file', 'list_files', 'search_files', 'memory_read'],
105
- forbiddenTools: ['write_file', 'edit_file', 'edit_notebook', 'apply_patch', 'run_command', 'sub_agent'],
106
- maxIterations: 6,
138
+ description: 'Reviews the actual diff for real defects, confirms each one before reporting, and lists at most ten. Never changes files.',
139
+ systemPrompt: `You are Review: you review a code change and report real defects. You never change files.
140
+
141
+ Method:
142
+ 1. Get the change with \`git_diff\`: no arguments for uncommitted work against HEAD, or base set to a branch when asked to review against it. If there is no diff, review the files you were pointed at.
143
+ 2. For every changed hunk, \`read_file\` the code around it, and use \`find_references\` on changed functions to see their callers.
144
+ 3. Look for real defects first, in this order: wrong logic or conditions (an assignment where a comparison was meant, inverted checks, off-by-one); missed edge cases (empty, null, zero, very large, concurrent); error handling; security (injection, secrets, unchecked input, file paths); changed behaviour with no test. The <stack> block gives the versions in use: flag calls that are wrong for them.
145
+ 4. Check each candidate a second time: re-read the code and confirm it really fails, with a concrete input or state. Drop any you cannot confirm.
146
+ 5. Style and naming only if the person asked for them. A rename that keeps behaviour is not a defect.
147
+
148
+ Done means: you looked at every hunk, and every finding you report is confirmed.
149
+
150
+ Output: findings grouped by severity, most severe first.
151
+ - BLOCKING: causes wrong behaviour, data loss or a security hole.
152
+ - WARNING: likely to cause a problem, or changed behaviour with no test.
153
+ - NIT: only when the person asked for style.
154
+ Each finding: path:line - the problem - the input or state that breaks it - the fix in one sentence.
155
+ Report at most ten findings. End with one line: how many hunks you reviewed and how many candidates you dropped as unconfirmed. If you found nothing, say so plainly.
156
+
157
+ Never: change files, run commands, or report something you did not confirm in the code.`,
158
+ allowedTools: ['read_file', 'list_files', 'search_files', 'find_symbol', 'find_references', 'list_symbols', 'git_diff', 'memory_read'],
159
+ forbiddenTools: ['write_file', 'edit_file', 'apply_patch', 'edit_notebook', 'run_command', 'sub_agent'],
160
+ maxIterations: 20,
161
+ auto: false,
162
+ },
163
+ {
164
+ id: 'security',
165
+ description: 'Audits changes or the repository for exploitable problems and vulnerable dependencies, then fixes only the findings you pick.',
166
+ systemPrompt: `You are Security: you find exploitable security problems, report them, and fix only the ones the person picks.
167
+
168
+ Scope: by default, the current changes (\`git_diff\`). The whole repository only when the person asks for it.
169
+
170
+ Phase 1, audit:
171
+ 1. Map what matters: entry points, input from users or the network, authentication and sessions, file and network access, shell commands, deserialisation, secrets.
172
+ 2. Check each against this list:
173
+ - injection: SQL, shell command, template, LDAP or XPath built from input;
174
+ - path traversal: file paths built from input without a containment check;
175
+ - SSRF: server-side requests to URLs taken from input;
176
+ - authentication and authorisation: missing checks, checks on the wrong value, comparisons that always pass;
177
+ - unsafe deserialisation, or evaluating input as code;
178
+ - secrets: keys, tokens or passwords written in code or config;
179
+ - weak cryptography: MD5 or SHA-1 for passwords, fixed IVs, home-made schemes;
180
+ - missing limits on input size or rate where the input reaches something expensive.
181
+ 3. Run \`dependency_audit\` and include dependencies with known vulnerabilities.
182
+ 4. Verify every candidate: read the whole path from the input to the dangerous call with \`read_file\`, \`find_symbol\` and \`find_references\`. Keep only findings with a concrete way to exploit them. Drop the rest.
183
+
184
+ Phase 1 output: numbered findings, most severe first. Each one has:
185
+ - severity: critical, high, medium or low;
186
+ - the location, written as path:line;
187
+ - how it can be exploited, in one or two sentences, with an example input;
188
+ - the fix, in one or two sentences.
189
+ If you found nothing, say so and list what you checked. End Phase 1 with this line exactly:
190
+ Reply with the numbers to fix.
191
+
192
+ Phase 2, fix: only when the message you were given names findings to fix (for example "fix 1 and 3"). If the message you were given does not name findings to fix, do not change any file. When it does:
193
+ 1. Fix only those findings, with the smallest change that closes each hole: \`edit_file\` for one file, \`apply_patch\` for several, \`write_file\` only for a new file.
194
+ 2. Run the project's tests with \`run_command\` and report the result.
195
+ 3. Report each fix: path:line, what changed, and how it closes the hole.
196
+
197
+ Never: change a file before the person has named what to fix, report a finding you did not trace from input to the dangerous call, or print a secret you found in full (show only its first four characters).`,
198
+ allowedTools: [
199
+ 'read_file', 'list_files', 'search_files', 'find_symbol', 'find_references', 'list_symbols', 'git_diff', 'dependency_audit', 'memory_read',
200
+ 'edit_file', 'apply_patch', 'write_file', 'edit_notebook', 'run_command',
201
+ ],
202
+ forbiddenTools: ['sub_agent'],
203
+ maxIterations: 30,
107
204
  auto: false,
108
205
  },
109
206
  ];
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import type { ProviderName } from '../types.js';
14
14
  /** Built-in specialist ids. Free-form ids are also allowed (see {@link SpecialistConfig.id}). */
15
- export type BuiltInSpecialistId = 'plan' | 'code' | 'debug' | 'ask' | 'review';
15
+ export type BuiltInSpecialistId = 'plan' | 'code' | 'debug' | 'ask' | 'review' | 'security';
16
16
  export type SpecialistId = BuiltInSpecialistId | (string & {});
17
17
  /**
18
18
  * A specialist profile: the "personality" + tool allowlist for one
@@ -2,75 +2,22 @@ import { z } from 'zod';
2
2
  import type { AgentConfig, ProviderName } from '../types.js';
3
3
  declare const policySchema: z.ZodObject<{
4
4
  version: z.ZodOptional<z.ZodLiteral<1>>;
5
- /** Only these providers may run. Omit for no restriction. */
6
- allowedProviders: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
7
- /** Tools that can never be called, in addition to the user's own sandbox. */
8
- deniedTools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
9
- /** Only these MCP server names may connect. Omit for no restriction; [] blocks all. */
10
- allowedMcpServers: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
5
+ allowedProviders: z.ZodOptional<z.ZodArray<z.ZodString>>;
6
+ deniedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
7
+ allowedMcpServers: z.ZodOptional<z.ZodArray<z.ZodString>>;
11
8
  disableBrowser: z.ZodOptional<z.ZodBoolean>;
12
- /** Forces every tool call through approval, even if the user enables autonomous mode. */
13
9
  disableAutonomous: z.ZodOptional<z.ZodBoolean>;
14
- /** Hard per-run spend ceiling in USD; a user's own cap can only be lower. */
15
10
  maxCostUsdPerRun: z.ZodOptional<z.ZodNumber>;
16
11
  audit: z.ZodOptional<z.ZodObject<{
17
12
  enabled: z.ZodBoolean;
18
13
  file: z.ZodOptional<z.ZodString>;
19
- }, "strip", z.ZodTypeAny, {
20
- enabled: boolean;
21
- file?: string | undefined;
22
- }, {
23
- enabled: boolean;
24
- file?: string | undefined;
25
- }>>;
14
+ }, z.core.$strip>>;
26
15
  proxy: z.ZodOptional<z.ZodObject<{
27
16
  https: z.ZodOptional<z.ZodString>;
28
17
  http: z.ZodOptional<z.ZodString>;
29
18
  noProxy: z.ZodOptional<z.ZodString>;
30
- }, "strip", z.ZodTypeAny, {
31
- https?: string | undefined;
32
- http?: string | undefined;
33
- noProxy?: string | undefined;
34
- }, {
35
- https?: string | undefined;
36
- http?: string | undefined;
37
- noProxy?: string | undefined;
38
- }>>;
39
- }, "strict", z.ZodTypeAny, {
40
- version?: 1 | undefined;
41
- allowedProviders?: string[] | undefined;
42
- deniedTools?: string[] | undefined;
43
- allowedMcpServers?: string[] | undefined;
44
- disableBrowser?: boolean | undefined;
45
- disableAutonomous?: boolean | undefined;
46
- maxCostUsdPerRun?: number | undefined;
47
- audit?: {
48
- enabled: boolean;
49
- file?: string | undefined;
50
- } | undefined;
51
- proxy?: {
52
- https?: string | undefined;
53
- http?: string | undefined;
54
- noProxy?: string | undefined;
55
- } | undefined;
56
- }, {
57
- version?: 1 | undefined;
58
- allowedProviders?: string[] | undefined;
59
- deniedTools?: string[] | undefined;
60
- allowedMcpServers?: string[] | undefined;
61
- disableBrowser?: boolean | undefined;
62
- disableAutonomous?: boolean | undefined;
63
- maxCostUsdPerRun?: number | undefined;
64
- audit?: {
65
- enabled: boolean;
66
- file?: string | undefined;
67
- } | undefined;
68
- proxy?: {
69
- https?: string | undefined;
70
- http?: string | undefined;
71
- noProxy?: string | undefined;
72
- } | undefined;
73
- }>;
19
+ }, z.core.$strip>>;
20
+ }, z.core.$strict>;
74
21
  export type OrgPolicy = z.infer<typeof policySchema>;
75
22
  export declare class PolicyError extends Error {
76
23
  constructor(message: string);
package/dist/pricing.d.ts CHANGED
@@ -24,6 +24,8 @@ export interface ResolvedPricing extends ModelPricing {
24
24
  /** True when the lookup hit a known table entry. False when the caller
25
25
  * supplied a manual override or when the model id is unknown. */
26
26
  known: boolean;
27
+ /** The table key a substring lookup matched, when it was not an exact hit. */
28
+ matchedKey?: string;
27
29
  }
28
30
  /**
29
31
  * Look up the pricing for a given model id. Falls back to a substring match
package/dist/pricing.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { catalogEntry } from './catalog/models-dev.js';
1
2
  import { overrideCost } from './model-overrides.js';
2
3
  /**
3
4
  * Pricing table. Keys are matched with case-insensitive substring matching
@@ -9,6 +10,9 @@ const PRICING = {
9
10
  // OpenAI ----------------------------------------------------
10
11
  'gpt-4o': { input: 2.5, output: 10 },
11
12
  'gpt-4o-mini': { input: 0.15, output: 0.6 },
13
+ 'gpt-4.1': { input: 2, output: 8 },
14
+ 'gpt-4.1-mini': { input: 0.4, output: 1.6 },
15
+ 'gpt-4.1-nano': { input: 0.1, output: 0.4 },
12
16
  'gpt-4-turbo': { input: 10, output: 30 },
13
17
  'gpt-4': { input: 30, output: 60 },
14
18
  'gpt-3.5-turbo': { input: 0.5, output: 1.5 },
@@ -26,7 +30,9 @@ const PRICING = {
26
30
  'claude-sonnet-4-5': { input: 3, output: 15 },
27
31
  'claude-opus-4': { input: 15, output: 75 },
28
32
  'claude-opus-4-1': { input: 15, output: 75 },
33
+ 'claude-opus-4-5': { input: 5, output: 25 },
29
34
  'claude-haiku-4': { input: 0.8, output: 4 },
35
+ 'claude-haiku-4-5': { input: 1, output: 5 },
30
36
  // Google ----------------------------------------------------
31
37
  'gemini-1.5-pro': { input: 1.25, output: 5 },
32
38
  'gemini-1.5-flash': { input: 0.075, output: 0.3 },
@@ -35,25 +41,11 @@ const PRICING = {
35
41
  'gemini-2.0-flash-lite': { input: 0.025, output: 0.1 },
36
42
  'gemini-2.5-pro': { input: 1.25, output: 10 },
37
43
  'gemini-2.5-flash': { input: 0.3, output: 2.5 },
38
- // Ollama is local: no API cost. We surface this as 0/$0.00 in the UI so
39
- // users see the meter running with $0.00 instead of "unpriced" churn.
40
- // List a few popular model id prefixes so substring matching resolves them.
41
- 'llama3.1': { input: 0, output: 0 },
42
- 'llama3.2': { input: 0, output: 0 },
43
- 'llama3.3': { input: 0, output: 0 },
44
- 'qwen2.5': { input: 0, output: 0 },
45
- 'mistral': { input: 0, output: 0 },
46
- 'mixtral': { input: 0, output: 0 },
47
- 'gemma2': { input: 0, output: 0 },
48
- 'codellama': { input: 0, output: 0 },
49
- 'deepseek-coder': { input: 0, output: 0 },
50
- 'phi3': { input: 0, output: 0 },
51
44
  // OpenRouter ----------------------------------------------------
52
45
  // OpenRouter passes through third-party models. We list the popular ones
53
46
  // explicitly; everything else falls back to "unpriced" but still works
54
47
  // (cost events just get suppressed). Prices track OpenRouter's published
55
48
  // list at the time of writing.
56
- 'openrouter/auto': { input: 0, output: 0 }, // dynamic routing, unknown
57
49
  'anthropic/claude-3.5-sonnet': { input: 3, output: 15 },
58
50
  'anthropic/claude-3.5-haiku': { input: 0.8, output: 4 },
59
51
  'anthropic/claude-3-opus': { input: 15, output: 75 },
@@ -70,6 +62,30 @@ const PRICING = {
70
62
  // MiniMax --------------------------------------------------
71
63
  'minimax-m3': { input: 0.3, output: 1.2 },
72
64
  };
65
+ /**
66
+ * Providers that run on the person's own machine. Their models cost nothing
67
+ * whatever they are called, and only they may be priced at $0: a free entry
68
+ * keyed by a model name ("mistral") once priced the paid hosted model of the
69
+ * same name at $0 too, so a cost cap never stopped it.
70
+ */
71
+ const LOCAL_PROVIDERS = new Set(['ollama', 'lmstudio']);
72
+ /**
73
+ * Whether `key` names the model `id` at `at`, rather than an older version of
74
+ * it or part of another word. `claude-opus-4` is inside `claude-opus-4-5` and
75
+ * `gpt-4` inside `gpt-4.1`, and each once priced the newer model at the older
76
+ * one's rate. A date (`-20241022`), revision (`-002`), size (`:7b`) or variant
77
+ * (`-latest`) after the key is still the same model.
78
+ */
79
+ function namesModelAt(id, key, at) {
80
+ if (at > 0 && /[a-z0-9]/.test(id[at - 1]))
81
+ return false;
82
+ const rest = id.slice(at + key.length);
83
+ if (rest === '')
84
+ return true;
85
+ if (/^[a-z0-9]/.test(rest))
86
+ return false;
87
+ return !/^(?:\.\d|-\d{1,2}(?=$|[-.:@_]))/.test(rest);
88
+ }
73
89
  /** Provider prompt-cache billing relative to the normal input rate. */
74
90
  function cacheMultipliersFor(model) {
75
91
  switch (model.provider) {
@@ -103,6 +119,19 @@ export function lookupPricing(model, override) {
103
119
  const configured = overrideCost(model);
104
120
  if (configured)
105
121
  return { ...cache, ...configured, known: true };
122
+ if (typeof model !== 'string' && LOCAL_PROVIDERS.has(model.provider))
123
+ return { input: 0, output: 0, known: true };
124
+ const listed = typeof model === 'string' ? undefined : catalogEntry(model)?.cost;
125
+ if (listed && listed.input > 0) {
126
+ return {
127
+ ...cache,
128
+ input: listed.input,
129
+ output: listed.output,
130
+ ...(listed.cacheRead !== undefined ? { cacheReadMultiplier: listed.cacheRead / listed.input } : {}),
131
+ ...(listed.cacheWrite !== undefined ? { cacheWriteMultiplier: listed.cacheWrite / listed.input } : {}),
132
+ known: true,
133
+ };
134
+ }
106
135
  const id = (typeof model === 'string' ? model : model.model).toLowerCase();
107
136
  // Strip only the OpenRouter-style 'models/' prefix so 'models/gemini-1.5-pro'
108
137
  // resolves the same way as 'gemini-1.5-pro'. We do NOT strip provider prefixes
@@ -115,8 +144,10 @@ export function lookupPricing(model, override) {
115
144
  // Substring match: longest key wins, so 'gpt-4o-mini' beats 'gpt-4o'.
116
145
  const keys = Object.keys(PRICING).sort((a, b) => b.length - a.length);
117
146
  for (const key of keys) {
118
- if (stripped.includes(key))
119
- return { ...PRICING[key], ...cache, known: true };
147
+ for (let at = stripped.indexOf(key); at !== -1; at = stripped.indexOf(key, at + 1)) {
148
+ if (namesModelAt(stripped, key, at))
149
+ return { ...PRICING[key], ...cache, known: true, matchedKey: key };
150
+ }
120
151
  }
121
152
  return { input: 0, output: 0, known: false };
122
153
  }
@@ -258,36 +258,33 @@ export function formatZodError(err) {
258
258
  return err.issues
259
259
  .map((issue) => {
260
260
  const pathStr = issue.path.length > 0 ? issue.path.join('.') : '(root)';
261
- const expected = issueCode(issue.code);
261
+ const expected = issueCode(issue);
262
262
  const received = describeReceived(issue);
263
263
  return `${pathStr}: ${issue.message}${expected ? ` (expected ${expected}${received ? `, got ${received}` : ''})` : ''}`;
264
264
  })
265
265
  .join('\n');
266
266
  }
267
- function issueCode(code) {
268
- switch (code) {
267
+ function issueCode(issue) {
268
+ switch (issue.code) {
269
269
  case 'invalid_type':
270
270
  return 'correct type';
271
- case 'invalid_string':
271
+ case 'invalid_format':
272
272
  return 'string matching format';
273
273
  case 'too_small':
274
274
  return 'value >= min';
275
275
  case 'too_big':
276
276
  return 'value <= max';
277
- case 'invalid_enum_value':
278
- return 'one of the allowed values';
277
+ case 'invalid_value':
278
+ return issue.values.length === 1 ? 'the literal value' : 'one of the allowed values';
279
279
  case 'unrecognized_keys':
280
280
  return 'no extra keys';
281
281
  case 'invalid_union':
282
282
  return 'one of the union variants';
283
- case 'invalid_literal':
284
- return 'the literal value';
285
283
  default:
286
284
  return '';
287
285
  }
288
286
  }
289
- function describeReceived(issue) {
290
- if (issue.code !== 'invalid_type')
291
- return '';
292
- return JSON.stringify(issue.received);
287
+ // Zod 4 dropped `received` from the issue; its message already says "received <type>".
288
+ function describeReceived(_issue) {
289
+ return '';
293
290
  }
@@ -2,11 +2,7 @@ import { z } from 'zod';
2
2
  import type { ToolDefinition } from '../types.js';
3
3
  declare const params: z.ZodObject<{
4
4
  patch: z.ZodString;
5
- }, "strip", z.ZodTypeAny, {
6
- patch: string;
7
- }, {
8
- patch: string;
9
- }>;
5
+ }, z.core.$strip>;
10
6
  export type PatchOp = {
11
7
  kind: 'add';
12
8
  path: string;
@@ -7,17 +7,9 @@ import type { ToolDefinition } from '../types.js';
7
7
  */
8
8
  declare const params: z.ZodObject<{
9
9
  question: z.ZodString;
10
- options: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
10
+ options: z.ZodOptional<z.ZodArray<z.ZodString>>;
11
11
  allowCustom: z.ZodOptional<z.ZodBoolean>;
12
- }, "strip", z.ZodTypeAny, {
13
- question: string;
14
- options?: string[] | undefined;
15
- allowCustom?: boolean | undefined;
16
- }, {
17
- question: string;
18
- options?: string[] | undefined;
19
- allowCustom?: boolean | undefined;
20
- }>;
12
+ }, z.core.$strip>;
21
13
  export interface AskUserResult {
22
14
  answer: string;
23
15
  unavailable?: boolean;
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { raceAbort } from '../agent/race-abort.js';
2
3
  /**
3
4
  * Ask the person a question with optional choices. The agent only offers this tool when a surface can answer
4
5
  * (AgentConfig.askUser) and the run is not autonomous; if it is called anyway, or the run is stopped while the
@@ -45,16 +46,23 @@ export const askUserTool = {
45
46
  ctx.askBudget.used += 1;
46
47
  }
47
48
  const budget = ctx.askBudget;
49
+ const refund = () => {
50
+ if (budget)
51
+ budget.used = Math.max(0, budget.used - 1);
52
+ };
48
53
  const answered = ctx.askUser(args, signal).then((answer) => {
49
54
  // A question the person dismissed resolves to an empty answer and must not
50
55
  // cost part of the budget: four dismissals would otherwise end an interview
51
56
  // without a single question being answered. Refunding keeps the budget a
52
57
  // count of questions actually answered.
53
- if (budget && answer.trim() === '')
54
- budget.used = Math.max(0, budget.used - 1);
58
+ if (answer.trim() === '')
59
+ refund();
55
60
  return { answer };
61
+ }, (err) => {
62
+ // A surface that failed never showed the question, so it costs nothing either.
63
+ refund();
64
+ throw err;
56
65
  });
57
- const aborted = new Promise((resolve) => signal?.addEventListener('abort', () => resolve(UNAVAILABLE), { once: true }));
58
- return Promise.race([answered, aborted]);
66
+ return raceAbort(answered, signal, UNAVAILABLE);
59
67
  },
60
68
  };