Skip to content

Orchestrator ↔ Agent messages

This layer carries job dispatch commands and execution status reports between customer-deployed orchestrators and agents.

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.ts

Error fields never carry raw exception text

Section titled “Error fields never carry raw exception text”

Nine orchestrator-to-agent fields describe a failure in free text: error on event.emit.response, agent.api.response, artifacts.upload.response and artifacts.download.response, and reason on step.approval-resolved, artifacts.upload.complete.ack, auth.failure, job.cancel and job.concurrency.ack.

None of them ever carries the text of an orchestrator exception. The rule is the same in every family:

  • A failure the workflow author can act on — an unknown job context, a rejected upload name, an approver’s decline note, an expired approval window — rides the orchestrator’s own return value with fixed, author-facing wording. It stays specific enough to fix the workflow without operator log access.
  • An internal failure is recorded in the orchestrator’s own logs with the full exception, and the agent receives a safe fixed string that classifies the failure without naming any infrastructure.

The reason is the trust boundary: the agent executes customer workflow code, and whatever these fields carry is readable by that code and persisted into the author’s step logs. Raw exception text there would disclose database and storage endpoints, constraint names and internal identifiers to anyone who can write a workflow.

A new orchestrator-to-agent failure field inherits this rule; it does not need its own entry here.

These messages carry job dispatch and cancellation, agent registration, and the status, log and heartbeat reports an agent sends back while it executes.

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.ts

Dispatches a job to an agent for execution. Contains everything the agent needs to clone, configure, and run the job.

FieldTypeRequiredDescription
type"job.dispatch"YesMessage discriminator
messageIdstringYesUnique message ID
runIdstringYesExecution run ID
jobIdstringYesJob ID within the run
repoUrlstringYesGit repository URL
refstringYesGit ref to checkout (branch name or SHA)
shastringYesCommit SHA
lockFileUrlstringYesURL to fetch the lock file
jobConfigRecord<string, unknown>YesJob configuration from the lock file (LockJob or LockDynamicJobFn)
timestampnumberYesUnix timestamp (milliseconds)
tokenstringNoShort-lived GitHub installation token for private repo clone
secretsRecord<string, string>NoOrchestrator-provided secrets to merge into step environment
namespacedSecretsRecord<string, Record<string, string>>NoNamespaced secrets by context name: { 'context-name': { KEY: 'value' } }
maxLogSizeBytesnumberNoMax log size per step in bytes (agent defaults to 10MB)
sourceTarUrlstringNoPre-signed URL to the .kici/ source tarball (skips agent-side clone). Agent extracts into the work directory and imports the workflow .ts via the shared TypeScript loader hook.
sourceTarHashstringNoWorkflow contentHash (NOT the tarball-bytes hash). The agent re-computes this hash against the extracted source to detect drift against the lock file; the tarball bytes themselves are trusted via the orchestrator-signed S3 GET URL.
depsUrlstringNoPre-signed URL to the node_modules tarball (skips npm install)
depsHashstringNoSHA-256 hash of the dependency tarball bytes, used for streaming integrity verification on download
requestIdstringNoTrace ID (UUIDv4) from the originating webhook event
runPublicKeystringNoBase64-encoded X25519 public key for encrypting secret outputs
upstreamJobOutputsRecord<string, Record<string, unknown>>NoOutputs from upstream dependency jobs, keyed by job name
sourceAuthGitAuthNoStructured clone auth for the source repo. Preferred over token; when both are set they must agree (sourceAuth.kind === 'basic' and sourceAuth.secret === token) or the dispatch is rejected.
workflowAuthGitAuthNoStructured clone auth for the workflow repo in a global-workflow dispatch where the workflow is authored on a different source than the source repo. Absent for same-provider global workflows (agent reuses sourceAuth for both clones).
npmRegistriesNpmRegistry[]NoPrivate npm registries the agent should authenticate against before npm install. Each entry’s token is the resolved value (the orchestrator already looked it up via the per-environment secret resolver and protection-rule gates have already passed). Untrusted contributors get an empty list.
installEnvSecretsRecord<string, string>NoExtra resolved secrets to project as env vars on the install subprocess. Keyed by the bare secret name (the qualified env: prefix is stripped at resolution time). For use with a customer-committed .kici/.npmrc containing ${VAR} placeholders.

NpmRegistry:

FieldTypeRequiredDescription
urlstringYesRegistry URL
scopestringNonpm scope this registry serves (e.g., @my-org)
alwaysAuthbooleanYesSend credentials on every request (not just authenticated routes)
tokenstringYesResolved auth token for the registry

GitAuth:

FieldTypeRequiredDescription
kindenumYesOne of: basic (HTTPS Basic auth, PAT/password) or ssh (SSH private key)
userstringNoBasic-auth username (omit for SSH; defaults filled in by the provider, e.g. x-access-token for GitHub-style PATs)
secretstringYesBasic-auth password/PAT, or PEM-encoded SSH private key
sshHostKeyPolicyenumNoSSH-only. accept-new trusts first-seen host keys; pinned requires sshKnownHostsPem
sshKnownHostsPemstringConditionalSSH-only, required when sshHostKeyPolicy === 'pinned'. OpenSSH known_hosts content

requestId persistence through the dispatch queue: When a job is dispatched immediately (agent available), requestId is read from the current AsyncLocalStorage context. When a job is queued (no agent available) and later drained, the requestId is persisted in the dispatch_queue database table and restored when the job is dequeued. This ensures the job.dispatch message always carries the original webhook’s trace ID, enabling end-to-end traceability even for delayed dispatches.

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.tsjobDispatchSchema

Cancels a running or queued job on the agent.

FieldTypeRequiredDescription
type"job.cancel"YesMessage discriminator
messageIdstringYesUnique message ID
runIdstringYesExecution run ID
jobIdstringYesJob ID to cancel
reasonstringYesHuman-readable cancellation reason
forcebooleanNoWhen true, force-cancel immediately without waiting for hooks

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.tsjobCancelSchema

Acknowledges agent registration and sends confirmed config back to the agent. The agent transitions to registered state only after receiving this message (with a 10s fallback for backward compatibility with older orchestrators).

FieldTypeRequiredDescription
type"register.ack"YesMessage discriminator
agentIdstringYesConfirmed agent identifier
labelsstring[]YesConfirmed capability labels
scalerManagedbooleanNoWhether agent is managed by auto-scaler (default: false)
pendingDispatchbooleanNoWhen set, the orchestrator’s scaler bound a specific queued job to this agent at spawn time and the job.dispatch message is in flight. Scaler-managed agents that see this flag must not arm the short KICI_SCALER_IDLE_TIMEOUT timer on register — the KICI_SCALER_PENDING_DISPATCH_TIMEOUT safety net still applies if the dispatch never arrives.
capabilitiesobjectNoOptional agent-facing capabilities this orchestrator supports (absent on orchestrators that predate capability advertisement). Every flag is optional and absent means unsupported, so a newer agent falls back to the unnegotiated behavior. Today: artifactCompleteAck — the orchestrator acks artifacts.upload.complete.

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.tsregisterAckSchema

Sent immediately after connection as the first message. The orchestrator responds with register.ack to confirm registration and provide config. The agent transitions to registered state upon receiving the ack. A 10s fallback timer allows backward compatibility with older orchestrators that do not send register.ack.

FieldTypeRequiredDescription
type"agent.register"YesMessage discriminator
messageIdstringYesUnique message ID
agentIdstringYesUnique agent identifier
labelsstring[]YesCapability labels for job routing
maxConcurrencynumberNoMaximum concurrent jobs this agent handles (defaults to 1)
platformstringNoAgent platform (os.platform(), e.g., linux, darwin)
archstringNoAgent architecture (os.arch(), e.g., x64, arm64)
versionstringNoAgent version (e.g., "0.0.1")
inFlightJobsInFlightJob[]NoJobs still running on reconnection (enables job recovery)
hostnamestringNoMachine hostname (os.hostname())
osReleasestringNoOS kernel release (os.release())
osVersionstringNoOS version string (os.version())
totalMemoryMbnumberNoTotal system memory in MiB
cpuCountnumberNoNumber of logical CPUs
nodeVersionstringNoNode.js version (process.versions.node)
runningAsUserstringNoUsername of the OS user running the agent process
runningAsUidnumberNoUID of the OS user running the agent process

InFlightJob: { jobId: string, runId: string }

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.tsagentRegisterSchema

Periodic capacity update. Tells the orchestrator how many job slots are available.

FieldTypeRequiredDescription
type"agent.status"YesMessage discriminator
messageIdstringYesUnique message ID
agentIdstringYesUnique agent identifier
activeJobsnumberYesNumber of currently running jobs
memoryUsedMbnumberNoUsed memory in MiB (os.totalmem() - os.freemem())
memoryAvailableMbnumberNoAvailable memory in MiB (os.freemem())
uptimeSecondsnumberNoSystem uptime in seconds (os.uptime())

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.tsagentStatusSchema

Reports a job execution state transition. Sent at each lifecycle boundary (queued, running, success, failed, etc.).

FieldTypeRequiredDescription
type"job.status"YesMessage discriminator
messageIdstringYesUnique message ID
runIdstringYesExecution run ID
jobIdstringYesJob ID within the run
stateenumYesOne of: pending, queued, running, recovering, cancelling, success, failed, cancelled, skipped, timed_out_stale, drift_dropped, unroutable
timestampnumberYesUnix timestamp (milliseconds)
dataRecord<string, unknown>NoOptional state-specific data (error messages, timing, etc.)
droppedJobsstring[]NoJob names dropped by determinism drift (agent re-eval produced fewer jobs than expected)
secretOutputsRecord<string, { agentPublicKey: string, encrypted: string }>NoEncrypted secret outputs from agent (present on job success when secret outputs exist)

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.tsjobStatusSchema

job.status (and step.status below) ride the agent’s durable send path: when the WebSocket is down or the agent has not yet re-registered, the frame is enqueued in the agent’s reconnect buffer and replayed after register.ack, in order, ahead of new frames. A terminal transition (success/failed/cancelled) reported the instant the connection drops is therefore delivered on reconnect rather than lost — the same replay guarantee log lines have. The orchestrator treats a re-delivered terminal frame idempotently (a completed dispatch row is not reopened). Only connection-liveness frames like job.heartbeat remain unbuffered.

