Skip to content

Pack protocol

View as Markdown

@kindgi/specs/pack-protocol.schema.json, schema version 2.10.0.

Pack protocol v2 — the messages a runtime exchanges with a pack service to run pack code (tool handlers and guardrail checks). One request, one response. Transport (HTTP): POST /v1/invoke takes a request and answers 200 with a response; GET /v1/info (token) answers info; GET /healthz (no token) answers 200 while the process is up; GET /readyz (no token) answers 200 once every module the index names has loaded and the service isn't draining. Headers: kindgi-pack-token (required on /v1/invoke and /v1/info; Authorization is deliberately not used, since platforms like Cloud Run put their own identity token there), kindgi-timeout-ms (relative deadline; default 120000), kindgi-run-id, kindgi-request-id, kindgi-protocol, traceparent (W3C Trace Context: the run's trace, so a pack service's records can carry its trace id; optional, protocol 2.4.1), kindgi-tenant-id (the tenant the call is for, set by the runtime from the deployment it routed the call to, on /v1/invoke and /v1/info; optional, protocol 2.7.0); the answer to an invoke carries kindgi-duration-ms and kindgi-artifact-version. Every outcome of running pack code is a response message with status 200 — a validation failure, a throw, an unknown tool, deadline-exceeded, cancelled (the caller closed the connection). Non-2xx statuses are transport-level only: 401 (bad token), 404, 405, 413 (body over 10 MiB), 415 (not application/json), 421 with {"code":"pack-tenant-mismatch"} (a service pinned to one tenant by KINDGI_PACK_TENANT_ID refused a call to /v1/invoke or /v1/info whose kindgi-tenant-id header is missing or names another tenant, or a tool call whose context names another: the call didn't run, and isn't retried; protocol 2.7.0), 503 with Retry-After (not ready, draining, or at the concurrency cap — the call provably did not run), 500 (the service itself failed). Process contract: a pack service reads --index, --module-root and --host arguments, KINDGI_PACK_SERVICE_TOKEN, PORT (0 picks a free port), KINDGI_PACK_SERVICE_MAX_CONCURRENCY and, optionally, KINDGI_PACK_TENANT_ID (protocol 2.7.0); it removes KINDGI_PACK_SERVICE_TOKEN from its process environment before it loads pack code, which never sees it; it loads every module before listening and exits 1 with a {"kind":"boot-failed","problems":[…]} JSON line on stderr when one fails; once listening it writes {"kind":"listening","port":N,…} on stderr; SIGTERM drains in-flight calls (up to 8 s; /readyz and new invokes answer 503 meanwhile) and exits 0.

Type: request or response or info

Protocol version of every message.

Type: 2

Type: toolInvoke or checkInvoke

Type: toolResult or toolPause or checkResult or error

