Handoffs between steps and recovery boundaries
A handoff is the durable transfer of a specific result and responsibility from one workflow step to the next. It serves normal execution, observability, human review and recovery after interruption. It must identify what is being transferred, the evidence supporting it and what the consumer is authorized to do.
Status: M2 implements the opt-in handoff-v1 source profile natively in TypeScript, Rust and Python. Use the source checkout for these APIs. The previously distributed 0.3.0 binary packages remain the finite-v1 baseline and do not contain these additions. A background engine, nested work-order accounting and protected summary allowance are separate milestones.
Runtime contract and identity
Section titled “Runtime contract and identity”The handoff contract defines aiws-handoff/1. Work order and mission identify the same assignment under the accepted WO-01–WO-08 decision. A run is an execution within that assignment. This profile supports same-run deliveries and RUN/BRANCH holds; WORK_ORDER hold resolution and nested resource accounting remain M3 and fail with SCOPE_UNSUPPORTED.
The wire schema validates shapes. The 40 original M1 scenarios are a requirements inventory. The executable M2 corpus separately compares committed events, state hashes, rejections, replay and transaction rollback across the native implementations. See the source docs/M2-IMPLEMENTATION.md for measured coverage and limits.
A receipt validates delivery only. Admission reserves work and fixes its inputs; dispatch authorizes an external attempt. Keep these three decisions separate. Releasing a hold preserves spending, attempts, approvals and outstanding exposure. A bounded loop produces a BLOCKED checkpoint at exhaustion; the old finite-v1 journal retains terminal FAILED semantics.
Public API map
Section titled “Public API map”| Purpose | TypeScript | Rust | Python |
|---|---|---|---|
| Import | @aiws/sdk/handoff |
aiws_sdk::handoff_store and handoff |
aiws.handoff_store and aiws.handoff |
| Validate immutable record | new HandoffManifest(value) |
HandoffManifest::from_value(value) |
HandoffManifest(value) |
| Open/create journal | new HandoffStore(path, contract, config) |
HandoffStore::open(path, Some(&contract), Some(&config)) |
HandoffStore(path, contract, config) |
| Reopen existing journal | new HandoffStore(path) |
HandoffStore::open(path, None, None) |
HandoffStore(path) |
| Application boundary | HandoffCoordinator |
HandoffCoordinator |
HandoffCoordinator |
| Durable bytes | FileArtifacts.put/read |
FileArtifacts::put/read through ArtifactProvider |
FileArtifacts.put/read |
| Apply/claim | await host.apply/claim(request) |
host.apply/claim(&request) |
host.apply/claim(request) |
| Committed report | handoffReport(state) |
handoff::report(&state) |
report(state) |
| Bounded metric counts | handoffMetrics(state) |
handoff::metrics(&state) |
metrics(state) |
| Notifications | pendingEvents/acknowledgeEvent |
pending_events/acknowledge_event |
pending_events/acknowledge_event |
HandoffDelivery, HandoffConsumption, HandoffHold, HandoffSummary, HandoffCommand and HandoffInvalidation use the same checked-record pattern. Construction validates shape, not caller authority or current state. Python records are imported from aiws.handoff_records; Rust from aiws_sdk::handoff_records.
Validate an immutable handoff record · Unreleased M2 source; shape validation only
import assert from 'node:assert/strict';import {readFileSync} from 'node:fs';import {HandoffManifest,sha} from '@aiws/sdk/handoff';// Run from the repository root. This is a wire-shape example, not an admission.const fixtures=JSON.parse(readFileSync('spec/handoff-v1/fixtures.json','utf8'));const record=new HandoffManifest(fixtures.valid.find((x:any)=>x.id==='manifest').value);const digest=sha(record.asValue());const detached=record.asValue();detached.result.changed=true;assert.equal(sha(record.asValue()),digest); // Caller mutations cannot change the record.assert.throws(()=>new HandoffManifest({...record.asValue(),unknownField:true}));console.log('PASS: handoff-records');use aiws_sdk::{Result,handoff::sha,handoff_records::HandoffManifest};use serde_json::{Value,json};fn main()->Result<()> { // Run from the repository root. This validates shape, not execution authority. let fixtures:Value=serde_json::from_str(&std::fs::read_to_string("spec/handoff-v1/fixtures.json").unwrap()).unwrap(); let value=fixtures["valid"].as_array().unwrap().iter().find(|x|x["id"]=="manifest").unwrap()["value"].clone(); let record=HandoffManifest::from_value(value)?; let digest=sha(record.as_value())?; let mut detached=record.as_value().clone();detached["result"]["changed"]=json!(true); assert_eq!(sha(record.as_value())?,digest); detached["unknownField"]=json!(true); assert!(HandoffManifest::from_value(detached).is_err()); println!("PASS: handoff-records");Ok(())}import jsonfrom pathlib import Pathfrom aiws import AiwsErrorfrom aiws.handoff import shafrom aiws.handoff_records import HandoffManifest# Run from the repository root. Shape validation does not authorize work.fixtures=json.loads(Path('spec/handoff-v1/fixtures.json').read_text())record=HandoffManifest(next(x['value'] for x in fixtures['valid'] if x['id']=='manifest'))digest=sha(record.as_value())detached=record.as_value();detached['result']['changed']=Trueassert sha(record.as_value())==digesttry: HandoffManifest({**record.as_value(),'unknownField':True})except AiwsError: passelse: raise AssertionError('Unknown field accepted')print('PASS: handoff-records')Create durable material before announcing it
Section titled “Create durable material before announcing it”Use a dedicated host-owned artifact directory. put writes a temporary file, syncs it, publishes an exclusive digest-addressed file and syncs the directory. Existing content must match its digest. Unsupported filesystem durability operations fail. Do not let an untrusted agent write directly into the store directory.
The host is responsible for retention and backups. retentionOwner and retainUntilMs record responsibility and an access boundary; they do not schedule garbage collection. Retain bytes while deliveries, recovery or evidence obligations require them. Credentials and expiring signed URLs belong in a private resolver, never immutable locators.
A custom artifact provider returns bytes. The coordinator independently checks SHA-256, size and any referenced schema, with no automatic remote schema retrieval. Verification is repeated at receipt, admission and dispatch. Use the coordinator’s verified byte copies for external execution. A digest check does not make artifact text safe instructions.
Publish and verify immutable local artifacts · Executable local filesystem example; directory sync required
import assert from 'node:assert/strict';import {mkdtempSync,rmSync} from 'node:fs';import {tmpdir} from 'node:os';import {join} from 'node:path';import {FileArtifacts} from '@aiws/sdk/handoff';const root=mkdtempSync(join(tmpdir(),'aiws-material-example-'));try { const provider=new FileArtifacts(root); // Retention is an owner's obligation; this adapter does not schedule deletion. const ref=provider.put(Buffer.from('reviewed output'),'output:1','text/plain','owner:operator','4102444800000'); assert.equal(Buffer.from(provider.read(ref)).toString(),'reviewed output'); assert.throws(()=>provider.read({...ref,sizeBytes:'1'}),e=>(e as any).code==='ARTIFACT_MISMATCH'); // Attach ref to a manifest only after put succeeds; keep this directory durable. console.log('PASS: handoff-artifacts');} finally {rmSync(root,{recursive:true,force:true});} // Demo cleanup only.use aiws_sdk::{Result,handoff_store::{ArtifactProvider,FileArtifacts}};use serde_json::json;use std::time::{SystemTime,UNIX_EPOCH};fn main()->Result<()> { let root=std::env::temp_dir().join(format!("aiws-material-example-{}-{}",std::process::id(),SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos())); let provider=FileArtifacts::new(root.to_str().unwrap())?; // The retention owner keeps these bytes available; there is no deletion scheduler. let reference=provider.put(b"reviewed output","output:1","text/plain","owner:operator","4102444800000",None)?; assert_eq!(provider.read(&reference)?,b"reviewed output"); let mut changed=reference.clone();changed["sizeBytes"]=json!("1"); assert_eq!(provider.read(&changed).unwrap_err().code,"ARTIFACT_MISMATCH"); std::fs::remove_dir_all(root).unwrap(); // Demo cleanup, never a production retention policy. println!("PASS: handoff-artifacts");Ok(())}from tempfile import TemporaryDirectoryfrom aiws import AiwsErrorfrom aiws.handoff_store import FileArtifactswith TemporaryDirectory(prefix='aiws-material-example-') as root: provider=FileArtifacts(root) # The retention owner must keep bytes available; no automatic deletion is scheduled. ref=provider.put(b'reviewed output','output:1','text/plain','owner:operator','4102444800000') assert provider.read(ref)==b'reviewed output' try: provider.read({**ref,'sizeBytes':'1'}) except AiwsError as error: assert error.code=='ARTIFACT_MISMATCH' else: raise AssertionError('Changed reference accepted') # Attach ref to a manifest after put succeeds. Production storage outlives the process.print('PASS: handoff-artifacts')Drive the coordinator from a trusted host
Section titled “Drive the coordinator from a trusted host”The equivalent examples below create a journal, commit a producer boundary, claim and acknowledge its delivery, admit its consumer, complete the run, reopen the journal and acknowledge safe notifications. They simulate adapter outcomes; they do not run a coding agent or authenticate a real person.
Run the handoff, receipt and recovery protocol · Executable local control simulation; fixed identity, clock and adapter outcomes
import assert from 'node:assert/strict';import {readFileSync,mkdtempSync,rmSync} from 'node:fs';import {join} from 'node:path';import {tmpdir} from 'node:os';import {HandoffStore,HandoffCoordinator,FileArtifacts,activation,handoffReport,sha,type HandoffFacts} from '@aiws/sdk/handoff';// The first acceptance case supplies an approved graph and command templates.// It makes NO external effect calls: its settle command is a simulated adapter result.const demo=JSON.parse(readFileSync('spec/handoff-v1/runtime-cases.json','utf8')).cases[0];const root=mkdtempSync(join(tmpdir(),'aiws-handoff-example-')),path=join(root,'run.db');let store=new HandoffStore(path,demo.contract,demo.config);try { const artifacts=new FileArtifacts(join(root,'material')); artifacts.put(Buffer.from('hello'),'artifact:1','text/plain','human:operator','10000'); // Demo host authority, never request-body assertions. Replace this fixed principal // and allow policy with authenticated middleware and the approved workflow policy. const authority:HandoffFacts={ allowed:true,actorId:'human:operator',actorKind:'HUMAN', policy:{id:'policy',revision:'1'},authorizedAtMs:'10', materials:{},materialErrors:{}, admissions:structuredClone(demo.steps.at(-1).facts.admissions), clearEvidence:['material:1','evidence:1'],restartAllowed:true }; const coordinator=new HandoffCoordinator(store,()=>structuredClone(authority),()=>'10',artifacts); const tokens=new Map<string,string>(); for(const template of demo.steps){ const s=store.snapshot(),request=structuredClone(template.request); request.expectedRevision=s.revision;request.ownerEpoch=s.core.runs.r?.epoch??'0'; const command=request.command,body=command.body; if(body){ command.expectedRevision=request.expectedRevision;command.ownerEpoch=request.ownerEpoch; if(body.type==='commitBoundary'){ const m=body.manifest; m.basis={revision:s.revision,eventHash:s.lastHash}; m.controls.accounting.revision=s.revision; m.inputs=Object.values(s.consumptions).find((c:any)=>c.nodeId===m.producer.nodeId)!.inputs; m.producer.activationId=activation(s,'r',m.producer.nodeId); } if(body.type==='acknowledgeDelivery'){ const d=s.deliveries[body.deliveryId]; body.claimToken=tokens.get(body.deliveryId);body.generation=d.generation;body.handoff=d.handoff; } if(body.type==='admitConsumer'){ const c=body.consumption;c.ownerEpoch=request.ownerEpoch;c.revision=(BigInt(s.revision)+1n).toString(); c.inputs=c.inputs.map((i:any)=>({deliveryId:i.deliveryId,handoff:s.deliveries[i.deliveryId].handoff})); } } if(body?.type==='claimDelivery'){ const response=await coordinator.claim(request);tokens.set(body.deliveryId,response.claimToken); }else await coordinator.apply(request); } const before=sha(store.snapshot());store.close();store=new HandoffStore(path); assert.equal(sha(store.snapshot()),before);assert.equal(handoffReport(store.snapshot()).spent,'3'); // Export only safe projections. A real exporter acknowledges after durable delivery. for(const event of store.pendingEvents()){ assert(event.eventId);store.acknowledgeEvent(event.sequence); } assert.equal(store.pendingEvents().length,0); console.log('PASS: handoff-coordinator');}finally{store.close();rmSync(root,{recursive:true,force:true});}use aiws_sdk::{Contract,Result,handoff::{activation,report,sha,HandoffState},handoff_store::{HandoffStore,HandoffCoordinator,FileArtifacts}};use serde_json::{Value,json};use std::{collections::BTreeMap,time::{SystemTime,UNIX_EPOCH}};fn main()->Result<()> { // Local control simulation: template settle outcomes do not perform real effects. let corpus:Value=serde_json::from_str(&std::fs::read_to_string("spec/handoff-v1/runtime-cases.json").unwrap()).unwrap(); let demo=&corpus["cases"][0];let contract=Contract::from_value(demo["contract"].clone())?; let root=std::env::temp_dir().join(format!("aiws-handoff-example-{}-{}",std::process::id(),SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos())); std::fs::create_dir(&root).unwrap();let path=root.join("run.db"); let store=HandoffStore::open(path.to_str().unwrap(),Some(&contract),Some(&demo["config"]))?; let artifacts=FileArtifacts::new(root.join("material").to_str().unwrap())?; artifacts.put(b"hello","artifact:1","text/plain","human:operator","10000",None)?; // Fixed demo host principal. Production must authenticate and evaluate policy; // a client must never supply this authority object. let authority=json!({"allowed":true,"actorId":"human:operator","actorKind":"HUMAN", "policy":{"id":"policy","revision":"1"},"authorizedAtMs":"10","materials":{},"materialErrors":{}, "admissions":demo["steps"].as_array().unwrap().last().unwrap()["facts"]["admissions"], "clearEvidence":["material:1","evidence:1"],"restartAllowed":true}); let authorize=move|_:&Value,_:&HandoffState,_:&str|Ok(authority.clone()); let mut coordinator=HandoffCoordinator::new(store,authorize,||"10".into(),artifacts); let mut tokens:BTreeMap<String,String>=BTreeMap::new(); for template in demo["steps"].as_array().unwrap(){ let snapshot=coordinator.store.snapshot()?;let s=snapshot.as_value();let mut request=template["request"].clone(); let epoch=s["core"]["runs"]["r"].get("epoch").cloned().unwrap_or(json!("0")); request["expectedRevision"]=s["revision"].clone();request["ownerEpoch"]=epoch.clone(); if request["command"].get("body").is_some(){ request["command"]["expectedRevision"]=s["revision"].clone();request["command"]["ownerEpoch"]=epoch.clone(); let b=&mut request["command"]["body"]; if b["type"]=="commitBoundary"{ let m=&mut b["manifest"];m["basis"]=json!({"revision":s["revision"],"eventHash":s["lastHash"]}); m["controls"]["accounting"]["revision"]=s["revision"].clone(); m["inputs"]=s["consumptions"].as_object().unwrap().values().find(|c|c["nodeId"]==m["producer"]["nodeId"]).unwrap()["inputs"].clone(); m["producer"]["activationId"]=json!(activation(&snapshot,"r",m["producer"]["nodeId"].as_str().unwrap())?); } if b["type"]=="acknowledgeDelivery"{ let id=b["deliveryId"].as_str().unwrap().to_owned();let d=&s["deliveries"][&id]; b["claimToken"]=json!(tokens[&id]);b["generation"]=d["generation"].clone();b["handoff"]=d["handoff"].clone(); } if b["type"]=="admitConsumer"{ let c=&mut b["consumption"];c["ownerEpoch"]=epoch;c["revision"]=json!((s["revision"].as_str().unwrap().parse::<u64>().unwrap()+1).to_string()); for input in c["inputs"].as_array_mut().unwrap(){input["handoff"]=s["deliveries"][input["deliveryId"].as_str().unwrap()]["handoff"].clone();} } } if request["command"]["body"]["type"]=="claimDelivery"{ let (_,token)=coordinator.claim(&request)?;tokens.insert(request["command"]["body"]["deliveryId"].as_str().unwrap().to_owned(),token); }else{coordinator.apply(&request)?;} } let before=sha(coordinator.store.snapshot()?.as_value())?;drop(coordinator); let mut reopened=HandoffStore::open(path.to_str().unwrap(),None,None)?; assert_eq!(sha(reopened.snapshot()?.as_value())?,before);assert_eq!(report(&reopened.snapshot()?)?["spent"],"3"); // A real exporter acknowledges after durable acceptance by its receiver. for event in reopened.pending_events(100)?{assert!(event["eventId"].is_string());reopened.acknowledge_event(event["sequence"].as_str().unwrap())?;} assert!(reopened.pending_events(100)?.is_empty());drop(reopened);std::fs::remove_dir_all(root).unwrap(); println!("PASS: handoff-coordinator");Ok(())}import copyimport jsonfrom pathlib import Pathfrom tempfile import TemporaryDirectoryfrom aiws.handoff import activation, report, shafrom aiws.handoff_store import HandoffStore, HandoffCoordinator, FileArtifacts# Approved command templates for a LOCAL CONTROL SIMULATION. No external effects# run here: settle uses the case's explicitly simulated adapter outcome.demo=json.loads(Path('spec/handoff-v1/runtime-cases.json').read_text())['cases'][0]with TemporaryDirectory(prefix='aiws-handoff-example-') as root: path=str(Path(root)/'run.db') store=HandoffStore(path,demo['contract'],demo['config']) try: artifacts=FileArtifacts(Path(root)/'material') artifacts.put(b'hello','artifact:1','text/plain','human:operator','10000') # This is a fixed demo host identity. Production obtains identity from # authenticated middleware and evaluates the current approved policy. authority={'allowed':True,'actorId':'human:operator','actorKind':'HUMAN', 'policy':{'id':'policy','revision':'1'},'authorizedAtMs':'10', 'materials':{},'materialErrors':{}, 'admissions':copy.deepcopy(demo['steps'][-1]['facts']['admissions']), 'clearEvidence':['material:1','evidence:1'],'restartAllowed':True} coordinator=HandoffCoordinator(store,lambda *_:copy.deepcopy(authority),lambda:'10',artifacts) tokens={} for template in demo['steps']: s=store.snapshot();request=copy.deepcopy(template['request']) request.update(expectedRevision=s['revision'],ownerEpoch=s['core']['runs'].get('r',{}).get('epoch','0')) command=request['command'];body=command.get('body') if body: command.update(expectedRevision=request['expectedRevision'],ownerEpoch=request['ownerEpoch']) if body['type']=='commitBoundary': m=body['manifest'];m['basis']={'revision':s['revision'],'eventHash':s['lastHash']} m['controls']['accounting']['revision']=s['revision'] m['inputs']=next(c['inputs'] for c in s['consumptions'].values() if c['nodeId']==m['producer']['nodeId']) m['producer']['activationId']=activation(s,'r',m['producer']['nodeId']) if body['type']=='acknowledgeDelivery': d=s['deliveries'][body['deliveryId']] body.update(claimToken=tokens[body['deliveryId']],generation=d['generation'],handoff=d['handoff']) if body['type']=='admitConsumer': c=body['consumption'];c.update(ownerEpoch=request['ownerEpoch'],revision=str(int(s['revision'])+1)) c['inputs']=[{'deliveryId':i['deliveryId'],'handoff':s['deliveries'][i['deliveryId']]['handoff']} for i in c['inputs']] if body and body['type']=='claimDelivery': response=coordinator.claim(request);tokens[body['deliveryId']]=response['claimToken'] else: coordinator.apply(request) before=sha(store.snapshot());store.close();store=HandoffStore(path) assert sha(store.snapshot())==before and report(store.snapshot())['spent']=='3' # A real exporter acknowledges only after its receiver durably accepts an event. for event in store.pending_events(): assert event['eventId'];store.acknowledge_event(event['sequence']) assert store.pending_events()==[] print('PASS: handoff-coordinator') finally: store.close()The host authorizer receives the request, current snapshot and time. It must authenticate the actor, evaluate the exact command under current policy, resolve approved admission references, and validate evidence IDs against its trusted evidence store. Never copy client-supplied allowed, actor kind, evidence or admission facts into this callback. The callback’s authorization timestamp is preserved through artifact reads and checked again at commit. A concurrent state change yields a revision conflict; refresh and reauthorize the request.
Pure reducers and direct store.apply calls are trusted/offline APIs. Do not expose them to clients. The coordinator overrides caller material assertions with independent verification. On a failed transaction, no new material handle is available. Serialize calls to a coordinator when using its last-result material accessor; each worker should have its own coordinator and request lifecycle.
Duplicate dispatch rule: TypeScript and Python responses contain committed; Rust responses contain an optional event. A duplicate returns its original logical result with committed: false or event: None. Never perform an external action from that response. A newly committed dispatch still needs an idempotent adapter and outcome reconciliation if its response is lost. The SDK cannot make an arbitrary external API exactly-once.
Checkpoints, holds and intervention
Section titled “Checkpoints, holds and intervention”commitBoundarycommits a checkpoint, immutable manifest, recipient intents and any required hold together. A rollback leaves none of those records visible. Artifacts written before a failed commit can remain orphaned; the host later cleans them up under retention policy.- A non-success BLOCKED checkpoint must carry an appropriately scoped hold and every outstanding producing attempt. FAILED and CANCELED checkpoints terminate the run in this profile while retaining exposure and accepting truthful late settlements. They never resume terminal execution.
claimDeliveryestablishes actor-bound, leased receipt ownership.acknowledgeDeliveryrequires the current claim, matching material and trusted evidence. Lost responses do not duplicate admission. Recovery fences stale epochs and restores unacknowledged claims to pending.admitConsumeratomically fixes the input set and reservation. An ALL join may accept a skipped input only when explicitly listed in immutableoptionalInputs. An ANY join cannot abandon another predecessor’s reserved or active work.placeHoldderives RUN or branch-descendant membership from the pinned graph.resolveHold(RECHECK)releases only a demonstrably cleared hold. An invalidated input stays blocked pending a separately governed replacement; there is no force-clear command.- On material failure the attempted command is rejected without a partial commit. The host records an owned artifact hold using
placeHoldand presents repair options. Repairing the identical bytes permits revalidation; changed bytes require a new result and invalidation/replanning. recoverOwnershipapplies AUTO, HUMAN or RULE policy and preserves admitted work. Uncertain effects require human intervention. It does not restart an agent process, restore a sandbox or schedule tasks.
A hold is an independent gate. The current run lifecycle field is not a complete scheduler aggregate: hosts should combine it with ready-node and hold projections. Task-aware draining, automated timer services and lifecycle aggregation belong to the engine.
Reports, summaries and telemetry
Section titled “Reports, summaries and telemetry”Reports and metric counts derive from committed state without a model call. Counts cover delivery states, open holds, blocked nodes, manifests and consumptions. Notifications carry stable event IDs and mission/run/request correlation; the durable outbox survives restart. Acknowledge after the receiving system has stored an event. Delivery is at least once, so receivers deduplicate by event ID.
No material bytes, locator credentials, summary text or claim token is automatically included in notification projections. Host-assigned identifiers must themselves be non-secret. The full audit export is a privileged record containing command data; never treat it as a redacted telemetry payload. Hosts own export scheduling, rejection counters, alert routing and telemetry retention.
recordSummary records an optional explanation with exact manifest basis and authenticated author. HUMAN attribution requires a human actor. MODEL summaries require a matching, successfully settled governed operation in the same run. The host supplies generation through an already admitted and bounded task; the SDK never starts a paid model call automatically. If the generator is unavailable or no approved budget remains, omit the summary and use the deterministic report. This does not implement M3’s protected summary allowance.
Compatibility and recovery procedure
Section titled “Compatibility and recovery procedure”- Open the original journal with its original profile. Legacy and handoff stores reject each other’s databases; do not relabel or import historical terminal loops into the new decoder.
- Reopen the handoff journal and inspect holds, pending deliveries, reservations and unresolved operations. Replay verifies every event against the native reducer.
- Apply the authorized ownership recovery policy. Investigate uncertain external outcomes before considering another dispatch.
- Restore required immutable bytes and recheck current authorization. Resolve only holds whose actual blockers are clear.
- Resume through the coordinator with the original operation/consumption identities. Do not reconstruct control state from summary prose.
SQLite uses FULL synchronous WAL transactions. A failed callback before commit is tested for rollback; close/reopen and lost-response paths are tested separately. Hardware power-loss durability still depends on the filesystem and device honoring synchronization. History compaction and months-long operational qualification remain later work.
Design rationale and remaining engine work
Section titled “Design rationale and remaining engine work”The following design rationale includes requirements for later engine milestones. The API boundary above states what M2 implements.
Two layers of a handoff
Section titled “Two layers of a handoff”The authoritative layer is machine-readable: immutable IDs, revisions, artifact digests, outcomes, outstanding effects, policy references and delivery state. The explanatory layer is a human/agent-readable summary derived from that record. The summary may explain intent and caveats, but cannot grant authority, change criteria or replace the underlying evidence.
The engine should create a minimal handoff automatically at every committed step boundary. An agent can supplement it within the reserved allowance. During an abrupt power failure no final agent call is possible; the next process must rebuild the handoff from already-committed state.
Proposed handoff record
Section titled “Proposed handoff record”| Field family | Purpose and required interpretation |
|---|---|
| Identity | handoff ID, schema version and immutable revision |
| Ownership | work-order, workflow/run, producer node, logical operation and producing attempt IDs |
| Basis | committed source event/sequence, plan/graph revision and authoritative state reference |
| Delivery | recipient node or recipient set, delivery identity, claim generation and acknowledgement |
| Artifacts | immutable reference, digest, media type, schema/version, size and declared retention |
| Outcome | result disposition, completed criteria, pending obligations and uncertain effects |
| Controls | current policy/approval references and recorded resource accounting; these are references, not new grants |
| Context | approved intent, relevant decisions, assumptions and limitations; no hidden reasoning requirement |
| Resume | next eligible task/checkpoint and blockers, with required revalidation |
| Summary | human-readable narrative, its author/generator and the exact basis revision |
Artifact content must be durably available before the handoff announces it. For a local file adapter, that can require durable file writes and directory metadata before committing a reference. For an object store, verify successful upload and its immutable reference before the ledger transaction. Orphan uploads can be garbage-collected; a committed handoff must never point at an upload merely planned in memory.
Proposed commit and delivery sequence
Section titled “Proposed commit and delivery sequence”- The producer completes or reconciles its effect and validates its output contract.
- It durably stores output artifacts and obtains immutable references and digests.
- The engine atomically records node completion, the handoff manifest and downstream delivery intents.
- An eligible consumer claims its delivery under current ownership, authenticates its authority and verifies artifact/schema/plan compatibility.
- The consumer durably acknowledges receipt of that exact handoff revision before starting governed work.
- Its eventual completion creates another handoff linked to the consumed revision.
An acknowledgement means “received and validated,” not “task succeeded.” Separate consumer execution and acceptance records establish those outcomes. Use one delivery identity per consumer at fan-out; a join records exactly which predecessor handoffs it consumed. Retries preserve logical identity while changing attempt identity.
No consumer should execute untrusted instructions hidden inside artifact text as engine policy. Context is input material. The authorizer and approved workflow decide which capabilities may be used.
Failure cases and required behavior
Section titled “Failure cases and required behavior”| Failure | Proposed behavior |
|---|---|
| Artifact stored, ledger commit fails | No delivery becomes visible; retain or collect orphan artifact |
| Node completion commits, process crashes | Delivery intent survives in the same transaction |
| Delivery is duplicated | Same consumer delivery ID prevents duplicate admission |
| Consumer crashes after claim | Resolve ownership before another consumer proceeds |
| Receipt acknowledged, execution crashes | Resume from consumer state; acknowledgement alone is not proof of an effect |
| Artifact missing or digest mismatches | Block consumer, retain evidence and require repair/review |
| Plan changes before consumption | Reject stale assumptions or explicitly authorize a compatible transition |
| Producer result is invalidated | Mark dependent handoffs stale; pause affected downstream work and assess already-applied effects |
| Branch skipped | Record the disposition so joins do not wait forever for a nonexistent success handoff |
| External effect remains uncertain | Issue a blocked handoff for human review; never represent it as ready work |
| Summary generation fails | Preserve the machine record and provide an engine-generated basic report |
Current SDK recipe
Section titled “Current SDK recipe”Build a human-readable handoff from a verified audit snapshot · Executable application recipe
import {readFileSync} from 'node:fs';import {parseContract} from '@aiws/sdk';import {SqliteStore,importAudit} from '@aiws/sdk/sqlite';const store=new SqliteStore(':memory:',parseContract(readFileSync('examples/guide/contract.json','utf8')));try { store.apply({type:'startRun',runId:'r'},'10'); const audit=store.exportAudit(); const state=importAudit(audit); // derive the report from this exact exported state const handoff={ format:'example-handoff/1', // application format, not an AIWS wire command missionId:state.contract.missionId, revision:state.revision, auditDigest:JSON.parse(audit).digest, spent:state.spent,reserved:state.reserved, unresolved:Object.values(state.attempts).filter(a=>['DISPATCHED','UNKNOWN'].includes(a.status)).map(a=>a.id), nextDecision:'Inspect current authority and pending work before resuming.' }; console.log(JSON.stringify(handoff,null,2));} finally { store.close(); }use aiws_sdk::*;use aiws_sdk::sqlite::{SqliteStore,import_audit};use serde_json::{json,Value};fn main() -> std::result::Result<(),Box<dyn std::error::Error>> { let contract=Contract::parse(&std::fs::read_to_string("examples/guide/contract.json")?)?; let mut store=SqliteStore::open(":memory:",Some(&contract))?; store.apply(&Command::from_value(json!({"type":"startRun","runId":"r"}))?,"10",None)?; let audit=store.export_audit()?; let state=import_audit(&audit)?; let s=state.as_value(); let bundle:Value=serde_json::from_str(&audit)?; let unresolved:Vec<_>=s["attempts"].as_object().unwrap().values() .filter(|a|a["status"]=="DISPATCHED"||a["status"]=="UNKNOWN") .map(|a|a["id"].clone()).collect(); println!("{}",json!({"format":"example-handoff/1","missionId":s["contract"]["missionId"], "revision":s["revision"],"auditDigest":bundle["digest"],"spent":s["spent"],"reserved":s["reserved"], "unresolved":unresolved,"nextDecision":"Inspect current authority and pending work before resuming."})); Ok(())}import jsonfrom pathlib import Pathfrom aiws import parse_contractfrom aiws.sqlite import SqliteStore,import_auditcontract=parse_contract(Path('examples/guide/contract.json').read_text())with SqliteStore(':memory:',contract) as store: store.apply(dict(type='startRun',runId='r'),'10') audit=store.export_audit() state=import_audit(audit) handoff=dict(format='example-handoff/1',missionId=state['contract']['missionId'], revision=state['revision'],auditDigest=json.loads(audit)['digest'], spent=state['spent'],reserved=state['reserved'], unresolved=[a['id'] for a in state['attempts'].values() if a['status'] in ('DISPATCHED','UNKNOWN')], nextDecision='Inspect current authority and pending work before resuming.') print(json.dumps(handoff,indent=2))The example imports one audit bundle and derives the report from that exact snapshot, avoiding a separate live-state read that could describe a later revision. The resulting example-handoff/1 object is application data, not an AIWS Command. It intentionally prints a report rather than pretending to implement the proposed queue or artifact protocol.
Today you can include additional application artifact references in a completeNode result and validate their shape with outputSchema. Verification of artifact bytes, cross-step input mapping, transactional handoff delivery and acknowledgements must be supplied by the application or future engine. Never add handoff fields to closed wire commands or contracts; they will be rejected.
Budget and intervention rules
Section titled “Budget and intervention rules”The user-approved design protects a small reserve within the existing allowance for checkpointing and handoff. Main work cannot borrow it automatically. Agent-written summaries use that reserve and stop when it is exhausted. The engine-generated record does not depend on another paid model call.
A branch limit holds that branch and dependent work; a work-order limit holds all descendants. Human intervention is required for uncertain outcomes and any change to agent permission or resource limits. Resuming later preserves spent resources, outstanding reservations and approval history.