chore: version packages - #88
Conversation
There was a problem hiding this comment.
⚠️ APPROVE withheld by policy — PR author @github-actions[bot] is not a member of OpenRouterTeam (association: CONTRIBUTOR). Review posted as COMMENT; a maintainer must approve out-of-band.
Summary
Automated Changesets release PR: consumes the three pending changesets, bumps @openrouter/agent 0.8.0 → 0.9.0 (minor, matching three minor changesets) and @openrouter/mcp 0.0.1 → 0.0.2 (patch, per updateInternalDependencies: "patch" in .changeset/config.json), with matching CHANGELOG entries. No source changes; version/changelog math is internally consistent and no dependency range edit is needed since packages/mcp/package.json uses @openrouter/agent: workspace:*.
✅ No findings.
a97ef22 to
4125c61
Compare
There was a problem hiding this comment.
⚠️ APPROVE withheld by policy — PR author @github-actions[bot] is not a member of OpenRouterTeam (association: CONTRIBUTOR). Review posted as COMMENT; a maintainer must approve out-of-band.
Summary
Update review of head 4125c61: the diff content is byte-identical to the version I reviewed at a97ef22 (same three changesets consumed, @openrouter/agent 0.8.0 → 0.9.0, @openrouter/mcp 0.0.1 → 0.0.2, same CHANGELOG bodies) — the synchronize was a regeneration/rebase of the Changesets branch, not a content change. Re-verified at the new head: .changeset/ contains no leftover pending changesets, packages/agent/package.json:3 reads 0.9.0 with the ./doom-loop subpath export present (matching the changelog's claim of a new @openrouter/agent/doom-loop entry point), and packages/mcp/package.json still depends on @openrouter/agent: workspace:*, so no dependency range rewrite is missing.
✅ No findings.
9763a5b to
9ea3633
Compare
9ea3633 to
36f6f6b
Compare
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@openrouter/agent@0.9.0
Minor Changes
#73
78c562eThanks @LukasParke! - Doom-loop detection for the tool-execution loop (opt-in viadoomLooponcallModel).Catches runs that stop making progress while continuing to spend: the model re-issuing the same tool call with identical arguments in consecutive rounds (including repeated empty
{}calls and repeated invalid-JSON calls), repeating identical server-tool requests (web_search_calletc., detected post-execution at the step checkpoint), or emitting the same text tokens over and over. Detection is deterministic — a verdict is a pure function of the transcript — and responds through a configurable graduated ladder:observe(emit the newDoomLoopDetectedhook) →steer(inject corrective guidance; queued guidance persists across pauses) →block(refuse the call with an explanatory tool error, before execution) →stop(halt before any further model request; unresolved calls in the final turn get synthesized halt-error outputs so persisted history stays well-formed;SessionEnd.reason: 'doom_loop').Streaks are round-scoped: N identical calls fanned out in parallel within one round count once (a streak measures the model re-issuing a call after seeing its result). Tools declare call identity via
loopKeyon the tool definition — a function computing key material (nullexempts a call), a declarative field list (['command', 'cwd']— data, not code), orfalse(statically exempt); absent means the full validated arguments. MCP-wrapped tools acceptloopKeyviamarkMcp(tool, { loopKey }). Fingerprints are a cross-port contract: RFC 8785 (JCS) canonicalization + SHA-256 over UTF-8 via WebCrypto, with conformance vectors intests/vectors/doom-loop-fingerprints.jsonfor the Python/Go ports. Unhashable key material (bigint, circular, >64 deep) falls back to the full-arguments identity — detection never fails a run.Detector state persists inside
ConversationState.doomLoop: streaks survive serialize → resume, astopverdict survives decision-only resumes (approve/reject) and clears on a fresh conversational turn, and queued steer guidance is delivered on resume. Ladder configs warn on dead rungs and onblockwithstop: false(unbounded block/re-issue). Documented, test-locked limits: varying-input (nonce) loops evade the default identity without aloopKey; paraphrased text repetition is not detected; manual/client-executed calls are not recorded. New@openrouter/agent/doom-loopsubpath exports the primitives;ModelResult.getDoomLoopVerdict()reports a stopping verdict.#73
78c562eThanks @LukasParke! - Doom-loop escalation recovery: a newescalateladder rung betweensteerandblockthat unblocks a stuck run by throwing more intelligence at the next turn instead of refusing or halting.Configure via
doomLoop.escalation:modelruns the NEXT turn on a stronger model (one-turn override, automatic revert), and/oradvisorforces anopenrouter:advisorconsult (the advisor server tool is appended withforwardTranscript: trueand loop-diagnosing instructions, andtoolChoiceis pinned to it viaallowed_tools/requiredso the stuck model must ask for guidance first; an object form passes through as advisor parameters). A user notice naming the detected loop accompanies the escalated turn.Escalations are real spend on a run already suspected of wasting it, so they are budgeted:
maxEscalations(default 2) caps recoveries per conversation, budget is consumed when a recovery is applied (not at verdict time),escalationsUsedpersists inConversationState.doomLoopso resumes cannot reset it, and concurrent detector verdicts in one window escalate once. Exhausted or unconfigured escalations fall through to the weaker rungs; resolve-time warnings flag anescalaterung without a mechanism (and vice versa). TheDoomLoopDetectedhook'saction/overrideActionenums gain'escalate'— an override without config/budget downgrades toobserve, never silently to a stronger action.#73
78c562eThanks @LukasParke! - Run-level cancellation and per-request timeout composition.New
signaloption oncallModel: aborting it stops the tool-execution loop at the next turn boundary AND aborts the in-flight API request/stream, so a stalled provider fails fast with the abort reason instead of hanging until an outer caller/test timeout. A pre-aborted signal fails before any network dispatch.RequestOptions.timeoutMs(the thirdcallModelargument) now reliably bounds each request the loop makes even when a signal is present: the underlying SDK skips its owntimeoutMswiring whenever a request carries a signal, so the engine composes{run signal, caller signal, per-request timeout}viaAbortSignal.anyper dispatch — each request gets a fresh timeout budget (not one shared per-run timer), and whichever bound fires first wins.Patch Changes
231fb65Thanks @w0nche0l! - Thread the executed tool call into the hook execute context.context.toolCallis part of the tool-facing contract, but only the non-streaming orchestrator populated it — the streamingModelResultloop builds its turn context with justnumberOfTurns, soexecute/onToolCalledhooks sawtoolCall: undefinedon the streaming path.buildExecuteCtxnow fills the gap from the executed call: a caller-providedturnContext.toolCallstill wins (the orchestrator's carriesstatus), and otherwise the executedParsedToolCallis converted back to a wire-shapedFunctionCallItem. TheonResponseReceivedpath intentionally threads nothing — only thefunction_call_outputitem is in scope there.@openrouter/mcp@0.1.0
Minor Changes
#74
f412281Thanks @LukasParke! - Doom-looploopKeysupport for MCP-wrapped tools (pairs with@openrouter/agent'sdoomLoopoption).Two ways to declare a wrapped tool's call identity: a client-side
loopKeysmap oncreateMCPTools/rehydrateMCPTools(keyed by unprefixed MCP tool name; anyToolLoopKeyform — function, field-name array, orfalseto exempt), and a server-advertised_meta['openrouter/loopKey']on the tool definition (data-only: field-name array orfalse). Client config takes precedence. Server-advertised declarations ride the cache snapshot (SerializedMCPToolDef.loopKey), so rehydrated tool sets keep their identities without alistTools()round-trip; function forms are client-side only and cannot be cached.A server can advertise the same thing itself via
_meta['openrouter/loopKey']on the tool definition (data-only: a field-namearray or
false). ClientloopKeyswin over a server declaration, andserver-advertised values survive a cache round-trip via
SerializedMCPToolDef.loopKey.Patch Changes
78c562e,78c562e,78c562e,231fb65]: