@ekairos/story 1.21.57-beta.0 → 1.21.59-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.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { story, createStory, type StoryConfig, type StoryInstance, type StoryOptions, type StoryStreamOptions, Story, type RegistrableStoryBuilder, } from "./story.js";
2
2
  export type { StoryStore, ContextIdentifier, StoredContext, ContextEvent, } from "./story.store.js";
3
+ export type { WireDate, StoryMirrorContext, StoryMirrorExecution, StoryMirrorWrite, StoryMirrorRequest, } from "./mirror.js";
3
4
  export { registerStory, getStory, getStoryFactory, hasStory, listStories, type StoryKey, } from "./story.registry.js";
4
5
  export { storyDomain } from "./schema.js";
5
6
  export { didToolExecute } from "./story.toolcalls.js";
@@ -0,0 +1,41 @@
1
+ import type { ContextEvent, StoredContext } from "./story.store";
2
+ /**
3
+ * Wire-safe (JSON) mirror types shared by:
4
+ * - the workflow sender (`@ekairos/story` steps)
5
+ * - the ekairos-core receiver (`/api/story`)
6
+ *
7
+ * Note: `StoredContext` contains Date objects, but over HTTP we send ISO strings.
8
+ */
9
+ export type WireDate = string;
10
+ export type StoryMirrorContext = Omit<StoredContext<unknown>, "createdAt" | "updatedAt"> & {
11
+ createdAt: WireDate;
12
+ updatedAt?: WireDate;
13
+ };
14
+ export type StoryMirrorExecution = Record<string, unknown> & {
15
+ createdAt?: WireDate;
16
+ updatedAt?: WireDate;
17
+ };
18
+ export type StoryMirrorWrite = {
19
+ type: "context.upsert";
20
+ context: StoryMirrorContext;
21
+ } | {
22
+ type: "event.upsert";
23
+ contextId: string;
24
+ event: ContextEvent;
25
+ } | {
26
+ type: "event.update";
27
+ eventId: string;
28
+ event: ContextEvent;
29
+ } | {
30
+ type: "execution.upsert";
31
+ contextId: string;
32
+ executionId: string;
33
+ execution: StoryMirrorExecution;
34
+ triggerEventId: string;
35
+ reactionEventId: string;
36
+ setCurrentExecution?: boolean;
37
+ };
38
+ export type StoryMirrorRequest = {
39
+ orgId: string;
40
+ writes: StoryMirrorWrite[];
41
+ };
package/dist/mirror.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { StoryEnvironment } from "../story.config";
2
+ import type { StoryMirrorWrite } from "../mirror";
3
+ export declare function mirrorStoryWrites(params: {
4
+ env: StoryEnvironment;
5
+ writes: StoryMirrorWrite[];
6
+ }): Promise<void>;
@@ -0,0 +1,44 @@
1
+ function requireOrgId(env) {
2
+ const orgId = env?.orgId;
3
+ if (typeof orgId !== "string" || !orgId) {
4
+ throw new Error("[story/mirror] Missing env.orgId");
5
+ }
6
+ return orgId;
7
+ }
8
+ function requireBaseUrl() {
9
+ const baseUrl = process.env.EKAIROS_CORE_BASE_URL ||
10
+ process.env.EKAIROS_MIRROR_BASE_URL ||
11
+ process.env.EKAIROS_BASE_URL;
12
+ if (!baseUrl) {
13
+ throw new Error("[story/mirror] Missing EKAIROS_CORE_BASE_URL (or EKAIROS_MIRROR_BASE_URL)");
14
+ }
15
+ return baseUrl.replace(/\/$/, "");
16
+ }
17
+ function requireToken() {
18
+ const token = process.env.EKAIROS_STORY_MIRROR_TOKEN;
19
+ if (!token) {
20
+ throw new Error("[story/mirror] Missing EKAIROS_STORY_MIRROR_TOKEN");
21
+ }
22
+ return token;
23
+ }
24
+ export async function mirrorStoryWrites(params) {
25
+ "use step";
26
+ if (!params.writes?.length)
27
+ return;
28
+ const orgId = requireOrgId(params.env);
29
+ const baseUrl = requireBaseUrl();
30
+ const token = requireToken();
31
+ const body = { orgId, writes: params.writes };
32
+ const res = await fetch(`${baseUrl}/api/story`, {
33
+ method: "POST",
34
+ headers: {
35
+ "content-type": "application/json",
36
+ authorization: `Bearer ${token}`,
37
+ },
38
+ body: JSON.stringify(body),
39
+ });
40
+ if (!res.ok) {
41
+ const text = await res.text().catch(() => "");
42
+ throw new Error(`[story/mirror] ekairos-core write failed (${res.status}): ${text}`);
43
+ }
44
+ }
@@ -1,4 +1,4 @@
1
- import type { ModelMessage } from "ai";
1
+ import type { ModelMessage, UIMessageChunk } from "ai";
2
2
  import type { StoryEnvironment } from "../story.config";