The upstream tier closes the complementary gap: if a run’s terminal transition is lost on a still-live connection (never disconnected, so no reconnect replay fires), a periodic reconciliation sweep re-reads the run’s current state from the owning orchestrator and settles the run. A run therefore reaches its terminal state even when the deciding frame is dropped mid-flight, not only when the connection drops and reconnects.

Positive dispatch acknowledgment. Sent the moment the agent receives a job.dispatch and accepts it — after the drain and busy checks pass, before execution begins. It resolves the orchestrator’s dispatch-ack deadline (see below) so a dispatch that actually arrived is never mistaken for a lost one.

FieldTypeRequiredDescription
type"job.ack"YesMessage discriminator
messageIdstringYesUnique message ID
runIdstringYesExecution run ID
jobIdstringYesJob ID within the run
timestampnumberYesUnix timestamp (milliseconds)

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.tsjobAckSchema

Explicit dispatch rejection. Sent when the agent cannot accept a job.dispatch — it is already running a job (busy) or is draining (draining). Every job.dispatch is answered: accepted with job.ack (a job.status with state running also resolves the deadline) or refused with this message. On receiving it the orchestrator undoes its dispatch accounting and requeues the job for another agent. An unanswered dispatch is recovered by the dispatch-ack deadline (below) and by disconnect-time triage.

FieldTypeRequiredDescription
type"job.reject"YesMessage discriminator
messageIdstringYesUnique message ID
runIdstringYesExecution run ID
jobIdstringYesJob ID within the run
reasonenumYesOne of: busy, draining
timestampnumberYesUnix timestamp (milliseconds)

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.tsjobRejectSchema

A job.dispatch message can be lost in transit — most visibly when a scaler-managed agent tears down inside its post-job idle window while a dispatch is in flight. To make “dispatched but never received” detectable, every job.dispatch carries a deadline:

  • When the orchestrator sends a dispatch it stamps a deadline (dispatch_queue.ack_deadline) and arms an in-memory timer. The deadline starts when the message is actually sent, not before the secret-merge and token-mint preparation.
  • The deadline is resolved by any answer: job.ack (accept), job.reject (refuse), or a job.status with state running (which doubles as an ack in case the ack itself was lost).
  • If no answer arrives in time, the dispatch is treated as lost: the orchestrator requeues the job (reusing the same attempt budget and scaler-consult machinery as a rejected dispatch) and disconnects the unresponsive agent with the 4031 close code. A scaler-managed agent is then destroyed by its normal lifecycle; a static agent reconnects and re-syncs.

The deadline survives an orchestrator restart or leader switch: it is persisted in dispatch_queue, re-armed from the persisted rows on boot, and any deadline that elapsed while no coordinator was watching is swept on the leader. The default is 10 seconds (KICI_DISPATCH_ACK_TIMEOUT_MS), overridable per org via org_settings.dispatch_ack_timeout_ms (kici-admin org-settings dispatch-ack).

Reports a step-level execution state transition. Sent for each step within a job.

FieldTypeRequiredDescription
type"step.status"YesMessage discriminator
messageIdstringYesUnique message ID
runIdstringYesExecution run ID
jobIdstringYesJob ID within the run
stepIndexnumber (int >= 0)YesZero-based step index
stepNamestringYesHuman-readable step name
stateenumYesOne of: running, success, failed, skipped
timestampnumberYesUnix timestamp (milliseconds)
dataRecord<string, unknown>NoOptional state-specific data (error, duration, etc.)
step_typeenumNoDistinguishes regular steps from hook executions: step, hook:onCancel, hook:cleanup, hook:onSuccess, hook:onFailure, hook:beforeStep, hook:afterStep
secretsAccessedstring[]NoSecret key names accessed by this step via ctx.secrets.get()/expose(). Never contains values
logBytesStreamednumber (int >= 0)NoTotal raw bytes streamed by this step’s LogStreamer at terminal time. Set on terminal step states only; reused by the orchestrator to accumulate per-job and per-run totals for the kici_org_log_bytes capacity-planning gauge

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.tsagentStepStatusSchema

Streams log output from step execution to the orchestrator. The orchestrator may forward these upstream for dashboard display.

FieldTypeRequiredDescription
type"log.chunk"YesMessage discriminator
messageIdstringYesUnique message ID
runIdstringYesExecution run ID
jobIdstringYesJob ID within the run
stepIndexnumberYesZero-based step index
linesstring[]YesArray of log output lines
timestampnumberYesUnix timestamp (milliseconds)
streamenumNoStream these lines came from: stdout, stderr. Absent (older agent) reads as stdout

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.tsagentLogChunkSchema

Periodic heartbeat sent by agents for each running job. Used by the orchestrator’s stale run detector to identify jobs that have stopped progressing. Unlike connection-level heartbeats, this is per-job.

FieldTypeRequiredDescription
type"job.heartbeat"YesMessage discriminator
runIdstringYesExecution run ID
jobIdstringYesJob ID within the run
timestampnumberYesUnix timestamp (milliseconds)

Note: job.heartbeat has no messageId field — it is a lightweight fire-and-forget message.

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.tsjobHeartbeatSchema

