Skip to main content

client_tool_completion

Function client_tool_completion 

Source
pub async fn client_tool_completion(
    __arg0: State<AppState>,
    __arg1: Path<String>,
    headers: HeaderMap,
    body: Bytes,
) -> Result<Json<Value>, ApiError>
Expand description

POST /v1/client-runs/{id}/client-tool-completion: record that a client-performed tool call finished.

The client ran the call in its own process and is now reporting what happened. Salvor did not witness it, so everything this endpoint can check, it checks before the report becomes history. Requires the X-Drive-Token header.

§Two shapes, and exactly one of them

The body carries output, what the call returned, or error, what went wrong instead when it returned nothing at all. Both, or neither, is a 400: they say opposite things about the same call and this server has no way to pick between them.

The error shape records the same __salvor_error sentinel completion the runtime records when a NATIVE tool exhausts its retries, through salvor_runtime::wire’s own encoder, so the bytes match (see [record_reported_failure]). A failure is not a new event and not a new run state: it is an outcome a completion is allowed to carry, so a recorded failure SETTLES the call exactly as a native one does, and the run carries on with the failure replaying from the log rather than the call happening again.

It refuses, recording nothing, when:

  • the body carries both output and error, or neither (400);
  • the log does not end at a tool intent, or ends at one whose seq is not the one this request names (409 divergence);
  • the pending intent was performed by the SERVER (403): a client must not close a call salvor made, since salvor holds the real result;
  • the declaration says trust_completion = false (403), for a reported failure as much as for a reported result. “It did not land” is a claim about money made by the party that benefits from it being believed, so an untrusted write is left dangling for a person either way;
  • the declaration carries no output_schema AND the body reports an output (403): with nothing to check the report against, the completion is unfalsifiable, which is exactly what the schema exists to prevent. A reported failure carries no value to check and is unaffected;
  • the reported output fails the declared output_schema (400);
  • a require_equal field’s reported value differs from the value the intent recorded (403): the output schema is a shape check and cannot know what was authorized, so a client report may not alter a pinned field;
  • a reported kind names no recorded failure layer (400).

The checks run in that order: the either-or rule first, then correlation, then the trust refusal before any value is compared, then the output shape, then the per-field equality. The output_schema, require_equal, and value-shape checks are skipped on the error path, which is the absence of a value for them to look at rather than a relaxation of the rules.

§Where a refused completion leaves the run, and why nothing else changes

A refusal is not a dead end and needed no new state to express. The log still ends at the recorded ToolCallRequested, and for an Effect::Write the pure fold in salvor-replay ALREADY reports that as RunStatus::NeedsReconciliation, because an uncompleted write intent as the log’s last word is precisely what that status means. POST /v1/client-runs/{id}/resolve already exists to settle it by hand, once a person has verified externally whether the call landed.

So trust_completion = false is fully implemented here, at the completion boundary, and deliberately NOT in derive_state. That fold is a pure function of the log with no access to declarations, and it must stay that way: a log has to mean the same thing to a replay on another machine that has never seen this server’s --client-tool files. A later reader who goes looking for the strict mode in the fold will not find it, and that is the design, not an omission.