Skip to content

Latest commit

 

History

History
195 lines (167 loc) · 12.5 KB

plugin-workflows.md

File metadata and controls

195 lines (167 loc) · 12.5 KB

Plugin-authored durable workflows

The host validates every compiled definition against [WorkflowProductionCapabilities] before persistence or start. This production contract is intentionally narrower than the in-process SDK surface. The current production host accepts Agent, Branch, Repeat, wait-all and fail-fast Parallel, PluginBlock, Input, and Approval. Closure-backed Task is explicitly in-process-only; Retry, FanOut, and retry edges are rejected until their complete durable implementations ship. Fail-fast persists a generation-scoped decision and exact sibling cancellation intents before signaling runtime owners; unsignalled intents survive restart and terminal sibling outcomes return through ordinary attempt observation. Explicit operator retry of a terminal failed activation remains a separate bounded store operation; ambiguous mutation requires explicit repair before any later attempt and is never automatic retry. Deterministic branch and repeat predicates use the current explicit versioned contract and are bounded and validated during production admission. Versioned bounded edge transforms are supported, including immutable run-state and canonical parallel join.left/join.right sources.

Registration and start both rerun production admission. Plugin block nodes must resolve to one byte-equivalent enabled manifest declaration, so a definition cannot be persisted or started with an ownerless or stale block contract.

Durable prompt nodes use the versioned WorkflowPromptConfiguration contract. Skills are not selected, required, or resolved by workflow contracts. Prompt text may ask the ordinary Bcode agent to load a named skill through the normal skill catalog and tool boundary; missing or disabled skills therefore remain ordinary prompt/tool outcomes and never invalidate a workflow definition or widen authority. The prompt's explicit model, tool, timeout, context-target, and structured-output policy remain authoritative.

Execution ownership at wait resolution

Application input/approval resolution acquires verified run execution authority before mutation. The workflow-store owned resolution methods compare artifact, daemon, generation and fencing token inside the same immediate transaction that resolves the wait. Foreign or stale resolutions leave waits untouched. This applies to explicit goal resume checkpoints as well as other workflow gates; it does not grant approval authority or establish communication routes between independent runs.

Explicit goal resume consent

/goal.status identifies a blocked loop approval checkpoint and shows its exact activation ID. /goal.unblock <activation-id> approve|deny resolves that checkpoint through the existing workflow application API. The activation ID is required so a delayed command cannot approve a later checkpoint. Approval resumes from retained goal state; it does not approve the original tool permission or assert that an external dependency is satisfied. Resolve that original request first. This is an explicit user-controlled fallback, not automatic authenticated dependency resolution.

Routed input exchanges and CLI discovery

The same explicitly admitted interaction destination is now carried by pending tool-exchange summaries. TUI and web pending-input hydration and CLI exchange/permission session filters include requests addressed to that destination while preserving the source session identity. Registration, resolution and cancellation invalidate the destination view. Payload semantics remain producer-owned; this does not enable the question tool in question-disabled turns or grant response authority. These are same-daemon live observations, not durable delivery or cross-daemon routing.

Routed permission notifications

Turn execution-options version 7 adds an explicit version-1 interaction_route with a destination_session_id. The application currently admits self routes and routes to the execution's recorded parent association, not arbitrary caller-selected sessions. Workflow dispatch installs this route explicitly; ordinary user turns have no route. It is restored with the admitted turn rather than inferred from which clients happen to be attached.

Pending permission summaries retain the canonical source session and carry the destination separately. TUI and web permission lists include requests addressed to the viewed session. Resolution uses the original permission owner and existing authorization path; routing grants no approval authority and never copies permission history into the destination. Live destination invalidations request a normal snapshot refresh on the destination's session stream as well as workflow-watch connections. A client need not subscribe to workflow events to receive a routed permission or exchange invalidation. Lag on the bounded invalidation channel requires resynchronization rather than silently losing a decision. Permission identities include the daemon instance to avoid reuse across restarts.

Workflow run-view version 5 includes bounded tool_permissions correlated to execution-session links and a separate pending_tool_permissions attention count. The inspector distinguishes these from workflow approval gates. Catalog attention filtering and counts include current execution-session tool permissions before page truncation, so tool-blocked runs appear in needs_attention even without a durable workflow approval gate. The overlay is a live observation and grants no execution authority; durable pending-invocation recovery remains unfinished. Permission registration records the canonical request before exposing it as pending. Resolution records the canonical decision before waking the invocation; persistence failures fail closed and cannot release an approved operation. This ordering is a prerequisite for recovery, not a durable continuation checkpoint.

This is a same-daemon permission-notification implementation, not a durable execution messaging transport. Pending decisions remain daemon-memory observations. Cross-daemon routing, arbitrary execution recipients, explicit associations between independently launched runs, general typed request/response delivery, durable acknowledgments/recovery, and dependency-driven blocked-loop resumption are not implemented here. Existing runs are not retroactively associated or approved.

User questions in background work

