run_prompt_turn previously performed a single completion and returned,
leaving tool calls unexecuted. It now mirrors start_directive's loop:
call -> execute tools -> merge results -> continue until no tool results,
then check the pending-agents guardrail before returning.
The pending-agents guardrail injects a reminder prompt when sub-agents
are pending, matching the same loop termination semantics as --headless.
The session is NOT exited between turns (unlike start_directive) so
multi-turn ACP conversations retain history across session/prompt calls.
Manual gate (tool-probe agent, fs_write + fs_read tools):
id 3 result: {"output":"DONE:probe.txt","stopReason":"end_turn"}
probe.txt exists: YES, content: hello
DEFECT 1: session/prompt now accepts the spec-shaped params as primary:
{"prompt": [{"type": "text", "text": "..."}]}
Text blocks are joined with newlines. Non-text block types are silently
ignored. The legacy params.text alias is preserved as a fallback. -32602
is returned only when neither a non-empty prompt array with text blocks
nor a non-empty text field is present.
DEFECT 2: initialize result now emits protocolVersion as integer 1
instead of the string "1", matching the ACP spec's InitializeResponse.
Tests: 4 new unit tests pin the spec-shaped prompt path, the non-text
block ignore behavior, the missing-both -32602 path, and the numeric
protocolVersion type.
All CLI flag processing (--agent, --role, --rag, --model, --no-memory,
--no-stream, --no-workspace-instructions, etc.) now runs through run()
before the REPL/cmd split. The ACP server is dispatched right before
match is_repl, after apply_prelude and skills loading, so it benefits
from the complete context setup with no duplication or drift risk.
These CLI flags were previously ignored because the ACP branch returned
before run() could apply them. Now the context is configured with the
requested agent, role, RAG index, and model before the server starts.
Session management remains protocol-driven via session/new and session/load.
RenderMode::Silent was incorrectly applied to --headless in addition to
--acp-server. Standalone headless should still display the LLM response;
only ACP mode requires stdout purity for JSON-RPC. The acp server's
run_prompt_turn already sets Silent before each prompt call.
- Hang fix: 120s timeout on compression LLM call + raw_stream
channel-close handling (None => break instead of spinning)
- Supervisor effective active count: stop counting finished
JoinHandles as occupying capacity slots
- Universal tool result size cap: truncate_if_needed() on
ToolResult, applied in eval_tool_calls() after escalation block;
configurable via max_tool_result_chars in AppConfig/AgentConfig
- Windowed compression: compression_keep_last config param keeps
the N most recent messages visible after compression
- Fix pre-existing flaky test: add #[serial] to
handle_list_available_unrestricted_when_no_whitelist so it does
not race with TestConfigDirGuard-based tests that temporarily
populate the agents data dir
Two coordinated changes that make openai-compatible OAuth providers usable
with a non-localhost redirect_uri (browser shows the callback URL, user
copies it back into the terminal — the same UX Claude uses).
Fix: OpenAICompatibleOAuthProvider::fixed_redirect_uri() previously returned
Some(uri) for any redirect_uri including public HTTPS URLs, which trapped
run_pkce_flow into trying to bind a TCP listener on a public URL. It now
returns Some only for loopback URIs (127.0.0.1, localhost, ::1). Non-loopback
URIs return None, routing run_pkce_flow to the paste branch.
New tri-format paste parser (parse_paste_input):
- Full callback URL (starts with http:// or https://): parse code + state from
the query string. This is what most modern OAuth providers redirect to and
what a naive user copies from the browser bar.
- Anthropic-style code#state fragment: preserved for Claude compatibility.
- Bare code: accepted with a warning that CSRF state validation is skipped.
For providers whose callback page shows only the code with no state.
State validation moved from mandatory to conditional — if a paste didn't
carry state (bare-code path), we warn and skip the check instead of hard-
failing. The listener path (localhost + LAN redirects) still requires state
because the server sends it in the query.
Adds 9 unit tests covering both changes.
GitHub OAuth Apps issue tokens that never expire and omit expires_in from
the response (they only send access_token, token_type, scope). RFC 6749 §5.1
allows this — expires_in is only REQUIRED for tokens that actually expire.
When expires_in is missing, save the token with expires_at = i64::MAX so
prepare_oauth_access_token never tries to refresh. If the token is ever
revoked server-side, the eventual 401 on the API call is the user's cue
to re-authenticate.
No effect on providers that include expires_in (Moonshot etc. — unchanged).
GitHub's device flow endpoints (and likely other RFC 8628 servers) default
to responding in application/x-www-form-urlencoded unless the client asks
for JSON via the Accept header. Our device auth and polling paths both call
.json() on the response and were failing to decode form-urlencoded bodies
with 'expected value at line 1 column 1'.
Adds Accept: application/json to:
- The device authorization POST in run_device_code_flow
- The device_code polling POST (on the RequestBuilder returned by build_token_request)
RFC 6749 §5.1 already specifies JSON as the token response format, so this
is spec-compliant across providers. Servers that already default to JSON
(Moonshot, etc.) ignore the redundant header.
Adds a commented example under the openai-compatible client section showing
how to configure flow: device_code for RFC 8628 device flow. Uses Moonshot's
kimi-code endpoints as the illustrative reference (users supply their own
client_id — no bundled defaults per §5.8 of the design plan).
Adds 9 unit tests covering:
- yaml deserialization of flow: device_code
- merge preserves base device_authorization_url when user omits
- merge lets user device_authorization_url win
- merge lets user use_pkce_in_device_flow win
- OpenAICompatibleOAuthProvider exposes / defaults both new trait methods
- Full serde roundtrip of a realistic device_code yaml block
No network or polling — pure config/serde logic tests. Brings the test
count from 1134 to 1143.
Projects ~/.cache/coyote/oauth/ from the host into /home/agent/.cache/coyote/oauth/
inside the sandbox so agents can call OAuth-authenticated providers without
re-authenticating. Same trust model as the existing config-dir and vault-password
copies. One-way copy (not bind-mount) — matches Docker's universal support
surface. Refreshed tokens die with the sandbox instance; run coyote --authenticate
inside if a fresh token is needed (Device Flow works via the QR code render).