experimental-a2 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/ai-server.browser.js +2 -2
  3. package/dist/ai-server.d.ts +19 -7
  4. package/dist/ai-server.js +730 -96
  5. package/dist/ai.d.ts +32 -11
  6. package/dist/ai.js +253 -75
  7. package/dist/client.d.ts +1 -1
  8. package/dist/client.js +4 -4
  9. package/dist/{contract-B0kAXoaL.js → contract-CG_adnu_.js} +2 -1
  10. package/dist/{contract-DL8btVd9.d.ts → contract-C_3dIIEU.d.ts} +4 -1
  11. package/dist/devtools-server.browser.js +2 -2
  12. package/dist/devtools-server.js +1 -1
  13. package/dist/http.d.ts +1 -1
  14. package/dist/http.js +4 -3
  15. package/dist/idempotent-replay-BMyHrP0L.js +19 -0
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +1 -1
  18. package/dist/{internal-Dm8Ejnud.js → internal-D6wNxTck.js} +3 -3
  19. package/dist/{log-Dg1I8NRr.d.ts → log-ldf5g8Cx.d.ts} +74 -56
  20. package/dist/log-memory.d.ts +1 -1
  21. package/dist/log-memory.js +173 -96
  22. package/dist/{log-polling-RO7kclzR.js → log-polling-6COoN60V.js} +1 -1
  23. package/dist/log-postgres.d.ts +1 -1
  24. package/dist/log-postgres.js +235 -192
  25. package/dist/log-redis.d.ts +1 -1
  26. package/dist/log-redis.js +453 -263
  27. package/dist/log-sqlite.d.ts +1 -1
  28. package/dist/log-sqlite.js +216 -127
  29. package/dist/otel.d.ts +1 -1
  30. package/dist/otel.js +1 -1
  31. package/dist/react.d.ts +1 -1
  32. package/dist/react.js +1 -1
  33. package/dist/recovery-vercel.d.ts +2 -2
  34. package/dist/recovery-vercel.js +9 -10
  35. package/dist/server-DJgD2YWP.js +877 -0
  36. package/dist/server.browser.js +4 -4
  37. package/dist/server.d.ts +46 -27
  38. package/dist/server.js +1 -1
  39. package/dist/{telemetry-C78al20p.d.ts → telemetry-Cso0qyHQ.d.ts} +1 -1
  40. package/dist/{wire-2QpU1EtJ.js → wire-BVsgR8o9.js} +1 -1
  41. package/docs/01-quickstart.mdx +7 -7
  42. package/docs/concepts/01-contracts.mdx +22 -22
  43. package/docs/concepts/02-handlers.mdx +223 -89
  44. package/docs/concepts/03-durability.mdx +199 -112
  45. package/docs/concepts/04-state.mdx +27 -1
  46. package/docs/guides/01-timers.mdx +4 -4
  47. package/docs/guides/02-cancellation.mdx +32 -4
  48. package/docs/guides/05-production.mdx +61 -27
  49. package/docs/guides/06-ai-agents.mdx +151 -70
  50. package/docs/guides/07-devtools.mdx +6 -3
  51. package/docs/guides/08-application-data.mdx +5 -6
  52. package/docs/index.mdx +30 -14
  53. package/docs/reference/01-api.mdx +305 -70
  54. package/package.json +31 -31
  55. package/dist/server-DYsnKTTy.js +0 -780
package/dist/ai.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as assertSyncSchema } from "./validate-XKT4FSNn.js";
2
- import { t as contract } from "./contract-B0kAXoaL.js";
2
+ import { t as contract } from "./contract-CG_adnu_.js";
3
3
  //#region src/ai-projector.ts
4
4
  const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5
