Skip to content

Collect output

A run’s output is available four ways, depending on how immediate you need it — and on whether the deliverable is what the agent said or a file it wrote.

The controller records a summary on the Agent once the run finishes:

Terminal window
kubectl get agent <name> -o yaml

Useful fields under status: phase, exitCode, output (a truncated tail of the pod logs, capped at 256 KiB by default), failureReason, startedAt, and completedAt.

If output is empty on a terminal Agent, check failureReason: when the run pod or its Job was garbage-collected before the controller could read the result back (e.g. the controller was down longer than the Job’s one-hour TTL), the Agent still finishes but failureReason says so (run output unavailable: pod garbage-collected or run record unavailable: …) rather than leaving you guessing at a blank output. An http sink (below) avoids this entirely by capturing events as they happen.

While a run is in progress, follow the supervisor’s stdout. The supervisor echoes every stream-json line, so the pod logs are the live event stream:

Terminal window
kubectl -n ai-agents logs -f job/agent-job-<name>

Declare an http sink in the recipe’s output to stream events to your own listener as they happen:

output:
format: stream-json
sinks:
- type: stdout
- type: http
url: http://collector.my-namespace.svc:8099/notify

The supervisor POSTs each stream-json line to that URL. For a batch run, which has no pod, the controller POSTs the same envelope once the batch ends. This is how a UI or an indexer consumes runs in real time. See Agent runtime for how the supervisor produces these events.

A failed POST is retried with capped exponential backoff before the event is dropped — a transient blip in your listener does not lose events, while a persistently unreachable sink never blocks or fails the run (the pod logs remain the source of truth).

When the deliverable is an artifact rather than a message, declare it under output.watch instead of asking the model to repeat the file as its closing text:

output:
format: stream-json
sinks:
- type: stdout
- type: http
url: http://collector.my-namespace.svc:8099/notify
watch:
- event: planning.result
path: target/result.json

Once the agent exits, the supervisor reads each declared path and raises a named {"kind": "file"} event carrying the file’s contents on the sinks above:

{ "kind": "file", "event": "planning.result", "path": "/workspace/target/result.json", "content": "{\"gap\":\"found\"}" }

Relative paths resolve against WORKSPACE_DIR, and a path escaping it is refused. The event is raised before the terminal agent/succeeded|failed lifecycle event, so a consumer that treats that as end-of-stream still receives it.

A declared artifact always reports. A file the agent never wrote still raises its event, carrying a reason instead of content, so your consumer learns the run delivered nothing rather than waiting on an event that never arrives:

{ "kind": "file", "event": "planning.result", "path": "/workspace/target/result.json", "reason": "missing" }

Oversized (>128 KiB) and unreadable files report the same way.

Inline content is capped at 128 KiB because it rides the event stream. For a larger artifact — or one that should never pass through the stream — give the watch an upload destination. The supervisor POSTs the file’s bytes there and the event carries only its size and digest:

output:
watch:
- event: report.ready
path: out/report.md
upload:
url: https://files.example.com/runs/{agent}/{event}
headers_secret: files-auth
{ "kind": "file", "event": "report.ready", "path": "/workspace/out/report.md", "uploaded": true, "bytes": 48213, "sha256": "9f86d0…" }

{agent} and {event} expand to the run’s Agent name and the watch’s event name, so one recipe serves every run. The body is application/octet-stream, and headers_secret names an agent-secrets key holding the header block, as for a sink. Each upload also carries X-Agent-Exit-Code, the agent’s exit code — the file is read after the agent exits either way, so without it a receiver would store a failed run’s leftovers as a result. Uploads are capped at 64 MiB by default (MAX_UPLOAD_BYTES in resources.env overrides it); past the cap the event reports too-large. An unreachable or failing receiver (408, 429, 5xx) is retried with backoff for about fifteen seconds, and an upload that is refused or never gets through reports upload-failed.

Together with an Agent’s input files, this makes a round trip by reference: the init downloads notes/brief.md into the workspace, the agent writes out/report.md, and the supervisor uploads it — no document ever sits in the CR, the event stream or status.output.

For the exact wire format your listener receives (the event envelope, lifecycle, file and stream-json payloads, the HTTP contract, and the retry env vars), see the Notification API reference. For a step-by-step setup with the example listener, see Receive notifications.