summaryrefslogtreecommitdiffhomepage
path: root/.dispatch
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-07 16:22:31 +0900
committerAdam Malczewski <[email protected]>2026-06-07 16:22:31 +0900
commit17bc0a2cdaeefd4974f785c907d3515a38d45363 (patch)
tree1834867d2f0ad5e82fbb985d7f602d8e1dffdb42 /.dispatch
parent635cb6de7342ac87b27243652b1ad3b3a133d6a4 (diff)
downloaddispatch-web-17bc0a2cdaeefd4974f785c907d3515a38d45363.tar.gz
dispatch-web-17bc0a2cdaeefd4974f785c907d3515a38d45363.zip
feat(chat): group batched tool calls into one DaisyUI list
Consume the backend's new stepId grouping key (wire/transport-contract 0.1.0 -> 0.2.0). foldEvent copies event.stepId onto live tool chunks so live and replay group identically. New pure selector groupRenderedChunks (core/chunks) folds a step's 2+ tool calls into one tool-batch group, pairing each call with its result by toolCallId; single/no-stepId calls stay as cards. ChatView renders a batch as a DaisyUI list (list-row per pair). Fixtures updated for the now-required event stepId.
Diffstat (limited to '.dispatch')
-rw-r--r--.dispatch/transport-contract.reference.md10
-rw-r--r--.dispatch/wire.reference.md63
2 files changed, 66 insertions, 7 deletions
diff --git a/.dispatch/transport-contract.reference.md b/.dispatch/transport-contract.reference.md
index 3a7a59c..fcc2cbf 100644
--- a/.dispatch/transport-contract.reference.md
+++ b/.dispatch/transport-contract.reference.md
@@ -5,9 +5,15 @@
> hangs on a permission prompt). Your CODE still imports `@dispatch/transport-contract` normally —
> this file is for READING only.
>
-> **Orchestrator:** SNAPSHOT of `[email protected]`. Regenerate whenever it changes.
-> Depends on `@dispatch/wire` (see `wire.reference.md`) + `@dispatch/ui-contract`
+> **Orchestrator:** SNAPSHOT of `[email protected]`. Regenerate whenever it changes.
+> Depends on `@dispatch/[email protected]` (see `wire.reference.md`) + `@dispatch/ui-contract`
> (see `ui-contract.reference.md`).
+>
+> **0.2.0 change (step grouping):** no shape change HERE — this contract's own types are
+> identical. It only re-exports the bumped `@dispatch/wire`, whose `AgentEvent` tool variants
+> now carry a required `stepId` and whose tool `Chunk`s carry an optional `stepId`. The
+> `chat.delta` events streamed over WS and the `ConversationHistoryResponse.chunks` you already
+> consume therefore now carry the step grouping key (see `wire.reference.md`).
## Endpoints (backend, confirmed live — CORS wildcard `*`, HTTP port 24203, WS port 24205)
diff --git a/.dispatch/wire.reference.md b/.dispatch/wire.reference.md
index ccf07bd..ed95351 100644
--- a/.dispatch/wire.reference.md
+++ b/.dispatch/wire.reference.md
@@ -4,7 +4,13 @@
> types WITHOUT following the `file:` dep symlink out of this repo (which hangs on a permission
> prompt). Your CODE still imports `@dispatch/wire` normally — this file is for READING only.
>
-> **Orchestrator:** SNAPSHOT of `[email protected]`. Regenerate whenever `@dispatch/wire` changes.
+> **Orchestrator:** SNAPSHOT of `[email protected]`. Regenerate whenever `@dispatch/wire` changes.
+>
+> **0.2.0 change (step grouping):** `ToolCallChunk`/`ToolResultChunk` gained an OPTIONAL
+> `stepId?: StepId`; `TurnToolCallEvent`/`TurnToolResultEvent` gained a REQUIRED `stepId: StepId`.
+> A `StepId` is the per-step grouping key for batched/parallel tool calls — group by equality.
+> Live: read `event.stepId`. Replay: read `storedChunk.chunk.stepId` (NOT the envelope; absent on
+> pre-0.2.0 rows / non-tool chunks — tolerate absence). `StoredChunk` envelope is UNCHANGED.
```ts
/**
@@ -23,7 +29,16 @@ export type Role = "system" | "user" | "assistant" | "tool";
/** Opaque identifier for a turn (one user→assistant cycle). */
export type TurnId = string & { readonly __brand: "TurnId" };
-/** Opaque identifier for a step (one LLM round-trip within a turn). */
+/**
+ * Opaque identifier for a step (one LLM round-trip within a turn). It is the
+ * authoritative grouping key for the tool calls a model batches together in a
+ * single step (parallel/batched calls): every `tool-call`/`tool-result` event
+ * and every persisted tool chunk (`ToolCallChunk`/`ToolResultChunk`) from the
+ * same step carries the SAME `stepId`, so a client groups a batch purely by
+ * equality — identically on the live stream and in replayed history. Per-turn
+ * unique and gap-free in step order; treat it as opaque (do not parse it). The
+ * runtime derives it deterministically from the turn id + 0-based step index.
+ */
export type StepId = string & { readonly __brand: "StepId" };
/**
@@ -60,6 +75,18 @@ export interface ToolCallChunk {
readonly toolCallId: string;
readonly toolName: string;
readonly input: unknown;
+ /**
+ * The step that produced this call — generation provenance stamped by the
+ * runtime when the model emits the call (NOT storage metadata like `seq`,
+ * which is why it lives on the chunk and travels with it through persistence
+ * and replay). Tool calls a model batches together in one step share the same
+ * `stepId`: the grouping key for rendering a parallel batch as one unit, and
+ * equal to the `stepId` on the matching `tool-call` AgentEvent. Optional:
+ * absent on chunks reconstructed outside a turn and on rows persisted before
+ * this field existed, so a consumer must tolerate its absence (render
+ * ungrouped).
+ */
+ readonly stepId?: StepId;
}
/**
@@ -73,6 +100,15 @@ export interface ToolResultChunk {
readonly toolName: string;
readonly content: string;
readonly isError: boolean;
+ /**
+ * The step that produced the originating call — equal to the `stepId` on the
+ * matching `tool-call` chunk (same `toolCallId`) and on the `tool-result`
+ * AgentEvent, so a consumer groups a step's calls with their results.
+ * Generation provenance, not storage metadata (see `ToolCallChunk.stepId`).
+ * Optional for the same reasons; `reconcile` copies it from the originating
+ * call onto a synthesized (interrupted) result.
+ */
+ readonly stepId?: StepId;
}
/** An error that occurred during generation or tool dispatch. */
@@ -107,9 +143,11 @@ export interface ChatMessage {
* sync cursor, assigned in append order) and records the `role` of the message
* it belongs to. This makes a flat seq-ordered stream both incrementally
* syncable ("give me chunks after seq N") and regroupable into messages by the
- * client. `chunk` is the pure content unit, unchanged — `Chunk` itself never
- * carries storage metadata (it is also passed to/from the provider, which has
- * no use for a cursor).
+ * client. `chunk` is the content unit — `Chunk` carries no storage/sync cursor
+ * (`seq` lives here on the envelope, not on the chunk, since it is assigned by
+ * the store and the provider has no use for it). A chunk MAY still carry
+ * generation provenance assigned at production time (e.g. a tool chunk's
+ * `stepId`), which is intrinsic to the content and so travels with it.
*/
export interface StoredChunk {
readonly seq: number;
@@ -184,6 +222,14 @@ export interface TurnToolCallEvent {
readonly type: "tool-call";
readonly conversationId: string;
readonly turnId: string;
+ /**
+ * The step that produced this call. Tool calls a model batches together in
+ * one step share the same `stepId` — the grouping key for rendering a
+ * parallel batch as one unit. Matches the `stepId` on the matching
+ * `tool-result` event and on the persisted tool chunk
+ * (`StoredChunk.chunk.stepId`).
+ */
+ readonly stepId: StepId;
readonly toolCallId: string;
readonly toolName: string;
readonly input: unknown;
@@ -194,6 +240,13 @@ export interface TurnToolResultEvent {
readonly type: "tool-result";
readonly conversationId: string;
readonly turnId: string;
+ /**
+ * The step that produced the originating call. Equal to the `stepId` on the
+ * matching `tool-call` event (same `toolCallId`) and on the persisted tool
+ * chunk (`StoredChunk.chunk.stepId`), so a client groups a step's calls with
+ * their results.
+ */
+ readonly stepId: StepId;
readonly toolCallId: string;
readonly toolName: string;
readonly content: string;