@ekairos/thread 1.21.88-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/README.md +363 -0
- package/dist/codex.d.ts +95 -0
- package/dist/codex.js +91 -0
- package/dist/env.d.ts +12 -0
- package/dist/env.js +62 -0
- package/dist/events.d.ts +35 -0
- package/dist/events.js +102 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +1 -0
- package/dist/mirror.d.ts +41 -0
- package/dist/mirror.js +1 -0
- package/dist/oidc.d.ts +7 -0
- package/dist/oidc.js +25 -0
- package/dist/polyfills/dom-events.d.ts +1 -0
- package/dist/polyfills/dom-events.js +89 -0
- package/dist/react.d.ts +62 -0
- package/dist/react.js +101 -0
- package/dist/runtime.d.ts +17 -0
- package/dist/runtime.js +23 -0
- package/dist/runtime.step.d.ts +9 -0
- package/dist/runtime.step.js +7 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +200 -0
- package/dist/steps/do-story-stream-step.d.ts +29 -0
- package/dist/steps/do-story-stream-step.js +89 -0
- package/dist/steps/do-thread-stream-step.d.ts +29 -0
- package/dist/steps/do-thread-stream-step.js +90 -0
- package/dist/steps/mirror.steps.d.ts +6 -0
- package/dist/steps/mirror.steps.js +48 -0
- package/dist/steps/reaction.steps.d.ts +43 -0
- package/dist/steps/reaction.steps.js +354 -0
- package/dist/steps/store.steps.d.ts +98 -0
- package/dist/steps/store.steps.js +512 -0
- package/dist/steps/stream.steps.d.ts +41 -0
- package/dist/steps/stream.steps.js +99 -0
- package/dist/steps/trace.steps.d.ts +37 -0
- package/dist/steps/trace.steps.js +265 -0
- package/dist/stores/instant.document-parser.d.ts +6 -0
- package/dist/stores/instant.document-parser.js +210 -0
- package/dist/stores/instant.documents.d.ts +16 -0
- package/dist/stores/instant.documents.js +152 -0
- package/dist/stores/instant.store.d.ts +78 -0
- package/dist/stores/instant.store.js +530 -0
- package/dist/story.actions.d.ts +60 -0
- package/dist/story.actions.js +120 -0
- package/dist/story.builder.d.ts +115 -0
- package/dist/story.builder.js +130 -0
- package/dist/story.config.d.ts +54 -0
- package/dist/story.config.js +125 -0
- package/dist/story.d.ts +2 -0
- package/dist/story.engine.d.ts +224 -0
- package/dist/story.engine.js +464 -0
- package/dist/story.hooks.d.ts +21 -0
- package/dist/story.hooks.js +31 -0
- package/dist/story.js +6 -0
- package/dist/story.registry.d.ts +21 -0
- package/dist/story.registry.js +30 -0
- package/dist/story.store.d.ts +107 -0
- package/dist/story.store.js +1 -0
- package/dist/story.toolcalls.d.ts +60 -0
- package/dist/story.toolcalls.js +73 -0
- package/dist/thread.builder.d.ts +118 -0
- package/dist/thread.builder.js +134 -0
- package/dist/thread.config.d.ts +15 -0
- package/dist/thread.config.js +30 -0
- package/dist/thread.d.ts +3 -0
- package/dist/thread.engine.d.ts +229 -0
- package/dist/thread.engine.js +471 -0
- package/dist/thread.events.d.ts +35 -0
- package/dist/thread.events.js +105 -0
- package/dist/thread.hooks.d.ts +21 -0
- package/dist/thread.hooks.js +31 -0
- package/dist/thread.js +7 -0
- package/dist/thread.reactor.d.ts +82 -0
- package/dist/thread.reactor.js +65 -0
- package/dist/thread.registry.d.ts +21 -0
- package/dist/thread.registry.js +30 -0
- package/dist/thread.store.d.ts +121 -0
- package/dist/thread.store.js +1 -0
- package/dist/thread.toolcalls.d.ts +60 -0
- package/dist/thread.toolcalls.js +73 -0
- package/dist/tools-to-model-tools.d.ts +19 -0
- package/dist/tools-to-model-tools.js +21 -0
- package/package.json +133 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { Tool, UIMessageChunk } from "ai";
|
|
2
|
+
import type { StoryEnvironment } from "./story.config.js";
|
|
3
|
+
import type { ContextEvent, ContextIdentifier, StoredContext } from "./story.store.js";
|
|
4
|
+
import { getClientResumeHookUrl, toolApprovalHookToken, toolApprovalWebhookToken } from "./story.hooks.js";
|
|
5
|
+
export interface StoryOptions<Context = any, Env extends StoryEnvironment = StoryEnvironment> {
|
|
6
|
+
onContextCreated?: (args: {
|
|
7
|
+
env: Env;
|
|
8
|
+
context: StoredContext<Context>;
|
|
9
|
+
}) => void | Promise<void>;
|
|
10
|
+
onContextUpdated?: (args: {
|
|
11
|
+
env: Env;
|
|
12
|
+
context: StoredContext<Context>;
|
|
13
|
+
}) => void | Promise<void>;
|
|
14
|
+
onEventCreated?: (event: ContextEvent) => void | Promise<void>;
|
|
15
|
+
onToolCallExecuted?: (executionEvent: any) => void | Promise<void>;
|
|
16
|
+
onEnd?: (lastEvent: ContextEvent) => void | {
|
|
17
|
+
end?: boolean;
|
|
18
|
+
} | Promise<void | {
|
|
19
|
+
end?: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
export interface StoryStreamOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Maximum loop iterations (LLM call → tool execution → repeat).
|
|
25
|
+
* Default: 20
|
|
26
|
+
*/
|
|
27
|
+
maxIterations?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Maximum model steps per LLM call.
|
|
30
|
+
* Default: 1 (or 5 if you override it in your implementation).
|
|
31
|
+
*/
|
|
32
|
+
maxModelSteps?: number;
|
|
33
|
+
/**
|
|
34
|
+
* If true, we do not close the workflow writable stream.
|
|
35
|
+
* Default: false.
|
|
36
|
+
*/
|
|
37
|
+
preventClose?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* If true, we write a `finish` chunk to the workflow stream.
|
|
40
|
+
* Default: true.
|
|
41
|
+
*/
|
|
42
|
+
sendFinish?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* If true, the story loop runs silently (no UI streaming output).
|
|
45
|
+
*
|
|
46
|
+
* Persistence (contexts/events/executions) still happens normally.
|
|
47
|
+
*
|
|
48
|
+
* Default: false.
|
|
49
|
+
*/
|
|
50
|
+
silent?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Optional workflow writable stream to emit UIMessageChunks into.
|
|
53
|
+
*
|
|
54
|
+
* When omitted, the story will obtain a namespaced writable automatically:
|
|
55
|
+
* `getWritable({ namespace: "context:<contextId>" })`.
|
|
56
|
+
*
|
|
57
|
+
* This allows multiple stories / contexts to stream concurrently within the same workflow run.
|
|
58
|
+
*/
|
|
59
|
+
writable?: WritableStream<UIMessageChunk>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Model initializer (DurableAgent-style).
|
|
63
|
+
*
|
|
64
|
+
* - `string`: Vercel AI Gateway model id (e.g. `"openai/gpt-5"`), resolved inside the LLM step.
|
|
65
|
+
* - `function`: a function that returns a model instance. For Workflow compatibility, this should
|
|
66
|
+
* be a `"use-step"` function (so it can be serialized by reference).
|
|
67
|
+
*/
|
|
68
|
+
export type StoryModelInit = string | (() => Promise<any>) | any;
|
|
69
|
+
export type StoryReactParams<Env extends StoryEnvironment = StoryEnvironment> = {
|
|
70
|
+
env: Env;
|
|
71
|
+
/**
|
|
72
|
+
* Context selector (exclusive: `{ id }` OR `{ key }`).
|
|
73
|
+
* If omitted/null, the story will create a new context.
|
|
74
|
+
*/
|
|
75
|
+
context?: ContextIdentifier | null;
|
|
76
|
+
options?: StoryStreamOptions;
|
|
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
|
+
export { toolApprovalHookToken, toolApprovalWebhookToken, getClientResumeHookUrl };
|
|
94
|
+
/**
|
|
95
|
+
* Story-level tool type.
|
|
96
|
+
*
|
|
97
|
+
* Allows stories to attach metadata to actions/tools (e.g. `{ auto: false }`)
|
|
98
|
+
* while remaining compatible with the AI SDK `Tool` runtime shape.
|
|
99
|
+
*
|
|
100
|
+
* Default behavior when omitted: `auto === true`.
|
|
101
|
+
*/
|
|
102
|
+
export type StoryTool = Tool & {
|
|
103
|
+
/**
|
|
104
|
+
* If `false`, this action is not intended for automatic execution by the engine.
|
|
105
|
+
* (Validation/enforcement can be added by callers; default is `true`.)
|
|
106
|
+
*/
|
|
107
|
+
auto?: boolean;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* ## Story loop continuation signal
|
|
111
|
+
*
|
|
112
|
+
* This hook result is intentionally a **boolean** so stories can be extremely declarative:
|
|
113
|
+
*
|
|
114
|
+
* - `return true` => **continue** the durable loop
|
|
115
|
+
* - `return false` => **finalize** the durable loop
|
|
116
|
+
*
|
|
117
|
+
* (No imports required in callers.)
|
|
118
|
+
*/
|
|
119
|
+
export type ShouldContinue = boolean;
|
|
120
|
+
export type StoryShouldContinueArgs<Context = any, Env extends StoryEnvironment = StoryEnvironment> = {
|
|
121
|
+
env: Env;
|
|
122
|
+
context: StoredContext<Context>;
|
|
123
|
+
/**
|
|
124
|
+
* The persisted reaction event **so far** for the current streaming run.
|
|
125
|
+
*
|
|
126
|
+
* This contains the assistant's streamed parts as well as merged tool execution
|
|
127
|
+
* outcomes (e.g. `state: "output-available"` / `"output-error"`).
|
|
128
|
+
*
|
|
129
|
+
* Stories can inspect `reactionEvent.content.parts` to determine stop conditions
|
|
130
|
+
* (for example: when `tool-end` has an `output-available` state).
|
|
131
|
+
*/
|
|
132
|
+
reactionEvent: ContextEvent;
|
|
133
|
+
assistantEvent: ContextEvent;
|
|
134
|
+
toolCalls: any[];
|
|
135
|
+
toolExecutionResults: Array<{
|
|
136
|
+
tc: any;
|
|
137
|
+
success: boolean;
|
|
138
|
+
output: any;
|
|
139
|
+
errorText?: string;
|
|
140
|
+
}>;
|
|
141
|
+
};
|
|
142
|
+
export declare abstract class Story<Context, Env extends StoryEnvironment = StoryEnvironment> {
|
|
143
|
+
private opts;
|
|
144
|
+
constructor(opts?: StoryOptions<Context, Env>);
|
|
145
|
+
protected abstract initialize(context: StoredContext<Context>, env: Env): Promise<Context> | Context;
|
|
146
|
+
protected abstract buildSystemPrompt(context: StoredContext<Context>, env: Env): Promise<string> | string;
|
|
147
|
+
protected abstract buildTools(context: StoredContext<Context>, env: Env): Promise<Record<string, StoryTool>> | Record<string, StoryTool>;
|
|
148
|
+
/**
|
|
149
|
+
* First-class event expansion stage (runs on every iteration of the durable loop).
|
|
150
|
+
*
|
|
151
|
+
* Use this to expand/normalize events before they are converted into model messages.
|
|
152
|
+
* Typical use-cases:
|
|
153
|
+
* - Expand file/document references into text (LlamaCloud/Reducto/…)
|
|
154
|
+
* - Token compaction / summarization of older parts
|
|
155
|
+
* - Attaching derived context snippets to the next model call
|
|
156
|
+
*
|
|
157
|
+
* IMPORTANT:
|
|
158
|
+
* - This stage is ALWAYS executed by the engine.
|
|
159
|
+
* - If you don't provide an implementation, the default behavior is an identity transform
|
|
160
|
+
* (events pass through unchanged).
|
|
161
|
+
* - If your implementation performs I/O, implement it as a `"use-step"` function (provided via
|
|
162
|
+
* the builder) so results are durable and replay-safe.
|
|
163
|
+
* - If it’s pure/deterministic, it can run in workflow context.
|
|
164
|
+
*/
|
|
165
|
+
protected expandEvents(events: ContextEvent[], _context: StoredContext<Context>, _env: Env): Promise<ContextEvent[]>;
|
|
166
|
+
protected getModel(_context: StoredContext<Context>, _env: Env): StoryModelInit;
|
|
167
|
+
/**
|
|
168
|
+
* Story stop/continue hook.
|
|
169
|
+
*
|
|
170
|
+
* After the model streamed and tools executed, the story can decide whether the loop should
|
|
171
|
+
* continue.
|
|
172
|
+
*
|
|
173
|
+
* Default: `true` (continue).
|
|
174
|
+
*/
|
|
175
|
+
protected shouldContinue(_args: StoryShouldContinueArgs<Context, Env>): Promise<ShouldContinue>;
|
|
176
|
+
/**
|
|
177
|
+
* Workflow-first execution entrypoint.
|
|
178
|
+
*
|
|
179
|
+
* - Streaming is written to the workflow run's output stream.
|
|
180
|
+
* - All I/O is delegated to steps (store access, LLM streaming, stream writes).
|
|
181
|
+
* - This method returns metadata only.
|
|
182
|
+
*/
|
|
183
|
+
/**
|
|
184
|
+
* React to an incoming event and advance the story.
|
|
185
|
+
*
|
|
186
|
+
* This is the primary workflow entrypoint.
|
|
187
|
+
*/
|
|
188
|
+
react(triggerEvent: ContextEvent, params: StoryReactParams<Env>): Promise<{
|
|
189
|
+
contextId: string;
|
|
190
|
+
context: StoredContext<Context>;
|
|
191
|
+
triggerEventId: string;
|
|
192
|
+
reactionEventId: string;
|
|
193
|
+
executionId: string;
|
|
194
|
+
}>;
|
|
195
|
+
/**
|
|
196
|
+
* @deprecated Back-compat: old object-style call signature.
|
|
197
|
+
*/
|
|
198
|
+
react(params: {
|
|
199
|
+
env: Env;
|
|
200
|
+
/** @deprecated Use `triggerEvent` */
|
|
201
|
+
incomingEvent?: ContextEvent;
|
|
202
|
+
triggerEvent?: ContextEvent;
|
|
203
|
+
contextIdentifier: ContextIdentifier | null;
|
|
204
|
+
options?: StoryStreamOptions;
|
|
205
|
+
}): Promise<{
|
|
206
|
+
contextId: string;
|
|
207
|
+
context: StoredContext<Context>;
|
|
208
|
+
triggerEventId: string;
|
|
209
|
+
reactionEventId: string;
|
|
210
|
+
executionId: string;
|
|
211
|
+
}>;
|
|
212
|
+
private static runWorkflow;
|
|
213
|
+
/**
|
|
214
|
+
* @deprecated Use `react()` instead. Kept for backwards compatibility.
|
|
215
|
+
*/
|
|
216
|
+
stream(triggerEvent: ContextEvent, params: StoryReactParams<Env>): Promise<{
|
|
217
|
+
contextId: string;
|
|
218
|
+
context: StoredContext<Context>;
|
|
219
|
+
triggerEventId: string;
|
|
220
|
+
reactionEventId: string;
|
|
221
|
+
executionId: string;
|
|
222
|
+
}>;
|
|
223
|
+
private callOnEnd;
|
|
224
|
+
}
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { getWritable } from "workflow";
|
|
2
|
+
import { registerStoryEnv } from "./env.js";
|
|
3
|
+
import { applyToolExecutionResultToParts } from "./story.toolcalls.js";
|
|
4
|
+
import { executeReaction } from "./steps/reaction.steps.js";
|
|
5
|
+
import { toolsToModelTools } from "./tools-to-model-tools.js";
|
|
6
|
+
import { closeStoryStream, writeContextSubstate, writeStoryPing, writeToolOutputs, } from "./steps/stream.steps.js";
|
|
7
|
+
import { completeExecution, createStoryStep, emitContextIdChunk, initializeContext, saveReactionEvent, saveTriggerAndCreateExecution, saveStoryPartsStep, updateStoryStep, updateContextContent, updateEvent, } from "./steps/store.steps.js";
|
|
8
|
+
import { getClientResumeHookUrl, toolApprovalHookToken, toolApprovalWebhookToken, } from "./story.hooks.js";
|
|
9
|
+
export { toolApprovalHookToken, toolApprovalWebhookToken, getClientResumeHookUrl };
|
|
10
|
+
export class Story {
|
|
11
|
+
constructor(opts = {}) {
|
|
12
|
+
this.opts = opts;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* First-class event expansion stage (runs on every iteration of the durable loop).
|
|
16
|
+
*
|
|
17
|
+
* Use this to expand/normalize events before they are converted into model messages.
|
|
18
|
+
* Typical use-cases:
|
|
19
|
+
* - Expand file/document references into text (LlamaCloud/Reducto/…)
|
|
20
|
+
* - Token compaction / summarization of older parts
|
|
21
|
+
* - Attaching derived context snippets to the next model call
|
|
22
|
+
*
|
|
23
|
+
* IMPORTANT:
|
|
24
|
+
* - This stage is ALWAYS executed by the engine.
|
|
25
|
+
* - If you don't provide an implementation, the default behavior is an identity transform
|
|
26
|
+
* (events pass through unchanged).
|
|
27
|
+
* - If your implementation performs I/O, implement it as a `"use-step"` function (provided via
|
|
28
|
+
* the builder) so results are durable and replay-safe.
|
|
29
|
+
* - If it’s pure/deterministic, it can run in workflow context.
|
|
30
|
+
*/
|
|
31
|
+
async expandEvents(events, _context, _env) {
|
|
32
|
+
return events;
|
|
33
|
+
}
|
|
34
|
+
getModel(_context, _env) {
|
|
35
|
+
return "openai/gpt-5";
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Story stop/continue hook.
|
|
39
|
+
*
|
|
40
|
+
* After the model streamed and tools executed, the story can decide whether the loop should
|
|
41
|
+
* continue.
|
|
42
|
+
*
|
|
43
|
+
* Default: `true` (continue).
|
|
44
|
+
*/
|
|
45
|
+
async shouldContinue(_args) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
async react(incomingEventOrParams, paramsMaybe) {
|
|
49
|
+
return await Story.runWorkflow(this, incomingEventOrParams, paramsMaybe);
|
|
50
|
+
}
|
|
51
|
+
static async runWorkflow(story, incomingEventOrParams, paramsMaybe) {
|
|
52
|
+
"use workflow";
|
|
53
|
+
const params = typeof incomingEventOrParams?.type === "string" && paramsMaybe
|
|
54
|
+
? {
|
|
55
|
+
env: paramsMaybe.env,
|
|
56
|
+
triggerEvent: incomingEventOrParams,
|
|
57
|
+
contextIdentifier: paramsMaybe.context ?? null,
|
|
58
|
+
options: paramsMaybe.options,
|
|
59
|
+
}
|
|
60
|
+
: incomingEventOrParams;
|
|
61
|
+
const triggerEvent = params.triggerEvent ?? params.incomingEvent;
|
|
62
|
+
if (!triggerEvent) {
|
|
63
|
+
throw new Error("Story.react: triggerEvent is required");
|
|
64
|
+
}
|
|
65
|
+
// Register env for step runtimes (workflow-friendly).
|
|
66
|
+
try {
|
|
67
|
+
const { getWorkflowMetadata } = await import("workflow");
|
|
68
|
+
const meta = getWorkflowMetadata?.();
|
|
69
|
+
const runId = meta?.workflowRunId ? String(meta.workflowRunId) : null;
|
|
70
|
+
registerStoryEnv(params.env, runId ?? undefined);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
registerStoryEnv(params.env);
|
|
74
|
+
}
|
|
75
|
+
const maxIterations = params.options?.maxIterations ?? 20;
|
|
76
|
+
const maxModelSteps = params.options?.maxModelSteps ?? 1;
|
|
77
|
+
const preventClose = params.options?.preventClose ?? false;
|
|
78
|
+
const sendFinish = params.options?.sendFinish ?? true;
|
|
79
|
+
const silent = params.options?.silent ?? false;
|
|
80
|
+
let writable = params.options?.writable;
|
|
81
|
+
// 1) Ensure context exists (step)
|
|
82
|
+
const ctxResult = await initializeContext(params.env, params.contextIdentifier, { silent, writable });
|
|
83
|
+
const currentContext = ctxResult.context;
|
|
84
|
+
// If the caller didn't provide a writable, we still stream by default (unless silent),
|
|
85
|
+
// using a namespaced stream per context: `context:<contextId>`.
|
|
86
|
+
if (!silent && !writable) {
|
|
87
|
+
writable = getWritable({
|
|
88
|
+
namespace: `context:${String(currentContext.id)}`,
|
|
89
|
+
});
|
|
90
|
+
// If the context was created in `initializeContext` (which didn't have a writable yet),
|
|
91
|
+
// re-emit the context id chunk now so clients can subscribe to the right persisted thread.
|
|
92
|
+
if (ctxResult.isNew) {
|
|
93
|
+
await emitContextIdChunk({
|
|
94
|
+
env: params.env,
|
|
95
|
+
contextId: String(currentContext.id),
|
|
96
|
+
writable,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const contextSelector = params.contextIdentifier?.id
|
|
101
|
+
? { id: String(params.contextIdentifier.id) }
|
|
102
|
+
: params.contextIdentifier?.key
|
|
103
|
+
? { key: params.contextIdentifier.key }
|
|
104
|
+
: { id: String(currentContext.id) };
|
|
105
|
+
if (ctxResult.isNew) {
|
|
106
|
+
await story.opts.onContextCreated?.({ env: params.env, context: currentContext });
|
|
107
|
+
}
|
|
108
|
+
// 2) Persist trigger event + create execution shell (single step)
|
|
109
|
+
const { triggerEventId, reactionEventId, executionId } = await saveTriggerAndCreateExecution({
|
|
110
|
+
env: params.env,
|
|
111
|
+
contextIdentifier: contextSelector,
|
|
112
|
+
triggerEvent,
|
|
113
|
+
});
|
|
114
|
+
// Emit a simple ping chunk early so clients can validate that streaming works end-to-end.
|
|
115
|
+
// This should be ignored safely by clients that don't care about it.
|
|
116
|
+
if (!silent) {
|
|
117
|
+
await writeStoryPing({ label: "story-start", writable });
|
|
118
|
+
}
|
|
119
|
+
let reactionEvent = null;
|
|
120
|
+
// Latest persisted context state for this run (we keep it in memory; store is updated via steps).
|
|
121
|
+
let updatedContext = currentContext;
|
|
122
|
+
let currentStepId = null;
|
|
123
|
+
const failExecution = async () => {
|
|
124
|
+
try {
|
|
125
|
+
await completeExecution(params.env, contextSelector, executionId, "failed");
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// noop
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
if (!silent) {
|
|
132
|
+
await closeStoryStream({ preventClose, sendFinish, writable });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// noop
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
try {
|
|
140
|
+
for (let iter = 0; iter < maxIterations; iter++) {
|
|
141
|
+
// Create a persisted step per iteration (IDs generated in step runtime for replay safety)
|
|
142
|
+
const stepCreate = await createStoryStep({
|
|
143
|
+
env: params.env,
|
|
144
|
+
executionId,
|
|
145
|
+
iteration: iter,
|
|
146
|
+
});
|
|
147
|
+
currentStepId = stepCreate.stepId;
|
|
148
|
+
// Hook: Story DSL `context()` (implemented by subclasses via `initialize()`)
|
|
149
|
+
const nextContent = await story.initialize(updatedContext, params.env);
|
|
150
|
+
updatedContext = await updateContextContent(params.env, contextSelector, nextContent);
|
|
151
|
+
await story.opts.onContextUpdated?.({ env: params.env, context: updatedContext });
|
|
152
|
+
// Hook: Story DSL `narrative()` (implemented by subclasses via `buildSystemPrompt()`)
|
|
153
|
+
const systemPrompt = await story.buildSystemPrompt(updatedContext, params.env);
|
|
154
|
+
// Hook: Story DSL `actions()` (implemented by subclasses via `buildTools()`)
|
|
155
|
+
const toolsAll = await story.buildTools(updatedContext, params.env);
|
|
156
|
+
// IMPORTANT: step args must be serializable.
|
|
157
|
+
// Match DurableAgent behavior: convert tool input schemas to plain JSON Schema in workflow context.
|
|
158
|
+
const toolsForModel = toolsToModelTools(toolsAll);
|
|
159
|
+
// Execute model reaction for this iteration using the stable reaction event id.
|
|
160
|
+
//
|
|
161
|
+
// IMPORTANT:
|
|
162
|
+
// We expose a single visible `context_event` per story turn (`reactionEventId`).
|
|
163
|
+
// If we stream with a per-step id, the UI will render an optimistic assistant message
|
|
164
|
+
// (step id) and then a second persisted assistant message (reaction id) with the same
|
|
165
|
+
// content once InstantDB updates.
|
|
166
|
+
const { assistantEvent, toolCalls, messagesForModel } = await executeReaction({
|
|
167
|
+
env: params.env,
|
|
168
|
+
contextIdentifier: contextSelector,
|
|
169
|
+
model: story.getModel(updatedContext, params.env),
|
|
170
|
+
system: systemPrompt,
|
|
171
|
+
tools: toolsForModel,
|
|
172
|
+
eventId: reactionEventId,
|
|
173
|
+
iteration: iter,
|
|
174
|
+
maxSteps: maxModelSteps,
|
|
175
|
+
// Only emit a `start` chunk once per story turn.
|
|
176
|
+
sendStart: !silent && iter === 0 && reactionEvent === null,
|
|
177
|
+
silent,
|
|
178
|
+
writable,
|
|
179
|
+
executionId,
|
|
180
|
+
contextId: String(currentContext.id),
|
|
181
|
+
stepId: String(stepCreate.stepId),
|
|
182
|
+
});
|
|
183
|
+
const reviewRequests = toolCalls.length > 0
|
|
184
|
+
? toolCalls.flatMap((tc) => {
|
|
185
|
+
const toolDef = toolsAll[tc.toolName];
|
|
186
|
+
const auto = toolDef?.auto !== false;
|
|
187
|
+
tc.auto = auto;
|
|
188
|
+
if (auto)
|
|
189
|
+
return [];
|
|
190
|
+
return [
|
|
191
|
+
{
|
|
192
|
+
toolCallId: String(tc.toolCallId),
|
|
193
|
+
toolName: String(tc.toolName ?? ""),
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
})
|
|
197
|
+
: [];
|
|
198
|
+
// Persist normalized parts hanging off the producing step (story_parts).
|
|
199
|
+
// IMPORTANT:
|
|
200
|
+
// We intentionally do NOT persist the per-step LLM assistant event as a `context_event`.
|
|
201
|
+
// The story exposes a single visible `context_event` per turn (`reactionEventId`) so the UI
|
|
202
|
+
// doesn't render duplicate assistant messages (LLM-step + aggregated reaction).
|
|
203
|
+
const stepParts = (assistantEvent?.content?.parts ?? []);
|
|
204
|
+
await saveStoryPartsStep({
|
|
205
|
+
env: params.env,
|
|
206
|
+
stepId: stepCreate.stepId,
|
|
207
|
+
parts: stepParts,
|
|
208
|
+
executionId,
|
|
209
|
+
contextId: String(currentContext.id),
|
|
210
|
+
iteration: iter,
|
|
211
|
+
});
|
|
212
|
+
// Persist/append the aggregated reaction event (stable `reactionEventId` for the execution).
|
|
213
|
+
if (!reactionEvent) {
|
|
214
|
+
const reactionPayload = {
|
|
215
|
+
...assistantEvent,
|
|
216
|
+
status: "pending",
|
|
217
|
+
};
|
|
218
|
+
reactionEvent = await saveReactionEvent(params.env, contextSelector, reactionPayload, {
|
|
219
|
+
executionId,
|
|
220
|
+
contextId: String(currentContext.id),
|
|
221
|
+
reviewRequests,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
reactionEvent = await updateEvent(params.env, reactionEvent.id, {
|
|
226
|
+
...reactionEvent,
|
|
227
|
+
content: {
|
|
228
|
+
parts: [
|
|
229
|
+
...(reactionEvent?.content?.parts ?? []),
|
|
230
|
+
...(assistantEvent?.content?.parts ?? []),
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
status: "pending",
|
|
234
|
+
}, { executionId, contextId: String(currentContext.id) });
|
|
235
|
+
}
|
|
236
|
+
story.opts.onEventCreated?.(assistantEvent);
|
|
237
|
+
// Done: no tool calls requested by the model
|
|
238
|
+
if (!toolCalls.length) {
|
|
239
|
+
const endResult = await story.callOnEnd(assistantEvent);
|
|
240
|
+
if (endResult) {
|
|
241
|
+
// Mark iteration step completed (no tools)
|
|
242
|
+
await updateStoryStep({
|
|
243
|
+
env: params.env,
|
|
244
|
+
stepId: stepCreate.stepId,
|
|
245
|
+
patch: {
|
|
246
|
+
status: "completed",
|
|
247
|
+
toolCalls: [],
|
|
248
|
+
toolExecutionResults: [],
|
|
249
|
+
continueLoop: false,
|
|
250
|
+
},
|
|
251
|
+
executionId,
|
|
252
|
+
contextId: String(currentContext.id),
|
|
253
|
+
iteration: iter,
|
|
254
|
+
});
|
|
255
|
+
// Mark reaction event completed
|
|
256
|
+
await updateEvent(params.env, reactionEventId, {
|
|
257
|
+
...reactionEvent,
|
|
258
|
+
status: "completed",
|
|
259
|
+
}, { executionId, contextId: String(currentContext.id) });
|
|
260
|
+
await completeExecution(params.env, contextSelector, executionId, "completed");
|
|
261
|
+
if (!silent) {
|
|
262
|
+
await closeStoryStream({ preventClose, sendFinish, writable });
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
contextId: currentContext.id,
|
|
266
|
+
context: updatedContext,
|
|
267
|
+
triggerEventId,
|
|
268
|
+
reactionEventId,
|
|
269
|
+
executionId,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Execute tool calls (workflow context; tool implementations decide step vs workflow)
|
|
274
|
+
if (!silent && toolCalls.length) {
|
|
275
|
+
await writeContextSubstate({ key: "actions", transient: true, writable });
|
|
276
|
+
}
|
|
277
|
+
const executionResults = await Promise.all(toolCalls.map(async (tc) => {
|
|
278
|
+
const toolDef = toolsAll[tc.toolName];
|
|
279
|
+
if (!toolDef || typeof toolDef.execute !== "function") {
|
|
280
|
+
return {
|
|
281
|
+
tc,
|
|
282
|
+
success: false,
|
|
283
|
+
output: null,
|
|
284
|
+
errorText: `Tool "${tc.toolName}" not found or has no execute().`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
let toolArgs = tc.args;
|
|
289
|
+
if (toolDef?.auto === false) {
|
|
290
|
+
const { createHook, createWebhook } = await import("workflow");
|
|
291
|
+
const toolCallId = String(tc.toolCallId);
|
|
292
|
+
const hookToken = toolApprovalHookToken({ executionId, toolCallId });
|
|
293
|
+
const webhookToken = toolApprovalWebhookToken({ executionId, toolCallId });
|
|
294
|
+
const hook = createHook({ token: hookToken });
|
|
295
|
+
const webhook = createWebhook({ token: webhookToken });
|
|
296
|
+
const approvalOrRequest = await Promise.race([
|
|
297
|
+
hook.then((approval) => ({ source: "hook", approval })),
|
|
298
|
+
webhook.then((request) => ({ source: "webhook", request })),
|
|
299
|
+
]);
|
|
300
|
+
const approval = approvalOrRequest.source === "hook"
|
|
301
|
+
? approvalOrRequest.approval
|
|
302
|
+
: await approvalOrRequest.request.json().catch(() => null);
|
|
303
|
+
if (!approval || approval.approved !== true) {
|
|
304
|
+
return {
|
|
305
|
+
tc,
|
|
306
|
+
success: false,
|
|
307
|
+
output: null,
|
|
308
|
+
errorText: approval && "comment" in approval && approval.comment
|
|
309
|
+
? `Tool execution not approved: ${approval.comment}`
|
|
310
|
+
: "Tool execution not approved",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if ("args" in approval && approval.args !== undefined) {
|
|
314
|
+
toolArgs = approval.args;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const output = await toolDef.execute(toolArgs, {
|
|
318
|
+
toolCallId: tc.toolCallId,
|
|
319
|
+
messages: messagesForModel,
|
|
320
|
+
eventId: reactionEventId,
|
|
321
|
+
executionId,
|
|
322
|
+
triggerEventId,
|
|
323
|
+
contextId: currentContext.id,
|
|
324
|
+
});
|
|
325
|
+
return { tc, success: true, output };
|
|
326
|
+
}
|
|
327
|
+
catch (e) {
|
|
328
|
+
return {
|
|
329
|
+
tc,
|
|
330
|
+
success: false,
|
|
331
|
+
output: null,
|
|
332
|
+
errorText: e instanceof Error ? e.message : String(e),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}));
|
|
336
|
+
// Emit tool outputs to the workflow stream (step)
|
|
337
|
+
if (!silent) {
|
|
338
|
+
await writeToolOutputs({
|
|
339
|
+
results: executionResults.map((r) => r.success
|
|
340
|
+
? { toolCallId: r.tc.toolCallId, success: true, output: r.output }
|
|
341
|
+
: {
|
|
342
|
+
toolCallId: r.tc.toolCallId,
|
|
343
|
+
success: false,
|
|
344
|
+
errorText: r.errorText,
|
|
345
|
+
}),
|
|
346
|
+
writable,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
// Clear action status once tool execution results have been emitted.
|
|
350
|
+
if (!silent && toolCalls.length) {
|
|
351
|
+
await writeContextSubstate({ key: null, transient: true, writable });
|
|
352
|
+
}
|
|
353
|
+
// Merge tool results into persisted parts (so next LLM call can see them)
|
|
354
|
+
if (reactionEvent) {
|
|
355
|
+
let parts = reactionEvent?.content?.parts ?? [];
|
|
356
|
+
for (const r of executionResults) {
|
|
357
|
+
parts = applyToolExecutionResultToParts(parts, r.tc, {
|
|
358
|
+
success: Boolean(r.success),
|
|
359
|
+
result: r.output,
|
|
360
|
+
message: r.errorText,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
reactionEvent = await updateEvent(params.env, reactionEventId, {
|
|
364
|
+
...reactionEvent,
|
|
365
|
+
content: { parts },
|
|
366
|
+
status: "pending",
|
|
367
|
+
}, { executionId, contextId: String(currentContext.id) });
|
|
368
|
+
}
|
|
369
|
+
// Callback for observability/integration
|
|
370
|
+
for (const r of executionResults) {
|
|
371
|
+
await story.opts.onToolCallExecuted?.({
|
|
372
|
+
toolCall: r.tc,
|
|
373
|
+
success: r.success,
|
|
374
|
+
output: r.output,
|
|
375
|
+
errorText: r.errorText,
|
|
376
|
+
eventId: reactionEventId,
|
|
377
|
+
executionId,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
// Stop/continue boundary: allow the Story to decide if the loop should continue.
|
|
381
|
+
// IMPORTANT: we call this after tool results have been merged into the persisted `reactionEvent`,
|
|
382
|
+
// so stories can inspect `reactionEvent.content.parts` deterministically.
|
|
383
|
+
const continueLoop = await story.shouldContinue({
|
|
384
|
+
env: params.env,
|
|
385
|
+
context: updatedContext,
|
|
386
|
+
reactionEvent: reactionEvent ?? assistantEvent,
|
|
387
|
+
assistantEvent,
|
|
388
|
+
toolCalls,
|
|
389
|
+
toolExecutionResults: executionResults,
|
|
390
|
+
});
|
|
391
|
+
// Persist per-iteration step outcome (tools + continue signal)
|
|
392
|
+
await updateStoryStep({
|
|
393
|
+
env: params.env,
|
|
394
|
+
stepId: stepCreate.stepId,
|
|
395
|
+
patch: {
|
|
396
|
+
status: "completed",
|
|
397
|
+
toolCalls,
|
|
398
|
+
toolExecutionResults: executionResults,
|
|
399
|
+
continueLoop: continueLoop !== false,
|
|
400
|
+
},
|
|
401
|
+
executionId,
|
|
402
|
+
contextId: String(currentContext.id),
|
|
403
|
+
iteration: iter,
|
|
404
|
+
});
|
|
405
|
+
if (continueLoop === false) {
|
|
406
|
+
await updateEvent(params.env, reactionEventId, {
|
|
407
|
+
...reactionEvent,
|
|
408
|
+
status: "completed",
|
|
409
|
+
}, { executionId, contextId: String(currentContext.id) });
|
|
410
|
+
await completeExecution(params.env, contextSelector, executionId, "completed");
|
|
411
|
+
if (!silent) {
|
|
412
|
+
await closeStoryStream({ preventClose, sendFinish, writable });
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
contextId: currentContext.id,
|
|
416
|
+
context: updatedContext,
|
|
417
|
+
triggerEventId,
|
|
418
|
+
reactionEventId,
|
|
419
|
+
executionId,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
throw new Error(`Story: maxIterations reached (${maxIterations}) without completion`);
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
// Best-effort: persist failure on the current iteration step (if any)
|
|
427
|
+
if (currentStepId) {
|
|
428
|
+
try {
|
|
429
|
+
await updateStoryStep({
|
|
430
|
+
env: params.env,
|
|
431
|
+
stepId: currentStepId,
|
|
432
|
+
patch: {
|
|
433
|
+
status: "failed",
|
|
434
|
+
errorText: error instanceof Error ? error.message : String(error),
|
|
435
|
+
},
|
|
436
|
+
executionId,
|
|
437
|
+
contextId: String(currentContext.id),
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// noop
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
await failExecution();
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* @deprecated Use `react()` instead. Kept for backwards compatibility.
|
|
450
|
+
*/
|
|
451
|
+
async stream(triggerEvent, params) {
|
|
452
|
+
return await this.react(triggerEvent, params);
|
|
453
|
+
}
|
|
454
|
+
async callOnEnd(lastEvent) {
|
|
455
|
+
if (!this.opts.onEnd)
|
|
456
|
+
return true;
|
|
457
|
+
const result = await this.opts.onEnd(lastEvent);
|
|
458
|
+
if (typeof result === "boolean")
|
|
459
|
+
return result;
|
|
460
|
+
if (result && typeof result === "object" && "end" in result)
|
|
461
|
+
return Boolean(result.end);
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
}
|