diff --git a/README.md b/README.md index b212b749..b5a61337 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ Built in Zig 0.17 dev

+

+ justrach/codegraff | Trendshift +

+

The most token-efficient coding harness we've built so far.
Prompt-cache max keeps repeated prefixes hot. RLM + spec-ptc turns wide work diff --git a/architecture.md b/architecture.md index cf5e82b2..413c4950 100644 --- a/architecture.md +++ b/architecture.md @@ -121,8 +121,9 @@ as codex). ### Streaming Root-agent requests stream on **all three** wire formats. `request()` decides -per call: `live = !sub && out != null && !stream_quiet` (compaction sets -`stream_quiet`; subagents always take the buffered `post()` path). When live, +per call: `live = !sub` (`Agent.usesLiveTransport`). `-p` and compaction set +`out=null` / `stream_quiet` to mute paint, not to leave the stall-watched +path; subagents always take the buffered `postWatched()` path. When live, `buildBody` adds `stream:true` (openai also gets `stream_options:{include_usage:true}` so token counts survive — dropped and retried once if a provider rejects it, same pattern as the soft-strict diff --git a/docs/adr/0045-glm-flash-swe-codegraff.md b/docs/adr/0045-glm-flash-swe-codegraff.md index bd952341..5dac60eb 100644 --- a/docs/adr/0045-glm-flash-swe-codegraff.md +++ b/docs/adr/0045-glm-flash-swe-codegraff.md @@ -45,6 +45,6 @@ completed the same model. Do not steal Pi's heap (167M vs 23–90M). ## Consequences - Fair Codegraff A/B is `--model glm-5.3-flash` with `CODEGRAFF_API_KEY`. -- Next graff cut on this model is finishing inside 300s (fewer / shorter - turns), not a catalog shrink to four tools. +- The next cut is ADR 0046: omit default `reasoning_effort` on flash + and compact lean tool prose. Not a four-tool catalog. - Rotate any `cg_sk_` pasted into a chat; do not commit it. diff --git a/docs/adr/0046-flash-omits-default-effort.md b/docs/adr/0046-flash-omits-default-effort.md new file mode 100644 index 00000000..3e413386 --- /dev/null +++ b/docs/adr/0046-flash-omits-default-effort.md @@ -0,0 +1,107 @@ +# 0046. Flash models omit default `reasoning_effort` + +Status: accepted 2026-08-29 + +## Context + +ADR 0045 ran Codegraff `glm-5.3-flash` SWE after the `-p` learn skip. +Pi 5/6 in 758s (`first` 0.6s). Graff 3/6 in 1353s (`first` 6.9s) with +three 300s timeouts that never edited. Output on the tasks that +finished was 3.5–7.5k tokens. `validated` was 10 calls / 164s vs Pi +5 / 26s. + +Pi's `first` is the JSONL `session` line, not first model work — do +not treat 0.6s as a TUI steal. The real gap is that Pi's +`models.json` sets `reasoning: false` / `supportsReasoningEffort: +false`. Graff's codegraff spec has `takes_effort = true`, so an +uncatalogued flash model inherited **default medium** +`reasoning_effort` on every request. + +That is the same footgun Gemini already had: default medium maps to +seconds of thinking (2× wall vs Pi). The Gemini skip was +`startsWith("gemini")` only, so `glm-5.3-flash` still thought. + +The lean keep-list is already 8 tools (ADR 0024: an rlm-only catalog +failed). Bash / read / edit / codedb / attempt_completion still shipped +interactive-length essays on every `-p` turn. + +## Decision + +- Flash / Gemini default `.medium` is sent as **`low`**, not omitted. + Omitting the field still bills `reasoning_tokens` (27 on a `pong`; + SWE dumps 1.9MB `reasoning_content`). `low` bills 0. `none` is a + 400. `/effort high` still sends high. +- Catalog `glm-5.3-flash` on codegraff (chat completions, 202752 ctx). + Do not pass `glm-5.3` (that row is zai). +- Compact lean one-shot **descriptions** only (same names and JSON + schemas). Do not drop to four tools. Do not steal Pi's heap. +- Root `-p` uses the streaming transport (`usesLiveTransport` = `!sub`). + `out=null` / `stream_quiet` mute paint only. The 5-minute + `postWatched` deadline is for subagents, not one-shots. + +## Consequences + +- Flash SWE wall is generation without thinking novels, not Zig vs JS. +- `/effort` remains the only way effort moves (ADR / `effort_route`: + no auto-flip). +- `-p` writes `calling ` to stderr at start and drops the + call-2 stdout pulse. Eval `first_out` is no longer "time to model + call 2"; stdout stays the answer. +- Revisit if a named flash model needs default thinking — pin it in + the catalog, do not restore a global medium default. + +## Confirm (2026-08-29, after this revision) + +`run-20260829-041235.jsonl`, Codegraff `glm-5.3-flash`, `--suite swe +-j 6`. No `reasoning_effort` on the wire. Local boot is 7ms; a +`pong` one-shot is 6.1s / 10 out (gateway TTFT). Pi `first` 0.6s is +still the JSONL `session` line. + +| | pass | wall | first | out | +|---|---:|---:|---:|---:| +| ADR 0045 | 3/6 | 1353s | 6.9s | 15k | +| after (thinking off) | 3/6 | 1247s | 6.7s* | 21k | + +\*first was the call-2 stdout pulse. `validated` 164s / 46k / 10 → +**57s / 11k / 4**. `json-stream` and `cookie-store` still SIGKILL at +300s after that pulse: the second model call took `postWatched` +(~250s of silence, then `retry` / 5xx) because `-p` gated `live` on +`out != null && !stream_quiet`. Root one-shots now `postLive` so the +30s head-stall fires. `label-sort` finished (174s) but still fails +the hidden check. Do not steal Pi's heap. + +## Confirm (2026-08-29, after streaming `-p`) + +`run-20260829-043927.jsonl`, Codegraff `glm-5.3-flash`, `--suite swe +-j 6`. Root one-shots `postLive`. Eval `first` is the stderr `calling` +line (~0.03s), not gateway TTFT. + +| | pass | wall | notes | +|---|---:|---:|---| +| ADR 0045 | 3/6 | 1353s | thinking on; 3× 300s `postWatched` | +| thinking off | 3/6 | 1247s | still `postWatched` | +| **this cut** | **4/6** | **1311s** | 26 calls / 157k in / 29k out | + +`json-stream` **passed** in 280s (was SIGKILL at 300s after a silent +second `postWatched`). `validated` 106s / 6 calls. `map-conflict` +116s. `config-parse` 208s. + +`cookie-store` still SIGKILL at 300s: call 2 traced `first_token` at +11.5s then never completed — a live stream, not the 5-minute watched +POST. `label-sort` was mid-turn (call 2 was 219s / 1.9MB SSE) when +the cap hit. Do not steal Pi's heap. Do not shrink the keep-list. + +## Confirm (2026-08-29, `reasoning_effort=low`) + +`run-20260829-045547.jsonl`. Gateway A/B: omit = 27 reasoning_tokens +on `pong`; `low` = 0; `thinking.disabled` still thinks; `none` is 400. + +| | pass | wall (sum) | out | calls | +|---|---:|---:|---:|---:| +| Pi (ADR 0045) | 5/6 | 758s | 27k | 35 | +| graff omit | 4/6 | 1311s | 29k | 26 | +| **graff `low`** | **5/6** | **286s** | **4.7k** | 38 | + +Same miss as Pi (`label-sort` hidden check). `cookie-store` 115s +(was 300s SIGKILL; Pi 301s). `json-stream` 36s (was 280s; Pi 117s). +Do not steal Pi's heap. Do not shrink the keep-list. diff --git a/docs/adr/README.md b/docs/adr/README.md index a2e1e637..73bd4603 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -56,6 +56,7 @@ record only when you need the evidence or the edge cases. | [0043](0043-pi-swe-same-seat.md) | Pi SWE A/B uses `pi-xai` on the SuperGrok seat; do not steal Pi's catalog or heap from the json-stream pass. | | [0044](0044-oneshot-skips-learn-auto.md) | `-p` and `--json` skip learn auto-init; the Pi SWE wall gap was a 38s `graff-pinned` copy, not their catalog. | | [0045](0045-glm-flash-swe-codegraff.md) | Codegraff `glm-5.3-flash` SWE: Pi 5/6 in 758s, graff 3/6 with three 300s timeouts; do not steal Pi's heap. | +| [0046](0046-flash-omits-default-effort.md) | Flash / Gemini send `reasoning_effort=low` (omit still thinks); lean `-p` shortens tool prose; `-p` streams. | ## When to write one diff --git a/examples/edge-worker/src/remote.ts b/examples/edge-worker/src/remote.ts index 4867205e..230920bb 100644 --- a/examples/edge-worker/src/remote.ts +++ b/examples/edge-worker/src/remote.ts @@ -9,7 +9,7 @@ export const HARNESS_VERSION = "0.12"; -export type ModelName = "MiniMax-M2.5" | "MiniMax-M2.7" | "MiniMax-M3" | "accounts/fireworks/models/deepseek-v4-flash" | "accounts/fireworks/models/deepseek-v4-pro" | "accounts/fireworks/models/glm-5p2" | "accounts/fireworks/models/gpt-oss-120b" | "accounts/fireworks/models/kimi-k2p6" | "accounts/fireworks/models/kimi-k2p7-code" | "accounts/fireworks/models/minimax-m3" | "accounts/fireworks/models/qwen3p7-plus" | "alibaba/qwen3.8-27b" | "anthropic/claude-sonnet-4.6" | "claude-fable-5" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6" | "claude-opus-4-7" | "claude-opus-4-8" | "claude-opus-4.8" | "claude-opus-5" | "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-sonnet-4.6" | "claude-sonnet-5" | "deepseek-chat" | "deepseek-reasoner" | "deepseek-v4-flash" | "deepseek-v4-pro" | "fugu" | "fugu-ultra" | "fugu-ultra-20260615" | "gemma-4-31b" | "glm-4.5" | "glm-4.7" | "glm-5" | "glm-5-turbo" | "glm-5.2" | "glm-5.3" | "glm-5v-turbo" | "gpt-5-codex" | "gpt-5.2" | "gpt-5.3-codex-spark" | "gpt-5.4" | "gpt-5.4-mini" | "gpt-5.4-pro" | "gpt-5.5" | "gpt-5.6" | "gpt-5.6-luna" | "gpt-5.6-sol" | "gpt-5.6-terra" | "gpt-oss-120b" | "grok-4.3" | "grok-4.6" | "grok-build" | "k3" | "kilo-auto/small" | "kimi-for-coding" | "kimi-for-coding-highspeed" | "kimi-k2.6" | "kimi-latest" | "lmstudio" | "mimo-v2-flash" | "mimo-v2.5" | "mimo-v2.5-pro" | "mimo-v2.5-pro-ultraspeed" | "minimax-m3" | "mistral-medium-latest" | "mlx-community/Qwen3.6-27B-OptiQ-4bit" | "openai/gpt-oss-120b" | (string & {}); +export type ModelName = "MiniMax-M2.5" | "MiniMax-M2.7" | "MiniMax-M3" | "accounts/fireworks/models/deepseek-v4-flash" | "accounts/fireworks/models/deepseek-v4-pro" | "accounts/fireworks/models/glm-5p2" | "accounts/fireworks/models/gpt-oss-120b" | "accounts/fireworks/models/kimi-k2p6" | "accounts/fireworks/models/kimi-k2p7-code" | "accounts/fireworks/models/minimax-m3" | "accounts/fireworks/models/qwen3p7-plus" | "alibaba/qwen3.8-27b" | "anthropic/claude-sonnet-4.6" | "claude-fable-5" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6" | "claude-opus-4-7" | "claude-opus-4-8" | "claude-opus-4.8" | "claude-opus-5" | "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-sonnet-4.6" | "claude-sonnet-5" | "deepseek-chat" | "deepseek-reasoner" | "deepseek-v4-flash" | "deepseek-v4-pro" | "fugu" | "fugu-ultra" | "fugu-ultra-20260615" | "gemma-4-31b" | "glm-4.5" | "glm-4.7" | "glm-5" | "glm-5-turbo" | "glm-5.2" | "glm-5.3" | "glm-5.3-flash" | "glm-5v-turbo" | "gpt-5-codex" | "gpt-5.2" | "gpt-5.3-codex-spark" | "gpt-5.4" | "gpt-5.4-mini" | "gpt-5.4-pro" | "gpt-5.5" | "gpt-5.6" | "gpt-5.6-luna" | "gpt-5.6-sol" | "gpt-5.6-terra" | "gpt-oss-120b" | "grok-4.3" | "grok-4.6" | "grok-build" | "k3" | "kilo-auto/small" | "kimi-for-coding" | "kimi-for-coding-highspeed" | "kimi-k2.6" | "kimi-latest" | "lmstudio" | "mimo-v2-flash" | "mimo-v2.5" | "mimo-v2.5-pro" | "mimo-v2.5-pro-ultraspeed" | "minimax-m3" | "mistral-medium-latest" | "mlx-community/Qwen3.6-27B-OptiQ-4bit" | "openai/gpt-oss-120b" | (string & {}); export type ToolName = "bash" | "bash_output" | "bash_kill" | "read_file" | "edit_file" | "write_file" | "webfetch" | "skill" | "codedb" | "read_tool_result" | "todo_write" | "todo_read" | "eval" | "note_constraint" | "ask_user" | "attempt_completion" | "load_tool_schemas" | "mcp_search_tools" | "mcp_select_tool" | "clock_sleep" | "subagent" | "workflow" | "agent_output" | "learn_candidate" | "peer_message" | "workspace" | "imagegen"; export type ProviderId = "anthropic" | "codegraff" | "deepseek" | "openai" | "minimax" | "xiaomi" | "kilo" | "groq" | "cerebras" | "mistral" | "kimi" | "moonshot" | "xai" | "zai" | "vercel" | "openrouter" | "fugu" | "fireworks" | "mlx" | "lmstudio" | "codex"; @@ -521,5 +521,5 @@ export async function* runAgentRemote(opts: RunAgentRemoteOptions): AsyncGenerat } } -export const MODELS: ModelName[] = ["MiniMax-M2.5", "MiniMax-M2.7", "MiniMax-M3", "accounts/fireworks/models/deepseek-v4-flash", "accounts/fireworks/models/deepseek-v4-pro", "accounts/fireworks/models/glm-5p2", "accounts/fireworks/models/gpt-oss-120b", "accounts/fireworks/models/kimi-k2p6", "accounts/fireworks/models/kimi-k2p7-code", "accounts/fireworks/models/minimax-m3", "accounts/fireworks/models/qwen3p7-plus", "alibaba/qwen3.8-27b", "anthropic/claude-sonnet-4.6", "claude-fable-5", "claude-haiku-4-5", "claude-opus-4-5", "claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8", "claude-opus-4.8", "claude-opus-5", "claude-sonnet-4-5", "claude-sonnet-4-6", "claude-sonnet-4.6", "claude-sonnet-5", "deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash", "deepseek-v4-pro", "fugu", "fugu-ultra", "fugu-ultra-20260615", "gemma-4-31b", "glm-4.5", "glm-4.7", "glm-5", "glm-5-turbo", "glm-5.2", "glm-5.3", "glm-5v-turbo", "gpt-5-codex", "gpt-5.2", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-pro", "gpt-5.5", "gpt-5.6", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-oss-120b", "grok-4.3", "grok-4.6", "grok-build", "k3", "kilo-auto/small", "kimi-for-coding", "kimi-for-coding-highspeed", "kimi-k2.6", "kimi-latest", "lmstudio", "mimo-v2-flash", "mimo-v2.5", "mimo-v2.5-pro", "mimo-v2.5-pro-ultraspeed", "minimax-m3", "mistral-medium-latest", "mlx-community/Qwen3.6-27B-OptiQ-4bit", "openai/gpt-oss-120b"]; +export const MODELS: ModelName[] = ["MiniMax-M2.5", "MiniMax-M2.7", "MiniMax-M3", "accounts/fireworks/models/deepseek-v4-flash", "accounts/fireworks/models/deepseek-v4-pro", "accounts/fireworks/models/glm-5p2", "accounts/fireworks/models/gpt-oss-120b", "accounts/fireworks/models/kimi-k2p6", "accounts/fireworks/models/kimi-k2p7-code", "accounts/fireworks/models/minimax-m3", "accounts/fireworks/models/qwen3p7-plus", "alibaba/qwen3.8-27b", "anthropic/claude-sonnet-4.6", "claude-fable-5", "claude-haiku-4-5", "claude-opus-4-5", "claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8", "claude-opus-4.8", "claude-opus-5", "claude-sonnet-4-5", "claude-sonnet-4-6", "claude-sonnet-4.6", "claude-sonnet-5", "deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash", "deepseek-v4-pro", "fugu", "fugu-ultra", "fugu-ultra-20260615", "gemma-4-31b", "glm-4.5", "glm-4.7", "glm-5", "glm-5-turbo", "glm-5.2", "glm-5.3", "glm-5.3-flash", "glm-5v-turbo", "gpt-5-codex", "gpt-5.2", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-pro", "gpt-5.5", "gpt-5.6", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-oss-120b", "grok-4.3", "grok-4.6", "grok-build", "k3", "kilo-auto/small", "kimi-for-coding", "kimi-for-coding-highspeed", "kimi-k2.6", "kimi-latest", "lmstudio", "mimo-v2-flash", "mimo-v2.5", "mimo-v2.5-pro", "mimo-v2.5-pro-ultraspeed", "minimax-m3", "mistral-medium-latest", "mlx-community/Qwen3.6-27B-OptiQ-4bit", "openai/gpt-oss-120b"]; export const TOOLS: ToolName[] = ["bash", "bash_output", "bash_kill", "read_file", "edit_file", "write_file", "webfetch", "skill", "codedb", "read_tool_result", "todo_write", "todo_read", "eval", "note_constraint", "ask_user", "attempt_completion", "load_tool_schemas", "mcp_search_tools", "mcp_select_tool", "clock_sleep", "subagent", "workflow", "agent_output", "learn_candidate", "peer_message", "workspace", "imagegen"]; diff --git a/graff-evals/harnesses.json b/graff-evals/harnesses.json index 9e8042a7..c09d7c3e 100644 --- a/graff-evals/harnesses.json +++ b/graff-evals/harnesses.json @@ -22,6 +22,7 @@ "usage": "graff-stderr", "capabilities": ["output-schema"], "schema_args": ["--output-schema", "{schema}"], + "env": {"GRAFF_POST_DEADLINE_SECS": "90"}, "default_model": "grok-4.6" }, "graff-dev-rlm": { diff --git a/sdk/py/harness_sdk.py b/sdk/py/harness_sdk.py index 623dd78d..a8272b03 100644 --- a/sdk/py/harness_sdk.py +++ b/sdk/py/harness_sdk.py @@ -13,7 +13,7 @@ import urllib.request from typing import Iterator, List, Literal, Optional, TypedDict, Union -MODELS = ["MiniMax-M2.5", "MiniMax-M2.7", "MiniMax-M3", "accounts/fireworks/models/deepseek-v4-flash", "accounts/fireworks/models/deepseek-v4-pro", "accounts/fireworks/models/glm-5p2", "accounts/fireworks/models/gpt-oss-120b", "accounts/fireworks/models/kimi-k2p6", "accounts/fireworks/models/kimi-k2p7-code", "accounts/fireworks/models/minimax-m3", "accounts/fireworks/models/qwen3p7-plus", "alibaba/qwen3.8-27b", "anthropic/claude-sonnet-4.6", "claude-fable-5", "claude-haiku-4-5", "claude-opus-4-5", "claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8", "claude-opus-4.8", "claude-opus-5", "claude-sonnet-4-5", "claude-sonnet-4-6", "claude-sonnet-4.6", "claude-sonnet-5", "deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash", "deepseek-v4-pro", "fugu", "fugu-ultra", "fugu-ultra-20260615", "gemma-4-31b", "glm-4.5", "glm-4.7", "glm-5", "glm-5-turbo", "glm-5.2", "glm-5.3", "glm-5v-turbo", "gpt-5-codex", "gpt-5.2", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-pro", "gpt-5.5", "gpt-5.6", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-oss-120b", "grok-4.3", "grok-4.6", "grok-build", "k3", "kilo-auto/small", "kimi-for-coding", "kimi-for-coding-highspeed", "kimi-k2.6", "kimi-latest", "lmstudio", "mimo-v2-flash", "mimo-v2.5", "mimo-v2.5-pro", "mimo-v2.5-pro-ultraspeed", "minimax-m3", "mistral-medium-latest", "mlx-community/Qwen3.6-27B-OptiQ-4bit", "openai/gpt-oss-120b"] +MODELS = ["MiniMax-M2.5", "MiniMax-M2.7", "MiniMax-M3", "accounts/fireworks/models/deepseek-v4-flash", "accounts/fireworks/models/deepseek-v4-pro", "accounts/fireworks/models/glm-5p2", "accounts/fireworks/models/gpt-oss-120b", "accounts/fireworks/models/kimi-k2p6", "accounts/fireworks/models/kimi-k2p7-code", "accounts/fireworks/models/minimax-m3", "accounts/fireworks/models/qwen3p7-plus", "alibaba/qwen3.8-27b", "anthropic/claude-sonnet-4.6", "claude-fable-5", "claude-haiku-4-5", "claude-opus-4-5", "claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8", "claude-opus-4.8", "claude-opus-5", "claude-sonnet-4-5", "claude-sonnet-4-6", "claude-sonnet-4.6", "claude-sonnet-5", "deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash", "deepseek-v4-pro", "fugu", "fugu-ultra", "fugu-ultra-20260615", "gemma-4-31b", "glm-4.5", "glm-4.7", "glm-5", "glm-5-turbo", "glm-5.2", "glm-5.3", "glm-5.3-flash", "glm-5v-turbo", "gpt-5-codex", "gpt-5.2", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-pro", "gpt-5.5", "gpt-5.6", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-oss-120b", "grok-4.3", "grok-4.6", "grok-build", "k3", "kilo-auto/small", "kimi-for-coding", "kimi-for-coding-highspeed", "kimi-k2.6", "kimi-latest", "lmstudio", "mimo-v2-flash", "mimo-v2.5", "mimo-v2.5-pro", "mimo-v2.5-pro-ultraspeed", "minimax-m3", "mistral-medium-latest", "mlx-community/Qwen3.6-27B-OptiQ-4bit", "openai/gpt-oss-120b"] TOOLS = ["bash", "bash_output", "bash_kill", "read_file", "edit_file", "write_file", "webfetch", "skill", "codedb", "read_tool_result", "todo_write", "todo_read", "eval", "note_constraint", "ask_user", "attempt_completion", "load_tool_schemas", "mcp_search_tools", "mcp_select_tool", "clock_sleep", "subagent", "workflow", "agent_output", "learn_candidate", "peer_message", "workspace", "imagegen"] PROVIDERS = ["anthropic", "codegraff", "deepseek", "openai", "minimax", "xiaomi", "kilo", "groq", "cerebras", "mistral", "kimi", "moonshot", "xai", "zai", "vercel", "openrouter", "fugu", "fireworks", "mlx", "lmstudio", "codex"] diff --git a/sdk/ts/harness.ts b/sdk/ts/harness.ts index 82d9aeaa..f75864c4 100644 --- a/sdk/ts/harness.ts +++ b/sdk/ts/harness.ts @@ -11,7 +11,7 @@ import type { Readable, Writable } from "node:stream"; export const HARNESS_VERSION = "0.12"; -export type ModelName = "MiniMax-M2.5" | "MiniMax-M2.7" | "MiniMax-M3" | "accounts/fireworks/models/deepseek-v4-flash" | "accounts/fireworks/models/deepseek-v4-pro" | "accounts/fireworks/models/glm-5p2" | "accounts/fireworks/models/gpt-oss-120b" | "accounts/fireworks/models/kimi-k2p6" | "accounts/fireworks/models/kimi-k2p7-code" | "accounts/fireworks/models/minimax-m3" | "accounts/fireworks/models/qwen3p7-plus" | "alibaba/qwen3.8-27b" | "anthropic/claude-sonnet-4.6" | "claude-fable-5" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6" | "claude-opus-4-7" | "claude-opus-4-8" | "claude-opus-4.8" | "claude-opus-5" | "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-sonnet-4.6" | "claude-sonnet-5" | "deepseek-chat" | "deepseek-reasoner" | "deepseek-v4-flash" | "deepseek-v4-pro" | "fugu" | "fugu-ultra" | "fugu-ultra-20260615" | "gemma-4-31b" | "glm-4.5" | "glm-4.7" | "glm-5" | "glm-5-turbo" | "glm-5.2" | "glm-5.3" | "glm-5v-turbo" | "gpt-5-codex" | "gpt-5.2" | "gpt-5.3-codex-spark" | "gpt-5.4" | "gpt-5.4-mini" | "gpt-5.4-pro" | "gpt-5.5" | "gpt-5.6" | "gpt-5.6-luna" | "gpt-5.6-sol" | "gpt-5.6-terra" | "gpt-oss-120b" | "grok-4.3" | "grok-4.6" | "grok-build" | "k3" | "kilo-auto/small" | "kimi-for-coding" | "kimi-for-coding-highspeed" | "kimi-k2.6" | "kimi-latest" | "lmstudio" | "mimo-v2-flash" | "mimo-v2.5" | "mimo-v2.5-pro" | "mimo-v2.5-pro-ultraspeed" | "minimax-m3" | "mistral-medium-latest" | "mlx-community/Qwen3.6-27B-OptiQ-4bit" | "openai/gpt-oss-120b" | (string & {}); +export type ModelName = "MiniMax-M2.5" | "MiniMax-M2.7" | "MiniMax-M3" | "accounts/fireworks/models/deepseek-v4-flash" | "accounts/fireworks/models/deepseek-v4-pro" | "accounts/fireworks/models/glm-5p2" | "accounts/fireworks/models/gpt-oss-120b" | "accounts/fireworks/models/kimi-k2p6" | "accounts/fireworks/models/kimi-k2p7-code" | "accounts/fireworks/models/minimax-m3" | "accounts/fireworks/models/qwen3p7-plus" | "alibaba/qwen3.8-27b" | "anthropic/claude-sonnet-4.6" | "claude-fable-5" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6" | "claude-opus-4-7" | "claude-opus-4-8" | "claude-opus-4.8" | "claude-opus-5" | "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-sonnet-4.6" | "claude-sonnet-5" | "deepseek-chat" | "deepseek-reasoner" | "deepseek-v4-flash" | "deepseek-v4-pro" | "fugu" | "fugu-ultra" | "fugu-ultra-20260615" | "gemma-4-31b" | "glm-4.5" | "glm-4.7" | "glm-5" | "glm-5-turbo" | "glm-5.2" | "glm-5.3" | "glm-5.3-flash" | "glm-5v-turbo" | "gpt-5-codex" | "gpt-5.2" | "gpt-5.3-codex-spark" | "gpt-5.4" | "gpt-5.4-mini" | "gpt-5.4-pro" | "gpt-5.5" | "gpt-5.6" | "gpt-5.6-luna" | "gpt-5.6-sol" | "gpt-5.6-terra" | "gpt-oss-120b" | "grok-4.3" | "grok-4.6" | "grok-build" | "k3" | "kilo-auto/small" | "kimi-for-coding" | "kimi-for-coding-highspeed" | "kimi-k2.6" | "kimi-latest" | "lmstudio" | "mimo-v2-flash" | "mimo-v2.5" | "mimo-v2.5-pro" | "mimo-v2.5-pro-ultraspeed" | "minimax-m3" | "mistral-medium-latest" | "mlx-community/Qwen3.6-27B-OptiQ-4bit" | "openai/gpt-oss-120b" | (string & {}); export type ToolName = "bash" | "bash_output" | "bash_kill" | "read_file" | "edit_file" | "write_file" | "webfetch" | "skill" | "codedb" | "read_tool_result" | "todo_write" | "todo_read" | "eval" | "note_constraint" | "ask_user" | "attempt_completion" | "load_tool_schemas" | "mcp_search_tools" | "mcp_select_tool" | "clock_sleep" | "subagent" | "workflow" | "agent_output" | "learn_candidate" | "peer_message" | "workspace" | "imagegen"; export type ProviderId = "anthropic" | "codegraff" | "deepseek" | "openai" | "minimax" | "xiaomi" | "kilo" | "groq" | "cerebras" | "mistral" | "kimi" | "moonshot" | "xai" | "zai" | "vercel" | "openrouter" | "fugu" | "fireworks" | "mlx" | "lmstudio" | "codex"; @@ -674,5 +674,5 @@ export async function* runAgent(opts: RunAgentOptions): AsyncGenerator { } } -export const MODELS: ModelName[] = ["MiniMax-M2.5", "MiniMax-M2.7", "MiniMax-M3", "accounts/fireworks/models/deepseek-v4-flash", "accounts/fireworks/models/deepseek-v4-pro", "accounts/fireworks/models/glm-5p2", "accounts/fireworks/models/gpt-oss-120b", "accounts/fireworks/models/kimi-k2p6", "accounts/fireworks/models/kimi-k2p7-code", "accounts/fireworks/models/minimax-m3", "accounts/fireworks/models/qwen3p7-plus", "alibaba/qwen3.8-27b", "anthropic/claude-sonnet-4.6", "claude-fable-5", "claude-haiku-4-5", "claude-opus-4-5", "claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8", "claude-opus-4.8", "claude-opus-5", "claude-sonnet-4-5", "claude-sonnet-4-6", "claude-sonnet-4.6", "claude-sonnet-5", "deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash", "deepseek-v4-pro", "fugu", "fugu-ultra", "fugu-ultra-20260615", "gemma-4-31b", "glm-4.5", "glm-4.7", "glm-5", "glm-5-turbo", "glm-5.2", "glm-5.3", "glm-5v-turbo", "gpt-5-codex", "gpt-5.2", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-pro", "gpt-5.5", "gpt-5.6", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-oss-120b", "grok-4.3", "grok-4.6", "grok-build", "k3", "kilo-auto/small", "kimi-for-coding", "kimi-for-coding-highspeed", "kimi-k2.6", "kimi-latest", "lmstudio", "mimo-v2-flash", "mimo-v2.5", "mimo-v2.5-pro", "mimo-v2.5-pro-ultraspeed", "minimax-m3", "mistral-medium-latest", "mlx-community/Qwen3.6-27B-OptiQ-4bit", "openai/gpt-oss-120b"]; +export const MODELS: ModelName[] = ["MiniMax-M2.5", "MiniMax-M2.7", "MiniMax-M3", "accounts/fireworks/models/deepseek-v4-flash", "accounts/fireworks/models/deepseek-v4-pro", "accounts/fireworks/models/glm-5p2", "accounts/fireworks/models/gpt-oss-120b", "accounts/fireworks/models/kimi-k2p6", "accounts/fireworks/models/kimi-k2p7-code", "accounts/fireworks/models/minimax-m3", "accounts/fireworks/models/qwen3p7-plus", "alibaba/qwen3.8-27b", "anthropic/claude-sonnet-4.6", "claude-fable-5", "claude-haiku-4-5", "claude-opus-4-5", "claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8", "claude-opus-4.8", "claude-opus-5", "claude-sonnet-4-5", "claude-sonnet-4-6", "claude-sonnet-4.6", "claude-sonnet-5", "deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash", "deepseek-v4-pro", "fugu", "fugu-ultra", "fugu-ultra-20260615", "gemma-4-31b", "glm-4.5", "glm-4.7", "glm-5", "glm-5-turbo", "glm-5.2", "glm-5.3", "glm-5.3-flash", "glm-5v-turbo", "gpt-5-codex", "gpt-5.2", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-pro", "gpt-5.5", "gpt-5.6", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-oss-120b", "grok-4.3", "grok-4.6", "grok-build", "k3", "kilo-auto/small", "kimi-for-coding", "kimi-for-coding-highspeed", "kimi-k2.6", "kimi-latest", "lmstudio", "mimo-v2-flash", "mimo-v2.5", "mimo-v2.5-pro", "mimo-v2.5-pro-ultraspeed", "minimax-m3", "mistral-medium-latest", "mlx-community/Qwen3.6-27B-OptiQ-4bit", "openai/gpt-oss-120b"]; export const TOOLS: ToolName[] = ["bash", "bash_output", "bash_kill", "read_file", "edit_file", "write_file", "webfetch", "skill", "codedb", "read_tool_result", "todo_write", "todo_read", "eval", "note_constraint", "ask_user", "attempt_completion", "load_tool_schemas", "mcp_search_tools", "mcp_select_tool", "clock_sleep", "subagent", "workflow", "agent_output", "learn_candidate", "peer_message", "workspace", "imagegen"]; diff --git a/sdk/ts/remote.ts b/sdk/ts/remote.ts index 4867205e..230920bb 100644 --- a/sdk/ts/remote.ts +++ b/sdk/ts/remote.ts @@ -9,7 +9,7 @@ export const HARNESS_VERSION = "0.12"; -export type ModelName = "MiniMax-M2.5" | "MiniMax-M2.7" | "MiniMax-M3" | "accounts/fireworks/models/deepseek-v4-flash" | "accounts/fireworks/models/deepseek-v4-pro" | "accounts/fireworks/models/glm-5p2" | "accounts/fireworks/models/gpt-oss-120b" | "accounts/fireworks/models/kimi-k2p6" | "accounts/fireworks/models/kimi-k2p7-code" | "accounts/fireworks/models/minimax-m3" | "accounts/fireworks/models/qwen3p7-plus" | "alibaba/qwen3.8-27b" | "anthropic/claude-sonnet-4.6" | "claude-fable-5" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6" | "claude-opus-4-7" | "claude-opus-4-8" | "claude-opus-4.8" | "claude-opus-5" | "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-sonnet-4.6" | "claude-sonnet-5" | "deepseek-chat" | "deepseek-reasoner" | "deepseek-v4-flash" | "deepseek-v4-pro" | "fugu" | "fugu-ultra" | "fugu-ultra-20260615" | "gemma-4-31b" | "glm-4.5" | "glm-4.7" | "glm-5" | "glm-5-turbo" | "glm-5.2" | "glm-5.3" | "glm-5v-turbo" | "gpt-5-codex" | "gpt-5.2" | "gpt-5.3-codex-spark" | "gpt-5.4" | "gpt-5.4-mini" | "gpt-5.4-pro" | "gpt-5.5" | "gpt-5.6" | "gpt-5.6-luna" | "gpt-5.6-sol" | "gpt-5.6-terra" | "gpt-oss-120b" | "grok-4.3" | "grok-4.6" | "grok-build" | "k3" | "kilo-auto/small" | "kimi-for-coding" | "kimi-for-coding-highspeed" | "kimi-k2.6" | "kimi-latest" | "lmstudio" | "mimo-v2-flash" | "mimo-v2.5" | "mimo-v2.5-pro" | "mimo-v2.5-pro-ultraspeed" | "minimax-m3" | "mistral-medium-latest" | "mlx-community/Qwen3.6-27B-OptiQ-4bit" | "openai/gpt-oss-120b" | (string & {}); +export type ModelName = "MiniMax-M2.5" | "MiniMax-M2.7" | "MiniMax-M3" | "accounts/fireworks/models/deepseek-v4-flash" | "accounts/fireworks/models/deepseek-v4-pro" | "accounts/fireworks/models/glm-5p2" | "accounts/fireworks/models/gpt-oss-120b" | "accounts/fireworks/models/kimi-k2p6" | "accounts/fireworks/models/kimi-k2p7-code" | "accounts/fireworks/models/minimax-m3" | "accounts/fireworks/models/qwen3p7-plus" | "alibaba/qwen3.8-27b" | "anthropic/claude-sonnet-4.6" | "claude-fable-5" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6" | "claude-opus-4-7" | "claude-opus-4-8" | "claude-opus-4.8" | "claude-opus-5" | "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-sonnet-4.6" | "claude-sonnet-5" | "deepseek-chat" | "deepseek-reasoner" | "deepseek-v4-flash" | "deepseek-v4-pro" | "fugu" | "fugu-ultra" | "fugu-ultra-20260615" | "gemma-4-31b" | "glm-4.5" | "glm-4.7" | "glm-5" | "glm-5-turbo" | "glm-5.2" | "glm-5.3" | "glm-5.3-flash" | "glm-5v-turbo" | "gpt-5-codex" | "gpt-5.2" | "gpt-5.3-codex-spark" | "gpt-5.4" | "gpt-5.4-mini" | "gpt-5.4-pro" | "gpt-5.5" | "gpt-5.6" | "gpt-5.6-luna" | "gpt-5.6-sol" | "gpt-5.6-terra" | "gpt-oss-120b" | "grok-4.3" | "grok-4.6" | "grok-build" | "k3" | "kilo-auto/small" | "kimi-for-coding" | "kimi-for-coding-highspeed" | "kimi-k2.6" | "kimi-latest" | "lmstudio" | "mimo-v2-flash" | "mimo-v2.5" | "mimo-v2.5-pro" | "mimo-v2.5-pro-ultraspeed" | "minimax-m3" | "mistral-medium-latest" | "mlx-community/Qwen3.6-27B-OptiQ-4bit" | "openai/gpt-oss-120b" | (string & {}); export type ToolName = "bash" | "bash_output" | "bash_kill" | "read_file" | "edit_file" | "write_file" | "webfetch" | "skill" | "codedb" | "read_tool_result" | "todo_write" | "todo_read" | "eval" | "note_constraint" | "ask_user" | "attempt_completion" | "load_tool_schemas" | "mcp_search_tools" | "mcp_select_tool" | "clock_sleep" | "subagent" | "workflow" | "agent_output" | "learn_candidate" | "peer_message" | "workspace" | "imagegen"; export type ProviderId = "anthropic" | "codegraff" | "deepseek" | "openai" | "minimax" | "xiaomi" | "kilo" | "groq" | "cerebras" | "mistral" | "kimi" | "moonshot" | "xai" | "zai" | "vercel" | "openrouter" | "fugu" | "fireworks" | "mlx" | "lmstudio" | "codex"; @@ -521,5 +521,5 @@ export async function* runAgentRemote(opts: RunAgentRemoteOptions): AsyncGenerat } } -export const MODELS: ModelName[] = ["MiniMax-M2.5", "MiniMax-M2.7", "MiniMax-M3", "accounts/fireworks/models/deepseek-v4-flash", "accounts/fireworks/models/deepseek-v4-pro", "accounts/fireworks/models/glm-5p2", "accounts/fireworks/models/gpt-oss-120b", "accounts/fireworks/models/kimi-k2p6", "accounts/fireworks/models/kimi-k2p7-code", "accounts/fireworks/models/minimax-m3", "accounts/fireworks/models/qwen3p7-plus", "alibaba/qwen3.8-27b", "anthropic/claude-sonnet-4.6", "claude-fable-5", "claude-haiku-4-5", "claude-opus-4-5", "claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8", "claude-opus-4.8", "claude-opus-5", "claude-sonnet-4-5", "claude-sonnet-4-6", "claude-sonnet-4.6", "claude-sonnet-5", "deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash", "deepseek-v4-pro", "fugu", "fugu-ultra", "fugu-ultra-20260615", "gemma-4-31b", "glm-4.5", "glm-4.7", "glm-5", "glm-5-turbo", "glm-5.2", "glm-5.3", "glm-5v-turbo", "gpt-5-codex", "gpt-5.2", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-pro", "gpt-5.5", "gpt-5.6", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-oss-120b", "grok-4.3", "grok-4.6", "grok-build", "k3", "kilo-auto/small", "kimi-for-coding", "kimi-for-coding-highspeed", "kimi-k2.6", "kimi-latest", "lmstudio", "mimo-v2-flash", "mimo-v2.5", "mimo-v2.5-pro", "mimo-v2.5-pro-ultraspeed", "minimax-m3", "mistral-medium-latest", "mlx-community/Qwen3.6-27B-OptiQ-4bit", "openai/gpt-oss-120b"]; +export const MODELS: ModelName[] = ["MiniMax-M2.5", "MiniMax-M2.7", "MiniMax-M3", "accounts/fireworks/models/deepseek-v4-flash", "accounts/fireworks/models/deepseek-v4-pro", "accounts/fireworks/models/glm-5p2", "accounts/fireworks/models/gpt-oss-120b", "accounts/fireworks/models/kimi-k2p6", "accounts/fireworks/models/kimi-k2p7-code", "accounts/fireworks/models/minimax-m3", "accounts/fireworks/models/qwen3p7-plus", "alibaba/qwen3.8-27b", "anthropic/claude-sonnet-4.6", "claude-fable-5", "claude-haiku-4-5", "claude-opus-4-5", "claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8", "claude-opus-4.8", "claude-opus-5", "claude-sonnet-4-5", "claude-sonnet-4-6", "claude-sonnet-4.6", "claude-sonnet-5", "deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash", "deepseek-v4-pro", "fugu", "fugu-ultra", "fugu-ultra-20260615", "gemma-4-31b", "glm-4.5", "glm-4.7", "glm-5", "glm-5-turbo", "glm-5.2", "glm-5.3", "glm-5.3-flash", "glm-5v-turbo", "gpt-5-codex", "gpt-5.2", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-pro", "gpt-5.5", "gpt-5.6", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-oss-120b", "grok-4.3", "grok-4.6", "grok-build", "k3", "kilo-auto/small", "kimi-for-coding", "kimi-for-coding-highspeed", "kimi-k2.6", "kimi-latest", "lmstudio", "mimo-v2-flash", "mimo-v2.5", "mimo-v2.5-pro", "mimo-v2.5-pro-ultraspeed", "minimax-m3", "mistral-medium-latest", "mlx-community/Qwen3.6-27B-OptiQ-4bit", "openai/gpt-oss-120b"]; export const TOOLS: ToolName[] = ["bash", "bash_output", "bash_kill", "read_file", "edit_file", "write_file", "webfetch", "skill", "codedb", "read_tool_result", "todo_write", "todo_read", "eval", "note_constraint", "ask_user", "attempt_completion", "load_tool_schemas", "mcp_search_tools", "mcp_select_tool", "clock_sleep", "subagent", "workflow", "agent_output", "learn_candidate", "peer_message", "workspace", "imagegen"]; diff --git a/src/additional_tests.zig b/src/additional_tests.zig index 0c9258c3..f6d9cf82 100644 --- a/src/additional_tests.zig +++ b/src/additional_tests.zig @@ -5,3 +5,7 @@ test { _ = @import("agent_server_compact_tests.zig"); _ = @import("provider_codegraff_tests.zig"); } + +test "oneshot -p still uses the live stall-watched transport" { + try @import("agent_tests.zig").oneshotUsesLiveTransport(@import("agent.zig").Agent); +} diff --git a/src/agent.zig b/src/agent.zig index 7f3ad6ac..75928435 100644 --- a/src/agent.zig +++ b/src/agent.zig @@ -197,7 +197,7 @@ pub const Agent = struct { /// Text streamed so far in the current request — on Esc-interrupt this is /// what survives into history (with an "[interrupted]" marker appended). partial_text: std.ArrayList(u8) = .empty, - stream_quiet: bool = false, // suppress live streaming (one-shot and internal requests) + stream_quiet: bool = false, // mute live paint (one-shot / compact); transport still streams compaction_request: bool = false, // the current model call is the synthetic compaction-summary request server_compaction_request: bool = false, // force one explicit Codex in-stream compaction pass responses_output_limit: ?u32 = null, // auxiliary override (title=64); compaction always uses 4096 @@ -260,16 +260,19 @@ pub const Agent = struct { return schema.providerTakesEffort(self.provider.kind, self.provider.id, self.provider.model); } - /// Whether this turn should put reasoning_effort on the wire. Gemini 3.x - /// maps that field to thinking_level; the default medium makes every - /// turn think for seconds (2× wall vs Pi, which omits it). /effort still - /// applies — we only skip the unset default. + /// Whether this turn should put reasoning_effort on the wire. + /// Flash / Gemini send `low` for the unset default (ADR 0046) — omitting + /// the field still bills reasoning_tokens. /effort still applies. pub fn sendReasoningEffort(self: *const Agent) bool { if (!self.effortApplies() or self.effort_rejected) return false; - if (std.mem.startsWith(u8, self.provider.model, "gemini") and self.reasoning == .medium) return false; return true; } + /// Root streams even on `-p`; `out`/`stream_quiet` only mute paint. + pub fn usesLiveTransport(self: *const Agent) bool { + return !self.sub; + } + pub fn toolsJson(self: *const Agent) []const u8 { return @import("agent_catalog.zig").toolsJson(self); } diff --git a/src/agent_request.zig b/src/agent_request.zig index 3cbf5ae0..a5f450aa 100644 --- a/src/agent_request.zig +++ b/src/agent_request.zig @@ -217,7 +217,7 @@ pub fn request(self: *Agent, tools_in: ?[]const u8) !std.json.ObjectMap { var openai404_retries: usize = 0; // #opencode-parity: bounded retries for OpenAI's spurious model 404s const max_openai404_retries: usize = 2; rebuild: while (true) { - const live = !self.sub and self.out != null and !self.stream_quiet; + const live = self.usesLiveTransport(); self.streamed_text = false; self.streamed_args = .none; const body = try self.buildBody(tools, force, live, stream_usage); diff --git a/src/agent_request_body.zig b/src/agent_request_body.zig index 04377d38..04fc5e6c 100644 --- a/src/agent_request_body.zig +++ b/src/agent_request_body.zig @@ -174,7 +174,7 @@ pub fn buildBody(self: *Agent, tools: ?[]const u8, force_tool: bool, stream: boo try s.objectField("prompt_cache_key"); try s.write(http_headers.requestCacheKey(self.io, self.label, self, self.provider.id, &ckbuf)); // reasoning_effort (codegraff/deepseek/zai) + Z.AI thinking + Vercel reasoning.effort. - try @import("zai_wire.zig").writeChatExtras(&s, self.provider.id, self.sendReasoningEffort(), @tagName(self.reasoning)); + try @import("zai_wire.zig").writeChatExtras(&s, self.provider.id, self.sendReasoningEffort(), @import("effort_route.zig").wireEffort(self.provider.model, @tagName(self.reasoning))); // --output-schema: structured outputs (xAI docs' response_format). // A provider that rejected json_schema (#543, deepseek) degrades // dsh-style: the tools-off formatting turn carries the schema as a diff --git a/src/agent_tests.zig b/src/agent_tests.zig index f103777c..2c4c93a9 100644 --- a/src/agent_tests.zig +++ b/src/agent_tests.zig @@ -81,3 +81,20 @@ pub fn lazyRootTools(comptime Agent: type) !void { try std.testing.expect(std.mem.indexOf(u8, agent.tools_anthropic, "bench (search)") != null); try std.testing.expectEqual(@as(usize, 0), agent.tools_responses.len); } + +/// `-p` sets `out=null` and `stream_quiet` so stdout stays the answer, but +/// the root still takes postLive (head/stream stall) instead of the 5-minute +/// watched POST that burned json-stream / cookie-store to a 300s SIGKILL. +pub fn oneshotUsesLiveTransport(comptime Agent: type) !void { + var root: Agent = undefined; + root.sub = false; + root.out = null; + root.stream_quiet = true; + try std.testing.expect(root.usesLiveTransport()); + + var child: Agent = undefined; + child.sub = true; + child.out = null; + child.stream_quiet = true; + try std.testing.expect(!child.usesLiveTransport()); +} diff --git a/src/effort_route.zig b/src/effort_route.zig index 7152789d..48bfa346 100644 --- a/src/effort_route.zig +++ b/src/effort_route.zig @@ -50,6 +50,24 @@ pub fn shouldRouteLookupLow(_: bool, _: bool, _: []const u8) bool { return false; } +/// Default `medium` thinking on flash / Gemini is silent seconds of TTFT +/// (ADR 0046). Gemini 3.x maps it to thinking_level; GLM flash through +/// codegraff thinks when the field is omitted. `/effort` still sends. +/// Do not treat this as a 4-tool catalog shrink. +pub fn omitsDefaultFlashEffort(model: []const u8) bool { + if (std.mem.startsWith(u8, model, "gemini")) return true; + return std.mem.indexOf(u8, model, "flash") != null; +} + +/// Wire `reasoning_effort` for an effort-capable provider. Flash / Gemini +/// default `medium` becomes `low`: omit still thinks (27 reasoning_tokens +/// on a `pong`); `low` bills 0. `none` is a 400. `/effort high` unchanged. +pub fn wireEffort(model: []const u8, requested: []const u8) []const u8 { + if (std.mem.eql(u8, requested, "medium") and omitsDefaultFlashEffort(model)) return "low"; + if (std.mem.eql(u8, requested, "ultra")) return "max"; + return requested; +} + test "lookup prompts route low, mutation prompts and bare commands do not" { // The three benchmark tasks are the pinned shape: two route low, and the // one that REGRESSED at low effort (permgate) must not route. @@ -64,3 +82,17 @@ test "lookup prompts route low, mutation prompts and bare commands do not" { try std.testing.expect(!shouldRouteLookupLow(false, true, q)); try std.testing.expect(!shouldRouteLookupLow(true, true, q)); } + +test "flash and Gemini map default medium to low; grok and glm-5.3 stay medium" { + try std.testing.expect(omitsDefaultFlashEffort("glm-5.3-flash")); + try std.testing.expect(omitsDefaultFlashEffort("gemini-3.7-flash")); + try std.testing.expect(omitsDefaultFlashEffort("gemini-3.7-pro")); + try std.testing.expect(omitsDefaultFlashEffort("deepseek-v4-flash")); + try std.testing.expect(!omitsDefaultFlashEffort("grok-4.6")); + try std.testing.expect(!omitsDefaultFlashEffort("glm-5.3")); + try std.testing.expect(!omitsDefaultFlashEffort("deepseek-v4-pro")); + try std.testing.expectEqualStrings("low", wireEffort("glm-5.3-flash", "medium")); + try std.testing.expectEqualStrings("high", wireEffort("glm-5.3-flash", "high")); + try std.testing.expectEqualStrings("medium", wireEffort("grok-4.6", "medium")); + try std.testing.expectEqualStrings("max", wireEffort("grok-4.6", "ultra")); +} diff --git a/src/http.zig b/src/http.zig index 2aa9ac13..2d513f03 100644 --- a/src/http.zig +++ b/src/http.zig @@ -191,9 +191,10 @@ fn post(gpa: Allocator, client: *std.http.Client, provider: Provider, body: []co /// Hard ceiling on one non-streaming request. The legitimate worst case /// observed (a 160k-token subagent context) completed in ~134s; anything /// past this is a hung response, not a slow one. Tunable via -/// GRAFF_POST_DEADLINE_SECS (#544): non-streaming callers (one-shots, -/// subagents) have no inter-chunk stall signal, so this deadline is their -/// ONLY hang detector — a harness with a tighter task timeout lowers it. +/// GRAFF_POST_DEADLINE_SECS (#544): non-streaming callers (subagents) +/// have no inter-chunk stall signal, so this deadline is their only hang +/// detector — a harness with a tighter task timeout lowers it. Root `-p` +/// streams (ADR 0046); this ceiling is not that path's hang detector. pub var post_deadline_ms: u64 = 5 * 60 * 1000; pub const WatchdogFired = enum { esc, deadline }; diff --git a/src/no_local_tools.zig b/src/no_local_tools.zig index 897cea62..88e1e835 100644 --- a/src/no_local_tools.zig +++ b/src/no_local_tools.zig @@ -122,13 +122,32 @@ pub fn filterLeanSpecs(comptime Spec: type, arena: Allocator, specs: []const Spe /// (evals) can act locally; the old lie forced extra child turns. pub const lean_subagent_desc = "Spawn a sidecar for a self-contained task. Child has bash, read_file, edit_file, write_file, codedb; it does NOT share your context — the prompt must say everything. Returns the final report. Keep the critical-path next step local; do not hand off the blocker and wait. Do file and command work yourself this turn; spawn only for independent work."; +/// One-shot tool prose (ADR 0046). Same names and JSON schemas; shorter +/// descriptions so flash models start and finish instead of rereading +/// essays. Do not drop to four tools (ADR 0024). +pub const lean_bash_desc = "Run /bin/sh -c in cwd. Returns stdout, stderr, exit. Foreground still running after 120s (or timeout ms) backgrounds — bash_output(wait_ms>0) waits; do not poll."; +pub const lean_read_file_desc = "Read a UTF-8 file. Call before editing. contains=exact-key numbered lines; start_line/end_line for a window."; +pub const lean_edit_file_desc = "Replace exact text. Prefer one batched edits[] over many calls. Prefer over write_file for an existing file."; +pub const lean_codedb_desc = "Indexed nav: context · around · callpath A B · list_dir · status. Prefer over bash grep/find/ls."; +pub const lean_attempt_completion_desc = "Task is done. Put the final answer in result."; + +fn leanCompactDesc(name: []const u8) ?[]const u8 { + if (std.mem.eql(u8, name, "subagent")) return lean_subagent_desc; + if (std.mem.eql(u8, name, "bash")) return lean_bash_desc; + if (std.mem.eql(u8, name, "read_file")) return lean_read_file_desc; + if (std.mem.eql(u8, name, "edit_file")) return lean_edit_file_desc; + if (std.mem.eql(u8, name, "codedb")) return lean_codedb_desc; + if (std.mem.eql(u8, name, "attempt_completion")) return lean_attempt_completion_desc; + return null; +} + /// schema.zig chains this after filterLeanSpecs: identical names and JSON /// schemas, compact prose. Returns the input untouched when lean is off. pub fn compactLeanSpecs(comptime Spec: type, arena: Allocator, specs: []const Spec) ![]const Spec { if (!lean) return specs; const buf = try arena.dupe(Spec, specs); for (buf) |*spec| { - if (std.mem.eql(u8, spec.name, "subagent")) spec.desc = lean_subagent_desc; + if (leanCompactDesc(spec.name)) |desc| spec.desc = desc; } return buf; } @@ -234,16 +253,20 @@ test "lean: the filter keeps exactly the seven one-shot tools, and allocates not try std.testing.expect(!leanKeeps("workflow")); try std.testing.expect(!leanKeeps("todo_write")); try std.testing.expect(!leanKeeps("webfetch")); - // …and the compact pass shrinks subagent's prose, leaves its name/schema - // and every other tool alone, and is a no-op when lean is off. + // …and the compact pass shrinks one-shot prose, leaves names/schemas, + // and is a no-op when lean is off. lean = false; try std.testing.expectEqual(specs.len, (try compactLeanSpecs(Spec, arena, &specs)).len); lean = true; const compact = try compactLeanSpecs(Spec, arena, &specs); try std.testing.expectEqual(specs.len, compact.len); for (compact) |s| { - if (std.mem.eql(u8, s.name, "subagent")) try std.testing.expectEqualStrings(lean_subagent_desc, s.desc) else try std.testing.expectEqualStrings("", s.desc); + if (std.mem.eql(u8, s.name, "subagent")) try std.testing.expectEqualStrings(lean_subagent_desc, s.desc); + if (std.mem.eql(u8, s.name, "bash")) try std.testing.expectEqualStrings(lean_bash_desc, s.desc); + if (leanCompactDesc(s.name) == null) try std.testing.expectEqualStrings("", s.desc); } + try std.testing.expect(lean_bash_desc.len < 200); + try std.testing.expect(lean_attempt_completion_desc.len < 80); try std.testing.expect(std.mem.indexOf(u8, lean_subagent_desc, "sidecar") != null); try std.testing.expect(std.mem.indexOf(u8, lean_subagent_desc, "critical-path") != null); try std.testing.expect(std.mem.indexOf(u8, lean_subagent_desc, "approval-denied") == null); // that fact lives on unattended_note, not here diff --git a/src/pricing.zig b/src/pricing.zig index 2760bc66..af22766d 100644 --- a/src/pricing.zig +++ b/src/pricing.zig @@ -276,6 +276,7 @@ pub const model_table = [_]ModelInfo{ .{ .provider = "codegraff", .name = "grok-4.6", .context = 500_000 }, .{ .provider = "codegraff", .name = "grok-build", .context = 256_000 }, .{ .provider = "codegraff", .name = "glm-5.2", .context = 204_800 }, + .{ .provider = "codegraff", .name = "glm-5.3-flash", .context = 202_752 }, .{ .provider = "codegraff", .name = "mimo-v2.5", .context = 128_000 }, .{ .provider = "codegraff", .name = "mimo-v2.5-pro", .context = 128_000 }, // Kimi Code offline fallback. Authenticated startup replaces this slice diff --git a/src/provider_codegraff.zig b/src/provider_codegraff.zig index c511e746..ecf6e132 100644 --- a/src/provider_codegraff.zig +++ b/src/provider_codegraff.zig @@ -20,7 +20,7 @@ test "Codegraff Responses allowlist matches the public gateway contract" { for ([_][]const u8{ "gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "grok-4.6" }) |model| try std.testing.expect(usesResponses("codegraff", model)); - for ([_][]const u8{ "gpt-5.5", "grok-4.3", "claude-opus-4.8", "gemini-3.7-flash" }) |model| + for ([_][]const u8{ "gpt-5.5", "grok-4.3", "claude-opus-4.8", "gemini-3.7-flash", "glm-5.3-flash" }) |model| try std.testing.expect(!usesResponses("codegraff", model)); try std.testing.expect(!usesResponses("openai", "gpt-5.6")); } diff --git a/src/provider_codegraff_tests.zig b/src/provider_codegraff_tests.zig index 27d1fba9..103fb16a 100644 --- a/src/provider_codegraff_tests.zig +++ b/src/provider_codegraff_tests.zig @@ -85,7 +85,7 @@ test "Codegraff Responses aliases pin a sticky prompt_cache_key" { } } -test "Codegraff Gemini omits default reasoning_effort; /effort high still sends" { +test "Codegraff Gemini sends low for default effort; /effort high still sends" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); @@ -109,10 +109,10 @@ test "Codegraff Gemini omits default reasoning_effort; /effort high still sends" .sys_normal = "system", }; try std.testing.expect(agent.effortApplies()); - try std.testing.expect(!agent.sendReasoningEffort()); - const omitted = try agent.buildBody(null, false, true, true); - defer std.testing.allocator.free(omitted); - try std.testing.expect(std.mem.indexOf(u8, omitted, "reasoning_effort") == null); + try std.testing.expect(agent.sendReasoningEffort()); + const low = try agent.buildBody(null, false, true, true); + defer std.testing.allocator.free(low); + try std.testing.expect(std.mem.indexOf(u8, low, "\"reasoning_effort\":\"low\"") != null); agent.reasoning = .high; try std.testing.expect(agent.sendReasoningEffort()); @@ -128,3 +128,39 @@ test "Codegraff Gemini omits default reasoning_effort; /effort high still sends" defer std.testing.allocator.free(ds); try std.testing.expect(std.mem.indexOf(u8, ds, "\"reasoning_effort\":\"medium\"") != null); } + +test "Codegraff glm-5.3-flash sends low for default effort; /effort high still sends" { + var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_state.deinit(); + const arena = arena_state.allocator(); + var messages = std.json.Array.init(arena); + try messages.append(.{ .object = blk: { + var obj: std.json.ObjectMap = .empty; + try obj.put(arena, "role", .{ .string = "user" }); + try obj.put(arena, "content", .{ .string = "hello" }); + break :blk obj; + } }); + var agent: Agent = .{ + .gpa = std.testing.allocator, + .arena = arena, + .io = std.testing.io, + .client = undefined, + .provider = build("glm-5.3-flash"), + .messages = messages, + .sub = false, + .label = "main", + .out = null, + .sys_normal = "system", + }; + try std.testing.expect(agent.effortApplies()); + try std.testing.expect(agent.sendReasoningEffort()); + const low = try agent.buildBody(null, false, true, true); + defer std.testing.allocator.free(low); + try std.testing.expect(std.mem.indexOf(u8, low, "\"reasoning_effort\":\"low\"") != null); + + agent.reasoning = .high; + try std.testing.expect(agent.sendReasoningEffort()); + const high = try agent.buildBody(null, false, true, true); + defer std.testing.allocator.free(high); + try std.testing.expect(std.mem.indexOf(u8, high, "\"reasoning_effort\":\"high\"") != null); +} diff --git a/src/session_run.zig b/src/session_run.zig index acbf4f3b..0ff4c2df 100644 --- a/src/session_run.zig +++ b/src/session_run.zig @@ -115,7 +115,7 @@ pub fn runReplCommand(gpa: Allocator, io: Io, environ_map: anytype, root: *agent /// One-shot print mode (`-p`/bare positional prompt): run the single prompt /// to completion, print the final text to stdout, exit. Tool progress goes -/// to stderr (say() with no out writer), streaming stays quiet, and the gate +/// to stderr (say() with no out writer), paint stays quiet, and the gate /// denies anything not pre-approved instead of prompting (there's no one to /// ask). Moved out of main() verbatim (600-line goal); `root`/`tracer` are /// already stable main()-owned storage by the time this runs. @@ -125,6 +125,9 @@ pub fn runOneshotPrompt(gpa: Allocator, io: Io, arena: Allocator, root: *agent_m root.in = null; // gate: deny instead of prompt; ask_user: self-decide root.out = null; // tool progress → stderr; stdout carries only the answer root.stream_quiet = true; + // stderr only: same class of "process is live" as Pi's JSONL session line. + // Eval first_out used to wait for the call-2 stdout pulse (ADR 0046). + std.debug.print("calling {s}\n", .{root.provider.model}); const ultracode_msg = try shapes.applyUltracodeSteering(arena, prompt_text, prompt_text, prompts.ultracodeActive(root)); if (ultracode_msg.explicit) { tracer.note("ultracode", prompt_text[0..@min(prompt_text.len, 120)]); diff --git a/src/turn_chrome.zig b/src/turn_chrome.zig index db90078d..d8fa0412 100644 --- a/src/turn_chrome.zig +++ b/src/turn_chrome.zig @@ -39,11 +39,16 @@ pub fn emitRetryNotice(io: Io, class: []const u8, attempt: usize, max_attempts: /// Count this inner-loop request, pulse from call 2, optionally pause if a /// non-zero cap is set. Returns pause text or null to proceed. Never pauses -/// when the cap is 0. +/// when the cap is 0. `-p` / unattended skip the pulse: it was landing on +/// stdout and eval `first_out` was "time to model call 2", not boot (ADR 0046). +pub fn shouldPulseTurn(unattended: bool, json_mode: bool, sub: bool, call_n: u64) bool { + return call_n >= 2 and !sub and !json_mode and !unattended; +} + pub fn beforeRequest(self: *Agent) !?[]const u8 { self.model_calls_this_turn += 1; const cap = max_turn_model_calls; - if (self.model_calls_this_turn >= 2 and !self.sub and !main_mod.json_mode) { + if (shouldPulseTurn(main_mod.unattended, main_mod.json_mode, self.sub, self.model_calls_this_turn)) { var buf: [120]u8 = undefined; const text = formatTurnPulse(&buf, self.model_calls_this_turn, cap, self.tool_calls_this_turn); if (text.len > 0) tool_pulse.emitNotice(self.io, "{s}", .{text}); @@ -78,3 +83,11 @@ test "formatTurnPulse omits a denominator when the cap is unlimited" { try std.testing.expectEqualStrings("· turn still going · model call 3 · 11 tools", formatTurnPulse(&buf, 3, 0, 11)); try std.testing.expectEqualStrings("· turn still going · model call 3/64 · 11 tools", formatTurnPulse(&buf, 3, 64, 11)); } + +test "one-shot and --json do not pulse turn chrome onto stdout" { + try std.testing.expect(shouldPulseTurn(false, false, false, 2)); + try std.testing.expect(!shouldPulseTurn(true, false, false, 2)); + try std.testing.expect(!shouldPulseTurn(false, true, false, 2)); + try std.testing.expect(!shouldPulseTurn(false, false, true, 2)); + try std.testing.expect(!shouldPulseTurn(false, false, false, 1)); +}