Operational log lines streamed from stateful/external agents to the orchestrator via WebSocket. Used for agents whose logs cannot be captured by the host (e.g., Firecracker VMs, external agents). Lines are batched (up to 50 lines or 100ms debounce) for efficiency.

FieldTypeRequiredDescription
type"agent.log"YesMessage discriminator
messageIdstringYesUnique message ID
agentIdstringYesAgent identifier
linesstring[]YesArray of log output lines
timestampnumberYesUnix timestamp (milliseconds)

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.tsagentLogSchema

Sent by the agent after receiving and applying register.ack config. Signals the orchestrator that MMDS data can be cleared for Firecracker agents. In Firecracker/scaler-managed mode, the agent also blocks MMDS access via iptables before sending this acknowledgment.

FieldTypeRequiredDescription
type"config.ack"YesMessage discriminator
messageIdstringYesUnique message ID
agentIdstringYesAgent identifier acknowledging config

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.tsconfigAckSchema

These messages support custom event emission from running workflow steps. When a step calls ctx.emit(), the agent sends an event.emit message to the orchestrator, which responds with a delivery receipt.

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.ts

Emits a custom event from a running workflow step. The orchestrator stores the event, performs fan-out matching against lock file subscriptions, and returns a delivery receipt.

FieldTypeRequiredDescription
type"event.emit"YesMessage discriminator
jobIdstringYesJob that is emitting the event
requestIdstringYesCorrelates request to response for routing
eventNamestringYesCustom event name (e.g., "deploy-complete")
payloadRecord<string, unknown>YesEvent payload data
targetobjectNoCross-repo targeting: { repos?: string[] }

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.tseventEmitSchema

Response confirming event delivery. Contains either a deliveryId on success or an error on failure.

FieldTypeRequiredDescription
type"event.emit.response"YesMessage discriminator
requestIdstringYesCorrelates to the original event.emit requestId
deliveryIdstringNoDelivery ID assigned by orchestrator (on success)
errorstringNoError description (on failure)

error is either a deliberate author-facing reason — an unknown job context — or a safe fixed string classifying an internal failure. It is never raw exception text.

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.tseventEmitResponseSchema

These messages support agent direct-to-S3 cache uploads. The agent requests a pre-signed upload URL from the orchestrator, performs the upload directly to object storage, then confirms completion so the orchestrator can update cache metadata.

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.ts

Request a pre-signed upload URL for cache storage (source bundle or dependency tarball).

FieldTypeRequiredDescription
type"cache.upload.request"YesMessage discriminator
messageIdstringYesUnique message ID
jobIdstringYesJob that produced the artifact
cacheTypeenumYesOne of: source, deps
contentHashstringNoContent hash for cache keying
lockfileHashstringNoLockfile hash for cache keying
platformstringYesOS platform (e.g., linux)
archstringYesCPU architecture (e.g., x64, arm64)

Confirm that an upload finished so the orchestrator can update metadata.

FieldTypeRequiredDescription
type"cache.upload.complete"YesMessage discriminator
messageIdstringYesUnique message ID
jobIdstringYesJob that produced the artifact
cacheTypeenumYesOne of: source, deps
contentHashstringNoContent hash for cache keying
lockfileHashstringNoLockfile hash for cache keying
platformstringYesOS platform (e.g., linux)
archstringYesCPU architecture (e.g., x64, arm64)
depsHashstringNoSHA-256 of the dep tarball (for agent-side integrity checking)

Request a user-cache restore (declarative or imperative ctx.cache) — the orchestrator returns a pre-signed download URL for the matching entry.

FieldTypeRequiredDescription
type"cache.user.restore.request"YesMessage discriminator
messageIdstringYesUnique message ID
jobIdstringYesJob requesting the restore
keystringYesExact cache key
restoreKeysstring[]NoOrdered prefix fallbacks (newest matching entry wins)

Request a pre-signed upload slot for a user-cache save. Keys are immutable, so the orchestrator may decline when the key already exists.

FieldTypeRequiredDescription
type"cache.user.save.request"YesMessage discriminator
messageIdstringYesUnique message ID
jobIdstringYesJob saving the cache
keystringYesExact cache key

Confirm a user-cache upload finished so the orchestrator can commit the temp object to its final key and record metadata.

FieldTypeRequiredDescription
type"cache.user.save.complete"YesMessage discriminator
messageIdstringYesUnique message ID
jobIdstringYesJob that produced the artifact
keystringYesExact cache key
tarHashstringYesSHA-256 of the tarball bytes
sizeBytesnumberYesTarball size in bytes (drives quota accounting)

Return the pre-signed upload URL for the agent to upload directly to object storage.

FieldTypeRequiredDescription
type"cache.upload.response"YesMessage discriminator
requestIdstringYesCorrelates to the original request
uploadUrlstringYesPre-signed URL for direct S3 upload

Return the matched user-cache entry’s pre-signed download URL, or signal a miss.

FieldTypeRequiredDescription
type"cache.user.restore.response"YesMessage discriminator
requestIdstringYesCorrelates to the original request
hitbooleanYesTrue when an entry matched (exact or prefix)
matchedKeystringNoFull key that matched (exact or the matched prefix entry)
downloadUrlstringNoPre-signed GET URL for the matched tarball (present only on hit)
tarHashstringNoSHA-256 of the tarball bytes for integrity verification

