SSyncropel Docs

Body kinds

Schema reference for built-in `body.kind` values. Records you can subscribe to, query against, and project from — emitted by the engine during dispatch, inference, and federation.

Body-kind family renamed: `core.agent.*` → `core.work.*`

The family of body kinds that describes goal-structured work was renamed from core.agent.* to core.work.*. The same primitive serves any actor doing goal-structured work — humans, LLM agents, workflows, automation — so the family name reflects that.

What changed:

  • core.agent.* (15 kinds: loop, turn, tool_call, tool_result, loop_outcome, loop_cancel, plan, criteria, evaluation, turn_judgment, note, compensation, artifact, coercion) → core.work.* with the same names.
  • core.agent.aitl.v1core.work.decision_request.v1, with a clearer body shape: requester + decider_did or decider_role + question
    • options + context.
  • core.scribe.delegation.v1 and core.guide.delegation.v1 → unified core.work.delegation.v1 with a delegator_kind field.
  • New kinds: core.work.trust_delta.v1, core.actor.preferences.v1, core.engine.actor_defaults.v1.

What stays the same:

  • Records emitted under the old names before v0.52 remain readable — you can still query them, fold them, and project from them.
  • The 8-record-field envelope (parents, thread, actor, act, body, clock, data_type, judged_by) is unchanged.

What to update:

  • Clients emitting core.agent.* records need to switch to the new names — the engine rejects new emits of the old names.

See the unified body-kinds list below for the v0.52 shape.

body.kind is the body-convention discriminant — a string in scope.category.entity[.version] format that names the schema of record.body. Built-in kinds emitted by the engine are documented here; user-extension kinds (anything outside the core.* and syncropel.* reserved scopes) follow the same grammar but live in their own registries.

For the kind grammar itself (3 vs 4 segments, allowed characters, reserved scopes, version semantics), see the body.kind manifest guide.

Kinds emitted by dispatch

Every spl task dispatch (or any other dispatch path) emits a sequence of records that lets operators reconstruct what happened from the log alone — no log scraping, no subprocess inspection. Subscribe to these kinds to build live observability dashboards; query against them to answer "what did this dispatch cost / why did it fail / where in the codepath did it exit."

syncropel.dispatch.stream_event.v1

One record per Assistant event from the dispatched agent. Carries token deltas (this event) and running totals (cumulative for the dispatch). Survives subprocess crashes — if the final result event never arrives, accumulated tokens and partial cost can still be reconstructed.

{
  "kind": "syncropel.dispatch.stream_event.v1",
  "topic": "stream_event_seen",
  "event_type": "assistant",
  "tokens_in_delta": 1247,
  "tokens_out_delta": 89,
  "tokens_in_accum": 8431,
  "tokens_out_accum": 1102,
  "cost_usd_delta": 0.02145,
  "turn_id": "turn_abc123"
}

syncropel.dispatch.session.v1

Captures the agent CLI's session UUID from the first system init event. Written once per dispatch. Bridges Syncropel records to the agent's own session log (e.g., ~/.claude/projects/<slug>/<session_id>.jsonl for Claude Code) for raw side-of-agent data — tool uses, tool results, full message history. spl task diagnose correlates via this record.

{
  "kind": "syncropel.dispatch.session.v1",
  "topic": "agent_session_captured",
  "session_id": "abc-123-...",
  "project_cwd": "/home/dev/my-project",
  "project_slug": "-home-dev-my-project",
  "model": "claude-opus-4",
  "turn_id": "turn_abc123"
}

syncropel.dispatch.subprocess_exit.v1

Emitted after child.wait() regardless of which completion codepath fired. Records the objective facts — PID, exit code, signal, duration, last stream event seen — that previously only existed as log lines.

{
  "kind": "syncropel.dispatch.subprocess_exit.v1",
  "topic": "subprocess_exited",
  "pid": 12345,
  "exit_code": 0,
  "signal": null,
  "duration_ms": 18400,
  "last_stream_event_type": "result",
  "last_stream_event_clock": 47,
  "turn_id": "turn_abc123"
}

syncropel.dispatch.complete.v1

The terminal completion record. Carries the same fields as the historical dispatch_complete plus three enums describing how the dispatch ended. Use these to distinguish "succeeded normally" from "subprocess crashed" from "we hit a wall-clock budget" in one query.

{
  "kind": "syncropel.dispatch.complete.v1",
  "topic": "dispatch_complete",
  "success": true,
  "cost_usd": 0.34,
  "cost_source": "result_event",
  "completion_codepath": "result_event",
  "failure_reason": null,
  "duration_ms": 18400,
  "turn_id": "turn_abc123"
}

