@ekairos/story 1.21.58-beta.0 → 1.21.60-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 +2 -4
- package/dist/steps/stream.steps.d.ts +6 -0
- package/dist/steps/stream.steps.js +15 -10
- package/dist/story.builder.d.ts +4 -5
- package/dist/story.engine.d.ts +64 -2
- package/dist/story.engine.js +65 -8
- 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;
|
|
@@ -30,10 +30,8 @@ export async function initializeContext(env, contextIdentifier, opts) {
|
|
|
30
30
|
console.log("[ekairos/story] story.engine initializeContext isNew", result.isNew);
|
|
31
31
|
// If we're running in a non-streaming context (e.g. tests or headless usage),
|
|
32
32
|
// we skip writing stream chunks entirely.
|
|
33
|
-
if (!opts?.silent) {
|
|
34
|
-
const
|
|
35
|
-
const writable = getWritable();
|
|
36
|
-
const writer = writable.getWriter();
|
|
33
|
+
if (!opts?.silent && opts?.writable) {
|
|
34
|
+
const writer = opts.writable.getWriter();
|
|
37
35
|
try {
|
|
38
36
|
await writer.write({
|
|
39
37
|
type: "data-context-id",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UIMessageChunk } from "ai";
|
|
1
2
|
export declare function writeContextSubstate(params: {
|
|
2
3
|
/**
|
|
3
4
|
* Ephemeral substate key for the UI (story engine internal state).
|
|
@@ -7,9 +8,11 @@ export declare function writeContextSubstate(params: {
|
|
|
7
8
|
*/
|
|
8
9
|
key: string | null;
|
|
9
10
|
transient?: boolean;
|
|
11
|
+
writable?: WritableStream<UIMessageChunk>;
|
|
10
12
|
}): Promise<void>;
|
|
11
13
|
export declare function writeContextIdChunk(params: {
|
|
12
14
|
contextId: string;
|
|
15
|
+
writable?: WritableStream<UIMessageChunk>;
|
|
13
16
|
}): Promise<void>;
|
|
14
17
|
export declare function writeStoryPing(params: {
|
|
15
18
|
/**
|
|
@@ -17,6 +20,7 @@ export declare function writeStoryPing(params: {
|
|
|
17
20
|
* This is intentionally generic so clients can ignore it safely.
|
|
18
21
|
*/
|
|
19
22
|
label?: string;
|
|
23
|
+
writable?: WritableStream<UIMessageChunk>;
|
|
20
24
|
}): Promise<void>;
|
|
21
25
|
export declare function writeToolOutputs(params: {
|
|
22
26
|
results: Array<{
|
|
@@ -28,8 +32,10 @@ export declare function writeToolOutputs(params: {
|
|
|
28
32
|
success: false;
|
|
29
33
|
errorText: string;
|
|
30
34
|
}>;
|
|
35
|
+
writable?: WritableStream<UIMessageChunk>;
|
|
31
36
|
}): Promise<void>;
|
|
32
37
|
export declare function closeStoryStream(params: {
|
|
33
38
|
preventClose?: boolean;
|
|
34
39
|
sendFinish?: boolean;
|
|
40
|
+
writable?: WritableStream<UIMessageChunk>;
|
|
35
41
|
}): Promise<void>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export async function writeContextSubstate(params) {
|
|
2
2
|
"use step";
|
|
3
|
-
const
|
|
4
|
-
|
|
3
|
+
const writable = params.writable;
|
|
4
|
+
if (!writable)
|
|
5
|
+
return;
|
|
5
6
|
const writer = writable.getWriter();
|
|
6
7
|
try {
|
|
7
8
|
await writer.write({
|
|
@@ -16,8 +17,9 @@ export async function writeContextSubstate(params) {
|
|
|
16
17
|
}
|
|
17
18
|
export async function writeContextIdChunk(params) {
|
|
18
19
|
"use step";
|
|
19
|
-
const
|
|
20
|
-
|
|
20
|
+
const writable = params.writable;
|
|
21
|
+
if (!writable)
|
|
22
|
+
return;
|
|
21
23
|
const writer = writable.getWriter();
|
|
22
24
|
try {
|
|
23
25
|
await writer.write({
|
|
@@ -32,8 +34,9 @@ export async function writeContextIdChunk(params) {
|
|
|
32
34
|
}
|
|
33
35
|
export async function writeStoryPing(params) {
|
|
34
36
|
"use step";
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
+
const writable = params.writable;
|
|
38
|
+
if (!writable)
|
|
39
|
+
return;
|
|
37
40
|
const writer = writable.getWriter();
|
|
38
41
|
try {
|
|
39
42
|
await writer.write({
|
|
@@ -48,8 +51,9 @@ export async function writeStoryPing(params) {
|
|
|
48
51
|
}
|
|
49
52
|
export async function writeToolOutputs(params) {
|
|
50
53
|
"use step";
|
|
51
|
-
const
|
|
52
|
-
|
|
54
|
+
const writable = params.writable;
|
|
55
|
+
if (!writable)
|
|
56
|
+
return;
|
|
53
57
|
const writer = writable.getWriter();
|
|
54
58
|
try {
|
|
55
59
|
for (const r of params.results) {
|
|
@@ -77,8 +81,9 @@ export async function closeStoryStream(params) {
|
|
|
77
81
|
"use step";
|
|
78
82
|
const sendFinish = params.sendFinish ?? true;
|
|
79
83
|
const preventClose = params.preventClose ?? false;
|
|
80
|
-
const
|
|
81
|
-
|
|
84
|
+
const writable = params.writable;
|
|
85
|
+
if (!writable)
|
|
86
|
+
return;
|
|
82
87
|
if (sendFinish) {
|
|
83
88
|
const writer = writable.getWriter();
|
|
84
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
|
@@ -3,6 +3,26 @@ 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
|
+
/**
|
|
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}`;
|
|
14
|
+
}
|
|
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}`;
|
|
25
|
+
}
|
|
6
26
|
export class Story {
|
|
7
27
|
constructor(opts = {}) {
|
|
8
28
|
this.opts = opts;
|
|
@@ -59,8 +79,9 @@ export class Story {
|
|
|
59
79
|
const preventClose = params.options?.preventClose ?? false;
|
|
60
80
|
const sendFinish = params.options?.sendFinish ?? true;
|
|
61
81
|
const silent = params.options?.silent ?? false;
|
|
82
|
+
const writable = params.options?.writable;
|
|
62
83
|
// 1) Ensure context exists (step)
|
|
63
|
-
const ctxResult = await initializeContext(params.env, params.contextIdentifier, { silent });
|
|
84
|
+
const ctxResult = await initializeContext(params.env, params.contextIdentifier, { silent, writable });
|
|
64
85
|
const currentContext = ctxResult.context;
|
|
65
86
|
const contextSelector = params.contextIdentifier?.id
|
|
66
87
|
? { id: String(params.contextIdentifier.id) }
|
|
@@ -81,7 +102,7 @@ export class Story {
|
|
|
81
102
|
// Emit a simple ping chunk early so clients can validate that streaming works end-to-end.
|
|
82
103
|
// This should be ignored safely by clients that don't care about it.
|
|
83
104
|
if (!silent) {
|
|
84
|
-
await writeStoryPing({ label: "story-start" });
|
|
105
|
+
await writeStoryPing({ label: "story-start", writable });
|
|
85
106
|
}
|
|
86
107
|
let reactionEvent = null;
|
|
87
108
|
// Latest persisted context state for this run (we keep it in memory; store is updated via steps).
|
|
@@ -95,7 +116,7 @@ export class Story {
|
|
|
95
116
|
}
|
|
96
117
|
try {
|
|
97
118
|
if (!silent) {
|
|
98
|
-
await closeStoryStream({ preventClose, sendFinish });
|
|
119
|
+
await closeStoryStream({ preventClose, sendFinish, writable });
|
|
99
120
|
}
|
|
100
121
|
}
|
|
101
122
|
catch {
|
|
@@ -126,6 +147,7 @@ export class Story {
|
|
|
126
147
|
// Only emit a `start` chunk once per story turn.
|
|
127
148
|
sendStart: !silent && iter === 0 && reactionEvent === null,
|
|
128
149
|
silent,
|
|
150
|
+
writable,
|
|
129
151
|
});
|
|
130
152
|
// Persist/append the assistant event for this iteration
|
|
131
153
|
if (!reactionEvent) {
|
|
@@ -159,7 +181,7 @@ export class Story {
|
|
|
159
181
|
await updateContextStatus(params.env, contextSelector, "open");
|
|
160
182
|
await completeExecution(params.env, contextSelector, executionId, "completed");
|
|
161
183
|
if (!silent) {
|
|
162
|
-
await closeStoryStream({ preventClose, sendFinish });
|
|
184
|
+
await closeStoryStream({ preventClose, sendFinish, writable });
|
|
163
185
|
}
|
|
164
186
|
return {
|
|
165
187
|
contextId: currentContext.id,
|
|
@@ -172,7 +194,7 @@ export class Story {
|
|
|
172
194
|
}
|
|
173
195
|
// Execute tool calls (workflow context; tool implementations decide step vs workflow)
|
|
174
196
|
if (!silent && toolCalls.length) {
|
|
175
|
-
await writeContextSubstate({ key: "actions", transient: true });
|
|
197
|
+
await writeContextSubstate({ key: "actions", transient: true, writable });
|
|
176
198
|
}
|
|
177
199
|
const executionResults = await Promise.all(toolCalls.map(async (tc) => {
|
|
178
200
|
const toolDef = toolsAll[tc.toolName];
|
|
@@ -185,7 +207,41 @@ export class Story {
|
|
|
185
207
|
};
|
|
186
208
|
}
|
|
187
209
|
try {
|
|
188
|
-
|
|
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, {
|
|
189
245
|
toolCallId: tc.toolCallId,
|
|
190
246
|
messages: messagesForModel,
|
|
191
247
|
eventId: reactionEventId,
|
|
@@ -214,11 +270,12 @@ export class Story {
|
|
|
214
270
|
success: false,
|
|
215
271
|
errorText: r.errorText,
|
|
216
272
|
}),
|
|
273
|
+
writable,
|
|
217
274
|
});
|
|
218
275
|
}
|
|
219
276
|
// Clear action status once tool execution results have been emitted.
|
|
220
277
|
if (!silent && toolCalls.length) {
|
|
221
|
-
await writeContextSubstate({ key: null, transient: true });
|
|
278
|
+
await writeContextSubstate({ key: null, transient: true, writable });
|
|
222
279
|
}
|
|
223
280
|
// Merge tool results into persisted parts (so next LLM call can see them)
|
|
224
281
|
if (reactionEvent) {
|
|
@@ -266,7 +323,7 @@ export class Story {
|
|
|
266
323
|
await updateContextStatus(params.env, contextSelector, "open");
|
|
267
324
|
await completeExecution(params.env, contextSelector, executionId, "completed");
|
|
268
325
|
if (!silent) {
|
|
269
|
-
await closeStoryStream({ preventClose, sendFinish });
|
|
326
|
+
await closeStoryStream({ preventClose, sendFinish, writable });
|
|
270
327
|
}
|
|
271
328
|
return {
|
|
272
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.60-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.60-beta.0",
|
|
74
74
|
"@instantdb/admin": "^0.22.13",
|
|
75
75
|
"@instantdb/core": "^0.22.13",
|
|
76
76
|
"@vercel/sandbox": "^0.0.23",
|