Per-call context a handler receives. Serializable only: a pack service adds its own cancellation (an abort signal, a cancellation token) on top.

  • tenantId (string, required): Tenant the call runs for.
  • runId (string, required): The kernel run the call belongs to.
  • projectId (string): The project of the run the call belongs to. Set by the runtime from the run, never from its input or a model's arguments. Absent from an older runtime.
  • orgId (string): The org of that project, when the project has one. Set by the runtime from the project, never from input. Absent when the project has no org, and from an older runtime.
  • requestId (string): The individual call — e.g. the model's tool-call id.
  • idempotencyKey (string (uuid)): The call's idempotency key (protocol 2.6.0): the same every time this call runs (a resume after a wait, a retry after a failure, a re-run after a crash), different for every other call. A tool that changes something passes it to the system it writes to, or looks for it there first. A version 5 UUID (RFC 9562, SHA-1) under the namespace a70d5bff-e713-4322-b2ea-9d5c4926a861, over the UTF-8 bytes of the JSON array [runId, stepScope, toolId] (with the model's call id appended for a call a model asked for) as JSON.stringify writes it: no whitespace, and characters beyond ASCII as themselves, never as \u escapes (Python: json.dumps(parts, separators=(',', ':'), ensure_ascii=False)); stepScope is the step's own (its node id, a loop body's step with its iteration, a fanout branch). Absent from a runtime that can't name its steps.
  • env (map of string): The env values the tool declares (needsSpec.env), by name, resolved for this call: the project's value, else its org's, else the tenant's, in the env the runtime serves; a schema default when no scope sets one (protocol 2.5.0). The runtime records them with the call, so the call re-run after a wait or a retry sends the same values. Absent when the tool declares none, and from an older runtime.
  • secrets (object): Resolved secrets for the call (open).
  • connections (map of connection): The connections the tool declares (needsSpec.connections), by the tool's name for each, resolved for this call (protocol 2.9.0). Like secrets, never logged. Absent when the tool declares none, and from an older runtime.
  • config (object): Reserved: no runtime sends it yet (open).
  • settings (map of object): The settings blocks the calling agent version pins: each block's values, by block id (protocol 2.4.0). Absent when it pins none, and from an older runtime.
  • waitToken (object): The wait token a tool that declares pauses may answer with (protocol 2.8.0). The tool hands url to the system that will answer, then answers pause; the run waits until that system posts its answer to url, and the answer becomes the call's output. Sent only for a tool whose index entry says pauses. Absent from an older runtime: a tool that reads it then fails with pause-not-available.
    • id (string, required): The token's id. The same every time this attempt of the call runs (a retry of its step gets a new token).
    • url (string (uri), required): Where the other system posts its answer: POST with the output as a JSON body, no Authorization header. The link is the credential: hand it only to the system that answers. A re-run after a crash gets a new link to the same wait, and either link completes it. Hand on the new one only if the old one never went out, and dedupe on idempotencyKey.
  • toolFiles (object): Where a tool that declares storesFiles or readsFiles stores and reads files (protocol 2.10.0). The pack service keeps it from the handler, which gets files instead, with what the tool declares: files.store on a call with an idempotencyKey, which sends a file's bytes to url, and files.read, which gets a file's bytes from under url; each answers what the runtime answered. Sent only for a tool whose index entry says storesFiles or readsFiles, when the runtime has a file store, and for one that only stores, when the call has an idempotencyKey. Like secrets, never logged. Absent from an older runtime: the handler then gets no files.
    • url (string (uri), required): POST each file here: its bytes as the body, its type as Content-Type, its name in Kindgi-File-Name (UTF-8, percent-encoded), and token in Authorization: Bearer. The runtime answers 200 with { file }, a file value, or { notStored }: type (the bytes aren't a type a model is shown), size (over its type's limit), limit (the call has stored 10 files) or storage (the tenant's storage is full). Storing the same bytes again in the same call, a re-run included, answers the first file. GET <url>/<artifactId> (the id percent-encoded) reads a file, with token in Authorization: Bearer: the runtime answers 200 with its bytes as application/octet-stream, or { notRead } as JSON: rule (the call's run may not be shown a file of that id), gone (deleted, or its bytes are missing), changed (its bytes aren't the ones that were checked) or size (over its type's limit). Either way, 503 tool-files-busy means the runtime had no room for the bytes then: nothing was stored or read.
    • token (string, required): The call's token: for this call only, until its deadline or its answer, and only for what the tool declares. Send it only to url and the reads under it, and only in the Authorization header.

Run a tool handler. The pack service validates input against the indexed input schema, runs the handler, validates its return value against the indexed output schema.

  • v (version, required)
  • kind ("invoke", required)
  • tool (object, required)
    • id (string, required): Tool id, resolved from the pack service's own index.
    • version (string): When set, the pack service refuses a different indexed version (tool-version-mismatch).
  • input (object, required): The tool's input (any JSON).
  • ctx (callContext, required)

