@tai42/api-client 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tai42/api-client",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Typed client for the tai42-skeleton HTTP API.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "tai42 <oss@tai42.ai>",
package/src/client.ts CHANGED
@@ -223,6 +223,20 @@ export interface StorageUploadBody {
223
223
  readonly content_base64?: string;
224
224
  }
225
225
 
226
+ /**
227
+ * One transcript read (GET `/api/conversations/{route}/transcript` query params).
228
+ * `order` is not a default the caller may skip: `asc` reads the thread from its
229
+ * beginning and `desc` from its newest end, and which one a screen wants is a
230
+ * decision that screen must make.
231
+ */
232
+ export interface ConversationTranscriptQuery {
233
+ readonly routeName: string;
234
+ readonly threadId: string;
235
+ readonly page: number;
236
+ readonly pageSize: number;
237
+ readonly order: s.TranscriptOrder;
238
+ }
239
+
226
240
  /** The observability-metrics window + bucket size (GET query params). */
227
241
  export interface MetricsQuery {
228
242
  readonly from?: string;
@@ -535,14 +549,14 @@ export function createApiClient(config: ApiConfig) {
535
549
  // the FLEET doors, which ride the app's worker bus and need no backend at all.
536
550
  // `getBackendInfo` reports `present: false` (a 200 empty state) when no backend
537
551
  // plugin is registered. `listFleetWorkers` reads the live bus presence census
538
- // (every subscribed origin, ASGI + backend-runtime) and never fabricates an empty
552
+ // (every subscribed worker, ASGI + backend-runtime) and never fabricates an empty
539
553
  // fleet — a census read that fails surfaces as a loud 500.
540
554
  getBackendInfo: (signal?: AbortSignal) => req('/api/backend', s.backendInfo, { signal }),
541
555
  listFleetWorkers: (signal?: AbortSignal) =>
542
556
  req('/api/fleet/workers', s.fleetWorkers, { signal }),
543
557
  // Soft-restart the fleet: `targets` names the workers to reload, or `null` for
544
558
  // the whole fleet. The serving worker applies locally then broadcasts; a 200
545
- // response carries the per-origin fleet report, so workers that did not converge
559
+ // response carries the per-worker fleet report, so workers that did not converge
546
560
  // are named, not hidden. A failed local apply instead re-raises as a loud error
547
561
  // (the transport throws with the failure message), not a returned report.
548
562
  reloadFleetConfig: (targets: string[] | null) =>
@@ -709,6 +723,44 @@ export function createApiClient(config: ApiConfig) {
709
723
  // The installed channel-plugin names (delivery media for ask_user questions).
710
724
  listChannels: (signal?: AbortSignal) => req('/api/channels', s.channels, { signal }),
711
725
 
726
+ // -- conversations ---------------------------------------------------------
727
+ // The stored routing rows, each with its `callback_secret` withheld — the
728
+ // conversation monitor's route picker.
729
+ listConversationRoutes: (signal?: AbortSignal) =>
730
+ req('/api/conversations', s.conversationRoutes, { signal }),
731
+ // One route's threads, newest activity first. Admin-only server-side: a
732
+ // listing spans every caller on the route, so a scoped session gets a 403.
733
+ listConversationThreads: (
734
+ routeName: string,
735
+ page: number,
736
+ pageSize: number,
737
+ signal?: AbortSignal,
738
+ ) =>
739
+ req(`/api/conversations/${encodeSegment(routeName)}/threads`, s.conversationThreadsPage, {
740
+ signal,
741
+ query: { page, pageSize },
742
+ }),
743
+ // One thread's transcript, caller-scoped, read in the asked-for direction.
744
+ // The `thread_id` rides the QUERY STRING, never the path: it embeds the api
745
+ // door's `{principal}/{end user}` address with the principal already
746
+ // percent-encoded, which a path segment would have to encode a second time
747
+ // and the server would then decode once too few. A query value is decoded
748
+ // exactly once whatever it holds, so it is passed through verbatim.
749
+ readConversationTranscript: (query: ConversationTranscriptQuery, signal?: AbortSignal) =>
750
+ req(
751
+ `/api/conversations/${encodeSegment(query.routeName)}/transcript`,
752
+ s.conversationTranscriptPage,
753
+ {
754
+ signal,
755
+ query: {
756
+ thread_id: query.threadId,
757
+ page: query.page,
758
+ pageSize: query.pageSize,
759
+ order: query.order,
760
+ },
761
+ },
762
+ ),
763
+
712
764
  // -- notifications ---------------------------------------------------------
713
765
  // The internal notifications sink, newest-first — the messages the `notify_user`
714
766
  // operation records with no channel, so they surface only in this inbox.
@@ -3,9 +3,9 @@
3
3
  * fleet broadcast. Every mutation surface (connector, backup, mcp-config, env,
4
4
  * extensions, single-MCP reload, fleet reload) routes its broadcast report through
5
5
  * this one function so a FAILED propagation is never invisible: an unconfirmed
6
- * sibling (`failed`/`missing`/`departed`/`timed_out`) or an unreachable bus becomes
7
- * an explicit summary the UI renders as a loud, user-visible state — never a faked
8
- * success on a departed/timed_out/failed origin.
6
+ * sibling (any non-`applied` outcome) or an unreachable bus becomes an explicit
7
+ * summary the UI renders as a loud, user-visible state — never a faked success on a
8
+ * worker that did not converge.
9
9
  *
10
10
  * A mutation embeds its broadcast in one of two shapes: the mode-wrapped
11
11
  * {@link FleetReportFanout} (`fanout` field) or the bare {@link FleetResult} (the
@@ -15,12 +15,12 @@
15
15
  */
16
16
  import type { FleetOutcome, FleetReportFanout, FleetResult } from './schemas';
17
17
 
18
- /** A per-origin propagation FAILURE: an origin that did not confirm `applied`. */
18
+ /** A per-worker propagation FAILURE: a worker that did not confirm `applied`. */
19
19
  export type FleetFailureOutcome = Exclude<FleetOutcome, 'applied'>;
20
20
 
21
- /** One origin that failed to converge, with the report's message for it. */
22
- export interface FleetOriginFailure {
23
- readonly origin: string;
21
+ /** One worker that failed to converge, with the report's message for it. */
22
+ export interface FleetWorkerFailure {
23
+ readonly name: string;
24
24
  readonly outcome: FleetFailureOutcome;
25
25
  /** The failed apply's `error`, or the publisher's computed-outcome `detail`. */
26
26
  readonly message: string | null;
@@ -28,10 +28,10 @@ export interface FleetOriginFailure {
28
28
 
29
29
  /**
30
30
  * The interpreted state of one broadcast.
31
- * - `converged` — the bus was reached and every origin applied (or the change was
31
+ * - `converged` — the bus was reached and every worker applied (or the change was
32
32
  * local-only on a lone worker). The caller renders its own success; the report
33
33
  * needs no loud state.
34
- * - `degraded` — the bus was reached but one or more origins did not confirm; the
34
+ * - `degraded` — the bus was reached but one or more workers did not confirm; the
35
35
  * named `failures` are stale/unreached siblings the operator must see.
36
36
  * - `unreachable` — the bus itself could not be reached, so no sibling was told;
37
37
  * `error` carries the transport failure.
@@ -42,8 +42,8 @@ export interface FleetReportSummary {
42
42
  readonly status: FleetReportStatus;
43
43
  /** The single-worker `local-only` note, when the broadcast reached no sibling. */
44
44
  readonly note: string | null;
45
- /** The origins that did not apply — empty unless `status` is `degraded`. */
46
- readonly failures: readonly FleetOriginFailure[];
45
+ /** The workers that did not apply — empty unless `status` is `degraded`. */
46
+ readonly failures: readonly FleetWorkerFailure[];
47
47
  /** The bus-unreachable transport error — non-null only when `unreachable`. */
48
48
  readonly error: string | null;
49
49
  }
@@ -56,16 +56,16 @@ export function isFleetReportFailure(summary: FleetReportSummary | null): boolea
56
56
  /**
57
57
  * Interpret a bare {@link FleetResult} (the fleet-reload / single-MCP-reload body).
58
58
  * An unreachable bus is `unreachable`; a reachable report with any non-`applied`
59
- * origin is `degraded` (those origins named); otherwise `converged`.
59
+ * worker is `degraded` (those workers named); otherwise `converged`.
60
60
  */
61
61
  export function summarizeFleetResult(result: FleetResult): FleetReportSummary {
62
62
  if (!result.reachable) {
63
63
  return { status: 'unreachable', note: null, failures: [], error: result.error };
64
64
  }
65
- const failures: FleetOriginFailure[] = result.results
65
+ const failures: FleetWorkerFailure[] = result.results
66
66
  .filter((entry) => entry.outcome !== 'applied')
67
67
  .map((entry) => ({
68
- origin: entry.origin,
68
+ name: entry.name,
69
69
  outcome: entry.outcome as FleetFailureOutcome,
70
70
  message: entry.error ?? entry.detail ?? null,
71
71
  }));
package/src/index.ts CHANGED
@@ -24,6 +24,7 @@ export type {
24
24
  ValidatePresetBody,
25
25
  ToolMetaPatch,
26
26
  StorageUploadBody,
27
+ ConversationTranscriptQuery,
27
28
  MetricsQuery,
28
29
  RunsQuery,
29
30
  MarketplaceSearchQuery,
@@ -62,7 +63,7 @@ export { summarizeFleetResult, summarizeFleetFanout, isFleetReportFailure } from
62
63
  export type {
63
64
  FleetReportSummary,
64
65
  FleetReportStatus,
65
- FleetOriginFailure,
66
+ FleetWorkerFailure,
66
67
  FleetFailureOutcome,
67
68
  } from './fleet-report';
68
69
  export * as schemas from './schemas';
@@ -110,6 +111,17 @@ export type {
110
111
  MediaKind,
111
112
  AnswerFormat,
112
113
  Channels,
114
+ ConversationDoor,
115
+ ConversationTargetKind,
116
+ ConversationDeliveryStatus,
117
+ ConversationAnswerStatus,
118
+ ConversationRoute,
119
+ ConversationRoutes,
120
+ ConversationThread,
121
+ ConversationThreadsPage,
122
+ ConversationMessage,
123
+ TranscriptOrder,
124
+ ConversationTranscriptPage,
113
125
  Notification,
114
126
  Notifications,
115
127
  HookParams,
@@ -139,11 +151,13 @@ export type {
139
151
  StorageInfo,
140
152
  StorageResourceStat,
141
153
  BackendInfo,
142
- OriginKind,
143
- FleetOrigin,
154
+ WorkerKind,
155
+ WorkerState,
156
+ WorkerLastOp,
157
+ FleetWorker,
144
158
  FleetWorkers,
145
159
  FleetOutcome,
146
- FleetOriginResult,
160
+ FleetWorkerResult,
147
161
  FleetResult,
148
162
  FleetReloadResult,
149
163
  FleetReportFanout,
package/src/schemas.ts CHANGED
@@ -319,77 +319,117 @@ export type BackendInfo = z.infer<typeof backendInfo>;
319
319
 
320
320
  /** The two worker kinds that join the bus: an ASGI `serve` worker or a
321
321
  * `backend` runtime process. A drift throws `ApiSchemaError`. */
322
- export const originKind = z.enum(['serve', 'backend']);
323
- export type OriginKind = z.infer<typeof originKind>;
322
+ export const workerKind = z.enum(['serve', 'backend']);
323
+ export type WorkerKind = z.infer<typeof workerKind>;
324
+
325
+ /** The lifecycle state a worker advertises in its presence value: `ready` once its
326
+ * resync has converged, `resyncing` while a boot/reconnect resync runs, `recycling`
327
+ * once it has begun a graceful self-exit. A drift throws `ApiSchemaError`. */
328
+ export const workerState = z.enum(['ready', 'resyncing', 'recycling']);
329
+ export type WorkerState = z.infer<typeof workerState>;
330
+
331
+ /** The last op a worker applied, stamped onto its presence row after the terminal
332
+ * reply. `outcome` is the free op-outcome string; `at` is the ISO instant it landed. */
333
+ export const workerLastOp = z.object({
334
+ op: z.string(),
335
+ outcome: z.string(),
336
+ at: z.string(),
337
+ });
338
+ export type WorkerLastOp = z.infer<typeof workerLastOp>;
324
339
 
325
340
  /**
326
- * One live worker on the bus — a presence-census entry. `origin` is the
327
- * `{kind}-{uuid}` identity used as a reload target; `kind` tells the two worker
328
- * kinds apart; `pid` is the process id the presence key carries.
341
+ * One live worker on the bus — a presence-census row. `name` is the stable slot
342
+ * (`{kind}-{n}`) that doubles as a reload target; `generation` is the monotonic life
343
+ * counter minted with the worker's claim; `kind` tells the two worker kinds apart;
344
+ * `pid` is the process id. `joined_at`/`beat_at` are ISO instants (first-seen and last
345
+ * heartbeat) for the cosmetic seen-since display; `state` is the advertised lifecycle
346
+ * state; `stale` is SERVER-computed from the row's remaining presence TTL against the
347
+ * bus cadence (a quiet row past the freshness bound — reconnecting or dead) and is the
348
+ * only freshness signal a client reads, never a client-side threshold; `last_op` is the
349
+ * worker's last applied op, or `null` before it has applied one.
329
350
  */
330
- export const fleetOrigin = z.object({
331
- origin: z.string(),
332
- kind: originKind,
351
+ export const fleetWorker = z.object({
352
+ name: z.string(),
353
+ kind: workerKind,
333
354
  pid: z.number(),
355
+ generation: z.number(),
356
+ joined_at: z.string(),
357
+ beat_at: z.string(),
358
+ state: workerState,
359
+ stale: z.boolean(),
360
+ last_op: workerLastOp.nullable(),
334
361
  });
335
- export type FleetOrigin = z.infer<typeof fleetOrigin>;
362
+ export type FleetWorker = z.infer<typeof fleetWorker>;
336
363
 
337
364
  /**
338
365
  * `GET /api/fleet/workers` — the live worker fleet from the bus presence census.
339
- * The census lists ALL subscribed origins (ASGI `serve` workers and backend-runtime
366
+ * The census lists ALL subscribed workers (ASGI `serve` workers and backend-runtime
340
367
  * processes alike), not only backend workers. A census read that fails surfaces as a
341
368
  * loud 500, never a fabricated empty fleet.
342
369
  */
343
- export const fleetWorkers = z.object({ workers: z.array(fleetOrigin) });
370
+ export const fleetWorkers = z.object({ workers: z.array(fleetWorker) });
344
371
  export type FleetWorkers = z.infer<typeof fleetWorkers>;
345
372
 
346
373
  /**
347
- * One origin's outcome within a fleet broadcast. `applied`/`failed` are terminal
348
- * wire replies from a subscriber; `missing`/`departed`/`timed_out` are
349
- * publisher-computed from the presence census (a silent origin never sends them).
350
- */
351
- export const fleetOutcome = z.enum(['applied', 'failed', 'missing', 'departed', 'timed_out']);
374
+ * One worker's outcome within a fleet broadcast. `applied`/`failed` are terminal wire
375
+ * replies from a subscriber; `missing`/`departed`/`timed_out` are publisher-computed
376
+ * from the presence census (a silent worker never sends them); `resyncing`/`recycling`/
377
+ * `stale` are the gap outcomes for a row that failed the ready+fresh gate — reported as
378
+ * its own actual condition rather than dropped (`resyncing` converges on its resync,
379
+ * `recycling` is departing, `stale` is a quiet row past the freshness bound carrying no
380
+ * convergence promise).
381
+ */
382
+ export const fleetOutcome = z.enum([
383
+ 'applied',
384
+ 'failed',
385
+ 'missing',
386
+ 'departed',
387
+ 'timed_out',
388
+ 'resyncing',
389
+ 'recycling',
390
+ 'stale',
391
+ ]);
352
392
  export type FleetOutcome = z.infer<typeof fleetOutcome>;
353
393
 
354
394
  /**
355
- * One origin's result within a {@link fleetResult}. `payload` carries query-op data
395
+ * One worker's result within a {@link fleetResult}. `payload` carries query-op data
356
396
  * (opaque to the UI); `error` carries a failed apply's message; `detail` carries the
357
- * publisher's report text for a computed `missing`/`departed`/`timed_out`. A drift
358
- * throws `ApiSchemaError`.
397
+ * publisher's report text for a computed gap outcome (`missing`/`departed`/`timed_out`/
398
+ * `resyncing`/`recycling`/`stale`). A drift throws `ApiSchemaError`.
359
399
  */
360
- export const fleetOriginResult = z.object({
361
- origin: z.string(),
400
+ export const fleetWorkerResult = z.object({
401
+ name: z.string(),
362
402
  outcome: fleetOutcome,
363
403
  payload: jsonValue,
364
404
  error: z.string().nullable(),
365
405
  detail: z.string().nullable(),
366
406
  });
367
- export type FleetOriginResult = z.infer<typeof fleetOriginResult>;
407
+ export type FleetWorkerResult = z.infer<typeof fleetWorkerResult>;
368
408
 
369
409
  /**
370
- * The awaited per-origin report of one fleet broadcast. Two honest shapes ride the
410
+ * The awaited per-worker report of one fleet broadcast. Two honest shapes ride the
371
411
  * same schema. Reachable (`reachable: true`): `results` holds one entry per expected
372
- * origin (the serving worker's own entry included), so an unconfirmed sibling is a
373
- * visible `failed`/`missing`/`departed`/`timed_out` entry, never a silent stale
374
- * worker. Bus-unreachable (`reachable: false`): the transport failed before any
375
- * origin could reply, so `results` is empty and only `error` is carried. This is the
376
- * bare body of `POST /api/fleet/reload-config` and the single-MCP reload; it is also
377
- * the body the mode-wrapped {@link fleetReportFanout} carries under its `fleet` /
378
- * `unreachable` modes.
412
+ * worker (the serving worker's own entry included) plus the gap rows, so an unconfirmed
413
+ * sibling is a visible `failed`/`missing`/`departed`/`timed_out`/`resyncing`/`recycling`/
414
+ * `stale` entry, never a silent stale worker. Bus-unreachable (`reachable: false`): the
415
+ * transport failed before any worker could reply, so `results` is empty and only `error`
416
+ * is carried. This is the bare body of `POST /api/fleet/reload-config` and the single-MCP
417
+ * reload; it is also the body the mode-wrapped {@link fleetReportFanout} carries under its
418
+ * `fleet` / `unreachable` modes.
379
419
  */
380
420
  export const fleetResult = z.object({
381
421
  op: z.string(),
382
422
  reachable: z.boolean(),
383
423
  local_only: z.boolean(),
384
- results: z.array(fleetOriginResult),
424
+ results: z.array(fleetWorkerResult),
385
425
  error: z.string().nullable(),
386
426
  });
387
427
  export type FleetResult = z.infer<typeof fleetResult>;
388
428
 
389
429
  /**
390
- * `POST /api/fleet/reload-config` — soft-restart the fleet and report per-origin. The
430
+ * `POST /api/fleet/reload-config` — soft-restart the fleet and report per-worker. The
391
431
  * serving worker applies its own reload first, then broadcasts; the returned report
392
- * names every origin's outcome. A failed local apply does not abort the broadcast, so
432
+ * names every worker's outcome. A failed local apply does not abort the broadcast, so
393
433
  * the report is embedded even on the convergence-failure path.
394
434
  */
395
435
  export const fleetReloadResult = fleetResult;
@@ -403,7 +443,7 @@ export type FleetReloadResult = z.infer<typeof fleetReloadResult>;
403
443
  * - `fleet` — a reachable multi-worker broadcast; carries the full {@link
404
444
  * fleetResult} so an unconfirmed sibling is visible.
405
445
  * - `unreachable` — the bus itself could not be reached; carries the
406
- * bus-unreachable {@link fleetResult} (no origin list, only `error`).
446
+ * bus-unreachable {@link fleetResult} (no worker list, only `error`).
407
447
  *
408
448
  * Every mutation whose response embeds this parses it EXPLICITLY: zod's default
409
449
  * unknown-key stripping would otherwise silently drop the field, hiding a failed
@@ -501,7 +541,7 @@ export const mcpStatus = z.object({
501
541
 
502
542
  /**
503
543
  * `POST /api/mcp-status/{title}/reload` — re-probe a single MCP server and broadcast
504
- * the reload to the fleet. The response is the bare per-origin {@link fleetResult}
544
+ * the reload to the fleet. The response is the bare per-worker {@link fleetResult}
505
545
  * (the serving worker's re-probe rides its own `results` entry as the payload); there
506
546
  * is no local/workers split. A drift throws `ApiSchemaError`.
507
547
  */
@@ -615,16 +655,28 @@ export type SettingsProfileDiff = z.infer<typeof settingsProfileDiff>;
615
655
  * `POST /api/config/profiles/{name}/apply` — the dedicated `profile_apply_response`
616
656
  * (action=fenced, destructive). NAMES-ONLY (no env value in any
617
657
  * section): `hot` is the hot-swapped key names; `recycle` is the orchestrator's
618
- * per-origin section — the applier's OWN entry carries the `self-deferred` status
619
- * literal when the diff has serve-affecting recycle keys; `refused` is `[]` on
620
- * success by construction (populated `{key, reason}` only on a refusal), the
621
- * populated refusal surfaces being the diff preview and the refusal error report.
622
- * `fanout` is the mode-wrapped fleet broadcast, parsed EXPLICITLY so a failed
623
- * propagation reaches the shared fleet-report handler. A drift throws `ApiSchemaError`.
658
+ * per-worker section — one `{name, kind, status, generation_before}` row per recycled
659
+ * target, `generation_before` the life that was recycled, the applier's OWN row carrying
660
+ * the `self-deferred` status literal when the diff has serve-affecting recycle keys;
661
+ * `fresh` is the per-kind list of NEW ready lives observed since the pre-apply snapshot
662
+ * (each `{name, kind, generation}`), evidence of fresh capacity and never claimed as any
663
+ * target's successor; `refused` is `[]` on success by construction (populated
664
+ * `{key, reason}` only on a refusal), the populated refusal surfaces being the diff
665
+ * preview and the refusal error report. `fanout` is the mode-wrapped fleet broadcast,
666
+ * parsed EXPLICITLY so a failed propagation reaches the shared fleet-report handler. A
667
+ * drift throws `ApiSchemaError`.
624
668
  */
625
669
  export const profileApplyResponse = z.object({
626
670
  hot: z.array(z.string()),
627
- recycle: z.array(z.object({ origin: z.string(), kind: z.string(), status: z.string() })),
671
+ recycle: z.array(
672
+ z.object({
673
+ name: z.string(),
674
+ kind: z.string(),
675
+ status: z.string(),
676
+ generation_before: z.number(),
677
+ }),
678
+ ),
679
+ fresh: z.array(z.object({ name: z.string(), kind: z.string(), generation: z.number() })),
628
680
  refused: z.array(z.object({ key: z.string(), reason: z.string() })),
629
681
  fanout: fleetReportFanout,
630
682
  });
@@ -900,6 +952,139 @@ export const channels = z.object({
900
952
  });
901
953
  export type Channels = z.infer<typeof channels>;
902
954
 
955
+ // -- conversations -----------------------------------------------------------
956
+ // The cross-channel conversation monitor's READ side: the stored routing rows
957
+ // (the route picker) from `GET /api/conversations`, a route's threads from
958
+ // `GET /api/conversations/{route}/threads`, and one thread's transcript from
959
+ // `GET /api/conversations/{route}/transcript?thread_id=`. Timestamps are float
960
+ // epoch seconds, not ISO strings, and both paged doors carry the same window
961
+ // fields; the transcript adds the direction it was read in.
962
+
963
+ /** The inbound door a route is bound to: the authed API, or a channel medium. */
964
+ export const conversationDoor = z.enum(['api', 'channel']);
965
+ export type ConversationDoor = z.infer<typeof conversationDoor>;
966
+
967
+ /** What a route's turn runs: a registered agent, or a tool dispatch. */
968
+ export const conversationTargetKind = z.enum(['agent', 'tool']);
969
+ export type ConversationTargetKind = z.infer<typeof conversationTargetKind>;
970
+
971
+ /**
972
+ * Where a record sits between intake and a terminal outcome. `failed` is the one
973
+ * the monitor surfaces loudly; `shed` and `silent` are terminal by design and
974
+ * never send.
975
+ */
976
+ export const conversationDeliveryStatus = z.enum([
977
+ 'accepted',
978
+ 'pending_delivery',
979
+ 'provisional',
980
+ 'delivered',
981
+ 'failed',
982
+ 'shed',
983
+ 'silent',
984
+ ]);
985
+ export type ConversationDeliveryStatus = z.infer<typeof conversationDeliveryStatus>;
986
+
987
+ /** The nature of a turn's outcome, orthogonal to where its delivery stands. */
988
+ export const conversationAnswerStatus = z.enum(['answered', 'error', 'silent']);
989
+ export type ConversationAnswerStatus = z.infer<typeof conversationAnswerStatus>;
990
+
991
+ /** A stored routing row as a read door returns it, its `callback_secret` withheld. */
992
+ export const conversationRoute = z.object({
993
+ route_name: z.string(),
994
+ door: conversationDoor,
995
+ target_kind: conversationTargetKind,
996
+ target_name: z.string(),
997
+ payload_expr: z.string().nullable(),
998
+ reply_expr: z.string().nullable(),
999
+ execution_key: z.string(),
1000
+ channel: z.string().nullable(),
1001
+ our_identity: z.string().nullable(),
1002
+ callback_url: z.string().nullable(),
1003
+ execution_key_fingerprint: z.string(),
1004
+ });
1005
+ export type ConversationRoute = z.infer<typeof conversationRoute>;
1006
+
1007
+ export const conversationRoutes = z.object({
1008
+ items: z.array(conversationRoute),
1009
+ total: z.number(),
1010
+ });
1011
+ export type ConversationRoutes = z.infer<typeof conversationRoutes>;
1012
+
1013
+ /**
1014
+ * One thread of a route, summarized from its newest record. A `thread_id` holds
1015
+ * an api-door address (`{principal}/{end user}`, percent-encoded), so it never
1016
+ * goes back into a URL as a path segment — the transcript door takes it as a
1017
+ * query value.
1018
+ */
1019
+ export const conversationThread = z.object({
1020
+ thread_id: z.string(),
1021
+ client_address: z.string(),
1022
+ last_activity_at: z.number(),
1023
+ message_count: z.number(),
1024
+ last_delivery_status: conversationDeliveryStatus,
1025
+ });
1026
+ export type ConversationThread = z.infer<typeof conversationThread>;
1027
+
1028
+ /** The page window both thread read doors carry; `next_page` is null on the last. */
1029
+ const conversationPageWindow = {
1030
+ total: z.number(),
1031
+ page: z.number(),
1032
+ page_size: z.number(),
1033
+ next_page: z.number().nullable(),
1034
+ };
1035
+
1036
+ export const conversationThreadsPage = z.object({
1037
+ items: z.array(conversationThread),
1038
+ ...conversationPageWindow,
1039
+ });
1040
+ export type ConversationThreadsPage = z.infer<typeof conversationThreadsPage>;
1041
+
1042
+ /**
1043
+ * One exchange: the visitor's `inbound_text` — every record answers a message, so
1044
+ * both projections always carry it — and the agent's `answer`, plus where that
1045
+ * answer's delivery stands. The fields after `updated_at` are ADMIN ONLY —
1046
+ * the same door serves a caller-scoped projection that withholds them — so they
1047
+ * are optional here and absence means "not published to this reader".
1048
+ */
1049
+ export const conversationMessage = z.object({
1050
+ message_id: z.string(),
1051
+ route_name: z.string(),
1052
+ door: conversationDoor,
1053
+ thread_id: z.string(),
1054
+ client_address: z.string(),
1055
+ caller_principal: z.string().nullable(),
1056
+ inbound_text: z.string(),
1057
+ answer_status: conversationAnswerStatus.nullable(),
1058
+ answer: z.string().nullable(),
1059
+ delivery_status: conversationDeliveryStatus,
1060
+ created_at: z.number(),
1061
+ updated_at: z.number(),
1062
+ channel: z.string().nullable().optional(),
1063
+ our_identity: z.string().nullable().optional(),
1064
+ provider_message_id: z.string().nullable().optional(),
1065
+ callback_url: z.string().nullable().optional(),
1066
+ error: z.string().nullable().optional(),
1067
+ outbound_message_ids: z.array(z.string()).optional(),
1068
+ attempts: z.number().optional(),
1069
+ });
1070
+ export type ConversationMessage = z.infer<typeof conversationMessage>;
1071
+
1072
+ /**
1073
+ * The direction a transcript page reads in. `asc` is the reading order (oldest
1074
+ * first); `desc` is the live-tail order, where page 1 is always the newest page
1075
+ * and paging forward walks BACKWARDS through the thread's history.
1076
+ */
1077
+ export const transcriptOrder = z.enum(['asc', 'desc']);
1078
+ export type TranscriptOrder = z.infer<typeof transcriptOrder>;
1079
+
1080
+ /** A transcript page, echoing the order it was read in. */
1081
+ export const conversationTranscriptPage = z.object({
1082
+ items: z.array(conversationMessage),
1083
+ ...conversationPageWindow,
1084
+ order: transcriptOrder,
1085
+ });
1086
+ export type ConversationTranscriptPage = z.infer<typeof conversationTranscriptPage>;
1087
+
903
1088
  // -- notifications -----------------------------------------------------------
904
1089
  // The internal notifications sink: the messages the `notify_user` operation records
905
1090
  // with no channel, so they land only in the Studio inbox. Each record carries the