Skip to content

feat: optionally split output arguments into their own file [ROBO-5900] - #159

Merged
eduard-dumitru merged 4 commits into
mainfrom
feat/output-args-memory-optimization
Aug 12, 2026
Merged

feat: optionally split output arguments into their own file [ROBO-5900]#159
eduard-dumitru merged 4 commits into
mainfrom
feat/output-args-memory-optimization

Conversation

@eduard-dumitru

@eduard-dumitru eduard-dumitru commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Why

A consumer that only forwards the output arguments still has to materialize them. In Serverless the handler reads output.json, parses output into a JToken, stringifies it, converts to byte[], then uploads — roughly 4x the payload in the .NET heap for a byte-for-byte passthrough. At ~80 MB that is the difference between a job completing and an OOM.

This lets the runtime write the arguments to their own file and report only a path, so the consumer can stream it. Producer side only; the consumer change is in hdens.

What

One key, runtime.splitOutputArguments (bool, default false), in uipath.json. When set, __exit__ writes the payload next to the result file and replaces the envelope's output with an absolute outputArgumentsFilePath.

The host chooses neither the path nor the name. Both are derived from the result file, which it already named once via runtime.dir: the directory is the same, and the name inserts .args before the extension (output.jsonoutput.args.json). So the knob has exactly one encoding, the two files cannot land in different directories, and they cannot collide — a result file named output.args.json yields output.args.args.json.

Contract

new envelope key outputArgumentsFilePath (absolute)
opt-in runtime.splitOutputArguments
default off — emitted output byte-identical to before
on write failure faults the run
no job_id not written — same gate as the envelope

status / error / resume / resumeTriggers always stay inline, so faults and suspends still work. Consumers must treat the pointer as optional — absent by default and from older runtimes.

Worth reviewing closely:

  • With the knob off the block is skipped and content is untouched; the only change on that path is hoisting output_payload above the write.
  • The write is gated on job_id like the envelope write below it. --output-file is written job or no job because the caller named a path; this is a modifier on the envelope, not a request for a file, so it follows the envelope.
  • A failed write propagates, exactly like the result-file and --output-file writes either side of it, and the existing handler turns it into a structured RUNTIME_SHUTDOWN_ERROR. An earlier revision degraded to inline instead — dropped, because it writes the same bytes to the same volume (so it cannot rescue a full disk, only defer it) and it would hand the consumer the payload this feature exists to keep out of its heap.

Testing

425 passed; ruff, ruff-format and mypy clean. Covers:

  • knob off → result file byte-identical to the legacy envelope, no stray file
  • knob on → arguments in the sibling file, envelope carries the absolute pointer, no output key
  • the arguments file is a sibling of the result file and never in the CWD
  • the name cannot collide with the result file, whatever outputFile is called
  • nothing is written when there is no job_id
  • --output-file still receives the real arguments when both are set
  • resume / resumeTriggers stay inline
  • a faulted run keeps status/error inline and the advertised file exists
  • a failing write faults the run with RUNTIME_SHUTDOWN_ERROR

Five mutants that previously passed now fail: popping resume alongside output; skipping the write for an empty payload while still advertising the pointer; resolving the path against the CWD; reverting to a fixed filename; and dropping the job_id gate.

Also here: one unrelated CI fix

29b297c is the oldest commit and touches only .github/scripts/force-runtime-override.py, so it reads and reverts independently.

The script joined the existing override-dependencies items with the injected wheel entry but stripped only whitespace, so a multi-line array's trailing comma produced [.., , ..] and uv rejected the file. That is what failed langchain-cross here. The bug is latent on maintest-uipath.yml was skipped on every other recent branch, so this PR is just the first to run it.

Follow-ups (separate PRs, in order)

  1. uipath-python raises its uipath-runtime floor to this version.
  2. uipath-agents-python raises three ceilings that currently exclude it.
  3. hdens consumes the pointer and streams the file into the blob upload (streaming from the POD filesystem through the HTTP Request all the way to Orchestrator)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in optimization to UiPathRuntimeContext to write large output arguments into a separate JSON file and emit only an outputArgumentsFilePath pointer in the result envelope, enabling downstream consumers to stream the payload without materializing it in memory.

