Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -506,10 +506,11 @@ More: [Heartbeat guide](docs/guides/heartbeat.md)

### Programmatic Runtime

Heddle is not only a CLI. The npm package exposes three main programmatic layers:
Heddle is not only a CLI. The npm package exposes explicit programmatic layers:

- `createConversationEngine`: an alpha API for persisted multi-turn sessions with session storage, compaction, approvals, traces, semantic activity, and custom frontends or local hosts
- `ConversationRunService`: process-local run identity, ordered activity, bounded replay, cancellation, and approval resolution for reconnectable hosted conversations
- `@roackb2/heddle/hosted`: process-local run identity, ordered activity, bounded replay, cancellation, and approval resolution for reconnectable hosted conversations
- `@roackb2/heddle/remote`: runtime-validated public run envelopes plus shared cursor, duplicate, gap, terminal, and reconnect correctness for remote clients
- `AgentLoopRuntimeService.run(...)`: a lower-level single-run execution loop for hosts that do not need persisted chat or session behavior

Advanced hosts can also reuse lower-level class APIs such as `ToolRegistry`, `ToolExecutionService`, `TraceRecorder`, `TraceConsoleFormatter`, and `ReviewDiffParser` when they intentionally assemble custom runtime or review surfaces.
Expand All @@ -518,6 +519,7 @@ Other exported primitives include `HeartbeatRunnerAgent.run`, `HeartbeatSchedule

More: [Programmatic hosts](docs/guides/programmatic/README.md),
[choosing an integration layer](docs/guides/programmatic/integration-layers.md),
[remote conversation runs](docs/guides/programmatic/remote-runs.md),
and the runnable
[hosted service → HTTP/SSE → browser client example](examples/sdk/05-hosted-agent/README.md).

Expand Down
19 changes: 12 additions & 7 deletions docs/architecture/live-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ The ownership split is:
- `src/core/chat/runs` owns run identity, ordering, replay, terminal state,
cancellation, and pending approval coordination;
- the control plane owns transport projection and lifecycle discovery;
- `src/client-shared` owns frontend-neutral cursor and reconnect correctness;
- `src/core/chat/remote` owns frontend-neutral cursor and reconnect correctness;
- `src/client-shared` exposes that browser-safe policy to Heddle interfaces
without giving presentation code direct access to core internals;
- CLI and web own presentation state only.

## Activity And Run Vocabularies
Expand Down Expand Up @@ -75,8 +77,9 @@ flowchart TD
Run --> Adapter["Control-plane run-stream adapter"]
Adapter --> RunRPC["sessionRunEvents<br/>runId + afterSequence"]
Adapter --> Lifecycle["sessionEvents<br/>run started or settled"]
RunRPC --> Shared["client-shared run cursor service"]
RunRPC --> Remote["ConversationRunConsumerService<br/>cursor and reconnect policy"]
Lifecycle --> Shared
Remote --> Shared["client-shared public barrel"]
Shared --> CLI["cli-v2 presentation"]
Shared --> Web["web-v2 presentation"]
Engine --> Trace["TraceEvent<br/>durable evidence"]
Expand All @@ -92,9 +95,10 @@ The main implementation path is:
- `chat-session-events.ts` fans lifecycle, queue, and approval signals through
a workspace/session-scoped `EventEmitter`;
- `controlPlane.sessionRunEvents` exposes the replayable run stream;
- `ClientSharedConversationRunStreamService` enforces duplicate suppression,
- `ConversationRunConsumerService` enforces duplicate suppression,
sequence-gap detection, terminal detection, and reconnect cursors;
- cli-v2 and web-v2 bind that shared behavior to their local state models.
- `src/client-shared` reexports that public remote-run service so cli-v2 and
web-v2 can bind it to their local state models without importing core.

Conversation activities are published only into `ConversationRunService`.
They are not also copied onto the session `EventEmitter`; one activity must not
Expand Down Expand Up @@ -205,7 +209,8 @@ The server keeps a bounded replay buffer for retained runs. A reconnecting
client sends the last accepted `sequence` as `afterSequence`; the server replays
only newer items and then resumes live delivery.

`ClientSharedConversationRunStreamService` is the mandatory CLI/web policy:
`ConversationRunConsumerService` from `@roackb2/heddle/remote` is the
mandatory CLI/web policy:

- ignore duplicate sequence numbers;
- reject a gap instead of silently losing activity;
Expand Down Expand Up @@ -249,8 +254,8 @@ When adding shared behavior:
1. Add structured activity fields at the owning core origin.
2. Publish through the existing run context; do not add a second session bus.
3. Keep server projection limited to real transport/security work.
4. Put cursor and reconnect policy in `client-shared` when both interfaces need
it.
4. Put transport-neutral cursor and reconnect policy in `src/core/chat/remote`;
expose it to Heddle presentation clients through `src/client-shared`.
5. Keep CLI/web differences limited to input and presentation.
6. Test run identity, ordered replay, terminal delivery, and reconnect behavior
before relying on interface-level snapshots.
13 changes: 10 additions & 3 deletions docs/guides/programmatic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ tools, host extensions, and approval callbacks.
These guides follow a **progressive-disclosure ladder**. Start at rung 1 and go
deeper only when you need to. This mirrors how the public API is organized.

There are two import entry points:
The public entry points span two independent axes: customization depth and
hosting assumptions.

- `@roackb2/heddle` — the **curated** default surface (rungs 1–5): everything a
product host needs to build an agentic experience.
- `@roackb2/heddle/advanced` — the **full** surface: the curated exports plus
- `@roackb2/heddle/hosted` — process-local run identity, replay, cancellation,
and approvals for a long-lived host process.
- `@roackb2/heddle/remote` — browser-safe runtime contracts plus transport-
neutral cursor, duplicate, gap, terminal, and reconnect correctness.
- `@roackb2/heddle/advanced` — the **deep core customization** surface: the curated exports plus
lower-level building blocks (LLM adapters, individual tools, trace, memory,
models, awareness) and specialized runtimes (agent loop, heartbeat,
integrations). Reach for this only when the curated surface is not enough.
integrations). It does not implicitly opt into remote hosting or a transport.

Before choosing an API, read
[Choose a Programmatic Integration Layer](integration-layers.md). It maps common
Expand All @@ -38,6 +43,8 @@ split between Heddle and the host explicit for developers and coding agents.
- [Conversation engine](conversation-engine.md): engine setup, state roots,
and persisted sessions.
- [Approvals](approvals.md): own policy decisions in the host.
- [Remote conversation runs](remote-runs.md): consume reconnectable runs
through any transport with runtime-validated public payloads.
- [Hosted agent stack](../../../examples/sdk/05-hosted-agent/README.md): compose
a transport-neutral run service, Express/SSE API, and browser client while
keeping each layer replaceable.
Expand Down
7 changes: 6 additions & 1 deletion docs/guides/programmatic/conversation-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ process-local run identity, cancellation, ordered activity delivery, approvals,
and bounded replay for reconnecting subscribers.

```ts
import { ConversationRunService, createConversationEngine } from '@roackb2/heddle'
import { createConversationEngine } from '@roackb2/heddle'
import { ConversationRunService } from '@roackb2/heddle/hosted'

