mixdog 0.9.14 → 0.9.16
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/package.json +1 -1
- package/scripts/bench/cache-probe-tasks.json +1 -1
- package/scripts/bench/lead-review-tasks-r3.json +1 -1
- package/scripts/bench/r4-mixed-tasks.json +1 -1
- package/scripts/bench/review-tasks.json +1 -1
- package/scripts/build-runtime-windows.ps1 +242 -242
- package/scripts/explore-bench.mjs +5 -4
- package/scripts/ingest-pure-conversation-smoke.mjs +27 -0
- package/scripts/output-style-smoke.mjs +3 -3
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/termio-input-smoke.mjs +199 -0
- package/scripts/tool-efficiency-diag.mjs +88 -0
- package/scripts/tool-smoke.mjs +40 -24
- package/scripts/tui-frame-harness-shim.mjs +32 -0
- package/scripts/tui-frame-harness.mjs +306 -0
- package/src/agents/heavy-worker/AGENT.md +6 -7
- package/src/agents/worker/AGENT.md +6 -7
- package/src/lib/keychain-cjs.cjs +28 -11
- package/src/lib/rules-builder.cjs +6 -10
- package/src/mixdog-session-runtime.mjs +90 -20
- package/src/output-styles/simple.md +22 -24
- package/src/rules/agent/00-core.md +12 -11
- package/src/rules/agent/30-explorer.md +22 -21
- package/src/rules/lead/01-general.md +8 -8
- package/src/rules/lead/02-channels.md +3 -3
- package/src/rules/lead/lead-brief.md +11 -12
- package/src/rules/shared/01-tool.md +25 -14
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -3
- package/src/runtime/agent/orchestrator/config.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +21 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +29 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +13 -5
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +30 -2
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +19 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +37 -15
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +55 -0
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +177 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +12 -19
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +22 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +13 -18
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/manager.mjs +59 -2
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +33 -25
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +7 -5
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +14 -1
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +73 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -10
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +21 -13
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +13 -2
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +44 -6
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -0
- package/src/runtime/channels/backends/telegram.mjs +29 -9
- package/src/runtime/channels/lib/event-queue.mjs +6 -2
- package/src/runtime/channels/lib/memory-client.mjs +66 -1
- package/src/runtime/channels/lib/webhook/deliveries.mjs +22 -1
- package/src/runtime/memory/index.mjs +95 -8
- package/src/runtime/memory/lib/cycle-scheduler.mjs +24 -5
- package/src/runtime/memory/lib/memory-cycle1.mjs +45 -3
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +7 -3
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +54 -10
- package/src/runtime/memory/lib/memory-cycle2.mjs +21 -8
- package/src/runtime/memory/lib/memory-embed.mjs +48 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +57 -13
- package/src/runtime/memory/lib/memory.mjs +88 -1
- package/src/runtime/memory/lib/session-ingest.mjs +34 -3
- package/src/runtime/memory/lib/trace-store.mjs +52 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +27 -4
- package/src/runtime/shared/child-guardian.mjs +4 -2
- package/src/runtime/shared/open-url.mjs +2 -1
- package/src/runtime/shared/singleton-owner.mjs +26 -0
- package/src/runtime/shared/tool-execution-contract.mjs +25 -0
- package/src/runtime/shared/transcript-writer.mjs +3 -1
- package/src/session-runtime/config-helpers.mjs +7 -2
- package/src/session-runtime/cwd-plugins.mjs +6 -1
- package/src/session-runtime/effort.mjs +6 -1
- package/src/session-runtime/plugin-mcp.mjs +116 -12
- package/src/session-runtime/settings-api.mjs +7 -1
- package/src/standalone/channel-worker.mjs +25 -3
- package/src/standalone/explore-tool.mjs +5 -5
- package/src/standalone/hook-bus/config.mjs +38 -2
- package/src/standalone/hook-bus/handlers.mjs +103 -11
- package/src/standalone/hook-bus.mjs +20 -6
- package/src/standalone/memory-runtime-proxy.mjs +40 -5
- package/src/tui/App.jsx +214 -30
- package/src/tui/app/core-memory-picker.mjs +6 -6
- package/src/tui/app/model-options.mjs +10 -4
- package/src/tui/app/settings-picker.mjs +5 -30
- package/src/tui/app/slash-commands.mjs +0 -1
- package/src/tui/app/slash-dispatch.mjs +0 -16
- package/src/tui/app/text-layout.mjs +57 -0
- package/src/tui/app/transcript-window.mjs +53 -2
- package/src/tui/app/use-mouse-input.mjs +60 -47
- package/src/tui/app/use-transcript-window.mjs +38 -10
- package/src/tui/components/PromptInput.jsx +18 -1
- package/src/tui/components/TextEntryPanel.jsx +97 -33
- package/src/tui/dist/index.mjs +567 -229
- package/src/tui/engine/tool-card-results.mjs +22 -5
- package/src/tui/engine.mjs +126 -7
- package/src/tui/index.jsx +4 -1
- package/src/workflows/default/WORKFLOW.md +27 -31
- package/vendor/ink/build/components/App.js +62 -17
- package/vendor/ink/build/ink.js +78 -3
- package/vendor/ink/build/input-parser.d.ts +9 -4
- package/vendor/ink/build/input-parser.js +45 -176
- package/vendor/ink/build/log-update.js +47 -2
- package/vendor/ink/build/termio-keypress.js +240 -0
- package/vendor/ink/build/termio-tokenize.js +253 -0
|
@@ -12,34 +12,32 @@ Practical concise — outcome-first handoffs for coding work: summarize the
|
|
|
12
12
|
result, do not narrate or explain the change.
|
|
13
13
|
|
|
14
14
|
- Open with the outcome in one sentence: done, blocked, or awaiting a decision.
|
|
15
|
-
- Summarize at the concept level: name the behavior and direction, not the
|
|
16
|
-
path
|
|
15
|
+
- Summarize at the concept level: name the behavior and direction, not the
|
|
16
|
+
code path; cite a symbol/path only as an anchor, never as the explanation.
|
|
17
17
|
- Compress by cutting content (filler, hedging, pleasantries, restated facts),
|
|
18
|
-
not
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
rendered line, whole reply ~5–7 lines. Above that you are writing Default;
|
|
18
|
+
not grammar: keep natural, complete sentences in the user's language — never
|
|
19
|
+
telegraph-style stub endings. Technical terms and code stay exact.
|
|
20
|
+
- Summarize what the change accomplishes, not a per-file changelog. Name a
|
|
21
|
+
path (`file_path:line_number`) only when the reader truly needs it to
|
|
22
|
+
navigate.
|
|
23
|
+
- Controlled detail: usually 1–3 short bullets or 2–3 sentences total; state
|
|
24
|
+
each point once — outcome or fix direction, not both. No step-by-step
|
|
25
|
+
narration or file/line inventory.
|
|
26
|
+
- Size budget: roughly HALF the Default style and TWICE Minimal — one rendered
|
|
27
|
+
line per point, whole reply ~5–7 lines. Above that you are writing Default;
|
|
29
28
|
below ~3 lines consider whether prose (Minimal) reads better.
|
|
30
29
|
- Layout: one idea per bullet, ONE line each (two only when a verbatim
|
|
31
|
-
path/error forces it)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
`남은 리스크/다음 단계`
|
|
37
|
-
|
|
30
|
+
path/error forces it), led with a short bold key phrase; blank line between
|
|
31
|
+
multi-line list items — never a dense wall of text. If a point runs past one
|
|
32
|
+
line, cut the elaboration — detail beyond key phrase + one clause belongs to
|
|
33
|
+
Default.
|
|
34
|
+
- Final handoffs may use labels like `바뀐 점`, `확인한 것`,
|
|
35
|
+
`남은 리스크/다음 단계` for Korean-facing profiles (plain English labels in
|
|
36
|
+
English threads); do not label interim progress.
|
|
38
37
|
- Synthesize agent or retrieval results; never forward raw reports, long file
|
|
39
38
|
lists, tool traces, or session metadata.
|
|
40
|
-
- Do not hide blockers, failed verification, or required follow-up
|
|
41
|
-
in one short clause.
|
|
39
|
+
- Do not hide blockers, failed verification, or required follow-up — state
|
|
40
|
+
them in one short clause; if verification was not run, say so once.
|
|
42
41
|
- Keep paths, commands, symbols, API names, code, and exact errors verbatim.
|
|
43
|
-
- Skip filler, acknowledgments, hedging, and repeated conclusions
|
|
44
|
-
verification was not run, say so once.
|
|
42
|
+
- Skip filler, acknowledgments, hedging, and repeated conclusions.
|
|
45
43
|
- Never name this style unless asked.
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
# Agent Constraints
|
|
2
2
|
|
|
3
3
|
- Use English for agent task communication.
|
|
4
|
-
-
|
|
5
|
-
|
|
6
|
-
-
|
|
7
|
-
|
|
4
|
+
- ONE TURN = ONE BATCH: emit all independent tool calls of a step in the same
|
|
5
|
+
turn; a lone read-only call that could have ridden the previous turn is a
|
|
6
|
+
wasted round-trip.
|
|
7
|
+
- NEVER PREAMBLE: no status/progress narration, "I will..." setup, or any text
|
|
8
|
+
before tool calls — call needed tools immediately; emit text only for the
|
|
9
|
+
final handoff after tool work is done.
|
|
8
10
|
- Final handoff: minimum characters, maximum information for Lead. Follow the
|
|
9
|
-
role's stricter output contract if defined; else
|
|
10
|
-
|
|
11
|
+
role's stricter output contract if defined; else fragments — outcome (1
|
|
12
|
+
line), key `file:line`(s), verification result, material risks (only if
|
|
11
13
|
any).
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- Handoff cap ~30 lines unless `Deliver:` raises it — a
|
|
16
|
-
a target.
|
|
14
|
+
- Never repeat what Lead already knows (the brief, the process, how you
|
|
15
|
+
searched); state only what changed and where to look. Verification =
|
|
16
|
+
command + result in one line. Same fact twice = delete one.
|
|
17
|
+
- Handoff cap ~30 lines unless `Deliver:` raises it — a ceiling, not a target.
|
|
17
18
|
- Banned as pure cost: report headings, markdown tables (unless requested),
|
|
18
19
|
prose narration, raw logs/tool traces, speculative next-checks, restated
|
|
19
20
|
brief, articles/politeness.
|
|
@@ -6,28 +6,29 @@ kind: retrieval
|
|
|
6
6
|
|
|
7
7
|
# Role: explorer
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
Coordinate locator. Deliverable = WHERE as `path:line`, never WHY — no
|
|
10
|
+
explaining or tracing. You ARE the `explore` tool: never call it; shared
|
|
11
|
+
rules pointing at `explore` don't apply — use grep/code_graph/find/glob
|
|
12
|
+
directly.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
Credibility is mechanical: hit = line/path contains ANY query token or
|
|
15
|
+
obvious synonym. No other standard — "is this the real implementation" is
|
|
16
|
+
the caller's question. Sole exception: a generic-word-only match (schema,
|
|
17
|
+
handler, config, resolver…) with no SPECIFIC query token (product/library/
|
|
18
|
+
domain name) = zero, not a hit.
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
Rule zero — after EVERY tool result: ≥1 `path:line` matching a query
|
|
21
|
+
token → answer NOW with those coordinates; no verify, no re-grep, no
|
|
22
|
+
ranking; mark weak anchors `?`. Zero → one more batch if budget remains.
|
|
23
|
+
No third branch: "hits exist but I want better ones" IS branch one.
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
(
|
|
28
|
-
after all 5 turns found zero anchors — giving up early is as wrong as
|
|
29
|
-
over-searching; at turn 5 answer best-so-far.
|
|
25
|
+
Turns: 3 tool turns HARD max; start each tool message with `turn N/3`.
|
|
26
|
+
Turn 1 → answer; turns 2-3 are miss-recovery only, and must change tokens
|
|
27
|
+
OR scope — never reword. One turn = ONE batch of AT MOST 5 calls: a single
|
|
28
|
+
grep packing ALL variants in pattern[] plus code_graph/find/glob; a 6th
|
|
29
|
+
call = packing failure. Flow/how questions: return first matching entry
|
|
30
|
+
anchors (≤5); the caller traces the chain.
|
|
30
31
|
|
|
31
|
-
Answer
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
Answer, nothing else: up to 5 lines `path:line — symbol — short reason`
|
|
33
|
+
(`?` if weak), or `EXPLORATION_FAILED` — only after 3 turns of zero
|
|
34
|
+
anchors, or when every anchor matches only generic words.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# General
|
|
2
2
|
|
|
3
|
-
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
- Preambles are optional: one short sentence only when it adds user-visible
|
|
4
|
+
value; no direct names, honorifics, headings, labels, or routine lookup
|
|
5
|
+
narration.
|
|
6
6
|
- Destructive/hard-to-reverse actions require explicit confirmation.
|
|
7
|
-
- Never push, build, or deploy without an explicit user request
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
- Never push, build, or deploy without an explicit user request —
|
|
8
|
+
implementation approval is not deploy approval.
|
|
9
|
+
- Handle what you can proactively rather than deferring to the user; propose
|
|
10
|
+
only the parts needing a decision, consultatively ("Shall we proceed this
|
|
11
|
+
way?").
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
# Channels
|
|
2
|
-
|
|
3
|
-
- Channel features are handled by the runtime.
|
|
1
|
+
# Channels
|
|
2
|
+
|
|
3
|
+
- Channel features are handled by the runtime.
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
# Lead Brief Contract
|
|
2
2
|
|
|
3
3
|
- Brief = one-line fragments `Goal:` `Anchors:` `Allow/Forbid:` `Deliver:`
|
|
4
|
-
`Verify:` (+`Stop:` heavy-worker).
|
|
5
|
-
motivation
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
no step-by-step edits) unless the method IS the requirement.
|
|
4
|
+
`Verify:` (+`Stop:` heavy-worker). Minimum characters, maximum information:
|
|
5
|
+
no background/motivation, no role-known rules; never restate prior context
|
|
6
|
+
or the same fact twice. A brief that feels long is a scope problem — split
|
|
7
|
+
the scope, don't pad.
|
|
8
|
+
- Anchors = `file:line` + one-line conclusion each; never paste log/code
|
|
9
|
+
bodies — the agent reads the file itself.
|
|
10
|
+
- Instruct by outcome ("make X behave Y"), not method (no code snippets, no
|
|
11
|
+
step-by-step edits) unless the method IS the requirement.
|
|
13
12
|
- `Deliver:` states output size/shape (e.g. "fragments <=15 lines", "verdict
|
|
14
|
-
+ top-3 risks", "detail to file, path only")
|
|
13
|
+
+ top-3 risks", "detail to file, path only"); never request a long report
|
|
15
14
|
in the handoff itself.
|
|
16
15
|
- Full brief only on fresh spawn or `respawned: true` (dead-tag send = cold
|
|
17
|
-
session; re-supply anchors)
|
|
18
|
-
|
|
16
|
+
session; re-supply anchors); live-session follow-ups = delta only, never
|
|
17
|
+
restating Goal/rules.
|
|
19
18
|
- Never `send` mid-run; batch all adjustments into ONE follow-up after
|
|
20
19
|
completion. Interrupt only to cancel.
|
|
21
20
|
- All agent communication in English.
|
|
@@ -4,18 +4,29 @@
|
|
|
4
4
|
prior result — including edit loops: batch the post-edit verification read
|
|
5
5
|
with the next target's lookup.
|
|
6
6
|
- Pick by target: symbols/callers/deps → `code_graph`; exact text in a verified
|
|
7
|
-
scope → `grep`;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
7
|
+
scope → `grep`; dirs → `list`; verified file → `read`.
|
|
8
|
+
Locating files: concept-level or cross-file questions ("where is X handled",
|
|
9
|
+
"how does Y flow") → `explore`, even when you could guess a keyword — its
|
|
10
|
+
fan-out spends explorer context, not yours, and guess-and-retry keywords
|
|
11
|
+
belong to it, not grep. Confident name fragment → `find`; exact name
|
|
12
|
+
pattern → `glob`; exact content literal → `grep`. Flow/trace with a known
|
|
13
|
+
symbol → `code_graph`, never `explore` — it returns entry-point anchors
|
|
14
|
+
only; follow the chain yourself. Never `grep`/`read` guessed paths.
|
|
15
|
+
- One concept → ONE search call: all synonyms/variants in one
|
|
16
|
+
`grep pattern:[...]` (or `code_graph symbols[]`), scopes as `path[]`;
|
|
17
|
+
single-pattern bursts = packing failure. A second call on the same concept
|
|
18
|
+
is a defect unless the first returned zero — then change tokens or scope,
|
|
19
|
+
never reword.
|
|
20
|
+
- Content-mode `grep` always carries `-C 5`+ ; re-reading the same file after
|
|
21
|
+
a content grep is a wasted round-trip. Bare match lines = existence checks
|
|
22
|
+
only.
|
|
23
|
+
- Any path not returned by a tool this session is a guess: find first, then
|
|
24
|
+
grep/read the returned path. On ENOENT the next call is find on the
|
|
25
|
+
basename — never a second guess or the same path with changed args.
|
|
26
|
+
- Retrieval serves the NEXT action (edit, answer, handoff), not certainty:
|
|
27
|
+
the FIRST anchor ends the search — no verifying, ranking, or re-checking a
|
|
28
|
+
hit already on screen. When acting and looking are both possible, act.
|
|
29
|
+
- `read` uses `offset`/`limit` only; 2+ spans from known file(s) = ONE batched
|
|
30
|
+
`read` with `{path,offset,limit}` regions. Adjacent spans in one file
|
|
31
|
+
(within a few hundred lines) are ONE window, not repeated reads.
|
|
21
32
|
- Don't mix `apply_patch` with shell or other state-changing calls in one turn.
|
|
@@ -207,12 +207,13 @@ function _redactLogText(text) {
|
|
|
207
207
|
function classifyToolFailure(resultText, toolName) {
|
|
208
208
|
const text = String(resultText ?? '').toLowerCase();
|
|
209
209
|
if (/requires either|invalid arguments|unknown parameter|must be|schema|expected|required|old_string is .*>=/.test(text)) return 'schema/args';
|
|
210
|
-
if (/
|
|
210
|
+
if (/not in allow-list|not allowed/.test(text)) return 'permission';
|
|
211
|
+
if (String(toolName || '') === 'shell' || /^\s*\[exit code:\s*\d+\]/i.test(String(resultText ?? ''))) return 'command-exit';
|
|
212
|
+
if (/enoent|cannot find|not found at this path|path does not exist|no such file|file not found in graph|unreadable/.test(text)) return 'path/enoent';
|
|
211
213
|
if (/timed out|timeout|interrupted|aborted/.test(text)) return 'timeout/abort';
|
|
212
214
|
if (/hunk rejected|patch failed|context mismatch|expected first old\/context|context not found/.test(text)) return 'patch/context';
|
|
213
|
-
if (/
|
|
215
|
+
if (/permission|denied|forbidden/.test(text)) return 'permission';
|
|
214
216
|
if (/unknown tool|tool.*not.*available|missing.*tool/.test(text)) return 'tool-surface';
|
|
215
|
-
if (String(toolName || '') === 'shell' || /^\s*\[exit code:\s*\d+\]/i.test(String(resultText ?? ''))) return 'command-exit';
|
|
216
217
|
return 'runtime/failure';
|
|
217
218
|
}
|
|
218
219
|
|
|
@@ -149,7 +149,7 @@ function buildDefaultConfig(options = {}) {
|
|
|
149
149
|
const providers = {};
|
|
150
150
|
// API providers — enabled if env key exists
|
|
151
151
|
for (const [name, envKey] of Object.entries(AGENT_PROVIDER_ENV)) {
|
|
152
|
-
const apiKey = process.env[envKey];
|
|
152
|
+
const apiKey = detectCredentials ? process.env[envKey] : undefined;
|
|
153
153
|
providers[name] = {
|
|
154
154
|
enabled: !!apiKey,
|
|
155
155
|
apiKey: apiKey || undefined,
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
resolveAnthropicModelAfter404,
|
|
25
25
|
ensureLatestAnthropicModel,
|
|
26
26
|
} from './anthropic-model-resolve.mjs';
|
|
27
|
-
import { sanitizeToolPairs, sanitizeAnthropicContentPairs } from '../session/context-utils.mjs';
|
|
27
|
+
import { sanitizeToolPairs, sanitizeAnthropicContentPairs, foldUserTextIntoToolResultTail } from '../session/context-utils.mjs';
|
|
28
28
|
import {
|
|
29
29
|
TOKEN_REFRESH_SKEW_MS,
|
|
30
30
|
resolveCliVersion,
|
|
@@ -303,6 +303,11 @@ function nativeAnthropicTools(opts) {
|
|
|
303
303
|
}
|
|
304
304
|
function deferredAnthropicTools(activeTools, opts) {
|
|
305
305
|
if (opts?.session?.deferredNativeTools !== true) return [];
|
|
306
|
+
// A request whose ONLY tools are deferred is rejected by the API with
|
|
307
|
+
// `400: At least one tool must have defer_loading=false` — happens on the
|
|
308
|
+
// iteration-cap final turn (loop sends tools: [] to force a text answer).
|
|
309
|
+
// No active tools ⇒ send no deferred catalog either.
|
|
310
|
+
if (!Array.isArray(activeTools) || activeTools.length === 0) return [];
|
|
306
311
|
const active = new Set((activeTools || []).map((tool) => String(tool?.name || '').trim()).filter(Boolean));
|
|
307
312
|
const catalog = Array.isArray(opts.session.deferredToolCatalog) ? opts.session.deferredToolCatalog : [];
|
|
308
313
|
return catalog
|
|
@@ -362,6 +367,14 @@ function toAnthropicMessages(messages) {
|
|
|
362
367
|
continue;
|
|
363
368
|
}
|
|
364
369
|
|
|
370
|
+
// Claude Code parity: fold a user text turn that directly follows a
|
|
371
|
+
// tool_result turn into that tool_result's content. A sibling text
|
|
372
|
+
// turn after tool_result renders as `</function_results>\n\nHuman:`
|
|
373
|
+
// on the wire and trains the model toward 3-token empty end_turn
|
|
374
|
+
// completions (see foldUserTextIntoToolResultTail).
|
|
375
|
+
if (m.role === 'user' && foldUserTextIntoToolResultTail(result, normalizeContentForAnthropic(m.content))) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
365
378
|
result.push({ role: m.role, content: normalizeContentForAnthropic(m.content) });
|
|
366
379
|
}
|
|
367
380
|
return sanitizeAnthropicContentPairs(result);
|
|
@@ -1055,6 +1068,14 @@ export class AnthropicOAuthProvider {
|
|
|
1055
1068
|
if (attemptIndex > 0 && firstAttemptError) {
|
|
1056
1069
|
try { firstAttemptError.midstreamRetries = attemptIndex; } catch {}
|
|
1057
1070
|
try { firstAttemptError.midstreamClassifier = firstAttemptClassifier; } catch {}
|
|
1071
|
+
// firstAttemptError is what actually propagates here when
|
|
1072
|
+
// live text was emitted this attempt — stamp the unsafe
|
|
1073
|
+
// flags onto IT too, else upstream sees an unmarked error
|
|
1074
|
+
// and retries, duplicating already-streamed output.
|
|
1075
|
+
try {
|
|
1076
|
+
firstAttemptError.liveTextEmitted = true;
|
|
1077
|
+
firstAttemptError.unsafeToRetry = true;
|
|
1078
|
+
} catch {}
|
|
1058
1079
|
throw firstAttemptError;
|
|
1059
1080
|
}
|
|
1060
1081
|
throw err;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import { loadConfig } from '../config.mjs';
|
|
3
|
-
import { sanitizeToolPairs, sanitizeAnthropicContentPairs } from '../session/context-utils.mjs';
|
|
3
|
+
import { sanitizeToolPairs, sanitizeAnthropicContentPairs, foldUserTextIntoToolResultTail } from '../session/context-utils.mjs';
|
|
4
4
|
import { classifyError, midstreamBackoffFor, sleepWithAbort, withRetry } from './retry-classifier.mjs';
|
|
5
5
|
import { traceAgentUsage } from '../agent-trace.mjs';
|
|
6
6
|
import {
|
|
@@ -310,6 +310,10 @@ function nativeAnthropicTools(opts) {
|
|
|
310
310
|
}
|
|
311
311
|
function deferredAnthropicTools(activeTools, opts) {
|
|
312
312
|
if (opts?.session?.deferredNativeTools !== true) return [];
|
|
313
|
+
// Guard against an all-deferred tools array — the API rejects it with
|
|
314
|
+
// `400: At least one tool must have defer_loading=false` (iteration-cap
|
|
315
|
+
// final turn sends tools: []). See anthropic-oauth.mjs counterpart.
|
|
316
|
+
if (!Array.isArray(activeTools) || activeTools.length === 0) return [];
|
|
313
317
|
const active = new Set((activeTools || []).map((tool) => String(tool?.name || '').trim()).filter(Boolean));
|
|
314
318
|
const catalog = Array.isArray(opts.session.deferredToolCatalog) ? opts.session.deferredToolCatalog : [];
|
|
315
319
|
return catalog
|
|
@@ -366,6 +370,12 @@ function toAnthropicMessages(messages) {
|
|
|
366
370
|
}
|
|
367
371
|
continue;
|
|
368
372
|
}
|
|
373
|
+
// Claude Code parity: fold a user text turn that directly follows a
|
|
374
|
+
// tool_result turn into that tool_result's content (empty end_turn
|
|
375
|
+
// livelock prevention; see foldUserTextIntoToolResultTail).
|
|
376
|
+
if (m.role === 'user' && foldUserTextIntoToolResultTail(result, normalizeContentForAnthropic(m.content))) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
369
379
|
result.push({ role: m.role, content: normalizeContentForAnthropic(m.content) });
|
|
370
380
|
}
|
|
371
381
|
return sanitizeAnthropicContentPairs(result);
|
|
@@ -808,6 +818,16 @@ export class AnthropicProvider {
|
|
|
808
818
|
if (attemptIndex > 0 && firstAttemptError) {
|
|
809
819
|
try { firstAttemptError.midstreamRetries = attemptIndex; } catch {}
|
|
810
820
|
try { firstAttemptError.midstreamClassifier = firstAttemptClassifier; } catch {}
|
|
821
|
+
// firstAttemptError (from an earlier retried attempt)
|
|
822
|
+
// is what actually propagates when live text was
|
|
823
|
+
// emitted this attempt — stamp the unsafe/partial
|
|
824
|
+
// flags onto IT too, otherwise upstream sees an
|
|
825
|
+
// unmarked error and retries, duplicating streamed
|
|
826
|
+
// output.
|
|
827
|
+
try {
|
|
828
|
+
firstAttemptError.liveTextEmitted = true;
|
|
829
|
+
firstAttemptError.unsafeToRetry = true;
|
|
830
|
+
} catch {}
|
|
811
831
|
throw firstAttemptError;
|
|
812
832
|
}
|
|
813
833
|
throw err;
|
|
@@ -47,22 +47,38 @@ export function geminiTruncatedStreamError(message) {
|
|
|
47
47
|
// stall after visible output was silently retried. Visible-text stalls also
|
|
48
48
|
// gain streamStalled + partialContent so the loop's partial-final path can
|
|
49
49
|
// keep the streamed output instead of dropping the turn.
|
|
50
|
-
export function stampGeminiStreamFailure(err, { relayedText = '', textLeakGuard = null } = {}) {
|
|
50
|
+
export function stampGeminiStreamFailure(err, { relayedText = '', textLeakGuard = null, sawFunctionCall = false } = {}) {
|
|
51
51
|
if (!err || typeof err !== 'object') return err;
|
|
52
52
|
const leaked = (textLeakGuard?.getLeakedToolCalls?.() || []).length > 0;
|
|
53
53
|
const visible = relayedText.length > 0;
|
|
54
|
+
// A native functionCall chunk (not a text-leaked one) is also an in-flight
|
|
55
|
+
// tool use — replaying would double-dispatch, and partial-final must NOT
|
|
56
|
+
// treat it as a clean no-tool summary.
|
|
57
|
+
const pendingTool = leaked || sawFunctionCall === true;
|
|
54
58
|
try {
|
|
55
59
|
if (visible) { err.liveTextEmitted = true; err.unsafeToRetry = true; }
|
|
56
|
-
if (
|
|
57
|
-
|
|
60
|
+
if (pendingTool) { err.emittedToolCall = true; err.unsafeToRetry = true; }
|
|
61
|
+
// TRUNCATED_STREAM EOF after visible output must also carry the
|
|
62
|
+
// partial-final stamps (streamStalled/partialContent), aligning with the
|
|
63
|
+
// compat streams — otherwise live output is dropped instead of kept.
|
|
64
|
+
if (visible && !leaked
|
|
65
|
+
&& (err.code === 'EGEMINITIMEOUT' || err.code === 'TRUNCATED_STREAM' || err.truncatedStream === true)) {
|
|
58
66
|
err.streamStalled = true;
|
|
59
67
|
if (typeof err.partialContent !== 'string') err.partialContent = relayedText;
|
|
60
|
-
if (err.pendingToolUse === undefined) err.pendingToolUse =
|
|
68
|
+
if (err.pendingToolUse === undefined) err.pendingToolUse = pendingTool;
|
|
61
69
|
}
|
|
62
70
|
} catch { /* best-effort */ }
|
|
63
71
|
return err;
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
// True when a streamed Gemini chunk carries a native functionCall part (as
|
|
75
|
+
// opposed to a tool call leaked as plain text, tracked by textLeakGuard).
|
|
76
|
+
export function geminiChunkHasFunctionCall(chunk) {
|
|
77
|
+
const parts = chunk?.candidates?.[0]?.content?.parts;
|
|
78
|
+
if (!Array.isArray(parts)) return false;
|
|
79
|
+
return parts.some((p) => p && p.functionCall);
|
|
80
|
+
}
|
|
81
|
+
|
|
66
82
|
/**
|
|
67
83
|
* Aggregate streamed GenerateContentResponse chunks into one response object
|
|
68
84
|
* (same shape as a non-streaming generateContent JSON body).
|
|
@@ -245,6 +261,7 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
|
|
|
245
261
|
let idleTimer = null;
|
|
246
262
|
let idleReject = null;
|
|
247
263
|
let relayedText = '';
|
|
264
|
+
let sawFunctionCall = false;
|
|
248
265
|
|
|
249
266
|
let firstByteTimer = setTimeout(() => {
|
|
250
267
|
try { reader.cancel('first byte timeout'); } catch {}
|
|
@@ -332,6 +349,7 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
|
|
|
332
349
|
}
|
|
333
350
|
allChunks.push(parsed);
|
|
334
351
|
try { onStreamDelta?.(); } catch {}
|
|
352
|
+
if (!sawFunctionCall && geminiChunkHasFunctionCall(parsed)) sawFunctionCall = true;
|
|
335
353
|
if (onTextDelta || textLeakGuard) {
|
|
336
354
|
const t = geminiChunkText(parsed);
|
|
337
355
|
if (t) relayedText += t;
|
|
@@ -352,6 +370,7 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
|
|
|
352
370
|
}
|
|
353
371
|
allChunks.push(parsed);
|
|
354
372
|
try { onStreamDelta?.(); } catch {}
|
|
373
|
+
if (!sawFunctionCall && geminiChunkHasFunctionCall(parsed)) sawFunctionCall = true;
|
|
355
374
|
if (onTextDelta || textLeakGuard) {
|
|
356
375
|
const t = geminiChunkText(parsed);
|
|
357
376
|
if (t) relayedText += t;
|
|
@@ -362,7 +381,7 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
|
|
|
362
381
|
}
|
|
363
382
|
}
|
|
364
383
|
} catch (err) {
|
|
365
|
-
throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard });
|
|
384
|
+
throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard, sawFunctionCall });
|
|
366
385
|
} finally {
|
|
367
386
|
clearFirstByteTimer();
|
|
368
387
|
if (idleTimer) clearTimeout(idleTimer);
|
|
@@ -380,7 +399,7 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
|
|
|
380
399
|
try {
|
|
381
400
|
assertGeminiStreamCompleted({ sawStreamChunk, finishReason, label });
|
|
382
401
|
} catch (err) {
|
|
383
|
-
throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard });
|
|
402
|
+
throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard, sawFunctionCall });
|
|
384
403
|
}
|
|
385
404
|
return aggregated;
|
|
386
405
|
}
|
|
@@ -393,6 +412,7 @@ export async function consumeGeminiSdkStream(streamResult, { signal, onStreamDel
|
|
|
393
412
|
let firstByteTimer = null;
|
|
394
413
|
let inFlightReject = null;
|
|
395
414
|
let relayedText = '';
|
|
415
|
+
let sawFunctionCall = false;
|
|
396
416
|
|
|
397
417
|
const armFirstByteTimer = () => {
|
|
398
418
|
if (firstByteTimer) clearTimeout(firstByteTimer);
|
|
@@ -504,6 +524,7 @@ export async function consumeGeminiSdkStream(streamResult, { signal, onStreamDel
|
|
|
504
524
|
resetIdleTimer();
|
|
505
525
|
if (step.value) collectedChunks.push(step.value);
|
|
506
526
|
try { onStreamDelta?.(); } catch {}
|
|
527
|
+
if (!sawFunctionCall && geminiChunkHasFunctionCall(step.value)) sawFunctionCall = true;
|
|
507
528
|
if (onTextDelta || textLeakGuard) {
|
|
508
529
|
const t = geminiChunkText(step.value);
|
|
509
530
|
if (t) relayedText += t;
|
|
@@ -519,7 +540,7 @@ export async function consumeGeminiSdkStream(streamResult, { signal, onStreamDel
|
|
|
519
540
|
const reason = signal.reason;
|
|
520
541
|
throw reason instanceof Error ? reason : new Error(`${label} aborted`);
|
|
521
542
|
}
|
|
522
|
-
throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard });
|
|
543
|
+
throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard, sawFunctionCall });
|
|
523
544
|
} finally {
|
|
524
545
|
clearFirstByteTimer();
|
|
525
546
|
if (idleTimer) clearTimeout(idleTimer);
|
|
@@ -559,7 +580,7 @@ export async function consumeGeminiSdkStream(streamResult, { signal, onStreamDel
|
|
|
559
580
|
try {
|
|
560
581
|
assertGeminiStreamCompleted({ sawStreamChunk, finishReason, label });
|
|
561
582
|
} catch (err) {
|
|
562
|
-
throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard });
|
|
583
|
+
throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard, sawFunctionCall });
|
|
563
584
|
}
|
|
564
585
|
return raw;
|
|
565
586
|
}
|
|
@@ -68,7 +68,7 @@ const DEFAULT_MODEL = MODELS[0].id;
|
|
|
68
68
|
const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
69
69
|
// Bump when the on-disk cache shape changes so stale-shape entries are
|
|
70
70
|
// discarded instead of misread.
|
|
71
|
-
const GEMINI_MODEL_CACHE_SCHEMA_VERSION =
|
|
71
|
+
const GEMINI_MODEL_CACHE_SCHEMA_VERSION = 2;
|
|
72
72
|
|
|
73
73
|
// De-dupes concurrent force-refreshes so they share one HTTP round-trip,
|
|
74
74
|
// mirroring anthropic-oauth's _modelRefreshInFlight.
|
|
@@ -155,6 +155,10 @@ export class GeminiProvider {
|
|
|
155
155
|
const cfg = freshConfig.providers?.gemini;
|
|
156
156
|
const newKey = cfg?.apiKey || process.env.GEMINI_API_KEY;
|
|
157
157
|
if (newKey) {
|
|
158
|
+
// Keep this.config in sync so REST/cache paths (which read the
|
|
159
|
+
// key via _getApiKey() → this.config.apiKey) don't keep using
|
|
160
|
+
// the stale key after a rotation; genAI alone is not enough.
|
|
161
|
+
this.config = { ...(this.config || {}), ...(cfg || {}), apiKey: newKey };
|
|
158
162
|
this.genAI = new GoogleGenerativeAI(newKey);
|
|
159
163
|
}
|
|
160
164
|
} catch { /* best effort */ }
|
|
@@ -871,10 +875,11 @@ export class GeminiProvider {
|
|
|
871
875
|
if (!res.ok) throw new Error(`gemini list_models ${res.status}`);
|
|
872
876
|
const data = await res.json();
|
|
873
877
|
const items = Array.isArray(data?.models) ? data.models : [];
|
|
874
|
-
// Filter to Gemini family; skip embedding/imagen
|
|
878
|
+
// Filter to Gemini family; skip embedding/imagen and non-coding SKUs
|
|
879
|
+
// (robotics, computer-use) that the chat picker should never show.
|
|
875
880
|
const normalized = items
|
|
876
881
|
.filter(m => (m?.name || '').includes('gemini'))
|
|
877
|
-
.filter(m => !/embedding|aqa|imagen/.test(m?.name || ''))
|
|
882
|
+
.filter(m => !/embedding|aqa|imagen|robotics|computer-use/.test(m?.name || ''))
|
|
878
883
|
.map(m => {
|
|
879
884
|
const id = (m.name || '').replace(/^models\//, '');
|
|
880
885
|
const family = /flash-lite/.test(id) ? 'gemini-flash-lite'
|
|
@@ -886,8 +891,11 @@ export class GeminiProvider {
|
|
|
886
891
|
display: m.displayName || id,
|
|
887
892
|
family,
|
|
888
893
|
provider: 'gemini',
|
|
889
|
-
|
|
890
|
-
|
|
894
|
+
// No fabricated fallbacks: when the API omits limits leave
|
|
895
|
+
// them null so enrichModels can fill from the catalog and
|
|
896
|
+
// the picker shows no context label instead of a fake 1M.
|
|
897
|
+
contextWindow: m.inputTokenLimit || null,
|
|
898
|
+
outputTokens: m.outputTokenLimit || null,
|
|
891
899
|
tier: 'version',
|
|
892
900
|
latest: false,
|
|
893
901
|
description: m.description || '',
|
|
@@ -250,7 +250,7 @@ function saveTokens(tokens) {
|
|
|
250
250
|
expires_at: tokens.expires_at || 0,
|
|
251
251
|
token_endpoint: tokens.token_endpoint || null,
|
|
252
252
|
user_id: tokens.user_id || tokens.userId || _identityFromAccessToken(tokens.access_token).user_id || undefined,
|
|
253
|
-
}, { lock: true, fsyncDir: true });
|
|
253
|
+
}, { lock: true, fsyncDir: true, mode: 0o600, secret: true });
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
function _scrubTokens(text) {
|
|
@@ -41,6 +41,7 @@ const NON_CHAT_RE = new RegExp(
|
|
|
41
41
|
'dall[-_]?e', 'sora', 'imagen', 'imagine', 'veo', 'kling', 'runway',
|
|
42
42
|
'realtime', 'transcribe', 'transcription', 'diarize',
|
|
43
43
|
'guard', 'safety', 'classifier',
|
|
44
|
+
'robotics', 'computer[-_]?use',
|
|
44
45
|
].join('|') +
|
|
45
46
|
')([-_/\\s]|$)',
|
|
46
47
|
'i',
|
|
@@ -64,6 +65,15 @@ const NON_LLM_ID_PATTERNS = [
|
|
|
64
65
|
/^gpt-audio(\b|-)/i, // gpt-audio, gpt-audio-mini, gpt-audio-1.5
|
|
65
66
|
/^gpt-realtime(\b|-)/i, // gpt-realtime*, gpt-realtime-mini
|
|
66
67
|
/-tts(\b|-)/i, // any *-tts SKU
|
|
68
|
+
// Purpose-specific SKUs that report mode:'chat' but are never useful for a
|
|
69
|
+
// coding agent (embodied/robotics, browser computer-use, audio-native,
|
|
70
|
+
// customtools picker-duplicate variants).
|
|
71
|
+
/(^|-)robotics(\b|-)/i, // gemini-robotics-er-*
|
|
72
|
+
/computer[-_]?use/i, // gemini-2.5-computer-use-preview-*
|
|
73
|
+
/native[-_]?audio/i, // gemini-2.5-flash-native-audio-*
|
|
74
|
+
/-customtools(\b|-|$)/i, // gemini-3.1-pro-preview-customtools
|
|
75
|
+
/^codex-auto-review$/i, // Codex backend auto-review model, not a picker choice
|
|
76
|
+
/^grok-build$/i, // proxy alias duplicating grok-build-0.1 (exact — 0.1 stays)
|
|
67
77
|
];
|
|
68
78
|
|
|
69
79
|
function _isNonLlmId(lid) {
|
|
@@ -89,6 +99,7 @@ const LEGACY_PATTERNS = [
|
|
|
89
99
|
/^claude-(1|2|3|instant)(\b|-|\.)/i,
|
|
90
100
|
// Gemini legacy — gemini-1*, gemini-pro. gemini-2/3 not matched.
|
|
91
101
|
/^gemini-1(\b|-|\.)/i,
|
|
102
|
+
/^gemini-2\.0(\b|-|\.)/i,
|
|
92
103
|
/^gemini-pro(\b|-)/i,
|
|
93
104
|
// Grok legacy — grok-1/2/3 and grok-beta. The digit boundary stops grok-3
|
|
94
105
|
// from catching grok-4.20-beta.
|