Agent workflow
This page walks through the coordinator recipe: a producer workspace edits and publishes, a dependent workspace stacks on that publication, advances when the producer publishes again, and pins exact snapshots for execution.
The same steps exist in two forms:
- Daemon (
gin serve): every step is agin.v1request. Use the CLI (gin call --json), MCP (gin_call), or a Rust client session. - Embedded: a Rust coordinator uses the versioned
gin_workspace::agent::v1::Agentlifecycle API and hands workspaces to theginCLI orgin mcpfor editing.
The examples below use the daemon form. The shell setup is the one from Getting started.
Requests are conditional and idempotent
Every mutation carries a request ID, an attempt ID, the state it expects and, for workspace writes, a write fence. A retry with the same IDs and identical bytes returns the recorded outcome. The same ID with different bytes is an idempotency conflict. Nothing is retried automatically.
Fork a producer
Send Fork with a fresh workspace ID, no parents and a workspace-absent precondition:
json
{"majorVersion": 1, "requestId": "fork-p", "repositoryId": "<repository>",
"mutation": {"attemptId": "fork-p", "expected": [{
"resourceId": "<producer>", "resourceKind": "RESOURCE_KIND_WORKSPACE", "absent": true}]},
"fork": {"workspaceId": "<producer>"}}The returned state is the private version. Its related_ids contain the store incarnation and the lease epoch, in that order: together with the workspace ID they form the write fence for later mutations.
Edit through the CLI or MCP
Agents read and edit the private tree with gin context, gin read and gin save (or the MCP tools gin_context, gin_read and gin_save).
gin contextreports the incarnation, generation and epoch.gin read PATH --contentreturns the entry and bytes, with the content identity first instable_ids.gin savenames that state, the lease, and an expectation for every path.
sh
gin save --incarnation I --generation G --epoch E \
--lease-incarnation STORE --lease-epoch L --request-id edit-1 \
--path src/lib.rs --expect CONTENT_ID --file ./lib.rs \
--path old.rs --expect OLD_ID --deleteSaves remain private. Retries must reuse the same request ID and payload.
Checkpoint and publish
Send Publish with the observed workspace version, the write fence and an explicit channel precondition (absent for a new channel, or its present state). checkpointId: "current" checkpoints exactly this private version and publishes it:
json
{"majorVersion": 1, "requestId": "pub-1", "repositoryId": "<repository>",
"mutation": {"attemptId": "pub-1",
"expected": [
{"resourceId": "<producer>", "resourceKind": "RESOURCE_KIND_WORKSPACE", "present": <version>},
{"resourceId": "contract", "resourceKind": "RESOURCE_KIND_CHANNEL", "absent": true}],
"fence": {"workspaceId": "<producer>", "incarnationId": "<store>", "leaseEpoch": "<epoch>"}},
"publish": {"workspaceId": "<producer>", "channelId": "contract", "checkpointId": "current"}}The response's related_ids name the immutable checkpoint and the decimal publication sequence. Another client can read it with gin read PATH --snapshot CHECKPOINT --content. Later private edits leave it unchanged.
If an object is too large, publication is refused permanently for that input. Reduce the content, checkpoint again and publish with a new request ID.
Fork a dependent
A dependent forks the published channel at that sequence:
json
"fork": {"workspaceId": "<dependent>",
"parents": [{"channelId": "contract", "publicationId": "1"}]}It then edits its own implementation as above. New publications on contract change nothing in the dependent until it asks.
Advance a dependent
When the producer publishes again, the dependent previews the complete parent set it wants, including parents it keeps:
json
"preview": {"workspaceId": "<dependent>",
"parents": [{"channelId": "contract", "publicationId": "2"}]}Preview changes nothing. Its result is an opaque state ID. If the preview is conflict-free, advance with that ID, the observed workspace version and the write fence:
json
"advance": {"workspaceId": "<dependent>", "previewId": "<preview state id>"}Rules to remember:
- Recompute the preview after any private edit or advance.
- Two proposals at one epoch compete: the first advance wins and the second is refused as stale.
- A parent publication made after your preview does not invalidate it. The newer one is offered by the next preview.
- Advance does not change any pinned snapshot.
Pin before running
Before a command runs, capture a checkpoint or materialised savepoint and acquire a pin:
PinSnapshotwith the workspace ID, a fresh pin ID, the immutable snapshot and a pin-absent precondition.- Read that exact snapshot (
ReadPathwith the snapshot, orReadManifestandReadBlobsfor published checkpoints). Never read the live private tree during the run. - Renew while the run continues, and release when the executor finishes, using the same workspace binding and the observed pin version.
Pins never require or confer a write lease. The executor owns its process tree.
Inspecting and resolving conflicts
A conflicted preview returns a candidate with a durable state.id, its exact workspace version, a page of conflicts and, when available, a resolved tree identity. It never activates anything or edits private work.
To work with it, send another Preview with workspaceId and previewId = state.id, and omit parents (the handle already binds them):
| Field | Purpose |
|---|---|
page | page through conflict entries; reuse the cursor only with the same candidate and workspace version |
read | read an exact candidate path; side selects a zero-based conflict side instead of the resolved tree; offset and limit page file bytes (at most 64 KiB) or directory entries (at most 256) |
resolutions | propose decisions per exact path: inline file content (with the executable bit), delete: true, or a whole entry chosen from one side |
Side zero is always the private snapshot, followed by the exact parent publications. A conflict side with no value is a deletion. Each proposal returns a new candidate handle; advance with that handle. Unresolved candidates refuse activation. Workspace edits, lease changes and parent publication changes invalidate a candidate for new uses.
Resolutions are attributed to the authenticated principal, never to an author string in tool input. A resolution recorded once is reused only for an identical conflict.
Current limits
File-identity and directory-replacement resolutions are refused: use inline bytes for file decisions. Each principal's distinct proposals at one workspace version are bounded (64 MiB, or a sixteenth of the journal quota if less); past that, new proposals are refused with RESOURCE_EXHAUSTED until the workspace changes.
Paths and inline contents use base64 in protobuf JSON input (gin call --json or gin_call). Rendered responses show exact UTF-8 or hexadecimal bytes.
Recovering from a lost reply
An interrupted or timed-out mutation has an unknown outcome:
- Keep its original request and attempt IDs.
- Reconnect and query
RequestOutcomewithoriginalRequestId, or retry the identical request. - A changed payload needs fresh request and attempt IDs.
An uncertain publication blocks other operations on that workspace until its identical retry reconciles it.
The embedded recipe
The embedded form follows the same shape with Rust calls:
- Provision a
Hostper worker and an authorised plane. UseHost::fork_rootfor a producer orHost::forkfor a dependent. - Close the coordinator's workspaces and the
Host, then start the tool subprocess with--host,--repositoryand--workspace. Journals and the worker store hold exclusive locks. - After the tool exits, reopen the workspace, wrap it in
Agent::new, captureagent.version()andcheckpoint; thenpublishto a channel. - A dependent calls
preview(&version, plane, &[(channel, sequence), ...]), checkspreview.candidate().conflicts()andis_consumable(), thenadvance. - Pin before execution, as above.
Preview data belongs to the caller: discard and recompute it after a coordinator restart.
Daemon coverage
The daemon adapter does not advertise multi-parent fork creation, lease management operations or standalone checkpoint RPCs. The embedded coordinator remains available for those.