@ekairos/events 1.22.35-beta.development.0 → 1.22.35

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 (73) hide show
  1. package/README.md +5 -3
  2. package/dist/codex.d.ts +11 -2
  3. package/dist/codex.js +16 -8
  4. package/dist/context.action-calls.d.ts +48 -0
  5. package/dist/context.action-calls.js +123 -0
  6. package/dist/context.action.d.ts +55 -0
  7. package/dist/context.action.js +25 -0
  8. package/dist/context.builder.d.ts +71 -43
  9. package/dist/context.builder.js +123 -28
  10. package/dist/context.config.d.ts +2 -1
  11. package/dist/context.config.js +8 -3
  12. package/dist/context.contract.d.ts +2 -4
  13. package/dist/context.contract.js +3 -9
  14. package/dist/context.d.ts +3 -2
  15. package/dist/context.engine.d.ts +60 -52
  16. package/dist/context.engine.js +506 -297
  17. package/dist/context.events.js +28 -87
  18. package/dist/context.js +1 -0
  19. package/dist/context.part-identity.d.ts +40 -0
  20. package/dist/context.part-identity.js +270 -0
  21. package/dist/context.parts.d.ts +389 -164
  22. package/dist/context.parts.js +343 -218
  23. package/dist/context.registry.d.ts +1 -1
  24. package/dist/context.runtime.d.ts +14 -4
  25. package/dist/context.runtime.js +21 -3
  26. package/dist/context.step-stream.d.ts +16 -2
  27. package/dist/context.step-stream.js +58 -16
  28. package/dist/context.store.d.ts +55 -10
  29. package/dist/context.stream.d.ts +14 -4
  30. package/dist/context.stream.js +31 -3
  31. package/dist/domain.d.ts +1 -0
  32. package/dist/domain.js +1 -0
  33. package/dist/index.d.ts +13 -10
  34. package/dist/index.js +7 -6
  35. package/dist/react.context-event-parts.d.ts +18 -0
  36. package/dist/react.context-event-parts.js +509 -0
  37. package/dist/react.d.ts +7 -42
  38. package/dist/react.js +4 -87
  39. package/dist/react.step-stream.d.ts +39 -0
  40. package/dist/react.step-stream.js +625 -0
  41. package/dist/react.types.d.ts +121 -0
  42. package/dist/react.types.js +2 -0
  43. package/dist/react.use-context.d.ts +7 -0
  44. package/dist/react.use-context.js +867 -0
  45. package/dist/reactors/ai-sdk.chunk-map.d.ts +1 -0
  46. package/dist/reactors/ai-sdk.chunk-map.js +56 -5
  47. package/dist/reactors/ai-sdk.reactor.d.ts +8 -9
  48. package/dist/reactors/ai-sdk.reactor.js +6 -9
  49. package/dist/reactors/ai-sdk.step.d.ts +4 -5
  50. package/dist/reactors/ai-sdk.step.js +24 -17
  51. package/dist/reactors/scripted.reactor.d.ts +7 -4
  52. package/dist/reactors/types.d.ts +19 -10
  53. package/dist/runtime.d.ts +6 -0
  54. package/dist/runtime.js +9 -0
  55. package/dist/runtime.step.js +1 -1
  56. package/dist/schema.d.ts +268 -2
  57. package/dist/schema.js +4 -9
  58. package/dist/steps/do-context-stream-step.js +4 -4
  59. package/dist/steps/durable.steps.d.ts +28 -0
  60. package/dist/steps/durable.steps.js +34 -0
  61. package/dist/steps/store.steps.d.ts +64 -22
  62. package/dist/steps/store.steps.js +192 -35
  63. package/dist/steps/stream.steps.d.ts +32 -0
  64. package/dist/steps/stream.steps.js +124 -6
  65. package/dist/steps/trace.steps.d.ts +4 -4
  66. package/dist/steps/trace.steps.js +21 -6
  67. package/dist/stores/instant.store.d.ts +11 -11
  68. package/dist/stores/instant.store.js +136 -6
  69. package/dist/tools-to-model-tools.d.ts +4 -2
  70. package/dist/tools-to-model-tools.js +30 -11
  71. package/package.json +18 -7
  72. package/dist/context.toolcalls.d.ts +0 -60
  73. package/dist/context.toolcalls.js +0 -117
@@ -1,16 +1,21 @@
1
1
  import { getContextRuntimeServices } from "./context.runtime.js";
2
2
  import { OUTPUT_ITEM_TYPE, WEB_CHANNEL } from "./context.events.js";
3
- import { applyToolExecutionResultToParts } from "./context.toolcalls.js";
3
+ import { applyActionExecutionResultToParts } from "./context.action-calls.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, initializeContext, saveTriggerAndCreateExecution, saveContextPartsStep, updateContextContent, updateContextReactor, updateContextStatus, updateItem, updateContextStep, } from "./steps/store.steps.js";
6
+ import { abortPersistedContextStepStream, closeContextStream, createPersistedContextStepStreamForRuntime, finalizePersistedContextStepStreamForRuntime, writeActionResultPartChunksToSession, } from "./steps/stream.steps.js";
7
+ import { completeExecution, completeExecutionStep, createContextStep, getContextItems, initializeContext, openExecutionStep, openExecution, saveExecutionStepOutput, updateContextContent, updateContextDefinition, updateContextReactor, updateContextStatus, updateItem, updateContextStep, updateExecutionWorkflowRun, upsertContextResources, } from "./steps/store.steps.js";
8
+ import { readContextDurableWorkflowReturnValue, readContextDurableWorkflowStatus, resumeContextReturnValueHook, startContextDurableWorkflow, } from "./steps/durable.steps.js";
9
9
  import { getClientResumeHookUrl, toolApprovalHookToken, toolApprovalWebhookToken, } from "./context.hooks.js";
10
10
  import { getContextDurableWorkflow } from "./context.durable.js";
11
11
  export async function runContextReactionDirect(context, triggerEvent, params) {
12
12
  return await ContextEngine.runDirect(context, triggerEvent, params);
13
13
  }
14
+ async function resolveReactRuntime(params) {
15
+ if (params.runtime)
16
+ return params.runtime;
17
+ throw new Error("ContextEngine.react requires runtime.");
18
+ }
14
19
  export { toolApprovalHookToken, toolApprovalWebhookToken, getClientResumeHookUrl };
