Half the Job Is a Real Answer: Designing for Partial Success

Thirty-one of forty invoices reconciled. Reporting success hides nine; reporting failure throws away thirty-one. The middle is where most runs land, and most designs can't express it.

An agent asked to reconcile forty invoices against the ledger finished thirty-one. It reported success.

The alternative implementation would have reported failure and thrown away the thirty-one correct reconciliations, so someone could run all forty again. Both outcomes are wrong, and they share one cause: the design has only two result shapes, and the run landed between them.

Any agent doing list-shaped work spends most of its time in that middle. Making it representable is a small change with a large effect on how usable the agent is.

Binary outcomes destroy information

A loop that returns either a result or an error forces every run into one of two lies when the truth is partial.

Claiming success produces the silent partial — the most common quiet agent failure there is. The output looks like a completed job, the summary is accurate about what happened and silent about what didn't, and the shortfall is discovered by whoever depended on the missing nine.

Failing the run is honest and wasteful. Thirty-one items of real work discarded, and the retry redoes all forty — which costs the same again and, without idempotency, may double the side effects of the successful ones.

Make partial a first-class result

{ status: "complete" | "partial" | "failed",
  completed: [{item_id, result_ref}],
  skipped:   [{item_id, reason_code, detail}],
  remainder: "retryable" | "blocked" | "impossible",
  resume:    "run_8821:items_32-40"
}

Four fields carry the weight, and each answers a question the caller has:

completed — with enough identity to avoid redoing them. Not a count; the actual item IDs.

skipped, per item, with a reason code. ⚠️ This is where implementations go wrong most often: a run-level summary like "some records were missing fields" is unusable. The caller cannot retry it, route it, or report it. Nine item IDs with missing_fx_rate against seven and no_matching_ledger_entry against two is immediately actionable.

remainder — is the rest worth trying again? Retryable means a transient obstacle. Blocked means it needs something the agent can't obtain. Impossible means don't schedule the retry at all. The caller's behavior differs for each, and asking it to infer this from reason strings is how retry loops get written badly.

resume — a handle that continues from where this stopped, which only works if the completed items are identifiable and the writes are idempotent. Partial success and idempotency are the same design problem viewed from two angles.

What the caller can then do

With per-item reasons, the calling code becomes ordinary:

result = agent.reconcile(invoices)

if result.status == "partial":
    if result.remainder == "retryable":
        retry(result.resume)                       # only the nine
    else:
        queue_for_human(result.skipped)            # with reasons attached
    report(completed=result.completed)             # the 31 still count

None of that is possible with a boolean. The whole value of the pattern is that it moves the decision about the remainder out of the agent — which lacks the context to make it — and into the caller, which has it.

Report the shortfall first

For anything a person reads, lead with what didn't happen:

"Reconciled 31 invoices.""31 of 40 reconciled. 9 need attention: 7 missing an FX rate (INV-203, INV-207, …), 2 with no matching ledger entry (INV-218, INV-224)."

The first sentence is technically true and reads as completion. The second tells someone what to do next and takes the same space. → A partial result presented as an achievement is functionally the same failure as claiming success, because nobody acts on it.

🔍 Where partial results should be measured

Two numbers worth tracking once this exists:

Partial rate by reason code. A reason that dominates is a design finding, not a data problem. Seven of nine failing on a missing FX rate says the agent needs a rate-lookup tool, or the upstream data needs fixing — either way it's a fix that converts the most common partial into a completion.

Completion rate on retry. If retrying the remainder rarely succeeds, retryable is being applied too generously and the retries are pure cost.

The related design rule

The pattern generalizes past batch work. Any agent output that could be incomplete should say so structurally rather than in prose:

  • A search that hit a result limit → truncated: true with the total.
  • A summary that skipped a section it couldn't parse → the section named.
  • An analysis that couldn't reach one data source → which one, and what that omits.

The common failure across all of these is an output that reads as complete because nothing in its shape can express incompleteness. ✅ The fix is always the same: give the result a field for what's missing, and make the caller handle it.

The takeaway

Agents doing multi-item work land between success and failure most of the time, and a two-state result forces a lie in either direction. Return a partial status with completed items identified, skipped items reasoned individually, a classification of whether the remainder is worth retrying, and a resume handle. Then lead every human-facing summary with the shortfall — because a partial result nobody notices is indistinguishable from work that was never done.

Keep reading

Similar posts

Matched on shared tags and category — the more bars, the stronger the overlap with what you just read.