@ekairos/events 1.22.4-beta.feature-events-package-refactor.0 → 1.22.5-beta.development.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +59 -25
  2. package/dist/context.builder.d.ts +4 -0
  3. package/dist/context.builder.js +9 -0
  4. package/dist/context.d.ts +2 -1
  5. package/dist/context.engine.d.ts +26 -3
  6. package/dist/context.engine.js +115 -43
  7. package/dist/context.events.d.ts +20 -0
  8. package/dist/context.events.js +341 -7
  9. package/dist/context.parts.d.ts +241 -0
  10. package/dist/context.parts.js +360 -0
  11. package/dist/context.runtime.d.ts +11 -0
  12. package/dist/context.runtime.js +21 -0
  13. package/dist/context.skill.d.ts +9 -0
  14. package/dist/context.skill.js +1 -0
  15. package/dist/context.step-stream.d.ts +26 -0
  16. package/dist/context.step-stream.js +59 -0
  17. package/dist/context.store.d.ts +5 -0
  18. package/dist/context.toolcalls.js +55 -11
  19. package/dist/index.d.ts +7 -2
  20. package/dist/index.js +3 -1
  21. package/dist/reactors/ai-sdk.reactor.d.ts +4 -0
  22. package/dist/reactors/ai-sdk.reactor.js +7 -2
  23. package/dist/{steps/reaction.steps.d.ts → reactors/ai-sdk.step.d.ts} +11 -6
  24. package/dist/{steps/reaction.steps.js → reactors/ai-sdk.step.js} +102 -82
  25. package/dist/reactors/types.d.ts +5 -0
  26. package/dist/runtime.d.ts +1 -0
  27. package/dist/runtime.js +1 -0
  28. package/dist/schema.js +24 -2
  29. package/dist/steps/store.steps.d.ts +55 -22
  30. package/dist/steps/store.steps.js +71 -78
  31. package/dist/steps/stream.steps.d.ts +76 -0
  32. package/dist/steps/stream.steps.js +239 -2
  33. package/dist/steps/trace.steps.d.ts +2 -0
  34. package/dist/steps/trace.steps.js +5 -2
  35. package/dist/stores/instant.store.d.ts +2 -0
  36. package/dist/stores/instant.store.js +100 -5
  37. package/package.json +9 -4
package/README.md CHANGED
@@ -2,53 +2,87 @@
2
2
 
3
3
  Context-first durable execution runtime for Ekairos.
4
4
 
5
- ## Surface
5
+ ## What this package does
6
6
 
7
- - `createContext`, `context`, `ContextEngine`
8
- - `createAiSdkReactor`, `createScriptedReactor`
9
- - `useContext`
7
+ - creates durable contexts with `createContext(...)`
8
+ - persists executions, steps, parts, and items
9
+ - runs direct or durable `react(...)` loops
10
+ - adapts model/tool output into canonical `event_parts`
11
+
12
+ ## Main APIs
13
+
14
+ - `createContext`
15
+ - `ContextEngine`
16
+ - `createAiSdkReactor`
17
+ - `createScriptedReactor`
18
+ - `runContextReactionDirect`
10
19
  - `eventsDomain`
11
- - `getContextRuntime`, `getContextEnv`, `registerContextEnv`
12
20
 
13
21
  ## Runtime model
14
22
 
23
+ Canonical entities:
24
+
15
25
  - `event_contexts`
16
26
  - `event_items`
17
27
  - `event_executions`
18
28
  - `event_steps`
19
29
  - `event_parts`
20
- - `event_trace_events`
21
- - `event_trace_runs`
22
- - `event_trace_spans`
23
-
24
- The aggregate is `context`. Executions, steps, parts, and items are scoped to a context.
30
+ - `event_trace_*`
25
31
 
26
- ## Install
27
-
28
- ```bash
29
- pnpm add @ekairos/events
30
- ```
32
+ `event_parts` is the source of truth for replay.
31
33
 
32
34
  ## Example
33
35
 
34
36
  ```ts
35
- import { createContext, createAiSdkReactor } from "@ekairos/events";
37
+ import { createContext } from "@ekairos/events";
36
38
 
37
- type Env = { orgId: string };
38
-
39
- export const supportContext = createContext<Env>("support.agent")
39
+ const supportContext = createContext<{ orgId: string }>("support.agent")
40
40
  .context((stored, env) => ({
41
- orgId: env.orgId,
42
41
  ...stored.content,
42
+ orgId: env.orgId,
43
43
  }))
44
44
  .narrative(() => "You are a precise assistant.")
45
45
  .actions(() => ({}))
46
- .reactor(createAiSdkReactor())
47
46
  .build();
48
47
  ```
49
48
 
