@kolisachint/hoocode-agent 0.5.30 → 0.5.32
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/CHANGELOG.md +234 -0
- package/dist/core/agent-selection-eval.d.ts +91 -0
- package/dist/core/agent-selection-eval.d.ts.map +1 -0
- package/dist/core/agent-selection-eval.js +186 -0
- package/dist/core/agent-selection-eval.js.map +1 -0
- package/dist/core/agent-session-runtime.d.ts +28 -0
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +61 -2
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/builtin-skills.d.ts +61 -0
- package/dist/core/builtin-skills.d.ts.map +1 -0
- package/dist/core/builtin-skills.js +106 -0
- package/dist/core/builtin-skills.js.map +1 -0
- package/dist/core/extensions/plugins/default-marketplace/.agents-plugin/marketplace.json +5 -1
- package/dist/core/extensions/plugins/trigger-judge.d.ts +42 -0
- package/dist/core/extensions/plugins/trigger-judge.d.ts.map +1 -0
- package/dist/core/extensions/plugins/trigger-judge.js +121 -0
- package/dist/core/extensions/plugins/trigger-judge.js.map +1 -0
- package/dist/core/external-tools.d.ts +67 -0
- package/dist/core/external-tools.d.ts.map +1 -0
- package/dist/core/external-tools.js +120 -0
- package/dist/core/external-tools.js.map +1 -0
- package/dist/core/keybindings.d.ts +102 -30
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +123 -29
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/mode-prompts.d.ts +15 -3
- package/dist/core/mode-prompts.d.ts.map +1 -1
- package/dist/core/mode-prompts.js +17 -29
- package/dist/core/mode-prompts.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +1 -1
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +1 -1
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +10 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +19 -8
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +1 -1
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +5 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tool-output-view.d.ts +33 -0
- package/dist/core/tool-output-view.d.ts.map +1 -0
- package/dist/core/tool-output-view.js +31 -0
- package/dist/core/tool-output-view.js.map +1 -0
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +4 -0
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/propose-plugin.d.ts.map +1 -1
- package/dist/core/tools/propose-plugin.js +7 -7
- package/dist/core/tools/propose-plugin.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +7 -1
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +12 -24
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/extensions/core/modes.d.ts.map +1 -1
- package/dist/extensions/core/modes.js +21 -23
- package/dist/extensions/core/modes.js.map +1 -1
- package/dist/extensions/core/scaffold.d.ts.map +1 -1
- package/dist/extensions/core/scaffold.js +28 -24
- package/dist/extensions/core/scaffold.js.map +1 -1
- package/dist/init-templates.generated.d.ts +3 -0
- package/dist/init-templates.generated.d.ts.map +1 -1
- package/dist/init-templates.generated.js +12 -0
- package/dist/init-templates.generated.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +14 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/command-executor.d.ts +20 -0
- package/dist/modes/interactive/command-executor.d.ts.map +1 -1
- package/dist/modes/interactive/command-executor.js +138 -9
- package/dist/modes/interactive/command-executor.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +4 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +31 -2
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts +2 -2
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +2 -2
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +2 -2
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +10 -2
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +165 -23
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-chain-summary.d.ts +65 -0
- package/dist/modes/interactive/components/tool-chain-summary.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-chain-summary.js +216 -0
- package/dist/modes/interactive/components/tool-chain-summary.js.map +1 -0
- package/dist/modes/interactive/components/tool-chain.d.ts +61 -0
- package/dist/modes/interactive/components/tool-chain.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-chain.js +156 -0
- package/dist/modes/interactive/components/tool-chain.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts +55 -9
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +173 -33
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tool-signal.d.ts +50 -0
- package/dist/modes/interactive/components/tool-signal.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-signal.js +151 -0
- package/dist/modes/interactive/components/tool-signal.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +50 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +244 -29
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/resource-display.d.ts +9 -0
- package/dist/modes/interactive/resource-display.d.ts.map +1 -1
- package/dist/modes/interactive/resource-display.js +13 -0
- package/dist/modes/interactive/resource-display.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +30 -0
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +35 -0
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/modes.md +4 -0
- package/docs/plugins.md +10 -0
- package/docs/settings.md +42 -0
- package/docs/skills.md +30 -0
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +5 -4
- package/templates/prompts/grill-bridge.md +1 -0
- package/templates/prompts/grill-me.md +7 -0
- package/templates/prompts/grill-plan.md +9 -0
- package/templates/prompts/task-background-agents.md +2 -0
- package/templates/prompts/task-background-none.md +1 -0
- package/templates/prompts/task-main.md +19 -0
- package/templates/skills/plugin-authoring/SKILL.md +81 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,239 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.32] - 2026-08-23
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
|
|
7
|
+
- The tool-output setting is renamed and its values with it: `toolOutputDisplay`
|
|
8
|
+
(`collapsed` / `peek` / `standard`) is now `toolOutputView` (`radar` /
|
|
9
|
+
`glance` / `full`), and the default moves from `standard` to `glance`. Old
|
|
10
|
+
settings files still load — `collapsed` reads as `radar`, `peek` as `glance`,
|
|
11
|
+
`standard` as `full` — and the old key is rewritten the first time the view is
|
|
12
|
+
changed. Nothing is lost, but a session that used to open with every tool
|
|
13
|
+
result on screen now opens with call lines and folds the bodies away.
|
|
14
|
+
|
|
15
|
+
- Keybindings moved off keys the terminal, the shell, or the prompt editor
|
|
16
|
+
already owned. The bindings are configurable as before, so a
|
|
17
|
+
`~/.hoocode/keybindings.json` that pins any of these keeps working:
|
|
18
|
+
|
|
19
|
+
| Action | Was | Now |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| Open model selector | `ctrl+l` | `alt+m` |
|
|
22
|
+
| Open external editor | `ctrl+g` | `alt+e` |
|
|
23
|
+
| Voice record | `ctrl+r` | `alt+r` |
|
|
24
|
+
| Session picker: toggle path | `ctrl+p` | `alt+p` |
|
|
25
|
+
| Session picker: sort order | `ctrl+s` | `alt+o` |
|
|
26
|
+
| Session picker: rename | `ctrl+r` | `alt+r` |
|
|
27
|
+
| Session picker: delete | `ctrl+d` | `alt+x` |
|
|
28
|
+
| Session picker: named filter | `ctrl+n` | `alt+n` |
|
|
29
|
+
| Model picker: save | `ctrl+s` | `alt+s` |
|
|
30
|
+
| Model picker: enable all | `ctrl+a` | `alt+a` |
|
|
31
|
+
| Model picker: clear all | `ctrl+x` | `alt+x` |
|
|
32
|
+
| Model picker: toggle provider | `ctrl+p` | `alt+g` |
|
|
33
|
+
| Tree filters | `ctrl+d/t/u/l/a` | `alt+1`…`alt+5` |
|
|
34
|
+
| Tree filter cycle | `ctrl+o` / `shift+ctrl+o` | `alt+c` / `shift+alt+c` |
|
|
35
|
+
| Team roster focus | `alt+n` | unchanged |
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- A **view dial** for tool output, on `alt+o` (`shift+alt+o` cycles back), with
|
|
40
|
+
three stops from least to most:
|
|
41
|
+
|
|
42
|
+
- **radar** — one line per *chain*: a run of consecutive tool calls. While the
|
|
43
|
+
run is working the line shows its shape, in order, with the failures marked
|
|
44
|
+
— `◐ grep › read › bash✗ › edit › bash… 4 done · 1 failed · running`. Once
|
|
45
|
+
the agent moves on it becomes what the run amounted to —
|
|
46
|
+
`● Edited packages/tui/src/keys.ts 5 calls · 1 failed · 453 lines`.
|
|
47
|
+
Consecutive repeats collapse (`read ×4`) and long chains elide their middle,
|
|
48
|
+
but never a failure. `alt+u` turns a chain back into its individual calls.
|
|
49
|
+
- **glance** — the tool's own call line, one per call, body folded away. The
|
|
50
|
+
new default.
|
|
51
|
+
- **full** — call line plus the result body, as before.
|
|
52
|
+
|
|
53
|
+
- **A failure always shows why it failed, in every view.** Previously a failed
|
|
54
|
+
tool in the folded views was a red dot and nothing else — the one thing you
|
|
55
|
+
always want to see was the one thing they hid.
|
|
56
|
+
|
|
57
|
+
The footer shows where the dial sits, with a glyph that fills up as the view
|
|
58
|
+
widens.
|
|
59
|
+
|
|
60
|
+
- `alt+u` opens one thing, newest first, repeating to peel backwards;
|
|
61
|
+
`shift+alt+u` re-folds. In radar it turns a chain back into its calls;
|
|
62
|
+
elsewhere it opens a single tool body. `ctrl+o` is unchanged and still expands
|
|
63
|
+
everything at once.
|
|
64
|
+
|
|
65
|
+
It works from the newest backwards rather than through a cursor because the
|
|
66
|
+
transcript is bottom-anchored with no app-level scrolling — anything far
|
|
67
|
+
enough up is in the terminal's own scrollback, where this process can neither
|
|
68
|
+
scroll nor place a selection, so a cursor would move somewhere you cannot see.
|
|
69
|
+
|
|
70
|
+
### Removed
|
|
71
|
+
|
|
72
|
+
- The `▸` disclosure caret on folded tool rows. It is a click-target idiom in a
|
|
73
|
+
TUI with no pointer, it cost two columns on every row, and it advertised a
|
|
74
|
+
per-row action that could only ever be reached from the keyboard. Opening is
|
|
75
|
+
now `ctrl+o` (everything) or `alt+u` (one thing, newest first).
|
|
76
|
+
|
|
77
|
+
- `/cd <path>` moves the whole session to another directory without leaving the
|
|
78
|
+
process — provider auth, the warmed model list and the terminal all survive.
|
|
79
|
+
Everything cwd-bound is rebuilt for the new root (tools, context files,
|
|
80
|
+
project settings, extensions, skills, agents, MCP servers), and because
|
|
81
|
+
sessions are stored per project, a fresh session starts there; the one you
|
|
82
|
+
left is still on disk and `/resume` in the old directory reopens it. Bare
|
|
83
|
+
`/cd` goes home, `/cd -` returns to the previous directory, and the argument
|
|
84
|
+
completes against real subdirectories. Bound to `alt+w`.
|
|
85
|
+
|
|
86
|
+
- Keys for the cockpit moves that had none: `alt+g` cycles the agent mode
|
|
87
|
+
(ask → plan → build → debug, taken from the mode command's own list rather
|
|
88
|
+
than a copy), `alt+s` opens settings, `alt+k` opens the shortcut list,
|
|
89
|
+
`alt+t` opens the session tree, and `alt+h` resumes from history. `/new` and
|
|
90
|
+
`/fork` stay unbound by default — one replaces the transcript, the other needs
|
|
91
|
+
a message picked out of it.
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
|
|
95
|
+
- The keybinding set is organized into three rings, and which ring a key is in
|
|
96
|
+
is decided by its modifier. `ctrl` is the view: what is on screen right now.
|
|
97
|
+
`alt` is the cockpit: what the agent is and where it works. Inside a picker,
|
|
98
|
+
every `ctrl` key belongs to the query you are typing, so a picker's own verbs
|
|
99
|
+
are all on `alt`. That last rule is what most of the moves above are: the
|
|
100
|
+
session picker took `ctrl+a` (start of line), `ctrl+u` (kill to start) and
|
|
101
|
+
`ctrl+d` (delete character) out of its own search field, and the model picker
|
|
102
|
+
put "save" on `ctrl+s`, which is XOFF on a terminal with flow control on.
|
|
103
|
+
|
|
104
|
+
Every default is checked against what a terminal without the Kitty keyboard
|
|
105
|
+
protocol actually sends. Only `alt+<letter>` and `alt+<digit>` survive there,
|
|
106
|
+
and legacy `alt+p`/`alt+n` double as `alt+up`/`alt+down`, so no scope binds
|
|
107
|
+
both halves of either pair — that is why the model picker's provider toggle is
|
|
108
|
+
`alt+g` rather than the obvious `alt+p`, which would have fired the reorder
|
|
109
|
+
key sitting next to it. `test/keybinding-layout.test.ts` holds all of this.
|
|
110
|
+
|
|
111
|
+
- `ctrl+n` no longer means two things. It was both "cycle the task panel" and
|
|
112
|
+
the session picker's named filter; the filter moves to `alt+n`.
|
|
113
|
+
|
|
114
|
+
- `/hotkeys` is grouped by ring — Flow, View, Cockpit — instead of by
|
|
115
|
+
Navigation / Editing / Other.
|
|
116
|
+
|
|
117
|
+
### Fixed
|
|
118
|
+
|
|
119
|
+
- A self-rendering tool's call line (`edit`) sat one column left of every other
|
|
120
|
+
row while its body was folded away, because it skipped the padded shell its
|
|
121
|
+
own diff frame needs. Harmless while `peek` was opt-in, visible on every edit
|
|
122
|
+
now that `glance` is the default.
|
|
123
|
+
|
|
124
|
+
- The startup banner names the working directory and never re-read it, so after
|
|
125
|
+
`/cd` it kept advertising the directory you had left.
|
|
126
|
+
|
|
127
|
+
- `edit`'s call line sat one column right of every other tool's in the folded
|
|
128
|
+
views. Its render component pads horizontally to carry the diff's header band,
|
|
129
|
+
and with no band to draw that padding was just an indent. It now pads only
|
|
130
|
+
when the band is there.
|
|
131
|
+
|
|
132
|
+
- A failed call's reason now hangs off its radar row instead of starting back at
|
|
133
|
+
the left margin.
|
|
134
|
+
## [0.5.31] - 2026-08-23
|
|
135
|
+
|
|
136
|
+
### Added
|
|
137
|
+
|
|
138
|
+
- `/settings` has an **External tools** category. hoocode ships five optional
|
|
139
|
+
Rust binaries — `rg`, `fd`, `embsearch`, `webtools`, `voicetools` — that
|
|
140
|
+
expand what it can do, and nothing in the product ever said so. hoocode works
|
|
141
|
+
without every one of them, which is exactly why they stayed invisible: search
|
|
142
|
+
silently got slower, semantic ranking silently never happened, and web and
|
|
143
|
+
voice were features nobody knew were there. Each row shows live status
|
|
144
|
+
(installed / found on `PATH` / env override / not installed), and opens a
|
|
145
|
+
detail view naming what it enables, what hoocode does instead when it is
|
|
146
|
+
missing, where the release comes from, and the env vars that steer it.
|
|
147
|
+
|
|
148
|
+
- hoocode can ship skills of its own. It read them from `~/.agents/skills`,
|
|
149
|
+
`.hoocode/skills`, `.claude/skills` and installed packages — every source
|
|
150
|
+
except itself — which is why it shipped three subagents and zero skills while
|
|
151
|
+
telling users skills are the extension unit. Built-ins are catalogued in
|
|
152
|
+
`core/builtin-skills.ts` and load at lowest precedence, so a skill of the same
|
|
153
|
+
name from anywhere else wins and the collision is reported.
|
|
154
|
+
|
|
155
|
+
A skill costs its description on every turn, so each built-in can be gated on
|
|
156
|
+
the feature it serves rather than on everyone's token budget.
|
|
157
|
+
|
|
158
|
+
- The first one: `plugin-authoring`, the craft half of `ProposePlugin`/
|
|
159
|
+
`UpdatePlugin` — when a capability is worth extracting, naming and describing
|
|
160
|
+
it so it triggers again, portability rules, and the hook trap where a changed
|
|
161
|
+
command adds a second hook instead of replacing one. Gated on
|
|
162
|
+
`enablePluginTools`, which is off by default, so a default session pays
|
|
163
|
+
nothing for it.
|
|
164
|
+
|
|
165
|
+
Built-ins are materialized to a content-addressed cache under
|
|
166
|
+
`~/.hoocode/cache/builtin-skills/`. A skill is loaded by reading its file, so
|
|
167
|
+
its location has to be a real path, and the compiled standalone binary has no
|
|
168
|
+
install directory to read from; materializing the same embedded copy
|
|
169
|
+
everywhere keeps the skill set identical across npm, pnpm, source and the
|
|
170
|
+
binary. If the cache cannot be written the built-ins are absent and everything
|
|
171
|
+
else runs normally. `--no-skills` and `--light` suppress them.
|
|
172
|
+
|
|
173
|
+
- An agent-selection eval: `bun run agent-eval` scores the built-in agent roster
|
|
174
|
+
against a gold set of real tasks, reporting how often each agent is chosen
|
|
175
|
+
when it should be, how often the parent correctly keeps work inline, and a
|
|
176
|
+
confusion matrix naming which agent loses to which. It reuses the plugin G4
|
|
177
|
+
trigger harness rather than adding a second one, and describes each agent with
|
|
178
|
+
the summarized text `<available_agents>` actually emits.
|
|
179
|
+
|
|
180
|
+
This exists to settle whether `plan` and `explore` are two agents or one:
|
|
181
|
+
they ship with the same tools, the same isolation and the same background
|
|
182
|
+
flag, and `complexity` on the Task tool already expresses the only other
|
|
183
|
+
difference. That was being argued from intuition; it is now measurable.
|
|
184
|
+
|
|
185
|
+
### Fixed
|
|
186
|
+
|
|
187
|
+
- The G4 plugin trigger gate never had a judge. `trigger-eval.ts` takes its
|
|
188
|
+
model call as a parameter so scoring stays testable, and nothing in the tree
|
|
189
|
+
ever passed one — so every G4 run has reported `not-run` since it was written.
|
|
190
|
+
`createLlmTriggerJudge` is that judge, shared with the agent-selection eval.
|
|
191
|
+
|
|
192
|
+
- `/new-skill`, `/new-agent` and `/new-command` scaffolded different content
|
|
193
|
+
depending on whether `--platform` was set. Each command has two write paths —
|
|
194
|
+
the per-vendor emitters and the plain `.hoocode/` writer — and each carried
|
|
195
|
+
its own copy of the body, which had drifted in both directions: the
|
|
196
|
+
`.hoocode/` command body documented the `${@:N}` / `${@:N:L}` slice
|
|
197
|
+
placeholders that the platform path silently omitted, and the `.hoocode/`
|
|
198
|
+
agent body identified the agent as running inside hoocode where the platform
|
|
199
|
+
one did not. Both paths read one definition now, and the richer text won in
|
|
200
|
+
each case.
|
|
201
|
+
|
|
202
|
+
### Changed
|
|
203
|
+
|
|
204
|
+
- The four built-in mode prompts have one home. `templates/modes/<mode>/system.md`
|
|
205
|
+
is now the only copy: `core/mode-prompts.ts` re-exports the embedded copy as
|
|
206
|
+
`DEFAULT_MODE_PROMPTS` instead of carrying a second, hand-written set. The two
|
|
207
|
+
had already drifted — `/init` scaffolds the template text into a project, so a
|
|
208
|
+
user who ran it and a user who did not were getting differently worded mode
|
|
209
|
+
rules from the same version.
|
|
210
|
+
|
|
211
|
+
- The `/grill` phases moved out of TypeScript into `templates/prompts/*.md`.
|
|
212
|
+
They are prose the runtime injects verbatim — no interpolation, no branching —
|
|
213
|
+
so they are edited as prose now. The message `/grill` produces is unchanged.
|
|
214
|
+
|
|
215
|
+
- The Task delegation appendix moved to `templates/prompts/task-main.md` and its
|
|
216
|
+
two `task-background-*` variants. At ~600 tok/turn it is the largest block of
|
|
217
|
+
always-on text hoocode emits once the Task tool is on, and it had exactly one
|
|
218
|
+
interpolation slot in otherwise static prose, so a string constant bought
|
|
219
|
+
nothing. The rendered prompt is byte-identical on both the with- and
|
|
220
|
+
without-background-agents branches.
|
|
221
|
+
|
|
222
|
+
- `ProposePlugin` and `UpdatePlugin` shed ~55% of their always-on prompt
|
|
223
|
+
guidance (~626 to ~301 tok/turn when the plugin system is enabled). What was
|
|
224
|
+
removed was how-to-author-well guidance, which is not a tool contract and now
|
|
225
|
+
lives in the `plugin-authoring` skill; what stayed is the trigger, the
|
|
226
|
+
transparency rule for autonomous authoring, the hook trap, and the two hard
|
|
227
|
+
prohibitions. Several removed lines also restated the tools' own descriptions
|
|
228
|
+
or each other.
|
|
229
|
+
|
|
230
|
+
- Settings rows that are inert without one of those binaries now say so. The
|
|
231
|
+
`web` and `semantic search` tool-group switches, the web tools timeout and the
|
|
232
|
+
voice silence window carry a `needs <binary>` marker and explain the fallback
|
|
233
|
+
and whether hoocode will fetch the binary. The rows stay settable — the
|
|
234
|
+
setting is what makes hoocode fetch the binary in the first place, and hiding
|
|
235
|
+
a row when its dependency is missing would recreate the same silence.
|
|
236
|
+
|
|
3
237
|
## [0.5.30] - 2026-08-23
|
|
4
238
|
|
|
5
239
|
### Added
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does the agent roster steer a dispatch to the right agent?
|
|
3
|
+
*
|
|
4
|
+
* The `Task` tool asks the model to pick a `subagent_type` from
|
|
5
|
+
* `<available_agents>`, and that block contains nothing but each agent's
|
|
6
|
+
* summarized description. So agent selection is the same question G4 asks about
|
|
7
|
+
* skills — "given these descriptions and this situation, which one fires?" —
|
|
8
|
+
* and it reuses the same harness rather than growing a second one.
|
|
9
|
+
*
|
|
10
|
+
* What this exists to decide: `plan` and `explore` ship with the same tools, the
|
|
11
|
+
* same isolation and the same `background` flag, differing only in model tier
|
|
12
|
+
* and output contract — and `complexity` on the Task tool already expresses the
|
|
13
|
+
* tier. Whether they are two agents or one is a question about whether the model
|
|
14
|
+
* can actually tell them apart from their descriptions, which is measurable and
|
|
15
|
+
* was previously being argued from intuition.
|
|
16
|
+
*
|
|
17
|
+
* The candidates use `summarizeAgentDescription`, not the raw frontmatter: the
|
|
18
|
+
* summary is what the system prompt actually emits, and evaluating the full
|
|
19
|
+
* description would score text the model never sees.
|
|
20
|
+
*/
|
|
21
|
+
import { type TriggerCandidate, type TriggerCase, type TriggerEvalOutcome, type TriggerJudge } from "./extensions/plugins/trigger-eval.js";
|
|
22
|
+
/** How often one agent was chosen where another was expected. */
|
|
23
|
+
export interface ConfusionEntry {
|
|
24
|
+
expected: string;
|
|
25
|
+
actual: string;
|
|
26
|
+
count: number;
|
|
27
|
+
}
|
|
28
|
+
export interface AgentSelectionReport {
|
|
29
|
+
corpusHash: string;
|
|
30
|
+
agents: string[];
|
|
31
|
+
caseCount: number;
|
|
32
|
+
/** Cases where the expected agent was chosen, over cases expecting any agent. */
|
|
33
|
+
accuracy?: number;
|
|
34
|
+
/** Cases correctly left to the parent, over cases expecting no delegation. */
|
|
35
|
+
inlineAccuracy?: number;
|
|
36
|
+
/** Every wrong pick, most frequent first. The pairs here are the finding. */
|
|
37
|
+
confusion: ConfusionEntry[];
|
|
38
|
+
/** Per-agent recall: chosen / expected. An agent nobody picks is dead weight. */
|
|
39
|
+
perAgent: Array<{
|
|
40
|
+
agent: string;
|
|
41
|
+
expected: number;
|
|
42
|
+
chosen: number;
|
|
43
|
+
recall: number;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The built-in agents as judge candidates, described exactly as the system
|
|
48
|
+
* prompt describes them.
|
|
49
|
+
*
|
|
50
|
+
* `own: true` for all of them: unlike a plugin eval there is no foreign roster
|
|
51
|
+
* to discriminate against, so every case is scored against the same closed set.
|
|
52
|
+
* The `expect: null` cases carry the discriminative half instead — they ask
|
|
53
|
+
* whether the model declines to delegate work it should keep.
|
|
54
|
+
*/
|
|
55
|
+
export declare function agentCandidates(cwd?: string): TriggerCandidate[];
|
|
56
|
+
/** Load a gold set: `{ "cases": [{ "prompt": "...", "expect": "explore" | null }] }`. */
|
|
57
|
+
export declare function loadAgentCases(file: string): TriggerCase[] | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Every expected agent in the gold set must exist in the roster.
|
|
60
|
+
*
|
|
61
|
+
* A typo'd or removed agent name would otherwise score as a permanent miss and
|
|
62
|
+
* read as a description problem, which is the most expensive way to be wrong
|
|
63
|
+
* about an eval.
|
|
64
|
+
*/
|
|
65
|
+
export declare function validateAgentCases(candidates: readonly TriggerCandidate[], cases: readonly TriggerCase[]): string[];
|
|
66
|
+
/**
|
|
67
|
+
* Turn a scored run into the report that answers the design question.
|
|
68
|
+
*
|
|
69
|
+
* `runTriggerEval`'s recall/specificity are the right numbers for a plugin
|
|
70
|
+
* defending itself against a foreign roster. Here the roster is closed, so the
|
|
71
|
+
* useful shape is a confusion matrix: which agent loses to which, and how often.
|
|
72
|
+
* "explore and plan are interchangeable" is a claim about one cell.
|
|
73
|
+
*/
|
|
74
|
+
export declare function summarizeAgentSelection(outcome: Extract<TriggerEvalOutcome, {
|
|
75
|
+
status: "ran";
|
|
76
|
+
}>, candidates: readonly TriggerCandidate[]): AgentSelectionReport;
|
|
77
|
+
export type AgentSelectionOutcome = {
|
|
78
|
+
status: "not-run";
|
|
79
|
+
reason: string;
|
|
80
|
+
} | {
|
|
81
|
+
status: "ran";
|
|
82
|
+
report: AgentSelectionReport;
|
|
83
|
+
outcome: Extract<TriggerEvalOutcome, {
|
|
84
|
+
status: "ran";
|
|
85
|
+
}>;
|
|
86
|
+
};
|
|
87
|
+
/** Score the roster against a gold set. Never throws; a missing model is `not-run`. */
|
|
88
|
+
export declare function runAgentSelectionEval(candidates: readonly TriggerCandidate[], cases: readonly TriggerCase[] | undefined, judge: TriggerJudge | undefined): Promise<AgentSelectionOutcome>;
|
|
89
|
+
/** Human-readable report for the CLI. */
|
|
90
|
+
export declare function formatAgentSelectionReport(report: AgentSelectionReport): string;
|
|
91
|
+
//# sourceMappingURL=agent-selection-eval.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-selection-eval.d.ts","sourceRoot":"","sources":["../../src/core/agent-selection-eval.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,OAAO,EAEN,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,MAAM,sCAAsC,CAAC;AAE9C,iEAAiE;AACjE,MAAM,WAAW,cAAc;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,oBAAoB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,iFAAiF;IACjF,QAAQ,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrF;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAsB,GAAG,gBAAgB,EAAE,CAU/E;AAED,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,GAAG,SAAS,CAiBtE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,SAAS,gBAAgB,EAAE,EAAE,KAAK,EAAE,SAAS,WAAW,EAAE,GAAG,MAAM,EAAE,CASnH;AASD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACtC,OAAO,EAAE,OAAO,CAAC,kBAAkB,EAAE;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,CAAC,EACvD,UAAU,EAAE,SAAS,gBAAgB,EAAE,GACrC,oBAAoB,CAiDtB;AAED,MAAM,MAAM,qBAAqB,GAC9B;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,MAAM,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC,kBAAkB,EAAE;QAAE,MAAM,EAAE,KAAK,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC;AAE5G,uFAAuF;AACvF,wBAAsB,qBAAqB,CAC1C,UAAU,EAAE,SAAS,gBAAgB,EAAE,EACvC,KAAK,EAAE,SAAS,WAAW,EAAE,GAAG,SAAS,EACzC,KAAK,EAAE,YAAY,GAAG,SAAS,GAC7B,OAAO,CAAC,qBAAqB,CAAC,CAmBhC;AAED,yCAAyC;AACzC,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAkB/E","sourcesContent":["/**\n * Does the agent roster steer a dispatch to the right agent?\n *\n * The `Task` tool asks the model to pick a `subagent_type` from\n * `<available_agents>`, and that block contains nothing but each agent's\n * summarized description. So agent selection is the same question G4 asks about\n * skills — \"given these descriptions and this situation, which one fires?\" —\n * and it reuses the same harness rather than growing a second one.\n *\n * What this exists to decide: `plan` and `explore` ship with the same tools, the\n * same isolation and the same `background` flag, differing only in model tier\n * and output contract — and `complexity` on the Task tool already expresses the\n * tier. Whether they are two agents or one is a question about whether the model\n * can actually tell them apart from their descriptions, which is measurable and\n * was previously being argued from intuition.\n *\n * The candidates use `summarizeAgentDescription`, not the raw frontmatter: the\n * summary is what the system prompt actually emits, and evaluating the full\n * description would score text the model never sees.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { loadAgentRegistry, summarizeAgentDescription } from \"./agent-registry.js\";\nimport {\n\trunTriggerEval,\n\ttype TriggerCandidate,\n\ttype TriggerCase,\n\ttype TriggerEvalOutcome,\n\ttype TriggerJudge,\n} from \"./extensions/plugins/trigger-eval.js\";\n\n/** How often one agent was chosen where another was expected. */\nexport interface ConfusionEntry {\n\texpected: string;\n\tactual: string;\n\tcount: number;\n}\n\nexport interface AgentSelectionReport {\n\tcorpusHash: string;\n\tagents: string[];\n\tcaseCount: number;\n\t/** Cases where the expected agent was chosen, over cases expecting any agent. */\n\taccuracy?: number;\n\t/** Cases correctly left to the parent, over cases expecting no delegation. */\n\tinlineAccuracy?: number;\n\t/** Every wrong pick, most frequent first. The pairs here are the finding. */\n\tconfusion: ConfusionEntry[];\n\t/** Per-agent recall: chosen / expected. An agent nobody picks is dead weight. */\n\tperAgent: Array<{ agent: string; expected: number; chosen: number; recall: number }>;\n}\n\n/**\n * The built-in agents as judge candidates, described exactly as the system\n * prompt describes them.\n *\n * `own: true` for all of them: unlike a plugin eval there is no foreign roster\n * to discriminate against, so every case is scored against the same closed set.\n * The `expect: null` cases carry the discriminative half instead — they ask\n * whether the model declines to delegate work it should keep.\n */\nexport function agentCandidates(cwd: string = process.cwd()): TriggerCandidate[] {\n\tconst registry = loadAgentRegistry({ cwd, includeBuiltins: true, includeClaude: false });\n\treturn registry\n\t\t.list()\n\t\t.map((agent) => ({\n\t\t\tname: agent.name,\n\t\t\tdescription: summarizeAgentDescription(agent.description ?? \"\"),\n\t\t\town: true,\n\t\t}))\n\t\t.sort((a, b) => a.name.localeCompare(b.name));\n}\n\n/** Load a gold set: `{ \"cases\": [{ \"prompt\": \"...\", \"expect\": \"explore\" | null }] }`. */\nexport function loadAgentCases(file: string): TriggerCase[] | undefined {\n\tif (!existsSync(file)) return undefined;\n\ttry {\n\t\tconst raw = JSON.parse(readFileSync(file, \"utf-8\")) as { cases?: unknown };\n\t\tif (!Array.isArray(raw.cases)) return undefined;\n\t\tconst cases = raw.cases.filter(\n\t\t\t(c): c is TriggerCase =>\n\t\t\t\t!!c &&\n\t\t\t\ttypeof c === \"object\" &&\n\t\t\t\ttypeof (c as TriggerCase).prompt === \"string\" &&\n\t\t\t\t(c as TriggerCase).prompt.trim().length > 0 &&\n\t\t\t\t((c as TriggerCase).expect === null || typeof (c as TriggerCase).expect === \"string\"),\n\t\t);\n\t\treturn cases.length > 0 ? cases : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Every expected agent in the gold set must exist in the roster.\n *\n * A typo'd or removed agent name would otherwise score as a permanent miss and\n * read as a description problem, which is the most expensive way to be wrong\n * about an eval.\n */\nexport function validateAgentCases(candidates: readonly TriggerCandidate[], cases: readonly TriggerCase[]): string[] {\n\tconst known = new Set(candidates.map((c) => c.name));\n\tconst problems: string[] = [];\n\tfor (const [i, testCase] of cases.entries()) {\n\t\tif (testCase.expect !== null && !known.has(testCase.expect)) {\n\t\t\tproblems.push(`case ${i} expects \"${testCase.expect}\", which is not in the roster`);\n\t\t}\n\t}\n\treturn problems;\n}\n\nfunction hashCorpus(candidates: readonly TriggerCandidate[], cases: readonly TriggerCase[]): string {\n\tconst h = createHash(\"sha256\");\n\tfor (const c of candidates) h.update(`${c.name} ${c.description} `);\n\tfor (const c of cases) h.update(`${c.prompt} ${c.expect ?? \"\"} `);\n\treturn h.digest(\"hex\").slice(0, 16);\n}\n\n/**\n * Turn a scored run into the report that answers the design question.\n *\n * `runTriggerEval`'s recall/specificity are the right numbers for a plugin\n * defending itself against a foreign roster. Here the roster is closed, so the\n * useful shape is a confusion matrix: which agent loses to which, and how often.\n * \"explore and plan are interchangeable\" is a claim about one cell.\n */\nexport function summarizeAgentSelection(\n\toutcome: Extract<TriggerEvalOutcome, { status: \"ran\" }>,\n\tcandidates: readonly TriggerCandidate[],\n): AgentSelectionReport {\n\tconst confusion = new Map<string, ConfusionEntry>();\n\tconst expectedCounts = new Map<string, number>();\n\tconst chosenCounts = new Map<string, number>();\n\tlet delegated = 0;\n\tlet delegatedCorrect = 0;\n\tlet inline = 0;\n\tlet inlineCorrect = 0;\n\n\tfor (const result of outcome.record.results) {\n\t\tif (result.expected === null) {\n\t\t\tinline++;\n\t\t\tif (result.actual === null) inlineCorrect++;\n\t\t\tcontinue;\n\t\t}\n\t\tdelegated++;\n\t\texpectedCounts.set(result.expected, (expectedCounts.get(result.expected) ?? 0) + 1);\n\t\tif (result.correct) {\n\t\t\tdelegatedCorrect++;\n\t\t\tchosenCounts.set(result.expected, (chosenCounts.get(result.expected) ?? 0) + 1);\n\t\t\tcontinue;\n\t\t}\n\t\t// \"(none)\" is a real outcome, not a missing value: declining to delegate\n\t\t// work that should have been delegated is a different failure from picking\n\t\t// the wrong agent, and collapsing them would hide which one is happening.\n\t\tconst actual = result.actual ?? \"(none)\";\n\t\tconst key = `${result.expected} ${actual}`;\n\t\tconst entry = confusion.get(key) ?? { expected: result.expected, actual, count: 0 };\n\t\tentry.count++;\n\t\tconfusion.set(key, entry);\n\t}\n\n\tconst perAgent = candidates\n\t\t.map((candidate) => {\n\t\t\tconst expected = expectedCounts.get(candidate.name) ?? 0;\n\t\t\tconst chosen = chosenCounts.get(candidate.name) ?? 0;\n\t\t\treturn { agent: candidate.name, expected, chosen, recall: expected > 0 ? chosen / expected : 0 };\n\t\t})\n\t\t.sort((a, b) => a.recall - b.recall);\n\n\treturn {\n\t\tcorpusHash: outcome.record.corpusHash,\n\t\tagents: candidates.map((c) => c.name),\n\t\tcaseCount: outcome.record.caseCount,\n\t\taccuracy: delegated > 0 ? delegatedCorrect / delegated : undefined,\n\t\tinlineAccuracy: inline > 0 ? inlineCorrect / inline : undefined,\n\t\tconfusion: [...confusion.values()].sort((a, b) => b.count - a.count),\n\t\tperAgent,\n\t};\n}\n\nexport type AgentSelectionOutcome =\n\t| { status: \"not-run\"; reason: string }\n\t| { status: \"ran\"; report: AgentSelectionReport; outcome: Extract<TriggerEvalOutcome, { status: \"ran\" }> };\n\n/** Score the roster against a gold set. Never throws; a missing model is `not-run`. */\nexport async function runAgentSelectionEval(\n\tcandidates: readonly TriggerCandidate[],\n\tcases: readonly TriggerCase[] | undefined,\n\tjudge: TriggerJudge | undefined,\n): Promise<AgentSelectionOutcome> {\n\tif (cases && cases.length > 0) {\n\t\tconst problems = validateAgentCases(candidates, cases);\n\t\tif (problems.length > 0) return { status: \"not-run\", reason: `invalid gold set: ${problems.join(\"; \")}` };\n\t}\n\n\tconst outcome = await runTriggerEval(\"agent-selection\", candidates, cases, judge);\n\tif (outcome.status === \"not-run\") return outcome;\n\n\treturn {\n\t\tstatus: \"ran\",\n\t\treport: {\n\t\t\t...summarizeAgentSelection(outcome, candidates),\n\t\t\t// The shared harness hashes its own way; restate it over exactly what\n\t\t\t// this eval judged so two reports are comparable on their own terms.\n\t\t\tcorpusHash: hashCorpus(candidates, cases ?? []),\n\t\t},\n\t\toutcome,\n\t};\n}\n\n/** Human-readable report for the CLI. */\nexport function formatAgentSelectionReport(report: AgentSelectionReport): string {\n\tconst pct = (v: number | undefined) => (v === undefined ? \"n/a\" : `${Math.round(v * 100)}%`);\n\tconst lines = [\n\t\t`corpus ${report.corpusHash} - ${report.caseCount} case(s) over ${report.agents.length} agent(s): ${report.agents.join(\", \")}`,\n\t\t`delegation accuracy ${pct(report.accuracy)} inline accuracy ${pct(report.inlineAccuracy)}`,\n\t\t\"\",\n\t\t\"per agent (lowest recall first):\",\n\t];\n\tfor (const row of report.perAgent) {\n\t\tlines.push(` ${row.agent.padEnd(18)} ${row.chosen}/${row.expected} chosen (${pct(row.recall)})`);\n\t}\n\tif (report.confusion.length > 0) {\n\t\tlines.push(\"\", \"confusions (expected then actual):\");\n\t\tfor (const entry of report.confusion) {\n\t\t\tlines.push(` ${entry.expected} => ${entry.actual} x${entry.count}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n"]}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does the agent roster steer a dispatch to the right agent?
|
|
3
|
+
*
|
|
4
|
+
* The `Task` tool asks the model to pick a `subagent_type` from
|
|
5
|
+
* `<available_agents>`, and that block contains nothing but each agent's
|
|
6
|
+
* summarized description. So agent selection is the same question G4 asks about
|
|
7
|
+
* skills — "given these descriptions and this situation, which one fires?" —
|
|
8
|
+
* and it reuses the same harness rather than growing a second one.
|
|
9
|
+
*
|
|
10
|
+
* What this exists to decide: `plan` and `explore` ship with the same tools, the
|
|
11
|
+
* same isolation and the same `background` flag, differing only in model tier
|
|
12
|
+
* and output contract — and `complexity` on the Task tool already expresses the
|
|
13
|
+
* tier. Whether they are two agents or one is a question about whether the model
|
|
14
|
+
* can actually tell them apart from their descriptions, which is measurable and
|
|
15
|
+
* was previously being argued from intuition.
|
|
16
|
+
*
|
|
17
|
+
* The candidates use `summarizeAgentDescription`, not the raw frontmatter: the
|
|
18
|
+
* summary is what the system prompt actually emits, and evaluating the full
|
|
19
|
+
* description would score text the model never sees.
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
23
|
+
import { loadAgentRegistry, summarizeAgentDescription } from "./agent-registry.js";
|
|
24
|
+
import { runTriggerEval, } from "./extensions/plugins/trigger-eval.js";
|
|
25
|
+
/**
|
|
26
|
+
* The built-in agents as judge candidates, described exactly as the system
|
|
27
|
+
* prompt describes them.
|
|
28
|
+
*
|
|
29
|
+
* `own: true` for all of them: unlike a plugin eval there is no foreign roster
|
|
30
|
+
* to discriminate against, so every case is scored against the same closed set.
|
|
31
|
+
* The `expect: null` cases carry the discriminative half instead — they ask
|
|
32
|
+
* whether the model declines to delegate work it should keep.
|
|
33
|
+
*/
|
|
34
|
+
export function agentCandidates(cwd = process.cwd()) {
|
|
35
|
+
const registry = loadAgentRegistry({ cwd, includeBuiltins: true, includeClaude: false });
|
|
36
|
+
return registry
|
|
37
|
+
.list()
|
|
38
|
+
.map((agent) => ({
|
|
39
|
+
name: agent.name,
|
|
40
|
+
description: summarizeAgentDescription(agent.description ?? ""),
|
|
41
|
+
own: true,
|
|
42
|
+
}))
|
|
43
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
44
|
+
}
|
|
45
|
+
/** Load a gold set: `{ "cases": [{ "prompt": "...", "expect": "explore" | null }] }`. */
|
|
46
|
+
export function loadAgentCases(file) {
|
|
47
|
+
if (!existsSync(file))
|
|
48
|
+
return undefined;
|
|
49
|
+
try {
|
|
50
|
+
const raw = JSON.parse(readFileSync(file, "utf-8"));
|
|
51
|
+
if (!Array.isArray(raw.cases))
|
|
52
|
+
return undefined;
|
|
53
|
+
const cases = raw.cases.filter((c) => !!c &&
|
|
54
|
+
typeof c === "object" &&
|
|
55
|
+
typeof c.prompt === "string" &&
|
|
56
|
+
c.prompt.trim().length > 0 &&
|
|
57
|
+
(c.expect === null || typeof c.expect === "string"));
|
|
58
|
+
return cases.length > 0 ? cases : undefined;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Every expected agent in the gold set must exist in the roster.
|
|
66
|
+
*
|
|
67
|
+
* A typo'd or removed agent name would otherwise score as a permanent miss and
|
|
68
|
+
* read as a description problem, which is the most expensive way to be wrong
|
|
69
|
+
* about an eval.
|
|
70
|
+
*/
|
|
71
|
+
export function validateAgentCases(candidates, cases) {
|
|
72
|
+
const known = new Set(candidates.map((c) => c.name));
|
|
73
|
+
const problems = [];
|
|
74
|
+
for (const [i, testCase] of cases.entries()) {
|
|
75
|
+
if (testCase.expect !== null && !known.has(testCase.expect)) {
|
|
76
|
+
problems.push(`case ${i} expects "${testCase.expect}", which is not in the roster`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return problems;
|
|
80
|
+
}
|
|
81
|
+
function hashCorpus(candidates, cases) {
|
|
82
|
+
const h = createHash("sha256");
|
|
83
|
+
for (const c of candidates)
|
|
84
|
+
h.update(`${c.name} ${c.description} `);
|
|
85
|
+
for (const c of cases)
|
|
86
|
+
h.update(`${c.prompt} ${c.expect ?? ""} `);
|
|
87
|
+
return h.digest("hex").slice(0, 16);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Turn a scored run into the report that answers the design question.
|
|
91
|
+
*
|
|
92
|
+
* `runTriggerEval`'s recall/specificity are the right numbers for a plugin
|
|
93
|
+
* defending itself against a foreign roster. Here the roster is closed, so the
|
|
94
|
+
* useful shape is a confusion matrix: which agent loses to which, and how often.
|
|
95
|
+
* "explore and plan are interchangeable" is a claim about one cell.
|
|
96
|
+
*/
|
|
97
|
+
export function summarizeAgentSelection(outcome, candidates) {
|
|
98
|
+
const confusion = new Map();
|
|
99
|
+
const expectedCounts = new Map();
|
|
100
|
+
const chosenCounts = new Map();
|
|
101
|
+
let delegated = 0;
|
|
102
|
+
let delegatedCorrect = 0;
|
|
103
|
+
let inline = 0;
|
|
104
|
+
let inlineCorrect = 0;
|
|
105
|
+
for (const result of outcome.record.results) {
|
|
106
|
+
if (result.expected === null) {
|
|
107
|
+
inline++;
|
|
108
|
+
if (result.actual === null)
|
|
109
|
+
inlineCorrect++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
delegated++;
|
|
113
|
+
expectedCounts.set(result.expected, (expectedCounts.get(result.expected) ?? 0) + 1);
|
|
114
|
+
if (result.correct) {
|
|
115
|
+
delegatedCorrect++;
|
|
116
|
+
chosenCounts.set(result.expected, (chosenCounts.get(result.expected) ?? 0) + 1);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// "(none)" is a real outcome, not a missing value: declining to delegate
|
|
120
|
+
// work that should have been delegated is a different failure from picking
|
|
121
|
+
// the wrong agent, and collapsing them would hide which one is happening.
|
|
122
|
+
const actual = result.actual ?? "(none)";
|
|
123
|
+
const key = `${result.expected} ${actual}`;
|
|
124
|
+
const entry = confusion.get(key) ?? { expected: result.expected, actual, count: 0 };
|
|
125
|
+
entry.count++;
|
|
126
|
+
confusion.set(key, entry);
|
|
127
|
+
}
|
|
128
|
+
const perAgent = candidates
|
|
129
|
+
.map((candidate) => {
|
|
130
|
+
const expected = expectedCounts.get(candidate.name) ?? 0;
|
|
131
|
+
const chosen = chosenCounts.get(candidate.name) ?? 0;
|
|
132
|
+
return { agent: candidate.name, expected, chosen, recall: expected > 0 ? chosen / expected : 0 };
|
|
133
|
+
})
|
|
134
|
+
.sort((a, b) => a.recall - b.recall);
|
|
135
|
+
return {
|
|
136
|
+
corpusHash: outcome.record.corpusHash,
|
|
137
|
+
agents: candidates.map((c) => c.name),
|
|
138
|
+
caseCount: outcome.record.caseCount,
|
|
139
|
+
accuracy: delegated > 0 ? delegatedCorrect / delegated : undefined,
|
|
140
|
+
inlineAccuracy: inline > 0 ? inlineCorrect / inline : undefined,
|
|
141
|
+
confusion: [...confusion.values()].sort((a, b) => b.count - a.count),
|
|
142
|
+
perAgent,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** Score the roster against a gold set. Never throws; a missing model is `not-run`. */
|
|
146
|
+
export async function runAgentSelectionEval(candidates, cases, judge) {
|
|
147
|
+
if (cases && cases.length > 0) {
|
|
148
|
+
const problems = validateAgentCases(candidates, cases);
|
|
149
|
+
if (problems.length > 0)
|
|
150
|
+
return { status: "not-run", reason: `invalid gold set: ${problems.join("; ")}` };
|
|
151
|
+
}
|
|
152
|
+
const outcome = await runTriggerEval("agent-selection", candidates, cases, judge);
|
|
153
|
+
if (outcome.status === "not-run")
|
|
154
|
+
return outcome;
|
|
155
|
+
return {
|
|
156
|
+
status: "ran",
|
|
157
|
+
report: {
|
|
158
|
+
...summarizeAgentSelection(outcome, candidates),
|
|
159
|
+
// The shared harness hashes its own way; restate it over exactly what
|
|
160
|
+
// this eval judged so two reports are comparable on their own terms.
|
|
161
|
+
corpusHash: hashCorpus(candidates, cases ?? []),
|
|
162
|
+
},
|
|
163
|
+
outcome,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Human-readable report for the CLI. */
|
|
167
|
+
export function formatAgentSelectionReport(report) {
|
|
168
|
+
const pct = (v) => (v === undefined ? "n/a" : `${Math.round(v * 100)}%`);
|
|
169
|
+
const lines = [
|
|
170
|
+
`corpus ${report.corpusHash} - ${report.caseCount} case(s) over ${report.agents.length} agent(s): ${report.agents.join(", ")}`,
|
|
171
|
+
`delegation accuracy ${pct(report.accuracy)} inline accuracy ${pct(report.inlineAccuracy)}`,
|
|
172
|
+
"",
|
|
173
|
+
"per agent (lowest recall first):",
|
|
174
|
+
];
|
|
175
|
+
for (const row of report.perAgent) {
|
|
176
|
+
lines.push(` ${row.agent.padEnd(18)} ${row.chosen}/${row.expected} chosen (${pct(row.recall)})`);
|
|
177
|
+
}
|
|
178
|
+
if (report.confusion.length > 0) {
|
|
179
|
+
lines.push("", "confusions (expected then actual):");
|
|
180
|
+
for (const entry of report.confusion) {
|
|
181
|
+
lines.push(` ${entry.expected} => ${entry.actual} x${entry.count}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return lines.join("\n");
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=agent-selection-eval.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-selection-eval.js","sourceRoot":"","sources":["../../src/core/agent-selection-eval.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Does the agent roster steer a dispatch to the right agent?\n *\n * The `Task` tool asks the model to pick a `subagent_type` from\n * `<available_agents>`, and that block contains nothing but each agent's\n * summarized description. So agent selection is the same question G4 asks about\n * skills — \"given these descriptions and this situation, which one fires?\" —\n * and it reuses the same harness rather than growing a second one.\n *\n * What this exists to decide: `plan` and `explore` ship with the same tools, the\n * same isolation and the same `background` flag, differing only in model tier\n * and output contract — and `complexity` on the Task tool already expresses the\n * tier. Whether they are two agents or one is a question about whether the model\n * can actually tell them apart from their descriptions, which is measurable and\n * was previously being argued from intuition.\n *\n * The candidates use `summarizeAgentDescription`, not the raw frontmatter: the\n * summary is what the system prompt actually emits, and evaluating the full\n * description would score text the model never sees.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { loadAgentRegistry, summarizeAgentDescription } from \"./agent-registry.js\";\nimport {\n\trunTriggerEval,\n\ttype TriggerCandidate,\n\ttype TriggerCase,\n\ttype TriggerEvalOutcome,\n\ttype TriggerJudge,\n} from \"./extensions/plugins/trigger-eval.js\";\n\n/** How often one agent was chosen where another was expected. */\nexport interface ConfusionEntry {\n\texpected: string;\n\tactual: string;\n\tcount: number;\n}\n\nexport interface AgentSelectionReport {\n\tcorpusHash: string;\n\tagents: string[];\n\tcaseCount: number;\n\t/** Cases where the expected agent was chosen, over cases expecting any agent. */\n\taccuracy?: number;\n\t/** Cases correctly left to the parent, over cases expecting no delegation. */\n\tinlineAccuracy?: number;\n\t/** Every wrong pick, most frequent first. The pairs here are the finding. */\n\tconfusion: ConfusionEntry[];\n\t/** Per-agent recall: chosen / expected. An agent nobody picks is dead weight. */\n\tperAgent: Array<{ agent: string; expected: number; chosen: number; recall: number }>;\n}\n\n/**\n * The built-in agents as judge candidates, described exactly as the system\n * prompt describes them.\n *\n * `own: true` for all of them: unlike a plugin eval there is no foreign roster\n * to discriminate against, so every case is scored against the same closed set.\n * The `expect: null` cases carry the discriminative half instead — they ask\n * whether the model declines to delegate work it should keep.\n */\nexport function agentCandidates(cwd: string = process.cwd()): TriggerCandidate[] {\n\tconst registry = loadAgentRegistry({ cwd, includeBuiltins: true, includeClaude: false });\n\treturn registry\n\t\t.list()\n\t\t.map((agent) => ({\n\t\t\tname: agent.name,\n\t\t\tdescription: summarizeAgentDescription(agent.description ?? \"\"),\n\t\t\town: true,\n\t\t}))\n\t\t.sort((a, b) => a.name.localeCompare(b.name));\n}\n\n/** Load a gold set: `{ \"cases\": [{ \"prompt\": \"...\", \"expect\": \"explore\" | null }] }`. */\nexport function loadAgentCases(file: string): TriggerCase[] | undefined {\n\tif (!existsSync(file)) return undefined;\n\ttry {\n\t\tconst raw = JSON.parse(readFileSync(file, \"utf-8\")) as { cases?: unknown };\n\t\tif (!Array.isArray(raw.cases)) return undefined;\n\t\tconst cases = raw.cases.filter(\n\t\t\t(c): c is TriggerCase =>\n\t\t\t\t!!c &&\n\t\t\t\ttypeof c === \"object\" &&\n\t\t\t\ttypeof (c as TriggerCase).prompt === \"string\" &&\n\t\t\t\t(c as TriggerCase).prompt.trim().length > 0 &&\n\t\t\t\t((c as TriggerCase).expect === null || typeof (c as TriggerCase).expect === \"string\"),\n\t\t);\n\t\treturn cases.length > 0 ? cases : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Every expected agent in the gold set must exist in the roster.\n *\n * A typo'd or removed agent name would otherwise score as a permanent miss and\n * read as a description problem, which is the most expensive way to be wrong\n * about an eval.\n */\nexport function validateAgentCases(candidates: readonly TriggerCandidate[], cases: readonly TriggerCase[]): string[] {\n\tconst known = new Set(candidates.map((c) => c.name));\n\tconst problems: string[] = [];\n\tfor (const [i, testCase] of cases.entries()) {\n\t\tif (testCase.expect !== null && !known.has(testCase.expect)) {\n\t\t\tproblems.push(`case ${i} expects \"${testCase.expect}\", which is not in the roster`);\n\t\t}\n\t}\n\treturn problems;\n}\n\nfunction hashCorpus(candidates: readonly TriggerCandidate[], cases: readonly TriggerCase[]): string {\n\tconst h = createHash(\"sha256\");\n\tfor (const c of candidates) h.update(`${c.name} ${c.description} `);\n\tfor (const c of cases) h.update(`${c.prompt} ${c.expect ?? \"\"} `);\n\treturn h.digest(\"hex\").slice(0, 16);\n}\n\n/**\n * Turn a scored run into the report that answers the design question.\n *\n * `runTriggerEval`'s recall/specificity are the right numbers for a plugin\n * defending itself against a foreign roster. Here the roster is closed, so the\n * useful shape is a confusion matrix: which agent loses to which, and how often.\n * \"explore and plan are interchangeable\" is a claim about one cell.\n */\nexport function summarizeAgentSelection(\n\toutcome: Extract<TriggerEvalOutcome, { status: \"ran\" }>,\n\tcandidates: readonly TriggerCandidate[],\n): AgentSelectionReport {\n\tconst confusion = new Map<string, ConfusionEntry>();\n\tconst expectedCounts = new Map<string, number>();\n\tconst chosenCounts = new Map<string, number>();\n\tlet delegated = 0;\n\tlet delegatedCorrect = 0;\n\tlet inline = 0;\n\tlet inlineCorrect = 0;\n\n\tfor (const result of outcome.record.results) {\n\t\tif (result.expected === null) {\n\t\t\tinline++;\n\t\t\tif (result.actual === null) inlineCorrect++;\n\t\t\tcontinue;\n\t\t}\n\t\tdelegated++;\n\t\texpectedCounts.set(result.expected, (expectedCounts.get(result.expected) ?? 0) + 1);\n\t\tif (result.correct) {\n\t\t\tdelegatedCorrect++;\n\t\t\tchosenCounts.set(result.expected, (chosenCounts.get(result.expected) ?? 0) + 1);\n\t\t\tcontinue;\n\t\t}\n\t\t// \"(none)\" is a real outcome, not a missing value: declining to delegate\n\t\t// work that should have been delegated is a different failure from picking\n\t\t// the wrong agent, and collapsing them would hide which one is happening.\n\t\tconst actual = result.actual ?? \"(none)\";\n\t\tconst key = `${result.expected} ${actual}`;\n\t\tconst entry = confusion.get(key) ?? { expected: result.expected, actual, count: 0 };\n\t\tentry.count++;\n\t\tconfusion.set(key, entry);\n\t}\n\n\tconst perAgent = candidates\n\t\t.map((candidate) => {\n\t\t\tconst expected = expectedCounts.get(candidate.name) ?? 0;\n\t\t\tconst chosen = chosenCounts.get(candidate.name) ?? 0;\n\t\t\treturn { agent: candidate.name, expected, chosen, recall: expected > 0 ? chosen / expected : 0 };\n\t\t})\n\t\t.sort((a, b) => a.recall - b.recall);\n\n\treturn {\n\t\tcorpusHash: outcome.record.corpusHash,\n\t\tagents: candidates.map((c) => c.name),\n\t\tcaseCount: outcome.record.caseCount,\n\t\taccuracy: delegated > 0 ? delegatedCorrect / delegated : undefined,\n\t\tinlineAccuracy: inline > 0 ? inlineCorrect / inline : undefined,\n\t\tconfusion: [...confusion.values()].sort((a, b) => b.count - a.count),\n\t\tperAgent,\n\t};\n}\n\nexport type AgentSelectionOutcome =\n\t| { status: \"not-run\"; reason: string }\n\t| { status: \"ran\"; report: AgentSelectionReport; outcome: Extract<TriggerEvalOutcome, { status: \"ran\" }> };\n\n/** Score the roster against a gold set. Never throws; a missing model is `not-run`. */\nexport async function runAgentSelectionEval(\n\tcandidates: readonly TriggerCandidate[],\n\tcases: readonly TriggerCase[] | undefined,\n\tjudge: TriggerJudge | undefined,\n): Promise<AgentSelectionOutcome> {\n\tif (cases && cases.length > 0) {\n\t\tconst problems = validateAgentCases(candidates, cases);\n\t\tif (problems.length > 0) return { status: \"not-run\", reason: `invalid gold set: ${problems.join(\"; \")}` };\n\t}\n\n\tconst outcome = await runTriggerEval(\"agent-selection\", candidates, cases, judge);\n\tif (outcome.status === \"not-run\") return outcome;\n\n\treturn {\n\t\tstatus: \"ran\",\n\t\treport: {\n\t\t\t...summarizeAgentSelection(outcome, candidates),\n\t\t\t// The shared harness hashes its own way; restate it over exactly what\n\t\t\t// this eval judged so two reports are comparable on their own terms.\n\t\t\tcorpusHash: hashCorpus(candidates, cases ?? []),\n\t\t},\n\t\toutcome,\n\t};\n}\n\n/** Human-readable report for the CLI. */\nexport function formatAgentSelectionReport(report: AgentSelectionReport): string {\n\tconst pct = (v: number | undefined) => (v === undefined ? \"n/a\" : `${Math.round(v * 100)}%`);\n\tconst lines = [\n\t\t`corpus ${report.corpusHash} - ${report.caseCount} case(s) over ${report.agents.length} agent(s): ${report.agents.join(\", \")}`,\n\t\t`delegation accuracy ${pct(report.accuracy)} inline accuracy ${pct(report.inlineAccuracy)}`,\n\t\t\"\",\n\t\t\"per agent (lowest recall first):\",\n\t];\n\tfor (const row of report.perAgent) {\n\t\tlines.push(` ${row.agent.padEnd(18)} ${row.chosen}/${row.expected} chosen (${pct(row.recall)})`);\n\t}\n\tif (report.confusion.length > 0) {\n\t\tlines.push(\"\", \"confusions (expected then actual):\");\n\t\tfor (const entry of report.confusion) {\n\t\t\tlines.push(` ${entry.expected} => ${entry.actual} x${entry.count}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n"]}
|
|
@@ -26,6 +26,13 @@ export type CreateAgentSessionRuntimeFactory = (options: {
|
|
|
26
26
|
sessionManager: SessionManager;
|
|
27
27
|
sessionStartEvent?: SessionStartEvent;
|
|
28
28
|
}) => Promise<CreateAgentSessionRuntimeResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Thrown when `/cd` is pointed at something that is not a usable directory.
|
|
31
|
+
*/
|
|
32
|
+
export declare class ChangeDirectoryError extends Error {
|
|
33
|
+
readonly path: string;
|
|
34
|
+
constructor(message: string, path: string);
|
|
35
|
+
}
|
|
29
36
|
/**
|
|
30
37
|
* Thrown when /import references a JSONL file path that does not exist.
|
|
31
38
|
*/
|
|
@@ -82,6 +89,27 @@ export declare class AgentSessionRuntime {
|
|
|
82
89
|
}): Promise<{
|
|
83
90
|
cancelled: boolean;
|
|
84
91
|
}>;
|
|
92
|
+
/**
|
|
93
|
+
* Move the whole runtime to another directory without leaving the process.
|
|
94
|
+
*
|
|
95
|
+
* Everything cwd-bound is rebuilt for the new root — tools, context files
|
|
96
|
+
* (AGENTS.md and friends), project settings, extensions, skills, agents, the
|
|
97
|
+
* MCP server set — which is exactly why this cannot just reassign a string:
|
|
98
|
+
* those services are constructed per cwd and a half-moved runtime would run
|
|
99
|
+
* the new repo's code against the old repo's rules.
|
|
100
|
+
*
|
|
101
|
+
* Sessions are stored per project, so the move starts a fresh session in the
|
|
102
|
+
* target directory rather than dragging the current transcript across. The
|
|
103
|
+
* old session is already persisted; `/resume` in the old directory reopens
|
|
104
|
+
* it. A session directory the user pinned explicitly (`--session-dir`, the
|
|
105
|
+
* env var, or the setting) is *not* cwd-derived and is carried over as-is.
|
|
106
|
+
*
|
|
107
|
+
* @throws {ChangeDirectoryError} When the target is missing or not a directory.
|
|
108
|
+
*/
|
|
109
|
+
changeDirectory(targetCwd: string): Promise<{
|
|
110
|
+
cancelled: boolean;
|
|
111
|
+
cwd: string;
|
|
112
|
+
}>;
|
|
85
113
|
fork(entryId: string, options?: {
|
|
86
114
|
position?: "before" | "at";
|
|
87
115
|
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
|