Prompt configuration version 4 adds allow_user_questions (default true). Goal agent stages and delegated prompt/task-group workers set it to false. The host carries the setting into persisted turn execution options version 6, filters tools declaring the plugin-owned ask_user capability from model requests, and rejects their invocation before opening an exchange—even when discretionary permissions are bypassed. Other tools and permission checks are unchanged. The restriction is turn-scoped, so later interactive turns in a shared session remain interactive. Agent-authored graph edits from a restricted agent must also disable questions on added or replaced agent nodes; violations reject the edit rather than silently rewriting its identity.

Version 2/3 prompt configurations and older turn options retain interactive defaults. A restricted configuration cannot claim an older contract version, and older executors reject the new versions. Retries use the persisted setting. Existing pending questions are not answered or cancelled by this change.

Tool discovery uses argument-independent ToolList.discovery metadata (policy version 1), provided by each tool-owning plugin. It never prepares a synthetic null-argument invocation. Missing or unsupported discovery policies exclude tools from restricted catalogs; ordinary unrestricted catalogs retain compatibility with older plugins. Rebuild bundled tool plugins with the host to supply these policies. Discovery metadata does not authorize execution: real argument preparation and canonical authorization, including the question restriction, still run at dispatch.

Use typed workflow composition for domain behavior and let the host own durable registration, execution, discovery, and lifecycle state.

Structured loop blockers

New loop/goal definitions evaluate an external_blocker enum (none, approval_required, input_required, dependency_required). A non-none result parks at the durable loop.blocked approval gate before judgement or repeat, including when a contradictory completion flag is supplied. Scheduler passes and reopening the store do not spend another model turn or iteration. The normal authorized workflow approval operation addresses the exact run/node/activation; conflicting stale resolutions cannot change the accepted outcome. Approval means explicit consent to resume the goal, not approval of the underlying tool request or evidence that a dependency resolved. Denial fails the run without scheduling continuation. The gate forwards retained activation input; replacement goal-state input is rejected. Outgoing transforms restore the admitted objective, iteration limit and optional judgement policy from immutable run input, preserve retained iteration/evidence, clear the reported blocker and force completion false. A resume cannot attest successful completion. Existing persisted definitions retain their original input-gate semantics; this change applies to newly authored definitions and does not migrate or resolve existing waits.

This is a reported blocker, not an authenticated dependency reference or permission approval. Automatic correlated dependency resolution, independent-run linkage and cross-daemon delivery remain unfinished. A permission decision does not itself resolve this gate. Existing persisted definitions are not rewritten. The judgement block contract is version 2 with the additional blocker field; old block declarations must fail compatibility checks rather than silently run under the new declaration.

Loop iteration budgets

Plugin start requests carry renderer-neutral WorkflowRunLimitPolicy values; adapters must preserve these rather than replacing them with host defaults. /loop sets the cycle budget to the user's positive max_iterations and derives its node-attempt budget from both agent nodes and the permitted attempts per activation. There is no separate 100-cycle or 1,000-iteration ceiling. The iteration contract supports 1..=u32::MAX; out-of-range input is rejected explicitly. Node-attempt budgets use 64-bit integers (within SQLite's signed integer range), so supporting the full iteration range does not overflow a smaller execution budget. Existing stored integer budgets retain their values and semantics; existing runs are not rewritten. Older clients that cannot represent a wider budget reject it rather than truncate it. Native plugin ABI version 4 covers the changed workflow-start request layout and widened budget type; ABI-3 binaries are rejected. Rebuild native plugins with the matching SDK when updating the host.

Stop conditions, explicit cancellation, retry limits, per-agent timeouts, and execution ownership checks still apply. These changes affect newly started loops, not budgets of already-running ones.

let workflow = WorkflowBuilder::new(
    "review",
    Step::map("review", |input: ReviewInput| review(input)),
)
.build()?;
let spec = WorkflowSpec::new("code-review.review", &workflow)?;
let session_id = /* active persisted session */;
let binding = PluginWorkflowBinding {
    owner_plugin_id: "bcode.code-review".into(),
    workflow_kind: "code-review.review".into(),
    scope_key: session_id.to_string(),
    display_label: Some("Code review".into()),
    single_active: true,
};
let request = PluginWorkflowStartRequest::typed(
    &spec,
    &input,
    session_id,
    binding.clone(),
    Some(stable_retry_id),
)?;
host.start_workflow(request).await?;

Rules:

  • Carry evolving/original context explicitly with WorkflowStateEnvelope<State, Value> when a node accepts or returns a narrower value.
  • Put large retained values in the envelope's typed artifacts references rather than copying bytes inline.
  • Keep per-run values in typed input.
  • Let WorkflowSpec derive the exact content-addressed definition identity.
  • Use the logical workflow kind for product vocabulary and durable binding.
  • Use a caller-stable run ID when retrying after an uncertain start response.
  • Use single_active only when the owner/kind/scope permits one non-terminal run.
  • Find, inspect, pause, resume, or cancel through binding.lookup() and generic host methods.
  • Do not persist workflow attempts, scheduling state, run correlation, or recovery journals in plugins.
  • Keep product UI and commands plugin-owned; keep execution and reconciliation host-owned.