50
- ## Notes
49
+ Run directly:
50
+
51
+ ```ts
52
+ await supportContext.react(triggerEvent, {
53
+ runtime,
54
+ context: { key: "support:org_123" },
55
+ });
56
+ ```
57
+
58
+ Run durably:
59
+
60
+ ```ts
61
+ const shell = await supportContext.react(triggerEvent, {
62
+ runtime,
63
+ context: { key: "support:org_123" },
64
+ durable: true,
65
+ });
66
+
67
+ const final = await shell.run?.returnValue;
68
+ ```
69
+
70
+ ## Tool execution model
71
+
72
+ Context tools now receive runtime-aware execution context.
73
+ That lets a tool do this inside `"use step"`:
51
74
 
52
- - Public continuity is context-based.
53
- - Provider-specific IDs such as `providerContextId` may still exist when an upstream provider requires them.
54
- - Runtime wiring for stores lives under `@ekairos/events/runtime`.
75
+ ```ts
76
+ async function execute(input, ctx) {
77
+ "use step";
78
+ const domain = await ctx.runtime.use(myDomain);
79
+ return await domain.actions.doSomething(input);
80
+ }
81
+ ```
82
+
83
+ ## Tests
84
+
85
+ ```bash
86
+ pnpm --filter @ekairos/events test
87
+ pnpm --filter @ekairos/events test:workflow
88
+ ```
@@ -1,4 +1,5 @@
1
1
  import type { ContextEnvironment } from "./context.config.js";
2
+ import type { ContextSkillPackage } from "./context.skill.js";
2
3
  import { ContextEngine, type ContextModelInit, type ContextOptions, type ContextTool, type ShouldContinue, type ContextShouldContinueArgs, type ContextReactParams } from "./context.engine.js";
3
4
  import type { ContextReactor } from "./context.reactor.js";
4
5
  import type { ContextItem, StoredContext } from "./context.store.js";
@@ -7,6 +8,7 @@ export interface ContextConfig<Context, Env extends ContextEnvironment = Context
7
8
  context: (context: StoredContext<Context>, env: Env) => Promise<Context> | Context;
8
9
  expandEvents?: (events: ContextItem[], context: StoredContext<Context>, env: Env) => Promise<ContextItem[]> | ContextItem[];
9
10
  narrative: (context: StoredContext<Context>, env: Env) => Promise<string> | string;
11
+ skills?: (context: StoredContext<Context>, env: Env) => Promise<ContextSkillPackage[]> | ContextSkillPackage[];
10
12
  actions: (context: StoredContext<Context>, env: Env) => Promise<Record<string, ContextTool>> | Record<string, ContextTool>;