cost_source (where the final cost_usd figure came from)

ValueMeaning
result_eventCame from the terminal result stream event (authoritative).
accumulated_from_eventsComputed by summing per-event usage deltas (used when result never arrived).
unknownNo figure available — stream ended before any cost signal.

completion_codepath (which exit branch fired)

ValueMeaning
result_eventSubprocess emitted a terminal result stream event. The happy path.
line_timeoutper_line_timeout fired — subprocess went silent for too long.
budget_deadlineAbsolute wall-clock deadline (budget_deadline) was reached.
stream_eof_fallbackstdout closed without a result event — child exited or stdout broke.

failure_reason (null on success)

ValueMeaning
nullSuccess.
result_missingstdout closed before a result event arrived.
subprocess_exited_nonzeroSubprocess exited non-zero after closing stdout.
line_timeoutper_line_timeout fired.
budget_exceededAbsolute wall-clock deadline reached.
read_errorstdout read error mid-dispatch.
result_reported_errorresult event arrived but reported is_error: true.

See the dispatch observability guide for example queries against these records — including spl task diagnose <id> and spl logs --trace-id.

Kinds emitted by inference

The inference executor commits a KNOW record on the query thread when the pipeline cannot produce an answer. Every non-success path emits one infer.error.v1 record with a stable body.code.

infer.error.v1

{
  "kind": "infer.error.v1",
  "code": "quorum_not_met",
  "message": "Required min_quorum=3 valid responses; received 2.",
  "details": {
    "min_quorum": 3,
    "valid_responses": 2,
    "total_dispatched": 5
  },
  "partial_responses": ["rec_abc...", "rec_def..."]
}

details carries code-specific context (projected cost, elapsed seconds, missing field, etc.) — no internal Rust error strings leak through. partial_responses carries the IDs of DO records collected before a post-dispatch failure, so you can inspect what the responders did manage to produce.

code taxonomy (10 values)

Maps the 8-step inference pipeline. Every non-success branch emits exactly one of these.

CodeStepMeaning
invalid_query1Query body failed schema / grammar validation — missing required field, shape mismatch, unknown fold function.
no_candidates2Hard-filter produced zero candidates. The responder pool was non-empty but no entry satisfied the predicate set.
no_relevant_candidates3Candidates existed but the relevance scorer dropped every one below the configured threshold (top_k empty).
cost_budget_exceeded4 (pre-dispatch)Projected total cost exceeded side_effects.max_cost_usd (or the namespace default).
responder_unavailable4–5One or more selected responders returned dispatch errors before any response could be collected.
latency_timeout5The side_effects.max_latency_secs deadline elapsed before min_quorum valid responses arrived.
quorum_not_met6Valid (non-error) responses were fewer than fold.min_quorum after filtering error-shaped DOs.
fold_failed7The fold function returned an error (missing evaluator, expression failed, etc.).
answer_shape_mismatch8The folded answer did not satisfy answer_shape.kind / required_fields / schema_ref.
obligation_conflict8Obligation resolution raised a ValidationError conflict that the envelope's strategy could not merge.

Wire-form is snake_case as shown. Match on body.code to route or filter.

Kinds emitted by the work loop

Every run of a work loop (spl work-loop, POST /v1/work/loop, or a chat-actor delegating to one via @agent <goal>) emits a sequence of records that lets you reconstruct what the loop saw, what it tried, what tools it called, and how it terminated — entirely from the records. Subscribe to these on the loop's thread (or query against them) to build live progress UIs and post-hoc audits.

Loop lifecycle

  • core.work.loop.v1 — the loop record itself. Carries goal, tier, ceilings (max_turns, wall_clock_secs, token_budget), and any per-loop task_budget.
  • core.work.turn.v1 — one turn of the loop. Sequenced by body.turn_index; carries the prompt fold, model call summary, and per-turn cost.
  • core.work.tool_call.v1 + core.work.tool_result.v1 — every tool invocation and its result. The tool-call body is permission-checked through the engine; the result body carries success or a structured failure code. Failure feedback is always returned to the agent as a tool result so the next turn can read it and self-correct.
  • core.work.loop_outcome.v1 — the terminal record. body.outcome is one of completed, failed, cancelled, blocked, max_turns, budget_exhausted, guardrail_halt, or awaiting_actor (paused). Non-terminal outcomes carry structured payloads — budget_exhausted includes budget_kind; guardrail_halt includes tool, args_hash, and count.

