@ekairos/events 1.22.39-beta.development.0 → 1.22.40-beta.development.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 (45) hide show
  1. package/README.md +5 -3
  2. package/dist/codex.d.ts +11 -2
  3. package/dist/codex.js +13 -5
  4. package/dist/context.action.d.ts +55 -0
  5. package/dist/context.action.js +25 -0
  6. package/dist/context.builder.d.ts +52 -42
  7. package/dist/context.builder.js +29 -24
  8. package/dist/context.d.ts +2 -1
  9. package/dist/context.engine.d.ts +50 -47
  10. package/dist/context.engine.js +222 -173
  11. package/dist/context.events.js +28 -87
  12. package/dist/context.js +1 -0
  13. package/dist/context.part-identity.d.ts +40 -0
  14. package/dist/context.part-identity.js +268 -0
  15. package/dist/context.parts.d.ts +389 -164
  16. package/dist/context.parts.js +235 -224
  17. package/dist/context.registry.d.ts +1 -1
  18. package/dist/context.runtime.d.ts +10 -4
  19. package/dist/context.runtime.js +7 -1
  20. package/dist/context.step-stream.d.ts +16 -2
  21. package/dist/context.step-stream.js +58 -16
  22. package/dist/context.stream.d.ts +4 -0
  23. package/dist/context.stream.js +23 -1
  24. package/dist/context.toolcalls.d.ts +8 -20
  25. package/dist/context.toolcalls.js +61 -55
  26. package/dist/index.d.ts +8 -4
  27. package/dist/index.js +5 -3
  28. package/dist/reactors/ai-sdk.chunk-map.js +27 -0
  29. package/dist/reactors/ai-sdk.reactor.d.ts +8 -9
  30. package/dist/reactors/ai-sdk.reactor.js +2 -5
  31. package/dist/reactors/ai-sdk.step.d.ts +2 -3
  32. package/dist/reactors/ai-sdk.step.js +10 -7
  33. package/dist/reactors/scripted.reactor.d.ts +7 -4
  34. package/dist/reactors/types.d.ts +8 -8
  35. package/dist/schema.d.ts +273 -2
  36. package/dist/schema.js +1 -1
  37. package/dist/steps/store.steps.d.ts +51 -12
  38. package/dist/steps/store.steps.js +137 -0
  39. package/dist/steps/stream.steps.d.ts +15 -0
  40. package/dist/steps/stream.steps.js +16 -5
  41. package/dist/steps/trace.steps.d.ts +4 -4
  42. package/dist/steps/trace.steps.js +21 -6
  43. package/dist/tools-to-model-tools.d.ts +4 -2
  44. package/dist/tools-to-model-tools.js +30 -11
  45. package/package.json +7 -6
@@ -2,38 +2,18 @@ import { getContextRuntimeServices } from "./context.runtime.js";
2
2
  import { OUTPUT_ITEM_TYPE, WEB_CHANNEL } from "./context.events.js";
3
3
  import { applyToolExecutionResultToParts } from "./context.toolcalls.js";
4
4
  import { isContextPartEnvelope, normalizePartsForPersistence, } from "./context.parts.js";
5
- import { actionsToActionSpecs } from "./tools-to-model-tools.js";
6
5
  import { createAiSdkReactor, } from "./context.reactor.js";
7
- import { abortPersistedContextStepStream, closePersistedContextStepStream, createPersistedContextStepStream, closeContextStream, } from "./steps/stream.steps.js";
8
- import { completeExecution, createContextStep, getContextItems, initializeContext, saveTriggerAndCreateExecution, saveContextPartsStep, updateContextContent, updateContextReactor, updateContextStatus, updateItem, updateContextStep, } from "./steps/store.steps.js";
6
+ import { abortPersistedContextStepStream, closeContextStream, createPersistedContextStepStreamForRuntime, finalizePersistedContextStepStreamForRuntime, } from "./steps/stream.steps.js";
7
+ import { completeExecution, createContextStep, finalizeReactionStep, getContextItems, initializeContext, openReactionStep, saveTriggerAndCreateExecution, saveContextPartsAndUpdateReaction, saveContextPartsStep, updateContextContent, updateContextReactor, updateContextStatus, updateItem, updateContextStep, } from "./steps/store.steps.js";
9
8
  import { getClientResumeHookUrl, toolApprovalHookToken, toolApprovalWebhookToken, } from "./context.hooks.js";
10
9
  import { getContextDurableWorkflow } from "./context.durable.js";
11
10
  export async function runContextReactionDirect(context, triggerEvent, params) {
12
11
  return await ContextEngine.runDirect(context, triggerEvent, params);
13
12
  }
14
- async function legacyRuntimeFromEnv(env) {
15
- const { getContextRuntime } = await import("./runtime.step.js");
16
- const legacy = (await getContextRuntime(env));
17
- return {
18
- env,
19
- db: async () => legacy.db,
20
- resolve: async () => ({
21
- db: legacy.db,
22
- meta: () => ({
23
- domain: legacy.domain,
24
- }),
25
- }),
26
- meta: () => ({
27
- domain: legacy.domain,
28
- }),
29
- };
30
- }
31
13
  async function resolveReactRuntime(params) {
32
14
  if (params.runtime)
33
15
  return params.runtime;
34
- if (params.env)
35
- return await legacyRuntimeFromEnv(params.env);
36
- throw new Error("ContextEngine.react requires either runtime or env.");
16
+ throw new Error("ContextEngine.react requires runtime.");
37
17
  }
