@ekairos/events 1.22.34-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 (74) hide show
  1. package/README.md +58 -83
  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 +75 -46
  16. package/dist/context.engine.js +538 -302
  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 +21 -0
  25. package/dist/context.runtime.js +39 -0
  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 +63 -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 -5
  48. package/dist/reactors/ai-sdk.reactor.js +10 -9
  49. package/dist/reactors/ai-sdk.step.d.ts +6 -6
  50. package/dist/reactors/ai-sdk.step.js +32 -24
  51. package/dist/reactors/scripted.reactor.d.ts +7 -4
  52. package/dist/reactors/types.d.ts +23 -8
  53. package/dist/runtime.d.ts +6 -0
  54. package/dist/runtime.js +9 -0
  55. package/dist/runtime.step.js +2 -2
  56. package/dist/schema.d.ts +268 -2
  57. package/dist/schema.js +5 -9
  58. package/dist/steps/do-context-stream-step.d.ts +2 -2
  59. package/dist/steps/do-context-stream-step.js +6 -8
  60. package/dist/steps/durable.steps.d.ts +28 -0
  61. package/dist/steps/durable.steps.js +34 -0
  62. package/dist/steps/store.steps.d.ts +121 -39
  63. package/dist/steps/store.steps.js +266 -111
  64. package/dist/steps/stream.steps.d.ts +36 -3
  65. package/dist/steps/stream.steps.js +137 -14
  66. package/dist/steps/trace.steps.d.ts +4 -2
  67. package/dist/steps/trace.steps.js +26 -8
  68. package/dist/stores/instant.store.d.ts +15 -11
  69. package/dist/stores/instant.store.js +155 -6
  70. package/dist/tools-to-model-tools.d.ts +39 -3
  71. package/dist/tools-to-model-tools.js +63 -6
  72. package/package.json +20 -6
  73. package/dist/context.toolcalls.d.ts +0 -60
  74. package/dist/context.toolcalls.js +0 -117
@@ -1,16 +1,21 @@
1
- import { registerContextEnv } from "./env.js";
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 { toolsToModelTools } 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, 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,9 +102,39 @@ async function readActiveWorkflowRunId() {
89
102
  return null;
90
103
  }
91
104
  }