const runs = new ConversationRunService({
replay: { maxEventsPerRun: 512, retentionMs: 300_000 },
Expand Down Expand Up @@ -121,6 +122,10 @@ The replay buffer is intentionally process-local and bounded. Durable final
conversation state remains in the engine's session repository; transports and
cross-process delivery remain host responsibilities.

When a remote client owns a reconnect cursor, pair this service with the
[remote run consumer and protocol codec](remote-runs.md) rather than rebuilding
duplicate/gap/terminal/retry behavior in the client.

For a complete runnable host, follow the
[hosted agent stack example](../../../examples/sdk/05-hosted-agent/README.md). It
uses this exact service for account-scoped start/subscribe/cancel, then adds an
Expand Down
32 changes: 27 additions & 5 deletions docs/guides/programmatic/integration-layers.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ in control.
HOST-OWNED PRODUCT
UI state and rendering
|
transport client and public wire contract optional
@roackb2/heddle/remote consumer + public contract
|
server adapter, auth, tenancy, rate limits optional
transport client/server adapter optional
|
application service and composition root
(product session IDs, tools, config, repositories, policy)
Expand All @@ -44,10 +44,11 @@ not transport infrastructure.
| Agent execution | Model/tool loop, tool registry/execution, host-extension composition, traces, and artifacts | Product tools, system context, model choice, credentials, and capability policy |
| Approvals | Approval request/resolution lifecycle and run integration | Whether an action is allowed, authenticated approver, and approval UI/policy |
| Active runs | Run ID, ordered sequence, process-local replay, subscription, cancellation, and terminal result | Lifetime of the run service, address scope, process routing, draining, and multi-process delivery |
| Remote consumption | Cursor advancement, duplicate suppression, sequence-gap failure, terminal detection, bounded reconnect timing, and runtime envelope validation | Public activity/result schemas, actual transport timer/handle, and error UX |
| Persistence | File-backed defaults plus injectable session/artifact repository ports | Production repository implementations, retention, encryption, backup, and tenancy |
| Identity and authorization | No identity-provider assumption | Authentication, tenant/user mapping, authorization for start/subscribe/cancel |
| Transport/API | No HTTP, tRPC, SSE, or WebSocket assumption | Framework, routes/procedures, wire schemas, errors, limits, CORS, and rate limiting |
| Client experience | Semantic activities and terminal run events | Messages, tool rendering, UI state, retries, notifications, and product-specific result presentation |
| Client experience | Semantic activities, terminal run events, and remote cursor/retry calculations | Messages, tool rendering, UI state, transport timers, retry UX, notifications, and product-specific result presentation |

Do not rebuild Heddle-owned conversation or run behavior in the host. In
particular, avoid replaying product chat history into every prompt, generating a
Expand All @@ -61,9 +62,10 @@ disconnect as implicit cancellation.
| Nothing beyond a TypeScript process | `runQuickstartConversationCli` | [`01-interactive-chat.ts`](../../../examples/sdk/01-interactive-chat.ts) |
| A local loop that needs product tools or MCP | Quickstart plus tools/host extensions | [`02-add-a-tool.ts`](../../../examples/sdk/02-add-a-tool.ts), [`03-add-an-mcp-server.ts`](../../../examples/sdk/03-add-an-mcp-server.ts) |
| Its own output sink or local UI | `createConversationEngine` + `createConversationTextHost` or host callbacks | [`04-custom-output.ts`](../../../examples/sdk/04-custom-output.ts) |
| A server/worker that owns transport | `createConversationEngine` + `ConversationRunService` | [`05-hosted-agent/01-hosted-service`](../../../examples/sdk/05-hosted-agent/01-hosted-service) |
| A server/worker that owns transport | `@roackb2/heddle` + `@roackb2/heddle/hosted` | [`05-hosted-agent/01-hosted-service`](../../../examples/sdk/05-hosted-agent/01-hosted-service) |
| Express with REST + SSE | Same core plus a host adapter | [`05-hosted-agent/02-http-sse-api`](../../../examples/sdk/05-hosted-agent/02-http-sse-api) |
| A browser using that exact REST/SSE contract | Host API plus the protocol client | [`05-hosted-agent/03-browser-client`](../../../examples/sdk/05-hosted-agent/03-browser-client) |
| A remote client over any transport | `@roackb2/heddle/remote` plus a host transport | [Remote conversation runs](remote-runs.md) |
| A browser using the example REST/SSE contract | Remote layer plus the example protocol client | [`05-hosted-agent/03-browser-client`](../../../examples/sdk/05-hosted-agent/03-browser-client) |

For tRPC, Fastify, Hono, Nest, WebSocket, Electron IPC, queues, or another
transport, stop at the hosted-service layer and implement the adapter in the
Expand Down Expand Up @@ -93,13 +95,31 @@ lifetimes differ, clients can reconnect, or a turn needs a separately
addressable cancel/approval lifecycle. It is a process-local coordinator above
the engine, not a transport or durable message broker.

Import it from `@roackb2/heddle/hosted` so the hosted-process assumption is
visible. The root export remains available for compatibility.

### Remote run protocol

Use `ConversationRunProtocolCodec` and `ConversationRunConsumerService` from
`@roackb2/heddle/remote` when events cross an untrusted transport boundary or a
client can reconnect. The host supplies public activity/result schemas; Heddle
owns envelope validation, JSON safety, cursor advancement, duplicate/gap
handling, terminal detection, and retry calculation.

This layer does not own HTTP, SSE, tRPC, timers, auth, or UI state. See
[Remote conversation runs](remote-runs.md).

### Transport adapter

Add a host-owned adapter when a remote client needs start, subscribe, cancel, or
approval operations. Validate all untrusted wire data and project internal run
results into an explicitly public schema. Authentication and authorization must
happen before resolving the Heddle run address.

Web-standard HTTP/SSE helpers are a planned higher assumption layer; Express
and tRPC remain framework recipes until their residual code proves a meaningful
adapter boundary.

### Client protocol and UI

Add a transport client only when it matches the host's API. Keep protocol
Expand All @@ -118,6 +138,8 @@ normal client architecture.
| Approval behavior | `ConversationEngineHost.approvals` and the product's policy/UI |
| Session/artifact storage | `ChatSessionRepository` and `ArtifactRepository` |
| Public API fields | Host-owned validation schemas and terminal-result projection |
| Remote cursor/retry correctness | `ConversationRunConsumerService` |
| Runtime run-envelope validation | `ConversationRunProtocolCodec` with host activity/result schemas |
| REST, tRPC, SSE, WebSocket, or IPC | Host transport adapter above the application service |
| React or other UI state | Product client/application layer above the protocol client |
| Multi-process live delivery | Host-selected shared routing/broker infrastructure |
Expand Down
163 changes: 163 additions & 0 deletions docs/guides/programmatic/remote-runs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Remote Conversation Runs

Use the remote-run layer when a client observes a Heddle conversation through
a transport and may disconnect or reconnect while the run continues.

The public layers are intentionally separate:

```ts
import { createConversationEngine } from '@roackb2/heddle'
import { ConversationRunService } from '@roackb2/heddle/hosted'
import {
ConversationRunConsumerService,
ConversationRunProtocolCodec,
} from '@roackb2/heddle/remote'
```

- `@roackb2/heddle` owns persisted conversation semantics.
- `@roackb2/heddle/hosted` owns process-local active-run coordination.
- `@roackb2/heddle/remote` owns client cursor correctness and runtime wire
validation without choosing HTTP, SSE, tRPC, WebSocket, React, or auth.

## Define the public wire payload

Heddle owns the run envelope and terminal vocabulary. The host must explicitly
choose which activity and result fields are safe for remote clients.

```ts
import { z } from 'zod'
import { ConversationRunProtocolCodec } from '@roackb2/heddle/remote'

const PublicActivitySchema = z.object({
type: z.string().min(1),
}).passthrough()

const PublicResultSchema = z.object({
outcome: z.string().min(1),
summary: z.string(),
})

const protocol = new ConversationRunProtocolCodec({
activity: PublicActivitySchema,
result: PublicResultSchema,
})
```

`protocol.parseEvent(untrustedValue)` validates:

- non-empty `runId`;
- positive safe-integer `sequence`;
- ISO timestamp;
- one of `activity`, `result`, `cancelled`, or `error`;
- the host-supplied activity/result schema;
- JSON-safe values after schema parsing.

`protocol.stringifyEvent(value)` applies the same validation before JSON
serialization. This prevents passthrough or `unknown` payloads from carrying
values such as bigint, functions, symbols, non-finite numbers, or `undefined`
into a transport.

Use strict public schemas when internal activity can contain tool inputs,
results, filesystem paths, or other sensitive data. The codec validates the
schema you choose; it does not decide product authorization or sanitize secrets
on your behalf.

## Consume one run correctly

The consumer is a transport-neutral state machine. A reference may include any
host fields as long as it has a stable `runId`:

```ts
import { ConversationRunConsumerService } from '@roackb2/heddle/remote'

type ProductRunReference = {
accountId: string
sessionId: string
runId: string
}

const consumer = new ConversationRunConsumerService<ProductRunReference>({
retry: {
maxAttempts: 6,
baseDelayMs: 500,
maxDelayMs: 4_000,
},
})

consumer.select({ accountId, sessionId, runId })
```

Before opening a subscription, ask the consumer for the canonical cursor:

```ts
const input = consumer.subscriptionInput()

if (input) {
await transport.subscribe({
...input,
onEvent(rawEvent) {
const event = protocol.parseEvent(rawEvent)
const acceptance = consumer.accept(event)
if (acceptance.accepted) {
renderProductEvent(event)
}
},
})
}
```

`accept(...)`:

- ignores an event for another run;
- ignores an already accepted replay sequence;
- throws on a sequence gap;
- advances the cursor only after accepting an event;
- recognizes result/cancel/error as terminal;
- rejects a later event after terminal.

When a transport disconnects before terminal, request the next bounded retry:

```ts
const retry = consumer.nextRetry()
if (retry) {
await delay(retry.delayMs)
// reconnect with retry.input / consumer.subscriptionInput()
}
```

The consumer computes retry correctness and timing. The host still owns the
actual timer, subscription handle, error presentation, online/offline policy,
and UI state. Accepted progress resets the retry attempt budget.

## Server-side run ownership

Use one host-long-lived `ConversationRunService` from the hosted entrypoint:

```ts
import { ConversationRunService } from '@roackb2/heddle/hosted'

const runs = new ConversationRunService<ProductRunAddress>({
addressKey: ({ accountId, sessionId }) => JSON.stringify([accountId, sessionId]),
})
```

Authentication and authorization happen before the host constructs or resolves
the address. Do not treat possession of `runId` as authorization.

The run service remains process-local. Its replay buffer is bounded and does
not promise restart recovery or cross-instance delivery. Add shared routing or
durable delivery only when the deployment explicitly requires that additional
assumption layer.

## What remains host-owned

- start/cancel routes or procedures;
- HTTP/SSE/tRPC/WebSocket adapters;
- authentication, tenancy, CORS, rate limits, and audit;
- engine construction, credentials, tools, and approval policy;
- public activity/result projection;
- product finalization and UI state.

See the [hosted-agent example](../../../examples/sdk/05-hosted-agent/README.md)
for a runnable service → Express/SSE → browser flow. Its browser runner and
Heddle's own CLI/web clients reuse this same consumer implementation.
2 changes: 1 addition & 1 deletion examples/sdk/05-hosted-agent/01-hosted-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ for the runnable flow and production checklist.
## Read in this order

1. [`agent-service.ts`](agent-service.ts) — application-owned scope and lifecycle
composed over Heddle's `ConversationRunService`.
composed over `ConversationRunService` from `@roackb2/heddle/hosted`.
2. [`example-agent.ts`](example-agent.ts) — replaceable local composition and
demo policy.
3. [`run.ts`](run.ts) — disconnect, cursor replay, and explicit cancellation in
Expand Down
Loading
Loading