@statelyai/agent 2.0.0-alpha.14 → 2.0.0-alpha.16

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.
Files changed (35) hide show
  1. package/dist/ai-sdk.cjs +1 -1
  2. package/dist/ai-sdk.d.cts +2 -2
  3. package/dist/ai-sdk.d.mts +2 -2
  4. package/dist/ai-sdk.mjs +1 -1
  5. package/dist/{decision-C0cUKvNt.cjs → decision-Ba8qrT8r.cjs} +42 -28
  6. package/dist/{decision-D9Zi7Xi5.mjs → decision-Bt2HYYRo.mjs} +31 -23
  7. package/dist/{event-log-store-D7pWtIhb.mjs → event-log-store-B-1fcfkT.mjs} +149 -141
  8. package/dist/{event-log-store-BkUNtyOF.d.mts → event-log-store-BrC9Q1xW.d.mts} +14 -12
  9. package/dist/{event-log-store-CVd2eyRy.d.cts → event-log-store-CQJq8_v4.d.cts} +14 -12
  10. package/dist/{event-log-store-CNT_7F0V.cjs → event-log-store-yquOV1TX.cjs} +148 -140
  11. package/dist/index.cjs +520 -521
  12. package/dist/index.d.cts +128 -106
  13. package/dist/index.d.mts +128 -106
  14. package/dist/index.mjs +519 -520
  15. package/dist/machines.cjs +1 -1
  16. package/dist/machines.d.cts +1 -1
  17. package/dist/machines.d.mts +1 -1
  18. package/dist/machines.mjs +1 -1
  19. package/dist/otel.d.cts +1 -1
  20. package/dist/otel.d.mts +1 -1
  21. package/dist/{run-agent-BlqKwHIF.d.cts → run-agent-BxjGaVpL.d.cts} +72 -111
  22. package/dist/{run-agent-2MnlQTkB.d.mts → run-agent-COHoCgQd.d.mts} +72 -111
  23. package/dist/{setup-agent-CpK0ZRWV.cjs → setup-agent-BLU77gqr.cjs} +141 -172
  24. package/dist/{setup-agent-DeHRW-qX.mjs → setup-agent-SbiiSbAU.mjs} +136 -167
  25. package/dist/sqlite.cjs +1 -1
  26. package/dist/sqlite.d.cts +2 -2
  27. package/dist/sqlite.d.mts +2 -2
  28. package/dist/sqlite.mjs +1 -1
  29. package/dist/{text-logic-Jkilp1Ie.d.cts → text-logic-BFX5q7fM.d.cts} +26 -3
  30. package/dist/{text-logic-C5kbjaDz.d.mts → text-logic-DQW8_DWW.d.mts} +26 -3
  31. package/dist/{types-rMe7x6NR.d.cts → types-DYpK3QF4.d.mts} +35 -7
  32. package/dist/{types-9Bqg5rZB.d.mts → types-pJ5Hn8fv.d.cts} +35 -7
  33. package/package.json +28 -28
  34. package/readme.md +6 -19
  35. package/schemas/agent-workflow.json +1 -4
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { t as AgentError } from "./errors-CeSXQx0v.mjs";
2
- import { _ as resolveAgentStep, a as AgentReplayDivergenceError, b as messagesSchema, c as diffEventLogs, d as initEntry, f as replay, g as initialAgentStep, h as getInvokeEffectMetadata, i as AGENT_USAGE_EVENT_TYPE, l as getAgentEffects, m as executeAgentRequest, n as setupAgent, o as AgentReplayMachineMismatchError, p as verifyReplay, r as AGENT_INIT_EVENT_TYPE, s as createReplayEntry, t as createAgentSchemas, u as getCallUsage, v as transitionAgentStep, y as appendMessages } from "./setup-agent-DeHRW-qX.mjs";
3
- import { A as isUnboundPlaceholder, B as getStateMeta, C as parseStructuredEnvelope, D as getMachineStaticTransitionTargets, E as executorBoundLogics, F as findNonSerializableContextPaths, G as userMessage, H as persistSnapshot, I as getAgentMessages, L as getJsonSchema, O as getMachineSuspensionPredicate, P as assistantMessage, R as getJsonSchemaSync, S as parseOutput, U as systemMessage, V as isStandardSchema, W as toolMessage, _ as extractCallUsage, a as resolveDecision, b as normalizeGeneratorResult, c as AGENT_USAGE_TOKEN_FIELDS, f as bindRequestExecutor, h as createTextLogic, i as renderDecisionAttempts, k as getRegisteredAgentExecutionOptions, o as getAcceptedEvents, p as buildEnvelopeSchema, r as isDecisionLogic, s as parseAgentEvent, t as AgentDecisionExhaustedError, u as INTERPRET_SOURCE, v as getAgentOutputMode, x as parseModelRef, y as isTextLogic, z as getMachineStructuralHash } from "./decision-D9Zi7Xi5.mjs";
4
- import { a as assertEventLogStoreConformance, i as assertAgentLogEntry, n as AgentEventLogConflictError, o as assertJsonSerializable, r as NonSerializableAgentEventError, s as createInMemoryEventLogStore, t as AGENT_EVENT_SCHEMA_VERSION } from "./event-log-store-D7pWtIhb.mjs";
2
+ import { _ as resolveAgentStep, a as AgentReplayDivergenceError, b as messagesSchema, c as diffEventLogs, d as replay, f as validateReplayEntries, g as initialAgentStep, h as getInvokeEffectMetadata, i as AGENT_USAGE_EVENT_TYPE, l as getAgentEffects, m as executeAgentRequest, n as setupAgent, o as AgentReplayMachineMismatchError, p as verifyReplay, r as AGENT_INIT_EVENT_TYPE, s as createReplayEntry, t as createAgentSchemas, u as initEntry, v as transitionAgentStep, y as appendMessages } from "./setup-agent-SbiiSbAU.mjs";
3
+ import { A as isUnboundPlaceholder, B as getMachineStructuralHash, C as parseStructuredEnvelope, D as getMachineStaticTransitionTargets, E as executorBoundLogics, G as toolMessage, H as isStandardSchema, I as findNonSerializableContextPaths, K as userMessage, L as getAgentMessages, O as getMachineSuspensionPredicate, P as assistantMessage, R as getJsonSchema, S as parseOutput, U as persistSnapshot, V as getStateMeta, W as systemMessage, _ as getAgentOutputMode, a as resolveDecision, b as normalizeGeneratorResult, c as AGENT_USAGE_TOKEN_FIELDS, f as bindRequestExecutor, h as createTextLogic, i as renderDecisionAttempts, k as getRegisteredAgentExecutionOptions, o as getAcceptedEvents, p as buildEnvelopeSchema, q as validateSchemaSync, r as isDecisionLogic, s as parseAgentEvent, t as AgentDecisionExhaustedError, u as INTERPRET_SOURCE, v as getCallUsage, x as parseModelRef, y as isTextLogic, z as getJsonSchemaSync } from "./decision-Bt2HYYRo.mjs";
4
+ import { a as assertJsonSerializable, i as assertAgentLogEntry, n as AgentEventLogConflictError, o as createInMemoryEventLogStore, r as NonSerializableAgentEventError, s as assertEventLogStoreConformance, t as AGENT_EVENT_SCHEMA_VERSION } from "./event-log-store-B-1fcfkT.mjs";
5
5
  import { createActor, createAsyncLogic, getNextTransitions, isMachineSnapshot } from "xstate";
6
6
  //#region src/internal/state-request-pass.ts
