ADR-0002: One write path: commands through Workspace.commit¶
Status: Accepted Date: 2026-09-28 Deciders: Alex Nodeland
Context¶
Artifacts are changed by many actors: the built-in agent's tools, people through a UI over WebSocket or REST, external agents over MCP, and scripts. When each surface implements its own write logic, validation, merging and notification drift apart. Some writes end up silent, visible neither to other participants nor to the agent, which undermines the point of a shared workspace.
An earlier iteration of this design had tools return their effects as values for the framework to commit after the tool returned. That breaks down on conflicts: by the time the commit fails, the tool has already told the model it succeeded.
Decision¶
Every change is a command submitted through Workspace.commit(command):
- Open a host transaction and load the current state.
- Call
core.commit(command, current, actor), which either returns aCommitResultor raises a typed rejection. - Save the revisions, assign
seq, append the events, and commit. - Return
AppliedorProposedto the caller, or raise the rejection.
Nothing else writes artifacts or events. Run lifecycle facts (a run started, a tool was called) are recorded through Workspace.record, which uses the same transaction and sequencing. Agent tools call commit directly and see the outcome, so a VersionConflict reaches the model as a retry.
Options considered¶
Option A: Commands through one Workspace.commit (chosen)¶
| Dimension | Assessment |
|---|---|
| Complexity | Low |
| Consistency across surfaces | Guaranteed: one code path |
| Conflict feedback to the agent | Direct: the tool sees the rejection |
Pros: surfaces are thin adapters; every change is attributed and published; one place to test.
Cons: the Workspace API must stay small and stable, because everything depends on it.
Option B: Effects returned as values, committed after the tool returns¶
| Dimension | Assessment |
|---|---|
| Complexity | Medium |
| Consistency across surfaces | Good for tools; other surfaces need their own path |
| Conflict feedback to the agent | Broken: the tool has already returned success |
Pros: tools cannot write outside the pipeline. Cons: conflicts need extra machinery to retract a tool result; a second path is still needed for non-agent actors.
Option C: Each surface calls repositories directly¶
| Dimension | Assessment |
|---|---|
| Complexity | Low at first, high over time |
| Consistency across surfaces | Poor: logic is duplicated |
| Conflict feedback to the agent | Depends on each implementation |
Pros: familiar layering. Cons: duplicated accept, merge and validation logic that drifts; silent writes.
Trade-off analysis¶
Option B's appeal was that tools could not bypass the pipeline. Option A keeps that property, because tools can only reach storage through the Workspace, and it also lets the tool see the outcome. Option C is the default shape for many backends, and it is where duplicated write logic comes from.
Consequences¶
- Easier: adding a surface means translating its input into commands.
- Easier: every change produces the same events and change notes, whoever made it.
- Harder: operations that need several changes at once need a composite command, or a transaction spanning several commits, rather than ad hoc writes.
- Revisit if a use case needs writes that are not expressible as commands.
Action items¶
- Define the closed
Commandunion and theCommitResult,AppliedandProposedtypes in core. - Implement
Workspace.commitandWorkspace.recordover the storage protocol. - Add conformance fixtures for every command.