refactor(agent): clean-slate Agent loop — Message / AgentContext / single build_messages seam - #882
Open
moria97 wants to merge 33 commits into
Open
refactor(agent): clean-slate Agent loop — Message / AgentContext / single build_messages seam#882moria97 wants to merge 33 commits into
moria97 wants to merge 33 commits into
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…m_wire input assumption
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oundary; hoist json import
… wrapper Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Agent.__init__, _stream_turn, run() to agent.py; create tests/agent/fake_llm.py and tests/agent/test_agent_run.py (4 tests). Fix test_budgeting.py patch target from memory.utils.get_tokenizer to agent.budgeting.get_tokenizer (ordering fragility exposed by new module-level import). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…afe instance state Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…attachments as data Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hread guard + cleanup - Remove dead backend/agent/react_agent.py, state.py, tool_utils.py and their corresponding tests/agent/test_state.py, test_tool_utils.py - Add empty-thread guard in chat.py after from_thread() to return early if msgs is empty (mirrors the existing empty-messages early-return) - Retain append_text in agent_service.py (covered by test_agent_service.py) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pai_agent_wrapper read state.messages (old AgentState); the new Agent.run(ctx) passes AgentContext positionally. Resolve the trace source from either the legacy state= kwarg or the positional ctx, and derive wire messages from history + current_turn so span INPUT_VALUE / INPUT_MESSAGES are populated again. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…otes, guard nil session close - remove unused append_text + its obsolete test (logic now lives in render_current_turn) - sanitize double-quotes in attachment filenames so they can't break the name="..." tag - guard session.close() against None in the streaming finally (pre-existing nil deref) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
☂️ Python Coverage
Overall Coverage
New Files
Modified Files
|
…orted astream emits token usage on a dedicated empty-delta chunk (TextChunk(usage=...), delta=""). The new _stream_turn's `elif chunk.delta` guard dropped it, so the SSE serializer never accumulated usage and the response reported no token counts. Forward chunks that carry usage even when delta is empty. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clean core agent + dual public API: stateful /v1/responses (typed events, hybrid Redis+SQL store) and stateless /v1/chat/completions shim, both over one internal AgentEvent union. Decoupled from RAG/llamaindex; fresh schema. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per review: /v1/responses strictly conforms to OpenAI Responses event + item schemas (incl. tool arguments and function_call_output outputs) with no extra fields; PAI-RAG-only signals (tool progress, trace, latency, stderr, citations) move to a separate opt-in pai.* extension channel. Split compatibility bar by endpoint; SDK integration test asserts the default responses stream is pure-OpenAI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bite-sized TDD tasks: AgentEvent union, Agent.run emits events, ChatCompletionsSerializer (usage + invisible-timeout regression locks), repoint /v1/chat/completions and retire legacy chunk serializers. Phase 1 of the agent API protocol; ships the bug-class fix standalone. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…/error) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…egression locks) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…drail (ordering + fail_fast) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…to _event_to_chunks (DRY) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ToolStarted: emit empty content chunk (no actions — arguments not yet available)
- ToolCompleted: emit actions chunk with {id, type, function:{name, arguments}}
matching frontend's toolCall.function?.name / .function?.arguments reads
- ToolResult: emit observation as {tool:{id}, result, error}
matching frontend's observation.tool.id / .result / .error reads
- Sync serializer: same shapes for actions[] and observations[] in the response dict
- trace_id: emit on terminal stop chunk via get_request_id() (request-scoped id)
in both _event_to_chunks (pure stream path) and serialize_chat_stream_with_effects
(effects path); also included in sync response dict
- Tests: add test_tool_chunks_match_frontend_shape, test_tool_actions_not_emitted_on_tool_started,
test_sync_tool_actions_observation_match_frontend_shape, test_trace_id_emitted_on_stop_chunk
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…undary) 3 TDD tasks: clean Tool/ToolBox package, FunctionTool->Tool adapter, wire the adapter at the agent_service boundary. RAG tool factories unchanged; agent core decoupled from llama_index. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert backend/agent/tools.py into a package (tools/__init__.py + tools/base.py). Introduce a plain `Tool` dataclass (name, description, parameters schema, fn, return_direct) replacing FunctionTool. ToolBox now dispatches, retries, and traces against Tool objects directly. TDD: 5 new tests all pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… boundary Co-Authored-By: Claude Sonnet 4.6 <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.
Why
The agent layer was hard to debug — what the model receives was assembled across four files, and
react_agent.run_asyncwas a ~240-line generator mixing message prep, streaming, an intent-nudge hack, tool dispatch, return_direct, and token-fitting. This replaces it with a flat agent built around message / context / loop.What changed
New
backend/agent/layer (oldreact_agent.py/state.py/tool_utils.pydeleted):message.py— oneMessagetype +from_thread()normalizationcontext.py—AgentContext, assembled once; the single inspectable objecttools.py—ToolBox(schema / dispatch+retry / return_direct)agent.py—build_messages()(single model-input seam) + flat ~40-linerun()loopbudgeting.py— token manager (renamed, internals unchanged)chat.py/agent_service.pymigrated toAgent/AgentContext;parse_attachment_toolsreturns attachment content as data instead of mutating the message.Attachment bug structurally fixed: content flows
parse_attachment_tools → AgentContext.attachments → build_messages → <attached_file>. Message is never mutated.Preserved: budgeting, return_direct, guardrails, streaming (SSE unchanged), tracing. Dropped: intent-nudge buffering.
Testing
New
tests/agent/(31 tests) via aFakeLLM: text-stop, tool-call→answer, return_direct, max-steps, idle-timeout, thread normalization, attachment regression-lock, budget round-trip. Net −59 lines.Unit/type-verified only — not yet run against the real
PaiLlm. Smoke-test: upload a file → chat → confirm[agent] model input:shows the file body inside<attached_file>and the answer uses it.🤖 Generated with Claude Code