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
outputanderror, or neither (400); - the log does not end at a tool intent, or ends at one whose
seqis 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_schemaAND 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_equalfield’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
kindnames 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.