You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Generated by the Upstreamer pipeline in this PR, run locally end to end
(scripts/upstream --ref @openrouter/agent@0.8.0). Both gates passed:
mechanical verifier === PASS: 0 failures ===, parity eval PASS WITH
WARNINGS on the second pass (.upstreamer/eval-report.md).
Ports the 0.7.2 -> 0.8.0 delta: lifecycle hooks system (HooksManager +
nine built-in hooks), versioned conversation-state serialization,
awaiting_client_tools for unresolved manual tool calls, default-on
allow_final_response with DEFAULT_FINAL_RESPONSE_DIRECTIVE,
strict_final_response / empty-final-retry tolerance, and MCP tool-result
source discrimination. state.yaml advances to 680bceb, so the first CI
run after merge is a no-op until the next upstream release.
First-pass eval FAILed on three real findings (Stop-hook force_resume
was not a zero-cost retry, MCP branding surface missing, thin hooks test
coverage); all three fixed and re-verified in the second pass — see the
eval report for the traced evidence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: README.md
+21-1Lines changed: 21 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -136,6 +136,10 @@ web = server_tool({"type": "web_search_2025_08_26", "max_results": 5})
136
136
137
137
Use `require_approval` on a tool or `require_approval` on the request to pause sensitive calls before execution. Approval resume requires a state accessor with async `load()` and `save()` methods.
138
138
139
+
Manual tools (`execute=False`, no `on_tool_called`) pause the loop with status `"awaiting_client_tools"` when the model calls them, instead of silently dropping the call. Read the unresolved calls via `get_pending_tool_calls()` / `get_state()`, execute them yourself, and continue by calling `call_model` again with `function_call_output` items in `input`.
140
+
141
+
For durable cross-process storage, serialize state with `serialize_conversation_state` / `deserialize_conversation_state` rather than storing raw dataclass fields. The wire format is versioned (`CONVERSATION_STATE_VERSION`); a version mismatch raises `UnsupportedStateVersionError` and malformed JSON raises `InvalidStateError`, so a store can never silently misinterpret a future shape.
142
+
139
143
Tool context is kept outside the model transcript. Provide a context mapping with per-tool keys and optional `shared` state. Tool execution receives `ctx["local"]`, `ctx["shared"]`, `ctx["set_context"]`, and `ctx["set_shared_context"]`.
140
144
141
145
```python
@@ -151,6 +155,22 @@ result = call_model(
151
155
)
152
156
```
153
157
158
+
## Lifecycle Hooks
159
+
160
+
Pass a `HooksManager` (or an inline `{hook_name: [HookEntry(...)]}` dict of built-in hooks) via `hooks=` to observe or intervene in a run: `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `UserPromptSubmit`, `Stop`, `PermissionRequest`, `SessionStart`, `SessionEnd`, and `PostModelCall`. Handlers receive a validated payload dict and a `LifecycleHookContext` (`session_id`, `hook_name`, `cancel_event`).
161
+
162
+
```python
163
+
from openrouter_agent import HookEntry, HookName, HooksManager
`SessionStart` fires once per run with a config summary; `SessionEnd` fires once with aggregated `total_usage` (summed across every `PostModelCall`) and is guaranteed to fire — and any pending async hook work drained — even when a no-tools stream raises. `PreToolUse` can block a call (`{"block": "reason"}`) or mutate its input (`{"mutated_input": {...}}`); `PermissionRequest` can pre-empt the approval gate with `{"decision": "allow" | "deny" | "ask_user"}`; `Stop` can force the loop to keep going past a `stop_when` hit with `{"force_resume": True, "append_prompt": "..."}`. A `HooksManager` instance is safe to share across concurrent `call_model` runs — session identity is threaded per emit, not stored as manager-level mutable state.
173
+
154
174
## Stop Conditions
155
175
156
176
The built-ins mirror the TypeScript package and OR together when provided as a list:
@@ -161,7 +181,7 @@ The built-ins mirror the TypeScript package and OR together when provided as a l
161
181
-`max_cost(dollars)`
162
182
-`finish_reason_is(reason)`
163
183
164
-
Set `allow_final_response=True` or a string to ask for one final no-tools turn when a stop condition fires on a tool-call turn.
184
+
When a stop condition fires while the model is still emitting tool calls, `call_model` makes one more turn with `tool_choice="none"` by default (tools stay in the request so the prompt-cache prefix survives) so the run ends with a natural-language answer. `allow_final_response` tunes this: `True` or omitted appends `DEFAULT_FINAL_RESPONSE_DIRECTIVE` as a user message, a non-empty string replaces the wording, `""` appends nothing, and `False` disables the extra turn entirely.
0 commit comments