5
  const cloneMessage = (messageId, base) => structuredClone(base ?? {
@@ -442,7 +442,7 @@ const completedPayloadSchema = schema("ai.generation.completed", (value) => {
442
442
  return { value };
443
443
  });
444
444
  const compactionPayloadSchema = (messageSchema) => schema("ai.compaction.completed", (value) => {
445
- if (!isRecord(value) || !isJSONCompatible(value) || !stringField(value, "generationId") || !stringField(value, "throughMessageId") || !Array.isArray(value["messages"])) return issue("invalid ai.compaction.completed payload");
445
+ if (!isRecord(value) || !isJSONCompatible(value) || !stringField(value, "generationId") || !stringField(value, "throughMessageId") || !Array.isArray(value["messages"]) || value["retainedMessageIds"] !== void 0 && (!Array.isArray(value["retainedMessageIds"]) || !value["retainedMessageIds"].every((candidate) => typeof candidate === "string"))) return issue("invalid ai.compaction.completed payload");
446
446
  const messages = [];
447
447
  for (const candidate of value["messages"]) {
448
448
  const result = validateMessage(candidate, messageSchema);
@@ -452,7 +452,8 @@ const compactionPayloadSchema = (messageSchema) => schema("ai.compaction.complet
452
452
  return { value: {
453
453
  generationId: value["generationId"],
454
454
  throughMessageId: value["throughMessageId"],
455
- messages
455
+ messages,
456
+ ...value["retainedMessageIds"] === void 0 ? {} : { retainedMessageIds: value["retainedMessageIds"] }
456
457
  } };
457
458
  });
458
459
  function createEvents(options) {
@@ -464,17 +465,18 @@ function createEvents(options) {
464
465
  "ai.message.created": messagePayloadSchema(messageSchema),
465
466
  "ai.message.completed": jsonObject("ai.message.completed", (value) => stringField(value, "messageId")),
466
467
  "ai.message.interrupted": jsonObject("ai.message.interrupted", (value) => stringField(value, "messageId") && optionalStringField(value, "generationId") && optionalStringField(value, "reason") && (value["lastSeenIndex"] === void 0 || typeof value["lastSeenIndex"] === "number" && Number.isInteger(value["lastSeenIndex"]) && value["lastSeenIndex"] >= 0)),
467
- "ai.generation.requested": jsonObject("ai.generation.requested", (value) => stringField(value, "messageId") && optionalStringField(value, "responseMessageId") && (value["reason"] === "message" || value["reason"] === "approval" || value["reason"] === "input" || value["reason"] === "retry")),
468
+ "ai.generation.requested": jsonObject("ai.generation.requested", (value) => stringField(value, "messageId") && optionalStringField(value, "responseMessageId") && (value["reason"] === "message" || value["reason"] === "tool" || value["reason"] === "input" || value["reason"] === "retry")),
469
+ "ai.retry.requested": jsonObject("ai.retry.requested", (value) => stringField(value, "messageId") && stringField(value, "responseMessageId") && stringField(value, "retryId")),
468
470
  "ai.generation.started": jsonObject("ai.generation.started", (value) => stringField(value, "requestId") && stringField(value, "messageId") && stringField(value, "generationId") && stringField(value, "responseMessageId") && typeof value["attempt"] === "number" && Number.isInteger(value["attempt"]) && value["attempt"] > 0 && stringField(value, "model")),
469
471
  "ai.generation.progress": progressPayloadSchema,
470
472
  "ai.generation.completed": completedPayloadSchema,
471
- "ai.generation.failed": jsonObject("ai.generation.failed", (value) => stringField(value, "requestId") && stringField(value, "messageId") && stringField(value, "generationId") && stringField(value, "responseMessageId") && stringField(value, "error") && (value["superseded"] === void 0 || typeof value["superseded"] === "boolean")),
472
- "ai.tool.called": jsonObject("ai.tool.called", (value) => stringField(value, "requestId") && stringField(value, "messageId") && stringField(value, "generationId") && stringField(value, "toolCallId") && stringField(value, "toolName") && (value["dynamic"] === void 0 || typeof value["dynamic"] === "boolean") && (value["providerExecuted"] === void 0 || typeof value["providerExecuted"] === "boolean") && optionalStringField(value, "title")),
473
+ "ai.generation.failed": jsonObject("ai.generation.failed", (value) => stringField(value, "requestId") && stringField(value, "messageId") && stringField(value, "generationId") && stringField(value, "responseMessageId") && stringField(value, "error") && (value["superseded"] === void 0 || typeof value["superseded"] === "boolean") && (value["stepLimit"] === void 0 || typeof value["stepLimit"] === "boolean")),
474
+ "ai.tool.called": jsonObject("ai.tool.called", (value) => stringField(value, "requestId") && stringField(value, "messageId") && stringField(value, "generationId") && stringField(value, "toolCallId") && stringField(value, "toolName") && (value["dynamic"] === void 0 || typeof value["dynamic"] === "boolean") && (value["providerExecuted"] === void 0 || typeof value["providerExecuted"] === "boolean") && (value["supportsDeferredResults"] === void 0 || typeof value["supportsDeferredResults"] === "boolean") && optionalStringField(value, "title")),
473
475
  "ai.tool.result": jsonObject("ai.tool.result", (value) => stringField(value, "requestId") && stringField(value, "messageId") && stringField(value, "generationId") && stringField(value, "toolCallId") && optionalStringField(value, "toolName") && optionalStringField(value, "error") && (value["denied"] === void 0 || typeof value["denied"] === "boolean") && (value["preliminary"] === void 0 || typeof value["preliminary"] === "boolean") && (value["phase"] === void 0 || value["phase"] === "input" || value["phase"] === "execution") && (value["dynamic"] === void 0 || typeof value["dynamic"] === "boolean") && (value["providerExecuted"] === void 0 || typeof value["providerExecuted"] === "boolean")),
474
476
  "ai.approval.requested": jsonObject("ai.approval.requested", (value) => stringField(value, "messageId") && stringField(value, "generationId") && stringField(value, "approvalId") && stringField(value, "toolCallId") && (value["isAutomatic"] === void 0 || typeof value["isAutomatic"] === "boolean") && optionalStringField(value, "signature")),
475
- "ai.approval.responded": jsonObject("ai.approval.responded", (value) => stringField(value, "messageId") && stringField(value, "approvalId") && typeof value["approved"] === "boolean" && optionalStringField(value, "reason")),
476
- "ai.input.requested": jsonObject("ai.input.requested", (value) => stringField(value, "messageId") && optionalStringField(value, "generationId") && stringField(value, "inputId") && stringField(value, "name")),
477
- "ai.input.responded": jsonObject("ai.input.responded", (value) => stringField(value, "messageId") && stringField(value, "inputId") && stringField(value, "name")),
477
+ "ai.approval.responded": jsonObject("ai.approval.responded", (value) => stringField(value, "messageId") && stringField(value, "generationId") && stringField(value, "approvalId") && typeof value["approved"] === "boolean" && optionalStringField(value, "reason")),
478
+ "ai.input.requested": jsonObject("ai.input.requested", (value) => stringField(value, "messageId") && stringField(value, "generationId") && stringField(value, "inputId") && stringField(value, "name")),
479
+ "ai.input.responded": jsonObject("ai.input.responded", (value) => stringField(value, "messageId") && stringField(value, "generationId") && stringField(value, "inputId") && stringField(value, "name")),
478
480
  "ai.compaction.requested": jsonObject("ai.compaction.requested", (value) => stringField(value, "generationId") && stringField(value, "throughMessageId")),
479
481
  "ai.compaction.completed": compactionPayloadSchema(messageSchema)
480
482
  };
@@ -485,7 +487,10 @@ const initialState = () => ({
485
487
  messages: [],
486
488
  status: "idle",
487
489
  activeGeneration: null,
490
+ activeRequestId: null,
491
+ activeResponseMessageId: null,
488
492
  activeProjection: null,
493
+ responseGenerationIds: {},
489
494
  pendingApprovals: [],
490
495
  pendingInputs: [],
491
496
  tools: [],
@@ -522,12 +527,85 @@ const answerApproval = (messages, response) => messages.map((message) => {
522
527
  parts
523
528
  } : message;
524
529
  });
525
- const waitingStatus = (state) => state.pendingApprovals.length > 0 || state.pendingInputs.length > 0 ? "waiting" : "idle";
526
- const projectedMessage = (projection) => projectUIMessage({
527
- messageId: projection.responseMessageId,
528
- ...projection.baseMessage === void 0 ? {} : { base: projection.baseMessage },
529
- chunks: projection.batches.flatMap((batch) => batch.chunks)
530
+ const projectMessageChunks = (messages, messageId, chunks) => messages.map((message) => message.id === messageId ? projectUIMessage({
531
+ messageId,
532
+ base: message,
533
+ chunks
534
+ }) : message);
535
+ const approvalRequestedChunk = (payload) => ({
536
+ type: "tool-approval-request",
537
+ approvalId: payload.approvalId,
538
+ toolCallId: payload.toolCallId,
539
+ ...payload.isAutomatic === void 0 ? {} : { isAutomatic: payload.isAutomatic },
540
+ ...payload.signature === void 0 ? {} : { signature: payload.signature }
530
541
  });
542
+ const approvalRespondedChunk = (payload) => ({
543
+ type: "tool-approval-response",
544
+ approvalId: payload.approvalId,
545
+ approved: payload.approved,
546
+ ...payload.reason === void 0 ? {} : { reason: payload.reason }
547
+ });
548
+ const toolResultChunk = (payload) => {
549
+ if (payload.phase === "input") return {
550
+ type: "tool-input-error",
551
+ toolCallId: payload.toolCallId,
552
+ toolName: payload.toolName ?? "unknown",
553
+ input: payload.rawInput ?? payload.input,
554
+ errorText: payload.error ?? "Invalid tool input",
555
+ ...payload.dynamic === void 0 ? {} : { dynamic: payload.dynamic },
556
+ ...payload.providerExecuted === void 0 ? {} : { providerExecuted: payload.providerExecuted },
557
+ ...payload.providerMetadata === void 0 ? {} : { providerMetadata: payload.providerMetadata },
558
+ ...payload.toolMetadata === void 0 ? {} : { toolMetadata: payload.toolMetadata }
559
+ };
560
+ if (payload.denied === true) return {
561
+ type: "tool-output-denied",
562
+ toolCallId: payload.toolCallId
563
+ };
564
+ if (payload.error !== void 0) return {
565
+ type: "tool-output-error",
566
+ toolCallId: payload.toolCallId,
567
+ errorText: payload.error,
568
+ ...payload.dynamic === void 0 ? {} : { dynamic: payload.dynamic },
569
+ ...payload.providerExecuted === void 0 ? {} : { providerExecuted: payload.providerExecuted },
570
+ ...payload.providerMetadata === void 0 ? {} : { providerMetadata: payload.providerMetadata },
571
+ ...payload.toolMetadata === void 0 ? {} : { toolMetadata: payload.toolMetadata }
572
+ };
573
+ return {
574
+ type: "tool-output-available",
575
+ toolCallId: payload.toolCallId,
576
+ output: payload.output,
577
+ ...payload.preliminary === void 0 ? {} : { preliminary: payload.preliminary },
578
+ ...payload.dynamic === void 0 ? {} : { dynamic: payload.dynamic },
579
+ ...payload.providerExecuted === void 0 ? {} : { providerExecuted: payload.providerExecuted },
580
+ ...payload.providerMetadata === void 0 ? {} : { providerMetadata: payload.providerMetadata },
581
+ ...payload.toolMetadata === void 0 ? {} : { toolMetadata: payload.toolMetadata }
582
+ };
583
+ };
584
+ const waitingStatus = (state) => {
585
+ if (state.pendingApprovals.length > 0 || state.pendingInputs.length > 0) return "waiting";
586
+ return state.activeGeneration !== null || state.tools.some((tool) => tool.status === "running") ? "generating" : "idle";
587
+ };
588
+ const lifecycleStatus = (previous, next) => previous.status === "failed" ? "failed" : waitingStatus(next);
589
+ const isStaleGeneration = (state, messageId, generationId) => {
590
+ const current = state.responseGenerationIds[messageId];
591
+ return current !== void 0 && current !== generationId;
592
+ };
593
+ const projectedMessage = (projection) => {
594
+ const projectionEvents = [...projection.batches];
595
+ for (const event of projection.toolEvents) if (event.type === "ai.tool.result") projectionEvents.push({
596
+ index: event.index,
597
+ chunks: [toolResultChunk(event.payload)]
598
+ });
599
+ for (const event of projection.approvalEvents) projectionEvents.push({
600
+ index: event.index,
601
+ chunks: [event.type === "ai.approval.requested" ? approvalRequestedChunk(event.payload) : approvalRespondedChunk(event.payload)]
602
+ });
603
+ return projectUIMessage({
604
+ messageId: projection.responseMessageId,
605
+ ...projection.baseMessage === void 0 ? {} : { base: projection.baseMessage },
606
+ chunks: projectionEvents.toSorted((left, right) => left.index - right.index).flatMap((event) => event.chunks)
607
+ });
608
+ };
531
609
  const reduceToolActivity = (tools, event) => {
532
610
  if (event.type === "ai.tool.called") {
533
611
  const payload = event.payload;
@@ -537,10 +615,10 @@ const reduceToolActivity = (tools, event) => {
537
615
  ...providerMetadata === void 0 ? {} : { callProviderMetadata: providerMetadata },
538
616
  status: "running"
539
617
  };
540
- return [...tools.filter((candidate) => candidate.toolCallId !== payload.toolCallId), tool];
618
+ return [...tools.filter((candidate) => candidate.generationId !== payload.generationId || candidate.toolCallId !== payload.toolCallId), tool];
541
619
  }
542
620
  const payload = event.payload;
543
- const existing = tools.find((candidate) => candidate.toolCallId === payload.toolCallId);
621
+ const existing = tools.find((candidate) => candidate.generationId === payload.generationId && candidate.toolCallId === payload.toolCallId);
544
622
  const toolName = payload.toolName ?? existing?.toolName;
545
623
  const input = payload.input === void 0 ? existing?.input : payload.input;
546
624
  const dynamic = payload.dynamic ?? existing?.dynamic;
@@ -567,15 +645,19 @@ const reduceToolActivity = (tools, event) => {
567
645
  ...existing?.title === void 0 ? {} : { title: existing.title },
568
646
  status: payload.denied ? "denied" : payload.error ? "failed" : payload.preliminary ? "running" : "completed"
569
647
  };
570
- return [...tools.filter((candidate) => candidate.toolCallId !== payload.toolCallId), tool];
648
+ return [...tools.filter((candidate) => candidate.generationId !== payload.generationId || candidate.toolCallId !== payload.toolCallId), tool];
571
649
  };
572
650
  const projectedTools = (projection) => projection.toolEvents.reduce((tools, event) => reduceToolActivity(tools, event), projection.baseTools);
573
- const stopGenerationTools = (tools, generationId, responseMessageId, error) => tools.map((tool) => (tool.generationId === generationId || tool.messageId === responseMessageId) && tool.status === "running" ? {
651
+ const stopGenerationTools = (tools, generationId, responseMessageId, error) => tools.map((tool) => tool.generationId === generationId || tool.messageId === responseMessageId ? {
574
652
  ...tool,
575
653
  status: "failed",
576
654
  error,
577
655
  preliminary: false
578
656
  } : tool);
657
+ const generationRequestId = (generationId) => {
658
+ const markerIndex = generationId.lastIndexOf(":generation:");
659
+ return markerIndex === -1 ? void 0 : generationId.slice(0, markerIndex);
660
+ };
579
661
  /** Pure projection of the built-in AI protocol. Unknown extension events are ignored. */
580
662
  function reduceAIState(state, event) {
581
663
  if (state.status === "closed") return state;
@@ -594,6 +676,8 @@ function reduceAIState(state, event) {
594
676
  ...state,
595
677
  status: "closed",
596
678
  activeGeneration: null,
679
+ activeRequestId: null,
680
+ activeResponseMessageId: null,
597
681
  activeProjection: null
598
682
  };
599
683
  case "ai.message.created": {
@@ -605,27 +689,46 @@ function reduceAIState(state, event) {
605
689
  }
606
690
  case "ai.generation.requested": {
607
691
  const payload = event.payload;
692
+ const schedulesToolContinuation = payload.reason === "tool";
693
+ const responseMessageId = payload.responseMessageId ?? (payload.reason === "message" ? `${payload.messageId}:assistant` : payload.messageId);
694
+ const ownerGenerationId = state.responseGenerationIds[responseMessageId] ?? state.activeGeneration?.generationId;
695
+ const ownerRequestId = ownerGenerationId === void 0 ? void 0 : generationRequestId(ownerGenerationId);
696
+ const requestId = event.id;
697
+ if (!(payload.reason === "message" || (payload.reason === "tool" ? ownerGenerationId !== void 0 && requestId === `ai.generate:tools:${ownerGenerationId}` && state.activeRequestId === ownerRequestId && state.status !== "failed" : payload.reason === "retry" ? state.status === "failed" : ownerGenerationId !== void 0 && requestId?.startsWith(`ai.generate:input:${encodeURIComponent(responseMessageId)}:${encodeURIComponent(ownerGenerationId)}:`) === true && state.activeRequestId === ownerRequestId && state.status !== "failed"))) return state;
608
698
  return {
609
699
  ...state,
610
700
  messages: payload.reason === "retry" && payload.responseMessageId !== void 0 ? state.messages.filter((message) => message.id !== payload.responseMessageId) : state.messages,
611
- status: "generating",
612
- error: null
701
+ status: schedulesToolContinuation ? state.status : "generating",
702
+ activeRequestId: requestId ?? null,
703
+ activeResponseMessageId: responseMessageId,
704
+ error: schedulesToolContinuation ? state.error : null
613
705
  };
614
706
  }
615
707
  case "ai.generation.started": {
616
708
  const payload = event.payload;
709
+ if (state.activeRequestId !== null && state.activeRequestId !== payload.requestId) return state;
710
+ const currentOwner = state.responseGenerationIds[payload.responseMessageId];
711
+ const marker = ":generation:";
712
+ const currentAttempt = currentOwner?.startsWith(`${payload.requestId}${marker}`) ? Number(currentOwner.slice(`${payload.requestId}${marker}`.length)) : void 0;
713
+ if (currentAttempt !== void 0 && Number.isFinite(currentAttempt) && currentAttempt >= payload.attempt) return state;
617
714
  const baseMessage = state.messages.find((message) => message.id === payload.responseMessageId);
618
715
  return {
619
716
  ...state,
620
717
  status: "generating",
621
718
  activeGeneration: payload,
719
+ activeResponseMessageId: null,
720
+ responseGenerationIds: {
721
+ ...state.responseGenerationIds,
722
+ [payload.responseMessageId]: payload.generationId
723
+ },
622
724
  activeProjection: {
623
725
  generationId: payload.generationId,
624
726
  responseMessageId: payload.responseMessageId,
625
727
  ...baseMessage === void 0 ? {} : { baseMessage },
626
728
  batches: [],
627
729
  baseTools: state.tools,
628
- toolEvents: []
730
+ toolEvents: [],
731
+ approvalEvents: []
629
732
  },
630
733
  error: null
631
734
  };
@@ -668,13 +771,16 @@ function reduceAIState(state, event) {
668
771
  }
669
772
  case "ai.generation.failed": {
670
773
  const payload = event.payload;
671
- if (state.activeGeneration?.generationId !== payload.generationId) return state;
774
+ if (state.activeGeneration?.generationId !== payload.generationId && !(payload.stepLimit === true && state.responseGenerationIds[payload.responseMessageId] === payload.generationId)) return state;
672
775
  if (payload.superseded === true) return {
673
776
  ...state,
674
777
  messages: state.messages.filter((message) => message.id !== payload.responseMessageId),
675
778
  tools: state.tools.filter((tool) => tool.generationId !== payload.generationId),
779
+ pendingApprovals: state.pendingApprovals.filter((approval) => approval.generationId !== payload.generationId),
780
+ pendingInputs: state.pendingInputs.filter((input) => input.generationId !== payload.generationId),
676
781
  activeGeneration: null,
677
- activeProjection: null
782
+ activeProjection: null,
783
+ compaction: state.compaction?.generationId === payload.generationId ? null : state.compaction
678
784
  };
679
785
  const projection = state.activeProjection;
680
786
  const shouldProject = projection?.generationId === payload.generationId && (projection.baseMessage !== void 0 || projection.batches.length > 0);
@@ -683,7 +789,9 @@ function reduceAIState(state, event) {
683
789
  messages: shouldProject ? upsertMessage(state.messages, interruptUIMessage(projectedMessage(projection), `Generation failed before tool completion: ${payload.error}`)) : state.messages,
684
790
  status: "failed",
685
791
  activeGeneration: null,
792
+ activeResponseMessageId: null,
686
793
  activeProjection: null,
794
+ compaction: state.compaction?.generationId === payload.generationId ? null : state.compaction,
687
795
  pendingApprovals: state.pendingApprovals.filter((approval) => approval.generationId !== payload.generationId),
688
796
  tools: stopGenerationTools(state.tools, payload.generationId, payload.responseMessageId, payload.error),
689
797
  error: payload.error
@@ -692,32 +800,72 @@ function reduceAIState(state, event) {
692
800
  case "ai.message.interrupted": {
693
801
  const payload = event.payload;
694
802
  const active = state.activeGeneration;
695
- if (active === null || (payload.generationId === void 0 ? payload.messageId !== active.responseMessageId : payload.generationId !== active.generationId)) return state;
803
+ if (active === null) {
804
+ if (payload.generationId === void 0 && state.activeRequestId !== null && state.activeResponseMessageId === payload.messageId) return {
805
+ ...state,
806
+ status: "idle",
807
+ activeRequestId: null,
808
+ activeResponseMessageId: null,
809
+ error: null
810
+ };
811
+ if (!(state.pendingApprovals.some((approval) => approval.messageId === payload.messageId) || state.pendingInputs.some((input) => input.messageId === payload.messageId) || state.tools.some((tool) => tool.messageId === payload.messageId && tool.status === "running"))) return state;
812
+ const next = {
813
+ ...state,
814
+ activeRequestId: null,
815
+ activeResponseMessageId: null,
816
+ messages: state.messages.map((message) => message.id === payload.messageId ? interruptUIMessage(message) : message),
817
+ pendingApprovals: state.pendingApprovals.filter((approval) => approval.messageId !== payload.messageId),
818
+ pendingInputs: state.pendingInputs.filter((input) => input.messageId !== payload.messageId),
819
+ tools: stopGenerationTools(state.tools, payload.generationId ?? "", payload.messageId, "Tool execution was interrupted.")
820
+ };
821
+ return {
822
+ ...next,
823
+ status: lifecycleStatus(state, next)
824
+ };
825
+ }
826
+ if (payload.generationId === void 0 ? payload.messageId !== active.responseMessageId : payload.generationId !== active.generationId) return state;
696
827
  const projection = state.activeProjection;
697
828
  const cutoff = payload.lastSeenIndex ?? Number.POSITIVE_INFINITY;
698
829
  const retained = projection?.generationId === active.generationId ? {
699
830
  ...projection,
700
831
  batches: projection.batches.filter((batch) => batch.index <= cutoff),
701
- toolEvents: projection.toolEvents.filter((toolEvent) => toolEvent.index <= cutoff)
832
+ toolEvents: projection.toolEvents.filter((toolEvent) => toolEvent.index <= cutoff),
833
+ approvalEvents: projection.approvalEvents.filter((approvalEvent) => approvalEvent.index <= cutoff)
702
834
  } : null;
703
835
  const shouldProject = retained !== null && (retained.baseMessage !== void 0 || retained.batches.length > 0);
704
836
  const next = {
705
837
  ...state,
706
838
  messages: shouldProject ? upsertMessage(state.messages, interruptUIMessage(projectedMessage(retained))) : state.messages,
707
839
  activeGeneration: null,
840
+ activeRequestId: null,
841
+ activeResponseMessageId: null,
708
842
  activeProjection: null,
709
843
  pendingApprovals: state.pendingApprovals.filter((approval) => approval.generationId !== active.generationId),
710
844
  tools: stopGenerationTools(retained === null ? state.tools : projectedTools(retained), active.generationId, active.responseMessageId, "Tool execution was interrupted.")
711
845
  };
712
846
  return {
713
847
  ...next,
714
- status: waitingStatus(next)
848
+ status: lifecycleStatus(state, next)
715
849
  };
716
850
  }
717
851
  case "ai.tool.called": {
718
852
  const payload = event.payload;
719
853
  const projection = state.activeProjection;
720
- if (projection?.generationId !== payload.generationId) return state;
854
+ if (isStaleGeneration(state, payload.messageId, payload.generationId)) return state;
855
+ if (state.tools.some((tool) => tool.generationId === payload.generationId && tool.toolCallId === payload.toolCallId && tool.status === "failed" && state.activeProjection?.generationId !== payload.generationId)) return state;
856
+ if (projection?.generationId !== payload.generationId) {
857
+ const next = {
858
+ ...state,
859
+ tools: reduceToolActivity(state.tools, {
860
+ type: "ai.tool.called",
861
+ payload
862
+ })
863
+ };
864
+ return {
865
+ ...next,
866
+ status: lifecycleStatus(state, next)
867
+ };
868
+ }
721
869
  const activeProjection = {
722
870
  ...projection,
723
871
  toolEvents: [...projection.toolEvents, {
@@ -735,7 +883,22 @@ function reduceAIState(state, event) {
735
883
  case "ai.tool.result": {
736
884
  const payload = event.payload;
737
885
  const projection = state.activeProjection;
738
- if (projection?.generationId !== payload.generationId) return state;
886
+ if (isStaleGeneration(state, payload.messageId, payload.generationId)) return state;
887
+ if (state.tools.some((tool) => tool.generationId === payload.generationId && tool.toolCallId === payload.toolCallId && tool.status === "failed" && state.activeProjection?.generationId !== payload.generationId)) return state;
888
+ if (projection?.generationId !== payload.generationId) {
889
+ const next = {
890
+ ...state,
891
+ messages: projectMessageChunks(state.messages, payload.messageId, [toolResultChunk(payload)]),
892
+ tools: reduceToolActivity(state.tools, {
893
+ type: "ai.tool.result",
894
+ payload
895
+ })
896
+ };
897
+ return {
898
+ ...next,
899
+ status: lifecycleStatus(state, next)
900
+ };
901
+ }
739
902
  const activeProjection = {
740
903
  ...projection,
741
904
  toolEvents: [...projection.toolEvents, {
@@ -746,50 +909,88 @@ function reduceAIState(state, event) {
746
909
  };
747
910
  return {
748
911
  ...state,
912
+ messages: upsertMessage(state.messages, projectedMessage(activeProjection)),
749
913
  activeProjection,
750
914
  tools: projectedTools(activeProjection)
751
915
  };
752
916
  }
753
917
  case "ai.approval.requested": {
754
918
  const payload = event.payload;
755
- return {
919
+ const projection = state.activeProjection;
920
+ if (isStaleGeneration(state, payload.messageId, payload.generationId)) return state;
921
+ if (state.tools.some((tool) => tool.generationId === payload.generationId && tool.toolCallId === payload.toolCallId && tool.status === "failed" && projection?.generationId !== payload.generationId)) return state;
922
+ const activeProjection = projection?.generationId === payload.generationId ? {
923
+ ...projection,
924
+ approvalEvents: [...projection.approvalEvents, {
925
+ index: event.index,
926
+ type: "ai.approval.requested",
927
+ payload
928
+ }]
929
+ } : projection;
930
+ const next = {
756
931
  ...state,
757
- pendingApprovals: [...state.pendingApprovals.filter((approval) => approval.approvalId !== payload.approvalId), payload]
932
+ messages: activeProjection === null || activeProjection === projection ? projectMessageChunks(state.messages, payload.messageId, [approvalRequestedChunk(payload)]) : upsertMessage(state.messages, projectedMessage(activeProjection)),
933
+ activeProjection,
934
+ pendingApprovals: payload.isAutomatic === true ? state.pendingApprovals : [...state.pendingApprovals.filter((approval) => approval.approvalId !== payload.approvalId || approval.messageId !== payload.messageId), payload]
935
+ };
936
+ return {
937
+ ...next,
938
+ status: lifecycleStatus(state, next)
758
939
  };
759
940
  }
760
941
  case "ai.approval.responded": {
761
942
  const payload = event.payload;
943
+ if (isStaleGeneration(state, payload.messageId, payload.generationId)) return state;
944
+ const projection = state.activeProjection;
945
+ const pendingRequest = state.pendingApprovals.find((candidate) => candidate.approvalId === payload.approvalId && candidate.messageId === payload.messageId && candidate.generationId === payload.generationId);
946
+ const activeRequest = projection?.approvalEvents.findLast((candidate) => candidate.type === "ai.approval.requested" && candidate.payload.approvalId === payload.approvalId && candidate.payload.messageId === payload.messageId && candidate.payload.generationId === payload.generationId);
947
+ if (state.status === "failed" && pendingRequest === void 0 && activeRequest === void 0) return state;
948
+ const activeProjection = projection !== null && activeRequest !== void 0 ? {
949
+ ...projection,
950
+ approvalEvents: [...projection.approvalEvents, {
951
+ index: event.index,
952
+ type: "ai.approval.responded",
953
+ payload
954
+ }]
955
+ } : projection;
762
956
  const next = {
763
957
  ...state,
764
- messages: answerApproval(state.messages, payload),
765
- pendingApprovals: state.pendingApprovals.filter((approval) => approval.approvalId !== payload.approvalId)
958
+ messages: activeProjection === null || activeProjection === projection ? answerApproval(state.messages, payload) : upsertMessage(state.messages, projectedMessage(activeProjection)),
959
+ activeProjection,
960
+ pendingApprovals: state.pendingApprovals.filter((approval) => approval.approvalId !== payload.approvalId || approval.messageId !== payload.messageId || approval.generationId !== payload.generationId)
766
961
  };
767
962
  return {
768
963
  ...next,
769
- status: waitingStatus(next)
964
+ status: lifecycleStatus(state, next)
770
965
  };
771
966
  }
772
967
  case "ai.input.requested": {
773
968
  const payload = event.payload;
774
- return {
969
+ if (isStaleGeneration(state, payload.messageId, payload.generationId)) return state;
970
+ const next = {
775
971
  ...state,
776
- pendingInputs: [...state.pendingInputs.filter((input) => input.inputId !== payload.inputId), payload],
777
- status: "waiting"
972
+ pendingInputs: [...state.pendingInputs.filter((input) => input.messageId !== payload.messageId || input.generationId !== payload.generationId || input.inputId !== payload.inputId), payload]
973
+ };
974
+ return {
975
+ ...next,
976
+ status: lifecycleStatus(state, next)
778
977
  };
779
978
  }
780
979
  case "ai.input.responded": {
781
980
  const payload = event.payload;
981
+ if (isStaleGeneration(state, payload.messageId, payload.generationId)) return state;
782
982
  const next = {
783
983
  ...state,
784
- pendingInputs: state.pendingInputs.filter((input) => input.inputId !== payload.inputId)
984
+ pendingInputs: state.pendingInputs.filter((input) => input.inputId !== payload.inputId || input.messageId !== payload.messageId || input.generationId !== payload.generationId)
785
985
  };
786
986
  return {
787
987
  ...next,
788
- status: waitingStatus(next)
988
+ status: lifecycleStatus(state, next)
789
989
  };
790
990
  }
791
991
  case "ai.compaction.requested": {
792
992
  const payload = event.payload;
993
+ if (state.activeGeneration?.generationId !== payload.generationId) return state;
793
994
  return {
794
995
  ...state,
795
996
  compaction: {
@@ -800,6 +1001,7 @@ function reduceAIState(state, event) {
800
1001
  }
801
1002
  case "ai.compaction.completed": {
802
1003
  const payload = event.payload;
1004
+ if (state.activeGeneration?.generationId !== payload.generationId || state.compaction?.status !== "running" || state.compaction.generationId !== payload.generationId) return state;
803
1005
  return {
804
1006
  ...state,
805
1007
  compaction: {
@@ -820,19 +1022,10 @@ function deriveUIMessages(history) {
820
1022
  /** Build the standard AI state projection for any contract containing the protocol. */
821
1023
  function createReducer(options) {
822
1024
  return options.contract.reducer({
823
- name: options.name ?? "a2.ai.state.v2",
1025
+ name: options.name ?? "a2.ai.state.v6",
824
1026
  initialState: initialState()
825
1027
  }).fold((state, event) => reduceAIState(state, event));
826
1028
  }
827
- const generationRequest = (options) => ({
828
- type: "ai.generation.requested",
829
- id: options.id,
830
- payload: {
831
- messageId: options.messageId,
832
- reason: options.reason,
833
- ...options.responseMessageId === void 0 ? {} : { responseMessageId: options.responseMessageId }
834
- }
835
- });
836
1029
  const seedMessage = (message) => [{
837
1030
  type: "ai.message.created",
838
1031
  id: `ai.message:${message.id}`,
@@ -841,51 +1034,36 @@ const seedMessage = (message) => [{
841
1034
  /** Pure append inputs for the built-in AI protocol. */
842
1035
  const inputs = {
843
1036
  message(message) {
844
- const messageInputs = seedMessage(message);
845
- if (message.role !== "user") return messageInputs;
846
- return [...messageInputs, generationRequest({
847
- id: `ai.generate:message:${message.id}`,
848
- messageId: message.id,
849
- reason: "message"
850
- })];
1037
+ return seedMessage(message);
851
1038
  },
852
1039
  seed: seedMessage,
853
1040
  approval(response) {
854
1041
  return [{
855
1042
  type: "ai.approval.responded",
856
- id: `ai.approval:${response.approvalId}:response`,
1043
+ id: `ai.approval:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.approvalId)}:response`,
857
1044
  payload: response
858
- }, generationRequest({
859
- id: `ai.generate:approval:${response.approvalId}`,
860
- messageId: response.messageId,
861
- reason: "approval"
862
- })];
1045
+ }];
863
1046
  },
864
1047
  input(response) {
865
1048
  return [{
866
1049
  type: "ai.input.responded",
867
- id: `ai.input:${response.inputId}:response`,
1050
+ id: `ai.input:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.inputId)}:response`,
868
1051
  payload: response
869
- }, generationRequest({
870
- id: `ai.generate:input:${response.inputId}`,
871
- messageId: response.messageId,
872
- reason: "input"
873
- })];
1052
+ }];
874
1053
  },
875
1054
  requestInput(request) {
876
1055
  return [{
877
1056
  type: "ai.input.requested",
878
- id: `ai.input:${request.inputId}:request`,
1057
+ id: `ai.input:${encodeURIComponent(request.messageId)}:${encodeURIComponent(request.generationId)}:${encodeURIComponent(request.inputId)}:request`,
879
1058
  payload: request
880
1059
  }];
881
1060
  },
882
1061
  retry(retryOptions) {
883
- return [generationRequest({
884
- id: `ai.generate:retry:${retryOptions.retryId}`,
885
- messageId: retryOptions.messageId,
886
- responseMessageId: retryOptions.responseMessageId,
887
- reason: "retry"
888
- })];
1062
+ return [{
1063
+ type: "ai.retry.requested",
1064
+ id: `ai.retry:${retryOptions.retryId}`,
1065
+ payload: retryOptions
1066
+ }];
889
1067
  },
890
1068
  interrupt(interruption) {
891
1069
  return [{
@@ -913,7 +1091,7 @@ function agent(options) {
913
1091
  return {
914
1092
  contract: agentContract,
915
1093
  reducer: agentContract.reducer({
916
- name: options.reducerName ?? "a2.ai.state.v2",
1094
+ name: options.reducerName ?? "a2.ai.state.v6",
917
1095
  initialState: initialState()
918
1096
  }).fold((state, event) => reduceAIState(state, event))
919
1097
  };
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as EventDefs, r as ContractEvent, s as Reducer, t as AppendInput } from "./contract-DL8btVd9.js";
1
+ import { a as EventDefs, c as Reducer, r as ContractEvent, t as AppendInput } from "./contract-C_3dIIEU.js";
2
2
  //#region src/client.d.ts
3
3
  type ConnectionStatus = "idle" | "connecting" | "live" | "closed";
4
4
  /**
package/dist/client.js CHANGED
@@ -1,16 +1,16 @@
1
1
  import { n as validateSync } from "./validate-XKT4FSNn.js";
2
2
  import { t as A2Error } from "./errors-BJRMd-h6.js";
3
- import { r as STREAM_TIMINGS } from "./internal-Dm8Ejnud.js";
4
- import { i as eventFromWire, o as isWireEvent, t as errorFromWire } from "./wire-2QpU1EtJ.js";
3
+ import { r as STREAM_TIMINGS } from "./internal-D6wNxTck.js";
4
+ import { i as eventFromWire, o as isWireEvent, t as errorFromWire } from "./wire-BVsgR8o9.js";
5
5
  //#region src/client.ts
6
6
  /**
7
- * a2/client — the framework-agnostic session client.
7
+ * experimental-a2/client — the framework-agnostic session client.
8
8
  *
9
9
  * Everything the browser needs to read a session live and push
10
10
  * optimistically, with no framework attached: the SSE subscription with
11
11
  * frontier resume and reconnection, the optimistic push queue with
12
12
  * ack/rollback, and the local fold through the same reducer the server
13
- * uses. `a2/react`'s `createReact` is a thin binding over it, and the
13
+ * uses. `experimental-a2/react`'s `createReact` is a thin binding over it, and the
14
14
  * store contract (`subscribe`/`getSnapshot`) is exactly what
15
15
  * `useSyncExternalStore` wants.
16
16
  *
@@ -25,7 +25,7 @@ function makeReducerBuilder(events, options) {
25
25
  //#region src/contract.ts
26
26
  /**
27
27
  * The contract — a2's primary noun. A named event vocabulary, shared by
28
- * everything: the server implements it (a2/server's `createServer`),
28
+ * everything: the server implements it (experimental-a2/server's `createServer`),
29
29
  * reducers derive from it (`contract.reducer`), the browser types its
30
30
  * pushes off it. Isomorphic by construction: this module imports
31
31
  * nothing but schema plumbing.
@@ -51,6 +51,7 @@ function contract(options) {
51
51
  return {
52
52
  name,
53
53
  events,
54
+ batch: (...items) => items,
54
55
  reducer(reducerOptions) {
55
56
  return makeReducerBuilder(events, reducerOptions);
56
57
  }
@@ -110,6 +110,7 @@ type AppendInput<D extends EventDefs> = { [T in keyof D & string]: {
110
110
  payload: StandardSchemaV1.InferInput<D[T]>;
111
111
  id?: string;
112
112
  }; }[keyof D & string];
113
+ type EventBatchFactory<D extends EventDefs> = <const E extends readonly AppendInput<D>[]>(...events: E) => E;
113
114
  type ReducerOptions<Seed, S> = {
114
115
  /**
115
116
  * Identity and snapshot cache key. Rename it when the fold's logic
@@ -134,6 +135,8 @@ type Contract<D extends EventDefs = EventDefs> = {
134
135
  readonly name: string;
135
136
  /** The vocabulary. */
136
137
  readonly events: Readonly<D>;
138
+ /** Preserve literal types for a handler-returned event batch. */
139
+ readonly batch: EventBatchFactory<D>;
137
140
  /**
138
141
  * Derive a view from this contract. The reducer's `name` is its
139
142
  * identity and snapshot cache key: rename it when the fold's logic
@@ -158,4 +161,4 @@ declare function contract<D extends EventDefs>(options: {
158
161
  events: D;
159
162
  }): Contract<D>;
160
163
  //#endregion
161
- export { ReducerOptions as a, ReducerBuilder as c, EventDefs as i, StandardSchemaV1 as l, Contract as n, contract as o, ContractEvent as r, Reducer as s, AppendInput as t };
164
+ export { EventDefs as a, Reducer as c, EventBatchFactory as i, ReducerBuilder as l, Contract as n, ReducerOptions as o, ContractEvent as r, contract as s, AppendInput as t, StandardSchemaV1 as u };