Pass attachments to subagents directly and set subagent mode - #649
Conversation
elias-ba
left a comment
There was a problem hiding this comment.
Great work Hanna, and sorry this sat with me so long.
One change and two questions, all inline. The change is small: our own 403s from Anthropic currently come in at warning level, so they stop alerting.
The two questions are about intent rather than correctness, and I did not want to push a guess into your branch.
The only conflict was the global_chat README: main rewrote the sentence about the tool-call budget while this branch added a paragraph about subagent mode above it. Both kept.
build_focus_line returned "" whenever the caller could not say what the user had on screen. The planner is exactly that caller: it knows which step it asked for and nothing about the view, so it sent job_key and the sentence was never built. The subagent then got a redacted workflow with no indication of which step it was there to edit. It now names the editable step on its own and claims nothing about the view. The router paths are unchanged. FORBIDDEN joins PROVIDER_FAILURE_TYPES. It is only ever raised from Anthropic's PermissionDeniedError, so it means our account rather than the caller's, and it was going to Sentry at warning level with the caller errors. BAD_REQUEST is deliberately left out: it also covers genuine caller mistakes like echo's missing payload, so promoting it would page us for those.
There was a problem hiding this comment.
Amazing piece of work @hanna-paasivirta. I am merging this one. I pushed commits to address 2 things:
- The focus line now names the editable step when there is no
viewing, which is the planner's case, so a subagent finally learns which step it is there to edit.job_keywas reachingprompt.pyand becomingfocusedall along; it just could not render withoutviewing, so my earlier comment pointed at the wrong place. Sorry about that. FORBIDDENis inPROVIDER_FAILURE_TYPESnow. I leftBAD_REQUESTout on purpose, since it also covers genuine caller mistakes and would page us for those.
On the attachments question, I checked lightning#5096: the 3 types you send are all mapped, so nothing is dropped today.
I also merged main in to clear the conflict. The only clash was the global_chat README, where main had rewritten the budget sentence under your new paragraph, and I kept both.
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