Halts and signals

  • core.work.coercion.v1 — paired with a blocked outcome. Names the structured reason (tool_unavailable / ambiguous_goal / precondition_failed / decision_required / input_invalid / internal_error), a narrative field, and where relevant a missing_tools array or decision_request_ref. This is how the loop reports an honest "I can't do this" instead of producing noise.
  • core.work.guardrail.v1 — emitted when a repeated-failure pattern is detected. verdict: "block" fires mid-loop when the next attempt at a thrashing signature gets a synthetic failure result returned to the agent (the loop continues — the agent gets to change strategy); verdict: "halt" fires when the loop terminates with a guardrail_halt outcome. Body carries tool, args_hash, count, and threshold.
  • core.work.compensation.v1 — emitted when a loop violates a harness assumption (today: context-reset firings, where the model wraps up prematurely as it nears its context limit). The compensation record names the seam + the violated assumption + context. Operators can review per-actor anomalies via GET /v1/work/compensation/review.
  • core.work.decision_request.v1 — pause for human approval. When a loop needs a decision, it raises one of these and pauses with an awaiting_actor outcome until you respond.
  • core.work.loop_cancel.v1 — built-in cancel. A DO of this kind on the loop thread propagates through the reconciler to the in-process loop, which exits with a cancelled outcome.

Chat-actor delegation (v0.49)

When a Scribe or Guide chat-actor receives a message that opens with @agent <goal>, it delegates to a real loop running under the user's identity (not the chat-actor's), and pair-completes when the loop terminates.

  • core.work.delegation.v1 — Scribe's record that it has handed a goal off to a work loop. body.parent_loop_id references the spawned loop.
  • core.work.delegation.v1 — same shape, for the Guide actor.
  • core.actor.usage.v1 — daily per-actor aggregate (loop count, input/output tokens, cost). Emitted by the usage aggregator; consumed by the cost-preview endpoint to surface "you've used $X.YZ of your $Z.WW daily cap."

For the complete work-loop family (29 kinds total — including artifact, note, coercion, and the v0.39 scribe/guide call/response/error family) see the body-kinds manifest — the manifest is the canonical source, and stays byte-identical with the engine's registry.

Other built-in kinds

Pointers to the rest of the kind surface — full schemas live in their own reference pages.

  • core.workspace.v1 + core.workspace_view.v1 — a workspace (an interactive surface carried as a record) + per-thread-per-actor projection memory. See Workspaces.
  • core.instance.shell.v1 — an instance's Studio chrome (the navigation rail, side panels, and footer that frame every screen) carried as a record. Resolved at GET /v1/instance/shell and in the GET /v1/instance/bootstrap aggregate. See Instance chrome.
  • core.credential.v1 — secrets and external credentials. See Secrets.
  • core.consent.v1 — cross-namespace consent grants for federation. See Consent.
  • core.routing_rule.v1 / core.fold_rule.v1 / core.health_rule.v1 / core.aitl_rule.v1 / core.permission_rule.v1 / core.trigger.v1 — CEL-driven engine config records. See CEL expressions and Routing rules.
  • core.subscription.v1 — Server-Sent-Events subscription manifests.
  • core.invite.v1 — pair-share-invite envelope on th_invites. Fields: invite_id, issuer_did, issuer_key_fp, scope_target (instance / thread:<id> / namespace:<ns>), scopes[], ttl_seconds, max_uses, accept_holder_types[] (guest / federated / *), device_label?, notes?, signature. See Pair, share, invite and the API reference — pair, share, invite.
  • core.invite.event.v1 — invite lifecycle events on th_audit_invites mirror. Discriminated by event_kind: redeemed (carries holder_type, holder_did?, holder_pubkey?, bearer_token_id?, bearer_sa_id?), revoked (carries revoker_actor, revoker_reason?), exhausted (auto-emitted when uses_remaining reaches 0), refused (carries refusal_reason: expired / revoked / holder_type_mismatch / bad_proof / freshness_violation / key_unknown / consent_denied).
  • core.invite_template.v1 — operator's saved invite shapes on th_invite_templates. Fields: template_id, label, ttl_seconds, max_uses, scopes[], accept_holder_types[], notes?. Surfaces in /settings/devices template picker.
  • core.api_token.activity.v1 — bearer last-use telemetry (v0.64 B2). Emitted by the token-index mark_used path on every request authenticated by the token, joined into invite list rows via bearer_token_id. Used by the Last seen column on the operator panel.
  • core.runtime.v1 — pluggable runtime descriptor for extensions. See Extensions.
  • core.artifact.v1 — published artifact records.
  • infer.query.v1 — inference query records. Documented in detail at infer.query.v1 schema reference.

What's next

On this page