15
20
  function nowIso() {
16
21
  return new Date().toISOString();
@@ -24,13 +29,21 @@ function summarizePartPreview(part) {
24
29
  if (!part || typeof part !== "object")
25
30
  return {};
26
31
  if (isContextPartEnvelope(part)) {
27
- const preview = part.content[0]?.type === "text"
28
- ? part.content[0].text
29
- : JSON.stringify(part.content[0] ?? part);
30
- const state = "state" in part && typeof part.state === "string" ? part.state : undefined;
31
- const toolCallId = "toolCallId" in part && typeof part.toolCallId === "string"
32
- ? part.toolCallId
33
- : undefined;
32
+ const preview = part.type === "message"
33
+ ? part.content.text ?? JSON.stringify(part.content.blocks?.[0] ?? part)
34
+ : part.type === "reasoning"
35
+ ? part.content.text
36
+ : part.type === "source"
37
+ ? JSON.stringify(part.content.sources[0] ?? part)
38
+ : part.content.status === "failed"
39
+ ? part.content.error.message
40
+ : JSON.stringify(part.content);
41
+ const state = part.type === "reasoning"
42
+ ? part.content.state
43
+ : part.type === "action"
44
+ ? part.content.status
45
+ : undefined;
46
+ const toolCallId = part.type === "action" ? part.content.actionCallId : undefined;
34
47
  return {
35
48
  partPreview: preview ? clipPreview(preview) : undefined,
36
49
  partState: state,
@@ -89,6 +102,37 @@ async function readActiveWorkflowRunId() {
89
102
  return null;
90
103
  }
91
104
  }
105
+ function serializeContextReturnValueError(error) {
106
+ if (error instanceof Error) {
107
+ return {
108
+ name: error.name,
109
+ message: error.message,
110
+ stack: error.stack,
111
+ };
112
+ }
113
+ return {
114
+ message: String(error),
115
+ };
116
+ }
117
+ function unwrapContextReturnValueHookPayload(payload) {
118
+ if (payload.ok)
119
+ return payload.result;
120
+ const error = new Error(payload.error.message);
121
+ if (payload.error.name) {
122
+ error.name = payload.error.name;
123
+ }
124
+ if (payload.error.stack) {
125
+ error.stack = payload.error.stack;
126
+ }
127
+ throw error;
128
+ }
129
+ function isEmptyContextContent(content) {
130
+ if (content == null)
131
+ return true;
132
+ if (typeof content !== "object")
133
+ return false;
134
+ return Object.keys(content).length === 0;
135
+ }
92
136
  async function createRuntimeOps(runtimeHandle, benchmark) {
93
137
  const runtime = await getContextRuntimeServices(runtimeHandle);
94
138
  const { db } = runtime;
@@ -144,6 +188,8 @@ async function createRuntimeOps(runtimeHandle, benchmark) {
144
188
  return { context, isNew: true };
145
189
  },
146
190
  updateContextContent: async (contextIdentifier, content) => await store.updateContextContent(contextIdentifier, content),
191
+ updateContextDefinition: async (contextIdentifier, definition) => await store.updateContextDefinition(contextIdentifier, definition),
192
+ upsertContextResources: async (contextIdentifier, resources) => await store.upsertContextResources(contextIdentifier, resources),
147
193
  updateContextReactor: async (contextIdentifier, reactor) => await store.updateContextReactor(contextIdentifier, reactor),
148
194
  updateContextStatus: async (contextIdentifier, status) => await instrumentedDb.transact([
149
195
  instrumentedDb.tx.event_contexts[requireContextId(contextIdentifier)].update({
@@ -151,7 +197,7 @@ async function createRuntimeOps(runtimeHandle, benchmark) {
151
197
  updatedAt: new Date(),
152
198
  }),
153
199
  ]),
154
- saveTriggerAndCreateExecution: async ({ contextIdentifier, triggerEvent }) => {
200
+ openExecution: async ({ contextIdentifier, triggerEvent }) => {
155
201
  const contextId = requireContextId(contextIdentifier);
156
202
  const triggerId = String(triggerEvent.id);
157
203
  const reactionId = makeRuntimeId();
@@ -209,6 +255,31 @@ async function createRuntimeOps(runtimeHandle, benchmark) {
209
255
  },
210
256
  };
211
257
  },
258
+ openExecutionStep: async ({ contextIdentifier, content, executionId, iteration }) => {
259
+ const stepId = makeRuntimeId();
260
+ const now = new Date();
261
+ await instrumentedDb.transact([
262
+ instrumentedDb.tx.event_steps[stepId].create({
263
+ createdAt: now,
264
+ updatedAt: now,
265
+ status: "running",
266
+ iteration,
267
+ }),
268
+ instrumentedDb.tx.event_steps[stepId].link({ execution: executionId }),
269
+ ]);
270
+ const stream = await createPersistedContextStepStreamForRuntime({ db: instrumentedDb }, {
271
+ executionId,
272
+ stepId,
273
+ });
274
+ const context = await store.updateContextContent(contextIdentifier, content);
275
+ const events = await store.getItems(contextIdentifier);
276
+ return {
277
+ stepId,
278
+ stream,
279
+ context,
280
+ events,
281
+ };
282
+ },
212
283
  createContextStep: async ({ executionId, iteration }) => {
213
284
  const stepId = makeRuntimeId();
214
285
  await instrumentedDb.transact([
@@ -223,16 +294,67 @@ async function createRuntimeOps(runtimeHandle, benchmark) {
223
294
  return { stepId };
224
295
  },
225
296
  updateContextStep: async (params) => {
297
+ const update = { updatedAt: new Date() };
298
+ if (params.patch.status !== undefined)
299
+ update.status = params.patch.status;
300
+ if (params.patch.errorText !== undefined)
301
+ update.errorText = params.patch.errorText;
226
302
  await instrumentedDb.transact([
227
- instrumentedDb.tx.event_steps[params.stepId].update({
228
- ...params.patch,
229
- updatedAt: new Date(),
230
- }),
303
+ instrumentedDb.tx.event_steps[params.stepId].update(update),
231
304
  ]);
232
305
  },
233
- saveContextPartsStep: async (params) => {
306
+ completeExecutionStep: async (params) => {
307
+ const actionResultChunkEvents = await writeActionResultPartChunksToSession({
308
+ session: params.session,
309
+ contextId: String(params.contextId ?? ""),
310
+ executionId: String(params.executionId ?? ""),
311
+ itemId: String(params.reactionEventId ?? ""),
312
+ actionResults: params.actionResults ?? [],
313
+ });
314
+ if (params.parts) {
315
+ await store.saveStepParts({ stepId: params.stepId, parts: params.parts });
316
+ }
317
+ if (params.session) {
318
+ await finalizePersistedContextStepStreamForRuntime({
319
+ runtime: { db: instrumentedDb },
320
+ session: params.session,
321
+ mode: "close",
322
+ });
323
+ }
324
+ const update = { updatedAt: new Date() };
325
+ update.status = params.stepStatus ?? "completed";
326
+ if (params.errorText !== undefined)
327
+ update.errorText = params.errorText;
328
+ await instrumentedDb.transact([
329
+ instrumentedDb.tx.event_steps[params.stepId].update(update),
330
+ ]);
331
+ if (!params.reactionEventId || !params.reactionEvent) {
332
+ return { actionResultChunkEvents };
333
+ }
334
+ await instrumentedDb.transact([
335
+ instrumentedDb.tx.event_items[params.reactionEventId].update(params.reactionEvent),
336
+ ]);
337
+ return {
338
+ reactionEvent: {
339
+ ...params.reactionEvent,
340
+ id: params.reactionEventId,
341
+ },
342
+ actionResultChunkEvents,
343
+ };
344
+ },
345
+ saveExecutionStepOutput: async (params) => {
234
346
  await store.saveStepParts({ stepId: params.stepId, parts: params.parts });
347
+ await instrumentedDb.transact([
348
+ instrumentedDb.tx.event_items[params.reactionEventId].update(params.reactionEvent),
349
+ ]);
350
+ return {
351
+ reactionEvent: {
352
+ ...params.reactionEvent,
353
+ id: params.reactionEventId,
354
+ },
355
+ };
235
356
  },
357
+ getItems: async (contextIdentifier) => await store.getItems(contextIdentifier),
236
358
  updateItem: async (itemId, item) => {
237
359
  await instrumentedDb.transact([instrumentedDb.tx.event_items[itemId].update(item)]);
238
360
  return {
@@ -240,9 +362,9 @@ async function createRuntimeOps(runtimeHandle, benchmark) {
240
362
  id: itemId,
241
363
  };
242
364
  },
243
- completeExecution: async (contextIdentifier, executionId, status) => {
365
+ completeExecution: async (contextIdentifier, executionId, status, opts) => {
244
366
  const contextId = requireContextId(contextIdentifier);
245
- await instrumentedDb.transact([
367
+ const txs = [
246
368
  instrumentedDb.tx.event_executions[executionId].update({
247
369
  status,
248
370
  updatedAt: new Date(),
@@ -251,23 +373,40 @@ async function createRuntimeOps(runtimeHandle, benchmark) {
251
373
  status: "closed",
252
374
  updatedAt: new Date(),
253
375
  }),
254
- ]);
376
+ ];
377
+ if (opts?.reactionEventId && opts.reactionEvent) {
378
+ txs.push(instrumentedDb.tx.event_items[opts.reactionEventId].update(opts.reactionEvent));
379
+ }
380
+ await instrumentedDb.transact(txs);
381
+ return opts?.reactionEventId && opts.reactionEvent
382
+ ? {
383
+ reactionEvent: {
384
+ ...opts.reactionEvent,
385
+ id: opts.reactionEventId,
386
+ },
387
+ }
388
+ : {};
255
389
  },
256
390
  };
257
391
  }
258
392
  async function createWorkflowOps(runtime) {
259
393
  const env = runtime.env;
260
394
  return {
261
- initializeContext: async (contextIdentifier, opts) => await initializeContext({ runtime, contextIdentifier, opts }),
395
+ initializeContext: async (contextIdentifier) => await initializeContext({ runtime, contextIdentifier }),
262
396
  updateContextContent: async (contextIdentifier, content) => await updateContextContent({ runtime, contextIdentifier, content }),
397
+ updateContextDefinition: async (contextIdentifier, definition) => await updateContextDefinition({ runtime, contextIdentifier, definition }),
398
+ upsertContextResources: async (contextIdentifier, resources) => await upsertContextResources({ runtime, contextIdentifier, resources }),
263
399
  updateContextReactor: async (contextIdentifier, reactor) => await updateContextReactor({ runtime, contextIdentifier, reactor }),
264
400
  updateContextStatus: async (contextIdentifier, status) => await updateContextStatus({ runtime, contextIdentifier, status }),
265
- saveTriggerAndCreateExecution: async ({ contextIdentifier, triggerEvent }) => await saveTriggerAndCreateExecution({ runtime, contextIdentifier, triggerEvent }),
401
+ openExecution: async ({ contextIdentifier, triggerEvent }) => await openExecution({ runtime, contextIdentifier, triggerEvent }),
402
+ openExecutionStep: async (params) => await openExecutionStep({ runtime, ...params }),
266
403
  createContextStep: async ({ executionId, iteration }) => await createContextStep({ runtime, executionId, iteration }),
267
404
  updateContextStep: async (params) => await updateContextStep({ runtime, ...params }),
268
- saveContextPartsStep: async (params) => await saveContextPartsStep({ runtime, ...params }),
405
+ completeExecutionStep: async (params) => await completeExecutionStep({ runtime, ...params }),
406
+ saveExecutionStepOutput: async (params) => await saveExecutionStepOutput({ runtime, ...params }),
407
+ getItems: async (contextIdentifier) => await getContextItems({ runtime, contextIdentifier }),
269
408
  updateItem: async (itemId, item, opts) => await updateItem({ runtime, eventId: itemId, event: item, opts }),
270
- completeExecution: async (contextIdentifier, executionId, status) => await completeExecution({ runtime, contextIdentifier, executionId, status }),
409
+ completeExecution: async (contextIdentifier, executionId, status, opts) => await completeExecution({ runtime, contextIdentifier, executionId, status, ...opts }),
271
410
  };
272
411
  }
273
412
  async function getContextEngineOps(runtime, benchmark) {
@@ -281,9 +420,20 @@ async function getContextEngineOps(runtime, benchmark) {
281
420
  export class ContextEngine {
282
421
  constructor(opts = {}, reactor) {
283
422
  this.opts = opts;
284
- this.reactor = reactor ?? createAiSdkReactor();
423
+ this.reactor =
424
+ reactor ??
425
+ createAiSdkReactor();
426
+ }
427
+ async describeContext(_content, _context, _env, _runtime) {
428
+ return null;
429
+ }
430
+ async defineGoal(_content, _context, _env, _runtime) {
431
+ return null;
285
432
  }
286
- async buildSkills(_context, _env) {
433
+ async defineResources(_content, _context, _env, _runtime) {
434
+ return [];
435
+ }
436
+ async buildSkills(_context, _env, _runtime) {
287
437
  return [];
288
438
  }
289
439
  /**
@@ -303,13 +453,13 @@ export class ContextEngine {
303
453
  * the builder) so results are durable and replay-safe.
304
454
  * - If it’s pure/deterministic, it can run in workflow context.
305
455
  */
306
- async expandEvents(events, _context, _env) {
456
+ async expandEvents(events, _context, _env, _runtime) {
307
457
  return events;
308
458
  }
309
- getModel(_context, _env) {
459
+ getModel(_context, _env, _runtime) {
310
460
  return "openai/gpt-5";
311
461
  }
312
- getReactor(_context, _env) {
462
+ getReactor(_context, _env, _runtime) {
313
463
  return this.reactor;
314
464
  }
315
465
  /**
@@ -324,31 +474,36 @@ export class ContextEngine {
324
474
  return true;
325
475
  }
326
476
  async react(triggerEvent, params) {
327
- if (params.durable) {
328
- return await ContextEngine.startDurable(this, triggerEvent, params);
477
+ if (params.durable === false) {
478
+ return await ContextEngine.runDirect(this, triggerEvent, params);
329
479
  }
330
- return await ContextEngine.runDirect(this, triggerEvent, params);
480
+ return await ContextEngine.startDurable(this, triggerEvent, params);
331
481
  }
332
482
  static async prepareExecutionShell(story, triggerEvent, params) {
333
- const env = params.runtime.env;
334
- const ops = await measureBenchmark(params.__benchmark, "react.resolveOpsMs", async () => await getContextEngineOps(params.runtime, params.__benchmark));
335
- const silent = params.options?.silent ?? false;
336
- const ctxResult = await measureBenchmark(params.__benchmark, "react.initializeContextMs", async () => await ops.initializeContext(params.context ?? null, { silent }));
483
+ const runtimeHandle = await resolveReactRuntime(params);
484
+ const env = runtimeHandle.env;
485
+ const ops = await measureBenchmark(params.__benchmark, "react.resolveOpsMs", async () => await getContextEngineOps(runtimeHandle, params.__benchmark));
486
+ const ctxResult = await measureBenchmark(params.__benchmark, "react.initializeContextMs", async () => await ops.initializeContext(params.context ?? null));
337
487
  let currentContext = ctxResult.context;
338
488
  const contextSelector = { id: String(currentContext.id) };
339
489
  if (ctxResult.isNew) {
340
- await story.opts.onContextCreated?.({ env, context: currentContext });
490
+ await story.opts.onContextCreated?.({
491
+ env,
492
+ runtime: runtimeHandle,
493
+ context: currentContext,
494
+ });
341
495
  }
342
496
  if (currentContext.status === "closed") {
343
497
  await measureBenchmark(params.__benchmark, "react.reopenClosedContextMs", async () => await ops.updateContextStatus(contextSelector, "open_idle"));
344
498
  currentContext = { ...currentContext, status: "open_idle" };
345
499
  }
346
- const shell = await measureBenchmark(params.__benchmark, "react.bootstrapShellMs", async () => await ops.saveTriggerAndCreateExecution({
500
+ const shell = await measureBenchmark(params.__benchmark, "react.openExecutionMs", async () => await ops.openExecution({
347
501
  contextIdentifier: contextSelector,
348
502
  triggerEvent,
349
503
  }));
350
504
  currentContext = { ...currentContext, status: "open_streaming" };
351
505
  return {
506
+ runtimeHandle,
352
507
  contextSelector,
353
508
  currentContext,
354
509
  trigger: shell.triggerEvent,
@@ -357,7 +512,8 @@ export class ContextEngine {
357
512
  };
358
513
  }
359
514
  static async startDurable(story, triggerEvent, params) {
360
- const env = params.runtime.env;
515
+ const runtimeHandle = await resolveReactRuntime(params);
516
+ const env = runtimeHandle.env;
361
517
  if (params.options?.writable) {
362
518
  throw new Error("ContextEngine.react: durable runs manage their own workflow stream");
363
519
  }
@@ -367,50 +523,89 @@ export class ContextEngine {
367
523
  }
368
524
  const workflow = getContextDurableWorkflow();
369
525
  if (typeof workflow !== "function") {
370
- throw new Error("ContextEngine.react: durable workflow is not configured. Call configureContextDurableWorkflow(...) in runtime bootstrap.");
526
+ const contextKeyLabel = contextKey || "(missing)";
527
+ throw new Error([
528
+ "ContextEngine.react(..., { durable: true }) needs a registered durable context workflow.",
529
+ "Call configureContextDurableWorkflow(contextDurableWorkflow) during server/workflow bootstrap.",
530
+ "If you want inline execution inside the current workflow step, pass durable: false.",
531
+ `Context key: ${contextKeyLabel}.`,
532
+ ].join(" "));
371
533
  }
372
534
  const shell = await ContextEngine.prepareExecutionShell(story, triggerEvent, params);
535
+ if (params.__initialContent !== undefined &&
536
+ isEmptyContextContent(shell.currentContext.content)) {
537
+ const ops = await getContextEngineOps(runtimeHandle, params.__benchmark);
538
+ const initialContent = params.__initialContent;
539
+ shell.currentContext = await ops.updateContextContent(shell.contextSelector, initialContent);
540
+ }
373
541
  let run;
374
542
  try {
375
- const [{ start }] = await Promise.all([
376
- import("workflow/api"),
377
- ]);
378
- const startedRun = await start(workflow, [
379
- {
380
- contextKey,
381
- runtime: params.runtime,
382
- context: params.context ?? null,
383
- triggerEvent,
384
- options: {
385
- maxIterations: params.options?.maxIterations,
386
- maxModelSteps: params.options?.maxModelSteps,
387
- preventClose: params.options?.preventClose,
388
- sendFinish: params.options?.sendFinish,
389
- silent: params.options?.silent,
390
- },
391
- bootstrap: {
392
- contextId: shell.currentContext.id,
393
- trigger: shell.trigger,
394
- reaction: shell.reaction,
395
- execution: shell.execution,
396
- },
543
+ const parentWorkflowRunId = await readActiveWorkflowRunId();
544
+ let returnValueHook = null;
545
+ let returnValueHookPromise = null;
546
+ if (parentWorkflowRunId) {
547
+ try {
548
+ const { createHook } = await import("workflow");
549
+ const hook = createHook({
550
+ token: `context:return:${shell.execution.id}`,
551
+ metadata: {
552
+ kind: "context.returnValue",
553
+ contextId: shell.currentContext.id,
554
+ executionId: shell.execution.id,
555
+ parentWorkflowRunId,
556
+ },
557
+ });
558
+ returnValueHook = {
559
+ token: hook.token,
560
+ parentWorkflowRunId,
561
+ };
562
+ returnValueHookPromise = Promise.resolve(hook);
563
+ }
564
+ catch (error) {
565
+ const message = error instanceof Error ? error.message : String(error);
566
+ if (!message.includes("can only be called inside a workflow function")) {
567
+ throw error;
568
+ }
569
+ }
570
+ }
571
+ const payload = {
572
+ contextKey,
573
+ runtime: runtimeHandle,
574
+ context: params.context ?? null,
575
+ triggerEvent,
576
+ options: {
577
+ maxIterations: params.options?.maxIterations,
578
+ maxModelSteps: params.options?.maxModelSteps,
579
+ preventClose: params.options?.preventClose,
580
+ sendFinish: params.options?.sendFinish,
397
581
  },
398
- ]);
582
+ bootstrap: {
583
+ contextId: shell.currentContext.id,
584
+ trigger: shell.trigger,
585
+ reaction: shell.reaction,
586
+ execution: shell.execution,
587
+ returnValueHookToken: returnValueHook?.token ?? null,
588
+ },
589
+ };
590
+ const startedRun = await measureBenchmark(params.__benchmark, "react.durable.startWorkflowMs", async () => await startContextDurableWorkflow({ payload }));
399
591
  run = {
400
592
  runId: String(startedRun.runId),
401
- status: startedRun.status,
402
- returnValue: startedRun.returnValue,
593
+ status: readContextDurableWorkflowStatus({ runId: String(startedRun.runId) }),
594
+ returnValue: returnValueHookPromise
595
+ ? returnValueHookPromise.then(unwrapContextReturnValueHookPayload)
596
+ : readContextDurableWorkflowReturnValue({
597
+ runId: String(startedRun.runId),
598
+ }),
599
+ returnValueHook,
403
600
  };
404
- const runtime = await createRuntimeOps(params.runtime);
405
- await runtime.db.transact([
406
- runtime.db.tx.event_executions[shell.execution.id].update({
407
- workflowRunId: startedRun.runId,
408
- updatedAt: new Date(),
409
- }),
410
- ]);
601
+ await measureBenchmark(params.__benchmark, "react.durable.persistWorkflowRunIdMs", async () => await updateExecutionWorkflowRun({
602
+ runtime: runtimeHandle,
603
+ executionId: shell.execution.id,
604
+ workflowRunId: String(startedRun.runId),
605
+ }));
411
606
  }
412
607
  catch (error) {
413
- const ops = await getContextEngineOps(params.runtime, params.__benchmark);
608
+ const ops = await getContextEngineOps(runtimeHandle, params.__benchmark);
414
609
  await ops.completeExecution(shell.contextSelector, shell.execution.id, "failed").catch(() => null);
415
610
  throw error;
416
611
  }
@@ -423,19 +618,48 @@ export class ContextEngine {
423
618
  };
424
619
  }
425
620
  static async runDirect(story, triggerEvent, params) {
426
- const env = params.runtime.env;
427
- const ops = await measureBenchmark(params.__benchmark, "react.resolveOpsMs", async () => await getContextEngineOps(params.runtime, params.__benchmark));
621
+ if (!params.__bootstrap) {
622
+ const shell = await ContextEngine.prepareExecutionShell(story, triggerEvent, params);
623
+ const run = ContextEngine.runDirect(story, triggerEvent, {
624
+ ...params,
625
+ runtime: shell.runtimeHandle,
626
+ __bootstrap: {
627
+ contextId: shell.currentContext.id,
628
+ trigger: shell.trigger,
629
+ reaction: shell.reaction,
630
+ execution: shell.execution,
631
+ },
632
+ });
633
+ return {
634
+ context: shell.currentContext,
635
+ trigger: shell.trigger,
636
+ reaction: shell.reaction,
637
+ execution: shell.execution,
638
+ run,
639
+ };
640
+ }
641
+ const runtimeHandle = await resolveReactRuntime(params);
642
+ const env = runtimeHandle.env;
643
+ const ops = await measureBenchmark(params.__benchmark, "react.resolveOpsMs", async () => await getContextEngineOps(runtimeHandle, params.__benchmark));
428
644
  const maxIterations = params.options?.maxIterations ?? 20;
429
645
  const maxModelSteps = params.options?.maxModelSteps ?? 1;
430
646
  const preventClose = params.options?.preventClose ?? false;
431
647
  const sendFinish = params.options?.sendFinish ?? true;
432
- const silent = params.options?.silent ?? false;
433
648
  const writable = params.options?.writable;
434
649
  const bootstrapped = params.__bootstrap;
650
+ const returnValueHookToken = bootstrapped?.returnValueHookToken ?? null;
651
+ const resumeReturnValueHook = async (payload) => {
652
+ if (!returnValueHookToken)
653
+ return;
654
+ await resumeContextReturnValueHook({
655
+ token: returnValueHookToken,
656
+ payload,
657
+ });
658
+ };
435
659
  const shell = bootstrapped
436
660
  ? {
437
661
  contextSelector: { id: String(bootstrapped.contextId) },
438
- currentContext: (await measureBenchmark(params.__benchmark, "react.bootstrapContextLookupMs", async () => await ops.initializeContext({ id: String(bootstrapped.contextId) }, { silent }))).context,
662
+ currentContext: (await measureBenchmark(params.__benchmark, "react.bootstrapContextLookupMs", async () => await ops.initializeContext({ id: String(bootstrapped.contextId) }))).context,
439
663
  trigger: bootstrapped.trigger,
440
664
  reaction: bootstrapped.reaction,
441
665
  execution: bootstrapped.execution,
@@ -458,7 +682,6 @@ export class ContextEngine {
458
682
  execution = { ...execution, status: "failed" };
459
683
  updatedContext = { ...updatedContext, status: "closed" };
460
684
  await emitContextEvents({
461
- silent,
462
685
  writable,
463
686
  events: [
464
687
  {
@@ -481,9 +704,7 @@ export class ContextEngine {
481
704
  // noop
482
705
  }
483
706
  try {
484
- if (!silent) {
485
- await closeContextStream({ preventClose, sendFinish, writable });
486
- }
707
+ await closeContextStream({ preventClose, sendFinish, writable });
487
708
  }
488
709
  catch {
489
710
  // noop
@@ -491,55 +712,102 @@ export class ContextEngine {
491
712
  };
492
713
  try {
493
714
  for (let iter = 0; iter < maxIterations; iter++) {
494
- // Create a persisted step per iteration (IDs generated in step runtime for replay safety)
495
715
  const stagePrefix = `react.iteration.${iter}`;
496
- const stepCreate = await measureBenchmark(params.__benchmark, `${stagePrefix}.createStepMs`, async () => await ops.createContextStep({
716
+ runtimeHandle.__ekairosContextRun = {
717
+ contextId: currentContext.id,
497
718
  executionId,
719
+ triggerEventId,
720
+ reactionEventId,
498
721
  iteration: iter,
499
- }));
500
- currentStepId = stepCreate.stepId;
501
- currentStepStream = await createPersistedContextStepStream({
502
- runtime: params.runtime,
722
+ };
723
+ // Hook: Context DSL `context()` (implemented by subclasses via `initialize()`)
724
+ const nextContent = await measureBenchmark(params.__benchmark, `${stagePrefix}.contextMs`, async () => await story.initialize(updatedContext, env, runtimeHandle));
725
+ const openedStep = await measureBenchmark(params.__benchmark, `${stagePrefix}.openExecutionStepMs`, async () => await ops.openExecutionStep({
726
+ contextIdentifier: activeContextSelector,
727
+ content: nextContent,
503
728
  executionId,
504
- stepId: stepCreate.stepId,
505
- });
729
+ iteration: iter,
730
+ }));
731
+ currentStepId = openedStep.stepId;
732
+ currentStepStream = openedStep.stream;
733
+ updatedContext = openedStep.context;
734
+ const rawEvents = openedStep.events;
735
+ const previousResources = updatedContext.resources ?? [];
736
+ const resources = await measureBenchmark(params.__benchmark, `${stagePrefix}.resourcesMs`, async () => await story.defineResources(nextContent, updatedContext, env, runtimeHandle));
737
+ const shouldPersistResources = resources.length > 0 || previousResources.length > 0;
738
+ let contextResources = previousResources;
739
+ if (shouldPersistResources) {
740
+ contextResources = await measureBenchmark(params.__benchmark, `${stagePrefix}.contextResourcesMs`, async () => await ops.upsertContextResources(activeContextSelector, resources));
741
+ updatedContext = {
742
+ ...updatedContext,
743
+ resources: contextResources,
744
+ };
745
+ }
746
+ else {
747
+ updatedContext = {
748
+ ...updatedContext,
749
+ resources: [],
750
+ };
751
+ }
752
+ const description = await measureBenchmark(params.__benchmark, `${stagePrefix}.descriptionMs`, async () => await story.describeContext(nextContent, updatedContext, env, runtimeHandle));
753
+ const goal = await measureBenchmark(params.__benchmark, `${stagePrefix}.goalMs`, async () => await story.defineGoal(nextContent, updatedContext, env, runtimeHandle));
754
+ if (description !== null || goal !== null) {
755
+ updatedContext = await measureBenchmark(params.__benchmark, `${stagePrefix}.contextDefinitionMs`, async () => await ops.updateContextDefinition(activeContextSelector, {
756
+ ...(description !== null ? { description } : {}),
757
+ ...(goal !== null ? { goal } : {}),
758
+ }));
759
+ updatedContext = {
760
+ ...updatedContext,
761
+ resources: contextResources,
762
+ };
763
+ }
506
764
  await emitContextEvents({
507
- silent,
508
765
  writable,
509
766
  events: [
510
767
  {
511
768
  type: "step.created",
512
769
  at: nowIso(),
513
- stepId: String(stepCreate.stepId),
770
+ stepId: String(openedStep.stepId),
514
771
  executionId,
515
772
  iteration: iter,
516
773
  status: "running",
517
774
  },
518
- ],
519
- });
520
- // Hook: Context DSL `context()` (implemented by subclasses via `initialize()`)
521
- const nextContent = await measureBenchmark(params.__benchmark, `${stagePrefix}.contextMs`, async () => await story.initialize(updatedContext, env));
522
- updatedContext = await measureBenchmark(params.__benchmark, `${stagePrefix}.persistContextMs`, async () => await ops.updateContextContent(activeContextSelector, nextContent));
523
- await emitContextEvents({
524
- silent,
525
- writable,
526
- events: [
527
775
  {
528
776
  type: "context.content_updated",
529
777
  at: nowIso(),
530
778
  contextId: String(updatedContext.id),
531
779
  },
780
+ ...(description !== null || goal !== null
781
+ ? [
782
+ {
783
+ type: "context.definition_updated",
784
+ at: nowIso(),
785
+ contextId: String(updatedContext.id),
786
+ },
787
+ ]
788
+ : []),
789
+ ...(shouldPersistResources
790
+ ? [
791
+ {
792
+ type: "context.resources_updated",
793
+ at: nowIso(),
794
+ contextId: String(updatedContext.id),
795
+ },
796
+ ]
797
+ : []),
532
798
  ],
533
799
  });
534
- await story.opts.onContextUpdated?.({ env, context: updatedContext });
800
+ await story.opts.onContextUpdated?.({
801
+ env,
802
+ runtime: runtimeHandle,
803
+ context: updatedContext,
804
+ });
535
805
  // Hook: Context DSL `narrative()` (implemented by subclasses via `buildSystemPrompt()`)
536
- const systemPrompt = await measureBenchmark(params.__benchmark, `${stagePrefix}.narrativeMs`, async () => await story.buildSystemPrompt(updatedContext, env));
806
+ const systemPrompt = await measureBenchmark(params.__benchmark, `${stagePrefix}.narrativeMs`, async () => await story.buildSystemPrompt(updatedContext, env, runtimeHandle));
537
807
  // Hook: Context DSL `actions()` (implemented by subclasses via `buildTools()`)
538
- const toolsAll = await measureBenchmark(params.__benchmark, `${stagePrefix}.actionsMs`, async () => await story.buildTools(updatedContext, env));
539
- const skillsAll = await measureBenchmark(params.__benchmark, `${stagePrefix}.skillsMs`, async () => await story.buildSkills(updatedContext, env));
540
- // IMPORTANT: step args must be serializable.
541
- // Match DurableAgent behavior: convert tool input schemas to plain JSON Schema in workflow context.
542
- const actionSpecs = actionsToActionSpecs(toolsAll);
808
+ const toolsAll = await measureBenchmark(params.__benchmark, `${stagePrefix}.actionsMs`, async () => await story.buildTools(updatedContext, env, runtimeHandle));
809
+ const skillsAll = await measureBenchmark(params.__benchmark, `${stagePrefix}.skillsMs`, async () => await story.buildSkills(updatedContext, env, runtimeHandle));
810
+ const expandedEvents = await measureBenchmark(params.__benchmark, `${stagePrefix}.expandEventsMs`, async () => await story.expandEvents(rawEvents, updatedContext, env, runtimeHandle));
543
811
  // Execute model reaction for this iteration using the stable reaction event id.
544
812
  //
545
813
  // IMPORTANT:
@@ -547,7 +815,7 @@ export class ContextEngine {
547
815
  // If we stream with a per-step id, the UI will render an optimistic assistant message
548
816
  // (step id) and then a second persisted assistant message (reaction id) with the same
549
817
  // content once InstantDB updates.
550
- const reactor = story.getReactor(updatedContext, env);
818
+ const reactor = story.getReactor(updatedContext, env, runtimeHandle);
551
819
  const reactionPartsBeforeStep = Array.isArray(reactionEvent.content?.parts)
552
820
  ? [...reactionEvent.content.parts]
553
821
  : [];
@@ -558,42 +826,43 @@ export class ContextEngine {
558
826
  if (nextSignature === persistedReactionPartsSignature)
559
827
  return;
560
828
  persistedReactionPartsSignature = nextSignature;
561
- await ops.saveContextPartsStep({
562
- stepId: stepCreate.stepId,
829
+ const saved = await ops.saveExecutionStepOutput({
830
+ stepId: openedStep.stepId,
563
831
  parts: normalizedParts,
832
+ reactionEventId: reactionEvent.id,
833
+ reactionEvent: {
834
+ ...reactionEvent,
835
+ content: {
836
+ ...reactionEvent.content,
837
+ parts: [...reactionPartsBeforeStep, ...normalizedParts],
838
+ },
839
+ status: "pending",
840
+ },
564
841
  executionId,
565
842
  contextId: String(currentContext.id),
566
843
  iteration: iter,
567
844
  });
568
- reactionEvent = await ops.updateItem(reactionEvent.id, {
569
- ...reactionEvent,
570
- content: {
571
- ...reactionEvent.content,
572
- parts: [...reactionPartsBeforeStep, ...normalizedParts],
573
- },
574
- status: "pending",
575
- }, { executionId, contextId: String(currentContext.id) });
845
+ reactionEvent = saved.reactionEvent;
576
846
  };
577
847
  const reactionResult = await measureBenchmark(params.__benchmark, `${stagePrefix}.reactorMs`, async () => await reactor({
578
- runtime: params.runtime,
579
- env,
848
+ runtime: runtimeHandle,
580
849
  context: updatedContext,
581
850
  contextIdentifier: activeContextSelector,
851
+ resources: contextResources,
852
+ events: expandedEvents,
582
853
  triggerEvent,
583
- model: story.getModel(updatedContext, env),
854
+ model: story.getModel(updatedContext, env, runtimeHandle),
584
855
  systemPrompt,
585
856
  actions: toolsAll,
586
- actionSpecs,
587
857
  skills: skillsAll,
588
858
  eventId: reactionEventId,
589
859
  executionId,
590
860
  contextId: String(currentContext.id),
591
- stepId: String(stepCreate.stepId),
861
+ stepId: String(openedStep.stepId),
592
862
  iteration: iter,
593
863
  maxModelSteps,
594
864
  // Only emit a `start` chunk once per story turn.
595
- sendStart: !silent && iter === 0,
596
- silent,
865
+ sendStart: iter === 0,
597
866
  contextStepStream: currentStepStream?.stream,
598
867
  writable,
599
868
  persistReactionParts,
@@ -627,21 +896,34 @@ export class ContextEngine {
627
896
  parts: stepParts,
628
897
  },
629
898
  };
630
- await measureBenchmark(params.__benchmark, `${stagePrefix}.saveStepPartsMs`, async () => await ops.saveContextPartsStep({
631
- stepId: stepCreate.stepId,
899
+ const nextAssistantParts = Array.isArray(assistantEventEffective.content?.parts)
900
+ ? assistantEventEffective.content.parts
901
+ : [];
902
+ const nextReactionEvent = {
903
+ ...reactionEvent,
904
+ content: {
905
+ ...reactionEvent.content,
906
+ parts: [...reactionPartsBeforeStep, ...nextAssistantParts],
907
+ },
908
+ status: "pending",
909
+ };
910
+ const appendedReactorOutput = await measureBenchmark(params.__benchmark, `${stagePrefix}.saveExecutionStepOutputMs`, async () => await ops.saveExecutionStepOutput({
911
+ stepId: openedStep.stepId,
632
912
  parts: stepParts,
913
+ reactionEventId: reactionEvent.id,
914
+ reactionEvent: nextReactionEvent,
633
915
  executionId,
634
916
  contextId: String(currentContext.id),
635
917
  iteration: iter,
636
918
  }));
919
+ reactionEvent = appendedReactorOutput.reactionEvent;
637
920
  await emitContextEvents({
638
- silent,
639
921
  writable,
640
922
  events: stepParts.map((part, idx) => ({
641
923
  type: "part.created",
642
924
  at: nowIso(),
643
- partKey: `${String(stepCreate.stepId)}:${idx}`,
644
- stepId: String(stepCreate.stepId),
925
+ partKey: `${String(openedStep.stepId)}:${idx}`,
926
+ stepId: String(openedStep.stepId),
645
927
  idx,
646
928
  partType: part && typeof part.type === "string"
647
929
  ? String(part.type)
@@ -649,19 +931,6 @@ export class ContextEngine {
649
931
  ...summarizePartPreview(part),
650
932
  })),
651
933
  });
652
- // Persist/append the aggregated reaction event (stable `reactionEventId` for the execution).
653
- const nextAssistantParts = Array.isArray(assistantEventEffective.content?.parts)
654
- ? assistantEventEffective.content.parts
655
- : [];
656
- const nextReactionEvent = {
657
- ...reactionEvent,
658
- content: {
659
- ...reactionEvent.content,
660
- parts: [...reactionPartsBeforeStep, ...nextAssistantParts],
661
- },
662
- status: "pending",
663
- };
664
- reactionEvent = await measureBenchmark(params.__benchmark, `${stagePrefix}.persistAssistantReactionMs`, async () => await ops.updateItem(reactionEvent.id, nextReactionEvent, { executionId, contextId: String(currentContext.id) }));
665
934
  if (reactionResult.reactor?.kind) {
666
935
  updatedContext = await measureBenchmark(params.__benchmark, `${stagePrefix}.persistReactorStateMs`, async () => await ops.updateContextReactor(activeContextSelector, {
667
936
  kind: reactionResult.reactor.kind,
@@ -671,47 +940,17 @@ export class ContextEngine {
671
940
  },
672
941
  }));
673
942
  }
674
- if (currentStepStream) {
675
- await closePersistedContextStepStream({
676
- runtime: params.runtime,
677
- session: currentStepStream,
678
- });
679
- currentStepStream = null;
680
- }
681
943
  story.opts.onEventCreated?.(assistantEventEffective);
682
- const firstActionRequest = actionRequests?.[0];
683
- await measureBenchmark(params.__benchmark, `${stagePrefix}.markStepRunningMs`, async () => await ops.updateContextStep({
684
- stepId: stepCreate.stepId,
685
- patch: firstActionRequest
686
- ? {
687
- kind: "action_execute",
688
- actionName: typeof firstActionRequest.actionName === "string"
689
- ? firstActionRequest.actionName
690
- : undefined,
691
- actionInput: firstActionRequest.input,
692
- }
693
- : {
694
- kind: "message",
695
- },
696
- executionId,
697
- contextId: String(currentContext.id),
698
- iteration: iter,
699
- }));
700
944
  await emitContextEvents({
701
- silent,
702
945
  writable,
703
946
  events: [
704
947
  {
705
948
  type: "step.updated",
706
949
  at: nowIso(),
707
- stepId: String(stepCreate.stepId),
950
+ stepId: String(openedStep.stepId),
708
951
  executionId,
709
952
  iteration: iter,
710
953
  status: "running",
711
- kind: firstActionRequest ? "action_execute" : "message",
712
- actionName: firstActionRequest && typeof firstActionRequest.actionName === "string"
713
- ? firstActionRequest.actionName
714
- : undefined,
715
954
  },
716
955
  ],
717
956
  });
@@ -719,50 +958,45 @@ export class ContextEngine {
719
958
  if (!actionRequests.length) {
720
959
  const endResult = await story.callOnEnd(assistantEventEffective);
721
960
  if (endResult) {
722
- // Mark iteration step completed (no tools)
723
- await measureBenchmark(params.__benchmark, `${stagePrefix}.finalizeMessageStepMs`, async () => await ops.updateContextStep({
724
- stepId: stepCreate.stepId,
725
- patch: {
726
- status: "completed",
727
- kind: "message",
728
- actionRequests: [],
729
- actionResults: [],
730
- continueLoop: false,
731
- },
961
+ const completedReactionEvent = {
962
+ ...reactionEvent,
963
+ status: "completed",
964
+ };
965
+ const finalized = await measureBenchmark(params.__benchmark, `${stagePrefix}.completeExecutionStepMs`, async () => await ops.completeExecutionStep({
966
+ session: currentStepStream,
967
+ stepId: openedStep.stepId,
968
+ stepStatus: "completed",
969
+ reactionEventId,
970
+ reactionEvent: completedReactionEvent,
732
971
  executionId,
733
972
  contextId: String(currentContext.id),
734
973
  iteration: iter,
735
974
  }));
975
+ currentStepStream = null;
976
+ currentStepId = null;
977
+ reactionEvent = finalized.reactionEvent ?? completedReactionEvent;
736
978
  await emitContextEvents({
737
- silent,
738
979
  writable,
739
980
  events: [
740
981
  {
741
982
  type: "step.updated",
742
983
  at: nowIso(),
743
- stepId: String(stepCreate.stepId),
984
+ stepId: String(openedStep.stepId),
744
985
  executionId,
745
986
  iteration: iter,
746
987
  status: "completed",
747
- kind: "message",
748
988
  },
749
989
  {
750
990
  type: "step.completed",
751
991
  at: nowIso(),
752
- stepId: String(stepCreate.stepId),
992
+ stepId: String(openedStep.stepId),
753
993
  executionId,
754
994
  iteration: iter,
755
995
  status: "completed",
756
996
  },
757
997
  ],
758
998
  });
759
- // Mark reaction event completed
760
- await measureBenchmark(params.__benchmark, `${stagePrefix}.completeReactionMs`, async () => await ops.updateItem(reactionEventId, {
761
- ...reactionEvent,
762
- status: "completed",
763
- }, { executionId, contextId: String(currentContext.id) }));
764
999
  await emitContextEvents({
765
- silent,
766
1000
  writable,
767
1001
  events: [
768
1002
  {
@@ -779,7 +1013,6 @@ export class ContextEngine {
779
1013
  execution = { ...execution, status: "completed" };
780
1014
  updatedContext = { ...updatedContext, status: "closed" };
781
1015
  await emitContextEvents({
782
- silent,
783
1016
  writable,
784
1017
  events: [
785
1018
  {
@@ -797,19 +1030,15 @@ export class ContextEngine {
797
1030
  },
798
1031
  ],
799
1032
  });
800
- if (!silent) {
801
- await closeContextStream({ preventClose, sendFinish, writable });
802
- }
803
- reactionEvent = {
804
- ...reactionEvent,
805
- status: "completed",
806
- };
807
- return {
1033
+ await closeContextStream({ preventClose, sendFinish, writable });
1034
+ const result = {
808
1035
  context: updatedContext,
809
1036
  trigger,
810
1037
  reaction: reactionEvent,
811
1038
  execution,
812
1039
  };
1040
+ await resumeReturnValueHook({ ok: true, result });
1041
+ return result;
813
1042
  }
814
1043
  }
815
1044
  // Execute actions (workflow context; action implementations decide step vs workflow)
@@ -852,20 +1081,21 @@ export class ContextEngine {
852
1081
  actionInput = approval.args;
853
1082
  }
854
1083
  }
855
- const output = await toolDef.execute(actionInput, {
856
- runtime: params.runtime,
857
- env,
858
- context: updatedContext,
859
- contextIdentifier: activeContextSelector,
860
- toolCallId: actionRequest.actionRef,
861
- messages: messagesForModel,
862
- eventId: reactionEventId,
863
- executionId,
864
- triggerEventId,
865
- contextId: currentContext.id,
866
- stepId: String(stepCreate.stepId),
867
- iteration: iter,
868
- });
1084
+ const executeAction = toolDef.execute;
1085
+ const output = await Reflect.apply(executeAction, undefined, [actionInput, {
1086
+ runtime: runtimeHandle,
1087
+ context: updatedContext,
1088
+ contextIdentifier: activeContextSelector,
1089
+ toolCallId: actionRequest.actionRef,
1090
+ messages: messagesForModel,
1091
+ eventId: reactionEventId,
1092
+ executionId,
1093
+ triggerEventId,
1094
+ contextId: currentContext.id,
1095
+ stepId: String(openedStep.stepId),
1096
+ iteration: iter,
1097
+ contextStepStream: currentStepStream?.stream,
1098
+ }]);
869
1099
  return { actionRequest, success: true, output };
870
1100
  }
871
1101
  catch (e) {
@@ -877,26 +1107,19 @@ export class ContextEngine {
877
1107
  };
878
1108
  }
879
1109
  })));
880
- // Merge action results into persisted parts (so next LLM call can see them)
1110
+ // Merge action results into step parts so the next reaction can see them.
881
1111
  let finalizedStepParts = Array.isArray(stepParts) ? [...stepParts] : [];
882
1112
  for (const r of actionResults) {
883
- finalizedStepParts = applyToolExecutionResultToParts(finalizedStepParts, {
884
- toolCallId: r.actionRequest.actionRef,
885
- toolName: r.actionRequest.actionName,
1113
+ finalizedStepParts = applyActionExecutionResultToParts(finalizedStepParts, {
1114
+ actionCallId: r.actionRequest.actionRef,
1115
+ actionName: r.actionRequest.actionName,
886
1116
  }, {
887
1117
  success: Boolean(r.success),
888
1118
  result: r.output,
889
1119
  message: r.errorText,
890
1120
  });
891
1121
  }
892
- await measureBenchmark(params.__benchmark, `${stagePrefix}.saveFinalStepPartsMs`, async () => await ops.saveContextPartsStep({
893
- stepId: stepCreate.stepId,
894
- parts: finalizedStepParts,
895
- executionId,
896
- contextId: String(currentContext.id),
897
- iteration: iter,
898
- }));
899
- reactionEvent = {
1122
+ const pendingReactionEvent = {
900
1123
  ...reactionEvent,
901
1124
  content: {
902
1125
  ...reactionEvent.content,
@@ -906,85 +1129,70 @@ export class ContextEngine {
906
1129
  },
907
1130
  status: "pending",
908
1131
  };
909
- // Callback for observability/integration
910
- for (const r of actionResults) {
911
- await story.opts.onActionExecuted?.({
912
- actionRequest: r.actionRequest,
913
- success: r.success,
914
- output: r.output,
915
- errorText: r.errorText,
916
- eventId: reactionEventId,
917
- executionId,
918
- });
919
- }
920
- // Stop/continue boundary: allow the Context to decide if the loop should continue.
921
- // IMPORTANT: we call this after tool results have been merged into the persisted `reactionEvent`,
922
- // so stories can inspect `reactionEvent.content.parts` deterministically.
923
- const continueLoop = await measureBenchmark(params.__benchmark, `${stagePrefix}.shouldContinueMs`, async () => await story.shouldContinue({
924
- env,
925
- context: updatedContext,
926
- reactionEvent,
927
- assistantEvent: assistantEventEffective,
928
- actionRequests,
1132
+ const completedStep = await measureBenchmark(params.__benchmark, `${stagePrefix}.completeExecutionStepMs`, async () => await ops.completeExecutionStep({
1133
+ session: currentStepStream,
1134
+ stepId: openedStep.stepId,
1135
+ parts: finalizedStepParts,
929
1136
  actionResults: actionResults,
930
- }));
931
- // Persist per-iteration step outcome (tools + continue signal)
932
- await measureBenchmark(params.__benchmark, `${stagePrefix}.completeStepMs`, async () => await ops.updateContextStep({
933
- stepId: stepCreate.stepId,
934
- patch: {
935
- status: "completed",
936
- kind: actionRequests?.length ? "action_result" : "message",
937
- actionName: typeof actionResults?.[0]?.actionRequest?.actionName === "string"
938
- ? actionResults[0].actionRequest.actionName
939
- : undefined,
940
- actionInput: actionResults?.[0]?.actionRequest?.input,
941
- actionOutput: actionResults?.[0]?.success === true
942
- ? actionResults[0]?.output
943
- : undefined,
944
- actionError: actionResults?.[0]?.success === false
945
- ? String(actionResults[0]?.errorText ?? "action_execution_failed")
946
- : undefined,
947
- actionRequests,
948
- actionResults,
949
- continueLoop: continueLoop !== false,
950
- },
1137
+ stepStatus: "completed",
1138
+ reactionEventId,
1139
+ reactionEvent: pendingReactionEvent,
951
1140
  executionId,
952
1141
  contextId: String(currentContext.id),
953
1142
  iteration: iter,
954
1143
  }));
1144
+ currentStepStream = null;
1145
+ currentStepId = null;
1146
+ reactionEvent = completedStep.reactionEvent ?? pendingReactionEvent;
1147
+ await emitContextEvents({
1148
+ writable,
1149
+ events: completedStep.actionResultChunkEvents,
1150
+ });
955
1151
  await emitContextEvents({
956
- silent,
957
1152
  writable,
958
1153
  events: [
959
1154
  {
960
1155
  type: "step.updated",
961
1156
  at: nowIso(),
962
- stepId: String(stepCreate.stepId),
1157
+ stepId: String(openedStep.stepId),
963
1158
  executionId,
964
1159
  iteration: iter,
965
1160
  status: "completed",
966
- kind: actionRequests?.length ? "action_result" : "message",
967
- actionName: typeof actionResults?.[0]?.actionRequest?.actionName === "string"
968
- ? actionResults[0].actionRequest.actionName
969
- : undefined,
970
1161
  },
971
1162
  {
972
1163
  type: "step.completed",
973
1164
  at: nowIso(),
974
- stepId: String(stepCreate.stepId),
1165
+ stepId: String(openedStep.stepId),
975
1166
  executionId,
976
1167
  iteration: iter,
977
1168
  status: "completed",
978
1169
  },
979
1170
  ],
980
1171
  });
1172
+ // Callback for observability/integration
1173
+ for (const r of actionResults) {
1174
+ await story.opts.onActionExecuted?.({
1175
+ actionRequest: r.actionRequest,
1176
+ success: r.success,
1177
+ output: r.output,
1178
+ errorText: r.errorText,
1179
+ eventId: reactionEventId,
1180
+ executionId,
1181
+ });
1182
+ }
1183
+ // Stop/continue boundary: allow the Context to decide if the loop should continue.
1184
+ // Tool results are already persisted in the completed reaction step here.
1185
+ const continueLoop = await measureBenchmark(params.__benchmark, `${stagePrefix}.shouldContinueMs`, async () => await story.shouldContinue({
1186
+ env,
1187
+ runtime: runtimeHandle,
1188
+ context: updatedContext,
1189
+ reactionEvent,
1190
+ assistantEvent: assistantEventEffective,
1191
+ actionRequests,
1192
+ actionResults: actionResults,
1193
+ }));
981
1194
  if (continueLoop !== false) {
982
- reactionEvent = await measureBenchmark(params.__benchmark, `${stagePrefix}.persistPendingReactionMs`, async () => await ops.updateItem(reactionEventId, {
983
- ...reactionEvent,
984
- status: "pending",
985
- }, { executionId, contextId: String(currentContext.id) }));
986
1195
  await emitContextEvents({
987
- silent,
988
1196
  writable,
989
1197
  events: [
990
1198
  {
@@ -999,12 +1207,11 @@ export class ContextEngine {
999
1207
  });
1000
1208
  }
1001
1209
  if (continueLoop === false) {
1002
- await measureBenchmark(params.__benchmark, `${stagePrefix}.completeReactionMs`, async () => await ops.updateItem(reactionEventId, {
1210
+ reactionEvent = {
1003
1211
  ...reactionEvent,
1004
1212
  status: "completed",
1005
- }, { executionId, contextId: String(currentContext.id) }));
1213
+ };
1006
1214
  await emitContextEvents({
1007
- silent,
1008
1215
  writable,
1009
1216
  events: [
1010
1217
  {
@@ -1017,11 +1224,14 @@ export class ContextEngine {
1017
1224
  },
1018
1225
  ],
1019
1226
  });
1020
- await measureBenchmark(params.__benchmark, `${stagePrefix}.completeExecutionMs`, async () => await ops.completeExecution(activeContextSelector, executionId, "completed"));
1227
+ await measureBenchmark(params.__benchmark, `${stagePrefix}.completeExecutionMs`, async () => await ops.completeExecution(activeContextSelector, executionId, "completed", {
1228
+ contextId: String(currentContext.id),
1229
+ reactionEventId,
1230
+ reactionEvent,
1231
+ }));
1021
1232
  execution = { ...execution, status: "completed" };
1022
1233
  updatedContext = { ...updatedContext, status: "closed" };
1023
1234
  await emitContextEvents({
1024
- silent,
1025
1235
  writable,
1026
1236
  events: [
1027
1237
  {
@@ -1039,19 +1249,15 @@ export class ContextEngine {
1039
1249
  },
1040
1250
  ],
1041
1251
  });
1042
- if (!silent) {
1043
- await closeContextStream({ preventClose, sendFinish, writable });
1044
- }
1045
- reactionEvent = {
1046
- ...reactionEvent,
1047
- status: "completed",
1048
- };
1049
- return {
1252
+ await closeContextStream({ preventClose, sendFinish, writable });
1253
+ const result = {
1050
1254
  context: updatedContext,
1051
1255
  trigger,
1052
1256
  reaction: reactionEvent,
1053
1257
  execution,
1054
1258
  };
1259
+ await resumeReturnValueHook({ ok: true, result });
1260
+ return result;
1055
1261
  }
1056
1262
  }
1057
1263
  throw new Error(`ContextEngine: maxIterations reached (${maxIterations}) without completion`);
@@ -1060,7 +1266,7 @@ export class ContextEngine {
1060
1266
  if (currentStepStream) {
1061
1267
  try {
1062
1268
  await abortPersistedContextStepStream({
1063
- runtime: params.runtime,
1269
+ runtime: runtimeHandle,
1064
1270
  session: currentStepStream,
1065
1271
  reason: error instanceof Error ? error.message : String(error),
1066
1272
  });
@@ -1086,7 +1292,6 @@ export class ContextEngine {
1086
1292
  contextId: String(currentContext.id),
1087
1293
  }));
1088
1294
  await emitContextEvents({
1089
- silent,
1090
1295
  writable,
1091
1296
  events: [
1092
1297
  {
@@ -1105,6 +1310,10 @@ export class ContextEngine {
1105
1310
  }
1106
1311
  }
1107
1312
  await failExecution();
1313
+ await resumeReturnValueHook({
1314
+ ok: false,
1315
+ error: serializeContextReturnValueError(error),
1316
+ }).catch(() => null);
1108
1317
  throw error;
1109
1318
  }
1110
1319
  }