Return the pre-signed upload URL, or signal skip when the immutable key already exists.

FieldTypeRequiredDescription
type"cache.user.save.response"YesMessage discriminator
requestIdstringYesCorrelates to the original request
uploadUrlstringNoPre-signed PUT URL to the temp object (absent when skip is true)
skipbooleanYesTrue when the exact key already exists (immutable no-op)

These messages mirror the cache-upload handshake for build provenance bundles: the agent requests a pre-signed PUT URL keyed by the artifact’s subject digest, uploads the attestation directly to object storage, then confirms completion so the orchestrator records an attestations row. When the signed statement cannot be minted at build time, the agent instead sends provenance.upload.defer carrying the frozen DSSE envelope so the mint can complete later.

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.ts

Request a pre-signed PUT URL for a provenance bundle. The orchestrator resolves the run server-side and ownership-checks the job.

FieldTypeRequiredDescription
type"provenance.upload.request"YesMessage discriminator
messageIdstringYesUnique message ID
jobIdstringYesJob producing the attestation (ownership-checked)
subjectDigeststringYesPrimary subject digest (lowercase hex); storage-key discriminator

Confirm a provenance bundle upload so the orchestrator records an attestations row.

FieldTypeRequiredDescription
type"provenance.upload.complete"YesMessage discriminator
messageIdstringYesUnique message ID
jobIdstringYesJob producing the attestation
subjectNamestringYesCaller-supplied artifact name
subjectDigeststringYesPrimary subject digest (lowercase hex)
mediaTypestringYesBundle media type

Hand a frozen, DSSE-signed statement to the orchestrator for a later mint when the attestation cannot be signed at build time. The envelope and its ephemeral public key are carried inline; statementHash binds the deferred mint to this exact payload.

FieldTypeRequiredDescription
type"provenance.upload.defer"YesMessage discriminator
messageIdstringYesUnique message ID
jobIdstringYesJob producing the attestation
subjectNamestringYesCaller-supplied artifact name
subjectDigeststringYesPrimary subject digest (lowercase hex)
audiencestringYesRequested token audience for the later mint
mediaTypestringYesBundle media type
statementHashstringYesSHA-256 of the frozen DSSE statement payload
dsseEnvelopeobjectYesThe frozen, DSSE-signed statement envelope
publicKeyobjectYesEphemeral public key JWK the envelope was signed with

Return the pre-signed PUT URL for the agent to upload the bundle directly to object storage.

FieldTypeRequiredDescription
type"provenance.upload.response"YesMessage discriminator
requestIdstringYesCorrelates to the original request
uploadUrlstringYesPre-signed PUT URL, or "" when storage is unavailable

These messages carry the run-scoped artifact upload/download handshake. When a step uploads a named artifact, the agent requests a pre-signed PUT URL; the orchestrator validates the artifact name and runs the enforcement gates (duplicate name, per-artifact/per-run size cap, org quota) before minting the URL, so a granted URL is always safe to upload to. Downloads resolve a named artifact within the same run to a pre-signed GET.

The orchestrator checks the artifact name itself rather than trusting the agent’s own check, because the name becomes a path segment of the storage key. It must be a short token of letters, digits, ., _, and -, up to 128 characters, and not made only of dots. Under that contract the name maps to its storage segment unchanged, so two distinct names can never produce the same key — a name outside it would be folded onto another name’s key, letting a second upload overwrite the first object while both records keep their own hash. The same check runs on the upload request and on the upload completion, since either message can carry a name. The key also carries a hash of the exact name, so two names differing only by case differ by more than case and address distinct objects on a case-insensitive object store as well.

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.ts

Request a pre-signed PUT URL for a named artifact upload.

FieldTypeRequiredDescription
type"artifacts.upload.request"YesMessage discriminator
messageIdstringYesUnique message ID
jobIdstringYesJob producing the artifact (ownership-checked; run + org resolved server-side)
namestringYesArtifact name (immutability + storage-key discriminator within the run)
declaredSizeBytesnumber (int >= 0)YesPacked tarball size in bytes; drives the size/run/quota enforcement gates

Confirm an artifact upload finished so the orchestrator records the artifacts row. The orchestrator replies with an artifacts.upload.complete.ack when it advertises the artifactCompleteAck capability, and the agent waits for it before the upload step returns.

The orchestrator does not trust the agent’s storageKey or sizeBytes: it derives the storage key from the run and artifact name it resolved server-side, and records the real object size it reads back from storage. Both fields stay on the wire (deprecated, still sent) for compatibility with older orchestrators — see deprecations.

FieldTypeRequiredDescription
type"artifacts.upload.complete"YesMessage discriminator
messageIdstringYesUnique message ID
jobIdstringYesJob producing the artifact
namestringYesArtifact name
sizeBytesnumber (int >= 0)YesPacked tarball size in bytes. Deprecated — advisory; the orchestrator records the verified size
sha256stringYesSHA-256 (hex) of the tarball bytes
storageKeystringYesStorage key echoed from the grant response. Deprecated — ignored; the key is derived server-side

Request a pre-signed GET URL for a named artifact of this run.

