42 — Managed work contracts
Status: implementation complete for shipped managed-work paths. Managed agent turns dispatch contract-owned static flows through the production flow executor; standalone CLI flows remain deliberately unlinked unless a future explicit linkage option is added. Outcome revisions and collaborative goal updates are recorded by the shared outcome-directed runtime (docs/design/52-outcome-directed-runtime.md), while this document remains the durable work-item contract.
Mission
Make complex Vak work explicit, durable, inspectable, resumable, and evidence-backed without slowing down ordinary conversation.
Given a complex request, Vak must be able to answer, from durable state:
- What did the user ask for?
- Which constraints and assumptions were understood?
- What work items were created?
- Which item is ready, running, blocked, delegated, or complete?
- Which parent agent, worker, flow, tool, or human owns each item?
- What evidence supports each result?
- Why is the overall work complete, incomplete, failed, or awaiting input?
This document is an implementation contract for long-running general-purpose agent work, including coding, research, writing, analysis, and operational tasks. An implementing agent must continue through every phase, preserve the invariants below, run the required verification after each phase, and leave a clear handoff if a phase cannot be completed. Do not mark the mission complete because the types or UI exist: the end-to-end behavior, recovery behavior, and tests must pass.
Non-goals
This work must not:
- replace the existing direct agent loop;
- route every message through a planner;
- create a project-management record for every simple answer;
- give workers broader permissions than their parent;
- make model-authored text the source of progress truth;
- allow a model to mark work complete without verification;
- create a second durable store that competes with the session ledger;
- weaken gateway allowlists, permission checks, capability overlays, or brokered tool execution;
- add a dependency without an exact workspace version and justification.
Existing direct chat remains the default compatibility path. Managed work is selected explicitly first and becomes automatic only after the evaluation criteria in this document pass.
Existing foundation to preserve
The implementation builds on:
- append-only session JSONL in
crates/vak-session; SessionLog::derive_messages()as the model-context projection;- the
vak-agentmodel/tool loop and stop gate; PermissionEnginebefore every effectful tool execution;- brokered and sandboxed tools in
vak-tools; - frozen session contracts and capability descriptors;
- child sessions and
WorkerRegistryinvak-agent/src/task.rs; - static flows and dynamic planner in
vak-flow; - goal mode and audited completion in
vak-agent/src/goal.rs; - checkpoints, receipts, activities, SSE, and presentation projections;
- server endpoints and desktop/admin surfaces already used for sessions.
Read the relevant existing design documents and source before changing code:
docs/design/03-agent-loop.mddocs/design/08-permissions.mddocs/design/10-flows.mddocs/design/11-planner.mddocs/design/22-gateway.mddocs/design/30-output-engineering.mddocs/design/33-admin-console.mddocs/design/41-capability-registry.mdcrates/vak-session/src/types.rscrates/vak-session/src/log.rscrates/vak-agent/src/lib.rscrates/vak-agent/src/task.rscrates/vak-core/src/lib.rscrates/vak-server/src/lib.rs
Product behavior
simple request
→ direct agent loop
complex request
→ contract draft
→ deterministic validation
→ resolve material ambiguity
→ activate work items
→ parent/tool/worker/flow execution
→ evidence collection
→ criterion verification
→ complete, continue, block, or ask
The parent session owns the user-facing conversation and final answer. A worker owns only its assigned item and returns evidence to the parent. A flow owns deterministic nodes. A scheduled task is a recurring automation. These are different concepts and must remain different in names and APIs.
Terminology
Work contract
The durable, versioned interpretation of one user objective. It contains the objective, constraints, assumptions, acceptance criteria, work-item definition, and lifecycle status.
Work item
One independently trackable unit of work. It has dependencies, an owner, execution scope, status, and evidence. It is not necessarily a scheduled task and it is not necessarily a worker.
Scheduled task
The existing recurring TaskDef managed by the tasks tool and task store.
It fires later on a schedule. It is not a work item created for every turn.
Worker task
The existing one-shot task tool call. It creates a child session and blocks
the parent tool call until the child finishes. The implementation must attach
the child to a work item when managed mode is active.
Execution profiles
Add a run-scoped profile:
pub enum WorkMode {
Direct,
Managed,
Auto,
}
Direct is the current behavior. Managed always drafts a contract before
effectful execution. Auto evaluates whether a contract is warranted.
Initial rollout requirements:
- default remains
Direct; - CLI supports
vak exec --managed; - server accepts
work_mode: "managed"; - TUI and desktop expose an explicit “track this work” action;
Autois initially disabled or equivalent toDirect;- automatic activation is enabled only after the evaluation gate passes.
Superseded for Auto (docs/design/47-commitment-kernel.md). Auto no
longer evaluates a keyword heuristic. Managed admission is now a consequence
of the intent kernel's horizon axis: session and above run managed. The
is_managed_work_request scan this document's rollout depended on — thirteen
English verbs plus two conjunctions — is deleted; it read "explain what this
and that mean" as durable multi-step work. Direct and an explicit
work_mode: "managed" are unchanged, and an explicit run-scoped mode still
overrides the reading.
Durable model
Add work types to crates/vak-session/src/types.rs or a focused
crates/vak-session/src/work.rs module.
pub struct WorkContract {
pub contract_id: String,
pub revision: u32,
pub source_entry_id: String,
pub objective: String,
pub constraints: Vec<Constraint>,
pub assumptions: Vec<Assumption>,
pub criteria: Vec<WorkCriterion>,
pub items: Vec<WorkItemDefinition>,
}
pub struct WorkItemDefinition {
pub item_id: String,
pub title: String,
pub instructions: String,
pub dependencies: Vec<String>,
pub owner: WorkOwner,
pub required: bool,
pub readonly: bool,
pub path_claims: Vec<String>,
pub criterion_ids: Vec<String>,
}
pub enum WorkOwner {
ParentAgent,
Worker,
Flow { name: String },
Tool { name: String },
Human,
}
pub struct Constraint {
pub constraint_id: String,
pub text: String,
}
pub struct Assumption {
pub assumption_id: String,
pub text: String,
pub requires_confirmation: bool,
pub resolution: Option<String>,
}
pub struct WorkCriterion {
pub criterion_id: String,
pub statement: String,
pub kind: CriterionKind,
pub required: bool,
}
Runtime state is separate from the definition:
pub struct WorkItemState {
pub item_id: String,
pub status: WorkItemStatus,
pub attempt: u32,
pub child_session_id: Option<String>,
pub started_at: Option<DateTime<Utc>>,
pub finished_at: Option<DateTime<Utc>>,
pub blocker: Option<String>,
pub evidence: Vec<EvidenceRef>,
}
Contract status must include Draft, AwaitingInput, Active, Blocked,
Verifying, Completed, Failed, Cancelled, and Unverified.
Item status must include Proposed, Ready, Running, WaitingApproval,
Blocked, ReadyForVerification, Succeeded, Failed, Skipped,
Cancelled, and Interrupted.
Use serde defaults for newly added optional fields so old ledgers remain
readable. Do not use deny_unknown_fields on session ledger entries where
forward compatibility is required.
Ledger events
Add EntryPayload::Work(WorkEvent) to the append-only session ledger.
pub struct WorkEvent {
pub contract_id: String,
pub revision: u32,
pub kind: WorkEventKind,
}
pub enum WorkEventKind {
ContractCreated { contract: WorkContract },
ContractRevised {
previous_revision: u32,
contract: WorkContract,
reason: String,
},
ContractStatusChanged {
from: ContractStatus,
to: ContractStatus,
reason: String,
},
ItemStatusChanged {
item_id: String,
from: WorkItemStatus,
to: WorkItemStatus,
attempt: u32,
reason: String,
},
ItemAssigned {
item_id: String,
owner: WorkOwner,
child_session_id: Option<String>,
},
EvidenceAttached {
item_id: String,
evidence: EvidenceRef,
},
AssumptionResolved {
assumption_id: String,
resolution: String,
},
VerificationRecorded {
criterion_id: String,
result: CriterionResult,
},
}
Implement a pure projector:
pub fn project_work(entries: &[Entry]) -> Result<Option<WorkProjection>, WorkError>;
The projector must enforce:
- one active contract per parent session unless explicitly branched;
- strictly increasing revisions;
- valid item and criterion references;
- valid state transitions;
- dependency existence and acyclicity;
- no completion before required items and criteria pass;
- no model-authored transition directly to
SucceededorCompleted; - deterministic output after repeated projection;
- explicit warnings for interrupted or unreconciled runtime state.
The ledger is the sole durable authority. Runtime registries and UI caches are indexes or control channels only.
Model projection and compaction
The active work state must be visible to the model after every turn and after
compaction. It rides in the request tail (SessionLog::tail_sections)
rather than a parallel prompt path.
The <work_contract> block below renders in the per-turn tail
(docs/design/68-context-engine.md §6), never spliced into history, so it is
neither summarised into a compaction packet nor counted as history.
Project only one bounded control message for the latest active state:
<work_contract revision="3">
Objective: migrate the configuration loader
Required items:
- inspect: succeeded
- implement: running
- verify: ready
Rules:
- do not claim completion while required items remain unfinished
- use the work tool for state changes
</work_contract>
The control projection must:
- be reconstructable from JSONL;
- be bounded in size;
- survive compaction while active;
- not be counted as dropped transcript history;
- disappear from model context after terminal completion;
- never contain secrets or untrusted raw tool output.
Because it is tail material, control state is never summarised into a packet and never duplicated across repeated compactions.
Contract authoring
For managed mode, author a contract through a dedicated structured provider
dispatch. As built, that dispatch lives in crates/vak-agent/src/lib.rs
rather than the separate work_contract.rs this doc originally proposed —
it is recorded with WorkPurpose::Plan through the normal work-receipt
machinery, which is the part that mattered.
The authoring prompt must request strict JSON containing:
- objective;
- constraints;
- assumptions;
- acceptance criteria;
- work items;
- dependencies;
- owner recommendation;
- readonly/path scope.
The runtime must validate the response independently. The model is not trusted to validate its own contract.
Reject contracts with:
- empty objective;
- duplicate IDs;
- unknown dependencies;
- cycles;
- missing required criteria;
- unsupported owners;
- paths outside the workspace;
- invalid or excessive item counts;
- permission or capability escalation;
- unsupported tools or flow names.
Explicit managed mode fails closed on invalid authoring. Auto mode may fall back to direct mode only after recording the reason as an audit event.
Ambiguity and user confirmation
Introduce a typed NeedsInput outcome or equivalent durable status. Ask the
user only when an unresolved assumption materially changes:
- destructive scope;
- workspace, account, or recipient;
- security or permission boundary;
- external side effect;
- cost;
- acceptance criteria;
- output format that cannot be inferred safely.
Low-risk assumptions may proceed but must remain visible in the contract.
The answer to a clarification must append AssumptionResolved before the
next effectful action. A stale answer must not silently modify a newer
contract revision.
Work-management tool
Add a model-visible work tool with operations such as:
get
transition
revise
attach_evidence
request_input
The tool proposes a typed event. It must not write the session ledger itself. The agent validates the proposal, appends the event, and only then returns a successful tool result.
Allowed model transitions include:
ready → running
running → blocked
running → ready_for_verification
blocked → ready
Only deterministic runtime code or the verifier may transition an item to
Succeeded or a contract to Completed.
The work tool itself must pass through the same permission/broker registry
path. Internal state mutation is still an effect and must not bypass the
authorization boundary.
Agent-loop integration
In vak-agent/src/lib.rs:
- Append the user message as today.
- Load the active work projection.
- Drain steering input as today.
- Build the request from the normal session projection plus active work control state.
- Dispatch the provider with normal retry, route, receipt, spend, and timeout handling.
- Authorize every tool call before execution.
- Commit work events produced by the work tool.
- Attach tool, child, flow, and verification evidence.
- Run stop hooks and the work completion gate.
- Run goal verification when goal mode is active.
The work gate must block a final completion when a required item is pending, running, blocked, or awaiting verification; when a child is still live; when a required criterion lacks evidence; or when a newer mutation invalidated prior evidence.
Continuation is bounded by the existing max-turn and stop-guard policies.
Exhaustion produces Unverified or Failed with a clear reason.
Worker integration
Extend the existing task tool arguments with optional:
{
"contract_id": "work-...",
"work_item_id": "inspect_gateway"
}
Add optional lineage fields to SessionHeader:
pub contract_id: Option<String>,
pub work_item_id: Option<String>,
When a child starts:
ItemAssigned
ItemStatusChanged: ready → running
When it finishes, attach a child ledger reference as evidence and transition
the item to ReadyForVerification, Failed, or Cancelled as appropriate.
Preserve all existing narrowing rules:
- child tool set is inherited and narrowed;
- readonly children get read-only tools and permission mode;
- child cannot recursively spawn children;
- child path claims are enforced by the normal resource-claim scheduler;
- child tools independently pass permission and sandbox checks.
Keep WorkerRegistry for live list, steer, follow-up, and stop controls.
The registry must never be the only record of assignment or completion.
Evidence
Use typed references rather than copying large output into work events:
pub enum EvidenceRef {
LedgerEntry { session_id: String, entry_id: String },
ToolResult { session_id: String, tool_use_id: String },
Receipt { session_id: String, entry_id: String },
CheckpointDiff { session_id: String, from_seq: u32, to_seq: u32 },
FlowNode { flow: String, run_id: String, node_id: String },
ChildSession { session_id: String },
ExternalOperation { integration: String, operation_id: String },
}
Plain assistant prose is not sufficient evidence for deterministic criteria. Evidence must be resolvable after restart or be marked unavailable.
Verification
Support typed criteria:
pub enum CriterionKind {
Shell { command: String },
FileExists { path: PathBuf },
FileContains { path: PathBuf, pattern: String },
ToolSucceeded { tool: String },
FlowCompleted { flow: String },
ExternalReceipt { integration: String },
Semantic { statement: String },
}
Verification order:
- Run deterministic checks through brokered tools.
- Re-run regression obligations.
- Resolve child, tool, flow, receipt, and checkpoint evidence.
- Send only remaining semantic criteria to the read-only judge.
- Append criterion verdicts.
- Mark items succeeded only after their required criteria pass.
- Complete the contract only after all required items and criteria pass.
Goal mode composes with this flow:
work contract complete AND goal audit passes = audited completion
The verifier must not mutate the workspace it evaluates. Semantic managed criteria use the independent read-only judge; deterministic criteria are checked before that dispatch. The current goal-mode limitation—judging largely from transcript evidence—must be reduced by adding checkpoint/workspace-diff evidence before claiming production completeness. Shell criteria in managed verification therefore require a configured read-only sandbox and fail closed when one is unavailable. Flow execution can carry a contract/item context, records flow-node evidence, and maps its running item through interruption or verification readiness without allowing the flow to mark the item succeeded directly. The CLI flow entry points currently run without that context; managed contracts must not treat those standalone runs as linked work execution.
Recovery and idempotency
On session reopen, reconcile every Running item:
live child exists → remain running
finished child ledger exists → attach result and verify
no child + readonly/retry-safe operation → interrupted, retry eligible
possible write/external side effect → interrupted, require review
Child sessions persist an append-only terminal marker (completed, failed,
aborted, or max_turns) before their parent observes the result. Recovery
must use that marker; the existence of a child ledger alone is not evidence
that the child finished.
Never automatically replay a possible external mutation. Add an operation identity for retryable effects:
pub struct OperationIdentity {
pub contract_id: String,
pub item_id: String,
pub attempt: u32,
pub operation_id: String,
}
External integrations should use operation_id as an idempotency key where
supported. If an integration cannot prove idempotency, recovery must stop for
human review.
HTTP, TUI, desktop, and admin surfaces
Add authenticated endpoints:
GET /sessions/{id}/work
POST /sessions/{id}/work/confirm
POST /sessions/{id}/work/revise
POST /sessions/{id}/work/items/{item}/retry
POST /sessions/{id}/work/items/{item}/cancel
POST /sessions/{id}/work/items/{item}/reassign
GET /work returns the projected snapshot. It must not expose a merged view
that could be written back as a full replacement and must preserve workspace
boundary checks for every session.
Add typed SSE events:
WorkContractCreated
WorkContractRevised
WorkItemChanged
WorkEvidenceAttached
WorkVerificationFinished
WorkNeedsInput
SSE is a live projection only. Reconnect must fetch the snapshot and rebuild the view from the ledger.
The UI should show objective, revision, item status, owner, child session, blocker, criteria, and evidence links. Progress must never be derived from assistant prose.
Configuration and compatibility
Add a scoped [work] configuration section with defaults:
[work]
enabled = true
default_mode = "direct"
max_items = 20
max_revisions = 8
max_parallel = 4
confirmation = "risk-based"
Unknown keys warn and do not fail. Provider and model remain one atomic route. Work mode is scoped to the run; it must not silently change workspace defaults or an existing frozen session contract.
Do not touch the documented CorePool warm-entry permission limitation while implementing this feature unless a separate, fully tested change is requested.
File-level plan
Expected implementation locations:
| Area | Work |
|---|---|
crates/vak-session/src/types.rs | durable work types and ledger payload |
crates/vak-session/src/work.rs | pure projector and transition rules |
crates/vak-session/src/log.rs | append/query/project control state |
crates/vak-agent/src/lib.rs | contract authoring/validation, work projection, event commit, completion gate |
crates/vak-agent/src/task.rs | contract/item lineage and child evidence |
crates/vak-core/src/lib.rs | work mode and contract admission |
crates/vak-server/src/lib.rs | work APIs, auth, SSE, reconciliation |
crates/vak-admin-ui | admin work inspection and operations |
crates/vak-desktop | tracked-work panel and controls |
crates/vak-config | scoped [work] configuration |
docs/design/00-roadmap.md | implementation status and exit criteria |
Do not create a separate work database. If a query needs indexing, treat the
index as rebuildable cache derived from session JSONL, like vak-store.
Phased implementation
Phase 0 — contract and test harness — complete
- Add this design to the documentation index/roadmap.
- Add fixture builders for contracts, events, evidence, and projections.
- Define compatibility behavior for old ledgers.
- No runtime behavior change.
Exit: fixtures compile, old session tests pass, and the contract invariants are represented by tests.
Phase 1 — durable ledger and projection — complete
- Add work types and
EntryPayload::Work. - Implement append helpers and pure projector.
- Implement transition, revision, dependency, and cycle validation.
- Integrate active control state with
derive_messages(). - Make compaction preserve active control state.
Exit: restart and compaction tests pass; direct mode behavior is unchanged.
Phase 2 — explicit managed mode — complete
- Add
WorkModeand explicit CLI/API/UI controls. - Implement structured contract authoring with
WorkPurpose::Planreceipts. - Implement deterministic validation.
- Implement
NeedsInputand assumption resolution. - Add
GET /sessions/{id}/work.
Exit: a managed request produces a durable validated contract or a clear blocked/input outcome before effectful work begins.
Phase 3 — tracked execution — complete
- Add
worktool. - Commit proposed work events in the agent loop.
- Add work completion gate.
- Attach tool and receipt evidence.
- Combine work and goal verification.
Exit: required unfinished work cannot be reported as complete; verified work can complete; failed verification re-enters execution with findings.
Phase 4 — worker and flow ownership — complete
- Add contract/item fields to child session headers.
- Link task tool calls to work items.
- Attach child ledger evidence.
- Link flow nodes to work items.
- Expose a managed-only
flowtool through the core dispatcher. It validates the contract/item/flow owner tuple before execution, then the executor owns running, node evidence, completion-marker evidence, and final work state. - Add work-aware worker events and UI.
Exit: every delegated item can be traced parent → child/flow → evidence → verdict, including parallel execution.
Phase 5 — recovery and external safety — complete
- Reconcile interrupted items after restart.
- Classify retry-safe operations.
- Add operation identities and idempotency checks.
- Add durable approval/input resume behavior; chat clarification requires the explicit answer: prefix and the API cannot resume unresolved assumptions.
- Never replay uncertain external mutations automatically.
Exit: crash/restart tests demonstrate safe recovery without duplicate side-effects.
Phase 6 — automatic selection — complete
- Add deterministic complexity/risk signals.
- Add
Automode telemetry and fallback reasons. - Run direct-vs-managed evaluation comparison.
- Enable auto only when it improves completion and does not regress simple latency, cost, or safety.
Exit: auto mode is evidence-backed, reversible, and still fails closed when contract authoring is required but invalid.
Required tests
Session and projection
- old ledgers open without migration;
- work events round-trip;
- revisions are monotonic;
- invalid transitions fail;
- dependency cycles fail;
- projection is deterministic;
- active work survives compaction;
- terminal work leaves model context;
- torn/unknown entries remain recoverable with warnings.
Agent loop
- managed authoring occurs before effectful tools;
- invalid managed contract fails closed;
- model cannot mark completion directly;
- unfinished required item blocks completion;
- work tool events are authorized and committed;
- tool failure becomes evidence and model-visible feedback;
- goal and work gates compose;
- max-turn exhaustion is typed and durable.
Workers and flows
- child inherits narrowed capabilities;
- readonly child cannot write;
- child cannot spawn another child;
- child lineage contains parent, contract, and item IDs;
- parallel disjoint items run concurrently;
- overlapping claims serialize;
- child failure maps to item failure;
- flow node status maps to work state without duplicate ownership.
Recovery
- finished child is reconciled after restart;
- readonly interrupted work may retry;
- uncertain write is not replayed;
- duplicate operation ID is rejected or treated idempotently;
- approval/input resume uses the correct contract revision;
- SSE reconnect returns the same projected state.
Surfaces and security
- workspace/session boundary checks apply to single and bulk endpoints;
- gateway chat/bot/workspace policy remains unchanged;
- unattended gateway approval remains fail-closed;
- capability overlays remain restrictive;
- all work mutations appear in audit history;
- no secret appears in contract projection, evidence, or UI responses.
Verification protocol
After every implementation phase, run:
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
Before declaring the mission complete, also run the relevant deterministic scenario suite and verify:
target/debug/vak eval
scripts/scenarios/run_all.sh
If API keys are available, run target/debug/vak eval --live separately and
label it as live-provider evidence. Do not make live-provider success a
requirement for hermetic CI.
Inspect at least one real ledger for each path:
direct session
managed session
managed session with clarification
managed session with child
managed session with failed verification
managed session interrupted and resumed
Confirm that the raw JSONL is append-only, the projected work state is deterministic, and the UI/API agrees with the ledger.
Definition of done
The implementation is complete only when all statements are true:
- simple chat still follows the existing direct loop;
- complex managed work has a durable, versioned contract;
- material ambiguity pauses for input before effectful execution;
- every required work item has an owner and dependency state;
- parent, worker, flow, tool, and human ownership are distinguishable;
- every effectful action passes the existing authorization boundary;
- progress is projected from durable state, not generated prose;
- child sessions and flow nodes are linked to work items;
- criteria and evidence determine completion;
- goal mode and work mode cannot falsely claim success;
- restart does not duplicate uncertain side effects;
- API, SSE, desktop, and admin surfaces expose the same projection;
- old ledgers remain readable;
- all required format, lint, unit, integration, and scenario checks pass;
- documentation and roadmap status describe the shipped behavior accurately.
The final implementation report must name the completed phases, tests run, known limitations, and any deferred work. A partially implemented contract layer must remain marked incomplete rather than being described as production ready.