jeo-code 0.5.10 → 0.5.13
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 +287 -0
- package/README.ja.md +3 -3
- package/README.ko.md +3 -3
- package/README.md +3 -3
- package/README.zh.md +3 -3
- package/package.json +2 -1
- package/src/agent/engine.ts +27 -3
- package/src/agent/json.ts +105 -25
- package/src/agent/loop.ts +2 -0
- package/src/agent/tool-schemas.ts +132 -0
- package/src/agent/tools.ts +8 -2
- package/src/ai/model-manager.ts +1 -0
- package/src/ai/providers/anthropic.ts +60 -3
- package/src/ai/providers/antigravity.ts +31 -1
- package/src/ai/providers/openai-responses.ts +55 -0
- package/src/ai/providers/openai.ts +46 -3
- package/src/ai/types.ts +19 -0
- package/src/commands/launch.ts +53 -6
- package/src/commands/whats-new.ts +62 -0
- package/src/skills/catalog.ts +8 -0
- package/src/tui/app.ts +28 -9
- package/src/util/whats-new.ts +272 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **jeo-code** are documented here.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
|
|
8
|
+
|
|
9
|
+
## [0.5.13] - 2026-06-15
|
|
10
|
+
_Workflow `/` commands actually run — `/deep-interview`, `/team`, `/ultragoal`, `/ralplan` dispatch by name._
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Workflow skills listed in the `/` menu didn't run: a bare `/name` only resolved when the skill's SKILL.md happened to self-reference that slash token (so `/ralplan` worked by luck while `/deep-interview` and `/ultragoal` returned "Unknown command"). `parseSkillInvocation` now resolves a plain `/word` against skill NAMES (exact, then unique prefix) — the same entrypoint as `$name` and `/skill:name` (gjc parity) — so `/deep-interview`, `/ralplan`, `/team`, `/ultragoal` (and any loaded skill) dispatch from the slash menu. Dotted (`/speckit.plan`) and nested (`/a/b`) tokens keep their alias/file-path resolution untouched, and built-in commands still take precedence.
|
|
14
|
+
- The four bundled workflows are now always listed in the `/` menu as `/deep-interview`, `/ralplan`, `/team`, `/ultragoal`, even when their SKILL.md declares no slash alias, so they are discoverable as well as runnable.
|
|
15
|
+
|
|
16
|
+
## [0.5.12] - 2026-06-15
|
|
17
|
+
_Yellow status animation while a process runs, and elapsed `(Nms)` on every completed tool card._
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- The live status animation (spinner + activity gradient) turns amber/yellow while a tool/process is executing — "the agent is running/verifying a process" now reads at a glance, distinct from the cool thinking gradient (gjc `theme.fg("warning")` parity).
|
|
21
|
+
- Every completed tool card and light-tool ledger line now shows how long the tool ran, dim after the ✓/✗ glyph — `✓ Bash · (8ms)`, `✓ Read x.ts · (3ms)` (gjc duration-detail parity; sub-second as `Nms`, else `N.Ns`/`Nm Ns`).
|
|
22
|
+
|
|
23
|
+
## [0.5.11] - 2026-06-15
|
|
24
|
+
_Backspace on an empty prompt line no longer quits jeo._
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- Pressing Backspace with an empty input line could terminate the process on some Bun builds: an empty-line Backspace is a no-op edit, but those builds turn it into a spurious readline `close`, which the REPL treats as a hard exit. The multi-line input filter now swallows a standalone Backspace (DEL `0x7f` / BS `0x08`) when the line buffer is already empty, so the byte never reaches readline and the close can't fire. Backspace with text in the buffer still deletes normally, and Ctrl-C / paste / Enter are untouched.
|
|
28
|
+
|
|
29
|
+
## [0.5.10] - 2026-06-15
|
|
30
|
+
_`/resume` transcript no longer dumps raw JSON for batched tool calls._
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- Resuming a session that contains a BATCHED tool step (`{reasoning, tools:[…]}`) printed the raw JSON object into the transcript instead of a readable history. `formatTranscript` only recognized the single-call `{tool,arguments}` shape; the batch shape parsed to no `tool` field, fell through to the prose branch, and dumped the JSON. It now renders one compact `✔/✗ <tool> — <result>` ledger line per batched call (verdicts parsed in call order from the combined `Tool [x] result` message), matching how single calls already render. `/resume` and `/history` both go through this path, so both are fixed.
|
|
34
|
+
|
|
35
|
+
## [0.5.9] - 2026-06-15
|
|
36
|
+
_Bounded per-frame wrap for the live thinking/tool-output blocks — re-render cost no longer grows with stream length._
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- The live "thinking" and tool-output tail blocks only ever DISPLAY their last 6/8 wrapped rows, but they accumulate the whole step's text and were re-wrapping the FULL string on every 120ms repaint — so per-frame CPU and GC churn grew linearly with how much had streamed (a long reasoning trace or a chatty tool can reach hundreds of KB). The wrap input is now bounded to a fixed 16 KiB trailing window (`tailForWrap`), capping per-frame work at O(window) regardless of total size. The visible tail is byte-identical for normal multi-line content; an unbroken multi-hundred-KB blob still shows its genuine end. No memory leak existed (every live buffer — StreamRegion, ToolList, activityLog, forgeSummaries — is already ring-capped and the per-turn TUI instance is GC'd); this removes the one remaining O(stream-length) hot path in the repaint loop.
|
|
40
|
+
|
|
41
|
+
## [0.5.8] - 2026-06-15
|
|
42
|
+
_Native Opik observability for the turn loop (opt-in `JEO_OPIK`, pure-TS no-op when unset) + autopilot convergence tracking._
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- Native Opik observability for the agent turn loop (opt-in via `JEO_OPIK`): each turn becomes one Opik trace, each step/tool a span, with token usage and the `completed` / `verified` / `efficiency` eval scores attached. Implemented in pure TypeScript over `fetch` — no Python, no `opik` npm package — per jeo's zero-native-dependency constraint. Hard invariants: an unset `JEO_OPIK` is a complete no-op (zero Opik HTTP calls); no tracer error ever escapes an events callback; the API key travels only in the `Authorization` header; and engine output is identical regardless of tracing outcome.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
- Autopilot convergence tracking: a "keep" (a provable min/max improvement or a gate pass) now counts as forward progress, and anything else extends the no-progress streak toward the convergence cap.
|
|
49
|
+
|
|
50
|
+
## [0.5.7] - 2026-06-15
|
|
51
|
+
_`/model` picker is default-only, `/clear` resets to the initial screen, ESC clears the input box, and a launch process-listener leak is fixed._
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
- The `/model` model-action picker now offers ONLY the DEFAULT section: "Set model only" plus the default thinking levels. The per-role rows ("Set as EXECUTOR/PLANNER/ARCHITECT/CRITIC") and the "Apply OpenAI Codex role preset" entry are gone — role model and thinking are configured exclusively in `/agents` (and `/agents edit`). The default heading now points there (`roles → /agents`). This completes the 0.5.6 split: `/model` owns the default, `/agents` owns the roles. Dead helpers (`orderedModelRoles`, `applyOpenAiCodexRolePreset`, `CORE_MODEL_ACTION_ROLE_ORDER`) were removed with the menus.
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
- `/clear` now returns to the initial screen (fresh session view) and ESC clears the input box, matching the expected reset gestures.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
- Launch no longer leaks process listeners: the prompt-scoped stdin `data`/`keypress` and stdout `resize` handlers are now tracked and drained on every exit path, so repeated `launch()` (e.g. the test harness) no longer accumulates listeners past Node's 10-listener default (`MaxListenersExceededWarning` + a real leak).
|
|
61
|
+
|
|
62
|
+
## [0.5.6] - 2026-06-15
|
|
63
|
+
_`/model` sets only the default thinking; per-role reasoning moved to `/agents`._
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
- Thinking ownership split: `/model` now configures reasoning for the DEFAULT agent only, and per-role (subagent) thinking is owned by `/agents`. In the model-action picker the role rows offer just "Set model only" (the per-level thinking rows are gone), `/model subagent <role> thinking <level>` redirects to `/agents` instead of saving, and picking a role's model no longer prompts for a reasoning level. Set role reasoning via `/agents <role> thinking <level|inherit>` or the `/agents edit` picker. `/model thinking <level>` still sets the default, so nothing the default knob did is lost.
|
|
67
|
+
|
|
68
|
+
## [0.5.5] - 2026-06-15
|
|
69
|
+
_Full multi-line visibility — the input box scrolls to the caret and the submitted card shows every line._
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
- Full multi-line visibility for the multi-line input added in 0.5.4: the input box now scrolls so the caret row always stays in view as you move through a long draft (`…` markers flag rows hidden above/below, so no line is unreachable), and a submitted multi-line query renders ALL of its wrapped lines in the user card — the card lives in scrollback rather than the bounded live frame, so nothing is truncated.
|
|
73
|
+
|
|
74
|
+
## [0.5.4] - 2026-06-15
|
|
75
|
+
_Reliable multi-line input is ON by default — a paste fills the box and submits as one message._
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
- Multi-line input is now enabled by default on any interactive TTY (no flag needed): a bracketed paste arrives as ONE buffer, fills the input box, and submits as a single message instead of being split into one message per line. The prompt's stdin is routed through a filter that rewrites line breaks to a private-use sentinel before `node:readline` sees them, avoiding the per-line submit/race. The lone-`\n` Shift+Enter rule stays opt-in via `JEO_MULTILINE=1` (it needs ghostty's `keybind = shift+enter=text:\n` and could misfire on terminals that send LF for Enter), and `JEO_NO_MULTILINE=1` fully disables the filter (reads stdin directly, exactly as before).
|
|
79
|
+
|
|
80
|
+
## [0.5.3] - 2026-06-15
|
|
81
|
+
_`$` chains multiple skills in one line (all run, in order), plus multi-line prompt input — paste-merge and gated Shift+Enter._
|
|
82
|
+
|
|
83
|
+
### Added
|
|
84
|
+
- Multi-skill `$` chaining: a leading run of `$skill` tokens now invokes every resolved skill in order, sharing the trailing text as one intent — `$ralplan $team build the auth flow` runs ralplan then team, both with intent "build the auth flow". A lone `$skill` is just a chain of one, so existing single invocations are unchanged. Prefixes still resolve (`$te $ultra …`), `$UPPERCASE` env-var tokens end the chain and pass through to the model, and unknown tokens are collected so several typos report together (`No skills: $nope, $bad.`) instead of one at a time. Applies to both the interactive prompt and one-shot `-p`/`/skill:` runs.
|
|
85
|
+
- Multi-line paste merges into ONE message: a pasted block with embedded newlines is submitted as a single prompt instead of being split into one message per line.
|
|
86
|
+
- Shift+Enter multi-line input (opt-in via `JEO_MULTILINE=1`): Shift+Enter inserts a real line break instead of submitting. Terminal-specific Shift+Enter encodings (ghostty legacy `\x1b[27;2;13~`, kitty `\x1b[13;2u`, and a lone `\n`) are rewritten to a private-use sentinel before `node:readline` sees them, so it works through tmux (extended-keys off) without mangling; default OFF reads stdin exactly as before.
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
- The one-shot skill path was refactored to a shared `runOneSkillShot` runner so the single-invocation and chain paths execute identically (bundle workflow → engine; regular skill → agent turn).
|
|
90
|
+
|
|
91
|
+
## [0.5.2] - 2026-06-14
|
|
92
|
+
_`$skill` prompt invocation with prefix/fuzzy suggestions, and a per-session input-box hue (amber in cmd-mode)._
|
|
93
|
+
|
|
94
|
+
### Added
|
|
95
|
+
- `$skill` prompt invocation: typing `$<name>` runs a bundled/loaded skill directly. A unique prefix resolves precisely (`$te` → `$team`); an ambiguous prefix lists candidates (`Ambiguous skill '$te'. Did you mean: $team, …`); and an unknown `$word` lists the available skills (prefix-first, then fuzzy-subsequence) instead of silently sending the typo to the model. `$UPPERCASE` env-var-style tokens (e.g. `$HOME`) pass through untouched.
|
|
96
|
+
- Per-session input-box border hue: each newly opened jeo session gets a distinct border color so several concurrent sessions are tellable apart at a glance, and cmd-mode (`!`) overrides it with a caution amber so entering the shell escape is unmistakable.
|
|
97
|
+
|
|
98
|
+
## [0.5.1] - 2026-06-14
|
|
99
|
+
_cmd-mode `!<command>` shell escape — run a shell command without engaging the agent._
|
|
100
|
+
|
|
101
|
+
### Added
|
|
102
|
+
- gjc-parity cmd-mode shell escape: typing `!<command>` at the prompt runs the command directly in the shell and prints its output WITHOUT engaging the agent or touching conversation history (a REPL-style shell escape). Because the user is explicitly driving their own shell, the deep-interview mutation guard — which gates the AGENT's tools — does not apply; bare `!` prints a short usage hint.
|
|
103
|
+
|
|
104
|
+
## [0.5.0] - 2026-06-14
|
|
105
|
+
_Performance: workspace-scan, workflow-state, and DNA-Claw HUD caches; plus a credential-safety fix that never wipes OAuth over an invalid config._
|
|
106
|
+
|
|
107
|
+
### Fixed
|
|
108
|
+
- A schema-invalid `config.json` no longer wipes stored OAuth / provider credentials: the invalid config falls back to defaults while the credential block is preserved, so a bad config edit can't silently sign you out of every provider.
|
|
109
|
+
|
|
110
|
+
### Changed
|
|
111
|
+
- Workspace-tree scan is now LRU-cached per resolved cwd (cap 32), so a long session that touches many trees (subagents, worktrees, cross-tree `/view`) neither re-scans the same directory nor grows the cache without bound.
|
|
112
|
+
- Per-skill workflow-state JSON reads are mtime + size-validated cached. The mutation guard reads the workflow lock before every mutating tool, so re-reading and re-parsing each call was wasteful; the cache stays cross-process-safe — a write from another process bumps mtime/size and invalidates it, so an active interview lock can never be missed.
|
|
113
|
+
- DNA Claw HUD frames are memoized (LRU-capped) keyed on every input that affects output (grand/unicode/cols/color/level/phase/frame). The live HUD cycles a fixed ~60-frame set at ~120ms, so the 2nd+ cycle is now O(1) lookups instead of recomputing per-line ANSI gradients — lower steady-state HUD CPU.
|
|
114
|
+
|
|
115
|
+
## [0.4.9] - 2026-06-14
|
|
116
|
+
_Live-frame width-clamp (content-sized height) replaces the constant-height approach, typed text shows during a running turn, and a docs/AGENTS refresh._
|
|
117
|
+
|
|
118
|
+
### Fixed
|
|
119
|
+
- Typed text now appears in the input box DURING a running turn — keystrokes entered mid-turn render live in the prompt box (and as a pending steering card) instead of staying invisible until the turn ends.
|
|
120
|
+
- Live-frame anchor drift: 0.4.8's constant-height padding could grow the reserve and drift the cursor anchor by one row, reintroducing a duplicate model bar mid-turn. The live frame is content-sized again, and every rendered line is width-clamped to the terminal width so a long line (e.g. the model bar with a deep cwd) can't soft-wrap into a second physical row and desync the differential renderer's 1-line = 1-row accounting — keeping completed cards visible in scrollback above the live frame.
|
|
121
|
+
- Renderer `reset()` → `insertAbove()` ordering now erase-line-clears the old frame rows the inserted block did not cover (`occupied = max(prev, coverRows)`), closing the remaining duplicate-model-bar / orphaned-border case.
|
|
122
|
+
|
|
123
|
+
### Changed
|
|
124
|
+
- Regenerated every directory `AGENTS.md` guide and pruned stale working docs (the rolling improvements log, promo assets, and one-off review/analysis notes) so the tracked docs reflect the current tree.
|
|
125
|
+
|
|
126
|
+
## [0.4.8] - 2026-06-14
|
|
127
|
+
_Live-frame stability: constant-height live turn, renderer self-heal off-by-one fix, and frame-safe child-stdout sanitizing — no more duplicate model bar or torn escapes._
|
|
128
|
+
|
|
129
|
+
### Fixed
|
|
130
|
+
- The live turn now renders at a CONSTANT height: the in-flight tool-output / thinking block reserves a fixed row count (bottom-anchored, blank-padded at the top) and the whole frame is padded to exactly the terminal's rows. Streaming stdout growth no longer thrashes the frame height every 100ms — the height change that desynced the differential renderer and duplicated the model bar is gone.
|
|
131
|
+
- Renderer self-heal reset now remembers how many rows are physically on screen (`coverRows`), so a repaint of a SHORTER frame erase-line-clears the rows it no longer covers — fixing the persistent off-by-one that left a duplicate model bar / orphaned borders after a reset.
|
|
132
|
+
- Raw child stdout is sanitized before entering the live frame (`sanitizeForFrame`): carriage returns, erase-line/cursor-move escapes, OSC sequences, and incomplete trailing escapes are stripped (SGR color kept) so a streaming `bun test`'s `\r\x1b[2K` progress lines can no longer tear the renderer's own `\x1b[2K` (printing a literal "2K") or hijack the cursor.
|
|
133
|
+
|
|
134
|
+
## [0.4.7] - 2026-06-14
|
|
135
|
+
_Detached subagents + `subagent` control tool, live shaded in-flight output, registry-driven providers, fuller `read` budget, styled italics in the final report, and `gjc` retired._
|
|
136
|
+
|
|
137
|
+
### Added
|
|
138
|
+
- Detached subagents: `task {detached:true}` launches a background subagent and returns immediately; a new `subagent` control tool lists, inspects, awaits (optionally bounded), and cancels them (gjc parity, in-process turn-scoped registry — `cancelAll()` on teardown prevents background-promise leaks).
|
|
139
|
+
- Live shaded in-flight output: the running tool's stdout (bash) and native thinking deltas stream as a DIMMED bounded block above the status line, then flush UN-dimmed into scrollback once the model commits — gjc's "shaded until complete" effect.
|
|
140
|
+
- Update-check disk cache (`~/.jeo`): the update banner is instant from cache with a background refresh, and clears itself after an interim upgrade.
|
|
141
|
+
|
|
142
|
+
### Changed
|
|
143
|
+
- Provider registry bootstrap: `register-providers.ts` registers every built-in adapter; `model-manager` resolves adapters through the registry alone and no longer imports or names concrete providers — new built-ins register in one place.
|
|
144
|
+
- `read` default (no `lineRange`) now fills the model-visible output budget with WHOLE lines instead of a fixed 500-line cap, so a single read returns more of a file and forces far less needless pagination.
|
|
145
|
+
- Tool-output handling (model-visible budget, both-ends truncation, recoverable artifact spilling) extracted from `engine.ts` into `tool-output.ts`; `engine.ts` re-exports for compatibility.
|
|
146
|
+
- Final-report markdown: single `*italic*` / `_italic_` is now styled (list-bullet- and snake_case-safe), and a heading that follows content gets one blank line of breathing room above it.
|
|
147
|
+
- Mid-turn steering query now renders as a `user` card in scrollback; per-theme `userCard` palette and todo-card rendering refinements.
|
|
148
|
+
|
|
149
|
+
### Removed
|
|
150
|
+
- The `gjc` command and the bundled `gjc` skill — the skills catalog now ships five workflows (deep-interview, deep-dive, ralplan, team, ultragoal).
|
|
151
|
+
|
|
152
|
+
## [0.4.6] - 2026-06-14
|
|
153
|
+
_Width-correct forge cards for CJK/emoji, red borders on failed tool cards, aligned `ooo ralph` monitor HUD, and a per-theme user-card palette._
|
|
154
|
+
|
|
155
|
+
### Fixed
|
|
156
|
+
- Forge tool cards no longer tear their right border when the body contains CJK/Hangul/emoji: content wraps by DISPLAY width (wide glyphs count 2 columns) instead of code-point count, so a Korean line that previously rendered ~2× wide now stays inside the card at every width.
|
|
157
|
+
- The `ooo ralph` monitoring HUD box borders stay flush: every row is padded by display width (ANSI-aware) and the box auto-sizes to its widest row, replacing `String.padEnd` on colored strings that counted SGR escape bytes and spilled the right edge.
|
|
158
|
+
|
|
159
|
+
### Changed
|
|
160
|
+
- Failed forge tool cards now render with a red border (gjc-style state-encoded border) so failures pop out of scrollback at a glance; successful/neutral cards keep the theme accent identity.
|
|
161
|
+
- Every built-in theme now ships a `userCard` palette (accent/border/shadow/fill) for the mid-turn steering user card.
|
|
162
|
+
- `describeModel`/`resolveModelId` accept an already-read config to skip a redundant global-config read on the turn hot path.
|
|
163
|
+
|
|
164
|
+
## [0.4.5] - 2026-06-14
|
|
165
|
+
_First-class filesystem make/remove tools._
|
|
166
|
+
|
|
167
|
+
### Added
|
|
168
|
+
- `mkdir {dirPath}` tool: create a directory (parents included, idempotent) as a first-class tool instead of shelling out to `bash` — honors the deep-interview mutation lock and prefix-restricted roles.
|
|
169
|
+
- `delete {path, recursive?}` tool: remove a file (or a directory with `recursive:true`); refuses to wipe the working directory, treats a missing path as a soft error, and clears the file-freshness snapshot so a later write to the same path is not rejected as stale.
|
|
170
|
+
|
|
171
|
+
### Changed
|
|
172
|
+
- Read-only subagent lanes (planner/architect/critic) now also drop `mkdir`/`delete`, keeping review roles physically unable to mutate the repo.
|
|
173
|
+
|
|
174
|
+
## [0.4.4] - 2026-06-13
|
|
175
|
+
_Live subagent status mirroring, always-useful Ctrl+O activity tail, read lineRange crash guard._
|
|
176
|
+
|
|
177
|
+
### Added
|
|
178
|
+
- Per-turn activity-history ring (bounded at 200 plain-text entries): Ctrl+O now always answers "what has been happening" — the detail panel appends a timestamped `+N.Ns` recent-activity tail even before the first reply or tool detail exists.
|
|
179
|
+
|
|
180
|
+
### Changed
|
|
181
|
+
- The live status row now mirrors a delegated subagent's LATEST nested event (`EXECUTOR ✓ read src/…`) instead of a static `Task: executor …` title — a long `task` no longer reads as an opaque "calling model" stall.
|
|
182
|
+
|
|
183
|
+
### Fixed
|
|
184
|
+
- `read` no longer crashes with `spec.split is not a function` when the model passes a numeric/JSON `lineRange` (field bug, reproduced live twice): numbers are coerced, junk degrades to a polite selector error.
|
|
185
|
+
|
|
186
|
+
## [0.4.3] - 2026-06-13
|
|
187
|
+
_Readability pass for autopilot, subagent activity, and worked-history review._
|
|
188
|
+
|
|
189
|
+
### Added
|
|
190
|
+
- `jeo autopilot status` now renders a yellow ratchet status field with task, eval, score direction, keep/revert counts, patience, and the recommended next action.
|
|
191
|
+
- `/history` transcript output now adds turn headers and folds the first tool-result line into each tool activity row, so scrollback reads as user → activity → jeo instead of raw protocol traffic.
|
|
192
|
+
|
|
193
|
+
### Changed
|
|
194
|
+
- Subagent activity lines now render as an `AGENT` tree (`▸ ROLE`, `├─ ROLE`, `└─ ROLE`) in both TUI scrollback and non-TTY progress output for faster scanning.
|
|
195
|
+
- README command tables now call out `/history` and `autopilot` as first-class readable operation surfaces.
|
|
196
|
+
- Removed the standalone `jeo models` command/menu path; model discovery and assignment now stay inside `/model`, `/provider`, and setup/doctor flows.
|
|
197
|
+
|
|
198
|
+
## [0.4.2] - 2026-06-13
|
|
199
|
+
_Thinking-loop termination guarantees (cycle guard + turn wall-clock budget), unboxed live status without step counters, self-contained `.jeo` namespace, live next-prompt input card, role-targeted model/thinking picker._
|
|
200
|
+
|
|
201
|
+
### Added
|
|
202
|
+
- Agent-loop cycle guard: an A↔B tool-call ping-pong (re-reading one file ↔ re-running one command forever) now gets ONE corrective bounce, then a hard stop — the "stuck in thinking" spin the exact-repeat guard could never see.
|
|
203
|
+
- Turn wall-clock budget (`JEO_TURN_MAX_MS`, default 30 minutes, `0` disables): step budgets bound the COUNT of model calls, this bounds their total TIME — a turn that crosses it consolidates a wrap-up instead of spinning for hours.
|
|
204
|
+
- Live next-prompt input box in the TUI — text typed during a running turn stays in the same query surface instead of a separate queued row.
|
|
205
|
+
- jeo discovers skills from its own `~/.jeo/agent/skills` (+ project `.jeo/agent/skills`) and resolves hooks/rules under `.jeo` instead of referencing `.gjc`.
|
|
206
|
+
- Config-driven custom subagent roles: a non-bundled id declaring `title`/`description`/`prompt` becomes a first-class role at runtime.
|
|
207
|
+
- Ctrl+O mid-turn detail view: flush the full last reply + tool output into scrollback.
|
|
208
|
+
- `/fast [on|off|status]` slash command: enables minimal/low reasoning fast mode only when the active model advertises support.
|
|
209
|
+
- Task/team subagents now receive the same project context block as the parent agent, sourced from `JEO.md`, `AGENTS.md`, `.jeo/context.md`, `.agents/*`, and `.jeo/*` guidance — legacy `.gjc` context is not loaded.
|
|
210
|
+
|
|
211
|
+
### Changed
|
|
212
|
+
- Live status is UNBOXED: a flat `⠙ thinking · <live activity> ⟦esc⟧` row plus one dim metrics row replaces the bordered status box — the message is never trapped inside a border.
|
|
213
|
+
- Removed meaningless `step N/M` counters everywhere (status row, footer, plain-stream `[step N/M]` headers, nested subagent lines) along with step-driven `eta`/`evo %`: the dynamic step budget keeps extending the denominator, so the counters carried no information. The evolution stage track stays.
|
|
214
|
+
- Tool-call signature bookkeeping (repeat/cycle guards, step-budget novelty set) now stores fixed-size FNV digests instead of full JSON argument strings — a long turn's guard memory stays flat even when `write` calls embed whole file bodies.
|
|
215
|
+
- Unified model targeting: `/model` can now set default thinking, pick a model, apply it to the default agent or any subagent role, and set that target's thinking level in one flow.
|
|
216
|
+
- `/model` picker now shows DEFAULT/role badges with each target's thinking level, and the post-pick action menu uses the unified Set-as-role format plus an OpenAI Codex role preset.
|
|
217
|
+
- `/model` action selection now uses a Ralph-style nested sub-list: each DEFAULT/role header expands into selectable thinking rows, so target and thinking are chosen in one TUI screen.
|
|
218
|
+
- During a live reasoning turn, typed next-user text now renders as a styled pending `user` card with dark background while the normal input box remains editable.
|
|
219
|
+
- Update availability now renders as a yellow full-width field instead of a boxed card, matching the status-field TUI treatment.
|
|
220
|
+
- Removed the legacy `/models` slash-menu path; `/model` and `/provider` own interactive model selection.
|
|
221
|
+
- Canonicalized runtime naming on `.jeo` and `JEO_` only.
|
|
222
|
+
|
|
223
|
+
### Fixed
|
|
224
|
+
- jeo can no longer sit in "thinking" forever: every turn now terminates via the cycle guard, the wall-clock budget, or the existing step/repeat/failure guards — pathological spins consolidate a wrap-up instead of running unbounded.
|
|
225
|
+
- Ctrl-C now force-quits jeo immediately instead of being softened into an abort prompt.
|
|
226
|
+
- Done-time todo reconciliation gate — stale Todos can no longer survive a finished turn.
|
|
227
|
+
- MCP stdio framing for ralph tools.
|
|
228
|
+
|
|
229
|
+
## [0.4.1] - 2026-06-12
|
|
230
|
+
_TUI card parity polish + done-time todo reconciliation._
|
|
231
|
+
|
|
232
|
+
### Added
|
|
233
|
+
- gjc card parity: `⟦Ctrl+O for more⟧` clip hint, code highlighting in card bodies, and full tool output via Ctrl+O.
|
|
234
|
+
|
|
235
|
+
### Fixed
|
|
236
|
+
- Clip hint also covers summarize-stage markers.
|
|
237
|
+
- Done-time todo reconciliation gate so a finished turn's checklist reflects what actually completed.
|
|
238
|
+
|
|
239
|
+
## [0.4.0] - 2026-06-12
|
|
240
|
+
_Verified TUI, resilient engine, batch input, multilingual docs._
|
|
241
|
+
|
|
242
|
+
### Added
|
|
243
|
+
- Bracketed-paste batch input — a multi-line paste runs one command per line, in order (prompt_toolkit paste contract).
|
|
244
|
+
- jeo-ref transcript parity: Todo Write tree cards, line-numbered write previews, agent-name reasoning blocks, tree-style skill detail.
|
|
245
|
+
- Seed writer/parser round-trip integrity with a freeze-time assert.
|
|
246
|
+
- Slow-drip stream deadline + acceptance-criteria quality floor.
|
|
247
|
+
|
|
248
|
+
### Fixed
|
|
249
|
+
- Anthropic refusal recovery: context reset per the provider contract, neutral continuation note, OAuth/API-key guidance.
|
|
250
|
+
- Model discovery repaired against live endpoints (codex `client_version`, gemini pagination).
|
|
251
|
+
|
|
252
|
+
## [0.3.0] - 2026-06-02
|
|
253
|
+
_OAuth credentials + local Ollama provider._
|
|
254
|
+
|
|
255
|
+
### Added
|
|
256
|
+
- OAuth login (`jeo auth`) and a local Ollama provider.
|
|
257
|
+
- `jeo doctor`, multi-tool-call grouping, and CI/install hardening.
|
|
258
|
+
|
|
259
|
+
## [0.2.1] - 2026-06-02
|
|
260
|
+
_Setup and model configuration._
|
|
261
|
+
|
|
262
|
+
### Added
|
|
263
|
+
- `jeo setup` and `jeo models`; default Gemini 2.5-flash; verified real LLM turn.
|
|
264
|
+
|
|
265
|
+
## [0.2.0] - 2026-06-02
|
|
266
|
+
_Real LLM coding agent._
|
|
267
|
+
|
|
268
|
+
### Added
|
|
269
|
+
- Real LLM coding agent with provider + model configuration.
|
|
270
|
+
|
|
271
|
+
## [0.1.0] - 2026-06-01
|
|
272
|
+
_Initial release._
|
|
273
|
+
|
|
274
|
+
### Added
|
|
275
|
+
- Initial jeo-code agent and CLI.
|
|
276
|
+
|
|
277
|
+
[Unreleased]: https://github.com/akillness/jeo-code/compare/v0.4.5...HEAD
|
|
278
|
+
[0.4.5]: https://github.com/akillness/jeo-code/releases/tag/v0.4.5
|
|
279
|
+
[0.4.4]: https://github.com/akillness/jeo-code/releases/tag/v0.4.4
|
|
280
|
+
[0.4.3]: https://github.com/akillness/jeo-code/releases/tag/v0.4.3
|
|
281
|
+
[0.4.2]: https://github.com/akillness/jeo-code/releases/tag/v0.4.2
|
|
282
|
+
[0.4.1]: https://github.com/akillness/jeo-code/releases/tag/v0.4.1
|
|
283
|
+
[0.4.0]: https://github.com/akillness/jeo-code/releases/tag/v0.4.0
|
|
284
|
+
[0.3.0]: https://github.com/akillness/jeo-code/releases/tag/v0.3.0
|
|
285
|
+
[0.2.1]: https://github.com/akillness/jeo-code/releases/tag/v0.2.1
|
|
286
|
+
[0.2.0]: https://github.com/akillness/jeo-code/releases/tag/v0.2.0
|
|
287
|
+
[0.1.0]: https://github.com/akillness/jeo-code/releases/tag/v0.1.0
|
package/README.ja.md
CHANGED
|
@@ -150,11 +150,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
150
150
|
## 変更履歴 (Changelog)
|
|
151
151
|
|
|
152
152
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
153
|
+
- **[0.5.13]** (2026-06-15) — Workflow `/` commands actually run — `/deep-interview`, `/team`, `/ultragoal`, `/ralplan` dispatch by name.
|
|
154
|
+
- **[0.5.12]** (2026-06-15) — Yellow status animation while a process runs, and elapsed `(Nms)` on every completed tool card.
|
|
155
|
+
- **[0.5.11]** (2026-06-15) — Backspace on an empty prompt line no longer quits jeo.
|
|
153
156
|
- **[0.5.10]** (2026-06-15) — `/resume` transcript no longer dumps raw JSON for batched tool calls.
|
|
154
157
|
- **[0.5.9]** (2026-06-15) — Bounded per-frame wrap for the live thinking/tool-output blocks — re-render cost no longer grows with stream length.
|
|
155
|
-
- **[0.5.8]** (2026-06-15) — Native Opik observability for the turn loop (opt-in `JEO_OPIK`, pure-TS no-op when unset) + autopilot convergence tracking.
|
|
156
|
-
- **[0.5.7]** (2026-06-15) — `/model` picker is default-only, `/clear` resets to the initial screen, ESC clears the input box, and a launch process-listener leak is fixed.
|
|
157
|
-
- **[0.5.6]** (2026-06-15) — `/model` sets only the default thinking; per-role reasoning moved to `/agents`.
|
|
158
158
|
|
|
159
159
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
160
160
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -150,11 +150,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
150
150
|
## 변경 이력 (Changelog)
|
|
151
151
|
|
|
152
152
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
153
|
+
- **[0.5.13]** (2026-06-15) — Workflow `/` commands actually run — `/deep-interview`, `/team`, `/ultragoal`, `/ralplan` dispatch by name.
|
|
154
|
+
- **[0.5.12]** (2026-06-15) — Yellow status animation while a process runs, and elapsed `(Nms)` on every completed tool card.
|
|
155
|
+
- **[0.5.11]** (2026-06-15) — Backspace on an empty prompt line no longer quits jeo.
|
|
153
156
|
- **[0.5.10]** (2026-06-15) — `/resume` transcript no longer dumps raw JSON for batched tool calls.
|
|
154
157
|
- **[0.5.9]** (2026-06-15) — Bounded per-frame wrap for the live thinking/tool-output blocks — re-render cost no longer grows with stream length.
|
|
155
|
-
- **[0.5.8]** (2026-06-15) — Native Opik observability for the turn loop (opt-in `JEO_OPIK`, pure-TS no-op when unset) + autopilot convergence tracking.
|
|
156
|
-
- **[0.5.7]** (2026-06-15) — `/model` picker is default-only, `/clear` resets to the initial screen, ESC clears the input box, and a launch process-listener leak is fixed.
|
|
157
|
-
- **[0.5.6]** (2026-06-15) — `/model` sets only the default thinking; per-role reasoning moved to `/agents`.
|
|
158
158
|
|
|
159
159
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
160
160
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -150,11 +150,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
|
|
|
150
150
|
## Changelog
|
|
151
151
|
|
|
152
152
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
153
|
+
- **[0.5.13]** (2026-06-15) — Workflow `/` commands actually run — `/deep-interview`, `/team`, `/ultragoal`, `/ralplan` dispatch by name.
|
|
154
|
+
- **[0.5.12]** (2026-06-15) — Yellow status animation while a process runs, and elapsed `(Nms)` on every completed tool card.
|
|
155
|
+
- **[0.5.11]** (2026-06-15) — Backspace on an empty prompt line no longer quits jeo.
|
|
153
156
|
- **[0.5.10]** (2026-06-15) — `/resume` transcript no longer dumps raw JSON for batched tool calls.
|
|
154
157
|
- **[0.5.9]** (2026-06-15) — Bounded per-frame wrap for the live thinking/tool-output blocks — re-render cost no longer grows with stream length.
|
|
155
|
-
- **[0.5.8]** (2026-06-15) — Native Opik observability for the turn loop (opt-in `JEO_OPIK`, pure-TS no-op when unset) + autopilot convergence tracking.
|
|
156
|
-
- **[0.5.7]** (2026-06-15) — `/model` picker is default-only, `/clear` resets to the initial screen, ESC clears the input box, and a launch process-listener leak is fixed.
|
|
157
|
-
- **[0.5.6]** (2026-06-15) — `/model` sets only the default thinking; per-role reasoning moved to `/agents`.
|
|
158
158
|
|
|
159
159
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
160
160
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -150,11 +150,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
150
150
|
## 更新日志 (Changelog)
|
|
151
151
|
|
|
152
152
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
153
|
+
- **[0.5.13]** (2026-06-15) — Workflow `/` commands actually run — `/deep-interview`, `/team`, `/ultragoal`, `/ralplan` dispatch by name.
|
|
154
|
+
- **[0.5.12]** (2026-06-15) — Yellow status animation while a process runs, and elapsed `(Nms)` on every completed tool card.
|
|
155
|
+
- **[0.5.11]** (2026-06-15) — Backspace on an empty prompt line no longer quits jeo.
|
|
153
156
|
- **[0.5.10]** (2026-06-15) — `/resume` transcript no longer dumps raw JSON for batched tool calls.
|
|
154
157
|
- **[0.5.9]** (2026-06-15) — Bounded per-frame wrap for the live thinking/tool-output blocks — re-render cost no longer grows with stream length.
|
|
155
|
-
- **[0.5.8]** (2026-06-15) — Native Opik observability for the turn loop (opt-in `JEO_OPIK`, pure-TS no-op when unset) + autopilot convergence tracking.
|
|
156
|
-
- **[0.5.7]** (2026-06-15) — `/model` picker is default-only, `/clear` resets to the initial screen, ESC clears the input box, and a launch process-listener leak is fixed.
|
|
157
|
-
- **[0.5.6]** (2026-06-15) — `/model` sets only the default thinking; per-role reasoning moved to `/agents`.
|
|
158
158
|
|
|
159
159
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
160
160
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jeo-code",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.13",
|
|
4
4
|
"description": "Clean, highly optimized AI coding agent using spec-first loop",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/cli.ts",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"scripts/install.sh",
|
|
13
13
|
"scripts/uninstall.sh",
|
|
14
14
|
"tsconfig.json",
|
|
15
|
+
"CHANGELOG.md",
|
|
15
16
|
"README.md",
|
|
16
17
|
"README.ko.md",
|
|
17
18
|
"README.ja.md",
|
package/src/agent/engine.ts
CHANGED
|
@@ -11,6 +11,7 @@ import * as fs from "node:fs/promises";
|
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import type { Message } from "./loop";
|
|
13
13
|
import { extractJsonObject } from "./json";
|
|
14
|
+
import { nativeToolSchemasFor } from "./tool-schemas";
|
|
14
15
|
import { readTool, writeTool, editTool, bashTool, findTool, searchTool, lsTool, mkdirTool, deleteTool, type ToolResult } from "./tools";
|
|
15
16
|
import { webSearchTool, setWebSearchActiveModel } from "./web-search";
|
|
16
17
|
import { friendlyProviderError, isContextOverflowError, isRefusalError } from "../util/provider-error";
|
|
@@ -32,6 +33,7 @@ async function invokeCallLlm(history: Message[], options: {
|
|
|
32
33
|
onRetry?: (attempt: number, err: unknown, delayMs: number) => void;
|
|
33
34
|
onToken?: (delta: string) => void;
|
|
34
35
|
onReasoning?: (delta: string) => void;
|
|
36
|
+
tools?: import("../ai/types").NativeToolSchema[];
|
|
35
37
|
}): Promise<string> {
|
|
36
38
|
const mod = await import("./loop");
|
|
37
39
|
return mod.callLlm(history, options);
|
|
@@ -79,6 +81,8 @@ export const TOOL_PROTOCOL = [
|
|
|
79
81
|
"Alternatively, you may batch up to 6 independent calls in a single turn using the following format:",
|
|
80
82
|
'{ "reasoning": "<one short sentence>", "tools": [{ "tool": "<name>", "arguments": { ... } }, ...] }',
|
|
81
83
|
"Batch only independent calls; NEVER batch 'done', and NEVER put a mutating tool (write/edit/bash) after another mutating tool in one batch whose inputs depend on the earlier one.",
|
|
84
|
+
"Tool calibration: scale calls to difficulty — one for a known fact, a few for a normal task, more only when evidence is genuinely missing. Locate before you open: search/find first, then read the hit, instead of guessing paths.",
|
|
85
|
+
"web_search reflex: if the request hinges on a name, version, library, or event you do not actually recognize, search before answering instead of guessing; never claim a result's absence proves nonexistence.",
|
|
82
86
|
].join("\n");
|
|
83
87
|
|
|
84
88
|
/** Restricted protocol for read-only subagent roles (planner/architect/critic):
|
|
@@ -109,25 +113,39 @@ export const WORKING_DISCIPLINE = [
|
|
|
109
113
|
"- Correctness first, maintainability second, brevity third. Prefer boring, explicit code.",
|
|
110
114
|
"- Never present partial work as complete; never suppress tests or warnings to make code pass.",
|
|
111
115
|
"- Never fabricate tool results or test outcomes; verification claims must match what was actually run.",
|
|
116
|
+
"- Don't assume disk/state matches expectations or that a referenced file exists — read to verify first.",
|
|
117
|
+
"- Don't fabricate API/library surfaces from memory; check the source or --help for unfamiliar APIs.",
|
|
112
118
|
"- Never ship stubs, placeholders, or TODO-only code as a delivered feature.",
|
|
113
119
|
"- Never substitute the requested problem with an easier adjacent one.",
|
|
114
120
|
"- Update directly affected callsites, tests, and docs — or state why they are unchanged.",
|
|
115
121
|
"- Reuse existing patterns; parallel conventions are prohibited. Fix problems at their source.",
|
|
116
122
|
"- You are not alone in the repository: treat unexpected changes as user work; never revert or delete them.",
|
|
117
|
-
"-
|
|
123
|
+
"- Trust tool output as truth, but re-read/re-run if a tool fails, a file changed, or output looks stale or self-contradictory.",
|
|
118
124
|
"- Prefer dedicated tools over shell pipelines: read (not cat), search (not grep), edit (not sed).",
|
|
119
125
|
].join("\n");
|
|
120
126
|
|
|
127
|
+
/** Reply discipline (FABLE-5 tone + gjc communication/soul): shapes the agent's
|
|
128
|
+
* user-facing prose. Injected into the interactive + executor system prompts only;
|
|
129
|
+
* read-only subagents carry their own output contracts. */
|
|
130
|
+
export const OUTPUT_DISCIPLINE = [
|
|
131
|
+
"Reply discipline:",
|
|
132
|
+
"- Lead with the answer or result; no preamble, no progress narration, no restating the task.",
|
|
133
|
+
"- Default to tight prose; use headers/bullets/tables ONLY when the content is genuinely multi-part or the user asked — never bullet a one-idea answer.",
|
|
134
|
+
"- Report only what is done or in progress; never announce future work instead of doing it.",
|
|
135
|
+
"- Match reply length to the task: a one-line change gets a one-line report.",
|
|
136
|
+
].join("\n");
|
|
137
|
+
|
|
121
138
|
export function executorSystemPrompt(
|
|
122
139
|
role = "Executor Agent, a senior software developer",
|
|
123
140
|
protocol: string = TOOL_PROTOCOL,
|
|
124
|
-
verificationDirective = "
|
|
141
|
+
verificationDirective = "Before calling done, self-check: did I run the test or command that exercises this change, are directly-affected callsites/tests/docs updated, and does my claim match real output? If any answer is no, keep working — do not call done.",
|
|
125
142
|
): string {
|
|
126
143
|
return (
|
|
127
144
|
`You are the ${role}.\n` +
|
|
128
145
|
`Accomplish the user's request by calling tools and verifying your work.\n\n` +
|
|
129
146
|
`${protocol}\n\n` +
|
|
130
147
|
`${WORKING_DISCIPLINE}\n\n` +
|
|
148
|
+
`${OUTPUT_DISCIPLINE}\n\n` +
|
|
131
149
|
verificationDirective
|
|
132
150
|
);
|
|
133
151
|
}
|
|
@@ -413,6 +431,12 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
413
431
|
try {
|
|
414
432
|
responseText = await invokeCallLlm(history, {
|
|
415
433
|
jsonMode: true,
|
|
434
|
+
// NATIVE tool-calling: declare the ACTIVE toolset (read-only subagents
|
|
435
|
+
// expose only their non-mutating tools). Capable adapters (anthropic …)
|
|
436
|
+
// use these and re-serialize the structured call to canonical JSON; the
|
|
437
|
+
// antigravity/ollama fallback ignores them. Only on the main step — never
|
|
438
|
+
// the prose wrap-up call below.
|
|
439
|
+
tools: nativeToolSchemasFor(Object.keys(tools)),
|
|
416
440
|
model: opts.model,
|
|
417
441
|
maxTokens: opts.maxTokens,
|
|
418
442
|
signal: opts.signal,
|
|
@@ -503,7 +527,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
503
527
|
|
|
504
528
|
let invocation: any;
|
|
505
529
|
try {
|
|
506
|
-
invocation = extractJsonObject<any>(responseText);
|
|
530
|
+
invocation = extractJsonObject<any>(responseText, { preferKeys: ["tool", "tools"] });
|
|
507
531
|
} catch (err) {
|
|
508
532
|
ev.onAssistant?.(responseText, null);
|
|
509
533
|
// Prose salvage: a reply with no JSON object at all is a chat-style final
|
package/src/agent/json.ts
CHANGED
|
@@ -4,45 +4,109 @@
|
|
|
4
4
|
* Models (especially non-jsonMode backends like Anthropic/Ollama) routinely
|
|
5
5
|
* wrap JSON in prose, ```json fences, or trailing commentary. This recovers the
|
|
6
6
|
* first balanced top-level `{...}` object, respecting strings and escapes.
|
|
7
|
+
*
|
|
8
|
+
* gjc-robustness hardening (the JSON-mode path is the hot path for the default
|
|
9
|
+
* antigravity provider, which is text-only and cannot use native tool-calling):
|
|
10
|
+
* - tolerate trailing commas before `}`/`]` (a frequent small-model slip);
|
|
11
|
+
* - `preferKeys` lets the tool-call caller prefer the balanced object that
|
|
12
|
+
* actually carries a `tool`/`tools` field over an earlier stray JSON object.
|
|
7
13
|
*/
|
|
8
|
-
export function extractJsonObject<T = unknown>(
|
|
14
|
+
export function extractJsonObject<T = unknown>(
|
|
15
|
+
text: string,
|
|
16
|
+
opts?: { preferKeys?: string[] },
|
|
17
|
+
): T {
|
|
9
18
|
const raw = text.trim();
|
|
19
|
+
const preferKeys = opts?.preferKeys;
|
|
10
20
|
|
|
11
|
-
// Fast path: already pure JSON.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} catch {
|
|
15
|
-
/* fall through to recovery */
|
|
16
|
-
}
|
|
21
|
+
// Fast path: already pure JSON (optionally with a trailing comma to repair).
|
|
22
|
+
const fast = tryParse<T>(raw);
|
|
23
|
+
if (fast !== undefined) return fast;
|
|
17
24
|
|
|
18
|
-
// Strip common code fences and retry.
|
|
25
|
+
// Strip common code fences and retry (pure, then trailing-comma-repaired).
|
|
19
26
|
const defenced = raw.replace(/```(?:json|JSON)?/g, "").trim();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} catch {
|
|
23
|
-
/* fall through to brace scan */
|
|
24
|
-
}
|
|
27
|
+
const fromDefencedWhole = tryParse<T>(defenced);
|
|
28
|
+
if (fromDefencedWhole !== undefined) return fromDefencedWhole;
|
|
25
29
|
|
|
26
|
-
const parsedFromDefenced = findAndParseBalancedObject<T>(defenced);
|
|
27
|
-
if (parsedFromDefenced !==
|
|
30
|
+
const parsedFromDefenced = findAndParseBalancedObject<T>(defenced, preferKeys);
|
|
31
|
+
if (parsedFromDefenced !== undefined) {
|
|
28
32
|
return parsedFromDefenced;
|
|
29
33
|
}
|
|
30
|
-
const parsedFromRaw = findAndParseBalancedObject<T>(raw);
|
|
31
|
-
if (parsedFromRaw !==
|
|
34
|
+
const parsedFromRaw = findAndParseBalancedObject<T>(raw, preferKeys);
|
|
35
|
+
if (parsedFromRaw !== undefined) {
|
|
32
36
|
return parsedFromRaw;
|
|
33
37
|
}
|
|
34
38
|
throw new Error(`No parseable JSON object found in model output: ${truncate(raw, 200)}`);
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
/** Like {@link extractJsonObject} but returns null instead of throwing. */
|
|
38
|
-
export function tryExtractJsonObject<T = unknown>(
|
|
42
|
+
export function tryExtractJsonObject<T = unknown>(
|
|
43
|
+
text: string,
|
|
44
|
+
opts?: { preferKeys?: string[] },
|
|
45
|
+
): T | null {
|
|
39
46
|
try {
|
|
40
|
-
return extractJsonObject<T>(text);
|
|
47
|
+
return extractJsonObject<T>(text, opts);
|
|
41
48
|
} catch {
|
|
42
49
|
return null;
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Parse `s` as JSON, tolerating a single class of common model slip: a trailing
|
|
55
|
+
* comma right before a closing `}` or `]`. Returns `undefined` (not null — a bare
|
|
56
|
+
* `null`/`false` is a legal JSON value) when nothing parses.
|
|
57
|
+
*/
|
|
58
|
+
function tryParse<T>(s: string): T | undefined {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(s) as T;
|
|
61
|
+
} catch {
|
|
62
|
+
/* fall through to a trailing-comma repair */
|
|
63
|
+
}
|
|
64
|
+
const repaired = stripTrailingCommas(s);
|
|
65
|
+
if (repaired !== s) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(repaired) as T;
|
|
68
|
+
} catch {
|
|
69
|
+
/* unrecoverable here */
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Remove a comma that directly precedes a closing `}` or `]` (ignoring
|
|
77
|
+
* whitespace), but never a comma inside a string literal. String/escape state is
|
|
78
|
+
* tracked so a comma inside `"a,}"` is preserved.
|
|
79
|
+
*/
|
|
80
|
+
function stripTrailingCommas(s: string): string {
|
|
81
|
+
let out = "";
|
|
82
|
+
let inString = false;
|
|
83
|
+
let escaped = false;
|
|
84
|
+
for (let i = 0; i < s.length; i++) {
|
|
85
|
+
const ch = s[i]!;
|
|
86
|
+
if (inString) {
|
|
87
|
+
out += ch;
|
|
88
|
+
if (escaped) escaped = false;
|
|
89
|
+
else if (ch === "\\") escaped = true;
|
|
90
|
+
else if (ch === '"') inString = false;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (ch === '"') {
|
|
94
|
+
inString = true;
|
|
95
|
+
out += ch;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (ch === ",") {
|
|
99
|
+
let j = i + 1;
|
|
100
|
+
while (j < s.length && (s[j] === " " || s[j] === "\t" || s[j] === "\n" || s[j] === "\r")) j++;
|
|
101
|
+
if (j < s.length && (s[j] === "}" || s[j] === "]")) {
|
|
102
|
+
continue; // drop the trailing comma
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
out += ch;
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
46
110
|
/** Scan for a brace-balanced object starting at startIndex, ignoring braces inside strings. */
|
|
47
111
|
function extractBalancedObject(text: string, startIndex: number): string | null {
|
|
48
112
|
let depth = 0;
|
|
@@ -66,20 +130,36 @@ function extractBalancedObject(text: string, startIndex: number): string | null
|
|
|
66
130
|
return null;
|
|
67
131
|
}
|
|
68
132
|
|
|
69
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Find the first balanced `{...}` that parses as JSON. When `preferKeys` is given,
|
|
135
|
+
* keep scanning past an earlier parseable object that lacks every preferred key and
|
|
136
|
+
* return the first object that DOES carry one (e.g. the real `{ "tool": ... }` call
|
|
137
|
+
* after a stray JSON-looking object in reasoning prose); fall back to the first
|
|
138
|
+
* parseable object when none carries a preferred key.
|
|
139
|
+
*/
|
|
140
|
+
function findAndParseBalancedObject<T>(text: string, preferKeys?: string[]): T | undefined {
|
|
141
|
+
let firstParsed: T | undefined;
|
|
70
142
|
for (let i = 0; i < text.length; i++) {
|
|
71
143
|
if (text[i] === "{") {
|
|
72
144
|
const candidate = extractBalancedObject(text, i);
|
|
73
145
|
if (candidate) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
146
|
+
const parsed = tryParse<T>(candidate);
|
|
147
|
+
if (parsed !== undefined) {
|
|
148
|
+
if (firstParsed === undefined) firstParsed = parsed;
|
|
149
|
+
if (!preferKeys || preferKeys.length === 0) return parsed;
|
|
150
|
+
if (
|
|
151
|
+
parsed !== null &&
|
|
152
|
+
typeof parsed === "object" &&
|
|
153
|
+
preferKeys.some(k => k in (parsed as Record<string, unknown>))
|
|
154
|
+
) {
|
|
155
|
+
return parsed;
|
|
156
|
+
}
|
|
157
|
+
// else: keep scanning for an object that carries a preferred key
|
|
78
158
|
}
|
|
79
159
|
}
|
|
80
160
|
}
|
|
81
161
|
}
|
|
82
|
-
return
|
|
162
|
+
return firstParsed;
|
|
83
163
|
}
|
|
84
164
|
|
|
85
165
|
function truncate(s: string, n: number): string {
|