Fire agent.started before the supervisor spawn and agent.completed/failed
from the spawn task's result arms, resolved against the child context so
global hooks stay gated by the child's own whitelist. Graph AgentNode
failures fire agent.failed at the point of failure, before a declared
fallback converts them into a successful route. The graph executor fires
graph.node.started before each branch spawn and graph.node.completed/failed
beside the narrate calls, with per-branch pre-resolved snapshots and
metadata so parallel super-step branches never cross-contaminate. Base
envs for detached dispatches are rebuilt at fire time via a shared
hook_base_envs helper.
Introduce src/hooks.rs as a types-only module (HookDef, HooksMap) and
plumb hooks:/global_hooks: config fields through every scope:
- Config + AppConfig hooks field, both Default impls, the manual
from_config copy, and a COYOTE_HOOKS env override
- Role frontmatter: hand-parsed hooks arm, private field, getter, and
JSON-inline re-serialization in Role::export (malformed maps degrade
to None)
- AgentConfig + Graph: hooks + global_hooks on both structs, copied in
AgentConfig::from_graph, with a per-agent global_hooks env override
- paths::hooks_dir() = <config_dir>/hooks with COYOTE_HOOKS_DIR override
- Commented hooks: block in the global config template (global scope
only; no global_hooks key)
No engine or dispatch logic yet; existing configs parse unchanged.
Review findings: the streaming arm now mirrors the non-streaming twin
(openai_extract_responses): partial text/tool calls are returned rather
than discarded by a hard error; only an empty truncated response bails.
Billed usage on truncated turns is recorded before either outcome.
Also: CHANGELOG notes the streaming-incomplete surfacing and corrects
the include/strict phrasing (sent on every responses-wire body).
Add responses sub-patches to the six o-series quirks entries (temperature
and top_p removal; reasoning.effort high for the -high aliases) and
regenerate models.yaml from live models.dev/openrouter data, picking up
expected upstream price and model drift.
Parse Responses-API token usage on both wires: non-streaming responses
fill ChatCompletionsOutput.usage and response.completed events feed the
SseHandler accumulator, with cached input mapped from
input_tokens_details.cached_tokens. Harden failure paths: bail with the
payload message on response.failed and error stream events, and surface
incomplete_details.reason when a non-streaming response comes back
incomplete and empty.
Replay hardening: id-only Reasoning blocks (no encrypted_content) are
dropped at replay time since the API 400s on them under store:false,
which would permanently wedge a persisted session; a null summary is
normalized to [] at the same site. Claude's replay guards flip to a
positive Thinking/RedactedThinking allow-list.
Flip the default wire for stock openai (api.openai.com, no custom
api_base) to Responses. The resolver call site now passes a genuine
stock-openai predicate: api-key clients or stock oauth without api_base
qualify, config-oauth gateways never do. Custom api_base and
openai-compatible clients keep defaulting to chat; explicit wire_api
still always wins. Also reject a non-map responses section in
RequestPatch via test.
- assert old Thinking/RedactedThinking blocks re-serialize to the
original wire shape, proving both legs of old-session compat
- add serde_yaml round-trip test for Vec<ThinkingBlock> covering the
on-disk session format, including a Reasoning block with summary
payload and the encrypted_content: None omitted-field case
Add ThinkingBlock::Reasoning to carry OpenAI Responses reasoning items
verbatim. The Responses body builder now requests
reasoning.encrypted_content, marks tool defs strict:false, and replays
each round's reasoning items ahead of the assistant text and function
calls. Both extraction paths (non-streaming and streaming) capture
reasoning output items. Claude and Bedrock replay paths drop the
OpenAI-only variant so mid-session model switches keep working.
Completes the loop-tracker exemption change (8be9c8e) riding this
branch: the explanatory comment above LOOP_TRACKER_EXEMPT_TOOLS was
dropped in that commit; restore it, updated for the new entries.
Introduce a WireApi enum (responses|chat) with an optional wire_api
config field on the openai and openai-compatible clients, resolved
through a single resolve_wire_api fn: codex forces the responses wire
(an explicit wire_api: chat there is a hard config error), an explicit
wire_api wins everywhere else, and the default stays chat for now.
The openai dispatch generalizes from (RequestData, bool) to
(RequestData, WireApi), and the openai-compatible client gains opt-in
Responses dispatch (URL, body, extractor, and streaming forks).
Both patch layers are now wire-aware: RequestData carries its wire,
RequestPatch gains a responses section, and patch_request_data selects
patches per wire. This deliberately FIXES a codex bug: chat-shaped
model patches (e.g. o3-high's max_tokens/temperature/top_p/
reasoning_effort tweaks) previously merged wire-blind into codex
responses bodies, poisoning them with chat-only keys. On the responses
wire only an explicit responses sub-patch applies; chat-wire patching
is byte-identical to before (pinned by a regression test).
- ClaudeConfig gains an oauth: block (custom endpoints, e.g. a private
gateway); without it, auth: oauth keeps the stock Claude Pro/Max provider
- Claude-Code system-prefix spoof + anthropic-beta header are only sent on
the stock provider path
- OAuth tokens are stamped with an issuer fingerprint (token_url|client_id)
so switching endpoints never replays a token against the wrong issuer
- All auth flows refresh the in-memory access-token cache on save, so
.authenticate mid-session takes effect immediately
- A custom oauth: block now requires api_base: refusing to silently fall
back to api.anthropic.com prevents sending a gateway token to the wrong
host