Repository navigation
feat(llm-helper): the loop contract - llm.chat serves one turn of the engines' agent loop - #115
Merged
Merged
Conversation
… engines' agent loop The twin of mercury-python #47 (RFC-0011 build plan step 1). A request that carries tools is one model turn of llm.run, the agent loop of the Java and Rust engines: the transcript stays in the helper's store and the loop holds a reference to it. A later turn sends conversation, turn and results back; the helper appends the one tool_result message in call order and the model's turn exactly as the API returned it, thinking blocks included (blockOf keeps a block's enumerable fields, as the SDK sends one back). The reply carries status, calls, cap for a cut-off turn, the four usage fields, conversation and turn. A stale turn count or an ended run is a 409 and each write is a compare-and-set on the count; an ended or expired conversation is a 404. The model is pinned at the first turn, no server-side fallback is sent on a loop turn, and the helper places the cache markers. The store is llm.transcript.store: memory, or redis (the redis package, a dev dependency like the SDK) with the Python twin's Lua script under transcript:{conversation}, a command timeout and no offline queue, so a store that is gone answers 503 at once. The shared tests/vectors/llm-loop-vectors.json (66 cases, byte-identical with mercury-python) runs on the memory store, a Redis fake and, with LLM_TRANSCRIPT_REDIS_URL, a live Redis. A request without tools behaves as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
toolsis one model turn of an agent loop (loopTurn); withouttools,llm.chatis unchanged. The request fields (tools,conversation,turn,results,ttl,retention), the reply (status,text/data,calls,refusal,cap,conversation,turn, the fourusagefields) and every message match the Python helper word for word; the loop fields are a 400 onllm.stream.blockOf: the enumerable fields - the SDK's stream keeps a tool call's raw input in a non-enumerable__json_buf), so thinking signatures replay unchanged. The helper builds the onetool_resultmessage in call order. A stale turn or an ended run is a 409, an ended or expired conversation a 404, and each write is a compare-and-set on the turn count.llm.transcript.store, required for a loop turn;memoryin the sample config):MemoryStore, andRedisStoreon theredispackage 6.3.1 - the Python helper's Lua script and record JSON undertranscript:{conversation}, so the two packs can share one Redis. The client connects on the first command (a pending connect is shared and bounded bytimeout.ms), each command waits at mosttimeout.ms, nothing is queued while offline, and anerrorlistener keeps a lost connection from stopping the process.redisis imported on demand: without it a loop turn on the Redis store is a 503 that names the package.createLlmHelpergains thestoreandnewConversationIddependencies besidebackend;llm.healthinfo reportstranscript_store; the trace annotations addllm_status,llm_turnand the cache tokens; the log line carries status, turn and usage, never content or the conversation reference.redisjoins the dev dependencies beside the SDK. The npm package is unchanged: the same 44 files, the same runtime dependencies. The pre-commit guard names its existingpackage-lock.jsonexemption (audited 2026-10-13).test/vectors/llm-loop-vectors.json(66 cases, byte-identical with mercury-python, digest pinned) on the memory store, on a Redis fake that applies the store script's rule, and on a live Redis whenLLM_TRANSCRIPT_REDIS_URLnames one; plus tests for the stored blocks, logs, traces, store failures (503 by name), a lost compare-and-set (409), health, the connection settings, the missing package, the connect timeout and a closed port.npm test: 410 tests, 346 passed, 64 skipped (the live variants).helpers/redis-standalone) the 64 live cases pass and leave no key; the Python store wrote turn 1, this store read it and appended turn 2 under the compare-and-set, and the Python store read turn 2 back, Unicode intact. Six negative controls, each restored, fail exactly as many tests as in the Python twin: the thinking signature stripped 23, fallbacks on a loop turn 60, results out of call order 2, no retention 28, no conversation cache marker 62, no turn check 4.Why
Step 1 of the agent-loop build plan (RFC-0011 in mercury-composable) is the model turn and its transcript store in both language packs, in lockstep. The loop,
llm.run, runs in the engines and dispatches each tool call to a route; the model turn stays in the pack, and the transcript with it, so provider blocks never cross an engine codec, the graph's state stays small, the transcript stays append-only as the current models' thinking blocks require, and any replica - of either pack - serves any turn. With this merged, step 1 is complete and step 2 (llm.runin Java) builds on one contract proven on both packs.Upgrade note: a request without
toolsbehaves as before;tools,conversation,turn,results,ttlandretentionare now loop fields, and a loop turn needsllm.transcript.store.Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
🤖 Generated with Claude Code