REQ-F-082: Incomplete-Work Response
Tier: Framework-Automatic | Priority: P1
Source: §79 Work Outlives the Caller's Budget
Addresses: Severity: Critical / Token Spend: High / Time: Critical / Context: Medium
Description
When a call returns before its work ends and the work is not lost, the framework MUST exit INCOMPLETE (14) with a structured response that says how far the work got and how to continue it. Two states share one shape: running, when the work goes on in a background job (REQ-F-081), and paused, when it stopped at a checkpoint (REQ-C-035) and waits for the next call.
INCOMPLETE is neither success nor failure. It is not 0, because REQ-F-001 reserves 0 for an operation completed as intended, and tool analyze && tool report must not run report without a result. It is not TIMEOUT (10), because that code means the work stopped and its progress since the last checkpoint is gone.
The response MUST carry:
ok: falseanderror.code: "INCOMPLETE"error.retryableand the exit'sside_effectsfrom the command's declaredINCOMPLETEExitCodeEntry(REQ-C-033):retryable: truewithside_effects: "none"for read-only work,retryable: falsewithside_effects: "partial"for work that writesdata.state:"running"or"paused"data.job_id: the job or checkpoint identifierdata.progress:{done, total?, unit?}from the command's last progress report (REQ-C-034)data.continue_command: an exact invocation that continues the work, run verbatim. Forrunningit istool job wait <id>. Forpausedit is the original invocation, with--idempotency-key <key>appended when the command writes (REQ-C-007), so that a continuation is never a second, independent rundata.cancel_command: an exact invocation that stops the work and discards its checkpointmeta.budget_ms(REQ-F-080)
tool job wait and tool job status return this response, with exit 14, for any job that has not ended, including jobs of REQ-C-022 async commands.
The agent's loop is part of the contract: run continue_command until the exit is not 14; compare progress.done between continuations, and when it has not grown over three of them, run cancel_command and escalate. An agent never needs to know in advance how long the work takes.
Acceptance Criteria
- A call whose budget runs out on a
detach: truecommand exits14withdata.state: "running"anddata.continue_commandof the formtool job wait <id> - A call whose budget runs out on a
resume: true, detach: falsecommand exits14withdata.state: "paused"anddata.continue_commandequal to the original invocation - On a command declared
mutating, apausedresponse'scontinue_commandcarries--idempotency-key, anderror.retryableisfalse - Running
data.continue_commandverbatim, repeatedly, ends with the same response the work gives with--budget 0 data.progress.donenever decreases from one response of the same job to the nexttool job status <id>exits14for a running job of an async command, not3
Schema
Types: response-envelope.md · exit-code.md
INCOMPLETE (14) in the ExitCode table. The data of the response conforms to:
| Field | Type | Required | Description |
|---|---|---|---|
state |
"running" | "paused" |
yes | running: the work continues in a background job; paused: it stopped at a checkpoint |
job_id |
string | yes | Identifier of the job or checkpoint |
progress |
object | yes | done (integer ≥ 0, never decreasing), total (integer, when known), unit (string, when meaningful) |
continue_command |
string | yes | Exact invocation that continues the work |
cancel_command |
string | yes | Exact invocation that stops the work and discards its checkpoint |
Wire Format
A read-only command handed to the background:
{
"ok": false,
"data": {
"state": "running",
"job_id": "analyze-7f3a",
"progress": { "done": 120000000, "total": 980000000, "unit": "bytes" },
"continue_command": "tool job wait analyze-7f3a",
"cancel_command": "tool job cancel analyze-7f3a"
},
"error": {
"code": "INCOMPLETE",
"message": "Call budget of 30000ms reached at 12%; the work continues in the background",
"retryable": true
},
"warnings": [],
"meta": { "exit_code": 14, "duration_ms": 30004, "budget_ms": 30000 }
}
A writing command paused at a checkpoint:
{
"ok": false,
"data": {
"state": "paused",
"job_id": "import-c19e",
"progress": { "done": 4000, "total": 25000, "unit": "rows" },
"continue_command": "tool import rows.csv --idempotency-key imp-2f81",
"cancel_command": "tool job cancel import-c19e"
},
"error": {
"code": "INCOMPLETE",
"message": "Call budget of 30000ms reached after 4000 of 25000 rows; state is saved",
"retryable": false
},
"warnings": [],
"meta": { "exit_code": 14, "duration_ms": 30870, "budget_ms": 30000 }
}
Example
Framework-Automatic: the framework builds the response from the declaration, the job, and the last progress report.
on budget exhausted:
entry = command.exit_codes[INCOMPLETE]
continue = state == "running"
? "tool job wait " + job.id
: original_argv + (command.writes ? ["--idempotency-key", job.key] : [])
write envelope(ok=false, exit_code=14,
error={code: "INCOMPLETE", retryable: entry.retryable},
data={state, job_id: job.id, progress: job.last_progress,
continue_command: continue,
cancel_command: "tool job cancel " + job.id})
# Agent loop:
response = run(command)
stalls = 0
while response.exit_code == 14:
previous = response.data.progress.done
response = run(response.data.continue_command)
if response.exit_code == 14 and response.data.progress.done <= previous:
stalls += 1
if stalls == 3: run(response.data.cancel_command); escalate
Related
| Requirement | Tier | Relationship |
|---|---|---|
| REQ-F-001 | F | Extends: adds INCOMPLETE (14) to the standard table |
| REQ-F-080 | F | Consumes: the budget whose end produces this response |
| REQ-F-081 | F | Consumes: the job behind the running state and its job wait |
| REQ-C-035 | C | Consumes: the checkpoint behind the paused state |
| REQ-C-034 | C | Consumes: the progress report copied into data.progress |
| REQ-C-022 | C | Composes: job status of an async command uses this response for a running job |
| REQ-C-007 | C | Consumes: the key that makes a paused writer's continuation the same run |
| REQ-F-012 | F | Composes: TIMEOUT remains the response when work stops and is lost |