7
7
  async function runTextPhase(stateRequest, baseMessages, deps) {
@@ -41,7 +41,7 @@ async function runTextPhase(stateRequest, baseMessages, deps) {
41
41
  output = await normalizeGeneratorResult(raw, id, { request });
42
42
  const rawReasoning = raw?.reasoning;
43
43
  const reasoning = typeof rawReasoning === "string" ? rawReasoning : void 0;
44
- const usage = extractCallUsage(raw);
44
+ const usage = getCallUsage(raw);
45
45
  if (usage) deps.recordUsage?.(usage);
46
46
  deps.onResult?.(agentRequest, {
47
47
  output,
@@ -346,22 +346,22 @@ function isStateMachine(logic) {
346
346
  * its own executor (`.withExecutor(...)`, tracked in `executorBoundLogics`)
347
347
  * always runs itself; explicit binding shadows inheritance.
348
348
  */
349
- function assertBindable(machine, effectiveSources, options) {
350
- assertMachineBindable(machine, effectiveSources, options, {
349
+ function assertBindable(machine, effectiveSources, executors) {
350
+ assertMachineBindable(machine, effectiveSources, executors, {
351
351
  isChild: false,
352
352
  childPath: "",
353
353
  rebindable: true,
354
354
  visited: new Set([machine])
355
355
  });
356
356
  }
357
- function assertMachineBindable(machine, effectiveSources, options, ctx) {
357
+ function assertMachineBindable(machine, effectiveSources, executors, ctx) {
358
358
  const invokes = [];
359
359
  collectConfiguredInvokeSrcs(machine.config, machine.config.id ?? "(root)", invokes);
360
360
  const where = ctx.isChild ? `child machine '${ctx.childPath}' state` : "state";
361
361
  for (const { stateName, src } of invokes) {
362
362
  if (typeof src !== "string") {
363
363
  if (isStateMachine(src)) {
364
- assertChildMachineBindable(src, src, stateName, options, ctx);
364
+ assertChildMachineBindable(src, src, stateName, executors, ctx);
365
365
  continue;
366
366
  }
367
367
  if ((isTextLogic(src) || isDecisionLogic(src)) && !executorBoundLogics.has(src)) throw new Error(`runAgent: ${where} '${stateName}' invokes a direct-object actor logic (kind: '${src.kind}'). Direct-object invoke srcs cannot be rebound by runAgent — either call '.withExecutor(...)' on the logic before invoking it, or register it as a string-keyed actor source instead (machine.provide({ actors: { name: logic } })) and invoke it by name.`);
@@ -370,21 +370,21 @@ function assertMachineBindable(machine, effectiveSources, options, ctx) {
370
370
  const logic = effectiveSources[src];
371
371
  if (logic === void 0) throw new Error(`runAgent: ${where} '${stateName}' invokes unregistered actor source '${src}'. Provide it via machine.provide({ actors: { '${src}': ... } }) or runAgent(machine, { actors: { '${src}': ... } }).`);
372
372
  if (isStateMachine(logic)) {
373
- assertChildMachineBindable(logic, src, stateName, options, ctx);
373
+ assertChildMachineBindable(logic, src, stateName, executors, ctx);
374
374
  continue;
375
375
  }
376
376
  if (src === "agent.userInput") continue;
377
377
  if (isDecisionLogic(logic)) {
378
378
  if (executorBoundLogics.has(logic)) continue;
379
379
  if (!ctx.rebindable) throw unrebindableChildRequestError(ctx.childPath, stateName, src, "decision");
380
- if (!options.hasDecide) throw new Error(`runAgent: ${where} '${stateName}' invokes decision source '${src}' but no 'decide' executor was provided to runAgent(...).`);
380
+ if (!executors.decide) throw new Error(`runAgent: ${where} '${stateName}' invokes decision source '${src}' but no 'decide' executor was provided to runAgent(...).`);
381
381
  continue;
382
382
  }
383
383
  if (isTextLogic(logic)) {
384
384
  if (executorBoundLogics.has(logic)) continue;
385
385
  if (!ctx.rebindable) throw unrebindableChildRequestError(ctx.childPath, stateName, src, logic.mode === "stream" ? "streaming text" : "text");
386
- if (logic.mode === "stream" && !options.hasStreamText) throw new Error(`runAgent: ${where} '${stateName}' invokes streaming text source '${src}' but no 'streamText' executor was provided to runAgent(...).`);
387
- if (logic.mode !== "stream" && !options.hasGenerateText) throw new Error(`runAgent: ${where} '${stateName}' invokes text source '${src}' but no 'generateText' executor was provided to runAgent(...).`);
386
+ if (logic.mode === "stream" && !executors.streamText) throw new Error(`runAgent: ${where} '${stateName}' invokes streaming text source '${src}' but no 'streamText' executor was provided to runAgent(...).`);
387
+ if (logic.mode !== "stream" && !executors.generateText) throw new Error(`runAgent: ${where} '${stateName}' invokes text source '${src}' but no 'generateText' executor was provided to runAgent(...).`);
388
388
  continue;
389
389
  }
390
390
  if (isUnboundPlaceholder(logic)) throw new Error(`runAgent: ${where} '${stateName}' invokes actor source '${src}', which has no host execution. Provide it via machine.provide({ actors: { '${src}': ... } }) or runAgent(machine, { actors: { '${src}': ... } }).`);
@@ -392,12 +392,12 @@ function assertMachineBindable(machine, effectiveSources, options, ctx) {
392
392
  }
393
393
  /** Descends the bind-time walk into an invoked child state machine, guarding
394
394
  * against a machine that (transitively) invokes itself. */
395
- function assertChildMachineBindable(childMachine, childSrc, stateName, options, ctx) {
395
+ function assertChildMachineBindable(childMachine, childSrc, stateName, executors, ctx) {
396
396
  if (ctx.visited.has(childMachine)) return;
397
397
  const childName = typeof childSrc === "string" ? childSrc : childMachine.config.id ?? "(child machine)";
398
398
  const childPath = ctx.childPath ? `${ctx.childPath} > ${childName}` : childName;
399
399
  const childSources = childMachine.sources.actors;
400
- assertMachineBindable(childMachine, childSources, options, {
400
+ assertMachineBindable(childMachine, childSources, executors, {
401
401
  isChild: true,
402
402
  childPath,
403
403
  rebindable: ctx.rebindable && typeof childSrc === "string",
@@ -428,12 +428,45 @@ function unrebindableChildRequestError(childPath, stateName, requestSrc, kind) {
428
428
  function declaresUsageTransition(snapshot) {
429
429
  return getNextTransitions(snapshot).some((transition) => transition.eventType === AGENT_USAGE_EVENT_TYPE);
430
430
  }
431
+ /**
432
+ * Builds the ONE place a trace payload is emitted: it hands the payload to the
433
+ * envelope-stamping trace sink and, from that same payload, invokes the sugar
434
+ * callbacks that are projections of it — {@link RunAgentOptions.onChunk},
435
+ * {@link RunAgentOptions.onResult}, {@link RunAgentOptions.onTransition}. Each
436
+ * keeps its historical position relative to the trace: `onResult` fires just
437
+ * BEFORE its `request.end`, `onChunk`/`onTransition` just AFTER their
438
+ * `stream.chunk`/`machine.transition`. Sugar dispatch never depends on whether
439
+ * a trace sink is present, and the trace sink is never called when it is
440
+ * absent — so an `onTrace`-less run still mints no envelope (and advances no
441
+ * `seq`). @internal
442
+ */
443
+ function createTraceDispatch(sinks) {
444
+ return (payload, self) => {
445
+ switch (payload.type) {
446
+ case "stream.chunk":
447
+ sinks.onTrace?.(payload, self);
448
+ sinks.onChunk?.(payload.chunk, { request: payload.request });
449
+ return;
450
+ case "request.end":
451
+ sinks.onResult?.(payload.request, {
452
+ output: payload.output,
453
+ raw: payload.raw
454
+ });
455
+ sinks.onTrace?.(payload, self);
456
+ return;
457
+ case "machine.transition":
458
+ sinks.onTrace?.(payload, self);
459
+ sinks.onTransition?.(payload.snapshot, payload.event);
460
+ return;
461
+ default: sinks.onTrace?.(payload, self);
462
+ }
463
+ };
464
+ }
431
465
  /** Reads the durable invoke id/src off the async actor's own ref (`self`). */
432
466
  function selfIdAndSrc(self) {
433
- const ref = self;
434
467
  return {
435
- id: typeof ref?.id === "string" ? ref.id : "",
436
- src: typeof ref?.src === "string" ? ref.src : ""
468
+ id: typeof self?.id === "string" ? self.id : "",
469
+ src: typeof self?.src === "string" ? self.src : ""
437
470
  };
438
471
  }
439
472
  /**
@@ -455,8 +488,9 @@ function invokingActorOf(self, runCtx) {
455
488
  * Used by both `runAgent` and `provideExecutors` so the two paths produce
456
489
  * identical event shapes by construction. @internal
457
490
  */
458
- function wrapTextLogicForRunAgent(logic, runCtx) {
459
- return logic.withExecutor(async ({ request, self, signal }) => {
491
+ function bindTextLogic(logic, runCtx) {
492
+ return logic.withExecutor(async ({ request, self: selfArg, signal }) => {
493
+ const self = selfArg;
460
494
  const { id, src } = selfIdAndSrc(self);
461
495
  const executor = logic.mode === "stream" ? runCtx.streamText : runCtx.generateText;
462
496
  if (!executor) throw new Error(`runAgent: no '${logic.mode === "stream" ? "streamText" : "generateText"}' executor provided.`);
@@ -474,19 +508,18 @@ function wrapTextLogicForRunAgent(logic, runCtx) {
474
508
  events: []
475
509
  };
476
510
  runCtx.consumeModelCall();
477
- runCtx.onTrace?.({
511
+ runCtx.emitTrace?.({
478
512
  type: "request.start",
479
513
  request: agentRequest
480
514
  }, self);
481
515
  try {
482
516
  const raw = await executor(requestWithTools, {
483
517
  onChunk: (chunk) => {
484
- runCtx.onTrace?.({
518
+ runCtx.emitTrace?.({
485
519
  type: "stream.chunk",
486
520
  request: agentRequest,
487
521
  chunk
488
522
  }, self);
489
- runCtx.onChunk?.(chunk, { request: agentRequest });
490
523
  },
491
524
  signal,
492
525
  ...runCtx.runId !== void 0 ? { runId: runCtx.runId } : {},
@@ -495,17 +528,16 @@ function wrapTextLogicForRunAgent(logic, runCtx) {
495
528
  const output = await normalizeGeneratorResult(raw, id, {
496
529
  request,
497
530
  onChunk: (chunk) => {
498
- runCtx.onTrace?.({
531
+ runCtx.emitTrace?.({
499
532
  type: "stream.chunk",
500
533
  request: agentRequest,
501
534
  chunk
502
535
  }, self);
503
- runCtx.onChunk?.(chunk, { request: agentRequest });
504
536
  }
505
537
  });
506
538
  const rawReasoning = raw?.reasoning;
507
539
  const reasoning = typeof rawReasoning === "string" ? rawReasoning : void 0;
508
- const usage = extractCallUsage(raw);
540
+ const usage = getCallUsage(raw);
509
541
  if (usage) runCtx.recordUsage?.(usage, {
510
542
  kind: "text",
511
543
  ...id !== "" ? { id } : {},
@@ -513,11 +545,7 @@ function wrapTextLogicForRunAgent(logic, runCtx) {
513
545
  model: request.model,
514
546
  ...request.name !== void 0 ? { name: request.name } : {}
515
547
  }, self);
516
- runCtx.onResult?.(agentRequest, {
517
- output,
518
- raw
519
- });
520
- runCtx.onTrace?.({
548
+ runCtx.emitTrace?.({
521
549
  type: "request.end",
522
550
  request: agentRequest,
523
551
  output,
@@ -527,7 +555,7 @@ function wrapTextLogicForRunAgent(logic, runCtx) {
527
555
  }, self);
528
556
  return { output };
529
557
  } catch (error) {
530
- runCtx.onTrace?.({
558
+ runCtx.emitTrace?.({
531
559
  type: "request.error",
532
560
  request: agentRequest,
533
561
  error
@@ -539,7 +567,7 @@ function wrapTextLogicForRunAgent(logic, runCtx) {
539
567
  function createCountingDecide(runCtx, self) {
540
568
  return async (attemptRequest) => {
541
569
  runCtx.consumeModelCall();
542
- runCtx.onTrace?.({
570
+ runCtx.emitTrace?.({
543
571
  type: "request.start",
544
572
  request: attemptRequest
545
573
  }, self);
@@ -548,7 +576,7 @@ function createCountingDecide(runCtx, self) {
548
576
  ...attemptRequest,
549
577
  runId: runCtx.runId
550
578
  } : attemptRequest);
551
- const usage = extractCallUsage(result);
579
+ const usage = getCallUsage(result);
552
580
  if (usage) {
553
581
  const { src } = selfIdAndSrc(self);
554
582
  runCtx.recordUsage?.(usage, {
@@ -558,11 +586,7 @@ function createCountingDecide(runCtx, self) {
558
586
  model: attemptRequest.model
559
587
  }, self);
560
588
  }
561
- runCtx.onResult?.(attemptRequest, {
562
- output: result.event,
563
- raw: result
564
- });
565
- runCtx.onTrace?.({
589
+ runCtx.emitTrace?.({
566
590
  type: "request.end",
567
591
  request: attemptRequest,
568
592
  output: result.event,
@@ -571,7 +595,7 @@ function createCountingDecide(runCtx, self) {
571
595
  }, self);
572
596
  return result;
573
597
  } catch (error) {
574
- runCtx.onTrace?.({
598
+ runCtx.emitTrace?.({
575
599
  type: "request.error",
576
600
  request: attemptRequest,
577
601
  error
@@ -595,9 +619,10 @@ function createCountingDecide(runCtx, self) {
595
619
  * send-then-complete note inside `run` for how exit-cancels-invoke interacts
596
620
  * with `onDone`.
597
621
  */
598
- function createRunAgentDecisionLogic(logic, runCtx) {
599
- const decisionLogic = createAsyncLogic({ run: async ({ input, signal, self }) => {
622
+ function bindDecisionLogic(logic, runCtx) {
623
+ const decisionLogic = createAsyncLogic({ run: async ({ input, signal, self: selfArg }) => {
600
624
  if (!runCtx.decide) throw new Error("runAgent: no 'decide' executor provided.");
625
+ const self = selfArg;
601
626
  const { id } = selfIdAndSrc(self);
602
627
  const declaredEventTypes = logic.allowedEventTypes?.(input);
603
628
  await Promise.resolve();
@@ -623,7 +648,7 @@ function createRunAgentDecisionLogic(logic, runCtx) {
623
648
  kind: "statelyai.decisionLogic",
624
649
  maxRetries: logic.maxRetries,
625
650
  request: logic.request,
626
- withExecutor: (nextExecute) => createRunAgentDecisionLogic(logic.withExecutor(nextExecute), runCtx)
651
+ withExecutor: (nextExecute) => bindDecisionLogic(logic.withExecutor(nextExecute), runCtx)
627
652
  });
628
653
  }
629
654
  /**
@@ -646,8 +671,7 @@ let nextProvideRunId = 1;
646
671
  /** Walks `self._parent` from an invoked async leaf actor up to its root actor. */
647
672
  function rootActorOf(self) {
648
673
  let ref = self;
649
- if (!ref) return;
650
- while (ref._parent) ref = ref._parent;
674
+ while (ref?._parent) ref = ref._parent;
651
675
  return ref;
652
676
  }
653
677
  /** The per-root envelope state, minted on first use (runId `run_<n>`, matching runAgent). */
@@ -680,7 +704,7 @@ function stampRootTrace(root, payload) {
680
704
  ...payload
681
705
  };
682
706
  }
683
- /** Adapts a public `onTrace` into the payload-level {@link RunAgentBindContext.onTrace} sink used by the shared emission helpers. */
707
+ /** Adapts a public `onTrace` into the payload-level trace sink a {@link TraceDispatch} fans out to. */
684
708
  function provideTraceSink(onTrace) {
685
709
  if (!onTrace) return;
686
710
  return (payload, self) => {
@@ -699,43 +723,48 @@ function provideTraceSink(onTrace) {
699
723
  *
700
724
  * `recordUsage` has no run-level aggregate to fold into here (there is no
701
725
  * run), so it does one thing: deliver the reserved `@agent.usage` event, gated
702
- * exactly like runAgent's — see {@link deliverUsageToInvokingActor}.
726
+ * exactly like runAgent's — see {@link deliverUsageEvent}.
703
727
  */
704
728
  function provideBindContext(machine, executors, options) {
729
+ const traceSink = provideTraceSink(options.onTrace);
730
+ const onChunk = options.onChunk;
705
731
  return {
706
732
  generateText: executors.generateText,
707
733
  streamText: executors.streamText,
708
734
  decide: executors.decide,
709
- onChunk: options.onChunk ? (chunk) => options.onChunk(chunk) : void 0,
710
- onTrace: provideTraceSink(options.onTrace),
735
+ emitTrace: traceSink || onChunk ? createTraceDispatch({
736
+ onTrace: traceSink,
737
+ onChunk: onChunk ? (chunk) => onChunk(chunk) : void 0
738
+ }) : void 0,
711
739
  consumeModelCall: () => {},
712
740
  recordUsage: (usage, source, self) => {
713
- deliverUsageToInvokingActor(usage, source ?? {}, self);
741
+ deliverUsageEvent(usage, source ?? {}, () => self?._parent);
714
742
  },
715
743
  actorHolder: { actorRef: void 0 },
716
744
  schemas: getRegisteredAgentExecutionOptions(machine).schemas
717
745
  };
718
746
  }
719
747
  /**
720
- * `provideExecutors`' counterpart to runAgent's `deliverUsageEvent`: after a
721
- * bound call settles with reported usage, send the reserved
722
- * `@agent.usage` event to the machine actor that INVOKED the request — read
723
- * off the settling request actor's `self._parent`, which under a live
724
- * `createActor` tree is always the invoking machine (there is no run-scoped
725
- * root actor on this path).
748
+ * The single reserved-`@agent.usage` delivery seam, shared by both bind paths:
749
+ * after a bound call settles with reported usage, send the event to the machine
750
+ * actor `resolveActorRef` names the run's root actor on the `runAgent` path,
751
+ * the settling request actor's `self._parent` (always the invoking machine
752
+ * under a live `createActor` tree) on the `provideExecutors` path.
726
753
  *
727
- * Gated identically to runAgent: the invoking snapshot must be active, must
728
- * declare an `'@agent.usage'` transition EXPLICITLY (see
729
- * {@link declaresUsageTransition} — a catch-all `on: { '*' }` is not an opt-in),
730
- * and must be able to take the event. There is no cycle to settle in
731
- * uncontrolled mode, so there are no dropped stragglers.
754
+ * Gating is identical on both: the target snapshot must be active, must declare
755
+ * an `'@agent.usage'` transition EXPLICITLY (see {@link declaresUsageTransition}
756
+ * — a catch-all `on: { '*' }` is not an opt-in), and must be able to take the
757
+ * event. `onDropped` is the run path's straggler gate: it returns `true` for a
758
+ * call that settled after the cycle resolved, which drops the event (traced as
759
+ * `usage.dropped`) rather than delivering it. Uncontrolled mode has no cycle to
760
+ * settle, so it passes no gate and has no dropped stragglers.
732
761
  *
733
- * Delivery follows `provideExecutors`' binding boundary: only sources IT bound
734
- * report here, so an invoked child machine that was not itself passed through
762
+ * Delivery follows each path's binding boundary: only sources IT bound report
763
+ * here, so an invoked child machine that was not itself passed through
735
764
  * `provideExecutors` reports nothing. @internal
736
765
  */
737
- function deliverUsageToInvokingActor(usage, source, self) {
738
- const actorRef = self?._parent;
766
+ function deliverUsageEvent(usage, source, resolveActorRef, onDropped) {
767
+ const actorRef = resolveActorRef();
739
768
  if (!actorRef) return;
740
769
  const snapshot = actorRef.getSnapshot();
741
770
  if (snapshot?.status !== "active" || !declaresUsageTransition(snapshot)) return;
@@ -744,17 +773,18 @@ function deliverUsageToInvokingActor(usage, source, self) {
744
773
  ...source,
745
774
  usage
746
775
  };
776
+ if (onDropped?.(event)) return;
747
777
  if (!snapshot.can(event)) return;
748
778
  actorRef.send(event);
749
779
  }
750
780
  /**
751
781
  * Host-binds one text/stream source for {@link provideExecutors} using the SAME
752
- * emission helper as `runAgent` ({@link wrapTextLogicForRunAgent}), so a bound
782
+ * emission helper as `runAgent` ({@link bindTextLogic}), so a bound
753
783
  * text request emits request.start/stream.chunk/request.end/request.error with
754
784
  * identical shapes. @internal
755
785
  */
756
786
  function bindTextForProvide(machine, logic, executors, options) {
757
- return wrapTextLogicForRunAgent(logic, provideBindContext(machine, executors, options));
787
+ return bindTextLogic(logic, provideBindContext(machine, executors, options));
758
788
  }
759
789
  /**
760
790
  * Host-binds one `DecisionLogic`/`agent.decide` source for
@@ -763,7 +793,27 @@ function bindTextForProvide(machine, logic, executors, options) {
763
793
  * request-level tracing runAgent emits, minus run-scoped counting. @internal
764
794
  */
765
795
  function bindDecisionForProvide(machine, logic, executors, options) {
766
- return createRunAgentDecisionLogic(logic, provideBindContext(machine, executors, options));
796
+ return bindDecisionLogic(logic, provideBindContext(machine, executors, options));
797
+ }
798
+ /**
799
+ * Validates `input` against the machine's registered input schema, returning
800
+ * the schema's output — so defaults are filled and transforms applied before
801
+ * the value reaches `createActor` or the replayable event log.
802
+ *
803
+ * Standard Schema only (no validation library is referenced), so this works for
804
+ * whatever the machine was declared with. Omitted input stays omitted rather
805
+ * than being validated as `{}`: "started with no input" keeps meaning what it
806
+ * has always meant, instead of newly failing schemas with required fields.
807
+ */
808
+ function resolveMachineInput(machine, input) {
809
+ if (input === void 0) return input;
810
+ const schema = getRegisteredAgentExecutionOptions(machine).schemas?.input;
811
+ if (!isStandardSchema(schema)) return input;
812
+ try {
813
+ return validateSchemaSync(schema, input);
814
+ } catch (error) {
815
+ throw new AgentError("invalid-machine-input", `runAgent: machine input failed validation against the declared input schema: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
816
+ }
767
817
  }
768
818
  /**
769
819
  * Recursively rebinds an invoked child machine's own agent sources with the
@@ -786,11 +836,11 @@ function rebindChildMachine(childMachine, runCtx, visited) {
786
836
  const wrapped = {};
787
837
  for (const [key, logic] of Object.entries(sources)) {
788
838
  if (isDecisionLogic(logic)) {
789
- if (!executorBoundLogics.has(logic)) wrapped[key] = createRunAgentDecisionLogic(logic, runCtx);
839
+ if (!executorBoundLogics.has(logic)) wrapped[key] = bindDecisionLogic(logic, runCtx);
790
840
  continue;
791
841
  }
792
842
  if (isTextLogic(logic)) {
793
- if (!executorBoundLogics.has(logic)) wrapped[key] = wrapTextLogicForRunAgent(logic, runCtx);
843
+ if (!executorBoundLogics.has(logic)) wrapped[key] = bindTextLogic(logic, runCtx);
794
844
  continue;
795
845
  }
796
846
  if (isStateMachine(logic)) {
@@ -875,26 +925,29 @@ function createAgentSession(machine, options, lifecycle) {
875
925
  let warnedHeuristicIdle = false;
876
926
  const runId = `run_${nextRunAgentTraceId++}`;
877
927
  let traceSeq = 0;
928
+ const resolvedInput = resolveMachineInput(machine, options.input);
878
929
  const machineId = machine.config.id ?? machine.id ?? "(machine)";
879
930
  const machineVersion = options.machineVersion ?? machine.version ?? getMachineStructuralHash(machine);
880
931
  const agentMeta = {
881
932
  machineId,
882
933
  version: machineVersion
883
934
  };
884
- const stampAgentMeta = (snapshot) => {
885
- if (snapshot && typeof snapshot === "object") snapshot.agentMeta = agentMeta;
886
- };
887
- const onTrace = (event) => {
888
- options.onTrace?.({
889
- schemaVersion: 1,
890
- runId,
891
- seq: ++traceSeq,
892
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
893
- machineId,
894
- machineVersion,
895
- ...event
896
- });
897
- };
935
+ const onTrace = createTraceDispatch({
936
+ onTrace: (payload) => {
937
+ options.onTrace?.({
938
+ schemaVersion: 1,
939
+ runId,
940
+ seq: ++traceSeq,
941
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
942
+ machineId,
943
+ machineVersion,
944
+ ...payload
945
+ });
946
+ },
947
+ onChunk: options.onChunk,
948
+ onResult: options.onResult,
949
+ onTransition: options.onTransition
950
+ });
898
951
  const consumeModelCall = () => {
899
952
  if (budgetExceeded) throw new AgentMaxModelCallsExceededError();
900
953
  if (modelCallCount + 1 > maxModelCalls) {
@@ -905,33 +958,20 @@ function createAgentSession(machine, options, lifecycle) {
905
958
  };
906
959
  const tokenTotals = {};
907
960
  const cycleGate = { isResolved: () => false };
908
- const deliverUsageEvent = (usage, source) => {
909
- const actorRef = actorHolder.actorRef;
910
- if (!actorRef) return;
911
- const event = {
912
- type: AGENT_USAGE_EVENT_TYPE,
913
- ...source,
914
- usage
915
- };
916
- const snapshot = actorRef.getSnapshot();
917
- if (snapshot?.status !== "active" || !declaresUsageTransition(snapshot)) return;
918
- if (cycleGate.isResolved()) {
919
- onTrace({
920
- type: "usage.dropped",
921
- event,
922
- reason: "settled"
923
- });
924
- return;
925
- }
926
- if (!snapshot.can(event)) return;
927
- actorRef.send(event);
928
- };
929
961
  const recordUsage = (usage, source = {}) => {
930
962
  for (const field of AGENT_USAGE_TOKEN_FIELDS) {
931
963
  const value = usage[field];
932
964
  if (typeof value === "number" && Number.isFinite(value)) tokenTotals[field] = (tokenTotals[field] ?? 0) + value;
933
965
  }
934
- deliverUsageEvent(usage, source);
966
+ deliverUsageEvent(usage, source, () => actorHolder.actorRef, (event) => {
967
+ if (!cycleGate.isResolved()) return false;
968
+ onTrace({
969
+ type: "usage.dropped",
970
+ event,
971
+ reason: "settled"
972
+ });
973
+ return true;
974
+ });
935
975
  };
936
976
  const runUsage = () => ({
937
977
  ...tokenTotals,
@@ -951,20 +991,14 @@ function createAgentSession(machine, options, lifecycle) {
951
991
  };
952
992
  const provided = machine.provide({ actors: options.actors });
953
993
  const effectiveSources = provided.sources.actors;
954
- assertBindable(provided, effectiveSources, {
955
- hasGenerateText: !!options.executors?.generateText,
956
- hasDecide: !!options.executors?.decide,
957
- hasStreamText: !!options.executors?.streamText
958
- });
994
+ assertBindable(provided, effectiveSources, options.executors ?? {});
959
995
  if (options.getRequests && !options.executors?.generateText && !options.executors?.decide) throw new Error("runAgent: 'getRequests' requires a 'generateText' and/or 'decide' executor — the returned requests run through them.");
960
996
  const actorHolder = { actorRef: void 0 };
961
997
  const runCtx = {
962
998
  generateText: options.executors?.generateText,
963
999
  streamText: options.executors?.streamText,
964
1000
  decide: options.executors?.decide,
965
- onChunk: options.onChunk,
966
- onResult: options.onResult,
967
- onTrace,
1001
+ emitTrace: onTrace,
968
1002
  consumeModelCall,
969
1003
  recordUsage,
970
1004
  actorHolder,
@@ -985,11 +1019,11 @@ function createAgentSession(machine, options, lifecycle) {
985
1019
  continue;
986
1020
  }
987
1021
  if (isDecisionLogic(logic)) {
988
- wrappedSources[key] = createRunAgentDecisionLogic(logic, runCtx);
1022
+ wrappedSources[key] = bindDecisionLogic(logic, runCtx);
989
1023
  continue;
990
1024
  }
991
1025
  if (isTextLogic(logic)) {
992
- if (!executorBoundLogics.has(logic)) wrappedSources[key] = wrapTextLogicForRunAgent(logic, runCtx);
1026
+ if (!executorBoundLogics.has(logic)) wrappedSources[key] = bindTextLogic(logic, runCtx);
993
1027
  continue;
994
1028
  }
995
1029
  if (isStateMachine(logic)) {
@@ -1031,9 +1065,11 @@ function createAgentSession(machine, options, lifecycle) {
1031
1065
  }
1032
1066
  const priorMessages = getAgentMessages(effectiveSnapshot);
1033
1067
  const messages = typeof options.messages === "function" ? [...options.messages([...priorMessages])] : [...priorMessages, ...options.messages ?? []];
1034
- const stampMessages = (snapshot) => {
1068
+ const stampSettledSnapshot = (snapshot) => {
1069
+ if (!snapshot || typeof snapshot !== "object") return;
1070
+ snapshot.agentMeta = agentMeta;
1035
1071
  if (!options.getRequests && !options.messages && messages.length === 0) return;
1036
- if (snapshot && typeof snapshot === "object") snapshot.messages = [...messages];
1072
+ snapshot.messages = [...messages];
1037
1073
  };
1038
1074
  if (effectiveSnapshot !== void 0 && options.event !== void 0 && (options.onIllegalResumeEvent ?? "throw") === "throw") {
1039
1075
  const acceptedTypes = getAcceptedEvents(createActor(boundMachine, { snapshot: effectiveSnapshot }).getSnapshot(), { schemas: runCtx.schemas }).map((descriptor) => descriptor.type);
@@ -1041,21 +1077,11 @@ function createAgentSession(machine, options, lifecycle) {
1041
1077
  if (!acceptedTypes.includes(eventType)) throw new AgentIllegalResumeEventError(eventType, acceptedTypes);
1042
1078
  }
1043
1079
  const replayEvents = [...resumeEvents ?? []];
1044
- const replayEventIds = /* @__PURE__ */ new Set();
1045
- for (let index = 0; index < replayEvents.length; index++) {
1046
- const entry = replayEvents[index];
1047
- assertAgentLogEntry(entry);
1048
- if (entry.index !== index) throw new Error(`runAgent events must be contiguous from index 0; found entry.index ${entry.index} at position ${index}.`);
1049
- if (entry.machineId !== machineId || entry.machineVersion !== machineVersion) throw new AgentReplayMachineMismatchError(entry.id, entry.index, {
1050
- machineId,
1051
- machineVersion
1052
- }, {
1053
- machineId: entry.machineId,
1054
- machineVersion: entry.machineVersion
1055
- });
1056
- if (replayEventIds.has(entry.id)) throw new Error(`runAgent events contain duplicate event id '${entry.id}'.`);
1057
- replayEventIds.add(entry.id);
1058
- }
1080
+ validateReplayEntries(replayEvents, {
1081
+ machineId,
1082
+ machineVersion
1083
+ }, "runAgent events");
1084
+ const replayEventIds = new Set(replayEvents.map((entry) => entry.id));
1059
1085
  const hasCompleteReplayHistory = replayEvents[0]?.event.type === "@agent.init" || effectiveSnapshot === void 0 && replayEvents.length === 0;
1060
1086
  const appendReplayEvent = (event) => {
1061
1087
  const entry = createReplayEntry(machine, replayEvents, event, {
@@ -1069,252 +1095,244 @@ function createAgentSession(machine, options, lifecycle) {
1069
1095
  return entry;
1070
1096
  };
1071
1097
  if (replayEvents.length === 0 && effectiveSnapshot === void 0) {
1072
- const entry = initEntry(machine, options.input, { machineVersion });
1098
+ const entry = initEntry(machine, resolvedInput, { machineVersion });
1073
1099
  replayEventIds.add(entry.id);
1074
1100
  replayEvents.push(entry);
1075
1101
  options.onEvent?.(entry);
1076
1102
  }
1077
- return (() => {
1078
- let settled = false;
1079
- let finalized = false;
1080
- cycleGate.isResolved = () => settled;
1081
- let lastResult;
1082
- const waiters = [];
1083
- let idleTimer;
1084
- let actor;
1085
- let deliveringResumeEvent = options.event !== void 0;
1086
- const settle = (outcome) => {
1087
- if (settled) return;
1088
- settled = true;
1089
- const result = {
1090
- ...outcome,
1091
- events: [...replayEvents],
1092
- usage: runUsage()
1093
- };
1094
- if (idleTimer !== void 0) clearTimeout(idleTimer);
1095
- stampAgentMeta(result.snapshot);
1096
- stampMessages(result.snapshot);
1097
- if ("persistedSnapshot" in result) {
1098
- stampAgentMeta(result.persistedSnapshot);
1099
- stampMessages(result.persistedSnapshot);
1100
- }
1101
- onTrace({
1102
- type: "run.end",
1103
- ...outcome
1104
- });
1105
- if (lifecycle.oneShot || result.status !== "idle") {
1106
- finalized = true;
1107
- if (options.signal) options.signal.removeEventListener("abort", onAbort);
1108
- actor.stop();
1109
- }
1110
- lastResult = result;
1111
- for (const resolve of waiters.splice(0)) resolve(result);
1112
- };
1113
- const onAbort = () => {
1114
- settle({
1115
- status: "error",
1116
- cause: "aborted",
1117
- error: options.signal?.reason ?? /* @__PURE__ */ new Error("Aborted"),
1118
- snapshot: actor.getSnapshot()
1119
- });
1120
- };
1121
- const settleIdle = (current) => {
1122
- warnNonSerializableContext(current);
1123
- const pendingUserInputs = userInputIsPlaceholder ? collectPendingUserInputs(current) : [];
1124
- settle({
1125
- status: "idle",
1126
- snapshot: current,
1127
- persistedSnapshot: actor.getPersistedSnapshot(),
1128
- ...pendingUserInputs.length > 0 ? { pendingUserInputs } : {}
1129
- });
1103
+ let settled = false;
1104
+ let finalized = false;
1105
+ cycleGate.isResolved = () => settled;
1106
+ let lastResult;
1107
+ const waiters = [];
1108
+ let idleTimer;
1109
+ let actor;
1110
+ let deliveringResumeEvent = options.event !== void 0;
1111
+ const settle = (outcome) => {
1112
+ if (settled) return;
1113
+ settled = true;
1114
+ const result = {
1115
+ ...outcome,
1116
+ events: [...replayEvents],
1117
+ usage: runUsage()
1130
1118
  };
1131
- let interpreting = false;
1132
- let interpretSeq = 0;
1133
- const appendToLog = (...items) => {
1134
- messages.push(...items);
1135
- if (options.onMessage) {
1136
- const info = {
1137
- runId,
1138
- machineId,
1139
- machineVersion
1140
- };
1141
- for (const item of items) options.onMessage(item, info);
1142
- }
1143
- };
1144
- const runErrorCause = (error) => budgetExceeded ? "max-model-calls" : wrapsDecisionExhausted(error) ? "decision-exhausted" : "machine";
1145
- const settleInterpretError = (error) => {
1146
- settle({
1147
- status: "error",
1148
- cause: runErrorCause(error),
1149
- error,
1150
- snapshot: actor.getSnapshot()
1151
- });
1152
- };
1153
- const passDeps = {
1154
- getSnapshot: () => actor.getSnapshot(),
1155
- send: (event) => actor.send(event),
1156
- isSettled: () => settled,
1157
- messages,
1158
- appendToLog,
1159
- generateText: runCtx.generateText,
1160
- decide: runCtx.decide ? createCountingDecide(runCtx, void 0) : void 0,
1161
- consumeModelCall,
1162
- recordUsage,
1163
- nextRequestId: () => `interpret_${++interpretSeq}`,
1164
- onTrace,
1165
- onResult: runCtx.onResult,
1166
- schemas: runCtx.schemas,
1167
- signal: options.signal
1168
- };
1169
- const maybeInterpret = (snapshot) => {
1170
- if (!options.getRequests || settled) return false;
1171
- if (interpreting) return true;
1172
- let requested;
1173
- try {
1174
- requested = options.getRequests(snapshot, { messages });
1175
- } catch (error) {
1176
- settleInterpretError(error);
1177
- return true;
1178
- }
1179
- const requests = (Array.isArray(requested) ? requested : requested ? [requested] : []).filter((stateRequest) => Boolean(stateRequest));
1180
- if (requests.length === 0) return false;
1181
- interpreting = true;
1182
- runStateRequestPass(requests, passDeps).then(({ sentAny }) => {
1183
- if (settled || sentAny) return;
1184
- const current = actor.getSnapshot();
1185
- if (isIdleSnapshot(current, { ignoreUserInputChildren: userInputIsPlaceholder })) settleIdle(current);
1186
- }).catch((error) => settleInterpretError(error)).finally(() => {
1187
- interpreting = false;
1188
- if (!settled) scheduleIdleCheck();
1189
- });
1119
+ if (idleTimer !== void 0) clearTimeout(idleTimer);
1120
+ stampSettledSnapshot(result.snapshot);
1121
+ if ("persistedSnapshot" in result) stampSettledSnapshot(result.persistedSnapshot);
1122
+ onTrace({
1123
+ type: "run.end",
1124
+ ...outcome
1125
+ });
1126
+ if (lifecycle.oneShot || result.status !== "idle") {
1127
+ finalized = true;
1128
+ if (options.signal) options.signal.removeEventListener("abort", onAbort);
1129
+ actor.stop();
1130
+ }
1131
+ lastResult = result;
1132
+ for (const resolve of waiters.splice(0)) resolve(result);
1133
+ };
1134
+ const onAbort = () => {
1135
+ settle({
1136
+ status: "error",
1137
+ cause: "aborted",
1138
+ error: options.signal?.reason ?? /* @__PURE__ */ new Error("Aborted"),
1139
+ snapshot: actor.getSnapshot()
1140
+ });
1141
+ };
1142
+ const settleIdle = (current) => {
1143
+ warnNonSerializableContext(current);
1144
+ const pendingUserInputs = userInputIsPlaceholder ? collectPendingUserInputs(current) : [];
1145
+ settle({
1146
+ status: "idle",
1147
+ snapshot: current,
1148
+ persistedSnapshot: actor.getPersistedSnapshot(),
1149
+ ...pendingUserInputs.length > 0 ? { pendingUserInputs } : {}
1150
+ });
1151
+ };
1152
+ let interpreting = false;
1153
+ let interpretSeq = 0;
1154
+ const appendToLog = (...items) => {
1155
+ messages.push(...items);
1156
+ if (options.onMessage) {
1157
+ const info = {
1158
+ runId,
1159
+ machineId,
1160
+ machineVersion
1161
+ };
1162
+ for (const item of items) options.onMessage(item, info);
1163
+ }
1164
+ };
1165
+ const runErrorCause = (error) => budgetExceeded ? "max-model-calls" : wrapsDecisionExhausted(error) ? "decision-exhausted" : "machine";
1166
+ const settleInterpretError = (error) => {
1167
+ settle({
1168
+ status: "error",
1169
+ cause: runErrorCause(error),
1170
+ error,
1171
+ snapshot: actor.getSnapshot()
1172
+ });
1173
+ };
1174
+ const passDeps = {
1175
+ getSnapshot: () => actor.getSnapshot(),
1176
+ send: (event) => actor.send(event),
1177
+ isSettled: () => settled,
1178
+ messages,
1179
+ appendToLog,
1180
+ generateText: runCtx.generateText,
1181
+ decide: runCtx.decide ? createCountingDecide(runCtx, void 0) : void 0,
1182
+ consumeModelCall,
1183
+ recordUsage,
1184
+ nextRequestId: () => `interpret_${++interpretSeq}`,
1185
+ onTrace,
1186
+ schemas: runCtx.schemas,
1187
+ signal: options.signal
1188
+ };
1189
+ const maybeInterpret = (snapshot) => {
1190
+ if (!options.getRequests || settled) return false;
1191
+ if (interpreting) return true;
1192
+ let requested;
1193
+ try {
1194
+ requested = options.getRequests(snapshot, { messages });
1195
+ } catch (error) {
1196
+ settleInterpretError(error);
1190
1197
  return true;
1191
- };
1192
- const scheduleIdleCheck = () => {
1193
- if (idleTimer !== void 0) clearTimeout(idleTimer);
1194
- idleTimer = setTimeout(() => {
1195
- idleTimer = void 0;
1196
- if (settled) return;
1197
- const current = actor.getSnapshot();
1198
- if (isIdleSnapshot(current, { ignoreUserInputChildren: userInputIsPlaceholder })) {
1199
- if (!maybeInterpret(current)) {
1200
- if (!declaredSuspensionPredicate && current.status === "active" && !warnedHeuristicIdle && process.env.NODE_ENV !== "production") {
1201
- warnedHeuristicIdle = true;
1202
- console.warn("[@statelyai/agent] runAgent settled idle via the timing heuristic (no suspension predicate declared). This is best-effort; for deterministic idle detection, declare setupAgent({ isSuspended }) or pass runAgent(machine, { isSuspended }), e.g. (s) => s.hasTag('waiting').");
1203
- }
1204
- settleIdle(current);
1198
+ }
1199
+ const requests = (Array.isArray(requested) ? requested : requested ? [requested] : []).filter((stateRequest) => Boolean(stateRequest));
1200
+ if (requests.length === 0) return false;
1201
+ interpreting = true;
1202
+ runStateRequestPass(requests, passDeps).then(({ sentAny }) => {
1203
+ if (settled || sentAny) return;
1204
+ const current = actor.getSnapshot();
1205
+ if (isIdleSnapshot(current, { ignoreUserInputChildren: userInputIsPlaceholder })) settleIdle(current);
1206
+ }).catch((error) => settleInterpretError(error)).finally(() => {
1207
+ interpreting = false;
1208
+ if (!settled) scheduleIdleCheck();
1209
+ });
1210
+ return true;
1211
+ };
1212
+ const scheduleIdleCheck = () => {
1213
+ if (idleTimer !== void 0) clearTimeout(idleTimer);
1214
+ idleTimer = setTimeout(() => {
1215
+ idleTimer = void 0;
1216
+ if (settled) return;
1217
+ const current = actor.getSnapshot();
1218
+ if (isIdleSnapshot(current, { ignoreUserInputChildren: userInputIsPlaceholder })) {
1219
+ if (!maybeInterpret(current)) {
1220
+ if (!declaredSuspensionPredicate && current.status === "active" && !warnedHeuristicIdle && process.env.NODE_ENV !== "production") {
1221
+ warnedHeuristicIdle = true;
1222
+ console.warn("[@statelyai/agent] runAgent settled idle via the timing heuristic (no suspension predicate declared). This is best-effort; for deterministic idle detection, declare setupAgent({ isSuspended }) or pass runAgent(machine, { isSuspended }), e.g. (s) => s.hasTag('waiting').");
1205
1223
  }
1224
+ settleIdle(current);
1206
1225
  }
1207
- }, 0);
1208
- };
1209
- actor = createActor(boundMachine, {
1210
- input: options.input,
1211
- snapshot: effectiveSnapshot,
1212
- inspect: (event) => {
1213
- if (typeof options.inspect === "function") options.inspect(event);
1214
- else options.inspect?.next?.(event);
1215
- if (event.type !== "@xstate.transition" || event.actorRef !== actor.ref) return;
1216
- if (settled) {
1217
- if (finalized) return;
1218
- settled = false;
1219
- lastResult = void 0;
1220
- }
1221
- const snapshot = event.snapshot;
1222
- let eventId;
1223
- if (event.event.type !== "@xstate.init" && (event.sourceRef !== event.actorRef || event.event.type === "xstate.timer")) eventId = appendReplayEvent(event.event).id;
1224
- else if (event.event.type === "@xstate.init") eventId = replayEvents[0]?.event.type === "@agent.init" ? replayEvents[0].id : void 0;
1225
- onTrace({
1226
- type: "machine.transition",
1227
- snapshot,
1228
- event: event.event,
1229
- ...eventId !== void 0 ? { eventId } : {}
1230
- });
1231
- options.onTransition?.(snapshot, event.event);
1232
- if (snapshot.status === "done") {
1233
- settle({
1234
- status: "done",
1235
- output: snapshot.output,
1236
- snapshot
1237
- });
1238
- return;
1239
- }
1240
- if (snapshot.status === "error") {
1241
- settle({
1242
- status: "error",
1243
- cause: runErrorCause(snapshot.error),
1244
- error: snapshot.error,
1245
- snapshot
1246
- });
1247
- return;
1248
- }
1249
- if (snapshot.status === "stopped") {
1250
- settle({
1251
- status: "error",
1252
- cause: "stopped",
1253
- error: /* @__PURE__ */ new Error("Actor stopped externally."),
1254
- snapshot
1255
- });
1256
- return;
1257
- }
1258
- if (!deliveringResumeEvent && isSuspended(snapshot) && isIdleSnapshot(snapshot, { ignoreUserInputChildren: userInputIsPlaceholder })) {
1259
- queueMicrotask(() => {
1260
- if (settled) return;
1261
- const current = actor.getSnapshot();
1262
- if (isSuspended(current) && isIdleSnapshot(current, { ignoreUserInputChildren: userInputIsPlaceholder })) {
1263
- if (!maybeInterpret(current)) settleIdle(current);
1264
- } else scheduleIdleCheck();
1265
- });
1266
- return;
1267
- }
1268
- scheduleIdleCheck();
1269
1226
  }
1270
- });
1271
- actorHolder.actorRef = actor;
1272
- actor.subscribe({ error: () => {} });
1273
- actor.on("*", (event) => {
1227
+ }, 0);
1228
+ };
1229
+ actor = createActor(boundMachine, {
1230
+ input: resolvedInput,
1231
+ snapshot: effectiveSnapshot,
1232
+ inspect: (event) => {
1233
+ if (typeof options.inspect === "function") options.inspect(event);
1234
+ else options.inspect?.next?.(event);
1235
+ if (event.type !== "@xstate.transition" || event.actorRef !== actor.ref) return;
1236
+ if (settled) {
1237
+ if (finalized) return;
1238
+ settled = false;
1239
+ lastResult = void 0;
1240
+ }
1241
+ const snapshot = event.snapshot;
1242
+ let eventId;
1243
+ if (event.event.type !== "@xstate.init" && (event.sourceRef !== event.actorRef || event.event.type === "xstate.timer")) eventId = appendReplayEvent(event.event).id;
1244
+ else if (event.event.type === "@xstate.init") eventId = replayEvents[0]?.event.type === "@agent.init" ? replayEvents[0].id : void 0;
1274
1245
  onTrace({
1275
- type: "emit",
1276
- event
1246
+ type: "machine.transition",
1247
+ snapshot,
1248
+ event: event.event,
1249
+ ...eventId !== void 0 ? { eventId } : {}
1277
1250
  });
1278
- });
1279
- for (const [type, handler] of Object.entries(options.on ?? {})) if (typeof handler === "function") actor.on(type, handler);
1280
- const sessionApi = {
1281
- actor,
1282
- get events() {
1283
- return replayEvents;
1284
- },
1285
- usage: runUsage,
1286
- settled: () => settled && lastResult !== void 0 ? Promise.resolve(lastResult) : new Promise((resolve) => {
1287
- waiters.push(resolve);
1288
- }),
1289
- stop: () => {
1290
- actor.stop();
1251
+ if (snapshot.status === "done") {
1252
+ settle({
1253
+ status: "done",
1254
+ output: snapshot.output,
1255
+ snapshot
1256
+ });
1257
+ return;
1291
1258
  }
1292
- };
1293
- if (options.signal) {
1294
- if (options.signal.aborted) {
1259
+ if (snapshot.status === "error") {
1295
1260
  settle({
1296
1261
  status: "error",
1297
- cause: "aborted",
1298
- error: options.signal.reason ?? /* @__PURE__ */ new Error("Aborted"),
1299
- snapshot: actor.getSnapshot()
1262
+ cause: runErrorCause(snapshot.error),
1263
+ error: snapshot.error,
1264
+ snapshot
1300
1265
  });
1301
- return sessionApi;
1266
+ return;
1302
1267
  }
1303
- options.signal.addEventListener("abort", onAbort);
1268
+ if (snapshot.status === "stopped") {
1269
+ settle({
1270
+ status: "error",
1271
+ cause: "stopped",
1272
+ error: /* @__PURE__ */ new Error("Actor stopped externally."),
1273
+ snapshot
1274
+ });
1275
+ return;
1276
+ }
1277
+ if (!deliveringResumeEvent && isSuspended(snapshot) && isIdleSnapshot(snapshot, { ignoreUserInputChildren: userInputIsPlaceholder })) {
1278
+ queueMicrotask(() => {
1279
+ if (settled) return;
1280
+ const current = actor.getSnapshot();
1281
+ if (isSuspended(current) && isIdleSnapshot(current, { ignoreUserInputChildren: userInputIsPlaceholder })) {
1282
+ if (!maybeInterpret(current)) settleIdle(current);
1283
+ } else scheduleIdleCheck();
1284
+ });
1285
+ return;
1286
+ }
1287
+ scheduleIdleCheck();
1304
1288
  }
1289
+ });
1290
+ actorHolder.actorRef = actor;
1291
+ actor.subscribe({ error: () => {} });
1292
+ actor.on("*", (event) => {
1305
1293
  onTrace({
1306
- type: "run.start",
1307
- ...options.input !== void 0 ? { input: options.input } : {},
1308
- ...effectiveSnapshot !== void 0 ? { snapshot: effectiveSnapshot } : {},
1309
- ...options.event !== void 0 ? { event: options.event } : {}
1294
+ type: "emit",
1295
+ event
1310
1296
  });
1311
- actor.start();
1312
- if (options.event) {
1313
- deliveringResumeEvent = false;
1314
- actor.send(options.event);
1297
+ });
1298
+ for (const [type, handler] of Object.entries(options.on ?? {})) if (typeof handler === "function") actor.on(type, handler);
1299
+ const sessionApi = {
1300
+ actor,
1301
+ get events() {
1302
+ return replayEvents;
1303
+ },
1304
+ usage: runUsage,
1305
+ settled: () => settled && lastResult !== void 0 ? Promise.resolve(lastResult) : new Promise((resolve) => {
1306
+ waiters.push(resolve);
1307
+ }),
1308
+ stop: () => {
1309
+ actor.stop();
1310
+ }
1311
+ };
1312
+ if (options.signal) {
1313
+ if (options.signal.aborted) {
1314
+ settle({
1315
+ status: "error",
1316
+ cause: "aborted",
1317
+ error: options.signal.reason ?? /* @__PURE__ */ new Error("Aborted"),
1318
+ snapshot: actor.getSnapshot()
1319
+ });
1320
+ return sessionApi;
1315
1321
  }
1316
- return sessionApi;
1317
- })();
1322
+ options.signal.addEventListener("abort", onAbort);
1323
+ }
1324
+ onTrace({
1325
+ type: "run.start",
1326
+ ...resolvedInput !== void 0 ? { input: resolvedInput } : {},
1327
+ ...effectiveSnapshot !== void 0 ? { snapshot: effectiveSnapshot } : {},
1328
+ ...options.event !== void 0 ? { event: options.event } : {}
1329
+ });
1330
+ actor.start();
1331
+ if (options.event) {
1332
+ deliveringResumeEvent = false;
1333
+ actor.send(options.event);
1334
+ }
1335
+ return sessionApi;
1318
1336
  }
1319
1337
  /**
1320
1338
  * Runs an agent machine to a **final state**, for run-to-done flows where an
@@ -1470,14 +1488,17 @@ function createAgentRun(machine, options) {
1470
1488
  });
1471
1489
  else buffer.push(event);
1472
1490
  };
1473
- const close = () => {
1474
- if (closed) return;
1475
- closed = true;
1491
+ const drain = () => {
1476
1492
  while (resolvers.length > 0) resolvers.shift()({
1477
1493
  value: void 0,
1478
1494
  done: true
1479
1495
  });
1480
1496
  };
1497
+ const close = () => {
1498
+ if (closed) return;
1499
+ closed = true;
1500
+ drain();
1501
+ };
1481
1502
  const userOnTrace = options.onTrace;
1482
1503
  const result = runAgent(machine, {
1483
1504
  ...options,
@@ -1506,10 +1527,7 @@ function createAgentRun(machine, options) {
1506
1527
  return() {
1507
1528
  stopped = true;
1508
1529
  buffer.length = 0;
1509
- while (resolvers.length > 0) resolvers.shift()({
1510
- value: void 0,
1511
- done: true
1512
- });
1530
+ drain();
1513
1531
  return Promise.resolve({
1514
1532
  value: void 0,
1515
1533
  done: true
@@ -1557,7 +1575,7 @@ function createAgentRun(machine, options) {
1557
1575
  * Throws at bind time if a source needs an executor kind that `executors` does
1558
1576
  * not provide.
1559
1577
  *
1560
- * v1 does NOT descend into invoked child state machines: a string-keyed child
1578
+ * `provideExecutors` does not descend into invoked child state machines: a string-keyed child
1561
1579
  * machine source is left untouched, so a child with its own agent invokes needs
1562
1580
  * its own `provideExecutors(...)` (or `runAgent`, which does rebind children).
1563
1581
  */
@@ -1566,32 +1584,36 @@ function provideExecutors(machine, executors, options = {}) {
1566
1584
  onChunk: options.onChunk,
1567
1585
  onTrace: options.onTrace
1568
1586
  };
1569
- const provided = options.actors ? machine.provide({ actors: options.actors }) : machine;
1587
+ const withActors = (target, actors) => target.provide({ actors });
1588
+ const provided = options.actors ? withActors(machine, options.actors) : machine;
1570
1589
  const effectiveSources = provided.sources.actors;
1571
1590
  const wrappedSources = {};
1572
1591
  const invokedSrcs = getConfiguredInvokeSrcs(provided);
1573
1592
  for (const [key, logic] of Object.entries(effectiveSources)) {
1574
1593
  if (key === "agent.userInput") continue;
1575
- if (isDecisionLogic(logic)) {
1576
- if (executorBoundLogics.has(logic)) continue;
1577
- if (!executors.decide) {
1578
- if (invokedSrcs.has(key)) throw missingExecutorError(key, "decision", "decide");
1579
- continue;
1580
- }
1581
- wrappedSources[key] = bindDecisionForProvide(provided, logic, executors, bindOptions);
1582
- continue;
1594
+ let binding;
1595
+ if (isDecisionLogic(logic)) binding = {
1596
+ executorKey: "decide",
1597
+ kind: "decision",
1598
+ bind: () => bindDecisionForProvide(provided, logic, executors, bindOptions)
1599
+ };
1600
+ else if (isTextLogic(logic)) {
1601
+ const streaming = logic.mode === "stream";
1602
+ binding = {
1603
+ executorKey: streaming ? "streamText" : "generateText",
1604
+ kind: streaming ? "streaming text" : "text",
1605
+ bind: () => bindTextForProvide(provided, logic, executors, bindOptions)
1606
+ };
1583
1607
  }
1584
- if (isTextLogic(logic)) {
1585
- if (executorBoundLogics.has(logic)) continue;
1586
- if (!(logic.mode === "stream" ? executors.streamText : executors.generateText)) {
1587
- if (invokedSrcs.has(key)) throw logic.mode === "stream" ? missingExecutorError(key, "streaming text", "streamText") : missingExecutorError(key, "text", "generateText");
1588
- continue;
1589
- }
1590
- wrappedSources[key] = bindTextForProvide(provided, logic, executors, bindOptions);
1608
+ if (!binding) continue;
1609
+ if (executorBoundLogics.has(logic)) continue;
1610
+ if (!executors[binding.executorKey]) {
1611
+ if (invokedSrcs.has(key)) throw missingExecutorError(key, binding.kind, binding.executorKey);
1591
1612
  continue;
1592
1613
  }
1614
+ wrappedSources[key] = binding.bind();
1593
1615
  }
1594
- return provided.provide({ actors: wrappedSources });
1616
+ return withActors(provided, wrappedSources);
1595
1617
  }
1596
1618
  function missingExecutorError(src, kind, executor) {
1597
1619
  return /* @__PURE__ */ new Error(`provideExecutors: actor source '${src}' is a ${kind} source but no '${executor}' executor was provided. Add it to the executors object, or bind the source with its own executor (logic.withExecutor(...)) before calling provideExecutors.`);
@@ -1611,10 +1633,8 @@ function buildStateIndex(rootConfig) {
1611
1633
  const hasChildren = !!config.states && Object.keys(config.states).length > 0;
1612
1634
  index.set(path, {
1613
1635
  path,
1614
- name,
1615
1636
  config,
1616
1637
  parentPath,
1617
- type: config.type,
1618
1638
  isFinal: config.type === "final",
1619
1639
  isParallel: config.type === "parallel",
1620
1640
  isCompound: hasChildren && config.type !== "parallel",
@@ -1732,36 +1752,20 @@ function ancestorChain(node, index) {
1732
1752
  function hasNonEmptyOn(config) {
1733
1753
  return !!config.on && Object.keys(config.on).length > 0;
1734
1754
  }
1735
- function decisionKindOf(src, actors) {
1736
- if (typeof src === "string") {
1737
- if (src === DECIDE_SRC) return "decision";
1738
- return isDecisionLogic(actors[src]) ? "decision" : void 0;
1739
- }
1740
- return isDecisionLogic(src) ? "decision" : void 0;
1755
+ function isDecisionInvoke(src, actors) {
1756
+ if (typeof src === "string") return src === DECIDE_SRC || isDecisionLogic(actors[src]);
1757
+ return isDecisionLogic(src);
1741
1758
  }
1742
1759
  function isAgentLogicNeedingBinding(src) {
1743
1760
  return (isTextLogic(src) || isDecisionLogic(src)) && !executorBoundLogics.has(src);
1744
1761
  }
1745
- function schemaExposesJson(schema) {
1746
- if (!schema) return false;
1747
- try {
1748
- return getJsonSchemaSync(schema) !== void 0;
1749
- } catch {
1750
- return false;
1751
- }
1752
- }
1753
- function isDeclaredOutputSchema(schema) {
1754
- if (!schema) return false;
1755
- let json;
1762
+ function readJsonSchema(schema) {
1763
+ if (!schema) return;
1756
1764
  try {
1757
- json = getJsonSchemaSync(schema);
1765
+ return getJsonSchemaSync(schema);
1758
1766
  } catch {
1759
- return false;
1767
+ return;
1760
1768
  }
1761
- if (!json) return false;
1762
- const properties = json.properties;
1763
- const required = json.required;
1764
- return json.type === "object" && !!properties && Object.keys(properties).length > 0 || Array.isArray(required) && required.length > 0;
1765
1769
  }
1766
1770
  function checkUnreachableStates(ctx) {
1767
1771
  const out = [];
@@ -1776,8 +1780,7 @@ function checkUnreachableStates(ctx) {
1776
1780
  function checkDecideWithoutEvents(ctx) {
1777
1781
  const out = [];
1778
1782
  for (const node of ctx.index.values()) for (const invoke of node.invokes) {
1779
- const kind = decisionKindOf(invoke.src, ctx.actors);
1780
- if (!kind) continue;
1783
+ if (!isDecisionInvoke(invoke.src, ctx.actors)) continue;
1781
1784
  const selfHandles = hasNonEmptyOn(node.config);
1782
1785
  const ancestorHandles = ancestorChain(node, ctx.index).some((ancestor) => hasNonEmptyOn(ancestor.config));
1783
1786
  const invokeObserves = invoke.onDone !== void 0;
@@ -1787,7 +1790,7 @@ function checkDecideWithoutEvents(ctx) {
1787
1790
  code: "decide-without-events",
1788
1791
  severity: "error",
1789
1792
  path: node.path,
1790
- message: `State '${node.path}' invokes ${kind} source '${srcName}', but neither it nor any ancestor handles any event (no 'on:'), so the ${kind}'s chosen event can never be delivered. Add an 'on:' handler for the candidate events.`
1793
+ message: `State '${node.path}' invokes decision source '${srcName}', but neither it nor any ancestor handles any event (no 'on:'), so the decision's chosen event can never be delivered. Add an 'on:' handler for the candidate events.`
1791
1794
  });
1792
1795
  }
1793
1796
  return out;
@@ -1795,7 +1798,7 @@ function checkDecideWithoutEvents(ctx) {
1795
1798
  function checkUnserializableContext(ctx) {
1796
1799
  const contextSchema = ctx.schemas?.context;
1797
1800
  if (!contextSchema) return [];
1798
- if (schemaExposesJson(contextSchema)) return [];
1801
+ if (readJsonSchema(contextSchema)) return [];
1799
1802
  return [{
1800
1803
  code: "unserializable-context",
1801
1804
  severity: "warning",
@@ -1819,7 +1822,10 @@ function checkDirectObjectSrc(ctx) {
1819
1822
  return out;
1820
1823
  }
1821
1824
  function checkFinalWithoutOutput(ctx) {
1822
- if (!isDeclaredOutputSchema(ctx.schemas?.output)) return [];
1825
+ const outputJson = readJsonSchema(ctx.schemas?.output);
1826
+ const properties = outputJson?.properties;
1827
+ const required = outputJson?.required;
1828
+ if (!(outputJson?.type === "object" && !!properties && Object.keys(properties).length > 0 || Array.isArray(required) && required.length > 0)) return [];
1823
1829
  if (ctx.config.output !== void 0) return [];
1824
1830
  const out = [];
1825
1831
  for (const node of ctx.index.values()) {
@@ -1913,7 +1919,6 @@ function lintAgentMachine(machine, options = {}) {
1913
1919
  const reachable = computeReachable(config, index, getMachineStaticTransitionTargets(machine));
1914
1920
  const registered = getRegisteredAgentExecutionOptions(machine);
1915
1921
  const ctx = {
1916
- machine,
1917
1922
  config,
1918
1923
  index,
1919
1924
  reachable,
@@ -1996,7 +2001,7 @@ function takeFromQueue(channel, src) {
1996
2001
  async function simulateAgent(machine, options) {
1997
2002
  const maxSteps = options.maxSteps ?? 100;
1998
2003
  const script = {
1999
- text: { ...options.script.text },
2004
+ text: mapValues(options.script.text ?? {}, (arr) => [...arr]),
2000
2005
  decisions: mapValues(options.script.decisions ?? {}, (arr) => [...arr]),
2001
2006
  invokes: mapValues(options.script.invokes ?? {}, (arr) => [...arr])
2002
2007
  };
@@ -2068,6 +2073,7 @@ function scriptDryError(kind, src, id, request) {
2068
2073
  const events = request?.kind === "decision" ? ` Candidate events: ${request.events.map((e) => e.type).join(", ") || "(none)"}.` : "";
2069
2074
  return /* @__PURE__ */ new Error(`simulateAgent: script ran dry on a pending ${kind} request for src '${src}' (id '${id}'). Add a '${kind}' entry for '${src}' to the script.${events}`);
2070
2075
  }
2076
+ const MAX_ADVANCE_STEPS = 1e3;
2071
2077
  async function explore(machine, options, stopWhen) {
2072
2078
  const maxDepth = options.maxDepth ?? 8;
2073
2079
  const maxPaths = options.maxPaths ?? 200;
@@ -2092,7 +2098,7 @@ async function explore(machine, options, stopWhen) {
2092
2098
  if (stopWhen?.(initial.snapshot)) witness = [];
2093
2099
  const advance = (step) => {
2094
2100
  let current = step;
2095
- for (let i = 0; i < 1e3; i++) {
2101
+ for (let i = 0; i < MAX_ADVANCE_STEPS; i++) {
2096
2102
  if (current.done) return { step: current };
2097
2103
  const request = current.requests[0];
2098
2104
  if (request && request.kind === "text") {
@@ -2244,19 +2250,22 @@ async function canReach(machine, statePath, options = {}) {
2244
2250
  } : { canReach: false };
2245
2251
  }
2246
2252
  //#endregion
2247
- //#region src/trajectory.ts
2248
- function isRecord$2(value) {
2253
+ //#region src/internal/is-record.ts
2254
+ /** True for a plain object-like value (arrays excluded). @internal */
2255
+ function isRecord(value) {
2249
2256
  return typeof value === "object" && value !== null && !Array.isArray(value);
2250
2257
  }
2258
+ //#endregion
2259
+ //#region src/trajectory.ts
2251
2260
  /**
2252
2261
  * Classifies one item. Event-shaped things (log entries, events) become events;
2253
2262
  * everything else is a state value. A snapshot is unwrapped to its `value`.
2254
2263
  * @internal
2255
2264
  */
2256
2265
  function normalize(item) {
2257
- if (isRecord$2(item)) {
2266
+ if (isRecord(item)) {
2258
2267
  const nested = item["event"];
2259
- if (isRecord$2(nested) && typeof nested["type"] === "string") return {
2268
+ if (isRecord(nested) && typeof nested["type"] === "string") return {
2260
2269
  kind: "event",
2261
2270
  event: nested
2262
2271
  };
@@ -2277,7 +2286,7 @@ function normalize(item) {
2277
2286
  /** Every leaf of a state value as a dot path (`{ review: 'editing' }` → `review.editing`). @internal */
2278
2287
  function statePaths(value) {
2279
2288
  if (typeof value === "string") return [value];
2280
- if (!isRecord$2(value)) return [];
2289
+ if (!isRecord(value)) return [];
2281
2290
  return Object.entries(value).flatMap(([key, child]) => statePaths(child).map((path) => `${key}.${path}`));
2282
2291
  }
2283
2292
  /**
@@ -2290,12 +2299,13 @@ function stateMatchesPath(path, value) {
2290
2299
  function deepEqual(a, b) {
2291
2300
  if (Object.is(a, b)) return true;
2292
2301
  if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => deepEqual(item, b[index]));
2293
- if (!isRecord$2(a) || !isRecord$2(b)) return false;
2302
+ if (!isRecord(a) || !isRecord(b)) return false;
2294
2303
  const keys = Object.keys(a);
2295
2304
  return keys.length === Object.keys(b).length && keys.every((key) => deepEqual(a[key], b[key]));
2296
2305
  }
2297
2306
  /**
2298
- * Does one actual item satisfy one expected item?
2307
+ * Normalizes ONE expected item into a predicate over actual items, so the
2308
+ * expectation is classified once instead of once per candidate:
2299
2309
  *
2300
2310
  * - A string expectation matches an event's `type`, or a state value it names.
2301
2311
  * - An event expectation matches an event with the same `type` whose other
@@ -2303,13 +2313,22 @@ function deepEqual(a, b) {
2303
2313
  * - A state-value expectation matches structurally.
2304
2314
  * @internal
2305
2315
  */
2306
- function matchesItem(expected, actual) {
2307
- const actualItem = normalize(actual);
2308
- if (typeof expected === "string") return actualItem.kind === "event" ? actualItem.event.type === expected : stateMatchesPath(expected, actualItem.value);
2316
+ function matcherFor(expected) {
2317
+ if (typeof expected === "string") return (actual) => {
2318
+ const actualItem = normalize(actual);
2319
+ return actualItem.kind === "event" ? actualItem.event.type === expected : stateMatchesPath(expected, actualItem.value);
2320
+ };
2309
2321
  const expectedItem = normalize(expected);
2310
- if (expectedItem.kind !== actualItem.kind) return false;
2311
- if (expectedItem.kind === "event" && actualItem.kind === "event") return Object.keys(expectedItem.event).every((key) => deepEqual(expectedItem.event[key], actualItem.event[key]));
2312
- return deepEqual(expectedItem.value, actualItem.value);
2322
+ switch (expectedItem.kind) {
2323
+ case "event": return (actual) => {
2324
+ const actualItem = normalize(actual);
2325
+ return actualItem.kind === "event" && Object.keys(expectedItem.event).every((key) => deepEqual(expectedItem.event[key], actualItem.event[key]));
2326
+ };
2327
+ case "state": return (actual) => {
2328
+ const actualItem = normalize(actual);
2329
+ return actualItem.kind === "state" && deepEqual(expectedItem.value, actualItem.value);
2330
+ };
2331
+ }
2313
2332
  }
2314
2333
  /**
2315
2334
  * Matches a run's trajectory against an expected one.
@@ -2346,59 +2365,41 @@ function matchesItem(expected, actual) {
2346
2365
  */
2347
2366
  function matchesTrajectory(actual, expected, options = {}) {
2348
2367
  const expectedCount = expected.length;
2349
- if (options.exact) {
2350
- let matchedCount = 0;
2351
- let firstMiss;
2352
- for (let index = 0; index < expectedCount; index++) {
2353
- if (index < actual.length && matchesItem(expected[index], actual[index])) {
2354
- matchedCount++;
2355
- continue;
2356
- }
2357
- firstMiss = {
2358
- index,
2359
- expected: expected[index],
2360
- searchedFrom: index
2361
- };
2362
- break;
2363
- }
2364
- const matched = !firstMiss && actual.length === expectedCount;
2365
- const denominator = Math.max(expectedCount, actual.length);
2366
- return {
2367
- matched,
2368
- matchedCount,
2369
- expectedCount,
2370
- score: matched ? 1 : denominator === 0 ? 1 : matchedCount / denominator,
2371
- ...firstMiss ? { firstMiss } : {}
2372
- };
2373
- }
2368
+ const exact = options.exact === true;
2374
2369
  let cursor = 0;
2375
2370
  let matchedCount = 0;
2371
+ let firstMiss;
2376
2372
  for (let index = 0; index < expectedCount; index++) {
2377
2373
  const want = expected[index];
2374
+ const matches = matcherFor(want);
2375
+ const searchedFrom = exact ? index : cursor;
2378
2376
  let found = -1;
2379
- for (let at = cursor; at < actual.length; at++) if (matchesItem(want, actual[at])) {
2380
- found = at;
2381
- break;
2377
+ for (let at = searchedFrom; at < actual.length; at++) {
2378
+ if (matches(actual[at])) {
2379
+ found = at;
2380
+ break;
2381
+ }
2382
+ if (exact) break;
2382
2383
  }
2383
- if (found === -1) return {
2384
- matched: false,
2385
- matchedCount,
2386
- expectedCount,
2387
- score: matchedCount / expectedCount,
2388
- firstMiss: {
2384
+ if (found === -1) {
2385
+ firstMiss = {
2389
2386
  index,
2390
2387
  expected: want,
2391
- searchedFrom: cursor
2392
- }
2393
- };
2388
+ searchedFrom
2389
+ };
2390
+ break;
2391
+ }
2394
2392
  matchedCount++;
2395
2393
  cursor = found + 1;
2396
2394
  }
2395
+ const matched = !firstMiss && (!exact || actual.length === expectedCount);
2396
+ const denominator = exact ? Math.max(expectedCount, actual.length) : expectedCount;
2397
2397
  return {
2398
- matched: true,
2398
+ matched,
2399
2399
  matchedCount,
2400
2400
  expectedCount,
2401
- score: 1
2401
+ score: matched ? 1 : denominator === 0 ? 1 : matchedCount / denominator,
2402
+ ...firstMiss ? { firstMiss } : {}
2402
2403
  };
2403
2404
  }
2404
2405
  //#endregion
@@ -2411,17 +2412,9 @@ function matchesTrajectory(actual, expected, options = {}) {
2411
2412
  * `provideExecutors`, or a bare `TextLogic.execute`) runs with no API key and no
2412
2413
  * network. It is the fastest way to see a machine run, and the least ceremonial
2413
2414
  * way to test one: same machine, same executor contract, scripted answers.
2415
+ *
2416
+ * @module
2414
2417
  */
2415
- /** Thrown when a scripted queue runs dry on a pending request. */
2416
- var ScriptedExecutorsError = class extends AgentError {
2417
- constructor(message) {
2418
- super("scripted-executors-exhausted", message);
2419
- this.name = "ScriptedExecutorsError";
2420
- }
2421
- };
2422
- function isRecord$1(value) {
2423
- return typeof value === "object" && value !== null && !Array.isArray(value);
2424
- }
2425
2418
  /** The only own keys an executor-result envelope carries. @internal */
2426
2419
  const TEXT_ENVELOPE_KEYS = new Set([
2427
2420
  "output",
@@ -2446,7 +2439,22 @@ function isTextEnvelope(value) {
2446
2439
  */
2447
2440
  async function resolveScriptedTextEntry(entry, request, info) {
2448
2441
  const value = typeof entry === "function" ? await entry(request, info) : entry;
2449
- return isRecord$1(value) && isTextEnvelope(value) ? value : { output: value };
2442
+ return isRecord(value) && isTextEnvelope(value) ? value : { output: value };
2443
+ }
2444
+ /**
2445
+ * Names a pending text request in an error message. Shared with `runSeam`.
2446
+ * @internal
2447
+ */
2448
+ function describeText(request) {
2449
+ return request.name ? `'${request.name}' (model '${request.model}')` : `(model '${request.model}')`;
2450
+ }
2451
+ /**
2452
+ * Stream semantics with no model: the whole text lands as one chunk. Shared
2453
+ * with `runSeam`, whose scripted answers stream the same way. @internal
2454
+ */
2455
+ function emitScriptedChunk(result, info) {
2456
+ const output = isRecord(result) ? result["output"] : void 0;
2457
+ if (typeof output === "string") info?.onChunk?.(output);
2450
2458
  }
2451
2459
  /**
2452
2460
  * Keyless executors that replay a script instead of calling a model. Every
@@ -2481,45 +2489,26 @@ function createScriptedExecutors(script = {}) {
2481
2489
  const decisions = [...script.decisions ?? []];
2482
2490
  const text = [...script.text ?? []];
2483
2491
  const nextText = async (request, info) => {
2484
- if (text.length === 0) throw new ScriptedExecutorsError(`createScriptedExecutors: script ran dry on a pending text request ${describeText(request)}. Add another entry to the script's \`text\` queue.`);
2492
+ if (text.length === 0) throw new AgentError("scripted-executors-exhausted", `createScriptedExecutors: script ran dry on a pending text request ${describeText(request)}. Add another entry to the script's \`text\` queue.`);
2485
2493
  return resolveScriptedTextEntry(text.shift(), request, info);
2486
2494
  };
2487
2495
  return {
2488
2496
  generateText: nextText,
2489
2497
  streamText: async (request, info) => {
2490
2498
  const result = await nextText(request, info);
2491
- if (typeof result.output === "string") info?.onChunk?.(result.output);
2499
+ emitScriptedChunk(result, info);
2492
2500
  return result;
2493
2501
  },
2494
2502
  decide: async (request) => {
2495
- if (decisions.length === 0) throw new ScriptedExecutorsError(`createScriptedExecutors: script ran dry on a pending decision request (id '${request.id}'). Add another entry to the script's \`decisions\` queue. Candidate events: ${request.events.map((event) => event.type).join(", ") || "(none)"}.`);
2503
+ if (decisions.length === 0) throw new AgentError("scripted-executors-exhausted", `createScriptedExecutors: script ran dry on a pending decision request (id '${request.id}'). Add another entry to the script's \`decisions\` queue. Candidate events: ${request.events.map((event) => event.type).join(", ") || "(none)"}.`);
2496
2504
  const entry = decisions.shift();
2497
2505
  const value = typeof entry === "function" ? await entry(request) : entry;
2498
- return typeof value.type === "string" ? { event: value } : value;
2506
+ return isRecord(value) && typeof value["type"] !== "string" && "event" in value ? value : { event: value };
2499
2507
  }
2500
2508
  };
2501
2509
  }
2502
- function describeText(request) {
2503
- return request.name ? `'${request.name}'` : `(model '${request.model}')`;
2504
- }
2505
2510
  //#endregion
2506
2511
  //#region src/seam.ts
2507
- /** Thrown when the call plan runs dry on a request that is not the live seam. */
2508
- var SeamScriptError = class extends AgentError {
2509
- constructor(message) {
2510
- super("seam-script-exhausted", message);
2511
- this.name = "SeamScriptError";
2512
- }
2513
- };
2514
- function seamKeyOf(seam) {
2515
- return seam.request ?? seam.model;
2516
- }
2517
- function describeRequest(request) {
2518
- return request.name ? `'${request.name}' (model '${request.model}')` : `(model '${request.model}')`;
2519
- }
2520
- function isRecord(value) {
2521
- return typeof value === "object" && value !== null;
2522
- }
2523
2512
  /**
2524
2513
  * The seam's own answer, for scoring. Our `{ output }` envelope is read
2525
2514
  * directly; a raw AI SDK `generateText` result contributes its `text`. A raw
@@ -2574,40 +2563,50 @@ async function runSeam(machine, options) {
2574
2563
  let seamStateAt = 0;
2575
2564
  let seamEventAt = 0;
2576
2565
  let liveEvents = 0;
2577
- const scriptedAnswer = async (request, info, optional) => {
2578
- const key = request.name !== void 0 && queues.has(request.name) ? request.name : request.model;
2579
- const queue = queues.get(key);
2580
- if (!queue?.length) {
2581
- if (optional) return;
2582
- throw new SeamScriptError(`runSeam: no scripted answer left for request ${describeRequest(request)}. Add an entry to \`scripts.${key}\` — its last entry repeats, so one extra answer covers a longer branch.`);
2583
- }
2566
+ const queueKeyOf = (request) => request.name !== void 0 && queues.has(request.name) ? request.name : request.model;
2567
+ /**
2568
+ * Consumes this request's slot in the call plan, or resolves `undefined` when
2569
+ * its queue is dry. The LAST entry repeats: a live seam that branches further
2570
+ * still finds an answer instead of running dry.
2571
+ */
2572
+ const takeScriptedSlot = async (request, info) => {
2573
+ const queue = queues.get(queueKeyOf(request));
2574
+ if (!queue?.length) return;
2584
2575
  return resolveScriptedTextEntry(queue.length === 1 ? queue[0] : queue.shift(), request, info);
2585
2576
  };
2577
+ const scriptedAnswer = async (request, info) => {
2578
+ const scripted = await takeScriptedSlot(request, info);
2579
+ if (!scripted) throw new AgentError("seam-script-exhausted", `runSeam: no scripted answer left for request ${describeText(request)}. Add an entry to \`scripts.${queueKeyOf(request)}\` — its last entry repeats, so one extra answer covers a longer branch.`);
2580
+ return scripted;
2581
+ };
2586
2582
  const route = async (request, info) => {
2587
2583
  const callIndex = calls++;
2588
2584
  const isSeam = (seam.request !== void 0 ? request.name === seam.request : request.model === seam.model) && seamMatches++ === (seam.occurrence ?? 0);
2589
- const scripted = await scriptedAnswer(request, info, isSeam && candidate !== void 0);
2585
+ if (isSeam && candidate) {
2586
+ await takeScriptedSlot(request, info);
2587
+ seamReached = true;
2588
+ callsBeforeSeam = callIndex;
2589
+ seamStateAt = statePath.length;
2590
+ seamEventAt = liveEvents;
2591
+ const result = await candidate(request, info);
2592
+ seamOutput = await seamOutputOf(result, request);
2593
+ return result;
2594
+ }
2595
+ const scripted = await scriptedAnswer(request, info);
2590
2596
  if (!isSeam) return scripted;
2591
2597
  seamReached = true;
2592
2598
  callsBeforeSeam = callIndex;
2593
2599
  seamStateAt = statePath.length;
2594
2600
  seamEventAt = liveEvents;
2595
- if (!candidate) {
2596
- if (!scripted) throw new SeamScriptError(`runSeam: the seam '${seamKeyOf(seam)}' has no candidate and no scripted answer. Pass \`candidate\`, or script the seam's key.`);
2597
- seamOutput = scripted.output;
2598
- return scripted;
2599
- }
2600
- const result = await candidate(request, info);
2601
- seamOutput = await seamOutputOf(result, request);
2602
- return result;
2601
+ seamOutput = scripted.output;
2602
+ return scripted;
2603
2603
  };
2604
2604
  const executors = {
2605
2605
  ...options.executors,
2606
2606
  generateText: route,
2607
2607
  streamText: async (request, info) => {
2608
2608
  const result = await route(request, info);
2609
- const output = isRecord(result) ? result["output"] : void 0;
2610
- if (typeof output === "string") info?.onChunk?.(output);
2609
+ emitScriptedChunk(result, info);
2611
2610
  return result;
2612
2611
  }
2613
2612
  };