@ekairos/story 1.21.43-beta.0 → 1.21.53-beta.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.
@@ -18,6 +18,7 @@ export declare function executeReaction(params: {
18
18
  eventId: string;
19
19
  maxSteps: number;
20
20
  sendStart?: boolean;
21
+ silent?: boolean;
21
22
  }): Promise<{
22
23
  assistantEvent: ContextEvent;
23
24
  toolCalls: any[];
@@ -42,28 +42,30 @@ function safeErrorJson(error) {
42
42
  export async function executeReaction(params) {
43
43
  "use step";
44
44
  const { store } = await resolveStoryRuntime(params.env);
45
- console.log("executeReaction: begin");
45
+ console.log("[ekairos/story] reaction.step executeReaction begin");
46
46
  let events;
47
47
  try {
48
- console.log("executeReaction: store.getEvents begin");
48
+ console.log("[ekairos/story] reaction.step store.getEvents begin");
49
49
  events = await store.getEvents(params.contextIdentifier);
50
- console.log("executeReaction: store.getEvents ok");
50
+ console.log("[ekairos/story] reaction.step store.getEvents ok");
51
51
  }
52
52
  catch (error) {
53
- console.error("executeReaction: store.getEvents failed");
53
+ console.error("[ekairos/story] reaction.step store.getEvents failed");
54
54
  throw error;
55
55
  }
56
56
  let messagesForModel;
57
57
  try {
58
- console.log("executeReaction: store.eventsToModelMessages begin");
58
+ console.log("[ekairos/story] reaction.step store.eventsToModelMessages begin");
59
59
  messagesForModel = (await store.eventsToModelMessages(events));
60
- console.log("executeReaction: store.eventsToModelMessages ok");
60
+ console.log("[ekairos/story] reaction.step store.eventsToModelMessages ok");
61
61
  }
62
62
  catch (error) {
63
- console.error("executeReaction: store.eventsToModelMessages failed", safeErrorJson(error));
63
+ console.error("[ekairos/story] reaction.step store.eventsToModelMessages failed", safeErrorJson(error));
64
64
  throw error;
65
65
  }
66
- const writable = getWritable();
66
+ const writable = params.silent
67
+ ? new WritableStream({ write() { } })
68
+ : getWritable();
67
69
  // Match DurableAgent-style model init behavior:
68
70
  const resolvedModel = typeof params.model === "string"
69
71
  ? gateway(params.model)
@@ -78,7 +80,7 @@ export async function executeReaction(params) {
78
80
  inputSchema: jsonSchema(t.inputSchema),
79
81
  };
80
82
  }
81
- console.log("executeReaction: streamText begin");
83
+ console.log("[ekairos/story] reaction.step streamText begin");
82
84
  const result = streamText({
83
85
  model: resolvedModel,
84
86
  system: params.system,
@@ -88,7 +90,7 @@ export async function executeReaction(params) {
88
90
  stopWhen: stepCountIs(params.maxSteps),
89
91
  experimental_transform: smoothStream({ delayInMs: 30, chunking: "word" }),
90
92
  });
91
- console.log("executeReaction: streamText ok");
93
+ console.log("[ekairos/story] reaction.step streamText ok");
92
94
  // Ensure the underlying stream is consumed (AI SDK requirement)
93
95
  result.consumeStream();
94
96
  let resolveFinish;
@@ -5,7 +5,9 @@ import type { ContextEvent, ContextIdentifier, StoredContext } from "../story.st
5
5
  *
6
6
  * This is the "context init" boundary for the story engine.
7
7
  */
8
- export declare function initializeContext<C>(env: StoryEnvironment, contextIdentifier: ContextIdentifier | null): Promise<{
8
+ export declare function initializeContext<C>(env: StoryEnvironment, contextIdentifier: ContextIdentifier | null, opts?: {
9
+ silent?: boolean;
10
+ }): Promise<{
9
11
  context: StoredContext<C>;
10
12
  isNew: boolean;
11
13
  }>;
@@ -5,7 +5,7 @@ import { resolveStoryRuntime } from "../story.config";
5
5
  *
6
6
  * This is the "context init" boundary for the story engine.
7
7
  */
8
- export async function initializeContext(env, contextIdentifier) {
8
+ export async function initializeContext(env, contextIdentifier, opts) {
9
9
  "use step";
10
10
  const { store } = await resolveStoryRuntime(env);
11
11
  // Detect creation explicitly so the engine can run onContextCreated hooks.
@@ -24,17 +24,21 @@ export async function initializeContext(env, contextIdentifier) {
24
24
  result = { context: created, isNew: true };
25
25
  }
26
26
  }
27
- const writable = getWritable();
28
- const writer = writable.getWriter();
29
- try {
30
- await writer.write({
31
- type: "data-context-id",
32
- id: String(result.context.id),
33
- data: { contextId: String(result.context.id) },
34
- });
35
- }
36
- finally {
37
- writer.releaseLock();
27
+ // If we're running in a non-streaming context (e.g. tests or headless usage),
28
+ // we skip writing stream chunks entirely.
29
+ if (!opts?.silent) {
30
+ const writable = getWritable();
31
+ const writer = writable.getWriter();
32
+ try {
33
+ await writer.write({
34
+ type: "data-context-id",
35
+ id: String(result.context.id),
36
+ data: { contextId: String(result.context.id) },
37
+ });
38
+ }
39
+ finally {
40
+ writer.releaseLock();
41
+ }
38
42
  }
39
43
  return result;
40
44
  }
@@ -135,7 +135,9 @@ export class InstantStore {
135
135
  async getEvents(contextIdentifier) {
136
136
  const contextWhere = contextIdentifier.id
137
137
  ? { context: contextIdentifier.id }
138
- : { context: lookup("key", contextIdentifier.key) };
138
+ : // IMPORTANT: `lookup("key", ...)` is valid in transactions, but not in query filters for links.
139
+ // Use nested where on the linked context's indexed `key` instead.
140
+ { "context.key": contextIdentifier.key };
139
141
  const res = await this.db.query({
140
142
  context_events: {
141
143
  $: {
@@ -14,6 +14,7 @@ import type { StoryStore } from "./story.store";
14
14
  export type StoryEnvironment = Record<string, unknown>;
15
15
  export type StoryRuntime = {
16
16
  store: StoryStore;
17
+ db: any;
17
18
  };
18
19
  export type StoryRuntimeResolver<Env extends StoryEnvironment = StoryEnvironment> = (env: Env) => Promise<StoryRuntime> | StoryRuntime;
19
20
  /**
@@ -48,21 +48,6 @@ export async function resolveStoryRuntime(env) {
48
48
  }
49
49
  }
50
50
  }
51
- // Convention bootstrap (Next.js / monorepo apps):
52
- // If the app exposes `src/ekairos.ts` and uses the `@/` alias, loading that module will
53
- // run `ekairosConfig.setup()` which configures the resolver + bootstrap hook.
54
- //
55
- // This is intentionally ONLY attempted when runtime is missing, and is safe as long as
56
- // `story.config` is not part of client bundles (see `@ekairos/story/runtime`).
57
- if (!getRuntimeResolver()) {
58
- try {
59
- // @ts-expect-error - optional, app-provided convention module
60
- await import("@/ekairos");
61
- }
62
- catch {
63
- // ignore: module missing / alias not configured
64
- }
65
- }
66
51
  // If bootstrap succeeded, proceed.
67
52
  const resolver = getRuntimeResolver();
68
53
  if (resolver)
@@ -39,6 +39,14 @@ export interface StoryStreamOptions {
39
39
  * Default: true.
40
40
  */
41
41
  sendFinish?: boolean;
42
+ /**
43
+ * If true, the story loop runs silently (no UI streaming output).
44
+ *
45
+ * Persistence (contexts/events/executions) still happens normally.
46
+ *
47
+ * Default: false.
48
+ */
49
+ silent?: boolean;
42
50
  }
43
51
  /**
44
52
  * Model initializer (DurableAgent-style).
@@ -3,6 +3,14 @@ import { executeReaction } from "./steps/reaction.steps.js";
3
3
  import { toolsToModelTools } from "./tools-to-model-tools.js";
4
4
  import { closeStoryStream, writeContextSubstate, writeStoryPing, writeToolOutputs } from "./steps/stream.steps.js";
5
5
  import { completeExecution, createReactionEvent, initializeContext, saveReactionEvent, saveTriggerEvent, updateContextContent, updateContextStatus, updateEvent, } from "./steps/store.steps.js";
6
+ function storyEngineInfo(message, ...args) {
7
+ // CRITICAL: static string log messages only. Dynamic values go in args.
8
+ console.log(message, ...args);
9
+ }
10
+ function storyEngineDebug(message, ...args) {
11
+ // CRITICAL: static string log messages only. Dynamic values go in args.
12
+ console.debug(message, ...args);
13
+ }
6
14
  export class Story {
7
15
  constructor(opts = {}) {
8
16
  this.opts = opts;
@@ -58,9 +66,18 @@ export class Story {
58
66
  const maxModelSteps = params.options?.maxModelSteps ?? 1;
59
67
  const preventClose = params.options?.preventClose ?? false;
60
68
  const sendFinish = params.options?.sendFinish ?? true;
69
+ const silent = params.options?.silent ?? false;
70
+ storyEngineInfo("[ekairos/story] story.engine react begin");
71
+ storyEngineInfo("[ekairos/story] story.engine react contextIdentifier", params.contextIdentifier);
72
+ storyEngineInfo("[ekairos/story] story.engine react maxIterations", maxIterations);
73
+ storyEngineInfo("[ekairos/story] story.engine react maxModelSteps", maxModelSteps);
74
+ storyEngineInfo("[ekairos/story] story.engine react silent", silent);
61
75
  // 1) Ensure context exists (step)
62
- const ctxResult = await initializeContext(params.env, params.contextIdentifier);
76
+ const ctxResult = await initializeContext(params.env, params.contextIdentifier, { silent });
63
77
  const currentContext = ctxResult.context;
78
+ storyEngineInfo("[ekairos/story] story.engine initializeContext ok");
79
+ storyEngineInfo("[ekairos/story] story.engine initializeContext contextId", currentContext.id);
80
+ storyEngineInfo("[ekairos/story] story.engine initializeContext isNew", ctxResult.isNew);
64
81
  const contextSelector = params.contextIdentifier?.id
65
82
  ? { id: String(params.contextIdentifier.id) }
66
83
  : params.contextIdentifier?.key
@@ -72,14 +89,21 @@ export class Story {
72
89
  // 2) Persist trigger event + create execution shell (steps)
73
90
  const persistedTriggerEvent = await saveTriggerEvent(params.env, contextSelector, triggerEvent);
74
91
  const triggerEventId = persistedTriggerEvent.id;
92
+ storyEngineInfo("[ekairos/story] story.engine saveTriggerEvent ok");
93
+ storyEngineInfo("[ekairos/story] story.engine saveTriggerEvent triggerEventId", triggerEventId);
75
94
  const { reactionEventId, executionId } = await createReactionEvent({
76
95
  env: params.env,
77
96
  contextIdentifier: contextSelector,
78
97
  triggerEventId,
79
98
  });
99
+ storyEngineInfo("[ekairos/story] story.engine createReactionEvent ok");
100
+ storyEngineInfo("[ekairos/story] story.engine createReactionEvent reactionEventId", reactionEventId);
101
+ storyEngineInfo("[ekairos/story] story.engine createReactionEvent executionId", executionId);
80
102
  // Emit a simple ping chunk early so clients can validate that streaming works end-to-end.
81
103
  // This should be ignored safely by clients that don't care about it.
82
- await writeStoryPing({ label: "story-start" });
104
+ if (!silent) {
105
+ await writeStoryPing({ label: "story-start" });
106
+ }
83
107
  let reactionEvent = null;
84
108
  // Latest persisted context state for this run (we keep it in memory; store is updated via steps).
85
109
  let updatedContext = currentContext;
@@ -91,7 +115,9 @@ export class Story {
91
115
  // noop
92
116
  }
93
117
  try {
94
- await closeStoryStream({ preventClose, sendFinish });
118
+ if (!silent) {
119
+ await closeStoryStream({ preventClose, sendFinish });
120
+ }
95
121
  }
96
122
  catch {
97
123
  // noop
@@ -99,12 +125,22 @@ export class Story {
99
125
  };
100
126
  try {
101
127
  for (let iter = 0; iter < maxIterations; iter++) {
102
- // Normalize/initialize context (workflow-level; may call steps if needed)
128
+ storyEngineInfo("[ekairos/story] story.engine === LOOP ITERATION ===", iter);
129
+ // Hook: Story DSL `context()` (implemented by subclasses via `initialize()`)
130
+ storyEngineInfo("[ekairos/story] >>> HOOK context() BEGIN", iter);
103
131
  const nextContent = await this.initialize(updatedContext, params.env);
132
+ storyEngineInfo("[ekairos/story] <<< HOOK context() END", iter);
104
133
  updatedContext = await updateContextContent(params.env, contextSelector, nextContent);
134
+ storyEngineInfo("[ekairos/story] story.engine updateContextContent ok");
105
135
  await this.opts.onContextUpdated?.({ env: params.env, context: updatedContext });
136
+ // Hook: Story DSL `narrative()` (implemented by subclasses via `buildSystemPrompt()`)
137
+ storyEngineInfo("[ekairos/story] >>> HOOK narrative() BEGIN", iter);
106
138
  const systemPrompt = await this.buildSystemPrompt(updatedContext, params.env);
139
+ storyEngineInfo("[ekairos/story] <<< HOOK narrative() END", iter);
140
+ // Hook: Story DSL `actions()` (implemented by subclasses via `buildTools()`)
141
+ storyEngineInfo("[ekairos/story] >>> HOOK actions() BEGIN", iter);
107
142
  const toolsAll = await this.buildTools(updatedContext, params.env);
143
+ storyEngineInfo("[ekairos/story] <<< HOOK actions() END", iter);
108
144
  // IMPORTANT: step args must be serializable.
109
145
  // Match DurableAgent behavior: convert tool input schemas to plain JSON Schema in workflow context.
110
146
  const toolsForModel = toolsToModelTools(toolsAll);
@@ -117,14 +153,22 @@ export class Story {
117
153
  eventId: reactionEventId,
118
154
  maxSteps: maxModelSteps,
119
155
  // Only emit a `start` chunk once per story turn.
120
- sendStart: iter === 0 && reactionEvent === null,
156
+ sendStart: !silent && iter === 0 && reactionEvent === null,
157
+ silent,
121
158
  });
159
+ storyEngineInfo("[ekairos/story] story.engine executeReaction ok");
160
+ storyEngineInfo("[ekairos/story] story.engine executeReaction toolCallsCount", toolCalls.length);
161
+ if (toolCalls.length) {
162
+ storyEngineInfo("[ekairos/story] >>> TOOL_CALLS requested", toolCalls.map((tc) => tc?.toolName).filter(Boolean));
163
+ storyEngineDebug("[ekairos/story] >>> TOOL_CALLS payload", toolCalls);
164
+ }
122
165
  // Persist/append the assistant event for this iteration
123
166
  if (!reactionEvent) {
124
167
  reactionEvent = await saveReactionEvent(params.env, contextSelector, {
125
168
  ...assistantEvent,
126
169
  status: "pending",
127
170
  });
171
+ storyEngineInfo("[ekairos/story] story.engine saveReactionEvent ok");
128
172
  }
129
173
  else {
130
174
  reactionEvent = await updateEvent(params.env, reactionEvent.id, {
@@ -137,11 +181,14 @@ export class Story {
137
181
  },
138
182
  status: "pending",
139
183
  });
184
+ storyEngineInfo("[ekairos/story] story.engine updateEvent appendAssistantParts ok");
140
185
  }
141
186
  this.opts.onEventCreated?.(assistantEvent);
142
187
  // Done: no tool calls requested by the model
143
188
  if (!toolCalls.length) {
189
+ storyEngineInfo("[ekairos/story] >>> HOOK onEnd() BEGIN", iter);
144
190
  const endResult = await this.callOnEnd(assistantEvent);
191
+ storyEngineInfo("[ekairos/story] <<< HOOK onEnd() END", iter, endResult);
145
192
  if (endResult) {
146
193
  // Mark reaction event completed
147
194
  await updateEvent(params.env, reactionEventId, {
@@ -150,7 +197,9 @@ export class Story {
150
197
  });
151
198
  await updateContextStatus(params.env, contextSelector, "open");
152
199
  await completeExecution(params.env, contextSelector, executionId, "completed");
153
- await closeStoryStream({ preventClose, sendFinish });
200
+ if (!silent) {
201
+ await closeStoryStream({ preventClose, sendFinish });
202
+ }
154
203
  return {
155
204
  contextId: currentContext.id,
156
205
  context: updatedContext,
@@ -161,12 +210,13 @@ export class Story {
161
210
  }
162
211
  }
163
212
  // Execute tool calls (workflow context; tool implementations decide step vs workflow)
164
- if (toolCalls.length) {
213
+ if (!silent && toolCalls.length) {
165
214
  await writeContextSubstate({ key: "actions", transient: true });
166
215
  }
167
216
  const executionResults = await Promise.all(toolCalls.map(async (tc) => {
168
217
  const toolDef = toolsAll[tc.toolName];
169
218
  if (!toolDef || typeof toolDef.execute !== "function") {
219
+ storyEngineInfo("[ekairos/story] story.engine toolExecution missingTool", tc?.toolName);
170
220
  return {
171
221
  tc,
172
222
  success: false,
@@ -174,6 +224,8 @@ export class Story {
174
224
  errorText: `Tool "${tc.toolName}" not found or has no execute().`,
175
225
  };
176
226
  }
227
+ storyEngineInfo("[ekairos/story] >>> TOOL_EXEC BEGIN", tc?.toolName, tc?.toolCallId);
228
+ storyEngineDebug("[ekairos/story] >>> TOOL_EXEC input", tc?.toolName, tc?.toolCallId, tc?.args);
177
229
  try {
178
230
  const output = await toolDef.execute(tc.args, {
179
231
  toolCallId: tc.toolCallId,
@@ -183,9 +235,13 @@ export class Story {
183
235
  triggerEventId,
184
236
  contextId: currentContext.id,
185
237
  });
238
+ storyEngineInfo("[ekairos/story] <<< TOOL_EXEC OK", tc?.toolName, tc?.toolCallId);
239
+ storyEngineDebug("[ekairos/story] <<< TOOL_EXEC output", tc?.toolName, tc?.toolCallId, output);
186
240
  return { tc, success: true, output };
187
241
  }
188
242
  catch (e) {
243
+ storyEngineInfo("[ekairos/story] <<< TOOL_EXEC FAILED", tc?.toolName, tc?.toolCallId);
244
+ storyEngineDebug("[ekairos/story] <<< TOOL_EXEC error", tc?.toolName, tc?.toolCallId, e instanceof Error ? e.message : String(e));
189
245
  return {
190
246
  tc,
191
247
  success: false,
@@ -194,14 +250,21 @@ export class Story {
194
250
  };
195
251
  }
196
252
  }));
253
+ storyEngineInfo("[ekairos/story] story.engine toolExecution resultsCount", executionResults.length);
197
254
  // Emit tool outputs to the workflow stream (step)
198
- await writeToolOutputs({
199
- results: executionResults.map((r) => r.success
200
- ? { toolCallId: r.tc.toolCallId, success: true, output: r.output }
201
- : { toolCallId: r.tc.toolCallId, success: false, errorText: r.errorText }),
202
- });
255
+ if (!silent) {
256
+ await writeToolOutputs({
257
+ results: executionResults.map((r) => r.success
258
+ ? { toolCallId: r.tc.toolCallId, success: true, output: r.output }
259
+ : {
260
+ toolCallId: r.tc.toolCallId,
261
+ success: false,
262
+ errorText: r.errorText,
263
+ }),
264
+ });
265
+ }
203
266
  // Clear action status once tool execution results have been emitted.
204
- if (toolCalls.length) {
267
+ if (!silent && toolCalls.length) {
205
268
  await writeContextSubstate({ key: null, transient: true });
206
269
  }
207
270
  // Merge tool results into persisted parts (so next LLM call can see them)
@@ -219,6 +282,7 @@ export class Story {
219
282
  content: { parts },
220
283
  status: "pending",
221
284
  });
285
+ storyEngineInfo("[ekairos/story] story.engine updateEvent mergeToolResults ok");
222
286
  }
223
287
  // Callback for observability/integration
224
288
  for (const r of executionResults) {
@@ -234,6 +298,7 @@ export class Story {
234
298
  // Stop/continue boundary: allow the Story to decide if the loop should continue.
235
299
  // IMPORTANT: we call this after tool results have been merged into the persisted `reactionEvent`,
236
300
  // so stories can inspect `reactionEvent.content.parts` deterministically.
301
+ storyEngineInfo("[ekairos/story] >>> HOOK shouldContinue() BEGIN", iter);
237
302
  const continueLoop = await this.shouldContinue({
238
303
  env: params.env,
239
304
  context: updatedContext,
@@ -242,6 +307,7 @@ export class Story {
242
307
  toolCalls,
243
308
  toolExecutionResults: executionResults,
244
309
  });
310
+ storyEngineInfo("[ekairos/story] <<< HOOK shouldContinue() END", iter, continueLoop);
245
311
  if (continueLoop === false) {
246
312
  await updateEvent(params.env, reactionEventId, {
247
313
  ...reactionEvent,
@@ -249,7 +315,9 @@ export class Story {
249
315
  });
250
316
  await updateContextStatus(params.env, contextSelector, "open");
251
317
  await completeExecution(params.env, contextSelector, executionId, "completed");
252
- await closeStoryStream({ preventClose, sendFinish });
318
+ if (!silent) {
319
+ await closeStoryStream({ preventClose, sendFinish });
320
+ }
253
321
  return {
254
322
  contextId: currentContext.id,
255
323
  context: updatedContext,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekairos/story",
3
- "version": "1.21.43-beta.0",
3
+ "version": "1.21.53-beta.0",
4
4
  "description": "Pulzar Story - Workflow-based AI Stories",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -21,6 +21,7 @@
21
21
  ".": {
22
22
  "types": "./dist/index.d.ts",
23
23
  "import": "./dist/index.js",
24
+ "require": "./dist/index.js",
24
25
  "default": "./dist/index.js"
25
26
  },
26
27
  "./runtime": {
@@ -39,6 +40,19 @@
39
40
  "default": "./dist/stores/instant.store.js"
40
41
  }
41
42
  },
43
+ "typesVersions": {
44
+ "*": {
45
+ "runtime": [
46
+ "dist/runtime.d.ts"
47
+ ],
48
+ "next": [
49
+ "dist/next.d.ts"
50
+ ],
51
+ "instant": [
52
+ "dist/stores/instant.store.d.ts"
53
+ ]
54
+ }
55
+ },
42
56
  "scripts": {
43
57
  "build": "tsc -p tsconfig.json",
44
58
  "dev": "tsc -p tsconfig.json --watch",
@@ -48,7 +62,7 @@
48
62
  },
49
63
  "dependencies": {
50
64
  "@ai-sdk/openai": "^2.0.52",
51
- "@ekairos/domain": "^1.21.43-beta.0",
65
+ "@ekairos/domain": "^1.21.53-beta.0",
52
66
  "@instantdb/admin": "^0.22.13",
53
67
  "@instantdb/core": "^0.22.13",
54
68
  "@vercel/sandbox": "^0.0.23",