3
3
  import type { ContextEvent, ContextIdentifier } from "../story.store";
4
4
  import type { SerializableToolForModel } from "../tools-to-model-tools";
@@ -19,6 +19,7 @@ export declare function executeReaction(params: {
19
19
  maxSteps: number;
20
20
  sendStart?: boolean;
21
21
  silent?: boolean;
22
+ writable?: WritableStream<UIMessageChunk>;
22
23
  }): Promise<{
23
24
  assistantEvent: ContextEvent;
24
25
  toolCalls: any[];
@@ -60,9 +60,9 @@ export async function executeReaction(params) {
60
60
  console.error("[ekairos/story] reaction.step store.eventsToModelMessages failed", safeErrorJson(error));
61
61
  throw error;
62
62
  }
63
- const writable = params.silent
63
+ const writable = params.silent || !params.writable
64
64
  ? new WritableStream({ write() { } })
65
- : (await import("workflow")).getWritable();
65
+ : params.writable;
66
66
  const { jsonSchema, gateway, smoothStream, stepCountIs, streamText } = await import("ai");
67
67
  const { extractToolCallsFromParts } = await import("../story.toolcalls");
68
68
  // Match DurableAgent-style model init behavior:
@@ -1,3 +1,4 @@
1
+ import type { UIMessageChunk } from "ai";
1
2
  import type { StoryEnvironment } from "../story.config";
2
3
  import type { ContextEvent, ContextIdentifier, StoredContext } from "../story.store";
3
4
  /**
@@ -7,6 +8,7 @@ import type { ContextEvent, ContextIdentifier, StoredContext } from "../story.st
7
8
  */
8
9
  export declare function initializeContext<C>(env: StoryEnvironment, contextIdentifier: ContextIdentifier | null, opts?: {
9
10
  silent?: boolean;
11
+ writable?: WritableStream<UIMessageChunk>;
10
12
  }): Promise<{
11
13
  context: StoredContext<C>;
12
14
  isNew: boolean;
@@ -7,6 +7,8 @@ export async function initializeContext(env, contextIdentifier, opts) {
7
7
  "use step";
8
8
  const { resolveStoryRuntime } = await import("@ekairos/story/runtime");
9
9
  const { store } = await resolveStoryRuntime(env);
10
+ console.log("[ekairos/story] story.engine react begin");
11
+ console.log("[ekairos/story] story.engine react contextIdentifier", contextIdentifier);
10
12
  // Detect creation explicitly so the engine can run onContextCreated hooks.
11
13
  let result;
12
14
  if (!contextIdentifier) {
@@ -23,12 +25,13 @@ export async function initializeContext(env, contextIdentifier, opts) {
23
25
  result = { context: created, isNew: true };
24
26
  }
25
27
  }
28
+ console.log("[ekairos/story] story.engine initializeContext ok");
29
+ console.log("[ekairos/story] story.engine initializeContext contextId", result.context.id);
30
+ console.log("[ekairos/story] story.engine initializeContext isNew", result.isNew);
26
31
  // If we're running in a non-streaming context (e.g. tests or headless usage),
27
32
  // we skip writing stream chunks entirely.
28
- if (!opts?.silent) {
29
- const { getWritable } = await import("workflow");
30
- const writable = getWritable();
31
- const writer = writable.getWriter();
33
+ if (!opts?.silent && opts?.writable) {
34
+ const writer = opts.writable.getWriter();
32
35
  try {
33
36
  await writer.write({
34
37
  type: "data-context-id",
@@ -58,7 +61,10 @@ export async function saveTriggerEvent(env, contextIdentifier, event) {
58
61
  "use step";
59
62
  const { resolveStoryRuntime } = await import("@ekairos/story/runtime");
60
63
  const { store } = await resolveStoryRuntime(env);
61
- return await store.saveEvent(contextIdentifier, event);
64
+ const saved = await store.saveEvent(contextIdentifier, event);
65
+ console.log("[ekairos/story] story.engine saveTriggerEvent ok");
66
+ console.log("[ekairos/story] story.engine saveTriggerEvent triggerEventId", saved.id);
67
+ return saved;
62
68
  }
63
69
  export async function saveReactionEvent(env, contextIdentifier, event) {
64
70
  "use step";
@@ -89,6 +95,9 @@ export async function createReactionEvent(params) {
89
95
  : `${Date.now()}-${Math.random().toString(16).slice(2)}`;
90
96
  await store.updateContextStatus(params.contextIdentifier, "streaming");
91
97
  const execution = await store.createExecution(params.contextIdentifier, params.triggerEventId, reactionEventId);
98
+ console.log("[ekairos/story] story.engine createReactionEvent ok");
99
+ console.log("[ekairos/story] story.engine createReactionEvent reactionEventId", reactionEventId);
100
+ console.log("[ekairos/story] story.engine createReactionEvent executionId", execution.id);
92
101
  return { reactionEventId, executionId: execution.id };
93
102
  }
94
103
  export async function completeExecution(env, contextIdentifier, executionId, status) {
@@ -1,8 +1,4 @@
1
- export declare function writeStoryLog(params: {
2
- level?: "info" | "debug" | "warn" | "error";
3
- message: string;
4
- args?: Array<string | number | boolean | null>;
5
- }): Promise<void>;
1
+ import type { UIMessageChunk } from "ai";
6
2
  export declare function writeContextSubstate(params: {
7
3
  /**
8
4
  * Ephemeral substate key for the UI (story engine internal state).
@@ -12,9 +8,11 @@ export declare function writeContextSubstate(params: {
12
8
  */
13
9
  key: string | null;
14
10
  transient?: boolean;
11
+ writable?: WritableStream<UIMessageChunk>;
15
12
  }): Promise<void>;
16
13
  export declare function writeContextIdChunk(params: {
17
14
  contextId: string;
15
+ writable?: WritableStream<UIMessageChunk>;
18
16
  }): Promise<void>;
19
17
  export declare function writeStoryPing(params: {
20
18
  /**
@@ -22,6 +20,7 @@ export declare function writeStoryPing(params: {
22
20
  * This is intentionally generic so clients can ignore it safely.
23
21
  */
24
22
  label?: string;
23
+ writable?: WritableStream<UIMessageChunk>;
25
24
  }): Promise<void>;
26
25
  export declare function writeToolOutputs(params: {
27
26
  results: Array<{
@@ -33,8 +32,10 @@ export declare function writeToolOutputs(params: {
33
32
  success: false;
34
33
  errorText: string;
35
34
  }>;
35
+ writable?: WritableStream<UIMessageChunk>;
36
36
  }): Promise<void>;
37
37
  export declare function closeStoryStream(params: {
38
38
  preventClose?: boolean;
39
39
  sendFinish?: boolean;
40
+ writable?: WritableStream<UIMessageChunk>;
40
41
  }): Promise<void>;
@@ -1,19 +1,8 @@
1
- export async function writeStoryLog(params) {
2
- "use step";
3
- const level = params.level ?? "info";
4
- const fn = level === "debug"
5
- ? console.debug
6
- : level === "warn"
7
- ? console.warn
8
- : level === "error"
9
- ? console.error
10
- : console.log;
11
- fn(params.message, ...(params.args ?? []));
12
- }
13
1
  export async function writeContextSubstate(params) {
14
2
  "use step";
15
- const { getWritable } = await import("workflow");
16
- const writable = getWritable();
3
+ const writable = params.writable;
4
+ if (!writable)
5
+ return;
17
6
  const writer = writable.getWriter();
18
7
  try {
19
8
  await writer.write({
@@ -28,8 +17,9 @@ export async function writeContextSubstate(params) {
28
17
  }
29
18
  export async function writeContextIdChunk(params) {
30
19
  "use step";
31
- const { getWritable } = await import("workflow");
32
- const writable = getWritable();
20
+ const writable = params.writable;
21
+ if (!writable)
22
+ return;
33
23
  const writer = writable.getWriter();
34
24
  try {
35
25
  await writer.write({
@@ -44,8 +34,9 @@ export async function writeContextIdChunk(params) {
44
34
  }
45
35
  export async function writeStoryPing(params) {
46
36
  "use step";
47
- const { getWritable } = await import("workflow");
48
- const writable = getWritable();
37
+ const writable = params.writable;
38
+ if (!writable)
39
+ return;
49
40
  const writer = writable.getWriter();
50
41
  try {
51
42
  await writer.write({
@@ -60,8 +51,9 @@ export async function writeStoryPing(params) {
60
51
  }
61
52
  export async function writeToolOutputs(params) {
62
53
  "use step";
63
- const { getWritable } = await import("workflow");
64
- const writable = getWritable();
54
+ const writable = params.writable;
55
+ if (!writable)
56
+ return;
65
57
  const writer = writable.getWriter();
66
58
  try {
67
59
  for (const r of params.results) {
@@ -89,8 +81,9 @@ export async function closeStoryStream(params) {
89
81
  "use step";
90
82
  const sendFinish = params.sendFinish ?? true;
91
83
  const preventClose = params.preventClose ?? false;
92
- const { getWritable } = await import("workflow");
93
- const writable = getWritable();
84
+ const writable = params.writable;
85
+ if (!writable)
86
+ return;
94
87
  if (sendFinish) {
95
88
  const writer = writable.getWriter();
96
89
  try {
@@ -1,6 +1,5 @@
1
- import type { Tool } from "ai";
2
1
  import type { StoryEnvironment } from "./story.config.js";
3
- import { Story, type StoryModelInit, type StoryOptions, type ShouldContinue, type StoryShouldContinueArgs, type StoryReactParams } from "./story.engine.js";
2
+ import { Story, type StoryModelInit, type StoryOptions, type StoryTool, type ShouldContinue, type StoryShouldContinueArgs, type StoryReactParams } from "./story.engine.js";
4
3
  import type { ContextEvent, StoredContext } from "./story.store.js";
5
4
  import { type StoryKey } from "./story.registry.js";
6
5
  export interface StoryConfig<Context, Env extends StoryEnvironment = StoryEnvironment> {
@@ -27,11 +26,11 @@ export interface StoryConfig<Context, Env extends StoryEnvironment = StoryEnviro
27
26
  /**
28
27
  * Actions available to the model (aka "tools" in AI SDK terminology).
29
28
  */
30
- actions: (context: StoredContext<Context>, env: Env) => Promise<Record<string, Tool>> | Record<string, Tool>;
29
+ actions: (context: StoredContext<Context>, env: Env) => Promise<Record<string, StoryTool>> | Record<string, StoryTool>;
31
30
  /**
32
31
  * @deprecated Use `actions()` instead.
33
32
  */
34
- tools?: (context: StoredContext<Context>, env: Env) => Promise<Record<string, Tool>> | Record<string, Tool>;
33
+ tools?: (context: StoredContext<Context>, env: Env) => Promise<Record<string, StoryTool>> | Record<string, StoryTool>;
35
34
  /**
36
35
  * Model configuration (DurableAgent-style).
37
36
  *
@@ -59,7 +58,7 @@ export declare function story<Context, Env extends StoryEnvironment = StoryEnvir
59
58
  type AnyContextInitializer<Env extends StoryEnvironment> = (context: StoredContext<any>, env: Env) => Promise<any> | any;
60
59
  type InferContextFromInitializer<I extends AnyContextInitializer<any>> = Awaited<ReturnType<I>>;
61
60
  type BuilderSystemPrompt<Context, Env extends StoryEnvironment> = (context: StoredContext<Context>, env: Env) => Promise<string> | string;
62
- type BuilderTools<Context, Env extends StoryEnvironment> = (context: StoredContext<Context>, env: Env) => Promise<Record<string, Tool>> | Record<string, Tool>;
61
+ type BuilderTools<Context, Env extends StoryEnvironment> = (context: StoredContext<Context>, env: Env) => Promise<Record<string, StoryTool>> | Record<string, StoryTool>;
63
62
  type BuilderExpandEvents<Context, Env extends StoryEnvironment> = (events: ContextEvent[], context: StoredContext<Context>, env: Env) => Promise<ContextEvent[]> | ContextEvent[];
64
63
  type BuilderShouldContinue<Context, Env extends StoryEnvironment> = (args: StoryShouldContinueArgs<Context, Env>) => Promise<ShouldContinue> | ShouldContinue;
65
64
  type BuilderModel<Context, Env extends StoryEnvironment> = StoryModelInit | ((context: StoredContext<Context>, env: Env) => StoryModelInit);
@@ -1,4 +1,4 @@
1
- import type { Tool } from "ai";
1
+ import type { Tool, UIMessageChunk } from "ai";
2
2
  import type { StoryEnvironment } from "./story.config.js";
3
3
  import type { ContextEvent, ContextIdentifier, StoredContext } from "./story.store.js";
4
4
  export interface StoryOptions<Context = any, Env extends StoryEnvironment = StoryEnvironment> {
@@ -47,6 +47,16 @@ export interface StoryStreamOptions {
47
47
  * Default: false.
48
48
  */
49
49
  silent?: boolean;
50
+ /**
51
+ * Optional workflow writable stream to emit UIMessageChunks into.
52
+ *
53
+ * When omitted, the story will run without emitting stream chunks (equivalent to "headless"
54
+ * execution for output purposes).
55
+ *
56
+ * IMPORTANT: We intentionally avoid calling `getWritable()` inside steps; callers can pass a
57
+ * writable from a `"use workflow"` function if they need streaming.
58
+ */
59
+ writable?: WritableStream<UIMessageChunk>;
50
60
  }
51
61
  /**
52
62
  * Model initializer (DurableAgent-style).
@@ -65,6 +75,58 @@ export type StoryReactParams<Env extends StoryEnvironment = StoryEnvironment> =
65
75
  context?: ContextIdentifier | null;
66
76
  options?: StoryStreamOptions;
67
77
  };
78
+ /**
79
+ * Payload expected to resume an auto=false tool execution.
80
+ *
81
+ * This must be serializable because it crosses the workflow hook boundary.
82
+ *
83
+ * See: https://useworkflow.dev/docs/foundations/hooks
84
+ */
85
+ export type StoryToolApprovalPayload = {
86
+ approved: true;
87
+ comment?: string;
88
+ args?: Record<string, unknown>;
89
+ } | {
90
+ approved: false;
91
+ comment?: string;
92
+ };
93
+ /**
94
+ * Deterministic hook token for approving an `auto: false` tool call.
95
+ *
96
+ * External systems can resume the hook with:
97
+ * `resumeHook(toolApprovalHookToken({ executionId, toolCallId }), { approved: true })`
98
+ */
99
+ export declare function toolApprovalHookToken(params: {
100
+ executionId: string;
101
+ toolCallId: string;
102
+ }): string;
103
+ /**
104
+ * Deterministic webhook token for approving an `auto: false` tool call.
105
+ *
106
+ * When using Workflow DevKit, the webhook is available at:
107
+ * `/.well-known/workflow/v1/webhook/:token`
108
+ *
109
+ * See: https://useworkflow.dev/docs/foundations/hooks
110
+ */
111
+ export declare function toolApprovalWebhookToken(params: {
112
+ executionId: string;
113
+ toolCallId: string;
114
+ }): string;
115
+ /**
116
+ * Story-level tool type.
117
+ *
118
+ * Allows stories to attach metadata to actions/tools (e.g. `{ auto: false }`)
119
+ * while remaining compatible with the AI SDK `Tool` runtime shape.
120
+ *
121
+ * Default behavior when omitted: `auto === true`.
122
+ */
123
+ export type StoryTool = Tool & {
124
+ /**
125
+ * If `false`, this action is not intended for automatic execution by the engine.
126
+ * (Validation/enforcement can be added by callers; default is `true`.)
127
+ */
128
+ auto?: boolean;
129
+ };
68
130
  /**
69
131
  * ## Story loop continuation signal
70
132
  *
@@ -103,7 +165,7 @@ export declare abstract class Story<Context, Env extends StoryEnvironment = Stor
103
165
  constructor(opts?: StoryOptions<Context, Env>);
104
166
  protected abstract initialize(context: StoredContext<Context>, env: Env): Promise<Context> | Context;
105
167
  protected abstract buildSystemPrompt(context: StoredContext<Context>, env: Env): Promise<string> | string;
106
- protected abstract buildTools(context: StoredContext<Context>, env: Env): Promise<Record<string, Tool>> | Record<string, Tool>;
168
+ protected abstract buildTools(context: StoredContext<Context>, env: Env): Promise<Record<string, StoryTool>> | Record<string, StoryTool>;
107
169
  /**
108
170
  * First-class event expansion stage (runs on every iteration of the durable loop).
109
171
  *
@@ -1,49 +1,27 @@
1
1
  import { applyToolExecutionResultToParts } from "./story.toolcalls.js";
2
2
  import { executeReaction } from "./steps/reaction.steps.js";
3
3
  import { toolsToModelTools } from "./tools-to-model-tools.js";
4
- import { closeStoryStream, writeContextSubstate, writeStoryLog, writeStoryPing, writeToolOutputs, } from "./steps/stream.steps.js";
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 safeLogArg(value) {
7
- if (value === null)
8
- return null;
9
- if (value === undefined)
10
- return null;
11
- const t = typeof value;
12
- if (t === "string" || t === "number" || t === "boolean")
13
- return value;
14
- if (t === "bigint")
15
- return String(value);
16
- try {
17
- const seen = new WeakSet();
18
- return JSON.stringify(value, (_k, v) => {
19
- if (typeof v === "bigint")
20
- return String(v);
21
- if (typeof v === "string" && v.length > 5000)
22
- return "[truncated-string]";
23
- if (typeof v === "object" && v !== null) {
24
- if (seen.has(v))
25
- return "[circular]";
26
- seen.add(v);
27
- }
28
- return v;
29
- }, 2);
30
- }
31
- catch {
32
- try {
33
- return String(value);
34
- }
35
- catch {
36
- return "[unserializable]";
37
- }
38
- }
39
- }
40
- async function storyEngineInfo(message, ...args) {
41
- // CRITICAL: static string log messages only. Dynamic values go in args.
42
- await writeStoryLog({ level: "info", message, args: args.map(safeLogArg) });
6
+ /**
7
+ * Deterministic hook token for approving an `auto: false` tool call.
8
+ *
9
+ * External systems can resume the hook with:
10
+ * `resumeHook(toolApprovalHookToken({ executionId, toolCallId }), { approved: true })`
11
+ */
12
+ export function toolApprovalHookToken(params) {
13
+ return `ekairos_story:tool-approval:${params.executionId}:${params.toolCallId}`;
43
14
  }
44
- async function storyEngineDebug(message, ...args) {
45
- // CRITICAL: static string log messages only. Dynamic values go in args.
46
- await writeStoryLog({ level: "debug", message, args: args.map(safeLogArg) });
15
+ /**
16
+ * Deterministic webhook token for approving an `auto: false` tool call.
17
+ *
18
+ * When using Workflow DevKit, the webhook is available at:
19
+ * `/.well-known/workflow/v1/webhook/:token`
20
+ *
21
+ * See: https://useworkflow.dev/docs/foundations/hooks
22
+ */
23
+ export function toolApprovalWebhookToken(params) {
24
+ return `ekairos_story:tool-approval-webhook:${params.executionId}:${params.toolCallId}`;
47
25
  }
48
26
  export class Story {
49
27
  constructor(opts = {}) {
@@ -101,17 +79,10 @@ export class Story {
101
79
  const preventClose = params.options?.preventClose ?? false;
102
80
  const sendFinish = params.options?.sendFinish ?? true;
103
81
  const silent = params.options?.silent ?? false;
104
- await storyEngineInfo("[ekairos/story] story.engine react begin");
105
- await storyEngineInfo("[ekairos/story] story.engine react contextIdentifier", params.contextIdentifier);
106
- await storyEngineInfo("[ekairos/story] story.engine react maxIterations", maxIterations);
107
- await storyEngineInfo("[ekairos/story] story.engine react maxModelSteps", maxModelSteps);
108
- await storyEngineInfo("[ekairos/story] story.engine react silent", silent);
82
+ const writable = params.options?.writable;
109
83
  // 1) Ensure context exists (step)
110
- const ctxResult = await initializeContext(params.env, params.contextIdentifier, { silent });
84
+ const ctxResult = await initializeContext(params.env, params.contextIdentifier, { silent, writable });
111
85
  const currentContext = ctxResult.context;
112
- await storyEngineInfo("[ekairos/story] story.engine initializeContext ok");
113
- await storyEngineInfo("[ekairos/story] story.engine initializeContext contextId", currentContext.id);
114
- await storyEngineInfo("[ekairos/story] story.engine initializeContext isNew", ctxResult.isNew);
115
86
  const contextSelector = params.contextIdentifier?.id
116
87
  ? { id: String(params.contextIdentifier.id) }
117
88
  : params.contextIdentifier?.key
@@ -123,20 +94,15 @@ export class Story {
123
94
  // 2) Persist trigger event + create execution shell (steps)
124
95
  const persistedTriggerEvent = await saveTriggerEvent(params.env, contextSelector, triggerEvent);
125
96
  const triggerEventId = persistedTriggerEvent.id;
126
- await storyEngineInfo("[ekairos/story] story.engine saveTriggerEvent ok");
127
- await storyEngineInfo("[ekairos/story] story.engine saveTriggerEvent triggerEventId", triggerEventId);
128
97
  const { reactionEventId, executionId } = await createReactionEvent({
129
98
  env: params.env,
130
99
  contextIdentifier: contextSelector,
131
100
  triggerEventId,
132
101
  });
133
- await storyEngineInfo("[ekairos/story] story.engine createReactionEvent ok");
134
- await storyEngineInfo("[ekairos/story] story.engine createReactionEvent reactionEventId", reactionEventId);
135
- await storyEngineInfo("[ekairos/story] story.engine createReactionEvent executionId", executionId);
136
102
  // Emit a simple ping chunk early so clients can validate that streaming works end-to-end.
137
103
  // This should be ignored safely by clients that don't care about it.
138
104
  if (!silent) {
139
- await writeStoryPing({ label: "story-start" });
105
+ await writeStoryPing({ label: "story-start", writable });
140
106
  }
141
107
  let reactionEvent = null;
142
108
  // Latest persisted context state for this run (we keep it in memory; store is updated via steps).
@@ -150,7 +116,7 @@ export class Story {
150
116
  }
151
117
  try {
152
118
  if (!silent) {
153
- await closeStoryStream({ preventClose, sendFinish });
119
+ await closeStoryStream({ preventClose, sendFinish, writable });
154
120
  }
155
121
  }
156
122
  catch {
@@ -159,22 +125,14 @@ export class Story {
159
125
  };
160
126
  try {
161
127
  for (let iter = 0; iter < maxIterations; iter++) {
162
- await storyEngineInfo("[ekairos/story] story.engine === LOOP ITERATION ===", iter);
163
128
  // Hook: Story DSL `context()` (implemented by subclasses via `initialize()`)
164
- await storyEngineInfo("[ekairos/story] >>> HOOK context() BEGIN", iter);
165
129
  const nextContent = await this.initialize(updatedContext, params.env);
166
- await storyEngineInfo("[ekairos/story] <<< HOOK context() END", iter);
167
130
  updatedContext = await updateContextContent(params.env, contextSelector, nextContent);
168
- await storyEngineInfo("[ekairos/story] story.engine updateContextContent ok");
169
131
  await this.opts.onContextUpdated?.({ env: params.env, context: updatedContext });
170
132
  // Hook: Story DSL `narrative()` (implemented by subclasses via `buildSystemPrompt()`)
171
- await storyEngineInfo("[ekairos/story] >>> HOOK narrative() BEGIN", iter);
172
133
  const systemPrompt = await this.buildSystemPrompt(updatedContext, params.env);
173
- await storyEngineInfo("[ekairos/story] <<< HOOK narrative() END", iter);
174
134
  // Hook: Story DSL `actions()` (implemented by subclasses via `buildTools()`)
175
- await storyEngineInfo("[ekairos/story] >>> HOOK actions() BEGIN", iter);
176
135
  const toolsAll = await this.buildTools(updatedContext, params.env);
177
- await storyEngineInfo("[ekairos/story] <<< HOOK actions() END", iter);
178
136
  // IMPORTANT: step args must be serializable.
179
137
  // Match DurableAgent behavior: convert tool input schemas to plain JSON Schema in workflow context.
180
138
  const toolsForModel = toolsToModelTools(toolsAll);
@@ -189,20 +147,14 @@ export class Story {
189
147
  // Only emit a `start` chunk once per story turn.
190
148
  sendStart: !silent && iter === 0 && reactionEvent === null,
191
149
  silent,
150
+ writable,
192
151
  });
193
- await storyEngineInfo("[ekairos/story] story.engine executeReaction ok");
194
- await storyEngineInfo("[ekairos/story] story.engine executeReaction toolCallsCount", toolCalls.length);
195
- if (toolCalls.length) {
196
- await storyEngineInfo("[ekairos/story] >>> TOOL_CALLS requested", toolCalls.map((tc) => tc?.toolName).filter(Boolean));
197
- await storyEngineDebug("[ekairos/story] >>> TOOL_CALLS payload", toolCalls);
198
- }
199
152
  // Persist/append the assistant event for this iteration
200
153
  if (!reactionEvent) {
201
154
  reactionEvent = await saveReactionEvent(params.env, contextSelector, {
202
155
  ...assistantEvent,
203
156
  status: "pending",
204
157
  });
205
- await storyEngineInfo("[ekairos/story] story.engine saveReactionEvent ok");
206
158
  }
207
159
  else {
208
160
  reactionEvent = await updateEvent(params.env, reactionEvent.id, {
@@ -215,14 +167,11 @@ export class Story {
215
167
  },
216
168
  status: "pending",
217
169
  });
218
- await storyEngineInfo("[ekairos/story] story.engine updateEvent appendAssistantParts ok");
219
170
  }
220
171
  this.opts.onEventCreated?.(assistantEvent);
221
172
  // Done: no tool calls requested by the model
222
173
  if (!toolCalls.length) {
223
- await storyEngineInfo("[ekairos/story] >>> HOOK onEnd() BEGIN", iter);
224
174
  const endResult = await this.callOnEnd(assistantEvent);
225
- await storyEngineInfo("[ekairos/story] <<< HOOK onEnd() END", iter, endResult);
226
175
  if (endResult) {
227
176
  // Mark reaction event completed
228
177
  await updateEvent(params.env, reactionEventId, {
@@ -232,7 +181,7 @@ export class Story {
232
181
  await updateContextStatus(params.env, contextSelector, "open");
233
182
  await completeExecution(params.env, contextSelector, executionId, "completed");
234
183
  if (!silent) {
235
- await closeStoryStream({ preventClose, sendFinish });
184
+ await closeStoryStream({ preventClose, sendFinish, writable });
236
185
  }
237
186
  return {
238
187
  contextId: currentContext.id,
@@ -245,12 +194,11 @@ export class Story {
245
194
  }
246
195
  // Execute tool calls (workflow context; tool implementations decide step vs workflow)
247
196
  if (!silent && toolCalls.length) {
248
- await writeContextSubstate({ key: "actions", transient: true });
197
+ await writeContextSubstate({ key: "actions", transient: true, writable });
249
198
  }
250
199
  const executionResults = await Promise.all(toolCalls.map(async (tc) => {
251
200
  const toolDef = toolsAll[tc.toolName];
252
201
  if (!toolDef || typeof toolDef.execute !== "function") {
253
- await storyEngineInfo("[ekairos/story] story.engine toolExecution missingTool", tc?.toolName);
254
202
  return {
255
203
  tc,
256
204
  success: false,
@@ -258,10 +206,42 @@ export class Story {
258
206
  errorText: `Tool "${tc.toolName}" not found or has no execute().`,
259
207
  };
260
208
  }
261
- await storyEngineInfo("[ekairos/story] >>> TOOL_EXEC BEGIN", tc?.toolName, tc?.toolCallId);
262
- await storyEngineDebug("[ekairos/story] >>> TOOL_EXEC input", tc?.toolName, tc?.toolCallId, tc?.args);
263
209
  try {
264
- const output = await toolDef.execute(tc.args, {
210
+ let toolArgs = tc.args;
211
+ if (toolDef?.auto === false) {
212
+ const { createHook, createWebhook } = await import("workflow");
213
+ const hookToken = toolApprovalHookToken({
214
+ executionId,
215
+ toolCallId: String(tc.toolCallId),
216
+ });
217
+ const webhookToken = toolApprovalWebhookToken({
218
+ executionId,
219
+ toolCallId: String(tc.toolCallId),
220
+ });
221
+ const hook = createHook({ token: hookToken });
222
+ const webhook = createWebhook({ token: webhookToken });
223
+ const approvalOrRequest = await Promise.race([
224
+ hook.then((approval) => ({ source: "hook", approval })),
225
+ webhook.then((request) => ({ source: "webhook", request })),
226
+ ]);
227
+ const approval = approvalOrRequest.source === "hook"
228
+ ? approvalOrRequest.approval
229
+ : await approvalOrRequest.request.json().catch(() => null);
230
+ if (!approval || approval.approved !== true) {
231
+ return {
232
+ tc,
233
+ success: false,
234
+ output: null,
235
+ errorText: approval && "comment" in approval && approval.comment
236
+ ? `Tool execution not approved: ${approval.comment}`
237
+ : "Tool execution not approved",
238
+ };
239
+ }
240
+ if ("args" in approval && approval.args !== undefined) {
241
+ toolArgs = approval.args;
242
+ }
243
+ }
244
+ const output = await toolDef.execute(toolArgs, {
265
245
  toolCallId: tc.toolCallId,
266
246
  messages: messagesForModel,
267
247
  eventId: reactionEventId,
@@ -269,13 +249,9 @@ export class Story {
269
249
  triggerEventId,
270
250
  contextId: currentContext.id,
271
251
  });
272
- await storyEngineInfo("[ekairos/story] <<< TOOL_EXEC OK", tc?.toolName, tc?.toolCallId);
273
- await storyEngineDebug("[ekairos/story] <<< TOOL_EXEC output", tc?.toolName, tc?.toolCallId, output);
274
252
  return { tc, success: true, output };
275
253
  }
276
254
  catch (e) {
277
- await storyEngineInfo("[ekairos/story] <<< TOOL_EXEC FAILED", tc?.toolName, tc?.toolCallId);
278
- await storyEngineDebug("[ekairos/story] <<< TOOL_EXEC error", tc?.toolName, tc?.toolCallId, e instanceof Error ? e.message : String(e));
279
255
  return {
280
256
  tc,
281
257
  success: false,
@@ -284,7 +260,6 @@ export class Story {
284
260
  };
285
261
  }
286
262
  }));
287
- await storyEngineInfo("[ekairos/story] story.engine toolExecution resultsCount", executionResults.length);
288
263
  // Emit tool outputs to the workflow stream (step)
289
264
  if (!silent) {
290
265
  await writeToolOutputs({
@@ -295,11 +270,12 @@ export class Story {
295
270
  success: false,
296
271
  errorText: r.errorText,
297
272
  }),
273
+ writable,
298
274
  });
299
275
  }
300
276
  // Clear action status once tool execution results have been emitted.
301
277
  if (!silent && toolCalls.length) {
302
- await writeContextSubstate({ key: null, transient: true });
278
+ await writeContextSubstate({ key: null, transient: true, writable });
303
279
  }
304
280
  // Merge tool results into persisted parts (so next LLM call can see them)
305
281
  if (reactionEvent) {
@@ -316,7 +292,6 @@ export class Story {
316
292
  content: { parts },
317
293
  status: "pending",
318
294
  });
319
- await storyEngineInfo("[ekairos/story] story.engine updateEvent mergeToolResults ok");
320
295
  }
321
296
  // Callback for observability/integration
322
297
  for (const r of executionResults) {
@@ -332,7 +307,6 @@ export class Story {
332
307
  // Stop/continue boundary: allow the Story to decide if the loop should continue.
333
308
  // IMPORTANT: we call this after tool results have been merged into the persisted `reactionEvent`,
334
309
  // so stories can inspect `reactionEvent.content.parts` deterministically.
335
- await storyEngineInfo("[ekairos/story] >>> HOOK shouldContinue() BEGIN", iter);
336
310
  const continueLoop = await this.shouldContinue({
337
311
  env: params.env,
338
312
  context: updatedContext,
@@ -341,7 +315,6 @@ export class Story {
341
315
  toolCalls,
342
316
  toolExecutionResults: executionResults,
343
317
  });
344
- await storyEngineInfo("[ekairos/story] <<< HOOK shouldContinue() END", iter, continueLoop);
345
318
  if (continueLoop === false) {
346
319
  await updateEvent(params.env, reactionEventId, {
347
320
  ...reactionEvent,
@@ -350,7 +323,7 @@ export class Story {
350
323
  await updateContextStatus(params.env, contextSelector, "open");
351
324
  await completeExecution(params.env, contextSelector, executionId, "completed");
352
325
  if (!silent) {
353
- await closeStoryStream({ preventClose, sendFinish });
326
+ await closeStoryStream({ preventClose, sendFinish, writable });
354
327
  }
355
328
  return {
356
329
  contextId: currentContext.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekairos/story",
3
- "version": "1.21.57-beta.0",
3
+ "version": "1.21.59-beta.0",
4
4
  "description": "Pulzar Story - Workflow-based AI Stories",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -70,7 +70,7 @@
70
70
  },
71
71
  "dependencies": {
72
72
  "@ai-sdk/openai": "^2.0.52",
73
- "@ekairos/domain": "^1.21.57-beta.0",
73
+ "@ekairos/domain": "^1.21.59-beta.0",
74
74
  "@instantdb/admin": "^0.22.13",
75
75
  "@instantdb/core": "^0.22.13",
76
76
  "@vercel/sandbox": "^0.0.23",