@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 +1 -0
- package/dist/mirror.d.ts +41 -0
- package/dist/mirror.js +1 -0
- package/dist/steps/mirror.steps.d.ts +6 -0
- package/dist/steps/mirror.steps.js +44 -0
- package/dist/steps/reaction.steps.d.ts +2 -1
- package/dist/steps/reaction.steps.js +2 -2
- package/dist/steps/store.steps.d.ts +2 -0
- package/dist/steps/store.steps.js +14 -5
- package/dist/steps/stream.steps.d.ts +6 -5
- package/dist/steps/stream.steps.js +15 -22
- package/dist/story.builder.d.ts +4 -5
- package/dist/story.engine.d.ts +64 -2
- package/dist/story.engine.js +64 -91
- package/package.json +2 -2
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";
|
package/dist/mirror.d.ts
ADDED
|
@@ -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,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
|
-
:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
16
|
-
|
|
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
|
|
32
|
-
|
|
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
|
|
48
|
-
|
|
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
|
|
64
|
-
|
|
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
|
|
93
|
-
|
|
84
|
+
const writable = params.writable;
|
|
85
|
+
if (!writable)
|
|
86
|
+
return;
|
|
94
87
|
if (sendFinish) {
|
|
95
88
|
const writer = writable.getWriter();
|
|
96
89
|
try {
|
package/dist/story.builder.d.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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);
|
package/dist/story.engine.d.ts
CHANGED
|
@@ -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,
|
|
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
|
*
|
package/dist/story.engine.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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",
|