@llui/agent 0.0.38 → 0.0.40

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.
@@ -28,7 +28,8 @@ export async function handleSendMessage(host, args) {
28
28
  // correct from in one round trip. Reducers stay the last line of
29
29
  // defense; this is the first.
30
30
  const schema = host.getMsgSchema?.() ?? null;
31
- const validation = validatePayload(args.msg, schema);
31
+ const policy = host.getDispatchPolicy?.() ?? 'lenient';
32
+ const validation = validatePayload(args.msg, schema, { policy });
32
33
  if (!validation.ok) {
33
34
  return {
34
35
  status: 'rejected',
@@ -36,6 +37,11 @@ export async function handleSendMessage(host, args) {
36
37
  detail: validation.errors.map((e) => `${e.path}: ${e.message}`).join('; '),
37
38
  };
38
39
  }
40
+ // Warnings from the validator (strict-mode `untyped-field` flags etc.)
41
+ // ride along to `drain.warnings` so the agent sees them on the
42
+ // dispatched envelope. Lenient mode never emits warnings; this is
43
+ // a no-op array for the default path.
44
+ const validationWarnings = validation.warnings ?? [];
39
45
  if (ann?.requiresConfirm) {
40
46
  const id = randomUUID();
41
47
  const { type: _type, ...payload } = args.msg;
@@ -61,14 +67,14 @@ export async function handleSendMessage(host, args) {
61
67
  const prevState = host.getState();
62
68
  const includeState = args.includeState === true;
63
69
  if (waitFor === 'none') {
64
- host.send(args.msg);
65
- return dispatched(host, emptyDrain(), prevState, includeState);
70
+ safeSend(host, args.msg, []);
71
+ return dispatched(host, emptyDrain(), prevState, includeState, validationWarnings);
66
72
  }
67
73
  if (waitFor === 'idle') {
68
- host.send(args.msg);
69
- host.flush();
74
+ const dispatchErrors = [];
75
+ safeSendAndFlush(host, args.msg, dispatchErrors);
70
76
  await Promise.resolve();
71
- return dispatched(host, { effectsObserved: 1, durationMs: 0, timedOut: false, errors: [] }, prevState, includeState);
77
+ return dispatched(host, { effectsObserved: 1, durationMs: 0, timedOut: false, errors: dispatchErrors }, prevState, includeState, validationWarnings);
72
78
  }
73
79
  // waitFor === 'drained' — message-queue quiescence detection.
74
80
  // Clear any errors buffered before this call so `drain.errors`
@@ -83,9 +89,13 @@ export async function handleSendMessage(host, args) {
83
89
  wake = null;
84
90
  w?.('msg');
85
91
  });
92
+ // Synchronous throws during send/flush — captured here and folded
93
+ // into drain.errors. Async post-flush errors come in via
94
+ // `getAndClearDrainErrors` (effect handler crashes, async rejections
95
+ // observed by the runtime) and are merged at response time.
96
+ const dispatchErrors = [];
86
97
  try {
87
- host.send(args.msg);
88
- host.flush();
98
+ safeSendAndFlush(host, args.msg, dispatchErrors);
89
99
  while (true) {
90
100
  const elapsed = now() - t0;
91
101
  if (elapsed >= capMs) {
@@ -93,8 +103,8 @@ export async function handleSendMessage(host, args) {
93
103
  effectsObserved: observed,
94
104
  durationMs: elapsed,
95
105
  timedOut: true,
96
- errors: host.getAndClearDrainErrors?.() ?? [],
97
- }, prevState, includeState);
106
+ errors: mergeDrainErrors(dispatchErrors, host.getAndClearDrainErrors?.()),
107
+ }, prevState, includeState, validationWarnings);
98
108
  }
99
109
  const budget = Math.min(quietMs, capMs - elapsed);
100
110
  // When the cap is within `quietMs` of `elapsed`, the quiet
@@ -111,25 +121,80 @@ export async function handleSendMessage(host, args) {
111
121
  effectsObserved: observed,
112
122
  durationMs: now() - t0,
113
123
  timedOut: !fullQuiet,
114
- errors: host.getAndClearDrainErrors?.() ?? [],
115
- }, prevState, includeState);
124
+ errors: mergeDrainErrors(dispatchErrors, host.getAndClearDrainErrors?.()),
125
+ }, prevState, includeState, validationWarnings);
116
126
  }
117
127
  // A commit fired during the wait — flush any queued follow-ups so
118
128
  // effects dispatched by that cycle run before we re-check.
119
- host.flush();
129
+ try {
130
+ host.flush();
131
+ }
132
+ catch (e) {
133
+ dispatchErrors.push(toDrainError(e));
134
+ }
120
135
  }
121
136
  }
122
137
  finally {
123
138
  unsub();
124
139
  }
125
140
  }