FieldTypeRequiredDescription
type"artifacts.download.request"YesMessage discriminator
messageIdstringYesUnique message ID
jobIdstringYesJob requesting the download (ownership-checked; run resolved server-side)
namestringYesArtifact name to resolve within the run

Return a pre-signed PUT (granted) or a refusal (rejected). All enforcement happens before minting, so a granted URL is safe to upload to.

A rejected carries either reason (an enforcement gate was hit) or error (the request could not be serviced at all — the name violates the artifact-name contract, artifact storage is not configured, the job’s run could not be resolved, the job is not owned by this agent, or the grant failed internally), never both. Keeping the two apart is what stops a bad name or an orchestrator-side problem from reaching the workflow author as a storage-quota rejection. error is a safe fixed string, never raw exception text; an agent that predates the field falls back to a generic rejection message.

FieldTypeRequiredDescription
type"artifacts.upload.response"YesMessage discriminator
requestIdstringYesCorrelates to the original request
outcomeenumYesOne of: granted, rejected
uploadUrlstringNoPre-signed PUT URL (present only on granted)
storageKeystringNoFinal storage key the agent echoes back on complete (present only on granted)
reasonenumNoEnforcement refusal reason (present only on an enforcement rejected): duplicate_name, size_cap, run_cap, org_quota
errorstringNoFailure detail — invalid name or internal failure (present only on a rejected with no reason)

Report the outcome of committing an artifacts.upload.complete. The commit is retried a few times before failed is reported, because it is idempotent and a momentary storage/database blip should not fail a run. An agent that sees the artifactCompleteAck capability on register.ack waits for this message and fails the workflow step on failed (or on a 25-second timeout, deliberately inside the step’s own artifact-request timeout so the specific reason is what surfaces), so a commit that never landed surfaces as a failed step instead of a green run with a missing artifact. An orchestrator that does not advertise the capability sends nothing, and the agent then treats the complete message as fire-and-forget and does not wait.

Losing the connection while an ack is in flight does not fail the step. Because the commit is idempotent, the agent parks the in-flight complete instead of failing it, and re-sends it under a fresh messageId after the next register.ack — up to twice, and only while the reconnected orchestrator still advertises the capability. The correlation id is re-minted on every resend, so a late ack for the pre-disconnect send is ignored rather than answering for the new one. The original 25-second deadline is never restarted, so an orchestrator that never comes back still fails the step on the same schedule; when the resends run out, when the reconnected orchestrator no longer acks, or when the agent’s token is rejected outright so no reconnect will be attempted, the step fails with an error saying the artifact may nevertheless have been committed.

A failed ack carries reason, a safe fixed string classifying the failure — artifact storage is not configured, the job’s run could not be resolved, the job is not owned by this agent, the uploaded object was missing at commit time, the name violates the artifact-name contract, or the commit failed internally. As on the upload and download responses, reason is never raw exception text: the exception stays in the orchestrator’s own logs, so a database endpoint or a storage key never reaches the workflow author.

FieldTypeRequiredDescription
type"artifacts.upload.complete.ack"YesMessage discriminator
requestIdstringYesEchoes the complete message’s messageId
outcomeenumYesOne of: committed, failed
reasonstringNoSafe fixed failure classification (present only on failed)

Return a pre-signed GET plus size/sha256 for the named artifact, or not_found when the run never uploaded an artifact by that name.

not_found is also the outcome when the orchestrator could not perform the lookup at all — artifact storage is not configured, the job’s run could not be resolved, the job is not owned by this agent, or the lookup failed internally. Those replies carry error, which is what separates “this artifact does not exist” from “this orchestrator could not look it up”; a genuine miss carries no error. As on the upload response, error is a safe fixed string, never raw exception text, and an agent that predates the field renders the plain not-found message.

FieldTypeRequiredDescription
type"artifacts.download.response"YesMessage discriminator
requestIdstringYesCorrelates to the original request
outcomeenumYesOne of: found, not_found
downloadUrlstringNoPre-signed GET URL (present only on found)
sizeBytesnumber (int >= 0)NoArtifact size in bytes (present only on found)
sha256stringNoSHA-256 (hex) of the tarball bytes for integrity verification (present only on found)
errorstringNoInternal-failure detail (present only on a not_found caused by an orchestrator failure)

These messages support debug-bundle collection across a cluster: the orchestrator asks each agent for a log/diagnostic mini-bundle, and the agent streams the ZIP back in base64 frames (or reports an error).

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.ts

Ask an agent for its log/diagnostic mini-bundle.

FieldTypeRequiredDescription
type"fleet.logs.request"YesMessage discriminator
requestIdstringYesUUID correlating the chunked response
logWindowHoursnumberYesHours of log history to include
maxBytesnumberYesPer-node cap on raw log bytes

One base64 frame of an agent’s mini-bundle ZIP.

FieldTypeRequiredDescription
type"fleet.bundle.chunk"YesMessage discriminator
requestIdstringYesCorrelates to the originating request
seqnumberYesZero-based frame sequence number
isLastbooleanYesTrue on the final frame
dataB64stringYesBase64-encoded ZIP frame

Reported when an agent fails to build or stream its mini-bundle.

FieldTypeRequiredDescription
type"fleet.bundle.error"YesMessage discriminator
requestIdstringYesCorrelates to the originating request
messagestringYesFailure description