38
18
  export { toolApprovalHookToken, toolApprovalWebhookToken, getClientResumeHookUrl };
39
19
  function nowIso() {
@@ -48,13 +28,21 @@ function summarizePartPreview(part) {
48
28
  if (!part || typeof part !== "object")
49
29
  return {};
50
30
  if (isContextPartEnvelope(part)) {
51
- const preview = part.content[0]?.type === "text"
52
- ? part.content[0].text
53
- : JSON.stringify(part.content[0] ?? part);
54
- const state = "state" in part && typeof part.state === "string" ? part.state : undefined;
55
- const toolCallId = "toolCallId" in part && typeof part.toolCallId === "string"
56
- ? part.toolCallId
57
- : undefined;
31
+ const preview = part.type === "message"
32
+ ? part.content.text ?? JSON.stringify(part.content.blocks?.[0] ?? part)
33
+ : part.type === "reasoning"
34
+ ? part.content.text
35
+ : part.type === "source"
36
+ ? JSON.stringify(part.content.sources[0] ?? part)
37
+ : part.content.status === "failed"
38
+ ? part.content.error.message
39
+ : JSON.stringify(part.content);
40
+ const state = part.type === "reasoning"
41
+ ? part.content.state
42
+ : part.type === "action"
43
+ ? part.content.status
44
+ : undefined;
45
+ const toolCallId = part.type === "action" ? part.content.actionCallId : undefined;
58
46
  return {
59
47
  partPreview: preview ? clipPreview(preview) : undefined,
60
48
  partState: state,
@@ -233,6 +221,31 @@ async function createRuntimeOps(runtimeHandle, benchmark) {
233
221
  },
234
222
  };
235
223
  },
224
+ openReactionStep: async ({ contextIdentifier, content, executionId, iteration }) => {
225
+ const stepId = makeRuntimeId();
226
+ const now = new Date();
227
+ await instrumentedDb.transact([
228
+ instrumentedDb.tx.event_steps[stepId].create({
229
+ createdAt: now,
230
+ updatedAt: now,
231
+ status: "running",
232
+ iteration,
233
+ }),
234
+ instrumentedDb.tx.event_steps[stepId].link({ execution: executionId }),
235
+ ]);
236
+ const stream = await createPersistedContextStepStreamForRuntime({ db: instrumentedDb }, {
237
+ executionId,
238
+ stepId,
239
+ });
240
+ const context = await store.updateContextContent(contextIdentifier, content);
241
+ const events = await store.getItems(contextIdentifier);
242
+ return {
243
+ stepId,
244
+ stream,
245
+ context,
246
+ events,
247
+ };
248
+ },
236
249
  createContextStep: async ({ executionId, iteration }) => {
237
250
  const stepId = makeRuntimeId();
238
251
  await instrumentedDb.transact([
@@ -254,9 +267,48 @@ async function createRuntimeOps(runtimeHandle, benchmark) {
254
267
  }),
255
268
  ]);
256
269
  },
270
+ finalizeReactionStep: async (params) => {
271
+ if (params.session) {
272
+ await finalizePersistedContextStepStreamForRuntime({
273
+ runtime: { db: instrumentedDb },
274
+ session: params.session,
275
+ mode: "close",
276
+ });
277
+ }
278
+ await instrumentedDb.transact([
279
+ instrumentedDb.tx.event_steps[params.stepId].update({
280
+ ...params.patch,
281
+ updatedAt: new Date(),
282
+ }),
283
+ ]);
284
+ if (!params.reactionEventId || !params.reactionEvent) {
285
+ return {};
286
+ }
287
+ await instrumentedDb.transact([
288
+ instrumentedDb.tx.event_items[params.reactionEventId].update(params.reactionEvent),
289
+ ]);
290
+ return {
291
+ reactionEvent: {
292
+ ...params.reactionEvent,
293
+ id: params.reactionEventId,
294
+ },
295
+ };
296
+ },
257
297
  saveContextPartsStep: async (params) => {
258
298
  await store.saveStepParts({ stepId: params.stepId, parts: params.parts });
259
299
  },
300
+ saveContextPartsAndUpdateReaction: async (params) => {
301
+ await store.saveStepParts({ stepId: params.stepId, parts: params.parts });
302
+ await instrumentedDb.transact([
303
+ instrumentedDb.tx.event_items[params.reactionEventId].update(params.reactionEvent),
304
+ ]);
305
+ return {
306
+ reactionEvent: {
307
+ ...params.reactionEvent,
308
+ id: params.reactionEventId,
309
+ },
310
+ };
311
+ },
260
312
  getItems: async (contextIdentifier) => await store.getItems(contextIdentifier),
261
313
  updateItem: async (itemId, item) => {
262
314
  await instrumentedDb.transact([instrumentedDb.tx.event_items[itemId].update(item)]);
@@ -288,9 +340,12 @@ async function createWorkflowOps(runtime) {
288
340
  updateContextReactor: async (contextIdentifier, reactor) => await updateContextReactor({ runtime, contextIdentifier, reactor }),
289
341
  updateContextStatus: async (contextIdentifier, status) => await updateContextStatus({ runtime, contextIdentifier, status }),
290
342
  saveTriggerAndCreateExecution: async ({ contextIdentifier, triggerEvent }) => await saveTriggerAndCreateExecution({ runtime, contextIdentifier, triggerEvent }),
343
+ openReactionStep: async (params) => await openReactionStep({ runtime, ...params }),
291
344
  createContextStep: async ({ executionId, iteration }) => await createContextStep({ runtime, executionId, iteration }),
292
345
  updateContextStep: async (params) => await updateContextStep({ runtime, ...params }),
346
+ finalizeReactionStep: async (params) => await finalizeReactionStep({ runtime, ...params }),
293
347
  saveContextPartsStep: async (params) => await saveContextPartsStep({ runtime, ...params }),
348
+ saveContextPartsAndUpdateReaction: async (params) => await saveContextPartsAndUpdateReaction({ runtime, ...params }),
294
349
  getItems: async (contextIdentifier) => await getContextItems({ runtime, contextIdentifier }),
295
350
  updateItem: async (itemId, item, opts) => await updateItem({ runtime, eventId: itemId, event: item, opts }),
296
351
  completeExecution: async (contextIdentifier, executionId, status) => await completeExecution({ runtime, contextIdentifier, executionId, status }),
@@ -307,9 +362,11 @@ async function getContextEngineOps(runtime, benchmark) {
307
362
  export class ContextEngine {
308
363
  constructor(opts = {}, reactor) {
309
364
  this.opts = opts;
310
- this.reactor = reactor ?? createAiSdkReactor();
365
+ this.reactor =
366
+ reactor ??
367
+ createAiSdkReactor();
311
368
  }
312
- async buildSkills(_context, _env) {
369
+ async buildSkills(_context, _env, _runtime) {
313
370
  return [];
314
371
  }
315
372
  /**
@@ -329,13 +386,13 @@ export class ContextEngine {
329
386
  * the builder) so results are durable and replay-safe.
330
387
  * - If it’s pure/deterministic, it can run in workflow context.
331
388
  */
332
- async expandEvents(events, _context, _env) {
389
+ async expandEvents(events, _context, _env, _runtime) {
333
390
  return events;
334
391
  }
335
- getModel(_context, _env) {
392
+ getModel(_context, _env, _runtime) {
336
393
  return "openai/gpt-5";
337
394
  }
338
- getReactor(_context, _env) {
395
+ getReactor(_context, _env, _runtime) {
339
396
  return this.reactor;
340
397
  }
341
398
  /**
@@ -350,10 +407,10 @@ export class ContextEngine {
350
407
  return true;
351
408
  }
352
409
  async react(triggerEvent, params) {
353
- if (params.durable) {
354
- return await ContextEngine.startDurable(this, triggerEvent, params);
410
+ if (params.durable === false) {
411
+ return await ContextEngine.runDirect(this, triggerEvent, params);
355
412
  }
356
- return await ContextEngine.runDirect(this, triggerEvent, params);
413
+ return await ContextEngine.startDurable(this, triggerEvent, params);
357
414
  }
358
415
  static async prepareExecutionShell(story, triggerEvent, params) {
359
416
  const runtimeHandle = await resolveReactRuntime(params);
@@ -364,7 +421,11 @@ export class ContextEngine {
364
421
  let currentContext = ctxResult.context;
365
422
  const contextSelector = { id: String(currentContext.id) };
366
423
  if (ctxResult.isNew) {
367
- await story.opts.onContextCreated?.({ env, context: currentContext });
424
+ await story.opts.onContextCreated?.({
425
+ env,
426
+ runtime: runtimeHandle,
427
+ context: currentContext,
428
+ });
368
429
  }
369
430
  if (currentContext.status === "closed") {
370
431
  await measureBenchmark(params.__benchmark, "react.reopenClosedContextMs", async () => await ops.updateContextStatus(contextSelector, "open_idle"));
@@ -376,6 +437,7 @@ export class ContextEngine {
376
437
  }));
377
438
  currentContext = { ...currentContext, status: "open_streaming" };
378
439
  return {
440
+ runtimeHandle,
379
441
  contextSelector,
380
442
  currentContext,
381
443
  trigger: shell.triggerEvent,
@@ -400,10 +462,10 @@ export class ContextEngine {
400
462
  const shell = await ContextEngine.prepareExecutionShell(story, triggerEvent, params);
401
463
  let run;
402
464
  try {
403
- const [{ start }] = await Promise.all([
465
+ const [{ start }] = await measureBenchmark(params.__benchmark, "react.durable.importWorkflowApiMs", async () => await Promise.all([
404
466
  import("workflow/api"),
405
- ]);
406
- const startedRun = await start(workflow, [
467
+ ]));
468
+ const startedRun = await measureBenchmark(params.__benchmark, "react.durable.startWorkflowMs", async () => await start(workflow, [
407
469
  {
408
470
  contextKey,
409
471
  runtime: runtimeHandle,
@@ -423,19 +485,19 @@ export class ContextEngine {
423
485
  execution: shell.execution,
424
486
  },
425
487
  },
426
- ]);
488
+ ]));
427
489
  run = {
428
490
  runId: String(startedRun.runId),
429
491
  status: startedRun.status,
430
492
  returnValue: startedRun.returnValue,
431
493
  };
432
- const runtime = await createRuntimeOps(runtimeHandle);
433
- await runtime.db.transact([
494
+ const runtime = await measureBenchmark(params.__benchmark, "react.durable.resolveRuntimeOpsMs", async () => await createRuntimeOps(runtimeHandle, params.__benchmark));
495
+ await measureBenchmark(params.__benchmark, "react.durable.persistWorkflowRunIdMs", async () => await runtime.db.transact([
434
496
  runtime.db.tx.event_executions[shell.execution.id].update({
435
497
  workflowRunId: startedRun.runId,
436
498
  updatedAt: new Date(),
437
499
  }),
438
- ]);
500
+ ]));
439
501
  }
440
502
  catch (error) {
441
503
  const ops = await getContextEngineOps(runtimeHandle, params.__benchmark);
@@ -451,6 +513,26 @@ export class ContextEngine {
451
513
  };
452
514
  }
453
515
  static async runDirect(story, triggerEvent, params) {
516
+ if (!params.__bootstrap) {
517
+ const shell = await ContextEngine.prepareExecutionShell(story, triggerEvent, params);
518
+ const run = ContextEngine.runDirect(story, triggerEvent, {
519
+ ...params,
520
+ runtime: shell.runtimeHandle,
521
+ __bootstrap: {
522
+ contextId: shell.currentContext.id,
523
+ trigger: shell.trigger,
524
+ reaction: shell.reaction,
525
+ execution: shell.execution,
526
+ },
527
+ });
528
+ return {
529
+ context: shell.currentContext,
530
+ trigger: shell.trigger,
531
+ reaction: shell.reaction,
532
+ execution: shell.execution,
533
+ run,
534
+ };
535
+ }
454
536
  const runtimeHandle = await resolveReactRuntime(params);
455
537
  const env = runtimeHandle.env;
456
538
  const ops = await measureBenchmark(params.__benchmark, "react.resolveOpsMs", async () => await getContextEngineOps(runtimeHandle, params.__benchmark));
@@ -520,18 +602,19 @@ export class ContextEngine {
520
602
  };
521
603
  try {
522
604
  for (let iter = 0; iter < maxIterations; iter++) {
523
- // Create a persisted step per iteration (IDs generated in step runtime for replay safety)
524
605
  const stagePrefix = `react.iteration.${iter}`;
525
- const stepCreate = await measureBenchmark(params.__benchmark, `${stagePrefix}.createStepMs`, async () => await ops.createContextStep({
606
+ // Hook: Context DSL `context()` (implemented by subclasses via `initialize()`)
607
+ const nextContent = await measureBenchmark(params.__benchmark, `${stagePrefix}.contextMs`, async () => await story.initialize(updatedContext, env, runtimeHandle));
608
+ const openedStep = await measureBenchmark(params.__benchmark, `${stagePrefix}.openReactionStepMs`, async () => await ops.openReactionStep({
609
+ contextIdentifier: activeContextSelector,
610
+ content: nextContent,
526
611
  executionId,
527
612
  iteration: iter,
528
613
  }));
529
- currentStepId = stepCreate.stepId;
530
- currentStepStream = await createPersistedContextStepStream({
531
- runtime: runtimeHandle,
532
- executionId,
533
- stepId: stepCreate.stepId,
534
- });
614
+ currentStepId = openedStep.stepId;
615
+ currentStepStream = openedStep.stream;
616
+ updatedContext = openedStep.context;
617
+ const rawEvents = openedStep.events;
535
618
  await emitContextEvents({
536
619
  silent,
537
620
  writable,
@@ -539,20 +622,11 @@ export class ContextEngine {
539
622
  {
540
623
  type: "step.created",
541
624
  at: nowIso(),
542
- stepId: String(stepCreate.stepId),
625
+ stepId: String(openedStep.stepId),
543
626
  executionId,
544
627
  iteration: iter,
545
628
  status: "running",
546
629
  },
547
- ],
548
- });
549
- // Hook: Context DSL `context()` (implemented by subclasses via `initialize()`)
550
- const nextContent = await measureBenchmark(params.__benchmark, `${stagePrefix}.contextMs`, async () => await story.initialize(updatedContext, env));
551
- updatedContext = await measureBenchmark(params.__benchmark, `${stagePrefix}.persistContextMs`, async () => await ops.updateContextContent(activeContextSelector, nextContent));
552
- await emitContextEvents({
553
- silent,
554
- writable,
555
- events: [
556
630
  {
557
631
  type: "context.content_updated",
558
632
  at: nowIso(),
@@ -560,17 +634,17 @@ export class ContextEngine {
560
634
  },
561
635
  ],
562
636
  });
563
- await story.opts.onContextUpdated?.({ env, context: updatedContext });
637
+ await story.opts.onContextUpdated?.({
638
+ env,
639
+ runtime: runtimeHandle,
640
+ context: updatedContext,
641
+ });
564
642
  // Hook: Context DSL `narrative()` (implemented by subclasses via `buildSystemPrompt()`)
565
- const systemPrompt = await measureBenchmark(params.__benchmark, `${stagePrefix}.narrativeMs`, async () => await story.buildSystemPrompt(updatedContext, env));
643
+ const systemPrompt = await measureBenchmark(params.__benchmark, `${stagePrefix}.narrativeMs`, async () => await story.buildSystemPrompt(updatedContext, env, runtimeHandle));
566
644
  // Hook: Context DSL `actions()` (implemented by subclasses via `buildTools()`)
567
- const toolsAll = await measureBenchmark(params.__benchmark, `${stagePrefix}.actionsMs`, async () => await story.buildTools(updatedContext, env));
568
- const skillsAll = await measureBenchmark(params.__benchmark, `${stagePrefix}.skillsMs`, async () => await story.buildSkills(updatedContext, env));
569
- const rawEvents = await measureBenchmark(params.__benchmark, `${stagePrefix}.loadEventsMs`, async () => await ops.getItems(activeContextSelector));
570
- const expandedEvents = await measureBenchmark(params.__benchmark, `${stagePrefix}.expandEventsMs`, async () => await story.expandEvents(rawEvents, updatedContext, env));
571
- // IMPORTANT: step args must be serializable.
572
- // Match DurableAgent behavior: convert tool input schemas to plain JSON Schema in workflow context.
573
- const actionSpecs = actionsToActionSpecs(toolsAll);
645
+ const toolsAll = await measureBenchmark(params.__benchmark, `${stagePrefix}.actionsMs`, async () => await story.buildTools(updatedContext, env, runtimeHandle));
646
+ const skillsAll = await measureBenchmark(params.__benchmark, `${stagePrefix}.skillsMs`, async () => await story.buildSkills(updatedContext, env, runtimeHandle));
647
+ const expandedEvents = await measureBenchmark(params.__benchmark, `${stagePrefix}.expandEventsMs`, async () => await story.expandEvents(rawEvents, updatedContext, env, runtimeHandle));
574
648
  // Execute model reaction for this iteration using the stable reaction event id.
575
649
  //
576
650
  // IMPORTANT:
@@ -578,7 +652,7 @@ export class ContextEngine {
578
652
  // If we stream with a per-step id, the UI will render an optimistic assistant message
579
653
  // (step id) and then a second persisted assistant message (reaction id) with the same
580
654
  // content once InstantDB updates.
581
- const reactor = story.getReactor(updatedContext, env);
655
+ const reactor = story.getReactor(updatedContext, env, runtimeHandle);
582
656
  const reactionPartsBeforeStep = Array.isArray(reactionEvent.content?.parts)
583
657
  ? [...reactionEvent.content.parts]
584
658
  : [];
@@ -589,38 +663,38 @@ export class ContextEngine {
589
663
  if (nextSignature === persistedReactionPartsSignature)
590
664
  return;
591
665
  persistedReactionPartsSignature = nextSignature;
592
- await ops.saveContextPartsStep({
593
- stepId: stepCreate.stepId,
666
+ const saved = await ops.saveContextPartsAndUpdateReaction({
667
+ stepId: openedStep.stepId,
594
668
  parts: normalizedParts,
669
+ reactionEventId: reactionEvent.id,
670
+ reactionEvent: {
671
+ ...reactionEvent,
672
+ content: {
673
+ ...reactionEvent.content,
674
+ parts: [...reactionPartsBeforeStep, ...normalizedParts],
675
+ },
676
+ status: "pending",
677
+ },
595
678
  executionId,
596
679
  contextId: String(currentContext.id),
597
680
  iteration: iter,
598
681
  });
599
- reactionEvent = await ops.updateItem(reactionEvent.id, {
600
- ...reactionEvent,
601
- content: {
602
- ...reactionEvent.content,
603
- parts: [...reactionPartsBeforeStep, ...normalizedParts],
604
- },
605
- status: "pending",
606
- }, { executionId, contextId: String(currentContext.id) });
682
+ reactionEvent = saved.reactionEvent;
607
683
  };
608
684
  const reactionResult = await measureBenchmark(params.__benchmark, `${stagePrefix}.reactorMs`, async () => await reactor({
609
685
  runtime: runtimeHandle,
610
- env,
611
686
  context: updatedContext,
612
687
  contextIdentifier: activeContextSelector,
613
688
  events: expandedEvents,
614
689
  triggerEvent,
615
- model: story.getModel(updatedContext, env),
690
+ model: story.getModel(updatedContext, env, runtimeHandle),
616
691
  systemPrompt,
617
692
  actions: toolsAll,
618
- actionSpecs,
619
693
  skills: skillsAll,
620
694
  eventId: reactionEventId,
621
695
  executionId,
622
696
  contextId: String(currentContext.id),
623
- stepId: String(stepCreate.stepId),
697
+ stepId: String(openedStep.stepId),
624
698
  iteration: iter,
625
699
  maxModelSteps,
626
700
  // Only emit a `start` chunk once per story turn.
@@ -659,21 +733,35 @@ export class ContextEngine {
659
733
  parts: stepParts,
660
734
  },
661
735
  };
662
- await measureBenchmark(params.__benchmark, `${stagePrefix}.saveStepPartsMs`, async () => await ops.saveContextPartsStep({
663
- stepId: stepCreate.stepId,
736
+ const nextAssistantParts = Array.isArray(assistantEventEffective.content?.parts)
737
+ ? assistantEventEffective.content.parts
738
+ : [];
739
+ const nextReactionEvent = {
740
+ ...reactionEvent,
741
+ content: {
742
+ ...reactionEvent.content,
743
+ parts: [...reactionPartsBeforeStep, ...nextAssistantParts],
744
+ },
745
+ status: "pending",
746
+ };
747
+ const appendedReactorOutput = await measureBenchmark(params.__benchmark, `${stagePrefix}.appendReactorOutputMs`, async () => await ops.saveContextPartsAndUpdateReaction({
748
+ stepId: openedStep.stepId,
664
749
  parts: stepParts,
750
+ reactionEventId: reactionEvent.id,
751
+ reactionEvent: nextReactionEvent,
665
752
  executionId,
666
753
  contextId: String(currentContext.id),
667
754
  iteration: iter,
668
755
  }));
756
+ reactionEvent = appendedReactorOutput.reactionEvent;
669
757
  await emitContextEvents({
670
758
  silent,
671
759
  writable,
672
760
  events: stepParts.map((part, idx) => ({
673
761
  type: "part.created",
674
762
  at: nowIso(),
675
- partKey: `${String(stepCreate.stepId)}:${idx}`,
676
- stepId: String(stepCreate.stepId),
763
+ partKey: `${String(openedStep.stepId)}:${idx}`,
764
+ stepId: String(openedStep.stepId),
677
765
  idx,
678
766
  partType: part && typeof part.type === "string"
679
767
  ? String(part.type)
@@ -681,19 +769,6 @@ export class ContextEngine {
681
769
  ...summarizePartPreview(part),
682
770
  })),
683
771
  });
684
- // Persist/append the aggregated reaction event (stable `reactionEventId` for the execution).
685
- const nextAssistantParts = Array.isArray(assistantEventEffective.content?.parts)
686
- ? assistantEventEffective.content.parts
687
- : [];
688
- const nextReactionEvent = {
689
- ...reactionEvent,
690
- content: {
691
- ...reactionEvent.content,
692
- parts: [...reactionPartsBeforeStep, ...nextAssistantParts],
693
- },
694
- status: "pending",
695
- };
696
- reactionEvent = await measureBenchmark(params.__benchmark, `${stagePrefix}.persistAssistantReactionMs`, async () => await ops.updateItem(reactionEvent.id, nextReactionEvent, { executionId, contextId: String(currentContext.id) }));
697
772
  if (reactionResult.reactor?.kind) {
698
773
  updatedContext = await measureBenchmark(params.__benchmark, `${stagePrefix}.persistReactorStateMs`, async () => await ops.updateContextReactor(activeContextSelector, {
699
774
  kind: reactionResult.reactor.kind,
@@ -703,32 +778,8 @@ export class ContextEngine {
703
778
  },
704
779
  }));
705
780
  }
706
- if (currentStepStream) {
707
- await closePersistedContextStepStream({
708
- runtime: runtimeHandle,
709
- session: currentStepStream,
710
- });
711
- currentStepStream = null;
712
- }
713
781
  story.opts.onEventCreated?.(assistantEventEffective);
714
782
  const firstActionRequest = actionRequests?.[0];
715
- await measureBenchmark(params.__benchmark, `${stagePrefix}.markStepRunningMs`, async () => await ops.updateContextStep({
716
- stepId: stepCreate.stepId,
717
- patch: firstActionRequest
718
- ? {
719
- kind: "action_execute",
720
- actionName: typeof firstActionRequest.actionName === "string"
721
- ? firstActionRequest.actionName
722
- : undefined,
723
- actionInput: firstActionRequest.input,
724
- }
725
- : {
726
- kind: "message",
727
- },
728
- executionId,
729
- contextId: String(currentContext.id),
730
- iteration: iter,
731
- }));
732
783
  await emitContextEvents({
733
784
  silent,
734
785
  writable,
@@ -736,7 +787,7 @@ export class ContextEngine {
736
787
  {
737
788
  type: "step.updated",
738
789
  at: nowIso(),
739
- stepId: String(stepCreate.stepId),
790
+ stepId: String(openedStep.stepId),
740
791
  executionId,
741
792
  iteration: iter,
742
793
  status: "running",
@@ -751,9 +802,13 @@ export class ContextEngine {
751
802
  if (!actionRequests.length) {
752
803
  const endResult = await story.callOnEnd(assistantEventEffective);
753
804
  if (endResult) {
754
- // Mark iteration step completed (no tools)
755
- await measureBenchmark(params.__benchmark, `${stagePrefix}.finalizeMessageStepMs`, async () => await ops.updateContextStep({
756
- stepId: stepCreate.stepId,
805
+ const completedReactionEvent = {
806
+ ...reactionEvent,
807
+ status: "completed",
808
+ };
809
+ const finalized = await measureBenchmark(params.__benchmark, `${stagePrefix}.finalizeReactionStepMs`, async () => await ops.finalizeReactionStep({
810
+ session: currentStepStream,
811
+ stepId: openedStep.stepId,
757
812
  patch: {
758
813
  status: "completed",
759
814
  kind: "message",
@@ -761,10 +816,14 @@ export class ContextEngine {
761
816
  actionResults: [],
762
817
  continueLoop: false,
763
818
  },
819
+ reactionEventId,
820
+ reactionEvent: completedReactionEvent,
764
821
  executionId,
765
822
  contextId: String(currentContext.id),
766
823
  iteration: iter,
767
824
  }));
825
+ currentStepStream = null;
826
+ reactionEvent = finalized.reactionEvent ?? completedReactionEvent;
768
827
  await emitContextEvents({
769
828
  silent,
770
829
  writable,
@@ -772,7 +831,7 @@ export class ContextEngine {
772
831
  {
773
832
  type: "step.updated",
774
833
  at: nowIso(),
775
- stepId: String(stepCreate.stepId),
834
+ stepId: String(openedStep.stepId),
776
835
  executionId,
777
836
  iteration: iter,
778
837
  status: "completed",
@@ -781,18 +840,13 @@ export class ContextEngine {
781
840
  {
782
841
  type: "step.completed",
783
842
  at: nowIso(),
784
- stepId: String(stepCreate.stepId),
843
+ stepId: String(openedStep.stepId),
785
844
  executionId,
786
845
  iteration: iter,
787
846
  status: "completed",
788
847
  },
789
848
  ],
790
849
  });
791
- // Mark reaction event completed
792
- await measureBenchmark(params.__benchmark, `${stagePrefix}.completeReactionMs`, async () => await ops.updateItem(reactionEventId, {
793
- ...reactionEvent,
794
- status: "completed",
795
- }, { executionId, contextId: String(currentContext.id) }));
796
850
  await emitContextEvents({
797
851
  silent,
798
852
  writable,
@@ -832,10 +886,6 @@ export class ContextEngine {
832
886
  if (!silent) {
833
887
  await closeContextStream({ preventClose, sendFinish, writable });
834
888
  }
835
- reactionEvent = {
836
- ...reactionEvent,
837
- status: "completed",
838
- };
839
889
  return {
840
890
  context: updatedContext,
841
891
  trigger,
@@ -887,7 +937,6 @@ export class ContextEngine {
887
937
  const executeAction = toolDef.execute;
888
938
  const output = await Reflect.apply(executeAction, undefined, [actionInput, {
889
939
  runtime: runtimeHandle,
890
- env,
891
940
  context: updatedContext,
892
941
  contextIdentifier: activeContextSelector,
893
942
  toolCallId: actionRequest.actionRef,
@@ -896,8 +945,9 @@ export class ContextEngine {
896
945
  executionId,
897
946
  triggerEventId,
898
947
  contextId: currentContext.id,
899
- stepId: String(stepCreate.stepId),
948
+ stepId: String(openedStep.stepId),
900
949
  iteration: iter,
950
+ contextStepStream: currentStepStream?.stream,
901
951
  }]);
902
952
  return { actionRequest, success: true, output };
903
953
  }
@@ -923,7 +973,7 @@ export class ContextEngine {
923
973
  });
924
974
  }
925
975
  await measureBenchmark(params.__benchmark, `${stagePrefix}.saveFinalStepPartsMs`, async () => await ops.saveContextPartsStep({
926
- stepId: stepCreate.stepId,
976
+ stepId: openedStep.stepId,
927
977
  parts: finalizedStepParts,
928
978
  executionId,
929
979
  contextId: String(currentContext.id),
@@ -955,36 +1005,47 @@ export class ContextEngine {
955
1005
  // so stories can inspect `reactionEvent.content.parts` deterministically.
956
1006
  const continueLoop = await measureBenchmark(params.__benchmark, `${stagePrefix}.shouldContinueMs`, async () => await story.shouldContinue({
957
1007
  env,
1008
+ runtime: runtimeHandle,
958
1009
  context: updatedContext,
959
1010
  reactionEvent,
960
1011
  assistantEvent: assistantEventEffective,
961
1012
  actionRequests,
962
1013
  actionResults: actionResults,
963
1014
  }));
964
- // Persist per-iteration step outcome (tools + continue signal)
965
- await measureBenchmark(params.__benchmark, `${stagePrefix}.completeStepMs`, async () => await ops.updateContextStep({
966
- stepId: stepCreate.stepId,
1015
+ const firstActionResult = actionResults?.[0];
1016
+ const finalizedReactionStatus = continueLoop === false ? "completed" : "pending";
1017
+ const finalizedReactionEvent = {
1018
+ ...reactionEvent,
1019
+ status: finalizedReactionStatus,
1020
+ };
1021
+ const finalizedStep = await measureBenchmark(params.__benchmark, `${stagePrefix}.finalizeReactionStepMs`, async () => await ops.finalizeReactionStep({
1022
+ session: currentStepStream,
1023
+ stepId: openedStep.stepId,
967
1024
  patch: {
968
1025
  status: "completed",
969
1026
  kind: actionRequests?.length ? "action_result" : "message",
970
- actionName: typeof actionResults?.[0]?.actionRequest?.actionName === "string"
971
- ? actionResults[0].actionRequest.actionName
1027
+ actionName: typeof firstActionResult?.actionRequest?.actionName === "string"
1028
+ ? firstActionResult.actionRequest.actionName
972
1029
  : undefined,
973
- actionInput: actionResults?.[0]?.actionRequest?.input,
974
- actionOutput: actionResults?.[0]?.success === true
975
- ? actionResults[0]?.output
1030
+ actionInput: firstActionResult?.actionRequest?.input,
1031
+ actionOutput: firstActionResult?.success === true
1032
+ ? firstActionResult?.output
976
1033
  : undefined,
977
- actionError: actionResults?.[0]?.success === false
978
- ? String(actionResults[0]?.errorText ?? "action_execution_failed")
1034
+ actionError: firstActionResult?.success === false
1035
+ ? String(firstActionResult?.errorText ?? "action_execution_failed")
979
1036
  : undefined,
980
1037
  actionRequests,
981
1038
  actionResults,
982
1039
  continueLoop: continueLoop !== false,
983
1040
  },
1041
+ reactionEventId,
1042
+ reactionEvent: finalizedReactionEvent,
984
1043
  executionId,
985
1044
  contextId: String(currentContext.id),
986
1045
  iteration: iter,
987
1046
  }));
1047
+ currentStepStream = null;
1048
+ reactionEvent = finalizedStep.reactionEvent ?? finalizedReactionEvent;
988
1049
  await emitContextEvents({
989
1050
  silent,
990
1051
  writable,
@@ -992,19 +1053,19 @@ export class ContextEngine {
992
1053
  {
993
1054
  type: "step.updated",
994
1055
  at: nowIso(),
995
- stepId: String(stepCreate.stepId),
1056
+ stepId: String(openedStep.stepId),
996
1057
  executionId,
997
1058
  iteration: iter,
998
1059
  status: "completed",
999
1060
  kind: actionRequests?.length ? "action_result" : "message",
1000
- actionName: typeof actionResults?.[0]?.actionRequest?.actionName === "string"
1001
- ? actionResults[0].actionRequest.actionName
1061
+ actionName: typeof firstActionResult?.actionRequest?.actionName === "string"
1062
+ ? firstActionResult.actionRequest.actionName
1002
1063
  : undefined,
1003
1064
  },
1004
1065
  {
1005
1066
  type: "step.completed",
1006
1067
  at: nowIso(),
1007
- stepId: String(stepCreate.stepId),
1068
+ stepId: String(openedStep.stepId),
1008
1069
  executionId,
1009
1070
  iteration: iter,
1010
1071
  status: "completed",
@@ -1012,10 +1073,6 @@ export class ContextEngine {
1012
1073
  ],
1013
1074
  });
1014
1075
  if (continueLoop !== false) {
1015
- reactionEvent = await measureBenchmark(params.__benchmark, `${stagePrefix}.persistPendingReactionMs`, async () => await ops.updateItem(reactionEventId, {
1016
- ...reactionEvent,
1017
- status: "pending",
1018
- }, { executionId, contextId: String(currentContext.id) }));
1019
1076
  await emitContextEvents({
1020
1077
  silent,
1021
1078
  writable,
@@ -1032,10 +1089,6 @@ export class ContextEngine {
1032
1089
  });
1033
1090
  }
1034
1091
  if (continueLoop === false) {
1035
- await measureBenchmark(params.__benchmark, `${stagePrefix}.completeReactionMs`, async () => await ops.updateItem(reactionEventId, {
1036
- ...reactionEvent,
1037
- status: "completed",
1038
- }, { executionId, contextId: String(currentContext.id) }));
1039
1092
  await emitContextEvents({
1040
1093
  silent,
1041
1094
  writable,
@@ -1075,10 +1128,6 @@ export class ContextEngine {
1075
1128
  if (!silent) {
1076
1129
  await closeContextStream({ preventClose, sendFinish, writable });
1077
1130
  }
1078
- reactionEvent = {
1079
- ...reactionEvent,
1080
- status: "completed",
1081
- };
1082
1131
  return {
1083
1132
  context: updatedContext,
1084
1133
  trigger,