Run a guardrail check: evaluate(config, trace).

  • v (version, required)
  • kind ("check-invoke", required)
  • check (object, required)
    • id (string, required): Check id (the indexed guardrail's checkId, else its id).
  • config (object, required): The guardrail's config (open).
  • trace (object, required): The run trace the check evaluates (RunTrace in @kindgi/guardrails).
  • v (version, required)
  • kind ("result", required)
  • output (object, required): The handler's return value, valid against the tool's output schema.

A tool that declares pauses answers that its call waits for another system (protocol 2.8.0). Valid only as the answer to an invoke whose context carried waitToken. The run waits until that system posts to the token's url, or until timeoutMs passes; the posted value, valid against the tool's output schema, becomes the call's output.

  • v (version, required)
  • kind ("pause", required)
  • timeoutMs (integer): How long the run waits for the answer, in milliseconds: at most 24 days. Absent means the runtime's default, 24 hours.
  • name (string): What the run waits for, in a few words (the carrier's pickup confirmation), shown wherever the run's waits are listed.
  • v (version, required)
  • kind ("check-result", required)
  • result (object, required): CheckResult — open beyond the listed fields.
    • passed (boolean, required)
    • reason (string)
    • judgeResponse (string)
    • attributes (object)

One connection, resolved for a call: an account at another app and a short-lived token for it (ToolConnection).

  • connectionId (string, required)
  • app (string, required): The app entry's id (google, github).
  • appName (string, required): The app's name as people know it (Google).
  • accessToken (string, required): The token the tool sends to the app's API. Never logged or returned.
  • account (object, required): Whose account it is at the app.
    • id (string, required): Its stable id at the app.
    • label (string, required): What people see (ana@acme.example).
  • expiresAt (string (date-time)): When the token expires.

Why running pack code didn't produce a result. pause-not-available (protocol 2.8.0): a tool tried to pause, but the call carried no waitToken (an older runtime, or a tool whose index entry doesn't say pauses).

Type: "input-validation-failed" | "output-validation-failed" | "handler-import-failed" | "handler-shape-invalid" | "handler-throw" | "connection-rejected" | "check-shape-invalid" | "malformed-message" | "unknown-protocol-version" | "unexpected-message-kind" | "tool-not-in-pack" | "check-not-in-pack" | "tool-version-mismatch" | "deadline-exceeded" | "cancelled" | "pause-not-available"

One validation issue, Ajv-shaped. keyword names the failing JSON Schema keyword; it is absent when the failure came from a model's own validation (a Zod refinement, a pydantic validator) rather than a schema keyword. Open: an implementation may add fields.

  • instancePath (string, required): JSON Pointer to the failing value ("" for the root, /items/0/name).
  • schemaPath (string): URI fragment of the failing schema keyword (#/properties/name/type).
  • keyword (string): The failing keyword (type, required, additionalProperties, …).
  • params (object): Keyword parameters, as Ajv reports them: required → { missingProperty }, additionalProperties → { additionalProperty }, type → { type }, enum → { allowedValues }, limits → { limit }, pattern → { pattern }, format → { format }.
  • message (string, required): Human-readable message (must have required property 'name').
  • v (version, required)
  • kind ("error", required)
  • code (errorCode, required)
  • message (string, required)
  • toolId (string): Present when the error is about a tool.
  • checkId (string): Present when the error is about a check.
  • cause (object): The serialized throw for handler-throw / handler-import-failed ({ name, message, stack } for an error object).
  • issues (array of issue): Present for input-validation-failed / output-validation-failed.
  • connection (string): Present for connection-rejected (protocol 2.9.0): the tool's name for the connection whose token the app refused. The runtime renews that token once and runs the call again.

The answer to GET /v1/info.

  • protocol (version, required)
  • packId (string, required)
  • packVersion (string, required)
  • artifactVersion (string, required)
  • indexHash (string): sha256: and the hex SHA-256 of the service's index file, byte for byte: the indexHash a deployment signs. Lets the runtime check that an endpoint serves the deployment it was set for (protocol 2.7.0). Absent from an older pack service.
  • tools (array of object, required)
    • id (string, required)
    • version (string)
  • checks (array of string, required)
  • missingEnv (array of string): The index's required env names this process lacks (unset or empty), sorted. Empty when none is missing. Under the strict env check the service isn't ready while any is listed.