126
- function dispatched(host, drain, prevState, includeState) {
141
+ /**
142
+ * Send a Msg and capture any synchronous throw into `errors` rather
143
+ * than letting it propagate to the WS RPC layer. By the time `send`
144
+ * has thrown, the reducer may have partially run (state can advance),
145
+ * but bindings or downstream effects on the same commit may have
146
+ * crashed mid-flight. From the agent's POV: the dispatch IS dispatched,
147
+ * the state diff reflects what actually changed, and `drain.errors`
148
+ * reports the in-flight crash. That's strictly more useful than HTTP
149
+ * 500, which the agent reads as "the dispatch never happened."
150
+ */
151
+ function safeSend(host, msg, errors) {
152
+ try {
153
+ host.send(msg);
154
+ }
155
+ catch (e) {
156
+ errors.push(toDrainError(e));
157
+ }
158
+ }
159
+ function safeSendAndFlush(host, msg, errors) {
160
+ try {
161
+ host.send(msg);
162
+ }
163
+ catch (e) {
164
+ errors.push(toDrainError(e));
165
+ return; // can't flush something we never sent
166
+ }
167
+ try {
168
+ host.flush();
169
+ }
170
+ catch (e) {
171
+ errors.push(toDrainError(e));
172
+ }
173
+ }
174
+ function toDrainError(e) {
175
+ if (e instanceof Error) {
176
+ const stack = e.stack ? e.stack.split('\n').slice(0, 8).join('\n') : undefined;
177
+ return stack !== undefined
178
+ ? { kind: 'error', message: `${e.name}: ${e.message}`, stack }
179
+ : { kind: 'error', message: `${e.name}: ${e.message}` };
180
+ }
181
+ return { kind: 'error', message: String(e) };
182
+ }
183
+ function mergeDrainErrors(fromDispatch, fromHost) {
184
+ if (!fromHost || fromHost.length === 0)
185
+ return fromDispatch;
186
+ if (fromDispatch.length === 0)
187
+ return fromHost;
188
+ return [...fromDispatch, ...fromHost];
189
+ }
190
+ function dispatched(host, drain, prevState, includeState, validationWarnings = []) {
127
191
  const stateAfter = host.getState();
192
+ const drainWithWarnings = validationWarnings.length === 0 ? drain : { ...drain, warnings: validationWarnings };
128
193
  const base = {
129
194
  status: 'dispatched',
130
195
  stateDiff: computeStateDiff(prevState, stateAfter),
131
196
  actions: handleListActions(host).actions,
132
- drain,
197
+ drain: drainWithWarnings,
133
198
  };
134
199
  return includeState ? { ...base, stateAfter } : base;
135
200
  }
@@ -1 +1 @@
1
- {"version":3,"file":"send-message.js","sourceRoot":"","sources":["../../../src/client/rpc/send-message.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from '../uuid.js'\nimport { handleListActions, type ListActionsHost } from './list-actions.js'\nimport { computeStateDiff } from '../../state-diff.js'\nimport { validatePayload } from './validate-payload.js'\nimport type {\n LapActionsResponse,\n LapDrainMeta,\n LapMessageResponse,\n MessageAnnotations,\n} from '../../protocol.js'\n\nexport type SendMessageArgs = {\n msg: { type: string; [k: string]: unknown }\n reason?: string\n /** See LapMessageRequest['waitFor']. Default: 'drained'. */\n waitFor?: 'drained' | 'idle' | 'none'\n /** See LapMessageRequest['drainQuietMs']. Default: 100ms. */\n drainQuietMs?: number\n /** See LapMessageRequest['timeoutMs']. Default: 5000ms. */\n timeoutMs?: number\n /** See LapMessageRequest['includeState']. Default: false. */\n includeState?: boolean\n}\n\nexport type SendMessageHost = ListActionsHost & {\n getState(): unknown\n send(msg: unknown): void\n flush(): void\n /**\n * Register a listener called after every update cycle commits —\n * backed by `AppHandle.subscribe`. Returns an unsubscribe function.\n * The drain loop uses this to detect message-queue quiescence: each\n * listener fire resets the quiet-window timer; no fires for\n * `drainQuietMs` means the loop has gone idle and async effects (if\n * any) have either completed or are persistent\n * (websocket/interval/storageWatch).\n */\n subscribe(listener: () => void): () => void\n getMsgAnnotations(): Record<string, MessageAnnotations> | null\n /**\n * Snapshot and clear the drain-error buffer. The agent factory\n * installs persistent `window.error` / `unhandledrejection`\n * listeners that accumulate into this buffer; calling this at the\n * start of a drain discards stale errors from prior windows, and\n * calling it at the end yields just the errors that fired during\n * this drain. Optional — when omitted (e.g., Node test harness\n * without `window`), the drain envelope records an empty array.\n */\n getAndClearDrainErrors?: () => LapDrainMeta['errors']\n /** Called when @requiresConfirm; caller stores a ConfirmEntry in state. */\n proposeConfirm(entry: {\n id: string\n variant: string\n payload: unknown\n intent: string\n reason: string | null\n proposedAt: number\n status: 'pending'\n }): void\n}\n\nconst DEFAULT_QUIET_MS = 100\nconst DEFAULT_TIMEOUT_MS = 5_000\n\nexport async function handleSendMessage(\n host: SendMessageHost,\n args: SendMessageArgs,\n): Promise<LapMessageResponse> {\n if (!args.msg || typeof args.msg.type !== 'string') {\n return { status: 'rejected', reason: 'invalid' }\n }\n const annotations = host.getMsgAnnotations() ?? {}\n const ann = annotations[args.msg.type]\n\n // If annotations map is non-empty and this variant isn't in it, it's an\n // unknown msg type that the app never declared — reject early so the\n // browser never dispatches an unrecognised variant into update().\n const hasAnnotations = Object.keys(annotations).length > 0\n if (hasAnnotations && !ann) {\n return { status: 'rejected', reason: 'invalid', detail: `unknown variant: ${args.msg.type}` }\n }\n\n if (ann?.dispatchMode === 'human-only') {\n return { status: 'rejected', reason: 'human-only' }\n }\n\n // Schema validation: when the compiler emitted a `__msgSchema`,\n // check the payload against this variant's field shape before\n // dispatch. Catches the everyday agent bug — missing required\n // field, wrong enum value, missing discriminant on a tagged union,\n // typo in a key name — early, with structured errors the LLM can\n // correct from in one round trip. Reducers stay the last line of\n // defense; this is the first.\n const schema = host.getMsgSchema?.() ?? null\n const validation = validatePayload(args.msg, schema)\n if (!validation.ok) {\n return {\n status: 'rejected',\n reason: 'invalid',\n detail: validation.errors.map((e) => `${e.path}: ${e.message}`).join('; '),\n }\n }\n\n if (ann?.requiresConfirm) {\n const id = randomUUID()\n const { type: _type, ...payload } = args.msg\n host.proposeConfirm({\n id,\n variant: args.msg.type,\n payload,\n intent: ann?.intent ?? args.msg.type,\n reason: args.reason ?? null,\n proposedAt: Date.now(),\n status: 'pending',\n })\n return { status: 'pending-confirmation', confirmId: id }\n }\n\n const waitFor = args.waitFor ?? 'drained'\n const quietMs = Math.max(0, args.drainQuietMs ?? DEFAULT_QUIET_MS)\n const capMs = Math.max(0, args.timeoutMs ?? DEFAULT_TIMEOUT_MS)\n\n // Snapshot pre-dispatch state for diffing. The host's `getState`\n // returns a reference; capturing it here keeps a pre-mutation\n // pointer even after `host.send` triggers reducer-driven state\n // replacement (state itself is immutable per LLui's TEA contract,\n // so the reference stays valid).\n const prevState = host.getState()\n\n const includeState = args.includeState === true\n\n if (waitFor === 'none') {\n host.send(args.msg)\n return dispatched(host, emptyDrain(), prevState, includeState)\n }\n\n if (waitFor === 'idle') {\n host.send(args.msg)\n host.flush()\n await Promise.resolve()\n return dispatched(\n host,\n { effectsObserved: 1, durationMs: 0, timedOut: false, errors: [] },\n prevState,\n includeState,\n )\n }\n\n // waitFor === 'drained' — message-queue quiescence detection.\n // Clear any errors buffered before this call so `drain.errors`\n // attributes only to this window.\n host.getAndClearDrainErrors?.()\n\n const t0 = now()\n let observed = 0\n let wake: ((reason: 'msg' | 'timeout') => void) | null = null\n const unsub = host.subscribe(() => {\n observed++\n const w = wake\n wake = null\n w?.('msg')\n })\n try {\n host.send(args.msg)\n host.flush()\n\n while (true) {\n const elapsed = now() - t0\n if (elapsed >= capMs) {\n return dispatched(\n host,\n {\n effectsObserved: observed,\n durationMs: elapsed,\n timedOut: true,\n errors: host.getAndClearDrainErrors?.() ?? [],\n },\n prevState,\n includeState,\n )\n }\n const budget = Math.min(quietMs, capMs - elapsed)\n // When the cap is within `quietMs` of `elapsed`, the quiet\n // window is truncated. In that case a timeout resolution does\n // NOT mean we detected quiescence — it means the cap cut the\n // window short. Only a full-length quiet window that elapses\n // without a new commit counts as real idle.\n const fullQuiet = budget >= quietMs\n const reason = await awaitQuietOrMsg(budget, (resolve) => {\n wake = resolve\n })\n if (reason === 'timeout') {\n return dispatched(\n host,\n {\n effectsObserved: observed,\n durationMs: now() - t0,\n timedOut: !fullQuiet,\n errors: host.getAndClearDrainErrors?.() ?? [],\n },\n prevState,\n includeState,\n )\n }\n // A commit fired during the wait — flush any queued follow-ups so\n // effects dispatched by that cycle run before we re-check.\n host.flush()\n }\n } finally {\n unsub()\n }\n}\n\nfunction dispatched(\n host: SendMessageHost,\n drain: LapDrainMeta,\n prevState: unknown,\n includeState: boolean,\n): LapMessageResponse {\n const stateAfter = host.getState()\n const base = {\n status: 'dispatched' as const,\n stateDiff: computeStateDiff(prevState, stateAfter),\n actions: handleListActions(host).actions,\n drain,\n }\n return includeState ? { ...base, stateAfter } : base\n}\n\nfunction emptyDrain(): LapDrainMeta {\n return { effectsObserved: 0, durationMs: 0, timedOut: false, errors: [] }\n}\n\nfunction awaitQuietOrMsg(\n budgetMs: number,\n registerWake: (resolve: (r: 'msg' | 'timeout') => void) => void,\n): Promise<'msg' | 'timeout'> {\n return new Promise<'msg' | 'timeout'>((resolve) => {\n let settled = false\n const guarded = (r: 'msg' | 'timeout') => {\n if (settled) return\n settled = true\n resolve(r)\n }\n registerWake(guarded)\n setTimeout(() => guarded('timeout'), budgetMs)\n })\n}\n\nfunction now(): number {\n return typeof performance !== 'undefined' && typeof performance.now === 'function'\n ? performance.now()\n : Date.now()\n}\n\n// Helper types for external callers that want the dispatched envelope.\nexport type DispatchedEnvelope = Extract<LapMessageResponse, { status: 'dispatched' }>\nexport type { LapActionsResponse }\n"]}
1
+ {"version":3,"file":"send-message.js","sourceRoot":"","sources":["../../../src/client/rpc/send-message.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from '../uuid.js'\nimport { handleListActions, type ListActionsHost } from './list-actions.js'\nimport { computeStateDiff } from '../../state-diff.js'\nimport { validatePayload } from './validate-payload.js'\nimport type {\n LapActionsResponse,\n LapDrainMeta,\n LapMessageResponse,\n MessageAnnotations,\n} from '../../protocol.js'\n\nexport type SendMessageArgs = {\n msg: { type: string; [k: string]: unknown }\n reason?: string\n /** See LapMessageRequest['waitFor']. Default: 'drained'. */\n waitFor?: 'drained' | 'idle' | 'none'\n /** See LapMessageRequest['drainQuietMs']. Default: 100ms. */\n drainQuietMs?: number\n /** See LapMessageRequest['timeoutMs']. Default: 5000ms. */\n timeoutMs?: number\n /** See LapMessageRequest['includeState']. Default: false. */\n includeState?: boolean\n}\n\nexport type SendMessageHost = ListActionsHost & {\n getState(): unknown\n send(msg: unknown): void\n flush(): void\n /**\n * Register a listener called after every update cycle commits —\n * backed by `AppHandle.subscribe`. Returns an unsubscribe function.\n * The drain loop uses this to detect message-queue quiescence: each\n * listener fire resets the quiet-window timer; no fires for\n * `drainQuietMs` means the loop has gone idle and async effects (if\n * any) have either completed or are persistent\n * (websocket/interval/storageWatch).\n */\n subscribe(listener: () => void): () => void\n getMsgAnnotations(): Record<string, MessageAnnotations> | null\n /**\n * Snapshot and clear the drain-error buffer. The agent factory\n * installs persistent `window.error` / `unhandledrejection`\n * listeners that accumulate into this buffer; calling this at the\n * start of a drain discards stale errors from prior windows, and\n * calling it at the end yields just the errors that fired during\n * this drain. Optional — when omitted (e.g., Node test harness\n * without `window`), the drain envelope records an empty array.\n */\n getAndClearDrainErrors?: () => LapDrainMeta['errors']\n /**\n * Optional dispatch-policy accessor — when defined, returns the\n * server's configured `'strict' | 'lenient'` policy for payload\n * validation. Strict mode rejects fields not in the schema and\n * emits warnings for `'unknown'`-typed fields the agent provided\n * values for. Default is lenient (omit / undefined).\n */\n getDispatchPolicy?: () => 'strict' | 'lenient'\n /** Called when @requiresConfirm; caller stores a ConfirmEntry in state. */\n proposeConfirm(entry: {\n id: string\n variant: string\n payload: unknown\n intent: string\n reason: string | null\n proposedAt: number\n status: 'pending'\n }): void\n}\n\nconst DEFAULT_QUIET_MS = 100\nconst DEFAULT_TIMEOUT_MS = 5_000\n\nexport async function handleSendMessage(\n host: SendMessageHost,\n args: SendMessageArgs,\n): Promise<LapMessageResponse> {\n if (!args.msg || typeof args.msg.type !== 'string') {\n return { status: 'rejected', reason: 'invalid' }\n }\n const annotations = host.getMsgAnnotations() ?? {}\n const ann = annotations[args.msg.type]\n\n // If annotations map is non-empty and this variant isn't in it, it's an\n // unknown msg type that the app never declared — reject early so the\n // browser never dispatches an unrecognised variant into update().\n const hasAnnotations = Object.keys(annotations).length > 0\n if (hasAnnotations && !ann) {\n return { status: 'rejected', reason: 'invalid', detail: `unknown variant: ${args.msg.type}` }\n }\n\n if (ann?.dispatchMode === 'human-only') {\n return { status: 'rejected', reason: 'human-only' }\n }\n\n // Schema validation: when the compiler emitted a `__msgSchema`,\n // check the payload against this variant's field shape before\n // dispatch. Catches the everyday agent bug — missing required\n // field, wrong enum value, missing discriminant on a tagged union,\n // typo in a key name — early, with structured errors the LLM can\n // correct from in one round trip. Reducers stay the last line of\n // defense; this is the first.\n const schema = host.getMsgSchema?.() ?? null\n const policy = host.getDispatchPolicy?.() ?? 'lenient'\n const validation = validatePayload(args.msg, schema, { policy })\n if (!validation.ok) {\n return {\n status: 'rejected',\n reason: 'invalid',\n detail: validation.errors.map((e) => `${e.path}: ${e.message}`).join('; '),\n }\n }\n // Warnings from the validator (strict-mode `untyped-field` flags etc.)\n // ride along to `drain.warnings` so the agent sees them on the\n // dispatched envelope. Lenient mode never emits warnings; this is\n // a no-op array for the default path.\n const validationWarnings = validation.warnings ?? []\n\n if (ann?.requiresConfirm) {\n const id = randomUUID()\n const { type: _type, ...payload } = args.msg\n host.proposeConfirm({\n id,\n variant: args.msg.type,\n payload,\n intent: ann?.intent ?? args.msg.type,\n reason: args.reason ?? null,\n proposedAt: Date.now(),\n status: 'pending',\n })\n return { status: 'pending-confirmation', confirmId: id }\n }\n\n const waitFor = args.waitFor ?? 'drained'\n const quietMs = Math.max(0, args.drainQuietMs ?? DEFAULT_QUIET_MS)\n const capMs = Math.max(0, args.timeoutMs ?? DEFAULT_TIMEOUT_MS)\n\n // Snapshot pre-dispatch state for diffing. The host's `getState`\n // returns a reference; capturing it here keeps a pre-mutation\n // pointer even after `host.send` triggers reducer-driven state\n // replacement (state itself is immutable per LLui's TEA contract,\n // so the reference stays valid).\n const prevState = host.getState()\n\n const includeState = args.includeState === true\n\n if (waitFor === 'none') {\n safeSend(host, args.msg, [])\n return dispatched(host, emptyDrain(), prevState, includeState, validationWarnings)\n }\n\n if (waitFor === 'idle') {\n const dispatchErrors: LapDrainMeta['errors'] = []\n safeSendAndFlush(host, args.msg, dispatchErrors)\n await Promise.resolve()\n return dispatched(\n host,\n { effectsObserved: 1, durationMs: 0, timedOut: false, errors: dispatchErrors },\n prevState,\n includeState,\n validationWarnings,\n )\n }\n\n // waitFor === 'drained' — message-queue quiescence detection.\n // Clear any errors buffered before this call so `drain.errors`\n // attributes only to this window.\n host.getAndClearDrainErrors?.()\n\n const t0 = now()\n let observed = 0\n let wake: ((reason: 'msg' | 'timeout') => void) | null = null\n const unsub = host.subscribe(() => {\n observed++\n const w = wake\n wake = null\n w?.('msg')\n })\n // Synchronous throws during send/flush — captured here and folded\n // into drain.errors. Async post-flush errors come in via\n // `getAndClearDrainErrors` (effect handler crashes, async rejections\n // observed by the runtime) and are merged at response time.\n const dispatchErrors: LapDrainMeta['errors'] = []\n try {\n safeSendAndFlush(host, args.msg, dispatchErrors)\n\n while (true) {\n const elapsed = now() - t0\n if (elapsed >= capMs) {\n return dispatched(\n host,\n {\n effectsObserved: observed,\n durationMs: elapsed,\n timedOut: true,\n errors: mergeDrainErrors(dispatchErrors, host.getAndClearDrainErrors?.()),\n },\n prevState,\n includeState,\n validationWarnings,\n )\n }\n const budget = Math.min(quietMs, capMs - elapsed)\n // When the cap is within `quietMs` of `elapsed`, the quiet\n // window is truncated. In that case a timeout resolution does\n // NOT mean we detected quiescence — it means the cap cut the\n // window short. Only a full-length quiet window that elapses\n // without a new commit counts as real idle.\n const fullQuiet = budget >= quietMs\n const reason = await awaitQuietOrMsg(budget, (resolve) => {\n wake = resolve\n })\n if (reason === 'timeout') {\n return dispatched(\n host,\n {\n effectsObserved: observed,\n durationMs: now() - t0,\n timedOut: !fullQuiet,\n errors: mergeDrainErrors(dispatchErrors, host.getAndClearDrainErrors?.()),\n },\n prevState,\n includeState,\n validationWarnings,\n )\n }\n // A commit fired during the wait — flush any queued follow-ups so\n // effects dispatched by that cycle run before we re-check.\n try {\n host.flush()\n } catch (e) {\n dispatchErrors.push(toDrainError(e))\n }\n }\n } finally {\n unsub()\n }\n}\n\n/**\n * Send a Msg and capture any synchronous throw into `errors` rather\n * than letting it propagate to the WS RPC layer. By the time `send`\n * has thrown, the reducer may have partially run (state can advance),\n * but bindings or downstream effects on the same commit may have\n * crashed mid-flight. From the agent's POV: the dispatch IS dispatched,\n * the state diff reflects what actually changed, and `drain.errors`\n * reports the in-flight crash. That's strictly more useful than HTTP\n * 500, which the agent reads as \"the dispatch never happened.\"\n */\nfunction safeSend(\n host: SendMessageHost,\n msg: { type: string; [k: string]: unknown },\n errors: LapDrainMeta['errors'],\n): void {\n try {\n host.send(msg)\n } catch (e) {\n errors.push(toDrainError(e))\n }\n}\n\nfunction safeSendAndFlush(\n host: SendMessageHost,\n msg: { type: string; [k: string]: unknown },\n errors: LapDrainMeta['errors'],\n): void {\n try {\n host.send(msg)\n } catch (e) {\n errors.push(toDrainError(e))\n return // can't flush something we never sent\n }\n try {\n host.flush()\n } catch (e) {\n errors.push(toDrainError(e))\n }\n}\n\nfunction toDrainError(e: unknown): LapDrainMeta['errors'][number] {\n if (e instanceof Error) {\n const stack = e.stack ? e.stack.split('\\n').slice(0, 8).join('\\n') : undefined\n return stack !== undefined\n ? { kind: 'error', message: `${e.name}: ${e.message}`, stack }\n : { kind: 'error', message: `${e.name}: ${e.message}` }\n }\n return { kind: 'error', message: String(e) }\n}\n\nfunction mergeDrainErrors(\n fromDispatch: LapDrainMeta['errors'],\n fromHost: LapDrainMeta['errors'] | undefined,\n): LapDrainMeta['errors'] {\n if (!fromHost || fromHost.length === 0) return fromDispatch\n if (fromDispatch.length === 0) return fromHost\n return [...fromDispatch, ...fromHost]\n}\n\nfunction dispatched(\n host: SendMessageHost,\n drain: LapDrainMeta,\n prevState: unknown,\n includeState: boolean,\n validationWarnings: NonNullable<LapDrainMeta['warnings']> = [],\n): LapMessageResponse {\n const stateAfter = host.getState()\n const drainWithWarnings: LapDrainMeta =\n validationWarnings.length === 0 ? drain : { ...drain, warnings: validationWarnings }\n const base = {\n status: 'dispatched' as const,\n stateDiff: computeStateDiff(prevState, stateAfter),\n actions: handleListActions(host).actions,\n drain: drainWithWarnings,\n }\n return includeState ? { ...base, stateAfter } : base\n}\n\nfunction emptyDrain(): LapDrainMeta {\n return { effectsObserved: 0, durationMs: 0, timedOut: false, errors: [] }\n}\n\nfunction awaitQuietOrMsg(\n budgetMs: number,\n registerWake: (resolve: (r: 'msg' | 'timeout') => void) => void,\n): Promise<'msg' | 'timeout'> {\n return new Promise<'msg' | 'timeout'>((resolve) => {\n let settled = false\n const guarded = (r: 'msg' | 'timeout') => {\n if (settled) return\n settled = true\n resolve(r)\n }\n registerWake(guarded)\n setTimeout(() => guarded('timeout'), budgetMs)\n })\n}\n\nfunction now(): number {\n return typeof performance !== 'undefined' && typeof performance.now === 'function'\n ? performance.now()\n : Date.now()\n}\n\n// Helper types for external callers that want the dispatched envelope.\nexport type DispatchedEnvelope = Extract<LapMessageResponse, { status: 'dispatched' }>\nexport type { LapActionsResponse }\n"]}
@@ -30,14 +30,38 @@ export type ValidationError = {
30
30
  * branches.
31
31
  */
32
32
  path: string;
33
- code: 'unknown-variant' | 'missing' | 'wrong-type' | 'not-in-enum' | 'not-array' | 'not-object' | 'missing-discriminant' | 'unknown-discriminant-value';
33
+ code: 'unknown-variant' | 'missing' | 'wrong-type' | 'not-in-enum' | 'not-array' | 'not-object' | 'missing-discriminant' | 'unknown-discriminant-value' | 'unexpected-field' | 'validates-failed';
34
+ message: string;
35
+ };
36
+ export type ValidationWarning = {
37
+ path: string;
38
+ code: 'untyped-field';
34
39
  message: string;
35
40
  };
36
41
  export type ValidationResult = {
37
42
  ok: true;
43
+ warnings?: ValidationWarning[];
38
44
  } | {
39
45
  ok: false;
40
46
  errors: ValidationError[];
47
+ warnings?: ValidationWarning[];
48
+ };
49
+ export type ValidationOptions = {
50
+ /**
51
+ * `'strict'` rejects fields that aren't declared in the schema (typos,
52
+ * extra keys, fields the LLM hallucinated). Also emits warnings when
53
+ * the agent provides a value for a field whose schema is `'unknown'`
54
+ * — the validator can't structurally check the value, so the warning
55
+ * surfaces the gap to the LLM ("we accepted this but didn't validate
56
+ * it"). `'lenient'` (default) accepts extras silently and treats
57
+ * `'unknown'` as a passthrough.
58
+ *
59
+ * Strict mode pairs with the cross-file schema fidelity in
60
+ * `@llui/vite-plugin`@0.0.36+: with most fields fully resolved, strict
61
+ * is rarely surprising. Apps that haven't migrated yet may find
62
+ * strict overzealous and should stay on lenient.
63
+ */
64
+ policy?: 'strict' | 'lenient';
41
65
  };
42
- export declare function validatePayload(msg: unknown, schema: MsgSchemaShape | null): ValidationResult;
66
+ export declare function validatePayload(msg: unknown, schema: MsgSchemaShape | null, opts?: ValidationOptions): ValidationResult;
43
67
  //# sourceMappingURL=validate-payload.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate-payload.d.ts","sourceRoot":"","sources":["../../../src/client/rpc/validate-payload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAqC,MAAM,eAAe,CAAA;AAEtF;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;;;;;;OASG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EACA,iBAAiB,GACjB,SAAS,GACT,YAAY,GACZ,aAAa,GACb,WAAW,GACX,YAAY,GACZ,sBAAsB,GACtB,4BAA4B,CAAA;IAChC,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,eAAe,EAAE,CAAA;CAAE,CAAA;AAEtF,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,GAAG,IAAI,GAAG,gBAAgB,CAoD7F"}
1
+ {"version":3,"file":"validate-payload.d.ts","sourceRoot":"","sources":["../../../src/client/rpc/validate-payload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAqC,MAAM,eAAe,CAAA;AAEtF;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;;;;;;OASG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EACA,iBAAiB,GACjB,SAAS,GACT,YAAY,GACZ,aAAa,GACb,WAAW,GACX,YAAY,GACZ,sBAAsB,GACtB,4BAA4B,GAC5B,kBAAkB,GAClB,kBAAkB,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,eAAe,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,iBAAiB,EAAE,CAAA;CAAE,GAC5C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,eAAe,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,iBAAiB,EAAE,CAAA;CAAE,CAAA;AAE5E,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;CAC9B,CAAA;AAED,wBAAgB,eAAe,CAC7B,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,cAAc,GAAG,IAAI,EAC7B,IAAI,GAAE,iBAAsB,GAC3B,gBAAgB,CAyDlB"}
@@ -1,4 +1,4 @@
1
- export function validatePayload(msg, schema) {
1
+ export function validatePayload(msg, schema, opts = {}) {
2
2
  if (msg === null || typeof msg !== 'object' || Array.isArray(msg)) {
3
3
  return {
4
4
  ok: false,
@@ -40,15 +40,20 @@ export function validatePayload(msg, schema) {
40
40
  };
41
41
  }
42
42
  const errors = [];
43
+ const warnings = [];
44
+ const policy = opts.policy ?? 'lenient';
43
45
  const payload = {};
44
46
  for (const [k, v] of Object.entries(m)) {
45
47
  if (k !== schema.discriminant)
46
48
  payload[k] = v;
47
49
  }
48
- validateObjectShape(payload, variantSchema, '', errors);
49
- return errors.length === 0 ? { ok: true } : { ok: false, errors };
50
+ validateObjectShape(payload, variantSchema, '', errors, warnings, policy);
51
+ if (errors.length === 0) {
52
+ return warnings.length === 0 ? { ok: true } : { ok: true, warnings };
53
+ }
54
+ return warnings.length === 0 ? { ok: false, errors } : { ok: false, errors, warnings };
50
55
  }
51
- function validateObjectShape(value, shape, pathPrefix, errors) {
56
+ function validateObjectShape(value, shape, pathPrefix, errors, warnings, policy) {
52
57
  for (const [name, descriptor] of Object.entries(shape)) {
53
58
  const fieldPath = pathPrefix === '' ? name : `${pathPrefix}.${name}`;
54
59
  const present = Object.prototype.hasOwnProperty.call(value, name);
@@ -64,10 +69,66 @@ function validateObjectShape(value, shape, pathPrefix, errors) {
64
69
  }
65
70
  continue;
66
71
  }
67
- validateField(fieldValue, fieldType(descriptor), fieldPath, errors);
72
+ const ft = fieldType(descriptor);
73
+ if (ft === 'unknown' && policy === 'strict') {
74
+ warnings.push({
75
+ path: fieldPath,
76
+ code: 'untyped-field',
77
+ message: `value accepted but field schema is 'unknown' — the validator could not structurally check it. If this field is reachable across file boundaries, consider whether @llui/vite-plugin can resolve it.`,
78
+ });
79
+ }
80
+ const errCountBefore = errors.length;
81
+ validateField(fieldValue, ft, fieldPath, errors, warnings, policy);
82
+ const structurallyValid = errors.length === errCountBefore;
83
+ // Domain-invariant predicate (`@validates("expr")`). Runs only
84
+ // when structural validation passed for this field — without the
85
+ // right shape, the predicate would either throw (and we'd have to
86
+ // double-report) or accidentally pass (e.g. `v.length` on a string
87
+ // when we expected a number array). Predicate failures are errors
88
+ // regardless of policy — the author opted into the constraint
89
+ // deliberately.
90
+ if (structurallyValid) {
91
+ const validates = fieldValidatesPredicate(descriptor);
92
+ if (validates !== null) {
93
+ const predicate = compilePredicate(validates);
94
+ let passed;
95
+ try {
96
+ passed = Boolean(predicate(fieldValue));
97
+ }
98
+ catch {
99
+ passed = false;
100
+ }
101
+ if (!passed) {
102
+ errors.push({
103
+ path: fieldPath,
104
+ code: 'validates-failed',
105
+ message: `value violates \`@validates("${validates}")\``,
106
+ });
107
+ }
108
+ }
109
+ }
110
+ }
111
+ // Strict mode: reject fields the schema doesn't declare. Catches
112
+ // typos (`{tile: 'X'}` instead of `{title: 'X'}`), hallucinated
113
+ // fields, and stale field names from before a refactor. Lenient
114
+ // mode accepts extras silently — same shape TypeScript's structural
115
+ // subtyping accepts at the call site.
116
+ if (policy === 'strict') {
117
+ for (const key of Object.keys(value)) {
118
+ if (key in shape)
119
+ continue;
120
+ const fieldPath = pathPrefix === '' ? key : `${pathPrefix}.${key}`;
121
+ errors.push({
122
+ path: fieldPath,
123
+ code: 'unexpected-field',
124
+ message: `field '${key}' is not in the schema. Legal fields: ${Object.keys(shape)
125
+ .map((k) => `'${k}'`)
126
+ .join(', ')}.`,
127
+ });
128
+ }
68
129
  }
69
130
  }
70
- function validateField(value, type, path, errors) {
131
+ function validateField(value, type, path, errors, warnings, policy) {
71
132
  if (type === 'unknown')
72
133
  return; // schema gap; accept anything
73
134
  if (typeof type === 'string') {
@@ -105,7 +166,7 @@ function validateField(value, type, path, errors) {
105
166
  });
106
167
  return;
107
168
  }
108
- validateObjectShape(value, type.shape, path, errors);
169
+ validateObjectShape(value, type.shape, path, errors, warnings, policy);
109
170
  return;
110
171
  }
111
172
  if (type.kind === 'array') {
@@ -118,7 +179,7 @@ function validateField(value, type, path, errors) {
118
179
  return;
119
180
  }
120
181
  for (let i = 0; i < value.length; i++) {
121
- validateField(value[i], type.element, `${path}[${i}]`, errors);
182
+ validateField(value[i], type.element, `${path}[${i}]`, errors, warnings, policy);
122
183
  }
123
184
  return;
124
185
  }
@@ -162,7 +223,7 @@ function validateField(value, type, path, errors) {
162
223
  branchPayload[k] = v;
163
224
  }
164
225
  const branchPath = `${path}(${type.discriminant}=${discValue})`;
165
- validateObjectShape(branchPayload, branchSchema, branchPath, errors);
226
+ validateObjectShape(branchPayload, branchSchema, branchPath, errors, warnings, policy);
166
227
  }
167
228
  }
168
229
  function isOptional(d) {
@@ -173,6 +234,38 @@ function fieldType(d) {
173
234
  return d.type;
174
235
  return d;
175
236
  }
237
+ function fieldValidatesPredicate(d) {
238
+ if (typeof d === 'object' && d !== null && 'type' in d && typeof d.validates === 'string') {
239
+ return d.validates;
240
+ }
241
+ return null;
242
+ }
243
+ const predicateCache = new Map();
244
+ /**
245
+ * Compile a `@validates(...)` predicate string into a runtime function.
246
+ * Caches across calls — the schema is static at runtime, so each
247
+ * predicate is compiled at most once.
248
+ *
249
+ * The predicate sees `v` as the field's value and inherits the host
250
+ * environment's globals (Math, JSON, RegExp, etc.). On any compile
251
+ * error, returns a no-op `() => true` so a malformed predicate doesn't
252
+ * break dispatch — the build-time linter (`agent-validates-syntax`,
253
+ * future) is the right place to catch syntactic issues.
254
+ */
255
+ function compilePredicate(src) {
256
+ let fn = predicateCache.get(src);
257
+ if (fn)
258
+ return fn;
259
+ try {
260
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
261
+ fn = new Function('v', `return (${src})`);
262
+ }
263
+ catch {
264
+ fn = () => true;
265
+ }
266
+ predicateCache.set(src, fn);
267
+ return fn;
268
+ }
176
269
  function describeType(v) {
177
270
  if (v === null)
178
271
  return 'null';
@@ -1 +1 @@
1
- {"version":3,"file":"validate-payload.js","sourceRoot":"","sources":["../../../src/client/rpc/validate-payload.ts"],"names":[],"mappings":"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","sourcesContent":["import type { MsgSchemaShape, MsgSchemaField, MsgSchemaBareType } from '../factory.js'\n\n/**\n * Schema-driven payload validation for agent-dispatched Msgs. Walks\n * the compiler-emitted schema against a candidate Msg and reports\n * structural errors with a path-keyed list — the kind of feedback an\n * LLM can act on in a single round trip (\"set kind to one of: 'exact',\n * 'range', 'compound'\") instead of probing one field at a time.\n *\n * **What this is not.** This is not a TS type-checker. The schema is\n * best-effort: cross-file types, generics, complex unions, and\n * conditional types still surface as `'unknown'` and the validator\n * accepts anything for those. The validator's job is to catch the\n * mistakes a schema-aware LLM makes — wrong enum values, missing\n * discriminants, primitive type mismatches — not to mirror the entire\n * TypeScript surface area.\n *\n * **Tolerance for `'unknown'`.** Treat `'unknown'` as \"any goes.\" Don't\n * report errors against fields whose schema we don't know — those are\n * the schema's gaps, not the agent's.\n */\n\nexport type ValidationError = {\n /**\n * Dot-bracket path rooted at the Msg payload (NOT including `type`).\n * - top-level field: `'cells'`\n * - nested object property: `'cells.value'`\n * - array element: `'cells[0]'` (concrete index from the input)\n * - discriminated-union branch: `'format(kind=range).max'` — the\n * parenthesised `<discriminant>=<value>` segment names which branch\n * the error applies to, distinguishing the same field name across\n * branches.\n */\n path: string\n code:\n | 'unknown-variant'\n | 'missing'\n | 'wrong-type'\n | 'not-in-enum'\n | 'not-array'\n | 'not-object'\n | 'missing-discriminant'\n | 'unknown-discriminant-value'\n message: string\n}\n\nexport type ValidationResult = { ok: true } | { ok: false; errors: ValidationError[] }\n\nexport function validatePayload(msg: unknown, schema: MsgSchemaShape | null): ValidationResult {\n if (msg === null || typeof msg !== 'object' || Array.isArray(msg)) {\n return {\n ok: false,\n errors: [{ path: '', code: 'not-object', message: 'msg must be a plain object' }],\n }\n }\n const m = msg as Record<string, unknown>\n const variantKey = m[schema?.discriminant ?? 'type']\n if (typeof variantKey !== 'string') {\n return {\n ok: false,\n errors: [\n {\n path: schema?.discriminant ?? 'type',\n code: 'missing',\n message: `msg.${schema?.discriminant ?? 'type'} must be a string variant tag`,\n },\n ],\n }\n }\n\n // No schema available — the compiler didn't emit one, or the LLM is\n // talking to a build that predates schema emission. Accept the msg\n // structurally; the reducer will validate semantically.\n if (!schema) return { ok: true }\n\n const variantSchema = schema.variants[variantKey]\n if (!variantSchema) {\n return {\n ok: false,\n errors: [\n {\n path: schema.discriminant,\n code: 'unknown-variant',\n message: `'${variantKey}' is not a known variant. Legal values: ${Object.keys(\n schema.variants,\n )\n .map((v) => `'${v}'`)\n .join(', ')}.`,\n },\n ],\n }\n }\n\n const errors: ValidationError[] = []\n const payload: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(m)) {\n if (k !== schema.discriminant) payload[k] = v\n }\n validateObjectShape(payload, variantSchema, '', errors)\n return errors.length === 0 ? { ok: true } : { ok: false, errors }\n}\n\nfunction validateObjectShape(\n value: Record<string, unknown>,\n shape: Record<string, MsgSchemaField>,\n pathPrefix: string,\n errors: ValidationError[],\n): void {\n for (const [name, descriptor] of Object.entries(shape)) {\n const fieldPath = pathPrefix === '' ? name : `${pathPrefix}.${name}`\n const present = Object.prototype.hasOwnProperty.call(value, name)\n const optional = isOptional(descriptor)\n const fieldValue = present ? value[name] : undefined\n\n if (!present || fieldValue === undefined) {\n if (!optional) {\n errors.push({\n path: fieldPath,\n code: 'missing',\n message: `required field is missing`,\n })\n }\n continue\n }\n\n validateField(fieldValue, fieldType(descriptor), fieldPath, errors)\n }\n}\n\nfunction validateField(\n value: unknown,\n type: MsgSchemaBareType,\n path: string,\n errors: ValidationError[],\n): void {\n if (type === 'unknown') return // schema gap; accept anything\n if (typeof type === 'string') {\n // Primitive keyword: 'string', 'number', 'boolean'.\n if (typeof value !== type) {\n errors.push({\n path,\n code: 'wrong-type',\n message: `expected ${type}, got ${describeType(value)}`,\n })\n }\n return\n }\n if ('enum' in type) {\n // Use Object.is so NaN and -0 match correctly; falls back to ===\n // semantics for ordinary values.\n const ok = type.enum.some((legal) => Object.is(legal, value) || legal === value)\n if (!ok) {\n errors.push({\n path,\n code: 'not-in-enum',\n message: `'${String(value)}' is not in the enum. Legal values: ${type.enum\n .map((v) => formatEnumValue(v))\n .join(', ')}.`,\n })\n }\n return\n }\n if (type.kind === 'object') {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n errors.push({\n path,\n code: 'not-object',\n message: `expected object, got ${describeType(value)}`,\n })\n return\n }\n validateObjectShape(value as Record<string, unknown>, type.shape, path, errors)\n return\n }\n if (type.kind === 'array') {\n if (!Array.isArray(value)) {\n errors.push({\n path,\n code: 'not-array',\n message: `expected array, got ${describeType(value)}`,\n })\n return\n }\n for (let i = 0; i < value.length; i++) {\n validateField(value[i], type.element, `${path}[${i}]`, errors)\n }\n return\n }\n if (type.kind === 'discriminated-union') {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n errors.push({\n path,\n code: 'not-object',\n message: `expected discriminated-union object, got ${describeType(value)}`,\n })\n return\n }\n const obj = value as Record<string, unknown>\n const discValue = obj[type.discriminant]\n if (typeof discValue !== 'string') {\n errors.push({\n path: `${path}.${type.discriminant}`,\n code: 'missing-discriminant',\n message: `discriminant '${type.discriminant}' must be one of: ${Object.keys(type.variants)\n .map((v) => `'${v}'`)\n .join(', ')}`,\n })\n return\n }\n const branchSchema = type.variants[discValue]\n if (!branchSchema) {\n errors.push({\n path: `${path}.${type.discriminant}`,\n code: 'unknown-discriminant-value',\n message: `'${discValue}' is not a legal '${type.discriminant}'. Legal values: ${Object.keys(\n type.variants,\n )\n .map((v) => `'${v}'`)\n .join(', ')}.`,\n })\n return\n }\n // Recurse into the matched branch's payload (excluding the\n // discriminant itself, which is already validated above).\n const branchPayload: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(obj)) {\n if (k !== type.discriminant) branchPayload[k] = v\n }\n const branchPath = `${path}(${type.discriminant}=${discValue})`\n validateObjectShape(branchPayload, branchSchema, branchPath, errors)\n }\n}\n\nfunction isOptional(d: MsgSchemaField): boolean {\n return typeof d === 'object' && d !== null && 'type' in d && d.optional === true\n}\n\nfunction fieldType(d: MsgSchemaField): MsgSchemaBareType {\n if (typeof d === 'object' && d !== null && 'type' in d) return d.type\n return d\n}\n\nfunction describeType(v: unknown): string {\n if (v === null) return 'null'\n if (Array.isArray(v)) return 'array'\n return typeof v\n}\n\nfunction formatEnumValue(v: string | number | boolean): string {\n return typeof v === 'string' ? `'${v}'` : String(v)\n}\n"]}
1
+ {"version":3,"file":"validate-payload.js","sourceRoot":"","sources":["../../../src/client/rpc/validate-payload.ts"],"names":[],"mappings":"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","sourcesContent":["import type { MsgSchemaShape, MsgSchemaField, MsgSchemaBareType } from '../factory.js'\n\n/**\n * Schema-driven payload validation for agent-dispatched Msgs. Walks\n * the compiler-emitted schema against a candidate Msg and reports\n * structural errors with a path-keyed list — the kind of feedback an\n * LLM can act on in a single round trip (\"set kind to one of: 'exact',\n * 'range', 'compound'\") instead of probing one field at a time.\n *\n * **What this is not.** This is not a TS type-checker. The schema is\n * best-effort: cross-file types, generics, complex unions, and\n * conditional types still surface as `'unknown'` and the validator\n * accepts anything for those. The validator's job is to catch the\n * mistakes a schema-aware LLM makes — wrong enum values, missing\n * discriminants, primitive type mismatches — not to mirror the entire\n * TypeScript surface area.\n *\n * **Tolerance for `'unknown'`.** Treat `'unknown'` as \"any goes.\" Don't\n * report errors against fields whose schema we don't know — those are\n * the schema's gaps, not the agent's.\n */\n\nexport type ValidationError = {\n /**\n * Dot-bracket path rooted at the Msg payload (NOT including `type`).\n * - top-level field: `'cells'`\n * - nested object property: `'cells.value'`\n * - array element: `'cells[0]'` (concrete index from the input)\n * - discriminated-union branch: `'format(kind=range).max'` — the\n * parenthesised `<discriminant>=<value>` segment names which branch\n * the error applies to, distinguishing the same field name across\n * branches.\n */\n path: string\n code:\n | 'unknown-variant'\n | 'missing'\n | 'wrong-type'\n | 'not-in-enum'\n | 'not-array'\n | 'not-object'\n | 'missing-discriminant'\n | 'unknown-discriminant-value'\n | 'unexpected-field'\n | 'validates-failed'\n message: string\n}\n\nexport type ValidationWarning = {\n path: string\n code: 'untyped-field'\n message: string\n}\n\nexport type ValidationResult =\n | { ok: true; warnings?: ValidationWarning[] }\n | { ok: false; errors: ValidationError[]; warnings?: ValidationWarning[] }\n\nexport type ValidationOptions = {\n /**\n * `'strict'` rejects fields that aren't declared in the schema (typos,\n * extra keys, fields the LLM hallucinated). Also emits warnings when\n * the agent provides a value for a field whose schema is `'unknown'`\n * — the validator can't structurally check the value, so the warning\n * surfaces the gap to the LLM (\"we accepted this but didn't validate\n * it\"). `'lenient'` (default) accepts extras silently and treats\n * `'unknown'` as a passthrough.\n *\n * Strict mode pairs with the cross-file schema fidelity in\n * `@llui/vite-plugin`@0.0.36+: with most fields fully resolved, strict\n * is rarely surprising. Apps that haven't migrated yet may find\n * strict overzealous and should stay on lenient.\n */\n policy?: 'strict' | 'lenient'\n}\n\nexport function validatePayload(\n msg: unknown,\n schema: MsgSchemaShape | null,\n opts: ValidationOptions = {},\n): ValidationResult {\n if (msg === null || typeof msg !== 'object' || Array.isArray(msg)) {\n return {\n ok: false,\n errors: [{ path: '', code: 'not-object', message: 'msg must be a plain object' }],\n }\n }\n const m = msg as Record<string, unknown>\n const variantKey = m[schema?.discriminant ?? 'type']\n if (typeof variantKey !== 'string') {\n return {\n ok: false,\n errors: [\n {\n path: schema?.discriminant ?? 'type',\n code: 'missing',\n message: `msg.${schema?.discriminant ?? 'type'} must be a string variant tag`,\n },\n ],\n }\n }\n\n // No schema available — the compiler didn't emit one, or the LLM is\n // talking to a build that predates schema emission. Accept the msg\n // structurally; the reducer will validate semantically.\n if (!schema) return { ok: true }\n\n const variantSchema = schema.variants[variantKey]\n if (!variantSchema) {\n return {\n ok: false,\n errors: [\n {\n path: schema.discriminant,\n code: 'unknown-variant',\n message: `'${variantKey}' is not a known variant. Legal values: ${Object.keys(\n schema.variants,\n )\n .map((v) => `'${v}'`)\n .join(', ')}.`,\n },\n ],\n }\n }\n\n const errors: ValidationError[] = []\n const warnings: ValidationWarning[] = []\n const policy = opts.policy ?? 'lenient'\n const payload: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(m)) {\n if (k !== schema.discriminant) payload[k] = v\n }\n validateObjectShape(payload, variantSchema, '', errors, warnings, policy)\n if (errors.length === 0) {\n return warnings.length === 0 ? { ok: true } : { ok: true, warnings }\n }\n return warnings.length === 0 ? { ok: false, errors } : { ok: false, errors, warnings }\n}\n\nfunction validateObjectShape(\n value: Record<string, unknown>,\n shape: Record<string, MsgSchemaField>,\n pathPrefix: string,\n errors: ValidationError[],\n warnings: ValidationWarning[],\n policy: 'strict' | 'lenient',\n): void {\n for (const [name, descriptor] of Object.entries(shape)) {\n const fieldPath = pathPrefix === '' ? name : `${pathPrefix}.${name}`\n const present = Object.prototype.hasOwnProperty.call(value, name)\n const optional = isOptional(descriptor)\n const fieldValue = present ? value[name] : undefined\n\n if (!present || fieldValue === undefined) {\n if (!optional) {\n errors.push({\n path: fieldPath,\n code: 'missing',\n message: `required field is missing`,\n })\n }\n continue\n }\n\n const ft = fieldType(descriptor)\n if (ft === 'unknown' && policy === 'strict') {\n warnings.push({\n path: fieldPath,\n code: 'untyped-field',\n message: `value accepted but field schema is 'unknown' — the validator could not structurally check it. If this field is reachable across file boundaries, consider whether @llui/vite-plugin can resolve it.`,\n })\n }\n\n const errCountBefore = errors.length\n validateField(fieldValue, ft, fieldPath, errors, warnings, policy)\n const structurallyValid = errors.length === errCountBefore\n\n // Domain-invariant predicate (`@validates(\"expr\")`). Runs only\n // when structural validation passed for this field — without the\n // right shape, the predicate would either throw (and we'd have to\n // double-report) or accidentally pass (e.g. `v.length` on a string\n // when we expected a number array). Predicate failures are errors\n // regardless of policy — the author opted into the constraint\n // deliberately.\n if (structurallyValid) {\n const validates = fieldValidatesPredicate(descriptor)\n if (validates !== null) {\n const predicate = compilePredicate(validates)\n let passed: boolean\n try {\n passed = Boolean(predicate(fieldValue))\n } catch {\n passed = false\n }\n if (!passed) {\n errors.push({\n path: fieldPath,\n code: 'validates-failed',\n message: `value violates \\`@validates(\"${validates}\")\\``,\n })\n }\n }\n }\n }\n\n // Strict mode: reject fields the schema doesn't declare. Catches\n // typos (`{tile: 'X'}` instead of `{title: 'X'}`), hallucinated\n // fields, and stale field names from before a refactor. Lenient\n // mode accepts extras silently — same shape TypeScript's structural\n // subtyping accepts at the call site.\n if (policy === 'strict') {\n for (const key of Object.keys(value)) {\n if (key in shape) continue\n const fieldPath = pathPrefix === '' ? key : `${pathPrefix}.${key}`\n errors.push({\n path: fieldPath,\n code: 'unexpected-field',\n message: `field '${key}' is not in the schema. Legal fields: ${Object.keys(shape)\n .map((k) => `'${k}'`)\n .join(', ')}.`,\n })\n }\n }\n}\n\nfunction validateField(\n value: unknown,\n type: MsgSchemaBareType,\n path: string,\n errors: ValidationError[],\n warnings: ValidationWarning[],\n policy: 'strict' | 'lenient',\n): void {\n if (type === 'unknown') return // schema gap; accept anything\n if (typeof type === 'string') {\n // Primitive keyword: 'string', 'number', 'boolean'.\n if (typeof value !== type) {\n errors.push({\n path,\n code: 'wrong-type',\n message: `expected ${type}, got ${describeType(value)}`,\n })\n }\n return\n }\n if ('enum' in type) {\n // Use Object.is so NaN and -0 match correctly; falls back to ===\n // semantics for ordinary values.\n const ok = type.enum.some((legal) => Object.is(legal, value) || legal === value)\n if (!ok) {\n errors.push({\n path,\n code: 'not-in-enum',\n message: `'${String(value)}' is not in the enum. Legal values: ${type.enum\n .map((v) => formatEnumValue(v))\n .join(', ')}.`,\n })\n }\n return\n }\n if (type.kind === 'object') {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n errors.push({\n path,\n code: 'not-object',\n message: `expected object, got ${describeType(value)}`,\n })\n return\n }\n validateObjectShape(\n value as Record<string, unknown>,\n type.shape,\n path,\n errors,\n warnings,\n policy,\n )\n return\n }\n if (type.kind === 'array') {\n if (!Array.isArray(value)) {\n errors.push({\n path,\n code: 'not-array',\n message: `expected array, got ${describeType(value)}`,\n })\n return\n }\n for (let i = 0; i < value.length; i++) {\n validateField(value[i], type.element, `${path}[${i}]`, errors, warnings, policy)\n }\n return\n }\n if (type.kind === 'discriminated-union') {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n errors.push({\n path,\n code: 'not-object',\n message: `expected discriminated-union object, got ${describeType(value)}`,\n })\n return\n }\n const obj = value as Record<string, unknown>\n const discValue = obj[type.discriminant]\n if (typeof discValue !== 'string') {\n errors.push({\n path: `${path}.${type.discriminant}`,\n code: 'missing-discriminant',\n message: `discriminant '${type.discriminant}' must be one of: ${Object.keys(type.variants)\n .map((v) => `'${v}'`)\n .join(', ')}`,\n })\n return\n }\n const branchSchema = type.variants[discValue]\n if (!branchSchema) {\n errors.push({\n path: `${path}.${type.discriminant}`,\n code: 'unknown-discriminant-value',\n message: `'${discValue}' is not a legal '${type.discriminant}'. Legal values: ${Object.keys(\n type.variants,\n )\n .map((v) => `'${v}'`)\n .join(', ')}.`,\n })\n return\n }\n // Recurse into the matched branch's payload (excluding the\n // discriminant itself, which is already validated above).\n const branchPayload: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(obj)) {\n if (k !== type.discriminant) branchPayload[k] = v\n }\n const branchPath = `${path}(${type.discriminant}=${discValue})`\n validateObjectShape(branchPayload, branchSchema, branchPath, errors, warnings, policy)\n }\n}\n\nfunction isOptional(d: MsgSchemaField): boolean {\n return typeof d === 'object' && d !== null && 'type' in d && d.optional === true\n}\n\nfunction fieldType(d: MsgSchemaField): MsgSchemaBareType {\n if (typeof d === 'object' && d !== null && 'type' in d) return d.type\n return d\n}\n\nfunction fieldValidatesPredicate(d: MsgSchemaField): string | null {\n if (typeof d === 'object' && d !== null && 'type' in d && typeof d.validates === 'string') {\n return d.validates\n }\n return null\n}\n\nconst predicateCache = new Map<string, (v: unknown) => boolean>()\n\n/**\n * Compile a `@validates(...)` predicate string into a runtime function.\n * Caches across calls — the schema is static at runtime, so each\n * predicate is compiled at most once.\n *\n * The predicate sees `v` as the field's value and inherits the host\n * environment's globals (Math, JSON, RegExp, etc.). On any compile\n * error, returns a no-op `() => true` so a malformed predicate doesn't\n * break dispatch — the build-time linter (`agent-validates-syntax`,\n * future) is the right place to catch syntactic issues.\n */\nfunction compilePredicate(src: string): (v: unknown) => boolean {\n let fn = predicateCache.get(src)\n if (fn) return fn\n try {\n // eslint-disable-next-line @typescript-eslint/no-implied-eval\n fn = new Function('v', `return (${src})`) as (v: unknown) => boolean\n } catch {\n fn = () => true\n }\n predicateCache.set(src, fn)\n return fn\n}\n\nfunction describeType(v: unknown): string {\n if (v === null) return 'null'\n if (Array.isArray(v)) return 'array'\n return typeof v\n}\n\nfunction formatEnumValue(v: string | number | boolean): string {\n return typeof v === 'string' ? `'${v}'` : String(v)\n}\n"]}
@@ -84,6 +84,25 @@ export type WouldDispatchResult = {
84
84
  status: 'rejected';
85
85
  reason: 'schema-mismatch';
86
86
  errors: ValidationError[];
87
+ } | {
88
+ /**
89
+ * The reducer threw while running against the candidate Msg.
90
+ * State is poisoned (or the reducer has a latent bug); a real
91
+ * `send_message` would land state change + an error in
92
+ * `drain.errors`. `would_dispatch` mirrors that contract: the
93
+ * "diff" is empty (we couldn't compute it), and the throw text
94
+ * is surfaced so the agent knows to back off rather than
95
+ * retrying the same payload.
96
+ *
97
+ * Distinct from `'rejected'` because the agent learned something
98
+ * different: the reducer DOES accept this Msg shape but errors
99
+ * downstream. Often that means earlier state needs fixing first
100
+ * (a previously-dispatched bad Msg poisoned a derived path),
101
+ * not that this candidate is malformed.
102
+ */
103
+ status: 'reducer-threw';
104
+ message: string;
105
+ stack?: string;
87
106
  };
88
107
  export declare function handleWouldDispatch(host: WouldDispatchHost, args: WouldDispatchArgs): WouldDispatchResult;
89
108
  //# sourceMappingURL=would-dispatch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"would-dispatch.d.ts","sourceRoot":"","sources":["../../../src/client/rpc/would-dispatch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAC7E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,IAAI,OAAO,CAAA;IACnB;;;;;;;OAOG;IACH,UAAU,CAAC,GAAG,EAAE;QACd,IAAI,EAAE,MAAM,CAAA;QACZ,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KACrB,GAAG;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,EAAE,CAAA;KAAE,GAAG,IAAI,CAAA;IACjD;;;;;;;;OAQG;IACH,YAAY,CAAC,IAAI,cAAc,GAAG,IAAI,CAAA;CACvC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAA;CAC5C,CAAA;AAED,MAAM,MAAM,mBAAmB,GAC3B;IACE,MAAM,EAAE,WAAW,CAAA;IACnB,mEAAmE;IACnE,SAAS,EAAE,SAAS,CAAA;IACpB,0EAA0E;IAC1E,OAAO,EAAE,OAAO,EAAE,CAAA;CACnB,GACD;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,SAAS,GAAG,aAAa,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1E;IACE;;;;;;OAMG;IACH,MAAM,EAAE,UAAU,CAAA;IAClB,MAAM,EAAE,iBAAiB,CAAA;IACzB,MAAM,EAAE,eAAe,EAAE,CAAA;CAC1B,CAAA;AAEL,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,iBAAiB,EACvB,IAAI,EAAE,iBAAiB,GACtB,mBAAmB,CA6BrB"}
1
+ {"version":3,"file":"would-dispatch.d.ts","sourceRoot":"","sources":["../../../src/client/rpc/would-dispatch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAC7E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,IAAI,OAAO,CAAA;IACnB;;;;;;;OAOG;IACH,UAAU,CAAC,GAAG,EAAE;QACd,IAAI,EAAE,MAAM,CAAA;QACZ,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KACrB,GAAG;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,EAAE,CAAA;KAAE,GAAG,IAAI,CAAA;IACjD;;;;;;;;OAQG;IACH,YAAY,CAAC,IAAI,cAAc,GAAG,IAAI,CAAA;CACvC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAA;CAC5C,CAAA;AAED,MAAM,MAAM,mBAAmB,GAC3B;IACE,MAAM,EAAE,WAAW,CAAA;IACnB,mEAAmE;IACnE,SAAS,EAAE,SAAS,CAAA;IACpB,0EAA0E;IAC1E,OAAO,EAAE,OAAO,EAAE,CAAA;CACnB,GACD;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,SAAS,GAAG,aAAa,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1E;IACE;;;;;;OAMG;IACH,MAAM,EAAE,UAAU,CAAA;IAClB,MAAM,EAAE,iBAAiB,CAAA;IACzB,MAAM,EAAE,eAAe,EAAE,CAAA;CAC1B,GACD;IACE;;;;;;;;;;;;;;OAcG;IACH,MAAM,EAAE,eAAe,CAAA;IACvB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAEL,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,iBAAiB,EACvB,IAAI,EAAE,iBAAiB,GACtB,mBAAmB,CAyCrB"}
@@ -13,7 +13,20 @@ export function handleWouldDispatch(host, args) {
13
13
  if (!validation.ok) {
14
14
  return { status: 'rejected', reason: 'schema-mismatch', errors: validation.errors };
15
15
  }
16
- const result = host.runReducer(args.msg);
16
+ let result;
17
+ try {
18
+ result = host.runReducer(args.msg);
19
+ }
20
+ catch (e) {
21
+ // The reducer threw — same Phase-5 contract as `send_message`:
22
+ // surface the throw as structured data instead of letting it
23
+ // become an HTTP 500 the agent reads as "transport failure."
24
+ const err = e instanceof Error ? e : new Error(String(e));
25
+ const stack = err.stack ? err.stack.split('\n').slice(0, 8).join('\n') : undefined;
26
+ return stack !== undefined
27
+ ? { status: 'reducer-threw', message: `${err.name}: ${err.message}`, stack }
28
+ : { status: 'reducer-threw', message: `${err.name}: ${err.message}` };
29
+ }
17
30
  if (result === null) {
18
31
  return {
19
32
  status: 'rejected',