feat(run): InputFileStager seam — stage non-image attachments into the prompt
Adversarial Review (Gadfly) / review (pull_request) Has been cancelled
executus CI / test (pull_request) Successful in 2m21s

executus's tool.Invocation already carried InputFiles (audio/PDF/binary), but the
executor never staged them — only Images were folded into the run. This adds the
host seam mort's chat/chatbot surfaces need for audio-input parity with agentexec.

- run.Ports gains InputFiles InputFileStager (nil-safe; nil = input files silently
  ignored, run still proceeds text-only). The interface mirrors mort's skill
  FileStorage: StageInputFile(ctx, runID, agentID, name, mime, content) → file_id.
- run/input_files.go (ported from mort agentexec/input_files.go): stageInputFiles
  persists each file under run scope and appends an [ATTACHED FILES] descriptor
  block to the prompt so the agent can reach them by file_id (e.g. code_exec
  files_in → /workspace/<name>). Bytes are NEVER inlined into model context.
  Best-effort: empty/oversized(>50MB)/save-error files are skipped; colliding
  base names are disambiguated (name-2, name-3) so they don't clobber at
  /workspace/<name>.
- Executor.Run calls it after the model/toolbox build, before the loop, so the
  descriptor rides the first user turn (alongside the existing Images folding).

Tests: stages + builds the block; nil stager / no files leave the prompt intact;
dedup; empty/save-error skipping. Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 13:02:55 -04:00
parent 7a5eebc468
commit 2ef88f2a73
4 changed files with 259 additions and 0 deletions
+6
View File
@@ -318,6 +318,12 @@ func (e *Executor) Run(ctx context.Context, ra RunnableAgent, inv tool.Invocatio
}
ag := agent.New(model, e.systemPrompt(ra), opts...)
// Stage non-image input attachments (audio/PDF/binary) into the host file
// store and fold an [ATTACHED FILES] descriptor into the prompt so the agent
// can reach them by file_id. No-op when Ports.InputFiles is nil or there are
// no files. Done after the model/toolbox build but before the loop, so the
// descriptor rides the very first user turn.
input = e.stageInputFiles(runCtx, inv.RunID, ra.ID, inv.InputFiles, input)
// One WithSteer drains BOTH the session mailbox (a tool's AttachImages) and
// the critic's nudges before each step.
steer := func() []llm.Message { return append(mailbox.drain(), critic.drainSteer()...) }