> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kentron.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# The TypeScript SDK and actions

> Write a receipt-native agent with the eight SDK exports, and pick the side-effect class that decides how the runtime is allowed to retry each action.

The SDK is how you write a receipt-native agent: you declare the receipt types your agent reads and writes, fold them into a view, and list the actions that are runnable against that view. The runtime does the rest — choosing which action runs next, claiming it so no second worker repeats it, and replaying the [receipt chain](/core/receipts-and-streams) to rebuild the view each pass.

<Note>
  **The SDK is an in-repo module, not a published package.** It lives at `packages/receipt-app/src/sdk/index.ts` inside the repository, the package that holds it is private, and it is not listed in that package's exports — so there is nothing to install from a registry, and you write against it from a checkout. [Authoring an agent](/core/authoring-agents) covers the two directories a spec can live in and how to run one.
</Note>

## What the SDK exports

There are exactly **eight** exported values:

| Export        | What it is                              |
| ------------- | --------------------------------------- |
| `receipt`     | A type marker for a receipt declaration |
| `defineAgent` | Declares an agent spec                  |
| `action`      | An action constructor                   |
| `assistant`   | An action constructor                   |
| `tool`        | An action constructor                   |
| `human`       | An action constructor                   |
| `merge`       | Declares a merge policy                 |
| `rebracket`   | An alias of `merge`                     |

Alongside them are fourteen exported types: `ReceiptDeclaration`, `ReceiptBody`, `ModernAgentSpec`, `ActionCommitContext`, `ActionExecutionMode`, `ActionKind`, `ActionRunContext`, `ActionSideEffects`, `AgentAction`, `DurableActionContext`, `MergePolicy`, `MergeCandidate`, `MergeDecision` and `MergeScoreVector`.

<Warning>
  **There is no `goal` export.** `goal` is a *field* on the spec you pass to `defineAgent`, not a helper you import. Older internal notes list it among the SDK symbols; importing it will fail.
</Warning>

`receipt<T>()` returns `{ __receipt: true }`. It is a pure type marker — it carries your receipt body type into the spec and does nothing at runtime. `defineAgent(spec)` and `merge(policy)` likewise return their argument unchanged; all three exist for type inference, not behaviour.