92
- async function createRuntimeOps(env, benchmark) {
93
- const { getContextRuntime } = await import("./runtime.js");
94
- const runtime = await getContextRuntime(env);
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
+ }
136
+ async function createRuntimeOps(runtimeHandle, benchmark) {
137
+ const runtime = await getContextRuntimeServices(runtimeHandle);
95
138
  const { db } = runtime;
96
139
  const { InstantStore } = await import("./stores/instant.store.js");
97
140
  const requireContextId = (contextIdentifier) => {
@@ -145,13 +188,16 @@ async function createRuntimeOps(env, benchmark) {
145
188
  return { context, isNew: true };
146
189
  },
147
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),
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({
150
196
  status,
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(env, 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(env, 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(env, 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,38 +373,67 @@ async function createRuntimeOps(env, 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
- async function createWorkflowOps(env) {
392
+ async function createWorkflowOps(runtime) {
393
+ const env = runtime.env;
259
394
  return {
260
- initializeContext: async (contextIdentifier, opts) => await initializeContext(env, contextIdentifier, opts),
261
- updateContextContent: async (contextIdentifier, content) => await updateContextContent(env, contextIdentifier, content),
262
- updateContextStatus: async (contextIdentifier, status) => await updateContextStatus(env, contextIdentifier, status),
263
- saveTriggerAndCreateExecution: async ({ contextIdentifier, triggerEvent }) => await saveTriggerAndCreateExecution({ env, contextIdentifier, triggerEvent }),
264
- createContextStep: async ({ executionId, iteration }) => await createContextStep({ env, executionId, iteration }),
265
- updateContextStep: async (params) => await updateContextStep({ env, ...params }),
266
- saveContextPartsStep: async (params) => await saveContextPartsStep({ env, ...params }),
267
- updateItem: async (itemId, item, opts) => await updateItem(env, itemId, item, opts),
268
- completeExecution: async (contextIdentifier, executionId, status) => await completeExecution(env, contextIdentifier, executionId, status),
395
+ initializeContext: async (contextIdentifier) => await initializeContext({ runtime, contextIdentifier }),
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 }),
399
+ updateContextReactor: async (contextIdentifier, reactor) => await updateContextReactor({ runtime, contextIdentifier, reactor }),
400
+ updateContextStatus: async (contextIdentifier, status) => await updateContextStatus({ runtime, contextIdentifier, status }),
401
+ openExecution: async ({ contextIdentifier, triggerEvent }) => await openExecution({ runtime, contextIdentifier, triggerEvent }),
402
+ openExecutionStep: async (params) => await openExecutionStep({ runtime, ...params }),
403
+ createContextStep: async ({ executionId, iteration }) => await createContextStep({ runtime, executionId, iteration }),
404
+ updateContextStep: async (params) => await updateContextStep({ 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 }),
408
+ updateItem: async (itemId, item, opts) => await updateItem({ runtime, eventId: itemId, event: item, opts }),
409
+ completeExecution: async (contextIdentifier, executionId, status, opts) => await completeExecution({ runtime, contextIdentifier, executionId, status, ...opts }),
269
410
  };
270
411
  }
271
- async function getContextEngineOps(env, benchmark) {
412
+ async function getContextEngineOps(runtime, benchmark) {
413
+ const env = runtime.env;
272
414
  const workflowRunId = await readActiveWorkflowRunId();
273
415
  if (workflowRunId) {
274
- registerContextEnv(env, workflowRunId);
275
- return await createWorkflowOps(env);
416
+ return await createWorkflowOps(runtime);
276
417
  }
277
- registerContextEnv(env);
278
- return await createRuntimeOps(env, benchmark);
418
+ return await createRuntimeOps(runtime, benchmark);
279
419
  }
280
420
  export class ContextEngine {
281
421
  constructor(opts = {}, reactor) {
282
422
  this.opts = opts;
283
- this.reactor = reactor ?? createAiSdkReactor();
423
+ this.reactor =
424
+ reactor ??
425
+ createAiSdkReactor();
284
426
  }
285
- async buildSkills(_context, _env) {
427
+ async describeContext(_content, _context, _env, _runtime) {
428
+ return null;
429
+ }
430
+ async defineGoal(_content, _context, _env, _runtime) {
431
+ return null;
432
+ }
433
+ async defineResources(_content, _context, _env, _runtime) {
434
+ return [];
435
+ }
436
+ async buildSkills(_context, _env, _runtime) {
286
437
  return [];
287
438
  }
288
439
  /**
@@ -302,13 +453,13 @@ export class ContextEngine {
302
453
  * the builder) so results are durable and replay-safe.
303
454
  * - If it’s pure/deterministic, it can run in workflow context.
304
455
  */
305
- async expandEvents(events, _context, _env) {
456
+ async expandEvents(events, _context, _env, _runtime) {
306
457
  return events;
307
458
  }
308
- getModel(_context, _env) {
459
+ getModel(_context, _env, _runtime) {
309
460
  return "openai/gpt-5";
310
461
  }
311
- getReactor(_context, _env) {
462
+ getReactor(_context, _env, _runtime) {
312
463
  return this.reactor;
313
464
  }
314
465
  /**
@@ -323,30 +474,36 @@ export class ContextEngine {
323
474
  return true;
324
475
  }
325
476
  async react(triggerEvent, params) {
326
- if (params.durable) {
327
- return await ContextEngine.startDurable(this, triggerEvent, params);
477
+ if (params.durable === false) {
478
+ return await ContextEngine.runDirect(this, triggerEvent, params);
328
479
  }
329
- return await ContextEngine.runDirect(this, triggerEvent, params);
480
+ return await ContextEngine.startDurable(this, triggerEvent, params);
330
481
  }
331
482
  static async prepareExecutionShell(story, triggerEvent, params) {
332
- const ops = await measureBenchmark(params.__benchmark, "react.resolveOpsMs", async () => await getContextEngineOps(params.env, params.__benchmark));
333
- const silent = params.options?.silent ?? false;
334
- 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));
335
487
  let currentContext = ctxResult.context;
336
488
  const contextSelector = { id: String(currentContext.id) };
337
489
  if (ctxResult.isNew) {
338
- await story.opts.onContextCreated?.({ env: params.env, context: currentContext });
490
+ await story.opts.onContextCreated?.({
491
+ env,
492
+ runtime: runtimeHandle,
493
+ context: currentContext,
494
+ });
339
495
  }
340
496
  if (currentContext.status === "closed") {
341
497
  await measureBenchmark(params.__benchmark, "react.reopenClosedContextMs", async () => await ops.updateContextStatus(contextSelector, "open_idle"));
342
498
  currentContext = { ...currentContext, status: "open_idle" };
343
499
  }
344
- 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({
345
501
  contextIdentifier: contextSelector,
346
502
  triggerEvent,
347
503
  }));
348
504
  currentContext = { ...currentContext, status: "open_streaming" };
349
505
  return {
506
+ runtimeHandle,
350
507
  contextSelector,
351
508
  currentContext,
352
509
  trigger: shell.triggerEvent,
@@ -355,6 +512,8 @@ export class ContextEngine {
355
512
  };
356
513
  }
357
514
  static async startDurable(story, triggerEvent, params) {
515
+ const runtimeHandle = await resolveReactRuntime(params);
516
+ const env = runtimeHandle.env;
358
517
  if (params.options?.writable) {
359
518
  throw new Error("ContextEngine.react: durable runs manage their own workflow stream");
360
519
  }
@@ -364,44 +523,89 @@ export class ContextEngine {
364
523
  }
365
524
  const workflow = getContextDurableWorkflow();
366
525
  if (typeof workflow !== "function") {
367
- 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(" "));
368
533
  }
369
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
+ }
541
+ let run;
370
542
  try {
371
- const [{ start }] = await Promise.all([
372
- import("workflow/api"),
373
- ]);
374
- const run = await start(workflow, [
375
- {
376
- contextKey,
377
- env: params.env,
378
- context: params.context ?? null,
379
- triggerEvent,
380
- options: {
381
- maxIterations: params.options?.maxIterations,
382
- maxModelSteps: params.options?.maxModelSteps,
383
- preventClose: params.options?.preventClose,
384
- sendFinish: params.options?.sendFinish,
385
- silent: params.options?.silent,
386
- },
387
- bootstrap: {
388
- contextId: shell.currentContext.id,
389
- trigger: shell.trigger,
390
- reaction: shell.reaction,
391
- execution: shell.execution,
392
- },
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,
393
581
  },
394
- ]);
395
- const runtime = await createRuntimeOps(params.env);
396
- await runtime.db.transact([
397
- runtime.db.tx.event_executions[shell.execution.id].update({
398
- workflowRunId: run.runId,
399
- updatedAt: new Date(),
400
- }),
401
- ]);
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 }));
591
+ run = {
592
+ runId: String(startedRun.runId),
593
+ status: readContextDurableWorkflowStatus({ runId: String(startedRun.runId) }),
594
+ returnValue: returnValueHookPromise
595
+ ? returnValueHookPromise.then(unwrapContextReturnValueHookPayload)
596
+ : readContextDurableWorkflowReturnValue({
597
+ runId: String(startedRun.runId),
598
+ }),
599
+ returnValueHook,
600
+ };
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
+ }));
402
606
  }
403
607
  catch (error) {
404
- const ops = await getContextEngineOps(params.env, params.__benchmark);
608
+ const ops = await getContextEngineOps(runtimeHandle, params.__benchmark);
405
609
  await ops.completeExecution(shell.contextSelector, shell.execution.id, "failed").catch(() => null);
406
610
  throw error;
407
611
  }
@@ -410,21 +614,52 @@ export class ContextEngine {
410
614
  trigger: shell.trigger,
411
615
  reaction: shell.reaction,
412
616
  execution: shell.execution,
617
+ run,
413
618
  };
414
619
  }
415
620
  static async runDirect(story, triggerEvent, params) {
416
- const ops = await measureBenchmark(params.__benchmark, "react.resolveOpsMs", async () => await getContextEngineOps(params.env, 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));
417
644
  const maxIterations = params.options?.maxIterations ?? 20;
418
645
  const maxModelSteps = params.options?.maxModelSteps ?? 1;
419
646
  const preventClose = params.options?.preventClose ?? false;
420
647
  const sendFinish = params.options?.sendFinish ?? true;
421
- const silent = params.options?.silent ?? false;
422
648
  const writable = params.options?.writable;
423
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
+ };
424
659
  const shell = bootstrapped
425
660
  ? {
426
661
  contextSelector: { id: String(bootstrapped.contextId) },
427
- 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,
428
663
  trigger: bootstrapped.trigger,
429
664
  reaction: bootstrapped.reaction,
430
665
  execution: bootstrapped.execution,
@@ -447,7 +682,6 @@ export class ContextEngine {
447
682
  execution = { ...execution, status: "failed" };
448
683
  updatedContext = { ...updatedContext, status: "closed" };
449
684
  await emitContextEvents({
450
- silent,
451
685
  writable,
452
686
  events: [
453
687
  {
@@ -470,9 +704,7 @@ export class ContextEngine {
470
704
  // noop
471
705
  }
472
706
  try {
473
- if (!silent) {
474
- await closeContextStream({ preventClose, sendFinish, writable });
475
- }
707
+ await closeContextStream({ preventClose, sendFinish, writable });
476
708
  }
477
709
  catch {
478
710
  // noop
@@ -480,55 +712,102 @@ export class ContextEngine {
480
712
  };
481
713
  try {
482
714
  for (let iter = 0; iter < maxIterations; iter++) {
483
- // Create a persisted step per iteration (IDs generated in step runtime for replay safety)
484
715
  const stagePrefix = `react.iteration.${iter}`;
485
- const stepCreate = await measureBenchmark(params.__benchmark, `${stagePrefix}.createStepMs`, async () => await ops.createContextStep({
716
+ runtimeHandle.__ekairosContextRun = {
717
+ contextId: currentContext.id,
486
718
  executionId,
719
+ triggerEventId,
720
+ reactionEventId,
487
721
  iteration: iter,
488
- }));
489
- currentStepId = stepCreate.stepId;
490
- currentStepStream = await createPersistedContextStepStream({
491
- env: params.env,
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,
492
728
  executionId,
493
- stepId: stepCreate.stepId,
494
- });
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
+ }
495
764
  await emitContextEvents({
496
- silent,
497
765
  writable,
498
766
  events: [
499
767
  {
500
768
  type: "step.created",
501
769
  at: nowIso(),
502
- stepId: String(stepCreate.stepId),
770
+ stepId: String(openedStep.stepId),
503
771
  executionId,
504
772
  iteration: iter,
505
773
  status: "running",
506
774
  },
507
- ],
508
- });
509
- // Hook: Context DSL `context()` (implemented by subclasses via `initialize()`)
510
- const nextContent = await measureBenchmark(params.__benchmark, `${stagePrefix}.contextMs`, async () => await story.initialize(updatedContext, params.env));
511
- updatedContext = await measureBenchmark(params.__benchmark, `${stagePrefix}.persistContextMs`, async () => await ops.updateContextContent(activeContextSelector, nextContent));
512
- await emitContextEvents({
513
- silent,
514
- writable,
515
- events: [
516
775
  {
517
776
  type: "context.content_updated",
518
777
  at: nowIso(),
519
778
  contextId: String(updatedContext.id),
520
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
+ : []),
521
798
  ],
522
799
  });
523
- await story.opts.onContextUpdated?.({ env: params.env, context: updatedContext });
800
+ await story.opts.onContextUpdated?.({
801
+ env,
802
+ runtime: runtimeHandle,
803
+ context: updatedContext,
804
+ });
524
805
  // Hook: Context DSL `narrative()` (implemented by subclasses via `buildSystemPrompt()`)
525
- const systemPrompt = await measureBenchmark(params.__benchmark, `${stagePrefix}.narrativeMs`, async () => await story.buildSystemPrompt(updatedContext, params.env));
806
+ const systemPrompt = await measureBenchmark(params.__benchmark, `${stagePrefix}.narrativeMs`, async () => await story.buildSystemPrompt(updatedContext, env, runtimeHandle));
526
807
  // Hook: Context DSL `actions()` (implemented by subclasses via `buildTools()`)
527
- const toolsAll = await measureBenchmark(params.__benchmark, `${stagePrefix}.actionsMs`, async () => await story.buildTools(updatedContext, params.env));
528
- const skillsAll = await measureBenchmark(params.__benchmark, `${stagePrefix}.skillsMs`, async () => await story.buildSkills(updatedContext, params.env));
529
- // IMPORTANT: step args must be serializable.
530
- // Match DurableAgent behavior: convert tool input schemas to plain JSON Schema in workflow context.
531
- const toolsForModel = toolsToModelTools(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));
532
811
  // Execute model reaction for this iteration using the stable reaction event id.
533
812
  //
534
813
  // IMPORTANT:
@@ -536,7 +815,7 @@ export class ContextEngine {
536
815
  // If we stream with a per-step id, the UI will render an optimistic assistant message
537
816
  // (step id) and then a second persisted assistant message (reaction id) with the same
538
817
  // content once InstantDB updates.
539
- const reactor = story.getReactor(updatedContext, params.env);
818
+ const reactor = story.getReactor(updatedContext, env, runtimeHandle);
540
819
  const reactionPartsBeforeStep = Array.isArray(reactionEvent.content?.parts)
541
820
  ? [...reactionEvent.content.parts]
542
821
  : [];
@@ -547,45 +826,48 @@ export class ContextEngine {
547
826
  if (nextSignature === persistedReactionPartsSignature)
548
827
  return;
549
828
  persistedReactionPartsSignature = nextSignature;
550
- await ops.saveContextPartsStep({
551
- stepId: stepCreate.stepId,
829
+ const saved = await ops.saveExecutionStepOutput({
830
+ stepId: openedStep.stepId,
552
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
+ },
553
841
  executionId,
554
842
  contextId: String(currentContext.id),
555
843
  iteration: iter,
556
844
  });
557
- reactionEvent = await ops.updateItem(reactionEvent.id, {
558
- ...reactionEvent,
559
- content: {
560
- ...reactionEvent.content,
561
- parts: [...reactionPartsBeforeStep, ...normalizedParts],
562
- },
563
- status: "pending",
564
- }, { executionId, contextId: String(currentContext.id) });
845
+ reactionEvent = saved.reactionEvent;
565
846
  };
566
- const { assistantEvent, actionRequests, messagesForModel } = await measureBenchmark(params.__benchmark, `${stagePrefix}.reactorMs`, async () => await reactor({
567
- env: params.env,
847
+ const reactionResult = await measureBenchmark(params.__benchmark, `${stagePrefix}.reactorMs`, async () => await reactor({
848
+ runtime: runtimeHandle,
568
849
  context: updatedContext,
569
850
  contextIdentifier: activeContextSelector,
851
+ resources: contextResources,
852
+ events: expandedEvents,
570
853
  triggerEvent,
571
- model: story.getModel(updatedContext, params.env),
854
+ model: story.getModel(updatedContext, env, runtimeHandle),
572
855
  systemPrompt,
573
856
  actions: toolsAll,
574
- toolsForModel,
575
857
  skills: skillsAll,
576
858
  eventId: reactionEventId,
577
859
  executionId,
578
860
  contextId: String(currentContext.id),
579
- stepId: String(stepCreate.stepId),
861
+ stepId: String(openedStep.stepId),
580
862
  iteration: iter,
581
863
  maxModelSteps,
582
864
  // Only emit a `start` chunk once per story turn.
583
- sendStart: !silent && iter === 0,
584
- silent,
865
+ sendStart: iter === 0,
585
866
  contextStepStream: currentStepStream?.stream,
586
867
  writable,
587
868
  persistReactionParts,
588
869
  }));
870
+ const { assistantEvent, actionRequests, messagesForModel } = reactionResult;
589
871
  const reviewRequests = actionRequests.length > 0
590
872
  ? actionRequests.flatMap((actionRequest) => {
591
873
  const toolDef = toolsAll[actionRequest.actionName];
@@ -614,21 +896,34 @@ export class ContextEngine {
614
896
  parts: stepParts,
615
897
  },
616
898
  };
617
- await measureBenchmark(params.__benchmark, `${stagePrefix}.saveStepPartsMs`, async () => await ops.saveContextPartsStep({
618
- 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,
619
912
  parts: stepParts,
913
+ reactionEventId: reactionEvent.id,
914
+ reactionEvent: nextReactionEvent,
620
915
  executionId,
621
916
  contextId: String(currentContext.id),
622
917
  iteration: iter,
623
918
  }));
919
+ reactionEvent = appendedReactorOutput.reactionEvent;
624
920
  await emitContextEvents({
625
- silent,
626
921
  writable,
627
922
  events: stepParts.map((part, idx) => ({
628
923
  type: "part.created",
629
924
  at: nowIso(),
630
- partKey: `${String(stepCreate.stepId)}:${idx}`,
631
- stepId: String(stepCreate.stepId),
925
+ partKey: `${String(openedStep.stepId)}:${idx}`,
926
+ stepId: String(openedStep.stepId),
632
927
  idx,
633
928
  partType: part && typeof part.type === "string"
634
929
  ? String(part.type)
@@ -636,60 +931,26 @@ export class ContextEngine {
636
931
  ...summarizePartPreview(part),
637
932
  })),
638
933
  });
639
- // Persist/append the aggregated reaction event (stable `reactionEventId` for the execution).
640
- const nextAssistantParts = Array.isArray(assistantEventEffective.content?.parts)
641
- ? assistantEventEffective.content.parts
642
- : [];
643
- const nextReactionEvent = {
644
- ...reactionEvent,
645
- content: {
646
- ...reactionEvent.content,
647
- parts: [...reactionPartsBeforeStep, ...nextAssistantParts],
648
- },
649
- status: "pending",
650
- };
651
- reactionEvent = await measureBenchmark(params.__benchmark, `${stagePrefix}.persistAssistantReactionMs`, async () => await ops.updateItem(reactionEvent.id, nextReactionEvent, { executionId, contextId: String(currentContext.id) }));
652
- if (currentStepStream) {
653
- await closePersistedContextStepStream({
654
- env: params.env,
655
- session: currentStepStream,
656
- });
657
- currentStepStream = null;
934
+ if (reactionResult.reactor?.kind) {
935
+ updatedContext = await measureBenchmark(params.__benchmark, `${stagePrefix}.persistReactorStateMs`, async () => await ops.updateContextReactor(activeContextSelector, {
936
+ kind: reactionResult.reactor.kind,
937
+ state: {
938
+ ...(reactionResult.reactor.state ?? {}),
939
+ updatedAt: nowIso(),
940
+ },
941
+ }));
658
942
  }
659
943
  story.opts.onEventCreated?.(assistantEventEffective);
660
- const firstActionRequest = actionRequests?.[0];
661
- await measureBenchmark(params.__benchmark, `${stagePrefix}.markStepRunningMs`, async () => await ops.updateContextStep({
662
- stepId: stepCreate.stepId,
663
- patch: firstActionRequest
664
- ? {
665
- kind: "action_execute",
666
- actionName: typeof firstActionRequest.actionName === "string"
667
- ? firstActionRequest.actionName
668
- : undefined,
669
- actionInput: firstActionRequest.input,
670
- }
671
- : {
672
- kind: "message",
673
- },
674
- executionId,
675
- contextId: String(currentContext.id),
676
- iteration: iter,
677
- }));
678
944
  await emitContextEvents({
679
- silent,
680
945
  writable,
681
946
  events: [
682
947
  {
683
948
  type: "step.updated",
684
949
  at: nowIso(),
685
- stepId: String(stepCreate.stepId),
950
+ stepId: String(openedStep.stepId),
686
951
  executionId,
687
952
  iteration: iter,
688
953
  status: "running",
689
- kind: firstActionRequest ? "action_execute" : "message",
690
- actionName: firstActionRequest && typeof firstActionRequest.actionName === "string"
691
- ? firstActionRequest.actionName
692
- : undefined,
693
954
  },
694
955
  ],
695
956
  });
@@ -697,50 +958,45 @@ export class ContextEngine {
697
958
  if (!actionRequests.length) {
698
959
  const endResult = await story.callOnEnd(assistantEventEffective);
699
960
  if (endResult) {
700
- // Mark iteration step completed (no tools)
701
- await measureBenchmark(params.__benchmark, `${stagePrefix}.finalizeMessageStepMs`, async () => await ops.updateContextStep({
702
- stepId: stepCreate.stepId,
703
- patch: {
704
- status: "completed",
705
- kind: "message",
706
- actionRequests: [],
707
- actionResults: [],
708
- continueLoop: false,
709
- },
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,
710
971
  executionId,
711
972
  contextId: String(currentContext.id),
712
973
  iteration: iter,
713
974
  }));
975
+ currentStepStream = null;
976
+ currentStepId = null;
977
+ reactionEvent = finalized.reactionEvent ?? completedReactionEvent;
714
978
  await emitContextEvents({
715
- silent,
716
979
  writable,
717
980
  events: [
718
981
  {
719
982
  type: "step.updated",
720
983
  at: nowIso(),
721
- stepId: String(stepCreate.stepId),
984
+ stepId: String(openedStep.stepId),
722
985
  executionId,
723
986
  iteration: iter,
724
987
  status: "completed",
725
- kind: "message",
726
988
  },
727
989
  {
728
990
  type: "step.completed",
729
991
  at: nowIso(),
730
- stepId: String(stepCreate.stepId),
992
+ stepId: String(openedStep.stepId),
731
993
  executionId,
732
994
  iteration: iter,
733
995
  status: "completed",
734
996
  },
735
997
  ],
736
998
  });
737
- // Mark reaction event completed
738
- await measureBenchmark(params.__benchmark, `${stagePrefix}.completeReactionMs`, async () => await ops.updateItem(reactionEventId, {
739
- ...reactionEvent,
740
- status: "completed",
741
- }, { executionId, contextId: String(currentContext.id) }));
742
999
  await emitContextEvents({
743
- silent,
744
1000
  writable,
745
1001
  events: [
746
1002
  {
@@ -757,7 +1013,6 @@ export class ContextEngine {
757
1013
  execution = { ...execution, status: "completed" };
758
1014
  updatedContext = { ...updatedContext, status: "closed" };
759
1015
  await emitContextEvents({
760
- silent,
761
1016
  writable,
762
1017
  events: [
763
1018
  {
@@ -775,19 +1030,15 @@ export class ContextEngine {
775
1030
  },
776
1031
  ],
777
1032
  });
778
- if (!silent) {
779
- await closeContextStream({ preventClose, sendFinish, writable });
780
- }
781
- reactionEvent = {
782
- ...reactionEvent,
783
- status: "completed",
784
- };
785
- return {
1033
+ await closeContextStream({ preventClose, sendFinish, writable });
1034
+ const result = {
786
1035
  context: updatedContext,
787
1036
  trigger,
788
1037
  reaction: reactionEvent,
789
1038
  execution,
790
1039
  };
1040
+ await resumeReturnValueHook({ ok: true, result });
1041
+ return result;
791
1042
  }
792
1043
  }
793
1044
  // Execute actions (workflow context; action implementations decide step vs workflow)
@@ -807,9 +1058,8 @@ export class ContextEngine {
807
1058
  const { createHook, createWebhook } = await import("workflow");
808
1059
  const toolCallId = String(actionRequest.actionRef);
809
1060
  const hookToken = toolApprovalHookToken({ executionId, toolCallId });
810
- const webhookToken = toolApprovalWebhookToken({ executionId, toolCallId });
811
1061
  const hook = createHook({ token: hookToken });
812
- const webhook = createWebhook({ token: webhookToken });
1062
+ const webhook = createWebhook();
813
1063
  const approvalOrRequest = await Promise.race([
814
1064
  hook.then((approval) => ({ source: "hook", approval })),
815
1065
  webhook.then((request) => ({ source: "webhook", request })),
@@ -831,14 +1081,21 @@ export class ContextEngine {
831
1081
  actionInput = approval.args;
832
1082
  }
833
1083
  }
834
- const output = await toolDef.execute(actionInput, {
835
- toolCallId: actionRequest.actionRef,
836
- messages: messagesForModel,
837
- eventId: reactionEventId,
838
- executionId,
839
- triggerEventId,
840
- contextId: currentContext.id,
841
- });
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
+ }]);
842
1099
  return { actionRequest, success: true, output };
843
1100
  }
844
1101
  catch (e) {
@@ -850,26 +1107,19 @@ export class ContextEngine {
850
1107
  };
851
1108
  }
852
1109
  })));
853
- // 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.
854
1111
  let finalizedStepParts = Array.isArray(stepParts) ? [...stepParts] : [];
855
1112
  for (const r of actionResults) {
856
- finalizedStepParts = applyToolExecutionResultToParts(finalizedStepParts, {
857
- toolCallId: r.actionRequest.actionRef,
858
- toolName: r.actionRequest.actionName,
1113
+ finalizedStepParts = applyActionExecutionResultToParts(finalizedStepParts, {
1114
+ actionCallId: r.actionRequest.actionRef,
1115
+ actionName: r.actionRequest.actionName,
859
1116
  }, {
860
1117
  success: Boolean(r.success),
861
1118
  result: r.output,
862
1119
  message: r.errorText,
863
1120
  });
864
1121
  }
865
- await measureBenchmark(params.__benchmark, `${stagePrefix}.saveFinalStepPartsMs`, async () => await ops.saveContextPartsStep({
866
- stepId: stepCreate.stepId,
867
- parts: finalizedStepParts,
868
- executionId,
869
- contextId: String(currentContext.id),
870
- iteration: iter,
871
- }));
872
- reactionEvent = {
1122
+ const pendingReactionEvent = {
873
1123
  ...reactionEvent,
874
1124
  content: {
875
1125
  ...reactionEvent.content,
@@ -879,85 +1129,70 @@ export class ContextEngine {
879
1129
  },
880
1130
  status: "pending",
881
1131
  };
882
- // Callback for observability/integration
883
- for (const r of actionResults) {
884
- await story.opts.onActionExecuted?.({
885
- actionRequest: r.actionRequest,
886
- success: r.success,
887
- output: r.output,
888
- errorText: r.errorText,
889
- eventId: reactionEventId,
890
- executionId,
891
- });
892
- }
893
- // Stop/continue boundary: allow the Context to decide if the loop should continue.
894
- // IMPORTANT: we call this after tool results have been merged into the persisted `reactionEvent`,
895
- // so stories can inspect `reactionEvent.content.parts` deterministically.
896
- const continueLoop = await measureBenchmark(params.__benchmark, `${stagePrefix}.shouldContinueMs`, async () => await story.shouldContinue({
897
- env: params.env,
898
- context: updatedContext,
899
- reactionEvent,
900
- assistantEvent: assistantEventEffective,
901
- actionRequests,
1132
+ const completedStep = await measureBenchmark(params.__benchmark, `${stagePrefix}.completeExecutionStepMs`, async () => await ops.completeExecutionStep({
1133
+ session: currentStepStream,
1134
+ stepId: openedStep.stepId,
1135
+ parts: finalizedStepParts,
902
1136
  actionResults: actionResults,
903
- }));
904
- // Persist per-iteration step outcome (tools + continue signal)
905
- await measureBenchmark(params.__benchmark, `${stagePrefix}.completeStepMs`, async () => await ops.updateContextStep({
906
- stepId: stepCreate.stepId,
907
- patch: {
908
- status: "completed",
909
- kind: actionRequests?.length ? "action_result" : "message",
910
- actionName: typeof actionResults?.[0]?.actionRequest?.actionName === "string"
911
- ? actionResults[0].actionRequest.actionName
912
- : undefined,
913
- actionInput: actionResults?.[0]?.actionRequest?.input,
914
- actionOutput: actionResults?.[0]?.success === true
915
- ? actionResults[0]?.output
916
- : undefined,
917
- actionError: actionResults?.[0]?.success === false
918
- ? String(actionResults[0]?.errorText ?? "action_execution_failed")
919
- : undefined,
920
- actionRequests,
921
- actionResults,
922
- continueLoop: continueLoop !== false,
923
- },
1137
+ stepStatus: "completed",
1138
+ reactionEventId,
1139
+ reactionEvent: pendingReactionEvent,
924
1140
  executionId,
925
1141
  contextId: String(currentContext.id),
926
1142
  iteration: iter,
927
1143
  }));
1144
+ currentStepStream = null;
1145
+ currentStepId = null;
1146
+ reactionEvent = completedStep.reactionEvent ?? pendingReactionEvent;
1147
+ await emitContextEvents({
1148
+ writable,
1149
+ events: completedStep.actionResultChunkEvents,
1150
+ });
928
1151
  await emitContextEvents({
929
- silent,
930
1152
  writable,
931
1153
  events: [
932
1154
  {
933
1155
  type: "step.updated",
934
1156
  at: nowIso(),
935
- stepId: String(stepCreate.stepId),
1157
+ stepId: String(openedStep.stepId),
936
1158
  executionId,
937
1159
  iteration: iter,
938
1160
  status: "completed",
939
- kind: actionRequests?.length ? "action_result" : "message",
940
- actionName: typeof actionResults?.[0]?.actionRequest?.actionName === "string"
941
- ? actionResults[0].actionRequest.actionName
942
- : undefined,
943
1161
  },
944
1162
  {
945
1163
  type: "step.completed",
946
1164
  at: nowIso(),
947
- stepId: String(stepCreate.stepId),
1165
+ stepId: String(openedStep.stepId),
948
1166
  executionId,
949
1167
  iteration: iter,
950
1168
  status: "completed",
951
1169
  },
952
1170
  ],
953
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
+ }));
954
1194
  if (continueLoop !== false) {
955
- reactionEvent = await measureBenchmark(params.__benchmark, `${stagePrefix}.persistPendingReactionMs`, async () => await ops.updateItem(reactionEventId, {
956
- ...reactionEvent,
957
- status: "pending",
958
- }, { executionId, contextId: String(currentContext.id) }));
959
1195
  await emitContextEvents({
960
- silent,
961
1196
  writable,
962
1197
  events: [
963
1198
  {
@@ -972,12 +1207,11 @@ export class ContextEngine {
972
1207
  });
973
1208
  }
974
1209
  if (continueLoop === false) {
975
- await measureBenchmark(params.__benchmark, `${stagePrefix}.completeReactionMs`, async () => await ops.updateItem(reactionEventId, {
1210
+ reactionEvent = {
976
1211
  ...reactionEvent,
977
1212
  status: "completed",
978
- }, { executionId, contextId: String(currentContext.id) }));
1213
+ };
979
1214
  await emitContextEvents({
980
- silent,
981
1215
  writable,
982
1216
  events: [
983
1217
  {
@@ -990,11 +1224,14 @@ export class ContextEngine {
990
1224
  },
991
1225
  ],
992
1226
  });
993
- 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
+ }));
994
1232
  execution = { ...execution, status: "completed" };
995
1233
  updatedContext = { ...updatedContext, status: "closed" };
996
1234
  await emitContextEvents({
997
- silent,
998
1235
  writable,
999
1236
  events: [
1000
1237
  {
@@ -1012,19 +1249,15 @@ export class ContextEngine {
1012
1249
  },
1013
1250
  ],
1014
1251
  });
1015
- if (!silent) {
1016
- await closeContextStream({ preventClose, sendFinish, writable });
1017
- }
1018
- reactionEvent = {
1019
- ...reactionEvent,
1020
- status: "completed",
1021
- };
1022
- return {
1252
+ await closeContextStream({ preventClose, sendFinish, writable });
1253
+ const result = {
1023
1254
  context: updatedContext,
1024
1255
  trigger,
1025
1256
  reaction: reactionEvent,
1026
1257
  execution,
1027
1258
  };
1259
+ await resumeReturnValueHook({ ok: true, result });
1260
+ return result;
1028
1261
  }
1029
1262
  }
1030
1263
  throw new Error(`ContextEngine: maxIterations reached (${maxIterations}) without completion`);
@@ -1033,7 +1266,7 @@ export class ContextEngine {
1033
1266
  if (currentStepStream) {
1034
1267
  try {
1035
1268
  await abortPersistedContextStepStream({
1036
- env: params.env,
1269
+ runtime: runtimeHandle,
1037
1270
  session: currentStepStream,
1038
1271
  reason: error instanceof Error ? error.message : String(error),
1039
1272
  });
@@ -1059,7 +1292,6 @@ export class ContextEngine {
1059
1292
  contextId: String(currentContext.id),
1060
1293
  }));
1061
1294
  await emitContextEvents({
1062
- silent,
1063
1295
  writable,
1064
1296
  events: [
1065
1297
  {
@@ -1078,6 +1310,10 @@ export class ContextEngine {
1078
1310
  }
1079
1311
  }
1080
1312
  await failExecution();
1313
+ await resumeReturnValueHook({
1314
+ ok: false,
1315
+ error: serializeContextReturnValueError(error),
1316
+ }).catch(() => null);
1081
1317
  throw error;
1082
1318
  }
1083
1319
  }