These messages carry a generic request/response envelope over the existing agent connection. The orchestrator registers each callable method by name and required role; the agent names a method and passes its parameters, and the orchestrator replies with the method’s result or an error. Adding a method is a server-side registration, not a new message type.

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.ts

Calls a registered method on the orchestrator.

FieldTypeRequiredDescription
type"agent.api.request"YesMessage discriminator
requestIdstringYesCorrelates the response
methodstringYesDot-namespaced method name (e.g. infrastructure.list)
paramsobjectNoMethod-specific parameters (default: empty object)

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.tsagentApiRequestSchema

Result of one agent.api.request.

FieldTypeRequiredDescription
type"agent.api.response"YesMessage discriminator
requestIdstringYesCorrelates to the originating agent.api.request
resultunknownNoThe method’s return value (on success)
errorstringNoFailure description (on failure)

error carries the two deliberate rejections verbatim — the method is not registered, or the caller lacks the role the method requires — because both name only the caller’s own request. Any other failure is a safe fixed string, never raw exception text.

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.tsagentApiResponseSchema

These messages carry the step-level approval round-trip. When a step declares approval, the agent blocks its step loop and sends a step.approval-request; the orchestrator creates a step-scoped held-run from the requirement and replies with step.approval-resolved once the hold is approved, rejected, or expired. The agent keeps heartbeats flowing during the wait so it is not reaped as stale.

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.ts

A step carrying approval is about to run and the agent is blocking until the orchestrator resolves the approval. For a when: 'drift' gate the request carries the computed drift payload.

FieldTypeRequiredDescription
type"step.approval-request"YesMessage discriminator
messageIdstringYesUnique message ID (correlated by the resolution’s requestId)
runIdstringYesExecution run ID
jobIdstringYesJob ID within the run
stepIndexnumberYesZero-based step index
stepNamestringYesHuman-readable step name
clausesApproverClause[]YesAND-list of approver clauses (empty = any approval-capable member)
reasonstringYesHuman label for the gate (from the SDK approval reason)
timeoutSecondsnumberNoPer-gate timeout override; absent falls back to the org-default expiry
payload{ summaryMarkdown, drift }NoComputed drift, present only for a when: 'drift' gate; persisted on the hold and rendered in the approval queue + CLI

Resolution of a step-level approval hold. On approved the agent runs the step with its live workspace intact; on rejected/expired it fails the job with a clear reason.

FieldTypeRequiredDescription
type"step.approval-resolved"YesMessage discriminator
requestIdstringYesCorrelates to the originating step.approval-request messageId
runIdstringYesExecution run ID
jobIdstringYesJob ID within the run
stepIndexnumberYesZero-based step index
outcomeenumYesOne of: approved, rejected, expired
reasonstringNoOptional human reason (e.g. the reject reason)

On rejected, reason is either deliberate rejection wording — an invalid per-gate timeout, or the approver’s own decline note — or a safe fixed string classifying an internal failure. It is never raw exception text.

These messages flow from the orchestrator upstream to KiCI for execution metadata tracking and real-time dashboard updates.

Authoritative source: packages/engine/src/protocol/messages/execution-status.ts

Structured execution status update sent by the orchestrator when an execution run starts, completes, or changes status. The upstream tier stores this metadata for dashboard queries.

FieldTypeRequiredDescription
type"execution.status"YesMessage discriminator
messageIdstringYesUnique message ID
runIdstringYesExecution run ID
workflowNamestringYesName of the workflow being executed
statusenumYesOne of: pending, running, success, failed, cancelled, cancelling, held
routingKeystringNoRouting key of the run’s own source, so each run is attributed to its real source when one connection serves multiple sources (absent → the upstream falls back to the connection’s first routing key)
repoIdentifierstringNoRepository identifier (e.g., owner/repo)
repoProviderstringNoRun-level repo provider (origin host: github / gitlab / bitbucket / local); drives provider-aware repo links in the dashboard
localWorkingTreebooleanNoTrue when the run executed a developer’s uploaded local working tree (kici run remote)
shastringNoCommit SHA
refstringNoGit branch or tag (e.g., main, refs/tags/v1.0)
triggerEventstringNoTrigger event type (e.g., push, pr:open)
commitMessagestringNoFirst line of the commit message
jobCountnumberNoTotal number of jobs in this execution
startedAtnumberYesUnix timestamp when execution started
completedAtnumberNoUnix timestamp when execution completed
durationMsnumberNoTotal execution duration in milliseconds
timestampnumberYesUnix timestamp (milliseconds)
parentRunIdstring or nullNoParent run ID for re-run lineage (null/undefined for originals)
originalRunIdstring or nullNoRoot ancestor run ID (always points to first run in chain)
triggeredBystring or nullNoUser identity that triggered this re-run (null for webhook)
triggeredByAgentLabelstring or nullNoAgent provenance label when the run was triggered through an agent credential
triggerActorProviderstring or nullNoProvider of the triggering actor captured from the provider event (the pusher / PR author), distinct from triggeredBy
triggerActorUsernamestring or nullNoProvider login of the triggering actor
triggerActorUserIdstring or nullNoProvider user ID of the triggering actor
failureReasonstringNoHuman-readable reason why the run failed (only present for failed runs)
logBytesnumberNoTotal raw log bytes accumulated across all jobs of this run; only set on terminal run states
initFailureobjectNoStructured init-failure signal (scope, category, message, optional jobName) set when the run never executed a single step; only present when status is failed
failureClassenumNoWhy a terminal run failed: never_started, timed_out, dead_orchestrator, step_failure, cancelled (only present for failed/cancelled runs)