<Note>
  The barrel is not the whole SDK. A sixth module beside it holds `loadDefinedAgentSpec`, which is **not** re-exported — it is the seam the remote-action worker uses to reload your spec by id. You do not import it directly, but it is the reason [remote actions](#remote-actions) care where your file lives.
</Note>

## The spec shape

```ts theme={null}
type ModernAgentSpec<Receipts, View, Deps> = {
  readonly id: string;
  readonly version: string;
  readonly receipts: Receipts;                               // Record<string, ReceiptDeclaration<T>>
  readonly view: (helpers: ViewHelpers<Receipts>) => View;   // helpers: { on(type), chain() }
  readonly actions: (deps: Deps) => ReadonlyArray<AgentAction<View, EmitFn>>;
  readonly goal: (ctx: { view: View }) => boolean;
  readonly mergePolicy?: MergePolicy<MergeContext<View>, unknown>;
  readonly onMergeResult?: (ctx) => Promise<void> | void;
  readonly runtimePolicyVersion?: string;
  readonly maxIterations?: number;
  readonly maxConcurrency?: number;
};
```

The view function receives two helpers:

* `on(type)` returns `{ all(), last(), exists() }` for receipts of that type.
* `chain()` returns the raw chain.

## The action contract

```ts theme={null}
type ActionKind          = "action" | "assistant" | "tool" | "human";
type ActionExecutionMode = "local" | "remote";
type ActionSideEffects   = "receipt_only" | "query" | "external";

type DurableActionContext = {
  invocationId: string; selectionId: string; selectedHead?: string;
  claimId: string; claimOwnerId: string;
};

type ActionRunContext<View, EmitFn> = DurableActionContext & { view: View; emit: EmitFn };
type ActionCommitContext<View>      = DurableActionContext & { view: View; selectedView: View };

type AgentAction<View, EmitFn> = {
  id: string;
  kind: ActionKind;
  when?: (ctx: { view: View }) => boolean;
  run: (ctx: ActionRunContext<View, EmitFn>) => Promise<void> | void;
  sideEffects?: ActionSideEffects;
  responseWhen?: (ctx: { view; invocationId; selectionId }) => boolean;   // human only
  commitWhen?: (ctx: ActionCommitContext<View>) => boolean;
  exclusive?: boolean;
  maxConcurrency?: number;
  execution?: ActionExecutionMode;
  targetGroup?: string;
};
```

### The four kinds and how they are selected

Selection is deterministic. Every runnable action is sorted by kind priority — `human` first, then `assistant`, then `action`, then `tool`. From that ordering:

1. If any runnable action is `exclusive`, the first such action in that ordering is selected on its own, with reason `exclusive`.
2. Otherwise a concurrency cap is computed first, then applied: the cap is the minimum of the spec-level default and every **runnable** action's `maxConcurrency`, floored at 1. The first *n* ordered actions are selected, where *n* is that cap. The reason is `concurrency-cap` when the cap is smaller than the number of runnable actions, and `priority-order` otherwise.
3. If nothing is runnable, the reason is `settled`.

The spec-level default is `maxConcurrency` on the spec, or **8** when the spec does not set it. The spec's `maxIterations` bounds how many selections one run may make and defaults to **200**; reaching it fails the run with `max iterations reached (200)`.

The four constructors — `action`, `assistant`, `tool` and `human` — correspond to those four kinds. `human` requires `responseWhen` at the type level, and the controller enforces it again at run time with `human action '<id>' requires responseWhen({ view })`. When the predicate is false, the controller appends `human.requested`, records `run.blocked` with the reason `waiting for a correlated response to human action '<id>'`, and returns. The run does not wait in the process: the next pass over the same pending selection re-evaluates `responseWhen` against the rebuilt view and proceeds as soon as a newly appended receipt makes it true.

### The two execution modes

`execution` is `local` or `remote`. A remote action is dispatched through a durable function call rather than run in the calling process; see [Remote actions](#remote-actions) below.

### The three side-effect classes

`sideEffects` is the field that decides what the runtime is allowed to do to your action on a retry. Pick it deliberately.

<AccordionGroup>
  <Accordion title="receipt_only — replay-safe local work">
    Local work that only produces receipts. Safe to replay.
  </Accordion>

  <Accordion title="query — local reads and model calls">
    Local model or read work. It is **at-least-once** after lease ambiguity: the runtime may run it again. The buffered receipt output is still fenced by the durable claim and deduplicated by `invocationId`. An `action.output.manifest` receipt binds the whole result — its count and content hash — before any domain receipt is appended, so a repeated run cannot double-append, and a replay that produces different content is refused rather than spliced onto the prefix the previous owner committed.
  </Accordion>

  <Accordion title="external — a real mutation">
    A real mutation of something outside Receipt. It **must** set `execution: "remote"` — the controller checks this before every selection and throws `action '<id>' declares external side effects and must use execution: 'remote'` — and it **must** use `invocationId` as the downstream idempotency key, because remote delivery is at-least-once. Nothing else stops the same charge, message or write from happening twice.
  </Accordion>
</AccordionGroup>

### Optional members

<ParamField body="when" type="function">
  Gates whether the action is runnable against the current view.
</ParamField>

<ParamField body="commitWhen" type="function">
  An optimistic-validity fence. It is re-evaluated inside every retry, against both the view the action was selected at (`selectedView`) and the latest view (`view`). Returning `false` records an `action.superseded` receipt with the reason `commit predicate rejected the current receipt-derived view`, and the controller reselects from the new head — the action's work is discarded rather than committed against stale state.
</ParamField>

<ParamField body="exclusive" type="boolean">
  Forces a selection of exactly one action.
</ParamField>

<ParamField body="maxConcurrency" type="number">
  Lowers the per-selection cap. The effective cap is the minimum across **every runnable action** and the spec default, floored at 1 — so an action that lowers the cap can push itself out of the selection it just narrowed.
</ParamField>

<ParamField body="responseWhen" type="function">
  Required for `human` actions. Decides when the awaited human response has arrived.
</ParamField>

<ParamField body="targetGroup" type="string">
  Picks the worker group for a remote action. Falls back to the caller's default group.
</ParamField>

## Determinism

Both identifiers an action runs under are digests, not random values, so a recovered run reproduces exactly the same ids:

```
selectionId  = "agent_selection_"         + sha256([runId, spec.id, spec.version, policyVersion,
                                                    schedulerPolicyVersion, selectedHead ?? "root",
                                                    actionIds])[0:32]

invocationId = "agent_action_invocation_" + sha256([selectionId, actionId, index])[0:32]
```

The pinned policy versions are `runtime-policy-v2` for control receipts — unless the spec sets its own `runtimePolicyVersion` — and `scheduler-v2` for selection.

Receipt content hashing sorts object keys before hashing, so a harmless difference in key order does not invalidate a recovered output manifest.

## Control receipts

These are the receipts the agent loop writes on your behalf. They are the canonical set:

```
run.started, run.completed, run.blocked, run.failed
action.selected, action.started, action.output.manifest,
action.completed, action.failed, action.superseded
human.requested, human.responded
goal.completed
merge.started, merge.output.manifest, merge.selected,
merge.applied, merge.skipped, merge.failed
```

`action.started` may carry an `inlineSelection`. When a selection holds exactly one action that runs locally and is not a `human` action, the controller skips the separate `action.selected` receipt and durably selects and claims in that one receipt. Remote selections, multi-action selections and `human` actions still append `action.selected` first.

## Remote actions

A remote action dispatches through a pinned, versioned durable function — `receipt.agent.action.execute`, version 2, pinned on both registration and the call options so a rolling deploy cannot route a v2 payload to a v1 worker. The call id is the `invocationId`, and the call timeout is 120000 ms.

The worker does not trust the dispatch. It reloads the spec from `packages/receipt-app/src/agents/<id>.agent.ts`, re-derives the view from the selected head, and re-checks the spec id and version, that the action exists, the action's kind, that `execution === "remote"`, and `when(...)` before running anything. Each check has its own refusal:

* `agent spec '<id>' does not support remote execution`
* `remote action '<id>' requires <agent>@<version>, but the worker loaded <agent>@<version>`
* `remote action '<id>' selected head '<hash>' is unavailable`
* `unknown action '<id>' for agent '<agent>'`
* `remote action '<id>' no longer matches selected kind/execution provenance`
* `remote action '<id>' is no longer runnable at its selected head`

<Warning>
  There are two spec loaders and they read different directories. The CLI loads `<current working directory>/src/agents/<id>.agent.ts`, and `./.receipt/bin/receipt` changes to the repository root before it runs, so in practice that is `<repo root>/src/agents/`; the remote-action worker loads `packages/receipt-app/src/agents/<id>.agent.ts`. A spec scaffolded into the repository root is runnable from the CLI but is not visible to the remote-action worker unless it is moved into the package — and the reverse is equally true. [Authoring an agent](/core/authoring-agents) explains the trade-off.
</Warning>

## Merge policy

```ts theme={null}
type MergePolicy<Ctx, Evidence = unknown> = {
  id: string;
  version: string;
  shouldRecompute?: (ctx: Ctx) => boolean;                                 // optional
  candidates: (ctx: Ctx) => ReadonlyArray<MergeCandidate>;
  evidence: (ctx: Ctx) => Evidence;
  score: (candidate, evidence, ctx) => MergeScoreVector;                   // Readonly<Record<string, number>>
  choose: (scored: ReadonlyArray<{ candidate; score }>) => MergeDecision;  // { candidateId, reason? }
};
```

`shouldRecompute` is optional, not required. `rebracket` is an alias for `merge` — the same function under a second name.

A durable merge claims with `merge.started`, records the chosen candidate as `merge.selected`, binds any `onMergeResult` output with `merge.output.manifest`, and ends at `merge.applied`. A policy that is settled or returns no candidates ends at `merge.skipped` straight after `merge.started`, without a selection or a manifest; a failure ends at `merge.failed`. There is no `merge.evidence.computed` receipt and no `merge.candidate.scored` receipt.

## A minimal agent

A working spec is about thirty lines: declare two receipt types, fold them into a view, expose one `action` that emits the terminal receipt, and let the goal predicate read it back. [Authoring an agent](/core/authoring-agents) carries that example in full, along with the two directories a spec can live in and the `receipt run` invocation that executes it inline from a checkout.

Next step: [scaffold and run your own agent](/core/authoring-agents).
