theokit 0.1.0-alpha.13 → 0.1.0-alpha.15

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.
@@ -1,5 +1,29 @@
1
1
  // src/server/cost/track-agent-run.ts
2
+ var __IS_DEV = (() => {
3
+ try {
4
+ return import.meta.env?.DEV === true;
5
+ } catch {
6
+ return process.env.NODE_ENV !== "production";
7
+ }
8
+ })();
2
9
  async function trackAgentRun(input, opts) {
10
+ if (__IS_DEV) {
11
+ try {
12
+ const mod = await import("./dispatcher-IK3FVMX5.js");
13
+ mod.dispatcher.onAgentRun({
14
+ // eslint-disable-next-line sonarjs/pseudo-random -- non-secret correlation id
15
+ id: `run-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`,
16
+ timestamp: (input.timestamp ?? /* @__PURE__ */ new Date()).getTime(),
17
+ userId: input.userId,
18
+ model: input.model,
19
+ tokensInput: input.tokens.input,
20
+ tokensOutput: input.tokens.output,
21
+ costUsd: input.costUsd,
22
+ status: input.status ?? "finished"
23
+ });
24
+ } catch {
25
+ }
26
+ }
3
27
  if (!opts.storage) return;
4
28
  const record = {
5
29
  userId: input.userId,
@@ -158,4 +182,4 @@ export {
158
182
  InMemoryUsageStorage,
159
183
  trackAgentTools
160
184
  };
161
- //# sourceMappingURL=chunk-MCP6Y5BA.js.map
185
+ //# sourceMappingURL=chunk-JBC7PFV4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/cost/track-agent-run.ts","../src/server/cost/usage-storage-memory.ts","../src/server/cost/track-agent-tools.ts"],"sourcesContent":["import type { UsageRecord, UsageStorageAdapter } from './cost-types.js'\n\nexport interface TrackAgentRunInput {\n userId: string\n model: string\n tokens: { input: number; output: number }\n costUsd: number\n /** Defaults to `new Date()`. */\n timestamp?: Date\n /** Optional execution status surfaced to devtools (default 'finished'). */\n status?: 'finished' | 'error' | 'aborted'\n}\n\nexport interface TrackAgentRunOptions {\n /** Adapter resolved from `theo.config.ts > cost.storage`. */\n storage: UsageStorageAdapter | undefined\n}\n\n// v1.1 EC-4 MUST FIX — universal dev gate (Vite OR tsup-bundled).\n// Both checks are statically replaceable by bundlers in prod build,\n// so the entire dispatcher import is tree-shaken from the prod bundle.\nconst __IS_DEV = (() => {\n try {\n return (import.meta as { env?: { DEV?: boolean } }).env?.DEV === true\n } catch {\n return process.env.NODE_ENV !== 'production'\n }\n})()\n\n/**\n * Record a single agent run's usage + cost. Companion to the client-side\n * `<CostMeter>` from `@usetheo/ui`.\n *\n * EC-14: this function NEVER bubbles errors back to the caller. Adapter\n * failures (network outage, DB down, etc.) are logged via `console.warn`\n * and swallowed. The agent response MUST NOT fail because cost tracking\n * is degraded.\n *\n * No-op when `storage` is undefined (cost tracking unconfigured).\n *\n * In dev mode (Vite OR tsup-built with NODE_ENV != 'production'), fires a\n * `theokit-evolution-ci-and-dx` Phase 3 dispatcher event so the devtools\n * Agents tab can render the run. Prod tree-shakes the entire dispatcher\n * import via the `__IS_DEV` IIFE guard.\n *\n * @see docs/concepts/cost-tracking.md (when implemented in T6.3)\n */\nexport async function trackAgentRun(\n input: TrackAgentRunInput,\n opts: TrackAgentRunOptions,\n): Promise<void> {\n // Dev-only: surface run to devtools Agents tab (T3.1)\n if (__IS_DEV) {\n try {\n const mod = (await import('../../devtools/dispatcher.js')) as {\n dispatcher: { onAgentRun: (r: import('../../devtools/shared.js').AgentRunRecord) => void }\n }\n mod.dispatcher.onAgentRun({\n // eslint-disable-next-line sonarjs/pseudo-random -- non-secret correlation id\n id: `run-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`,\n timestamp: (input.timestamp ?? new Date()).getTime(),\n userId: input.userId,\n model: input.model,\n tokensInput: input.tokens.input,\n tokensOutput: input.tokens.output,\n costUsd: input.costUsd,\n status: input.status ?? 'finished',\n })\n } catch {\n // devtools dispatcher missing in some prod-like bundle — silently skip\n }\n }\n\n if (!opts.storage) return\n const record: UsageRecord = {\n userId: input.userId,\n model: input.model,\n tokens: input.tokens,\n costUsd: input.costUsd,\n timestamp: input.timestamp ?? new Date(),\n }\n try {\n await opts.storage.record(record)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n\n console.warn(\n `[theokit:cost] usage storage record failed: ${message}. ` +\n '(Response is unaffected — cost tracking degraded.)',\n )\n }\n}\n","import type { StorageAdapter } from '../storage/storage-types.js'\n\nimport type {\n ToolUsageRecord,\n UsageQuery,\n UsageRecord,\n UsageResult,\n UsageStorageAdapter,\n} from './cost-types.js'\n\n/**\n * In-memory usage storage for dev/tests. Unbounded — production\n * deployments MUST plug in a durable adapter (Postgres/Redis recipes\n * land with R0.6.7; documented in EC-114).\n *\n * Phase 5 — Production-Readiness #4: accepts both `UsageRecord` (LLM call,\n * kind='llm' or omitted) AND `ToolUsageRecord` (tool call, kind='tool').\n * `getUsage` only sums LLM records (tools have no token/cost dimension).\n *\n * EC-9 (backward compat): input without `kind` is normalized to `kind:'llm'`\n * — adapters from older versions stay working.\n *\n * Concurrency: Node's single-threaded event loop guarantees that\n * `Array.push` is atomic, so concurrent record() calls cannot lose data.\n */\nexport class InMemoryUsageStorage implements UsageStorageAdapter, StorageAdapter {\n readonly name = 'memory'\n readonly #records: (UsageRecord | ToolUsageRecord)[] = []\n\n /**\n * T2.3 — `StorageAdapter` lifecycle hook (ADR-0007 D6). In-memory storage\n * has no real cleanup to perform — this is intentionally a noop so the\n * adapter can be registered with `StorageManager.register()` and\n * participate in graceful shutdown without changing behavior.\n *\n * Does NOT clear stored records (dispose ≠ reset). For test reset use\n * a fresh instance.\n */\n dispose(): Promise<void> {\n // Intentional noop — see jsdoc above\n return Promise.resolve()\n }\n\n async record(input: UsageRecord | ToolUsageRecord): Promise<void> {\n // EC-9: normalize legacy input (no `kind` field) to kind:'llm'.\n const normalized: UsageRecord | ToolUsageRecord =\n 'kind' in input && input.kind === 'tool'\n ? { ...input }\n : ({ ...input, kind: 'llm' as const } satisfies UsageRecord)\n this.#records.push(normalized)\n return Promise.resolve()\n }\n\n async getUsage(query: UsageQuery): Promise<UsageResult> {\n const from = query.period.from.getTime()\n const to = query.period.to.getTime()\n let totalTokens = 0\n let totalCostUsd = 0\n let runs = 0\n for (const r of this.#records) {\n if (r.kind === 'tool') continue\n if (r.userId !== query.userId) continue\n const t = r.timestamp.getTime()\n if (t < from || t > to) continue\n totalTokens += r.tokens.input + r.tokens.output\n totalCostUsd += r.costUsd\n runs += 1\n }\n return Promise.resolve({ totalTokens, totalCostUsd, runs })\n }\n\n /** @internal — test helper to inspect raw record stream */\n __getRecords(): readonly (UsageRecord | ToolUsageRecord)[] {\n return this.#records\n }\n}\n","import type { ToolUsageRecord, UsageStorageAdapter } from './cost-types.js'\n\n/**\n * Phase 5 — Production-Readiness #4: tool lifecycle hooks for cost tracking.\n *\n * The SDK calls `onToolStart` → handler runs → `onToolEnd` (success) or\n * `onToolError` (throw). This factory returns the three callbacks ready to\n * attach to `Agent.create({ tools, onToolStart, onToolEnd, onToolError })`.\n *\n * Each successful tool call writes a `ToolUsageRecord{success:true}`; each\n * failure writes `ToolUsageRecord{success:false}` with `errorMessage` from\n * the thrown Error.\n *\n * Invariants:\n * - `onToolStart` records nothing yet (no duration available). Only stashes\n * a `startedAt` timestamp by `callId` in a per-factory Map.\n * - `onToolEnd` reads the stashed timestamp, computes `durationMs`, writes\n * a successful record. Removes the Map entry.\n * - `onToolError` analog with `success:false`.\n * - Hook throws are SWALLOWED to stderr — observation hooks must not crash\n * the run (per the v1.1.0 release contract).\n * - EC-8 (SHOULD TEST) — orphan starts (no matching End/Error) are pruned\n * on every onToolStart if older than 5 minutes. Bounded memory.\n * - EC-16 (DOCUMENT) — callId uniqueness is SDK contract; duplicate Start\n * for the same callId uses last-write-wins.\n */\n\nconst ORPHAN_TTL_MS = 5 * 60 * 1000\n\ninterface PendingStart {\n startedAt: number\n userId: string\n conversationId: string\n}\n\nexport interface TrackAgentToolsOptions {\n storage: UsageStorageAdapter\n /** Defaults to () => new Date() — overridable for deterministic testing. */\n now?: () => Date\n /** Identifier for the user — passed through to ToolUsageRecord.userId. */\n userId?: string\n /** Identifier for the conversation — passed through to ToolUsageRecord.conversationId. */\n conversationId?: string\n}\n\nexport interface ToolHookEvent {\n callId: string\n name: string\n // SDK shape — we only read these structurally\n [key: string]: unknown\n}\n\nexport interface TrackAgentToolsHooks {\n onToolStart: (event: ToolHookEvent) => void\n onToolEnd: (event: ToolHookEvent) => void\n onToolError: (event: ToolHookEvent) => void\n}\n\n/**\n * Create the three tool-lifecycle callbacks. Hand the returned object's\n * methods to `Agent.create({ tools, onToolStart, onToolEnd, onToolError })`.\n */\nexport function trackAgentTools(opts: TrackAgentToolsOptions): TrackAgentToolsHooks {\n const pending = new Map<string, PendingStart>()\n const now = opts.now ?? ((): Date => new Date())\n const userId = opts.userId ?? 'anonymous'\n const conversationId = opts.conversationId ?? 'unknown'\n\n function pruneOrphans(): void {\n const cutoff = now().getTime() - ORPHAN_TTL_MS\n for (const [id, p] of pending) {\n if (p.startedAt < cutoff) pending.delete(id)\n }\n }\n\n function safeRecord(record: ToolUsageRecord): void {\n Promise.resolve(opts.storage.record(record)).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : String(err)\n console.warn(`[theokit:trackAgentTools] storage.record failed: ${msg}`)\n })\n }\n\n return {\n onToolStart(event: ToolHookEvent): void {\n try {\n pruneOrphans()\n pending.set(event.callId, {\n startedAt: now().getTime(),\n userId,\n conversationId,\n })\n } catch (err) {\n console.warn(\n `[theokit:trackAgentTools] onToolStart hook crashed (swallowed): ${\n err instanceof Error ? err.message : String(err)\n }`,\n )\n }\n },\n onToolEnd(event: ToolHookEvent): void {\n try {\n const start = pending.get(event.callId)\n const startedAt = start?.startedAt ?? now().getTime()\n const durationMs = start ? now().getTime() - startedAt : 0\n pending.delete(event.callId)\n if (!start) {\n console.warn(\n `[theokit:trackAgentTools] orphan onToolEnd for callId=${event.callId}; durationMs:0 recorded`,\n )\n }\n safeRecord({\n kind: 'tool',\n userId: start?.userId ?? userId,\n conversationId: start?.conversationId ?? conversationId,\n toolName: event.name,\n callId: event.callId,\n success: true,\n durationMs,\n timestamp: now(),\n })\n } catch (err) {\n console.warn(\n `[theokit:trackAgentTools] onToolEnd hook crashed (swallowed): ${\n err instanceof Error ? err.message : String(err)\n }`,\n )\n }\n },\n onToolError(event: ToolHookEvent): void {\n try {\n const start = pending.get(event.callId)\n const startedAt = start?.startedAt ?? now().getTime()\n const durationMs = start ? now().getTime() - startedAt : 0\n pending.delete(event.callId)\n const error = event.error\n let errorMessage = 'unknown error'\n if (error instanceof Error) errorMessage = error.message\n else if (typeof error === 'string') errorMessage = error\n safeRecord({\n kind: 'tool',\n userId: start?.userId ?? userId,\n conversationId: start?.conversationId ?? conversationId,\n toolName: event.name,\n callId: event.callId,\n success: false,\n durationMs,\n errorMessage,\n timestamp: now(),\n })\n } catch (err) {\n console.warn(\n `[theokit:trackAgentTools] onToolError hook crashed (swallowed): ${\n err instanceof Error ? err.message : String(err)\n }`,\n )\n }\n },\n }\n}\n\n/**\n * @internal — testing helper. Inspects the size of the pending-starts Map of\n * a factory instance. Not in the public API.\n */\nexport const __internalsForTesting = {\n ORPHAN_TTL_MS,\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,105 @@
1
+ // src/devtools/shared.ts
2
+ var RING_BUFFER_CAP = 50;
3
+ var MAX_QUEUE_SIZE = 100;
4
+ var STORAGE_VERSION = 1;
5
+ var initialState = {
6
+ open: false,
7
+ visible: true,
8
+ position: "bottom-right",
9
+ theme: "system",
10
+ activeTab: "requests",
11
+ requests: [],
12
+ errors: [],
13
+ agentRuns: [],
14
+ routeManifest: null,
15
+ activeRoutePath: null,
16
+ activeChain: []
17
+ };
18
+ var CHANNEL_REQUEST = "theo:devtools:request";
19
+ var CHANNEL_ERROR = "theo:devtools:error";
20
+ var CHANNEL_CSRF_WARN = "theo:devtools:csrf.warn";
21
+ var CHANNEL_MANIFEST = "theo:devtools:manifest";
22
+ var CHANNEL_ROUTE_MATCHED = "theo:devtools:route-matched";
23
+
24
+ // src/devtools/dispatcher.ts
25
+ var _dispatch = null;
26
+ var _queue = [];
27
+ function queuable(fn) {
28
+ return (...args) => {
29
+ if (_dispatch) {
30
+ try {
31
+ fn(_dispatch, ...args);
32
+ } catch (err) {
33
+ console.error("[theo devtools] dispatch failed", err);
34
+ }
35
+ } else {
36
+ if (_queue.length >= MAX_QUEUE_SIZE) _queue.shift();
37
+ _queue.push((d) => {
38
+ fn(d, ...args);
39
+ });
40
+ }
41
+ };
42
+ }
43
+ function flushQueue() {
44
+ while (_queue.length) {
45
+ const fn = _queue.shift();
46
+ if (!fn || !_dispatch) break;
47
+ try {
48
+ fn(_dispatch);
49
+ } catch (err) {
50
+ console.error("[theo devtools] queued event failed", err);
51
+ }
52
+ }
53
+ }
54
+ var dispatcher = {
55
+ onRequest: queuable((d, req) => {
56
+ d({ type: "REQUEST_ADD", request: req });
57
+ }),
58
+ onError: queuable((d, err) => {
59
+ d({ type: "ERROR_ADD", error: err });
60
+ }),
61
+ onCsrfWarn: queuable((d, payload) => {
62
+ d({ type: "CSRF_WARN", payload });
63
+ }),
64
+ onManifestUpdated: queuable((d, manifest) => {
65
+ d({ type: "MANIFEST_UPDATED", manifest });
66
+ }),
67
+ onRouteMatched: queuable((d, path, chain) => {
68
+ d({ type: "ROUTE_MATCHED", path, chain });
69
+ }),
70
+ onAgentRun: queuable((d, run) => {
71
+ d({ type: "AGENT_RUN_ADD", run });
72
+ }),
73
+ /**
74
+ * Wire React's dispatch function in. Idempotent (EC-24): only the
75
+ * NULL → non-null transition flushes the queue; subsequent non-null
76
+ * sets just replace the reference (used for StrictMode re-mount).
77
+ */
78
+ setDispatch(d) {
79
+ const prev = _dispatch;
80
+ _dispatch = d;
81
+ if (d && !prev) flushQueue();
82
+ },
83
+ /** Testing helper — clear queue + dispatch reference between tests. */
84
+ _reset() {
85
+ _dispatch = null;
86
+ _queue.length = 0;
87
+ },
88
+ /** Testing helper — observe queue length. */
89
+ _queueLength() {
90
+ return _queue.length;
91
+ }
92
+ };
93
+
94
+ export {
95
+ RING_BUFFER_CAP,
96
+ STORAGE_VERSION,
97
+ initialState,
98
+ CHANNEL_REQUEST,
99
+ CHANNEL_ERROR,
100
+ CHANNEL_CSRF_WARN,
101
+ CHANNEL_MANIFEST,
102
+ CHANNEL_ROUTE_MATCHED,
103
+ dispatcher
104
+ };
105
+ //# sourceMappingURL=chunk-OK2PJOLK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/devtools/shared.ts","../src/devtools/dispatcher.ts"],"sourcesContent":["/**\n * Devtools — shared types.\n *\n * NEVER use dangerouslySetInnerHTML in any devtools component — see plan EC-20.\n */\n\nexport type DevtoolsPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\nexport type DevtoolsTab = 'requests' | 'routes' | 'agents' | 'errors' | 'csrf-readiness' | 'settings'\nexport type DevtoolsTheme = 'light' | 'dark' | 'system'\n\nexport interface RequestRecord {\n id: string\n traceId: string\n method: string\n path: string\n status: number\n durationMs: number\n startedAt: number\n csrfWarn?: { code: string; docsUrl: string }\n headers?: Record<string, string>\n bodyPreview?: string\n bodyLength?: number\n bodyTruncated?: boolean\n}\n\nexport interface ErrorRecord {\n id: string\n type: 'console' | 'unhandled' | 'csrf.warn'\n message: string\n stack?: string\n code?: string\n docsUrl?: string\n timestamp: number\n}\n\nexport interface CsrfWarnPayload {\n event: 'csrf.warn'\n code: string\n docsUrl: string\n method: string\n path: string\n reason: string\n}\n\nexport interface RouteInfo {\n path: string\n absoluteFilePath: string\n layoutChain: string[]\n hasLoading: boolean\n hasError: boolean\n hasNotFound: boolean\n}\n\nexport interface RouteManifest {\n routes: RouteInfo[]\n}\n\n/**\n * Per-agent-run telemetry surfaced to the Agents devtools tab.\n *\n * Emitted by `trackAgentRun` (server-side) via the dispatcher in dev mode;\n * prod tree-shakes the entire wire (v1.1 EC-4 `__IS_DEV` IIFE guard).\n * Type lives here (devtools/shared) since both producer (server/cost) and\n * consumer (AgentsTab) need the shape; following architecture v3 ADR-0001\n * rule \"shared types in core/contracts\" is the canonical home — for this\n * one we keep it in devtools/shared to minimize churn in core/contracts/.\n */\nexport interface AgentRunRecord {\n id: string\n timestamp: number\n userId: string\n model: string\n tokensInput: number\n tokensOutput: number\n costUsd: number\n status: 'finished' | 'error' | 'aborted'\n}\n\nexport interface DevtoolsState {\n open: boolean\n visible: boolean\n position: DevtoolsPosition\n theme: DevtoolsTheme\n activeTab: DevtoolsTab\n requests: RequestRecord[]\n errors: ErrorRecord[]\n agentRuns: AgentRunRecord[]\n routeManifest: RouteManifest | null\n activeRoutePath: string | null\n activeChain: string[]\n}\n\nexport type DevtoolsAction =\n | { type: 'TOGGLE_PANEL' }\n | { type: 'TOGGLE_VISIBLE' }\n | { type: 'SET_TAB'; tab: DevtoolsTab }\n | { type: 'SET_POSITION'; position: DevtoolsPosition }\n | { type: 'SET_THEME'; theme: DevtoolsTheme }\n | { type: 'REQUEST_ADD'; request: RequestRecord }\n | { type: 'ERROR_ADD'; error: ErrorRecord }\n | { type: 'CSRF_WARN'; payload: CsrfWarnPayload }\n | { type: 'MANIFEST_UPDATED'; manifest: RouteManifest }\n | { type: 'ROUTE_MATCHED'; path: string; chain: string[] }\n | { type: 'AGENT_RUN_ADD'; run: AgentRunRecord }\n | { type: 'RESET_REQUESTS' }\n | { type: 'RESET_ERRORS' }\n | { type: 'RESET_AGENT_RUNS' }\n\nexport const RING_BUFFER_CAP = 50\nexport const MAX_QUEUE_SIZE = 100\nexport const STORAGE_VERSION = 1\n\nexport const initialState: DevtoolsState = {\n open: false,\n visible: true,\n position: 'bottom-right',\n theme: 'system',\n activeTab: 'requests',\n requests: [],\n errors: [],\n agentRuns: [],\n routeManifest: null,\n activeRoutePath: null,\n activeChain: [],\n}\n\nexport const CHANNEL_REQUEST = 'theo:devtools:request' as const\nexport const CHANNEL_ERROR = 'theo:devtools:error' as const\nexport const CHANNEL_CSRF_WARN = 'theo:devtools:csrf.warn' as const\nexport const CHANNEL_MANIFEST = 'theo:devtools:manifest' as const\nexport const CHANNEL_ROUTE_MATCHED = 'theo:devtools:route-matched' as const\nexport const CHANNEL_AGENT_RUN = 'theo:devtools:agent.run' as const\n","/**\n * T2.1 — Devtools dispatcher with pre-mount event queue.\n *\n * Pattern: events emitted from anywhere (server WS, browser console.error,\n * dispatcher.onCsrfWarn) can fire BEFORE the React tree mounts. Each call\n * either dispatches immediately (when `setDispatch` registered a dispatch\n * fn) or queues until React is ready, then flushes on first mount.\n *\n * EC-23: queue capped at MAX_QUEUE_SIZE (FIFO eviction).\n * EC-24: setDispatch is idempotent — flush only on NULL → non-null\n * transition (StrictMode re-mount safe).\n *\n * NEVER use dangerouslySetInnerHTML in any devtools component — see plan EC-20.\n */\nimport {\n type AgentRunRecord,\n type CsrfWarnPayload,\n type DevtoolsAction,\n type ErrorRecord,\n MAX_QUEUE_SIZE,\n type RequestRecord,\n type RouteManifest,\n} from './shared.js'\n\ntype Dispatch = (action: DevtoolsAction) => void\ntype QueuedItem = (d: Dispatch) => void\n\nlet _dispatch: Dispatch | null = null\nconst _queue: QueuedItem[] = []\n\nfunction queuable<Args extends unknown[]>(\n fn: (d: Dispatch, ...args: Args) => void,\n): (...args: Args) => void {\n return (...args: Args) => {\n if (_dispatch) {\n try {\n fn(_dispatch, ...args)\n } catch (err) {\n // EC-25: an error inside reducer-bound dispatch path MUST NOT bubble\n // to the original event source (e.g. logger / HMR callback / global\n // error handler). Log and continue.\n\n console.error('[theo devtools] dispatch failed', err)\n }\n } else {\n // EC-23: cap queue, FIFO-evict if full\n if (_queue.length >= MAX_QUEUE_SIZE) _queue.shift()\n _queue.push((d) => {\n fn(d, ...args)\n })\n }\n }\n}\n\nfunction flushQueue(): void {\n while (_queue.length) {\n const fn = _queue.shift()\n if (!fn || !_dispatch) break\n try {\n fn(_dispatch)\n } catch (err) {\n console.error('[theo devtools] queued event failed', err)\n }\n }\n}\n\nexport const dispatcher = {\n onRequest: queuable((d: Dispatch, req: RequestRecord) => {\n d({ type: 'REQUEST_ADD', request: req })\n }),\n onError: queuable((d: Dispatch, err: ErrorRecord) => {\n d({ type: 'ERROR_ADD', error: err })\n }),\n onCsrfWarn: queuable((d: Dispatch, payload: CsrfWarnPayload) => {\n d({ type: 'CSRF_WARN', payload })\n }),\n onManifestUpdated: queuable((d: Dispatch, manifest: RouteManifest) => {\n d({ type: 'MANIFEST_UPDATED', manifest })\n }),\n onRouteMatched: queuable((d: Dispatch, path: string, chain: string[]) => {\n d({ type: 'ROUTE_MATCHED', path, chain })\n }),\n onAgentRun: queuable((d: Dispatch, run: AgentRunRecord) => {\n d({ type: 'AGENT_RUN_ADD', run })\n }),\n\n /**\n * Wire React's dispatch function in. Idempotent (EC-24): only the\n * NULL → non-null transition flushes the queue; subsequent non-null\n * sets just replace the reference (used for StrictMode re-mount).\n */\n setDispatch(d: Dispatch | null): void {\n const prev = _dispatch\n _dispatch = d\n if (d && !prev) flushQueue()\n },\n\n /** Testing helper — clear queue + dispatch reference between tests. */\n _reset(): void {\n _dispatch = null\n _queue.length = 0\n },\n\n /** Testing helper — observe queue length. */\n _queueLength(): number {\n return _queue.length\n },\n}\n\nexport type Dispatcher = typeof dispatcher\n"],"mappings":";AA4GO,IAAM,kBAAkB;AACxB,IAAM,iBAAiB;AACvB,IAAM,kBAAkB;AAExB,IAAM,eAA8B;AAAA,EACzC,MAAM;AAAA,EACN,SAAS;AAAA,EACT,UAAU;AAAA,EACV,OAAO;AAAA,EACP,WAAW;AAAA,EACX,UAAU,CAAC;AAAA,EACX,QAAQ,CAAC;AAAA,EACT,WAAW,CAAC;AAAA,EACZ,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,aAAa,CAAC;AAChB;AAEO,IAAM,kBAAkB;AACxB,IAAM,gBAAgB;AACtB,IAAM,oBAAoB;AAC1B,IAAM,mBAAmB;AACzB,IAAM,wBAAwB;;;ACvGrC,IAAI,YAA6B;AACjC,IAAM,SAAuB,CAAC;AAE9B,SAAS,SACP,IACyB;AACzB,SAAO,IAAI,SAAe;AACxB,QAAI,WAAW;AACb,UAAI;AACF,WAAG,WAAW,GAAG,IAAI;AAAA,MACvB,SAAS,KAAK;AAKZ,gBAAQ,MAAM,mCAAmC,GAAG;AAAA,MACtD;AAAA,IACF,OAAO;AAEL,UAAI,OAAO,UAAU,eAAgB,QAAO,MAAM;AAClD,aAAO,KAAK,CAAC,MAAM;AACjB,WAAG,GAAG,GAAG,IAAI;AAAA,MACf,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAEA,SAAS,aAAmB;AAC1B,SAAO,OAAO,QAAQ;AACpB,UAAM,KAAK,OAAO,MAAM;AACxB,QAAI,CAAC,MAAM,CAAC,UAAW;AACvB,QAAI;AACF,SAAG,SAAS;AAAA,IACd,SAAS,KAAK;AACZ,cAAQ,MAAM,uCAAuC,GAAG;AAAA,IAC1D;AAAA,EACF;AACF;AAEO,IAAM,aAAa;AAAA,EACxB,WAAW,SAAS,CAAC,GAAa,QAAuB;AACvD,MAAE,EAAE,MAAM,eAAe,SAAS,IAAI,CAAC;AAAA,EACzC,CAAC;AAAA,EACD,SAAS,SAAS,CAAC,GAAa,QAAqB;AACnD,MAAE,EAAE,MAAM,aAAa,OAAO,IAAI,CAAC;AAAA,EACrC,CAAC;AAAA,EACD,YAAY,SAAS,CAAC,GAAa,YAA6B;AAC9D,MAAE,EAAE,MAAM,aAAa,QAAQ,CAAC;AAAA,EAClC,CAAC;AAAA,EACD,mBAAmB,SAAS,CAAC,GAAa,aAA4B;AACpE,MAAE,EAAE,MAAM,oBAAoB,SAAS,CAAC;AAAA,EAC1C,CAAC;AAAA,EACD,gBAAgB,SAAS,CAAC,GAAa,MAAc,UAAoB;AACvE,MAAE,EAAE,MAAM,iBAAiB,MAAM,MAAM,CAAC;AAAA,EAC1C,CAAC;AAAA,EACD,YAAY,SAAS,CAAC,GAAa,QAAwB;AACzD,MAAE,EAAE,MAAM,iBAAiB,IAAI,CAAC;AAAA,EAClC,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOD,YAAY,GAA0B;AACpC,UAAM,OAAO;AACb,gBAAY;AACZ,QAAI,KAAK,CAAC,KAAM,YAAW;AAAA,EAC7B;AAAA;AAAA,EAGA,SAAe;AACb,gBAAY;AACZ,WAAO,SAAS;AAAA,EAClB;AAAA;AAAA,EAGA,eAAuB;AACrB,WAAO,OAAO;AAAA,EAChB;AACF;","names":[]}