Authoritative source: packages/engine/src/protocol/messages/execution-status.tsexecutionStatusSchema

Per-step status forwarded from agent through the orchestrator upstream in real-time. Enables live step-by-step progress in the dashboard.

FieldTypeRequiredDescription
type"step.status.forward"YesMessage discriminator
messageIdstringYesUnique message ID
runIdstringYesExecution run ID
jobIdstringYesJob ID within the run
jobNamestringYesHuman-readable job name
stepIndexnumberYesZero-based step index
stepNamestringYesHuman-readable step name
stateenumYesOne of: running, success, failed, skipped, pending, cancelled
timestampnumberYesUnix timestamp (milliseconds)
dataRecord<string, unknown>NoOptional state-specific data
secretsAccessedstring[]NoSecret key names accessed by this step. Forwarded from agent for dashboard display
concurrencyKindenumNoStep concurrency role: sequential, parallel-child, parallel-group; absent means an ordinary sequential step
groupIdstringNoParallel-group correlation ID shared by a group’s children (e.g., g0)

Authoritative source: packages/engine/src/protocol/messages/execution-status.tsstepStatusForwardSchema

Per-job status forwarded from orchestrator upstream in real-time. Enables live job-level progress in the dashboard.

FieldTypeRequiredDescription
type"job.status.forward"YesMessage discriminator
messageIdstringYesUnique message ID
runIdstringYesExecution run ID
jobIdstringYesJob ID within the run
jobNamestringYesHuman-readable job name
statusenumYesOne of: pending, queued, running, recovering, cancelling, success, failed, cancelled, skipped, timed_out_stale, drift_dropped, unroutable
matrixValuesRecord<string, unknown>NoMatrix parameter values for this job instance
startedAtnumberNoUnix timestamp when job started
completedAtnumberNoUnix timestamp when job completed
durationMsnumberNoJob duration in milliseconds
errorMessagestring or nullNoError message if job failed
agentIdstring or nullNoAgent ID executing this job
orchestratorIdstring or nullNoOrchestrator ID that dispatched this job
runsOnLabelsstring[]NoLabels used for agent routing
contextsstring[]NoOrdered bound context names for this job (multi-context jobs)
logBytesnumberNoTotal raw log bytes accumulated across all steps of this job; only set on terminal job states
timestampnumberYesUnix timestamp (milliseconds)
initFailureobjectNoStructured init-failure signal (scope, category, message, optional jobName) — set for synthetic rejected / init-failed jobs

Authoritative source: packages/engine/src/protocol/messages/execution-status.tsjobStatusForwardSchema

State replay sent on orchestrator reconnection. Contains a full snapshot of all active runs and their jobs so the upstream tier can reconstruct the current state without requiring the orchestrator to resend individual status messages.

FieldTypeRequiredDescription
type"state.replay"YesMessage discriminator
messageIdstringYesUnique message ID
runsRunSnapshot[]YesArray of active run snapshots
timestampnumberYesUnix timestamp (milliseconds)

Each RunSnapshot:

FieldTypeRequiredDescription
runIdstringYesExecution run ID
workflowNamestringYesWorkflow name
statusenumYesOne of: pending, running, success, failed, cancelled, cancelling
routingKeystringNoProvider routing key
repoIdentifierstringNoRepository identifier (e.g., owner/repo)
shastringNoCommit SHA
refstringNoGit branch or tag
triggerEventstringNoTrigger event type
commitMessagestringNoFirst line of the commit message
jobCountnumberYesTotal number of jobs in this run
startedAtnumberYesUnix timestamp when run started
completedAtnumberNoUnix timestamp when run completed
durationMsnumberNoRun duration in milliseconds
parentRunIdstring or nullNoParent run ID for re-run lineage
originalRunIdstring or nullNoRoot ancestor run ID (first run in the chain)
triggeredBystring or nullNoUser identity that triggered this re-run
failureReasonstringNoHuman-readable reason why the run failed (only present for failed runs)
jobsJobSnapshot[]YesArray of job snapshots within the run

Each JobSnapshot:

FieldTypeRequiredDescription
jobIdstringYesJob ID
jobNamestringYesHuman-readable job name
statusstringYesJob status
startedAtnumberNoUnix timestamp job started
completedAtnumberNoUnix timestamp job ended
durationMsnumberNoJob duration (ms)
errorMessagestring or nullNoError message if job failed
agentIdstring or nullNoAgent ID executing this job
runsOnLabelsstring[]NoLabels used for agent routing

Authoritative source: packages/engine/src/protocol/messages/execution-status.tsstateReplaySchema

  • Dashboard, metrics & wire format — concurrency, agent metrics, agent authentication, agent private API, join, peer-to-peer, the test-relay control plane, and wire-format messages
  • Protocol overview — message flow diagram, common envelopes (heartbeat, ack, nack, error), and authentication messages