11
13
  /**
12
14
  * @deprecated Use `actions()` instead.
@@ -25,6 +27,7 @@ export declare function context<Context, Env extends ContextEnvironment = Contex
25
27
  type AnyContextInitializer<Env extends ContextEnvironment> = (context: StoredContext<any>, env: Env) => Promise<any> | any;
26
28
  type InferContextFromInitializer<I extends AnyContextInitializer<any>> = Awaited<ReturnType<I>>;
27
29
  type BuilderSystemPrompt<Context, Env extends ContextEnvironment> = (context: StoredContext<Context>, env: Env) => Promise<string> | string;
30
+ type BuilderSkills<Context, Env extends ContextEnvironment> = (context: StoredContext<Context>, env: Env) => Promise<ContextSkillPackage[]> | ContextSkillPackage[];
28
31
  type BuilderTools<Context, Env extends ContextEnvironment> = (context: StoredContext<Context>, env: Env) => Promise<Record<string, ContextTool>> | Record<string, ContextTool>;
29
32
  type BuilderExpandEvents<Context, Env extends ContextEnvironment> = (events: ContextItem[], context: StoredContext<Context>, env: Env) => Promise<ContextItem[]> | ContextItem[];
30
33
  type BuilderShouldContinue<Context, Env extends ContextEnvironment> = (args: ContextShouldContinueArgs<Context, Env>) => Promise<ShouldContinue> | ShouldContinue;
@@ -38,6 +41,7 @@ type FluentContextBuilder<Context, Env extends ContextEnvironment> = {
38
41
  expandEvents(fn: BuilderExpandEvents<Context, Env>): FluentContextBuilder<Context, Env>;
39
42
  narrative(fn: BuilderSystemPrompt<Context, Env>): FluentContextBuilder<Context, Env>;
40
43
  system(fn: BuilderSystemPrompt<Context, Env>): FluentContextBuilder<Context, Env>;
44
+ skills(fn: BuilderSkills<Context, Env>): FluentContextBuilder<Context, Env>;
41
45
  actions(fn: BuilderTools<Context, Env>): FluentContextBuilder<Context, Env>;
42
46
  tools(fn: BuilderTools<Context, Env>): FluentContextBuilder<Context, Env>;
43
47
  model(model: BuilderModel<Context, Env>): FluentContextBuilder<Context, Env>;
@@ -22,6 +22,11 @@ export function context(config) {
22
22
  return config.narrative(contextValue, env);
23
23
  throw new Error("Context config is missing narrative()");
24
24
  }
25
+ async buildSkills(contextValue, env) {
26
+ if (config.skills)
27
+ return config.skills(contextValue, env);
28
+ return [];
29
+ }
25
30
  async buildTools(contextValue, env) {
26
31
  if (config.actions)
27
32
  return config.actions(contextValue, env);
@@ -84,6 +89,10 @@ export function createContext(key) {
84
89
  fluentState.narrative = system;
85
90
  return builder;
86
91
  },
92
+ skills(skillsFactory) {
93
+ fluentState.skills = skillsFactory;
94
+ return builder;
95
+ },
87
96
  actions(actionsFactory) {
88
97
  fluentState.actions = actionsFactory;
89
98
  return builder;
package/dist/context.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { ContextEngine, type ContextOptions, type ContextStreamOptions, type ShouldContinue, type ContextShouldContinueArgs, type ContextReactParams, type ContextReactResult, type ContextDurableWorkflowPayload, type ContextDurableWorkflowFunction, type ContextModelInit, type ContextTool, runContextReactionDirect, } from "./context.engine.js";
1
+ export { ContextEngine, type ContextOptions, type ContextStreamOptions, type ShouldContinue, type ContextShouldContinueArgs, type ContextReactParams, type ContextReactResult, type ContextWorkflowRun, type ContextDurableWorkflowPayload, type ContextDurableWorkflowFunction, type ContextModelInit, type ContextTool, type ContextToolExecuteContext, runContextReactionDirect, } from "./context.engine.js";
2
2
  export { context, createContext, type ContextConfig, type ContextInstance, type RegistrableContextBuilder, } from "./context.builder.js";
3
3
  export { createAiSdkReactor, createScriptedReactor, type CreateAiSdkReactorOptions, type CreateScriptedReactorOptions, type ScriptedReactorStep, type ContextReactor, type ContextReactorParams, type ContextReactionResult, type ContextActionRequest, type ContextReactionLLM, } from "./context.reactor.js";
4
+ export type { ContextSkillPackage, ContextSkillPackageFile, } from "./context.skill.js";
@@ -1,6 +1,8 @@
1
- import type { Tool, UIMessageChunk } from "ai";
1
+ import type { ModelMessage, Tool, UIMessageChunk } from "ai";
2
2
  import type { ContextEnvironment } from "./context.config.js";
3
+ import type { ContextRuntime } from "./context.runtime.js";
3
4
  import type { ContextExecution, ContextItem, ContextIdentifier, StoredContext } from "./context.store.js";
5
+ import type { ContextSkillPackage } from "./context.skill.js";
4
6
  import { type ContextReactor } from "./context.reactor.js";
5
7
  import { getClientResumeHookUrl, toolApprovalHookToken, toolApprovalWebhookToken } from "./context.hooks.js";
6
8
  export interface ContextOptions<Context = any, Env extends ContextEnvironment = ContextEnvironment> {
@@ -67,7 +69,7 @@ export interface ContextStreamOptions {
67
69
  */
68
70
  export type ContextModelInit = string | (() => Promise<any>);