Changes:

  • Introduces output_arguments_file in UiPathRuntimeContext and maps runtime.outputArgumentsFile from uipath.json.
  • Updates __exit__ to (optionally) write the output payload to the separate file and replace inline output with outputArgumentsFilePath on successful write (degrading to inline on failure).
  • Adds unit tests covering default-off byte identity, split-output behavior, absolute-path pointer behavior, and degradation-on-write-failure.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/uipath/runtime/context.py Adds the output_arguments_file knob, config mapping, and producer-side split-output write/pointer logic.
tests/test_context.py Adds coverage for split-output contract (default-off identity, pointer semantics, degradation behavior).
pyproject.toml Bumps package version to 0.13.1.
uv.lock Updates lockfile version entry for uipath-runtime to 0.13.1.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/uipath/runtime/context.py Outdated
Comment thread src/uipath/runtime/context.py
@eduard-dumitru
eduard-dumitru force-pushed the feat/output-args-memory-optimization branch 2 times, most recently from 12310d4 to 2f29340 Compare August 11, 2026 07:34
force-runtime-override.py joined the existing override-dependencies items with
the new wheel entry, but only stripped whitespace from them. A multi-line array
normally ends in a trailing comma, so the join produced `[.., , ..]` and uv
refused the file with "extra comma in array, expected value".

This is why langchain-cross failed: the testcase it rewrites in uipath-python
declares override-dependencies across several lines with a trailing comma.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@eduard-dumitru
eduard-dumitru force-pushed the feat/output-args-memory-optimization branch 2 times, most recently from e3c076d to 2c87963 Compare August 11, 2026 15:53
Adds runtime.splitOutputArguments to uipath.json. When set, the output arguments
are written next to the result file and the envelope carries an absolute
outputArgumentsFilePath pointer instead of the inline output value, so a consumer
can stream that file rather than materializing it.

Opt-in and default-off: with the knob unset the emitted output is byte-identical
to before. status/error/resume/resumeTriggers always stay inline. A failed write
faults the run, like the two writes either side of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@eduard-dumitru
eduard-dumitru force-pushed the feat/output-args-memory-optimization branch from 2c87963 to d3fa634 Compare August 11, 2026 16:54
…n job_id

Two collision and lifetime problems, both in the same guard:

The filename was a constant, so naming the result file output.args.json pointed
both writes at one path. The envelope landed last, overwriting the arguments and
carrying a pointer to itself - every write succeeded, so nothing raised and the
consumer read the envelope as the job's own arguments. Inserting the suffix before
the extension instead (output.json -> output.args.json) makes that structurally
impossible: a result file named output.args.json now yields output.args.args.json.

The write was also not gated on job_id, unlike the envelope write below it. A local
run or an inner runtime therefore wrote the full payload to disk and then wrote no
envelope pointing at it. The rule is explicit vs implicit: --output-file is written
job or no job because the caller named a path, whereas the envelope is written only
because a job implies one. This is a modifier on the envelope, not a request for a
file, so it follows the envelope.

Both are pinned: a fixed filename and a missing job_id gate each fail a test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os.makedirs(os.path.dirname(output_arguments_path), exist_ok=True)
with open(output_arguments_path, "w") as f:
json.dump(output_payload, f, default=str)
content.pop("output", None)

@radu-mocanu radu-mocanu Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
content.pop("output", None)
output_payload = content.pop("output",{})

no need for this line above
output_payload = content.get("output", {})

Review asked whether the read above the split could be folded into the pop
inside it. It cannot, and the comment now says so: --output-file is written
whether or not the split runs, so it needs the arguments in a local either way,
and popping to re-insert them would move "output" after "status" in the envelope
and break the byte-identity the default-off path is pinned on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@eduard-dumitru
eduard-dumitru merged commit 78808b7 into main Aug 12, 2026
100 of 101 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants