Documentsagenta
Workflow Run Context and Commit State
Workflow Run Context and Commit State
Type
Topic
Status
Published
Created
Jul 8, 2026
Updated
Jul 8, 2026

Workflow Run Context and Commit State#

Overview#

Workflow Run Context and Commit State describes the end-to-end mechanism by which the Agenta playground injects workflow identity into agent runs, gates what identity information is forwarded based on unsaved-edit detection, and keeps the UI in sync after a revision commit.

Four cooperating layers work together:

  1. Dirty-state detection (frontend) — tracks whether the current variant config matches the last-persisted revision.
  2. Reference forwarding (frontend → service) — conditionally sends application_revision references only when the config is clean, so the service can correctly set is_draft.
  3. RunContext binding (service → runner) — injects workflow identity fields into tool request bodies server-side, invisible to the model.
  4. Post-commit sync — after a revision is saved (manually or via a self-targeting tool), refreshes playground state to point to the new revision.

This machinery primarily supports self-targeting platform tools such as commit_revision, which need accurate identity context to write back to the correct revision.

RunContext Injection (Service Side)#

Introduced in PR #4892, runContext is an optional top-level field added to /run requests. Its structure is:

FieldDescription
workflow.id / slug / version / is_draftArtifact/variant/revision identity
trace.trace_id / span_idActive OpenTelemetry span IDs
session_idSession identifier

Keys are deliberately snake_case to match $ctx.<dotted.path> binding tokens exactly .

The run_context() function in services/oss/src/agent/tracing.py computes this per-turn context from the active OTel span and resolved references. It fails gracefully — missing or invalid contexts return None so the binding field is simply omitted .

PR #4936 added _run_context_reference_from_any() to normalize application* and evaluator* reference keys into the workflow* shape, allowing playground apps and evaluator invocations to use the same self-targeting tool pattern as native workflows.

Runner-Side Binding Resolution#

assembleBody() in services/agent/src/tools/direct.ts accepts the runContext object and processes each call.context binding with { bodyPath: "$ctx.<key>" } :

  1. resolveCtxToken() validates the $ctx. prefix, traverses only own-properties with safe-key validation (no __proto__, constructor, prototype), and returns undefined for missing keys.
  2. Any existing value at the target bodyPath is deep-deleted.
  3. The resolved value is deep-set last, winning over model arguments and static body fields.

This gives a model-invisible guarantee: the model never sees or can override a bound field .

PR #4936 hardened this to fail-closed: if resolveCtxToken() returns undefined, assembleBody() now throws instead of silently no-oping . Direct-call tools that require context bindings (such as commit_revision) will surface an explicit error rather than execute with a missing target field.

Clean/Dirty State Detection#

isVariantDirtyMiddleware.ts tracks per-variant dirty state using MD5 hashes stored in a dataRef map.

Detection logic :

  • After each fetch, hashVariant() computes a hash for each variant.
  • If the hash differs from the stored dataRef[variant.id], compareVariantsForDirtyState() performs a deep comparison, ignoring inputs and __isMutating .
  • dirtyStates[variant.id] is set to true when the comparison detects a difference .

On mutation : the wrapped mutate() re-evaluates dataRef and dirtyStates before committing state changes, using updatedAt timestamp and revision number to decide whether the new hash supersedes the stored reference.

When hashes match, dirtyStates[variant.id] is reset to false , marking the variant as clean.

Conditional Reference Forwarding#

PR #4904 fixed a critical correctness bug: the playground was forwarding application_revision references on every run, regardless of unsaved edits. Because the service derives is_draft solely from revision is None, a dirty run with a forwarded reference was incorrectly marked non-draft.

The fix is in buildAgentRequest() in agentRequest.ts :

const isCommittedRevisionRun = !isDirty && typeof fullReferences?.application_revision?.id === "string"
const references = isCommittedRevisionRun ? fullReferences : null
Conditionreferences valueService is_draft
Clean + committed revision ID presentfullReferencesfalse
Dirty (unsaved edits)nulltrue
Uncommitted local draftnulltrue

The application_id URL query is derived independently from full identity so draft runs remain app-scoped even when references is null .

Post-Commit State Synchronization#

Manual save (saveVariant)#

saveVariant() in playgroundVariantMiddleware.ts runs in three phases:

  1. Mark mutating — sets variant.__isMutating = true to signal in-progress save .
  2. Persist — PUT /api/variants/{variantId}/parameters with serialized ag_config and optional commit_message .
  3. Refresh — calls fetchAndProcessRevisions() with forceRefresh: true, sorts results by createdAtTimestamp to identify the new revision , then:
    • Swaps selected to point to the new revision ID .
    • Stores hashVariant(newRevision) in dataRef[newRevision.id] to mark it clean immediately .

Event-driven refresh (tool-initiated commits)#

PR #4936 introduced a complementary path for commits triggered by self-targeting tools :

  • The Vercel adapter emits a data-committed-revision SSE event with { variantId, revisionId, version } when commit_revision completes — even if the resumed stream doesn't replay the original tool call.
  • The playground handles this event via switchEntity(), refreshing the config without polling.

Patch endpoint safety#

To prevent sparse commit_revision calls from overwriting stored agent config, PR #4936 added /revisions/commit/patch, which uses _deep_merge_revision_data() to merge the incoming patch with the current revision, preserving omitted fields .

Note: PR #4975 (revert) subsequently removed the commit/patch router changes from the API layer. Verify against the current branch before relying on this endpoint.

Workflow Run Context and Commit State | Dosu