69
71
  export type ContextReactParams<Env extends ContextEnvironment = ContextEnvironment> = {
70
- env: Env;
72
+ runtime: ContextRuntime<Env>;
71
73
  /**
72
74
  * Context selector (exclusive: `{ id }` OR `{ key }`).
73
75
  * - `{ id }` resolves a concrete context id.
@@ -94,16 +96,36 @@ export type ContextReactResult<Context = any> = {
94
96
  trigger: ContextItem;
95
97
  reaction: ContextItem;
96
98
  execution: ContextExecution;
99
+ run?: ContextWorkflowRun<Context>;
100
+ };
101
+ export type ContextWorkflowRun<Context = any> = {
102
+ runId: string;
103
+ status: Promise<"pending" | "running" | "completed" | "failed" | "cancelled">;
104
+ returnValue: Promise<ContextReactResult<Context>>;
97
105
  };
98
106
  export type ContextDurableWorkflowPayload<Env extends ContextEnvironment = ContextEnvironment> = {
99
107
  contextKey: string;
100
- env: Env;
108
+ runtime: ContextRuntime<Env>;
101
109
  context?: ContextIdentifier | null;
102
110
  triggerEvent: ContextItem;
103
111
  options?: Omit<ContextStreamOptions, "writable">;
104
112
  bootstrap: NonNullable<ContextReactParams<Env>["__bootstrap"]>;
105
113
  };
106
114
  export type ContextDurableWorkflowFunction<Context = any, Env extends ContextEnvironment = ContextEnvironment> = (payload: ContextDurableWorkflowPayload<Env>) => Promise<ContextReactResult<Context>>;
115
+ export type ContextToolExecuteContext<Context = any, Env extends ContextEnvironment = ContextEnvironment> = {
116
+ runtime: ContextRuntime<Env>;
117
+ env: Env;
118
+ context: StoredContext<Context>;
119
+ contextIdentifier: ContextIdentifier;
120
+ toolCallId: string;
121
+ messages: ModelMessage[];
122
+ eventId: string;
123
+ executionId: string;
124
+ triggerEventId: string;
125
+ contextId: string;
126
+ stepId: string;
127
+ iteration: number;
128
+ };
107
129
  export { toolApprovalHookToken, toolApprovalWebhookToken, getClientResumeHookUrl };
108
130
  /**
109
131
  * Context-level tool type.
@@ -168,6 +190,7 @@ export declare abstract class ContextEngine<Context, Env extends ContextEnvironm
168
190
  protected abstract initialize(context: StoredContext<Context>, env: Env): Promise<Context> | Context;
169
191
  protected abstract buildSystemPrompt(context: StoredContext<Context>, env: Env): Promise<string> | string;
170
192
  protected abstract buildTools(context: StoredContext<Context>, env: Env): Promise<Record<string, ContextTool>> | Record<string, ContextTool>;
193
+ protected buildSkills(_context: StoredContext<Context>, _env: Env): Promise<ContextSkillPackage[]>;
171
194
  /**
172
195
  * First-class event expansion stage (runs on every iteration of the durable loop).
173
196
  *
@@ -1,9 +1,10 @@
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
3
  import { applyToolExecutionResultToParts } from "./context.toolcalls.js";
4
+ import { isContextPartEnvelope, normalizePartsForPersistence, } from "./context.parts.js";
4
5
  import { toolsToModelTools } from "./tools-to-model-tools.js";
5
6
  import { createAiSdkReactor, } from "./context.reactor.js";
6
- import { closeContextStream, } from "./steps/stream.steps.js";
7
+ import { abortPersistedContextStepStream, closePersistedContextStepStream, createPersistedContextStepStream, closeContextStream, } from "./steps/stream.steps.js";
7
8
  import { completeExecution, createContextStep, initializeContext, saveTriggerAndCreateExecution, saveContextPartsStep, updateContextContent, updateContextStatus, updateItem, updateContextStep, } from "./steps/store.steps.js";
8
9
  import { getClientResumeHookUrl, toolApprovalHookToken, toolApprovalWebhookToken, } from "./context.hooks.js";
9
10
  import { getContextDurableWorkflow } from "./context.durable.js";
@@ -22,6 +23,20 @@ function clipPreview(value, max = 240) {
22
23
  function summarizePartPreview(part) {
23
24
  if (!part || typeof part !== "object")
24
25
  return {};
26
+ 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;
34
+ return {
35
+ partPreview: preview ? clipPreview(preview) : undefined,
36
+ partState: state,
37
+ partToolCallId: toolCallId,
38
+ };
39
+ }
25
40
  const row = part;
26
41
  const partType = typeof row.type === "string" ? row.type : "";
27
42
  const partState = typeof row.state === "string" ? row.state : undefined;
@@ -74,9 +89,8 @@ async function readActiveWorkflowRunId() {
74
89
  return null;
75
90
  }
76
91
  }
77
- async function createRuntimeOps(env, benchmark) {
78
- const { getContextRuntime } = await import("./runtime.js");
79
- const runtime = await getContextRuntime(env);
92
+ async function createRuntimeOps(runtimeHandle, benchmark) {
93
+ const runtime = await getContextRuntimeServices(runtimeHandle);
80
94
  const { db } = runtime;
81
95
  const { InstantStore } = await import("./stores/instant.store.js");
82
96
  const requireContextId = (contextIdentifier) => {
@@ -240,33 +254,36 @@ async function createRuntimeOps(env, benchmark) {
240
254
  },
241
255
  };
242
256
  }
243
- async function createWorkflowOps(env) {
257
+ async function createWorkflowOps(runtime) {
258
+ const env = runtime.env;
244
259
  return {
245
- initializeContext: async (contextIdentifier, opts) => await initializeContext(env, contextIdentifier, opts),
246
- updateContextContent: async (contextIdentifier, content) => await updateContextContent(env, contextIdentifier, content),
247
- updateContextStatus: async (contextIdentifier, status) => await updateContextStatus(env, contextIdentifier, status),
248
- saveTriggerAndCreateExecution: async ({ contextIdentifier, triggerEvent }) => await saveTriggerAndCreateExecution({ env, contextIdentifier, triggerEvent }),
249
- createContextStep: async ({ executionId, iteration }) => await createContextStep({ env, executionId, iteration }),
250
- updateContextStep: async (params) => await updateContextStep({ env, ...params }),
251
- saveContextPartsStep: async (params) => await saveContextPartsStep({ env, ...params }),
252
- updateItem: async (itemId, item, opts) => await updateItem(env, itemId, item, opts),
253
- completeExecution: async (contextIdentifier, executionId, status) => await completeExecution(env, contextIdentifier, executionId, status),
260
+ initializeContext: async (contextIdentifier, opts) => await initializeContext({ runtime, contextIdentifier, opts }),
261
+ updateContextContent: async (contextIdentifier, content) => await updateContextContent({ runtime, contextIdentifier, content }),
262
+ updateContextStatus: async (contextIdentifier, status) => await updateContextStatus({ runtime, contextIdentifier, status }),
263
+ saveTriggerAndCreateExecution: async ({ contextIdentifier, triggerEvent }) => await saveTriggerAndCreateExecution({ runtime, contextIdentifier, triggerEvent }),
264
+ createContextStep: async ({ executionId, iteration }) => await createContextStep({ runtime, executionId, iteration }),
265
+ updateContextStep: async (params) => await updateContextStep({ runtime, ...params }),
266
+ saveContextPartsStep: async (params) => await saveContextPartsStep({ runtime, ...params }),
267
+ updateItem: async (itemId, item, opts) => await updateItem({ runtime, eventId: itemId, event: item, opts }),
268
+ completeExecution: async (contextIdentifier, executionId, status) => await completeExecution({ runtime, contextIdentifier, executionId, status }),
254
269
  };
255
270
  }
256
- async function getContextEngineOps(env, benchmark) {
271
+ async function getContextEngineOps(runtime, benchmark) {
272
+ const env = runtime.env;
257
273
  const workflowRunId = await readActiveWorkflowRunId();
258
274
  if (workflowRunId) {
259
- registerContextEnv(env, workflowRunId);
260
- return await createWorkflowOps(env);
275
+ return await createWorkflowOps(runtime);
261
276
  }
262
- registerContextEnv(env);
263
- return await createRuntimeOps(env, benchmark);
277
+ return await createRuntimeOps(runtime, benchmark);
264
278
  }
265
279
  export class ContextEngine {
266
280
  constructor(opts = {}, reactor) {
267
281
  this.opts = opts;
268
282
  this.reactor = reactor ?? createAiSdkReactor();
269
283
  }
284
+ async buildSkills(_context, _env) {
285
+ return [];
286
+ }
270
287
  /**
271
288
  * First-class event expansion stage (runs on every iteration of the durable loop).
272
289
  *
@@ -311,13 +328,14 @@ export class ContextEngine {
311
328
  return await ContextEngine.runDirect(this, triggerEvent, params);
312
329
  }
313
330
  static async prepareExecutionShell(story, triggerEvent, params) {
314
- const ops = await measureBenchmark(params.__benchmark, "react.resolveOpsMs", async () => await getContextEngineOps(params.env, params.__benchmark));
331
+ const env = params.runtime.env;
332
+ const ops = await measureBenchmark(params.__benchmark, "react.resolveOpsMs", async () => await getContextEngineOps(params.runtime, params.__benchmark));
315
333
  const silent = params.options?.silent ?? false;
316
334
  const ctxResult = await measureBenchmark(params.__benchmark, "react.initializeContextMs", async () => await ops.initializeContext(params.context ?? null, { silent }));
317
335
  let currentContext = ctxResult.context;
318
336
  const contextSelector = { id: String(currentContext.id) };
319
337
  if (ctxResult.isNew) {
320
- await story.opts.onContextCreated?.({ env: params.env, context: currentContext });
338
+ await story.opts.onContextCreated?.({ env, context: currentContext });
321
339
  }
322
340
  if (currentContext.status === "closed") {
323
341
  await measureBenchmark(params.__benchmark, "react.reopenClosedContextMs", async () => await ops.updateContextStatus(contextSelector, "open_idle"));
@@ -337,6 +355,7 @@ export class ContextEngine {
337
355
  };
338
356
  }
339
357
  static async startDurable(story, triggerEvent, params) {
358
+ const env = params.runtime.env;
340
359
  if (params.options?.writable) {
341
360
  throw new Error("ContextEngine.react: durable runs manage their own workflow stream");
342
361
  }
@@ -349,14 +368,15 @@ export class ContextEngine {
349
368
  throw new Error("ContextEngine.react: durable workflow is not configured. Call configureContextDurableWorkflow(...) in runtime bootstrap.");
350
369
  }
351
370
  const shell = await ContextEngine.prepareExecutionShell(story, triggerEvent, params);
371
+ let run;
352
372
  try {
353
373
  const [{ start }] = await Promise.all([
354
374
  import("workflow/api"),
355
375
  ]);
356
- const run = await start(workflow, [
376
+ const startedRun = await start(workflow, [
357
377
  {
358
378
  contextKey,
359
- env: params.env,
379
+ runtime: params.runtime,
360
380
  context: params.context ?? null,
361
381
  triggerEvent,
362
382
  options: {
@@ -374,16 +394,21 @@ export class ContextEngine {
374
394
  },
375
395
  },
376
396
  ]);
377
- const runtime = await createRuntimeOps(params.env);
397
+ run = {
398
+ runId: String(startedRun.runId),
399
+ status: startedRun.status,
400
+ returnValue: startedRun.returnValue,
401
+ };
402
+ const runtime = await createRuntimeOps(params.runtime);
378
403
  await runtime.db.transact([
379
404
  runtime.db.tx.event_executions[shell.execution.id].update({
380
- workflowRunId: run.runId,
405
+ workflowRunId: startedRun.runId,
381
406
  updatedAt: new Date(),
382
407
  }),
383
408
  ]);
384
409
  }
385
410
  catch (error) {
386
- const ops = await getContextEngineOps(params.env, params.__benchmark);
411
+ const ops = await getContextEngineOps(params.runtime, params.__benchmark);
387
412
  await ops.completeExecution(shell.contextSelector, shell.execution.id, "failed").catch(() => null);
388
413
  throw error;
389
414
  }
@@ -392,10 +417,12 @@ export class ContextEngine {
392
417
  trigger: shell.trigger,
393
418
  reaction: shell.reaction,
394
419
  execution: shell.execution,
420
+ run,
395
421
  };
396
422
  }
397
423
  static async runDirect(story, triggerEvent, params) {
398
- const ops = await measureBenchmark(params.__benchmark, "react.resolveOpsMs", async () => await getContextEngineOps(params.env, params.__benchmark));
424
+ const env = params.runtime.env;
425
+ const ops = await measureBenchmark(params.__benchmark, "react.resolveOpsMs", async () => await getContextEngineOps(params.runtime, params.__benchmark));
399
426
  const maxIterations = params.options?.maxIterations ?? 20;
400
427
  const maxModelSteps = params.options?.maxModelSteps ?? 1;
401
428
  const preventClose = params.options?.preventClose ?? false;
@@ -422,6 +449,7 @@ export class ContextEngine {
422
449
  const executionId = execution.id;
423
450
  let updatedContext = { ...currentContext, status: "open_streaming" };
424
451
  let currentStepId = null;
452
+ let currentStepStream = null;
425
453
  const failExecution = async () => {
426
454
  try {
427
455
  await ops.completeExecution(activeContextSelector, executionId, "failed");
@@ -468,6 +496,11 @@ export class ContextEngine {
468
496
  iteration: iter,
469
497
  }));
470
498
  currentStepId = stepCreate.stepId;
499
+ currentStepStream = await createPersistedContextStepStream({
500
+ runtime: params.runtime,
501
+ executionId,
502
+ stepId: stepCreate.stepId,
503
+ });
471
504
  await emitContextEvents({
472
505
  silent,
473
506
  writable,
@@ -483,7 +516,7 @@ export class ContextEngine {
483
516
  ],
484
517
  });
485
518
  // Hook: Context DSL `context()` (implemented by subclasses via `initialize()`)
486
- const nextContent = await measureBenchmark(params.__benchmark, `${stagePrefix}.contextMs`, async () => await story.initialize(updatedContext, params.env));
519
+ const nextContent = await measureBenchmark(params.__benchmark, `${stagePrefix}.contextMs`, async () => await story.initialize(updatedContext, env));
487
520
  updatedContext = await measureBenchmark(params.__benchmark, `${stagePrefix}.persistContextMs`, async () => await ops.updateContextContent(activeContextSelector, nextContent));
488
521
  await emitContextEvents({
489
522
  silent,
@@ -496,11 +529,12 @@ export class ContextEngine {
496
529
  },
497
530
  ],
498
531
  });
499
- await story.opts.onContextUpdated?.({ env: params.env, context: updatedContext });
532
+ await story.opts.onContextUpdated?.({ env, context: updatedContext });
500
533
  // Hook: Context DSL `narrative()` (implemented by subclasses via `buildSystemPrompt()`)
501
- const systemPrompt = await measureBenchmark(params.__benchmark, `${stagePrefix}.narrativeMs`, async () => await story.buildSystemPrompt(updatedContext, params.env));
534
+ const systemPrompt = await measureBenchmark(params.__benchmark, `${stagePrefix}.narrativeMs`, async () => await story.buildSystemPrompt(updatedContext, env));
502
535
  // Hook: Context DSL `actions()` (implemented by subclasses via `buildTools()`)
503
- const toolsAll = await measureBenchmark(params.__benchmark, `${stagePrefix}.actionsMs`, async () => await story.buildTools(updatedContext, params.env));
536
+ const toolsAll = await measureBenchmark(params.__benchmark, `${stagePrefix}.actionsMs`, async () => await story.buildTools(updatedContext, env));
537
+ const skillsAll = await measureBenchmark(params.__benchmark, `${stagePrefix}.skillsMs`, async () => await story.buildSkills(updatedContext, env));
504
538
  // IMPORTANT: step args must be serializable.
505
539
  // Match DurableAgent behavior: convert tool input schemas to plain JSON Schema in workflow context.
506
540
  const toolsForModel = toolsToModelTools(toolsAll);
@@ -511,13 +545,13 @@ export class ContextEngine {
511
545
  // If we stream with a per-step id, the UI will render an optimistic assistant message
512
546
  // (step id) and then a second persisted assistant message (reaction id) with the same
513
547
  // content once InstantDB updates.
514
- const reactor = story.getReactor(updatedContext, params.env);
548
+ const reactor = story.getReactor(updatedContext, env);
515
549
  const reactionPartsBeforeStep = Array.isArray(reactionEvent.content?.parts)
516
550
  ? [...reactionEvent.content.parts]
517
551
  : [];
518
552
  let persistedReactionPartsSignature = "";
519
553
  const persistReactionParts = async (nextParts) => {
520
- const normalizedParts = Array.isArray(nextParts) ? nextParts : [];
554
+ const normalizedParts = normalizePartsForPersistence(Array.isArray(nextParts) ? nextParts : []);
521
555
  const nextSignature = JSON.stringify(normalizedParts);
522
556
  if (nextSignature === persistedReactionPartsSignature)
523
557
  return;
@@ -539,14 +573,16 @@ export class ContextEngine {
539
573
  }, { executionId, contextId: String(currentContext.id) });
540
574
  };
541
575
  const { assistantEvent, actionRequests, messagesForModel } = await measureBenchmark(params.__benchmark, `${stagePrefix}.reactorMs`, async () => await reactor({
542
- env: params.env,
576
+ runtime: params.runtime,
577
+ env,
543
578
  context: updatedContext,
544
579
  contextIdentifier: activeContextSelector,
545
580
  triggerEvent,
546
- model: story.getModel(updatedContext, params.env),
581
+ model: story.getModel(updatedContext, env),
547
582
  systemPrompt,
548
583
  actions: toolsAll,
549
584
  toolsForModel,
585
+ skills: skillsAll,
550
586
  eventId: reactionEventId,
551
587
  executionId,
552
588
  contextId: String(currentContext.id),
@@ -556,6 +592,7 @@ export class ContextEngine {
556
592
  // Only emit a `start` chunk once per story turn.
557
593
  sendStart: !silent && iter === 0,
558
594
  silent,
595
+ contextStepStream: currentStepStream?.stream,
559
596
  writable,
560
597
  persistReactionParts,
561
598
  }));
@@ -579,7 +616,7 @@ export class ContextEngine {
579
616
  // We intentionally do NOT persist the per-step LLM assistant event as a `context_event`.
580
617
  // The story exposes a single visible `context_event` per turn (`reactionEventId`) so the UI
581
618
  // doesn't render duplicate assistant messages (LLM-step + aggregated reaction).
582
- const stepParts = (assistantEvent?.content?.parts ?? []);
619
+ const stepParts = normalizePartsForPersistence((assistantEvent?.content?.parts ?? []));
583
620
  const assistantEventEffective = {
584
621
  ...assistantEvent,
585
622
  content: {
@@ -622,6 +659,13 @@ export class ContextEngine {
622
659
  status: "pending",
623
660
  };
624
661
  reactionEvent = await measureBenchmark(params.__benchmark, `${stagePrefix}.persistAssistantReactionMs`, async () => await ops.updateItem(reactionEvent.id, nextReactionEvent, { executionId, contextId: String(currentContext.id) }));
662
+ if (currentStepStream) {
663
+ await closePersistedContextStepStream({
664
+ runtime: params.runtime,
665
+ session: currentStepStream,
666
+ });
667
+ currentStepStream = null;
668
+ }
625
669
  story.opts.onEventCreated?.(assistantEventEffective);
626
670
  const firstActionRequest = actionRequests?.[0];
627
671
  await measureBenchmark(params.__benchmark, `${stagePrefix}.markStepRunningMs`, async () => await ops.updateContextStep({
@@ -798,12 +842,18 @@ export class ContextEngine {
798
842
  }
799
843
  }
800
844
  const output = await toolDef.execute(actionInput, {
845
+ runtime: params.runtime,
846
+ env,
847
+ context: updatedContext,
848
+ contextIdentifier: activeContextSelector,
801
849
  toolCallId: actionRequest.actionRef,
802
850
  messages: messagesForModel,
803
851
  eventId: reactionEventId,
804
852
  executionId,
805
853
  triggerEventId,
806
854
  contextId: currentContext.id,
855
+ stepId: String(stepCreate.stepId),
856
+ iteration: iter,
807
857
  });
808
858
  return { actionRequest, success: true, output };
809
859
  }
@@ -817,11 +867,9 @@ export class ContextEngine {
817
867
  }
818
868
  })));
819
869
  // Merge action results into persisted parts (so next LLM call can see them)
820
- let parts = Array.isArray(reactionEvent.content?.parts)
821
- ? [...reactionEvent.content.parts]
822
- : [];
870
+ let finalizedStepParts = Array.isArray(stepParts) ? [...stepParts] : [];
823
871
  for (const r of actionResults) {
824
- parts = applyToolExecutionResultToParts(parts, {
872
+ finalizedStepParts = applyToolExecutionResultToParts(finalizedStepParts, {
825
873
  toolCallId: r.actionRequest.actionRef,
826
874
  toolName: r.actionRequest.actionName,
827
875
  }, {
@@ -830,11 +878,20 @@ export class ContextEngine {
830
878
  message: r.errorText,
831
879
  });
832
880
  }
881
+ await measureBenchmark(params.__benchmark, `${stagePrefix}.saveFinalStepPartsMs`, async () => await ops.saveContextPartsStep({
882
+ stepId: stepCreate.stepId,
883
+ parts: finalizedStepParts,
884
+ executionId,
885
+ contextId: String(currentContext.id),
886
+ iteration: iter,
887
+ }));
833
888
  reactionEvent = {
834
889
  ...reactionEvent,
835
890
  content: {
836
891
  ...reactionEvent.content,
837
- parts,
892
+ // Deprecated mirror for compatibility. `event_parts` are the
893
+ // source of truth for replay and step inspection.
894
+ parts: [...reactionPartsBeforeStep, ...finalizedStepParts],
838
895
  },
839
896
  status: "pending",
840
897
  };
@@ -853,7 +910,7 @@ export class ContextEngine {
853
910
  // IMPORTANT: we call this after tool results have been merged into the persisted `reactionEvent`,
854
911
  // so stories can inspect `reactionEvent.content.parts` deterministically.
855
912
  const continueLoop = await measureBenchmark(params.__benchmark, `${stagePrefix}.shouldContinueMs`, async () => await story.shouldContinue({
856
- env: params.env,
913
+ env,
857
914
  context: updatedContext,
858
915
  reactionEvent,
859
916
  assistantEvent: assistantEventEffective,
@@ -989,6 +1046,21 @@ export class ContextEngine {
989
1046
  throw new Error(`ContextEngine: maxIterations reached (${maxIterations}) without completion`);
990
1047
  }
991
1048
  catch (error) {
1049
+ if (currentStepStream) {
1050
+ try {
1051
+ await abortPersistedContextStepStream({
1052
+ runtime: params.runtime,
1053
+ session: currentStepStream,
1054
+ reason: error instanceof Error ? error.message : String(error),
1055
+ });
1056
+ }
1057
+ catch {
1058
+ // noop
1059
+ }
1060
+ finally {
1061
+ currentStepStream = null;
1062
+ }
1063
+ }
992
1064
  // Best-effort: persist failure on the current iteration step (if any)
993
1065
  if (currentStepId) {
994
1066
  const failedStepId = currentStepId;
@@ -6,6 +6,26 @@ export declare const INPUT_TEXT_ITEM_TYPE = "input";
6
6
  export declare const WEB_CHANNEL = "web";
7
7
  export declare const AGENT_CHANNEL = "whatsapp";
8
8
  export declare const EMAIL_CHANNEL = "email";
9
+ export type ContextOutputContentPart = {
10
+ type: "text";
11
+ text: string;
12
+ } | ({
13
+ type: "image-data";
14
+ data: string;
15
+ mediaType: string;
16
+ filename?: string;
17
+ } & Record<string, unknown>) | ({
18
+ type: string;
19
+ } & Record<string, unknown>);
20
+ export type ContextOutputPart = {
21
+ type: "json";
22
+ value: unknown;
23
+ } | {
24
+ type: "content";
25
+ value: ContextOutputContentPart[];
26
+ };
27
+ export declare function isContextOutputPart(value: unknown): value is ContextOutputPart;
28
+ export declare function normalizeContextOutputPart(value: unknown): ContextOutputPart;
9
29
  export declare function createUserItemFromUIMessages(messages: UIMessage[]): ContextItem;
10
30
  export declare function createAssistantItemFromUIMessages(itemId: string, messages: UIMessage[]): ContextItem;
11
31
  export declare function convertToUIMessage(item: ContextItem): UIMessage;