@ekairos/events 1.22.4-beta.development.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 +115 -0
- package/dist/codex.d.ts +95 -0
- package/dist/codex.js +91 -0
- package/dist/context.builder.d.ts +62 -0
- package/dist/context.builder.js +143 -0
- package/dist/context.config.d.ts +9 -0
- package/dist/context.config.js +30 -0
- package/dist/context.contract.d.ts +47 -0
- package/dist/context.contract.js +132 -0
- package/dist/context.d.ts +4 -0
- package/dist/context.durable.d.ts +5 -0
- package/dist/context.durable.js +13 -0
- package/dist/context.engine.d.ts +216 -0
- package/dist/context.engine.js +1098 -0
- package/dist/context.events.d.ts +55 -0
- package/dist/context.events.js +431 -0
- package/dist/context.hooks.d.ts +21 -0
- package/dist/context.hooks.js +31 -0
- package/dist/context.js +3 -0
- package/dist/context.parts.d.ts +241 -0
- package/dist/context.parts.js +360 -0
- package/dist/context.reactor.d.ts +3 -0
- package/dist/context.reactor.js +2 -0
- package/dist/context.registry.d.ts +13 -0
- package/dist/context.registry.js +30 -0
- package/dist/context.skill.d.ts +9 -0
- package/dist/context.skill.js +1 -0
- package/dist/context.step-stream.d.ts +26 -0
- package/dist/context.step-stream.js +59 -0
- package/dist/context.store.d.ts +85 -0
- package/dist/context.store.js +1 -0
- package/dist/context.stream.d.ts +148 -0
- package/dist/context.stream.js +141 -0
- package/dist/context.toolcalls.d.ts +60 -0
- package/dist/context.toolcalls.js +117 -0
- package/dist/env.d.ts +3 -0
- package/dist/env.js +53 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +11 -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 +42 -0
- package/dist/react.js +88 -0
- package/dist/reactors/ai-sdk.chunk-map.d.ts +12 -0
- package/dist/reactors/ai-sdk.chunk-map.js +143 -0
- package/dist/reactors/ai-sdk.reactor.d.ts +33 -0
- package/dist/reactors/ai-sdk.reactor.js +65 -0
- package/dist/reactors/ai-sdk.step.d.ts +48 -0
- package/dist/reactors/ai-sdk.step.js +343 -0
- package/dist/reactors/scripted.reactor.d.ts +17 -0
- package/dist/reactors/scripted.reactor.js +51 -0
- package/dist/reactors/types.d.ts +52 -0
- package/dist/reactors/types.js +1 -0
- package/dist/runtime.d.ts +19 -0
- package/dist/runtime.js +26 -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 +191 -0
- package/dist/steps/do-context-stream-step.d.ts +34 -0
- package/dist/steps/do-context-stream-step.js +96 -0
- package/dist/steps/mirror.steps.d.ts +6 -0
- package/dist/steps/mirror.steps.js +48 -0
- package/dist/steps/store.steps.d.ts +96 -0
- package/dist/steps/store.steps.js +595 -0
- package/dist/steps/stream.steps.d.ts +86 -0
- package/dist/steps/stream.steps.js +270 -0
- package/dist/steps/trace.steps.d.ts +38 -0
- package/dist/steps/trace.steps.js +270 -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 +66 -0
- package/dist/stores/instant.store.js +575 -0
- package/dist/tools-to-model-tools.d.ts +19 -0
- package/dist/tools-to-model-tools.js +21 -0
- package/package.json +142 -0
|
@@ -0,0 +1,1098 @@
|
|
|
1
|
+
import { registerContextEnv } from "./env.js";
|
|
2
|
+
import { OUTPUT_ITEM_TYPE, WEB_CHANNEL } from "./context.events.js";
|
|
3
|
+
import { applyToolExecutionResultToParts } from "./context.toolcalls.js";
|
|
4
|
+
import { isContextPartEnvelope, normalizePartsForPersistence, } from "./context.parts.js";
|
|
5
|
+
import { toolsToModelTools } from "./tools-to-model-tools.js";
|
|
6
|
+
import { createAiSdkReactor, } from "./context.reactor.js";
|
|
7
|
+
import { abortPersistedContextStepStream, closePersistedContextStepStream, createPersistedContextStepStream, closeContextStream, } from "./steps/stream.steps.js";
|
|
8
|
+
import { completeExecution, createContextStep, initializeContext, saveTriggerAndCreateExecution, saveContextPartsStep, updateContextContent, updateContextStatus, updateItem, updateContextStep, } from "./steps/store.steps.js";
|
|
9
|
+
import { getClientResumeHookUrl, toolApprovalHookToken, toolApprovalWebhookToken, } from "./context.hooks.js";
|
|
10
|
+
import { getContextDurableWorkflow } from "./context.durable.js";
|
|
11
|
+
export async function runContextReactionDirect(context, triggerEvent, params) {
|
|
12
|
+
return await ContextEngine.runDirect(context, triggerEvent, params);
|
|
13
|
+
}
|
|
14
|
+
export { toolApprovalHookToken, toolApprovalWebhookToken, getClientResumeHookUrl };
|
|
15
|
+
function nowIso() {
|
|
16
|
+
return new Date().toISOString();
|
|
17
|
+
}
|
|
18
|
+
function clipPreview(value, max = 240) {
|
|
19
|
+
if (value.length <= max)
|
|
20
|
+
return value;
|
|
21
|
+
return `${value.slice(0, max)}...`;
|
|
22
|
+
}
|
|
23
|
+
function summarizePartPreview(part) {
|
|
24
|
+
if (!part || typeof part !== "object")
|
|
25
|
+
return {};
|
|
26
|
+
if (isContextPartEnvelope(part)) {
|
|
27
|
+
const preview = part.content[0]?.type === "text"
|
|
28
|
+
? part.content[0].text
|
|
29
|
+
: JSON.stringify(part.content[0] ?? part);
|
|
30
|
+
const state = "state" in part && typeof part.state === "string" ? part.state : undefined;
|
|
31
|
+
const toolCallId = "toolCallId" in part && typeof part.toolCallId === "string"
|
|
32
|
+
? part.toolCallId
|
|
33
|
+
: undefined;
|
|
34
|
+
return {
|
|
35
|
+
partPreview: preview ? clipPreview(preview) : undefined,
|
|
36
|
+
partState: state,
|
|
37
|
+
partToolCallId: toolCallId,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const row = part;
|
|
41
|
+
const partType = typeof row.type === "string" ? row.type : "";
|
|
42
|
+
const partState = typeof row.state === "string" ? row.state : undefined;
|
|
43
|
+
const partToolCallId = typeof row.toolCallId === "string"
|
|
44
|
+
? row.toolCallId
|
|
45
|
+
: typeof row.id === "string"
|
|
46
|
+
? row.id
|
|
47
|
+
: undefined;
|
|
48
|
+
if (typeof row.text === "string" && row.text.trim().length > 0) {
|
|
49
|
+
return {
|
|
50
|
+
partPreview: clipPreview(row.text),
|
|
51
|
+
partState,
|
|
52
|
+
partToolCallId,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (partType.startsWith("tool-")) {
|
|
56
|
+
const payload = {
|
|
57
|
+
tool: partType,
|
|
58
|
+
state: partState,
|
|
59
|
+
input: row.input,
|
|
60
|
+
output: row.output,
|
|
61
|
+
errorText: row.errorText,
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
partPreview: clipPreview(JSON.stringify(payload)),
|
|
65
|
+
partState,
|
|
66
|
+
partToolCallId,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
partState,
|
|
71
|
+
partToolCallId,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async function emitContextEvents(params) {
|
|
75
|
+
void params;
|
|
76
|
+
}
|
|
77
|
+
async function measureBenchmark(benchmark, name, run) {
|
|
78
|
+
if (!benchmark)
|
|
79
|
+
return await run();
|
|
80
|
+
return await benchmark.measure(name, run);
|
|
81
|
+
}
|
|
82
|
+
async function readActiveWorkflowRunId() {
|
|
83
|
+
try {
|
|
84
|
+
const { getWorkflowMetadata } = await import("workflow");
|
|
85
|
+
const runId = getWorkflowMetadata?.()?.workflowRunId;
|
|
86
|
+
return runId ? String(runId) : null;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function createRuntimeOps(env, benchmark) {
|
|
93
|
+
const { getContextRuntime } = await import("./runtime.js");
|
|
94
|
+
const runtime = await getContextRuntime(env);
|
|
95
|
+
const { db } = runtime;
|
|
96
|
+
const { InstantStore } = await import("./stores/instant.store.js");
|
|
97
|
+
const requireContextId = (contextIdentifier) => {
|
|
98
|
+
if ("id" in contextIdentifier && typeof contextIdentifier.id === "string" && contextIdentifier.id) {
|
|
99
|
+
return String(contextIdentifier.id);
|
|
100
|
+
}
|
|
101
|
+
throw new Error("ContextEngine direct runtime requires resolved context ids.");
|
|
102
|
+
};
|
|
103
|
+
const makeRuntimeId = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
104
|
+
const instrumentAsync = async (kind, run) => {
|
|
105
|
+
const startedAt = Date.now();
|
|
106
|
+
try {
|
|
107
|
+
return await run();
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
const elapsedMs = Date.now() - startedAt;
|
|
111
|
+
benchmark?.add?.("react.network.totalMs", elapsedMs);
|
|
112
|
+
benchmark?.add?.(`react.network.${kind}Ms`, elapsedMs);
|
|
113
|
+
benchmark?.add?.(`react.network.${kind}Count`, 1);
|
|
114
|
+
const currentStage = benchmark?.getCurrentStage?.();
|
|
115
|
+
if (currentStage) {
|
|
116
|
+
benchmark?.add?.(`${currentStage}.networkMs`, elapsedMs);
|
|
117
|
+
benchmark?.add?.(`${currentStage}.${kind}Count`, 1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const instrumentedDb = new Proxy(db, {
|
|
122
|
+
get(target, prop, receiver) {
|
|
123
|
+
if (prop === "query") {
|
|
124
|
+
return async (...args) => await instrumentAsync("query", async () => await target.query(...args));
|
|
125
|
+
}
|
|
126
|
+
if (prop === "transact") {
|
|
127
|
+
return async (...args) => await instrumentAsync("transact", async () => await target.transact(...args));
|
|
128
|
+
}
|
|
129
|
+
return Reflect.get(target, prop, receiver);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
const store = new InstantStore(instrumentedDb);
|
|
133
|
+
return {
|
|
134
|
+
db: instrumentedDb,
|
|
135
|
+
initializeContext: async (contextIdentifier) => {
|
|
136
|
+
if (!contextIdentifier) {
|
|
137
|
+
const context = await store.getOrCreateContext(null);
|
|
138
|
+
return { context, isNew: true };
|
|
139
|
+
}
|
|
140
|
+
const existing = await store.getContext(contextIdentifier);
|
|
141
|
+
if (existing) {
|
|
142
|
+
return { context: existing, isNew: false };
|
|
143
|
+
}
|
|
144
|
+
const context = await store.getOrCreateContext(contextIdentifier);
|
|
145
|
+
return { context, isNew: true };
|
|
146
|
+
},
|
|
147
|
+
updateContextContent: async (contextIdentifier, content) => await store.updateContextContent(contextIdentifier, content),
|
|
148
|
+
updateContextStatus: async (contextIdentifier, status) => await instrumentedDb.transact([
|
|
149
|
+
instrumentedDb.tx.event_contexts[requireContextId(contextIdentifier)].update({
|
|
150
|
+
status,
|
|
151
|
+
updatedAt: new Date(),
|
|
152
|
+
}),
|
|
153
|
+
]),
|
|
154
|
+
saveTriggerAndCreateExecution: async ({ contextIdentifier, triggerEvent }) => {
|
|
155
|
+
const contextId = requireContextId(contextIdentifier);
|
|
156
|
+
const triggerId = String(triggerEvent.id);
|
|
157
|
+
const reactionId = makeRuntimeId();
|
|
158
|
+
const executionId = makeRuntimeId();
|
|
159
|
+
const reactionEvent = {
|
|
160
|
+
id: reactionId,
|
|
161
|
+
type: OUTPUT_ITEM_TYPE,
|
|
162
|
+
channel: typeof triggerEvent.channel === "string"
|
|
163
|
+
? triggerEvent.channel
|
|
164
|
+
: WEB_CHANNEL,
|
|
165
|
+
createdAt: new Date().toISOString(),
|
|
166
|
+
status: "pending",
|
|
167
|
+
content: { parts: [] },
|
|
168
|
+
};
|
|
169
|
+
const now = new Date();
|
|
170
|
+
await instrumentedDb.transact([
|
|
171
|
+
instrumentedDb.tx.event_items[triggerId].update({
|
|
172
|
+
...triggerEvent,
|
|
173
|
+
id: triggerId,
|
|
174
|
+
status: "stored",
|
|
175
|
+
}),
|
|
176
|
+
instrumentedDb.tx.event_items[triggerId].link({ context: contextId }),
|
|
177
|
+
instrumentedDb.tx.event_items[reactionId].update({
|
|
178
|
+
...reactionEvent,
|
|
179
|
+
id: reactionId,
|
|
180
|
+
status: "pending",
|
|
181
|
+
}),
|
|
182
|
+
instrumentedDb.tx.event_items[reactionId].link({ context: contextId }),
|
|
183
|
+
instrumentedDb.tx.event_executions[executionId].create({
|
|
184
|
+
createdAt: now,
|
|
185
|
+
updatedAt: now,
|
|
186
|
+
status: "executing",
|
|
187
|
+
}),
|
|
188
|
+
instrumentedDb.tx.event_executions[executionId].link({ context: contextId }),
|
|
189
|
+
instrumentedDb.tx.event_executions[executionId].link({ trigger: triggerId }),
|
|
190
|
+
instrumentedDb.tx.event_executions[executionId].link({ reaction: reactionId }),
|
|
191
|
+
instrumentedDb.tx.event_items[triggerId].link({ execution: executionId }),
|
|
192
|
+
instrumentedDb.tx.event_items[reactionId].link({ execution: executionId }),
|
|
193
|
+
instrumentedDb.tx.event_contexts[contextId].update({
|
|
194
|
+
status: "open_streaming",
|
|
195
|
+
updatedAt: now,
|
|
196
|
+
}),
|
|
197
|
+
instrumentedDb.tx.event_contexts[contextId].link({ currentExecution: executionId }),
|
|
198
|
+
]);
|
|
199
|
+
return {
|
|
200
|
+
triggerEvent: {
|
|
201
|
+
...triggerEvent,
|
|
202
|
+
id: triggerId,
|
|
203
|
+
status: "stored",
|
|
204
|
+
},
|
|
205
|
+
reactionEvent,
|
|
206
|
+
execution: {
|
|
207
|
+
id: executionId,
|
|
208
|
+
status: "executing",
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
createContextStep: async ({ executionId, iteration }) => {
|
|
213
|
+
const stepId = makeRuntimeId();
|
|
214
|
+
await instrumentedDb.transact([
|
|
215
|
+
instrumentedDb.tx.event_steps[stepId].create({
|
|
216
|
+
createdAt: new Date(),
|
|
217
|
+
updatedAt: new Date(),
|
|
218
|
+
status: "running",
|
|
219
|
+
iteration,
|
|
220
|
+
}),
|
|
221
|
+
instrumentedDb.tx.event_steps[stepId].link({ execution: executionId }),
|
|
222
|
+
]);
|
|
223
|
+
return { stepId };
|
|
224
|
+
},
|
|
225
|
+
updateContextStep: async (params) => {
|
|
226
|
+
await instrumentedDb.transact([
|
|
227
|
+
instrumentedDb.tx.event_steps[params.stepId].update({
|
|
228
|
+
...params.patch,
|
|
229
|
+
updatedAt: new Date(),
|
|
230
|
+
}),
|
|
231
|
+
]);
|
|
232
|
+
},
|
|
233
|
+
saveContextPartsStep: async (params) => {
|
|
234
|
+
await store.saveStepParts({ stepId: params.stepId, parts: params.parts });
|
|
235
|
+
},
|
|
236
|
+
updateItem: async (itemId, item) => {
|
|
237
|
+
await instrumentedDb.transact([instrumentedDb.tx.event_items[itemId].update(item)]);
|
|
238
|
+
return {
|
|
239
|
+
...item,
|
|
240
|
+
id: itemId,
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
completeExecution: async (contextIdentifier, executionId, status) => {
|
|
244
|
+
const contextId = requireContextId(contextIdentifier);
|
|
245
|
+
await instrumentedDb.transact([
|
|
246
|
+
instrumentedDb.tx.event_executions[executionId].update({
|
|
247
|
+
status,
|
|
248
|
+
updatedAt: new Date(),
|
|
249
|
+
}),
|
|
250
|
+
instrumentedDb.tx.event_contexts[contextId].update({
|
|
251
|
+
status: "closed",
|
|
252
|
+
updatedAt: new Date(),
|
|
253
|
+
}),
|
|
254
|
+
]);
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
async function createWorkflowOps(env) {
|
|
259
|
+
return {
|
|
260
|
+
initializeContext: async (contextIdentifier, opts) => await initializeContext(env, contextIdentifier, opts),
|
|
261
|
+
updateContextContent: async (contextIdentifier, content) => await updateContextContent(env, contextIdentifier, content),
|
|
262
|
+
updateContextStatus: async (contextIdentifier, status) => await updateContextStatus(env, contextIdentifier, status),
|
|
263
|
+
saveTriggerAndCreateExecution: async ({ contextIdentifier, triggerEvent }) => await saveTriggerAndCreateExecution({ env, contextIdentifier, triggerEvent }),
|
|
264
|
+
createContextStep: async ({ executionId, iteration }) => await createContextStep({ env, executionId, iteration }),
|
|
265
|
+
updateContextStep: async (params) => await updateContextStep({ env, ...params }),
|
|
266
|
+
saveContextPartsStep: async (params) => await saveContextPartsStep({ env, ...params }),
|
|
267
|
+
updateItem: async (itemId, item, opts) => await updateItem(env, itemId, item, opts),
|
|
268
|
+
completeExecution: async (contextIdentifier, executionId, status) => await completeExecution(env, contextIdentifier, executionId, status),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
async function getContextEngineOps(env, benchmark) {
|
|
272
|
+
const workflowRunId = await readActiveWorkflowRunId();
|
|
273
|
+
if (workflowRunId) {
|
|
274
|
+
registerContextEnv(env, workflowRunId);
|
|
275
|
+
return await createWorkflowOps(env);
|
|
276
|
+
}
|
|
277
|
+
registerContextEnv(env);
|
|
278
|
+
return await createRuntimeOps(env, benchmark);
|
|
279
|
+
}
|
|
280
|
+
export class ContextEngine {
|
|
281
|
+
constructor(opts = {}, reactor) {
|
|
282
|
+
this.opts = opts;
|
|
283
|
+
this.reactor = reactor ?? createAiSdkReactor();
|
|
284
|
+
}
|
|
285
|
+
async buildSkills(_context, _env) {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* First-class event expansion stage (runs on every iteration of the durable loop).
|
|
290
|
+
*
|
|
291
|
+
* Use this to expand/normalize events before they are converted into model messages.
|
|
292
|
+
* Typical use-cases:
|
|
293
|
+
* - Expand file/document references into text (LlamaCloud/Reducto/…)
|
|
294
|
+
* - Token compaction / summarization of older parts
|
|
295
|
+
* - Attaching derived context snippets to the next model call
|
|
296
|
+
*
|
|
297
|
+
* IMPORTANT:
|
|
298
|
+
* - This stage is ALWAYS executed by the engine.
|
|
299
|
+
* - If you don't provide an implementation, the default behavior is an identity transform
|
|
300
|
+
* (events pass through unchanged).
|
|
301
|
+
* - If your implementation performs I/O, implement it as a `"use-step"` function (provided via
|
|
302
|
+
* the builder) so results are durable and replay-safe.
|
|
303
|
+
* - If it’s pure/deterministic, it can run in workflow context.
|
|
304
|
+
*/
|
|
305
|
+
async expandEvents(events, _context, _env) {
|
|
306
|
+
return events;
|
|
307
|
+
}
|
|
308
|
+
getModel(_context, _env) {
|
|
309
|
+
return "openai/gpt-5";
|
|
310
|
+
}
|
|
311
|
+
getReactor(_context, _env) {
|
|
312
|
+
return this.reactor;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Context stop/continue hook.
|
|
316
|
+
*
|
|
317
|
+
* After the model streamed and tools executed, the story can decide whether the loop should
|
|
318
|
+
* continue.
|
|
319
|
+
*
|
|
320
|
+
* Default: `true` (continue).
|
|
321
|
+
*/
|
|
322
|
+
async shouldContinue(_args) {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
async react(triggerEvent, params) {
|
|
326
|
+
if (params.durable) {
|
|
327
|
+
return await ContextEngine.startDurable(this, triggerEvent, params);
|
|
328
|
+
}
|
|
329
|
+
return await ContextEngine.runDirect(this, triggerEvent, params);
|
|
330
|
+
}
|
|
331
|
+
static async prepareExecutionShell(story, triggerEvent, params) {
|
|
332
|
+
const ops = await measureBenchmark(params.__benchmark, "react.resolveOpsMs", async () => await getContextEngineOps(params.env, params.__benchmark));
|
|
333
|
+
const silent = params.options?.silent ?? false;
|
|
334
|
+
const ctxResult = await measureBenchmark(params.__benchmark, "react.initializeContextMs", async () => await ops.initializeContext(params.context ?? null, { silent }));
|
|
335
|
+
let currentContext = ctxResult.context;
|
|
336
|
+
const contextSelector = { id: String(currentContext.id) };
|
|
337
|
+
if (ctxResult.isNew) {
|
|
338
|
+
await story.opts.onContextCreated?.({ env: params.env, context: currentContext });
|
|
339
|
+
}
|
|
340
|
+
if (currentContext.status === "closed") {
|
|
341
|
+
await measureBenchmark(params.__benchmark, "react.reopenClosedContextMs", async () => await ops.updateContextStatus(contextSelector, "open_idle"));
|
|
342
|
+
currentContext = { ...currentContext, status: "open_idle" };
|
|
343
|
+
}
|
|
344
|
+
const shell = await measureBenchmark(params.__benchmark, "react.bootstrapShellMs", async () => await ops.saveTriggerAndCreateExecution({
|
|
345
|
+
contextIdentifier: contextSelector,
|
|
346
|
+
triggerEvent,
|
|
347
|
+
}));
|
|
348
|
+
currentContext = { ...currentContext, status: "open_streaming" };
|
|
349
|
+
return {
|
|
350
|
+
contextSelector,
|
|
351
|
+
currentContext,
|
|
352
|
+
trigger: shell.triggerEvent,
|
|
353
|
+
reaction: shell.reactionEvent,
|
|
354
|
+
execution: shell.execution,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
static async startDurable(story, triggerEvent, params) {
|
|
358
|
+
if (params.options?.writable) {
|
|
359
|
+
throw new Error("ContextEngine.react: durable runs manage their own workflow stream");
|
|
360
|
+
}
|
|
361
|
+
const contextKey = typeof story.__contextKey === "string" ? String(story.__contextKey) : "";
|
|
362
|
+
if (!contextKey) {
|
|
363
|
+
throw new Error("ContextEngine.react: durable mode requires a context built with createContext(...).build().");
|
|
364
|
+
}
|
|
365
|
+
const workflow = getContextDurableWorkflow();
|
|
366
|
+
if (typeof workflow !== "function") {
|
|
367
|
+
throw new Error("ContextEngine.react: durable workflow is not configured. Call configureContextDurableWorkflow(...) in runtime bootstrap.");
|
|
368
|
+
}
|
|
369
|
+
const shell = await ContextEngine.prepareExecutionShell(story, triggerEvent, params);
|
|
370
|
+
try {
|
|
371
|
+
const [{ start }] = await Promise.all([
|
|
372
|
+
import("workflow/api"),
|
|
373
|
+
]);
|
|
374
|
+
const run = await start(workflow, [
|
|
375
|
+
{
|
|
376
|
+
contextKey,
|
|
377
|
+
env: params.env,
|
|
378
|
+
context: params.context ?? null,
|
|
379
|
+
triggerEvent,
|
|
380
|
+
options: {
|
|
381
|
+
maxIterations: params.options?.maxIterations,
|
|
382
|
+
maxModelSteps: params.options?.maxModelSteps,
|
|
383
|
+
preventClose: params.options?.preventClose,
|
|
384
|
+
sendFinish: params.options?.sendFinish,
|
|
385
|
+
silent: params.options?.silent,
|
|
386
|
+
},
|
|
387
|
+
bootstrap: {
|
|
388
|
+
contextId: shell.currentContext.id,
|
|
389
|
+
trigger: shell.trigger,
|
|
390
|
+
reaction: shell.reaction,
|
|
391
|
+
execution: shell.execution,
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
]);
|
|
395
|
+
const runtime = await createRuntimeOps(params.env);
|
|
396
|
+
await runtime.db.transact([
|
|
397
|
+
runtime.db.tx.event_executions[shell.execution.id].update({
|
|
398
|
+
workflowRunId: run.runId,
|
|
399
|
+
updatedAt: new Date(),
|
|
400
|
+
}),
|
|
401
|
+
]);
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
const ops = await getContextEngineOps(params.env, params.__benchmark);
|
|
405
|
+
await ops.completeExecution(shell.contextSelector, shell.execution.id, "failed").catch(() => null);
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
context: shell.currentContext,
|
|
410
|
+
trigger: shell.trigger,
|
|
411
|
+
reaction: shell.reaction,
|
|
412
|
+
execution: shell.execution,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
static async runDirect(story, triggerEvent, params) {
|
|
416
|
+
const ops = await measureBenchmark(params.__benchmark, "react.resolveOpsMs", async () => await getContextEngineOps(params.env, params.__benchmark));
|
|
417
|
+
const maxIterations = params.options?.maxIterations ?? 20;
|
|
418
|
+
const maxModelSteps = params.options?.maxModelSteps ?? 1;
|
|
419
|
+
const preventClose = params.options?.preventClose ?? false;
|
|
420
|
+
const sendFinish = params.options?.sendFinish ?? true;
|
|
421
|
+
const silent = params.options?.silent ?? false;
|
|
422
|
+
const writable = params.options?.writable;
|
|
423
|
+
const bootstrapped = params.__bootstrap;
|
|
424
|
+
const shell = bootstrapped
|
|
425
|
+
? {
|
|
426
|
+
contextSelector: { id: String(bootstrapped.contextId) },
|
|
427
|
+
currentContext: (await measureBenchmark(params.__benchmark, "react.bootstrapContextLookupMs", async () => await ops.initializeContext({ id: String(bootstrapped.contextId) }, { silent }))).context,
|
|
428
|
+
trigger: bootstrapped.trigger,
|
|
429
|
+
reaction: bootstrapped.reaction,
|
|
430
|
+
execution: bootstrapped.execution,
|
|
431
|
+
}
|
|
432
|
+
: await ContextEngine.prepareExecutionShell(story, triggerEvent, params);
|
|
433
|
+
let currentContext = shell.currentContext;
|
|
434
|
+
let trigger = shell.trigger;
|
|
435
|
+
let reactionEvent = shell.reaction;
|
|
436
|
+
let execution = shell.execution;
|
|
437
|
+
const activeContextSelector = shell.contextSelector;
|
|
438
|
+
const triggerEventId = trigger.id;
|
|
439
|
+
const reactionEventId = reactionEvent.id;
|
|
440
|
+
const executionId = execution.id;
|
|
441
|
+
let updatedContext = { ...currentContext, status: "open_streaming" };
|
|
442
|
+
let currentStepId = null;
|
|
443
|
+
let currentStepStream = null;
|
|
444
|
+
const failExecution = async () => {
|
|
445
|
+
try {
|
|
446
|
+
await ops.completeExecution(activeContextSelector, executionId, "failed");
|
|
447
|
+
execution = { ...execution, status: "failed" };
|
|
448
|
+
updatedContext = { ...updatedContext, status: "closed" };
|
|
449
|
+
await emitContextEvents({
|
|
450
|
+
silent,
|
|
451
|
+
writable,
|
|
452
|
+
events: [
|
|
453
|
+
{
|
|
454
|
+
type: "execution.failed",
|
|
455
|
+
at: nowIso(),
|
|
456
|
+
executionId,
|
|
457
|
+
contextId: String(currentContext.id),
|
|
458
|
+
status: "failed",
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
type: "context.status_changed",
|
|
462
|
+
at: nowIso(),
|
|
463
|
+
contextId: String(currentContext.id),
|
|
464
|
+
status: "closed",
|
|
465
|
+
},
|
|
466
|
+
],
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
// noop
|
|
471
|
+
}
|
|
472
|
+
try {
|
|
473
|
+
if (!silent) {
|
|
474
|
+
await closeContextStream({ preventClose, sendFinish, writable });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
// noop
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
try {
|
|
482
|
+
for (let iter = 0; iter < maxIterations; iter++) {
|
|
483
|
+
// Create a persisted step per iteration (IDs generated in step runtime for replay safety)
|
|
484
|
+
const stagePrefix = `react.iteration.${iter}`;
|
|
485
|
+
const stepCreate = await measureBenchmark(params.__benchmark, `${stagePrefix}.createStepMs`, async () => await ops.createContextStep({
|
|
486
|
+
executionId,
|
|
487
|
+
iteration: iter,
|
|
488
|
+
}));
|
|
489
|
+
currentStepId = stepCreate.stepId;
|
|
490
|
+
currentStepStream = await createPersistedContextStepStream({
|
|
491
|
+
env: params.env,
|
|
492
|
+
executionId,
|
|
493
|
+
stepId: stepCreate.stepId,
|
|
494
|
+
});
|
|
495
|
+
await emitContextEvents({
|
|
496
|
+
silent,
|
|
497
|
+
writable,
|
|
498
|
+
events: [
|
|
499
|
+
{
|
|
500
|
+
type: "step.created",
|
|
501
|
+
at: nowIso(),
|
|
502
|
+
stepId: String(stepCreate.stepId),
|
|
503
|
+
executionId,
|
|
504
|
+
iteration: iter,
|
|
505
|
+
status: "running",
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
});
|
|
509
|
+
// Hook: Context DSL `context()` (implemented by subclasses via `initialize()`)
|
|
510
|
+
const nextContent = await measureBenchmark(params.__benchmark, `${stagePrefix}.contextMs`, async () => await story.initialize(updatedContext, params.env));
|
|
511
|
+
updatedContext = await measureBenchmark(params.__benchmark, `${stagePrefix}.persistContextMs`, async () => await ops.updateContextContent(activeContextSelector, nextContent));
|
|
512
|
+
await emitContextEvents({
|
|
513
|
+
silent,
|
|
514
|
+
writable,
|
|
515
|
+
events: [
|
|
516
|
+
{
|
|
517
|
+
type: "context.content_updated",
|
|
518
|
+
at: nowIso(),
|
|
519
|
+
contextId: String(updatedContext.id),
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
});
|
|
523
|
+
await story.opts.onContextUpdated?.({ env: params.env, context: updatedContext });
|
|
524
|
+
// Hook: Context DSL `narrative()` (implemented by subclasses via `buildSystemPrompt()`)
|
|
525
|
+
const systemPrompt = await measureBenchmark(params.__benchmark, `${stagePrefix}.narrativeMs`, async () => await story.buildSystemPrompt(updatedContext, params.env));
|
|
526
|
+
// Hook: Context DSL `actions()` (implemented by subclasses via `buildTools()`)
|
|
527
|
+
const toolsAll = await measureBenchmark(params.__benchmark, `${stagePrefix}.actionsMs`, async () => await story.buildTools(updatedContext, params.env));
|
|
528
|
+
const skillsAll = await measureBenchmark(params.__benchmark, `${stagePrefix}.skillsMs`, async () => await story.buildSkills(updatedContext, params.env));
|
|
529
|
+
// IMPORTANT: step args must be serializable.
|
|
530
|
+
// Match DurableAgent behavior: convert tool input schemas to plain JSON Schema in workflow context.
|
|
531
|
+
const toolsForModel = toolsToModelTools(toolsAll);
|
|
532
|
+
// Execute model reaction for this iteration using the stable reaction event id.
|
|
533
|
+
//
|
|
534
|
+
// IMPORTANT:
|
|
535
|
+
// We expose a single visible `context_event` per story turn (`reactionEventId`).
|
|
536
|
+
// If we stream with a per-step id, the UI will render an optimistic assistant message
|
|
537
|
+
// (step id) and then a second persisted assistant message (reaction id) with the same
|
|
538
|
+
// content once InstantDB updates.
|
|
539
|
+
const reactor = story.getReactor(updatedContext, params.env);
|
|
540
|
+
const reactionPartsBeforeStep = Array.isArray(reactionEvent.content?.parts)
|
|
541
|
+
? [...reactionEvent.content.parts]
|
|
542
|
+
: [];
|
|
543
|
+
let persistedReactionPartsSignature = "";
|
|
544
|
+
const persistReactionParts = async (nextParts) => {
|
|
545
|
+
const normalizedParts = normalizePartsForPersistence(Array.isArray(nextParts) ? nextParts : []);
|
|
546
|
+
const nextSignature = JSON.stringify(normalizedParts);
|
|
547
|
+
if (nextSignature === persistedReactionPartsSignature)
|
|
548
|
+
return;
|
|
549
|
+
persistedReactionPartsSignature = nextSignature;
|
|
550
|
+
await ops.saveContextPartsStep({
|
|
551
|
+
stepId: stepCreate.stepId,
|
|
552
|
+
parts: normalizedParts,
|
|
553
|
+
executionId,
|
|
554
|
+
contextId: String(currentContext.id),
|
|
555
|
+
iteration: iter,
|
|
556
|
+
});
|
|
557
|
+
reactionEvent = await ops.updateItem(reactionEvent.id, {
|
|
558
|
+
...reactionEvent,
|
|
559
|
+
content: {
|
|
560
|
+
...reactionEvent.content,
|
|
561
|
+
parts: [...reactionPartsBeforeStep, ...normalizedParts],
|
|
562
|
+
},
|
|
563
|
+
status: "pending",
|
|
564
|
+
}, { executionId, contextId: String(currentContext.id) });
|
|
565
|
+
};
|
|
566
|
+
const { assistantEvent, actionRequests, messagesForModel } = await measureBenchmark(params.__benchmark, `${stagePrefix}.reactorMs`, async () => await reactor({
|
|
567
|
+
env: params.env,
|
|
568
|
+
context: updatedContext,
|
|
569
|
+
contextIdentifier: activeContextSelector,
|
|
570
|
+
triggerEvent,
|
|
571
|
+
model: story.getModel(updatedContext, params.env),
|
|
572
|
+
systemPrompt,
|
|
573
|
+
actions: toolsAll,
|
|
574
|
+
toolsForModel,
|
|
575
|
+
skills: skillsAll,
|
|
576
|
+
eventId: reactionEventId,
|
|
577
|
+
executionId,
|
|
578
|
+
contextId: String(currentContext.id),
|
|
579
|
+
stepId: String(stepCreate.stepId),
|
|
580
|
+
iteration: iter,
|
|
581
|
+
maxModelSteps,
|
|
582
|
+
// Only emit a `start` chunk once per story turn.
|
|
583
|
+
sendStart: !silent && iter === 0,
|
|
584
|
+
silent,
|
|
585
|
+
contextStepStream: currentStepStream?.stream,
|
|
586
|
+
writable,
|
|
587
|
+
persistReactionParts,
|
|
588
|
+
}));
|
|
589
|
+
const reviewRequests = actionRequests.length > 0
|
|
590
|
+
? actionRequests.flatMap((actionRequest) => {
|
|
591
|
+
const toolDef = toolsAll[actionRequest.actionName];
|
|
592
|
+
const auto = toolDef?.auto !== false;
|
|
593
|
+
actionRequest.auto = auto;
|
|
594
|
+
if (auto)
|
|
595
|
+
return [];
|
|
596
|
+
return [
|
|
597
|
+
{
|
|
598
|
+
toolCallId: String(actionRequest.actionRef),
|
|
599
|
+
toolName: String(actionRequest.actionName ?? ""),
|
|
600
|
+
},
|
|
601
|
+
];
|
|
602
|
+
})
|
|
603
|
+
: [];
|
|
604
|
+
// Persist normalized parts hanging off the producing step (event_parts).
|
|
605
|
+
// IMPORTANT:
|
|
606
|
+
// We intentionally do NOT persist the per-step LLM assistant event as a `context_event`.
|
|
607
|
+
// The story exposes a single visible `context_event` per turn (`reactionEventId`) so the UI
|
|
608
|
+
// doesn't render duplicate assistant messages (LLM-step + aggregated reaction).
|
|
609
|
+
const stepParts = normalizePartsForPersistence((assistantEvent?.content?.parts ?? []));
|
|
610
|
+
const assistantEventEffective = {
|
|
611
|
+
...assistantEvent,
|
|
612
|
+
content: {
|
|
613
|
+
...(assistantEvent?.content ?? {}),
|
|
614
|
+
parts: stepParts,
|
|
615
|
+
},
|
|
616
|
+
};
|
|
617
|
+
await measureBenchmark(params.__benchmark, `${stagePrefix}.saveStepPartsMs`, async () => await ops.saveContextPartsStep({
|
|
618
|
+
stepId: stepCreate.stepId,
|
|
619
|
+
parts: stepParts,
|
|
620
|
+
executionId,
|
|
621
|
+
contextId: String(currentContext.id),
|
|
622
|
+
iteration: iter,
|
|
623
|
+
}));
|
|
624
|
+
await emitContextEvents({
|
|
625
|
+
silent,
|
|
626
|
+
writable,
|
|
627
|
+
events: stepParts.map((part, idx) => ({
|
|
628
|
+
type: "part.created",
|
|
629
|
+
at: nowIso(),
|
|
630
|
+
partKey: `${String(stepCreate.stepId)}:${idx}`,
|
|
631
|
+
stepId: String(stepCreate.stepId),
|
|
632
|
+
idx,
|
|
633
|
+
partType: part && typeof part.type === "string"
|
|
634
|
+
? String(part.type)
|
|
635
|
+
: undefined,
|
|
636
|
+
...summarizePartPreview(part),
|
|
637
|
+
})),
|
|
638
|
+
});
|
|
639
|
+
// Persist/append the aggregated reaction event (stable `reactionEventId` for the execution).
|
|
640
|
+
const nextAssistantParts = Array.isArray(assistantEventEffective.content?.parts)
|
|
641
|
+
? assistantEventEffective.content.parts
|
|
642
|
+
: [];
|
|
643
|
+
const nextReactionEvent = {
|
|
644
|
+
...reactionEvent,
|
|
645
|
+
content: {
|
|
646
|
+
...reactionEvent.content,
|
|
647
|
+
parts: [...reactionPartsBeforeStep, ...nextAssistantParts],
|
|
648
|
+
},
|
|
649
|
+
status: "pending",
|
|
650
|
+
};
|
|
651
|
+
reactionEvent = await measureBenchmark(params.__benchmark, `${stagePrefix}.persistAssistantReactionMs`, async () => await ops.updateItem(reactionEvent.id, nextReactionEvent, { executionId, contextId: String(currentContext.id) }));
|
|
652
|
+
if (currentStepStream) {
|
|
653
|
+
await closePersistedContextStepStream({
|
|
654
|
+
env: params.env,
|
|
655
|
+
session: currentStepStream,
|
|
656
|
+
});
|
|
657
|
+
currentStepStream = null;
|
|
658
|
+
}
|
|
659
|
+
story.opts.onEventCreated?.(assistantEventEffective);
|
|
660
|
+
const firstActionRequest = actionRequests?.[0];
|
|
661
|
+
await measureBenchmark(params.__benchmark, `${stagePrefix}.markStepRunningMs`, async () => await ops.updateContextStep({
|
|
662
|
+
stepId: stepCreate.stepId,
|
|
663
|
+
patch: firstActionRequest
|
|
664
|
+
? {
|
|
665
|
+
kind: "action_execute",
|
|
666
|
+
actionName: typeof firstActionRequest.actionName === "string"
|
|
667
|
+
? firstActionRequest.actionName
|
|
668
|
+
: undefined,
|
|
669
|
+
actionInput: firstActionRequest.input,
|
|
670
|
+
}
|
|
671
|
+
: {
|
|
672
|
+
kind: "message",
|
|
673
|
+
},
|
|
674
|
+
executionId,
|
|
675
|
+
contextId: String(currentContext.id),
|
|
676
|
+
iteration: iter,
|
|
677
|
+
}));
|
|
678
|
+
await emitContextEvents({
|
|
679
|
+
silent,
|
|
680
|
+
writable,
|
|
681
|
+
events: [
|
|
682
|
+
{
|
|
683
|
+
type: "step.updated",
|
|
684
|
+
at: nowIso(),
|
|
685
|
+
stepId: String(stepCreate.stepId),
|
|
686
|
+
executionId,
|
|
687
|
+
iteration: iter,
|
|
688
|
+
status: "running",
|
|
689
|
+
kind: firstActionRequest ? "action_execute" : "message",
|
|
690
|
+
actionName: firstActionRequest && typeof firstActionRequest.actionName === "string"
|
|
691
|
+
? firstActionRequest.actionName
|
|
692
|
+
: undefined,
|
|
693
|
+
},
|
|
694
|
+
],
|
|
695
|
+
});
|
|
696
|
+
// Done: no tool calls requested by the model
|
|
697
|
+
if (!actionRequests.length) {
|
|
698
|
+
const endResult = await story.callOnEnd(assistantEventEffective);
|
|
699
|
+
if (endResult) {
|
|
700
|
+
// Mark iteration step completed (no tools)
|
|
701
|
+
await measureBenchmark(params.__benchmark, `${stagePrefix}.finalizeMessageStepMs`, async () => await ops.updateContextStep({
|
|
702
|
+
stepId: stepCreate.stepId,
|
|
703
|
+
patch: {
|
|
704
|
+
status: "completed",
|
|
705
|
+
kind: "message",
|
|
706
|
+
actionRequests: [],
|
|
707
|
+
actionResults: [],
|
|
708
|
+
continueLoop: false,
|
|
709
|
+
},
|
|
710
|
+
executionId,
|
|
711
|
+
contextId: String(currentContext.id),
|
|
712
|
+
iteration: iter,
|
|
713
|
+
}));
|
|
714
|
+
await emitContextEvents({
|
|
715
|
+
silent,
|
|
716
|
+
writable,
|
|
717
|
+
events: [
|
|
718
|
+
{
|
|
719
|
+
type: "step.updated",
|
|
720
|
+
at: nowIso(),
|
|
721
|
+
stepId: String(stepCreate.stepId),
|
|
722
|
+
executionId,
|
|
723
|
+
iteration: iter,
|
|
724
|
+
status: "completed",
|
|
725
|
+
kind: "message",
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
type: "step.completed",
|
|
729
|
+
at: nowIso(),
|
|
730
|
+
stepId: String(stepCreate.stepId),
|
|
731
|
+
executionId,
|
|
732
|
+
iteration: iter,
|
|
733
|
+
status: "completed",
|
|
734
|
+
},
|
|
735
|
+
],
|
|
736
|
+
});
|
|
737
|
+
// Mark reaction event completed
|
|
738
|
+
await measureBenchmark(params.__benchmark, `${stagePrefix}.completeReactionMs`, async () => await ops.updateItem(reactionEventId, {
|
|
739
|
+
...reactionEvent,
|
|
740
|
+
status: "completed",
|
|
741
|
+
}, { executionId, contextId: String(currentContext.id) }));
|
|
742
|
+
await emitContextEvents({
|
|
743
|
+
silent,
|
|
744
|
+
writable,
|
|
745
|
+
events: [
|
|
746
|
+
{
|
|
747
|
+
type: "item.completed",
|
|
748
|
+
at: nowIso(),
|
|
749
|
+
itemId: String(reactionEventId),
|
|
750
|
+
contextId: String(currentContext.id),
|
|
751
|
+
executionId,
|
|
752
|
+
status: "completed",
|
|
753
|
+
},
|
|
754
|
+
],
|
|
755
|
+
});
|
|
756
|
+
await measureBenchmark(params.__benchmark, `${stagePrefix}.completeExecutionMs`, async () => await ops.completeExecution(activeContextSelector, executionId, "completed"));
|
|
757
|
+
execution = { ...execution, status: "completed" };
|
|
758
|
+
updatedContext = { ...updatedContext, status: "closed" };
|
|
759
|
+
await emitContextEvents({
|
|
760
|
+
silent,
|
|
761
|
+
writable,
|
|
762
|
+
events: [
|
|
763
|
+
{
|
|
764
|
+
type: "execution.completed",
|
|
765
|
+
at: nowIso(),
|
|
766
|
+
executionId,
|
|
767
|
+
contextId: String(currentContext.id),
|
|
768
|
+
status: "completed",
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
type: "context.status_changed",
|
|
772
|
+
at: nowIso(),
|
|
773
|
+
contextId: String(currentContext.id),
|
|
774
|
+
status: "closed",
|
|
775
|
+
},
|
|
776
|
+
],
|
|
777
|
+
});
|
|
778
|
+
if (!silent) {
|
|
779
|
+
await closeContextStream({ preventClose, sendFinish, writable });
|
|
780
|
+
}
|
|
781
|
+
reactionEvent = {
|
|
782
|
+
...reactionEvent,
|
|
783
|
+
status: "completed",
|
|
784
|
+
};
|
|
785
|
+
return {
|
|
786
|
+
context: updatedContext,
|
|
787
|
+
trigger,
|
|
788
|
+
reaction: reactionEvent,
|
|
789
|
+
execution,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
// Execute actions (workflow context; action implementations decide step vs workflow)
|
|
794
|
+
const actionResults = await measureBenchmark(params.__benchmark, `${stagePrefix}.actionExecutionMs`, async () => await Promise.all(actionRequests.map(async (actionRequest) => {
|
|
795
|
+
const toolDef = toolsAll[actionRequest.actionName];
|
|
796
|
+
if (!toolDef || typeof toolDef.execute !== "function") {
|
|
797
|
+
return {
|
|
798
|
+
actionRequest,
|
|
799
|
+
success: false,
|
|
800
|
+
output: null,
|
|
801
|
+
errorText: `Action "${actionRequest.actionName}" not found or has no execute().`,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
try {
|
|
805
|
+
let actionInput = actionRequest.input;
|
|
806
|
+
if (toolDef?.auto === false) {
|
|
807
|
+
const { createHook, createWebhook } = await import("workflow");
|
|
808
|
+
const toolCallId = String(actionRequest.actionRef);
|
|
809
|
+
const hookToken = toolApprovalHookToken({ executionId, toolCallId });
|
|
810
|
+
const webhookToken = toolApprovalWebhookToken({ executionId, toolCallId });
|
|
811
|
+
const hook = createHook({ token: hookToken });
|
|
812
|
+
const webhook = createWebhook({ token: webhookToken });
|
|
813
|
+
const approvalOrRequest = await Promise.race([
|
|
814
|
+
hook.then((approval) => ({ source: "hook", approval })),
|
|
815
|
+
webhook.then((request) => ({ source: "webhook", request })),
|
|
816
|
+
]);
|
|
817
|
+
const approval = approvalOrRequest.source === "hook"
|
|
818
|
+
? approvalOrRequest.approval
|
|
819
|
+
: await approvalOrRequest.request.json().catch(() => null);
|
|
820
|
+
if (!approval || approval.approved !== true) {
|
|
821
|
+
return {
|
|
822
|
+
actionRequest,
|
|
823
|
+
success: false,
|
|
824
|
+
output: null,
|
|
825
|
+
errorText: approval && "comment" in approval && approval.comment
|
|
826
|
+
? `Action execution not approved: ${approval.comment}`
|
|
827
|
+
: "Action execution not approved",
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
if ("args" in approval && approval.args !== undefined) {
|
|
831
|
+
actionInput = approval.args;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
const output = await toolDef.execute(actionInput, {
|
|
835
|
+
toolCallId: actionRequest.actionRef,
|
|
836
|
+
messages: messagesForModel,
|
|
837
|
+
eventId: reactionEventId,
|
|
838
|
+
executionId,
|
|
839
|
+
triggerEventId,
|
|
840
|
+
contextId: currentContext.id,
|
|
841
|
+
});
|
|
842
|
+
return { actionRequest, success: true, output };
|
|
843
|
+
}
|
|
844
|
+
catch (e) {
|
|
845
|
+
return {
|
|
846
|
+
actionRequest,
|
|
847
|
+
success: false,
|
|
848
|
+
output: null,
|
|
849
|
+
errorText: e instanceof Error ? e.message : String(e),
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
})));
|
|
853
|
+
// Merge action results into persisted parts (so next LLM call can see them)
|
|
854
|
+
let finalizedStepParts = Array.isArray(stepParts) ? [...stepParts] : [];
|
|
855
|
+
for (const r of actionResults) {
|
|
856
|
+
finalizedStepParts = applyToolExecutionResultToParts(finalizedStepParts, {
|
|
857
|
+
toolCallId: r.actionRequest.actionRef,
|
|
858
|
+
toolName: r.actionRequest.actionName,
|
|
859
|
+
}, {
|
|
860
|
+
success: Boolean(r.success),
|
|
861
|
+
result: r.output,
|
|
862
|
+
message: r.errorText,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
await measureBenchmark(params.__benchmark, `${stagePrefix}.saveFinalStepPartsMs`, async () => await ops.saveContextPartsStep({
|
|
866
|
+
stepId: stepCreate.stepId,
|
|
867
|
+
parts: finalizedStepParts,
|
|
868
|
+
executionId,
|
|
869
|
+
contextId: String(currentContext.id),
|
|
870
|
+
iteration: iter,
|
|
871
|
+
}));
|
|
872
|
+
reactionEvent = {
|
|
873
|
+
...reactionEvent,
|
|
874
|
+
content: {
|
|
875
|
+
...reactionEvent.content,
|
|
876
|
+
// Deprecated mirror for compatibility. `event_parts` are the
|
|
877
|
+
// source of truth for replay and step inspection.
|
|
878
|
+
parts: [...reactionPartsBeforeStep, ...finalizedStepParts],
|
|
879
|
+
},
|
|
880
|
+
status: "pending",
|
|
881
|
+
};
|
|
882
|
+
// Callback for observability/integration
|
|
883
|
+
for (const r of actionResults) {
|
|
884
|
+
await story.opts.onActionExecuted?.({
|
|
885
|
+
actionRequest: r.actionRequest,
|
|
886
|
+
success: r.success,
|
|
887
|
+
output: r.output,
|
|
888
|
+
errorText: r.errorText,
|
|
889
|
+
eventId: reactionEventId,
|
|
890
|
+
executionId,
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
// Stop/continue boundary: allow the Context to decide if the loop should continue.
|
|
894
|
+
// IMPORTANT: we call this after tool results have been merged into the persisted `reactionEvent`,
|
|
895
|
+
// so stories can inspect `reactionEvent.content.parts` deterministically.
|
|
896
|
+
const continueLoop = await measureBenchmark(params.__benchmark, `${stagePrefix}.shouldContinueMs`, async () => await story.shouldContinue({
|
|
897
|
+
env: params.env,
|
|
898
|
+
context: updatedContext,
|
|
899
|
+
reactionEvent,
|
|
900
|
+
assistantEvent: assistantEventEffective,
|
|
901
|
+
actionRequests,
|
|
902
|
+
actionResults: actionResults,
|
|
903
|
+
}));
|
|
904
|
+
// Persist per-iteration step outcome (tools + continue signal)
|
|
905
|
+
await measureBenchmark(params.__benchmark, `${stagePrefix}.completeStepMs`, async () => await ops.updateContextStep({
|
|
906
|
+
stepId: stepCreate.stepId,
|
|
907
|
+
patch: {
|
|
908
|
+
status: "completed",
|
|
909
|
+
kind: actionRequests?.length ? "action_result" : "message",
|
|
910
|
+
actionName: typeof actionResults?.[0]?.actionRequest?.actionName === "string"
|
|
911
|
+
? actionResults[0].actionRequest.actionName
|
|
912
|
+
: undefined,
|
|
913
|
+
actionInput: actionResults?.[0]?.actionRequest?.input,
|
|
914
|
+
actionOutput: actionResults?.[0]?.success === true
|
|
915
|
+
? actionResults[0]?.output
|
|
916
|
+
: undefined,
|
|
917
|
+
actionError: actionResults?.[0]?.success === false
|
|
918
|
+
? String(actionResults[0]?.errorText ?? "action_execution_failed")
|
|
919
|
+
: undefined,
|
|
920
|
+
actionRequests,
|
|
921
|
+
actionResults,
|
|
922
|
+
continueLoop: continueLoop !== false,
|
|
923
|
+
},
|
|
924
|
+
executionId,
|
|
925
|
+
contextId: String(currentContext.id),
|
|
926
|
+
iteration: iter,
|
|
927
|
+
}));
|
|
928
|
+
await emitContextEvents({
|
|
929
|
+
silent,
|
|
930
|
+
writable,
|
|
931
|
+
events: [
|
|
932
|
+
{
|
|
933
|
+
type: "step.updated",
|
|
934
|
+
at: nowIso(),
|
|
935
|
+
stepId: String(stepCreate.stepId),
|
|
936
|
+
executionId,
|
|
937
|
+
iteration: iter,
|
|
938
|
+
status: "completed",
|
|
939
|
+
kind: actionRequests?.length ? "action_result" : "message",
|
|
940
|
+
actionName: typeof actionResults?.[0]?.actionRequest?.actionName === "string"
|
|
941
|
+
? actionResults[0].actionRequest.actionName
|
|
942
|
+
: undefined,
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
type: "step.completed",
|
|
946
|
+
at: nowIso(),
|
|
947
|
+
stepId: String(stepCreate.stepId),
|
|
948
|
+
executionId,
|
|
949
|
+
iteration: iter,
|
|
950
|
+
status: "completed",
|
|
951
|
+
},
|
|
952
|
+
],
|
|
953
|
+
});
|
|
954
|
+
if (continueLoop !== false) {
|
|
955
|
+
reactionEvent = await measureBenchmark(params.__benchmark, `${stagePrefix}.persistPendingReactionMs`, async () => await ops.updateItem(reactionEventId, {
|
|
956
|
+
...reactionEvent,
|
|
957
|
+
status: "pending",
|
|
958
|
+
}, { executionId, contextId: String(currentContext.id) }));
|
|
959
|
+
await emitContextEvents({
|
|
960
|
+
silent,
|
|
961
|
+
writable,
|
|
962
|
+
events: [
|
|
963
|
+
{
|
|
964
|
+
type: "item.updated",
|
|
965
|
+
at: nowIso(),
|
|
966
|
+
itemId: String(reactionEventId),
|
|
967
|
+
contextId: String(currentContext.id),
|
|
968
|
+
executionId,
|
|
969
|
+
status: "pending",
|
|
970
|
+
},
|
|
971
|
+
],
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
if (continueLoop === false) {
|
|
975
|
+
await measureBenchmark(params.__benchmark, `${stagePrefix}.completeReactionMs`, async () => await ops.updateItem(reactionEventId, {
|
|
976
|
+
...reactionEvent,
|
|
977
|
+
status: "completed",
|
|
978
|
+
}, { executionId, contextId: String(currentContext.id) }));
|
|
979
|
+
await emitContextEvents({
|
|
980
|
+
silent,
|
|
981
|
+
writable,
|
|
982
|
+
events: [
|
|
983
|
+
{
|
|
984
|
+
type: "item.completed",
|
|
985
|
+
at: nowIso(),
|
|
986
|
+
itemId: String(reactionEventId),
|
|
987
|
+
contextId: String(currentContext.id),
|
|
988
|
+
executionId,
|
|
989
|
+
status: "completed",
|
|
990
|
+
},
|
|
991
|
+
],
|
|
992
|
+
});
|
|
993
|
+
await measureBenchmark(params.__benchmark, `${stagePrefix}.completeExecutionMs`, async () => await ops.completeExecution(activeContextSelector, executionId, "completed"));
|
|
994
|
+
execution = { ...execution, status: "completed" };
|
|
995
|
+
updatedContext = { ...updatedContext, status: "closed" };
|
|
996
|
+
await emitContextEvents({
|
|
997
|
+
silent,
|
|
998
|
+
writable,
|
|
999
|
+
events: [
|
|
1000
|
+
{
|
|
1001
|
+
type: "execution.completed",
|
|
1002
|
+
at: nowIso(),
|
|
1003
|
+
executionId,
|
|
1004
|
+
contextId: String(currentContext.id),
|
|
1005
|
+
status: "completed",
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
type: "context.status_changed",
|
|
1009
|
+
at: nowIso(),
|
|
1010
|
+
contextId: String(currentContext.id),
|
|
1011
|
+
status: "closed",
|
|
1012
|
+
},
|
|
1013
|
+
],
|
|
1014
|
+
});
|
|
1015
|
+
if (!silent) {
|
|
1016
|
+
await closeContextStream({ preventClose, sendFinish, writable });
|
|
1017
|
+
}
|
|
1018
|
+
reactionEvent = {
|
|
1019
|
+
...reactionEvent,
|
|
1020
|
+
status: "completed",
|
|
1021
|
+
};
|
|
1022
|
+
return {
|
|
1023
|
+
context: updatedContext,
|
|
1024
|
+
trigger,
|
|
1025
|
+
reaction: reactionEvent,
|
|
1026
|
+
execution,
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
throw new Error(`ContextEngine: maxIterations reached (${maxIterations}) without completion`);
|
|
1031
|
+
}
|
|
1032
|
+
catch (error) {
|
|
1033
|
+
if (currentStepStream) {
|
|
1034
|
+
try {
|
|
1035
|
+
await abortPersistedContextStepStream({
|
|
1036
|
+
env: params.env,
|
|
1037
|
+
session: currentStepStream,
|
|
1038
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
catch {
|
|
1042
|
+
// noop
|
|
1043
|
+
}
|
|
1044
|
+
finally {
|
|
1045
|
+
currentStepStream = null;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
// Best-effort: persist failure on the current iteration step (if any)
|
|
1049
|
+
if (currentStepId) {
|
|
1050
|
+
const failedStepId = currentStepId;
|
|
1051
|
+
try {
|
|
1052
|
+
await measureBenchmark(params.__benchmark, "react.failureStepPersistMs", async () => await ops.updateContextStep({
|
|
1053
|
+
stepId: failedStepId,
|
|
1054
|
+
patch: {
|
|
1055
|
+
status: "failed",
|
|
1056
|
+
errorText: error instanceof Error ? error.message : String(error),
|
|
1057
|
+
},
|
|
1058
|
+
executionId,
|
|
1059
|
+
contextId: String(currentContext.id),
|
|
1060
|
+
}));
|
|
1061
|
+
await emitContextEvents({
|
|
1062
|
+
silent,
|
|
1063
|
+
writable,
|
|
1064
|
+
events: [
|
|
1065
|
+
{
|
|
1066
|
+
type: "step.failed",
|
|
1067
|
+
at: nowIso(),
|
|
1068
|
+
stepId: String(failedStepId),
|
|
1069
|
+
executionId,
|
|
1070
|
+
status: "failed",
|
|
1071
|
+
errorText: error instanceof Error ? error.message : String(error),
|
|
1072
|
+
},
|
|
1073
|
+
],
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
catch {
|
|
1077
|
+
// noop
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
await failExecution();
|
|
1081
|
+
throw error;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* @deprecated Use `react()` instead. Kept for backwards compatibility.
|
|
1086
|
+
*/
|
|
1087
|
+
async stream(triggerEvent, params) {
|
|
1088
|
+
return await this.react(triggerEvent, params);
|
|
1089
|
+
}
|
|
1090
|
+
async callOnEnd(lastEvent) {
|
|
1091
|
+
if (!this.opts.onEnd)
|
|
1092
|
+
return true;
|
|
1093
|
+
const result = await this.opts.onEnd(lastEvent);
|
|
1094
|
+
if (typeof result === "boolean")
|
|
1095
|
+
return result;
|
|
1096
|
+
return true;
|
|
1097
|
+
}
|
|
1098
|
+
}
|