Pass attachments to subagents directly and set subagent mode - #649
Open
hanna-paasivirta wants to merge 12 commits into
Open
Pass attachments to subagents directly and set subagent mode#649hanna-paasivirta wants to merge 12 commits into
hanna-paasivirta wants to merge 12 commits into
Conversation
7 tasks
hanna-paasivirta
marked this pull request as ready for review
August 26, 2026 17:31
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.
Short Description
Attachments (run logs, dataclips) now reach subagents as the exact text the user sent. Before, only the planner's own message got through, so a subagent saw a summary of a log, or nothing. Attachments also no longer stick to the conversation history, and oversized ones are refused rather than quietly cut down.
Fixes #643
Implementation Details
The router used to splice attachments into the user's message as text, and nothing carried them further if the planner was called. So a user attaching a run log and asking "why did the last two steps fail?" got the planner's summary of the log instead of the log. The workflow YAML already travels as a payload field; now attachments do too.
Each agent receives them the way it already receives that kind of context.
job_chatis unchanged — byte-identical tomain. It already hadcontext.log/context.input/context.output, rendering as<run_logs>/<input>/<output>. Attachments map onto those rather than getting a second channel.workflow_chatand the planner have no such fields, so they take anattachmentspayload field and render it where their other context goes. With none,workflow_chat's prompt is byte-identical to before.input_dataclipis a step's input andrun_inputis the whole run's, so both wantinput. When a field has two sources both are kept and labelled with their type; with one source it is unlabelled, exactly as before. Unlabelled with two would tell the model a run's input is the step's.typeis outside those five is not passed on, and is reported (see Sentry below). A new type needs one line in the mapping.Who decides what a subagent gets depends on the route.
state.patientstostate.cases" is not billed for a log the planner already read. Both tools take a requiredattachmentslist.Attachments no longer stick to the history.
PAYLOAD_SPEC.md.Planner-invoked subagents were running in production mode.
call_job_agentnever setsubagent: True, and put the YAML incontext["workflow_yaml"], whichPayload.from_dictdoes not read. Sojob_chatran under the production scope prompt ("You ONLY help with job code..."), with no<workflow_structure>block and noinspect_job_codetool. The router's direct route set both correctly.call_workflow_agenthad the same defect, keeping its "save your workflow and go to the Inspector" instruction. Both now run in subagent mode.job_chatan escalation tool whose target is the planner, which is now the caller.format_subagent_result_for_llmturns that handover into the reason the agent could not finish, for the planner to act on.Oversized attachments are refused, never trimmed. Shortening one would mean answering from evidence the user thinks we read in full. Context Apollo injects itself, like adaptor docs, is a different case and is still truncated.
400 ATTACHMENT_TOO_LARGEbefore any model is called. Reading long logs properly is a separate future project, Add tools for reading long logs and other attachments #651.detailscarriestotal_characters,limit_charactersandlargest_attachment. On/streamit arrives as an SSEerrorevent under a 200, so match ontype, not HTTP status.max_tokensreserve, the static prompt and adaptor docs are accounted for. The arithmetic sits next to the constant.Typed attachment content, and the order to ship in. Lightning is moving
contentfrom always-a-string to typed per type — an array of lines for a log, an object for a dataclip.str()-wrapped. Butstr()on a list gives Python repr, so a log would arrive as one single-quoted line.attachment_textrenders by shape instead: lines joined with newlines, objects as indented JSON, strings untouched.Sentry now says what kind of failure it was. This is the change with reach beyond
global_chat, so it is the one to read closely.ApolloErrorgets anapollo_error_typetag and itscode/detailson anapollo_errorcontext, so a class of failure can be counted withapollo_error_type:ATTACHMENT_TOO_LARGErather than by matching message wording. That count is what decides whether Add tools for reading long logs and other attachments #651 is worth doing.warning: searchable, not paging. Anything we might have to fix stays aterror.AUTH_ERROR(401) andRATE_LIMIT(429) mean Anthropic rejected or throttled Apollo's own key, so they are named inPROVIDER_FAILURE_TYPESand keep alerting. Worth checking that list is complete.Nothing in the two production services changes. Checked rather than assumed, since Lightning calls them directly:
job_chathas no source change at all — only its README and two new acceptance specs.workflow_chat's built prompt hashes identically tomainacross all five modes (plain, errors, read-only, subagent, no YAML) when no attachments are sent, and its one productionbuild_promptcaller passes every argument by keyword, so the new parameter cannot shift a positional.util.pyis additions only; no existing helper changed.entry.pyis the exception, and it affects every service — see Sentry above.Tests
pyproject.tomlandconftest.py, until now unused). The whole router → planner →job_chatchain with every LLM call scripted, and the attachment carries a canary, so arrival is checked on bytes rather than wording. Free to run.The four tiers are now described in
services/testing/README.md.Known and not fixed here
typeis outside the five known ones is dropped on the job route, wheremaindelivered it as text. It reports to Sentry but does not reach the model.max_tool_calls.PROMPT_TOO_LONG, rawBAD_REQUEST, or a500). Attachments can no longer cause it, but a long conversation still can.AI Usage
Please disclose whether you've used AI in this work (it's cool, we just want to
know!):
You can read more details in our
Responsible AI Policy