@ekairos/thread 1.22.4-beta.feature-thread-unify.0 → 1.22.4-beta.feature-core-thread-registry-sync.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 +7 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/react.d.ts +2 -18
- package/dist/react.js +2 -15
- 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.js +5 -1
- package/dist/reactors/scripted.reactor.d.ts +2 -2
- package/dist/reactors/scripted.reactor.js +3 -2
- package/dist/reactors/types.d.ts +6 -6
- package/dist/schema.js +10 -9
- package/dist/steps/do-thread-stream-step.d.ts +6 -2
- package/dist/steps/do-thread-stream-step.js +9 -5
- package/dist/steps/reaction.steps.d.ts +1 -1
- package/dist/steps/reaction.steps.js +48 -14
- package/dist/steps/store.steps.d.ts +10 -11
- package/dist/steps/store.steps.js +34 -77
- package/dist/steps/stream.steps.d.ts +3 -33
- package/dist/steps/stream.steps.js +7 -68
- package/dist/steps/trace.steps.js +1 -1
- package/dist/stores/instant.documents.d.ts +1 -1
- package/dist/stores/instant.documents.js +1 -1
- package/dist/stores/instant.store.d.ts +7 -3
- package/dist/stores/instant.store.js +32 -86
- package/dist/thread.contract.d.ts +16 -8
- package/dist/thread.contract.js +61 -19
- package/dist/thread.d.ts +1 -1
- package/dist/thread.engine.d.ts +13 -9
- package/dist/thread.engine.js +463 -84
- package/dist/thread.events.d.ts +3 -3
- package/dist/thread.events.js +11 -34
- package/dist/thread.reactor.d.ts +1 -1
- package/dist/thread.store.d.ts +9 -9
- package/dist/thread.stream.d.ts +100 -33
- package/dist/thread.stream.js +72 -63
- package/package.json +3 -2
package/dist/thread.engine.js
CHANGED
|
@@ -3,10 +3,65 @@ import { registerThreadEnv } from "./env.js";
|
|
|
3
3
|
import { applyToolExecutionResultToParts } from "./thread.toolcalls.js";
|
|
4
4
|
import { toolsToModelTools } from "./tools-to-model-tools.js";
|
|
5
5
|
import { createAiSdkReactor, } from "./thread.reactor.js";
|
|
6
|
-
import { closeThreadStream,
|
|
7
|
-
import { completeExecution, createThreadStep,
|
|
6
|
+
import { closeThreadStream, writeThreadEvents, } from "./steps/stream.steps.js";
|
|
7
|
+
import { completeExecution, createThreadStep, initializeContext, saveReactionItem, saveTriggerAndCreateExecution, saveThreadPartsStep, updateThreadStep, updateContextContent, updateItem, } from "./steps/store.steps.js";
|
|
8
8
|
import { getClientResumeHookUrl, toolApprovalHookToken, toolApprovalWebhookToken, } from "./thread.hooks.js";
|
|
9
9
|
export { toolApprovalHookToken, toolApprovalWebhookToken, getClientResumeHookUrl };
|
|
10
|
+
function nowIso() {
|
|
11
|
+
return new Date().toISOString();
|
|
12
|
+
}
|
|
13
|
+
function clipPreview(value, max = 240) {
|
|
14
|
+
if (value.length <= max)
|
|
15
|
+
return value;
|
|
16
|
+
return `${value.slice(0, max)}...`;
|
|
17
|
+
}
|
|
18
|
+
function summarizePartPreview(part) {
|
|
19
|
+
if (!part || typeof part !== "object")
|
|
20
|
+
return {};
|
|
21
|
+
const row = part;
|
|
22
|
+
const partType = typeof row.type === "string" ? row.type : "";
|
|
23
|
+
const partState = typeof row.state === "string" ? row.state : undefined;
|
|
24
|
+
const partToolCallId = typeof row.toolCallId === "string"
|
|
25
|
+
? row.toolCallId
|
|
26
|
+
: typeof row.id === "string"
|
|
27
|
+
? row.id
|
|
28
|
+
: undefined;
|
|
29
|
+
if (typeof row.text === "string" && row.text.trim().length > 0) {
|
|
30
|
+
return {
|
|
31
|
+
partPreview: clipPreview(row.text),
|
|
32
|
+
partState,
|
|
33
|
+
partToolCallId,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (partType.startsWith("tool-")) {
|
|
37
|
+
const payload = {
|
|
38
|
+
tool: partType,
|
|
39
|
+
state: partState,
|
|
40
|
+
input: row.input,
|
|
41
|
+
output: row.output,
|
|
42
|
+
errorText: row.errorText,
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
partPreview: clipPreview(JSON.stringify(payload)),
|
|
46
|
+
partState,
|
|
47
|
+
partToolCallId,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
partState,
|
|
52
|
+
partToolCallId,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function contextThreadIdOrNull(context) {
|
|
56
|
+
return typeof context.threadId === "string" && context.threadId.length > 0
|
|
57
|
+
? context.threadId
|
|
58
|
+
: null;
|
|
59
|
+
}
|
|
60
|
+
async function emitThreadEvents(params) {
|
|
61
|
+
if (params.silent || !params.writable || params.events.length === 0)
|
|
62
|
+
return;
|
|
63
|
+
await writeThreadEvents({ events: params.events, writable: params.writable });
|
|
64
|
+
}
|
|
10
65
|
export class Thread {
|
|
11
66
|
constructor(opts = {}, reactor) {
|
|
12
67
|
this.opts = opts;
|
|
@@ -90,30 +145,33 @@ export class Thread {
|
|
|
90
145
|
writable = getWritable({
|
|
91
146
|
namespace: `context:${String(currentContext.id)}`,
|
|
92
147
|
});
|
|
93
|
-
// If the context was created in `initializeContext` (which didn't have a writable yet),
|
|
94
|
-
// re-emit the context id chunk now so clients can subscribe to the right persisted thread.
|
|
95
|
-
if (ctxResult.isNew) {
|
|
96
|
-
await emitContextIdChunk({
|
|
97
|
-
env: params.env,
|
|
98
|
-
contextId: String(currentContext.id),
|
|
99
|
-
writable,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
// Reactor/steps always receive a stream argument.
|
|
104
|
-
// In silent mode (or when no workflow writable is available), we use an in-memory sink.
|
|
105
|
-
if (!writable) {
|
|
106
|
-
writable = new WritableStream({
|
|
107
|
-
write() {
|
|
108
|
-
// noop
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
148
|
}
|
|
112
149
|
const contextSelector = params.contextIdentifier?.id
|
|
113
150
|
? { id: String(params.contextIdentifier.id) }
|
|
114
151
|
: params.contextIdentifier?.key
|
|
115
152
|
? { key: params.contextIdentifier.key }
|
|
116
153
|
: { id: String(currentContext.id) };
|
|
154
|
+
const threadId = contextThreadIdOrNull(currentContext);
|
|
155
|
+
const resolvedThreadId = threadId ?? String(currentContext.id);
|
|
156
|
+
await emitThreadEvents({
|
|
157
|
+
silent,
|
|
158
|
+
writable,
|
|
159
|
+
events: [
|
|
160
|
+
{
|
|
161
|
+
type: ctxResult.isNew ? "context.created" : "context.resolved",
|
|
162
|
+
at: nowIso(),
|
|
163
|
+
contextId: String(currentContext.id),
|
|
164
|
+
threadId: resolvedThreadId,
|
|
165
|
+
status: String(currentContext.status ?? "open"),
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
type: ctxResult.isNew ? "thread.created" : "thread.resolved",
|
|
169
|
+
at: nowIso(),
|
|
170
|
+
threadId: resolvedThreadId,
|
|
171
|
+
status: "idle",
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
});
|
|
117
175
|
if (ctxResult.isNew) {
|
|
118
176
|
await story.opts.onContextCreated?.({ env: params.env, context: currentContext });
|
|
119
177
|
}
|
|
@@ -123,11 +181,36 @@ export class Thread {
|
|
|
123
181
|
contextIdentifier: contextSelector,
|
|
124
182
|
triggerEvent,
|
|
125
183
|
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
184
|
+
await emitThreadEvents({
|
|
185
|
+
silent,
|
|
186
|
+
writable,
|
|
187
|
+
events: [
|
|
188
|
+
{
|
|
189
|
+
type: "item.created",
|
|
190
|
+
at: nowIso(),
|
|
191
|
+
itemId: triggerEventId,
|
|
192
|
+
contextId: String(currentContext.id),
|
|
193
|
+
threadId: resolvedThreadId,
|
|
194
|
+
status: "stored",
|
|
195
|
+
itemType: "input",
|
|
196
|
+
executionId,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
type: "thread.streaming_started",
|
|
200
|
+
at: nowIso(),
|
|
201
|
+
threadId: resolvedThreadId,
|
|
202
|
+
status: "streaming",
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
type: "execution.created",
|
|
206
|
+
at: nowIso(),
|
|
207
|
+
executionId,
|
|
208
|
+
contextId: String(currentContext.id),
|
|
209
|
+
threadId: resolvedThreadId,
|
|
210
|
+
status: "executing",
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
});
|
|
131
214
|
let reactionEvent = null;
|
|
132
215
|
// Latest persisted context state for this run (we keep it in memory; store is updated via steps).
|
|
133
216
|
let updatedContext = currentContext;
|
|
@@ -135,6 +218,33 @@ export class Thread {
|
|
|
135
218
|
const failExecution = async () => {
|
|
136
219
|
try {
|
|
137
220
|
await completeExecution(params.env, contextSelector, executionId, "failed");
|
|
221
|
+
await emitThreadEvents({
|
|
222
|
+
silent,
|
|
223
|
+
writable,
|
|
224
|
+
events: [
|
|
225
|
+
{
|
|
226
|
+
type: "execution.failed",
|
|
227
|
+
at: nowIso(),
|
|
228
|
+
executionId,
|
|
229
|
+
contextId: String(currentContext.id),
|
|
230
|
+
threadId: resolvedThreadId,
|
|
231
|
+
status: "failed",
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
type: "context.closed",
|
|
235
|
+
at: nowIso(),
|
|
236
|
+
contextId: String(currentContext.id),
|
|
237
|
+
threadId: resolvedThreadId,
|
|
238
|
+
status: "closed",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
type: "thread.idle",
|
|
242
|
+
at: nowIso(),
|
|
243
|
+
threadId: resolvedThreadId,
|
|
244
|
+
status: "idle",
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
});
|
|
138
248
|
}
|
|
139
249
|
catch {
|
|
140
250
|
// noop
|
|
@@ -157,9 +267,35 @@ export class Thread {
|
|
|
157
267
|
iteration: iter,
|
|
158
268
|
});
|
|
159
269
|
currentStepId = stepCreate.stepId;
|
|
270
|
+
await emitThreadEvents({
|
|
271
|
+
silent,
|
|
272
|
+
writable,
|
|
273
|
+
events: [
|
|
274
|
+
{
|
|
275
|
+
type: "step.created",
|
|
276
|
+
at: nowIso(),
|
|
277
|
+
stepId: String(stepCreate.stepId),
|
|
278
|
+
executionId,
|
|
279
|
+
iteration: iter,
|
|
280
|
+
status: "running",
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
});
|
|
160
284
|
// Hook: Thread DSL `context()` (implemented by subclasses via `initialize()`)
|
|
161
285
|
const nextContent = await story.initialize(updatedContext, params.env);
|
|
162
286
|
updatedContext = await updateContextContent(params.env, contextSelector, nextContent);
|
|
287
|
+
await emitThreadEvents({
|
|
288
|
+
silent,
|
|
289
|
+
writable,
|
|
290
|
+
events: [
|
|
291
|
+
{
|
|
292
|
+
type: "context.content_updated",
|
|
293
|
+
at: nowIso(),
|
|
294
|
+
contextId: String(updatedContext.id),
|
|
295
|
+
threadId: String(contextThreadIdOrNull(updatedContext) ?? ""),
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
});
|
|
163
299
|
await story.opts.onContextUpdated?.({ env: params.env, context: updatedContext });
|
|
164
300
|
// Hook: Thread DSL `narrative()` (implemented by subclasses via `buildSystemPrompt()`)
|
|
165
301
|
const systemPrompt = await story.buildSystemPrompt(updatedContext, params.env);
|
|
@@ -176,7 +312,7 @@ export class Thread {
|
|
|
176
312
|
// (step id) and then a second persisted assistant message (reaction id) with the same
|
|
177
313
|
// content once InstantDB updates.
|
|
178
314
|
const reactor = story.getReactor(updatedContext, params.env);
|
|
179
|
-
const { assistantEvent,
|
|
315
|
+
const { assistantEvent, actionRequests, messagesForModel } = await reactor({
|
|
180
316
|
env: params.env,
|
|
181
317
|
context: updatedContext,
|
|
182
318
|
contextIdentifier: contextSelector,
|
|
@@ -196,17 +332,17 @@ export class Thread {
|
|
|
196
332
|
silent,
|
|
197
333
|
writable,
|
|
198
334
|
});
|
|
199
|
-
const reviewRequests =
|
|
200
|
-
?
|
|
201
|
-
const toolDef = toolsAll[
|
|
335
|
+
const reviewRequests = actionRequests.length > 0
|
|
336
|
+
? actionRequests.flatMap((actionRequest) => {
|
|
337
|
+
const toolDef = toolsAll[actionRequest.actionName];
|
|
202
338
|
const auto = toolDef?.auto !== false;
|
|
203
|
-
|
|
339
|
+
actionRequest.auto = auto;
|
|
204
340
|
if (auto)
|
|
205
341
|
return [];
|
|
206
342
|
return [
|
|
207
343
|
{
|
|
208
|
-
toolCallId: String(
|
|
209
|
-
toolName: String(
|
|
344
|
+
toolCallId: String(actionRequest.actionRef),
|
|
345
|
+
toolName: String(actionRequest.actionName ?? ""),
|
|
210
346
|
},
|
|
211
347
|
];
|
|
212
348
|
})
|
|
@@ -232,6 +368,21 @@ export class Thread {
|
|
|
232
368
|
contextId: String(currentContext.id),
|
|
233
369
|
iteration: iter,
|
|
234
370
|
});
|
|
371
|
+
await emitThreadEvents({
|
|
372
|
+
silent,
|
|
373
|
+
writable,
|
|
374
|
+
events: stepParts.map((part, idx) => ({
|
|
375
|
+
type: "part.created",
|
|
376
|
+
at: nowIso(),
|
|
377
|
+
partKey: `${String(stepCreate.stepId)}:${idx}`,
|
|
378
|
+
stepId: String(stepCreate.stepId),
|
|
379
|
+
idx,
|
|
380
|
+
partType: part && typeof part.type === "string"
|
|
381
|
+
? String(part.type)
|
|
382
|
+
: undefined,
|
|
383
|
+
...summarizePartPreview(part),
|
|
384
|
+
})),
|
|
385
|
+
});
|
|
235
386
|
// Persist/append the aggregated reaction event (stable `reactionEventId` for the execution).
|
|
236
387
|
if (!reactionEvent) {
|
|
237
388
|
const reactionPayload = {
|
|
@@ -243,6 +394,22 @@ export class Thread {
|
|
|
243
394
|
contextId: String(currentContext.id),
|
|
244
395
|
reviewRequests,
|
|
245
396
|
});
|
|
397
|
+
await emitThreadEvents({
|
|
398
|
+
silent,
|
|
399
|
+
writable,
|
|
400
|
+
events: [
|
|
401
|
+
{
|
|
402
|
+
type: "item.created",
|
|
403
|
+
at: nowIso(),
|
|
404
|
+
itemId: String(reactionEvent.id),
|
|
405
|
+
contextId: String(currentContext.id),
|
|
406
|
+
threadId: resolvedThreadId,
|
|
407
|
+
executionId,
|
|
408
|
+
status: "pending",
|
|
409
|
+
itemType: "output",
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
});
|
|
246
413
|
}
|
|
247
414
|
else {
|
|
248
415
|
const existingReactionParts = Array.isArray(reactionEvent.content?.parts)
|
|
@@ -260,10 +427,62 @@ export class Thread {
|
|
|
260
427
|
status: "pending",
|
|
261
428
|
};
|
|
262
429
|
reactionEvent = await updateItem(params.env, reactionEvent.id, nextReactionEvent, { executionId, contextId: String(currentContext.id) });
|
|
430
|
+
await emitThreadEvents({
|
|
431
|
+
silent,
|
|
432
|
+
writable,
|
|
433
|
+
events: [
|
|
434
|
+
{
|
|
435
|
+
type: "item.updated",
|
|
436
|
+
at: nowIso(),
|
|
437
|
+
itemId: String(reactionEvent.id),
|
|
438
|
+
contextId: String(currentContext.id),
|
|
439
|
+
threadId: resolvedThreadId,
|
|
440
|
+
executionId,
|
|
441
|
+
status: "pending",
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
});
|
|
263
445
|
}
|
|
264
446
|
story.opts.onEventCreated?.(assistantEventEffective);
|
|
447
|
+
const firstActionRequest = actionRequests?.[0];
|
|
448
|
+
await updateThreadStep({
|
|
449
|
+
env: params.env,
|
|
450
|
+
stepId: stepCreate.stepId,
|
|
451
|
+
patch: firstActionRequest
|
|
452
|
+
? {
|
|
453
|
+
kind: "action_execute",
|
|
454
|
+
actionName: typeof firstActionRequest.actionName === "string"
|
|
455
|
+
? firstActionRequest.actionName
|
|
456
|
+
: undefined,
|
|
457
|
+
actionInput: firstActionRequest.input,
|
|
458
|
+
}
|
|
459
|
+
: {
|
|
460
|
+
kind: "message",
|
|
461
|
+
},
|
|
462
|
+
executionId,
|
|
463
|
+
contextId: String(currentContext.id),
|
|
464
|
+
iteration: iter,
|
|
465
|
+
});
|
|
466
|
+
await emitThreadEvents({
|
|
467
|
+
silent,
|
|
468
|
+
writable,
|
|
469
|
+
events: [
|
|
470
|
+
{
|
|
471
|
+
type: "step.updated",
|
|
472
|
+
at: nowIso(),
|
|
473
|
+
stepId: String(stepCreate.stepId),
|
|
474
|
+
executionId,
|
|
475
|
+
iteration: iter,
|
|
476
|
+
status: "running",
|
|
477
|
+
kind: firstActionRequest ? "action_execute" : "message",
|
|
478
|
+
actionName: firstActionRequest && typeof firstActionRequest.actionName === "string"
|
|
479
|
+
? firstActionRequest.actionName
|
|
480
|
+
: undefined,
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
});
|
|
265
484
|
// Done: no tool calls requested by the model
|
|
266
|
-
if (!
|
|
485
|
+
if (!actionRequests.length) {
|
|
267
486
|
const endResult = await story.callOnEnd(assistantEventEffective);
|
|
268
487
|
if (endResult) {
|
|
269
488
|
// Mark iteration step completed (no tools)
|
|
@@ -272,20 +491,86 @@ export class Thread {
|
|
|
272
491
|
stepId: stepCreate.stepId,
|
|
273
492
|
patch: {
|
|
274
493
|
status: "completed",
|
|
275
|
-
|
|
276
|
-
|
|
494
|
+
kind: "message",
|
|
495
|
+
actionRequests: [],
|
|
496
|
+
actionResults: [],
|
|
277
497
|
continueLoop: false,
|
|
278
498
|
},
|
|
279
499
|
executionId,
|
|
280
500
|
contextId: String(currentContext.id),
|
|
281
501
|
iteration: iter,
|
|
282
502
|
});
|
|
503
|
+
await emitThreadEvents({
|
|
504
|
+
silent,
|
|
505
|
+
writable,
|
|
506
|
+
events: [
|
|
507
|
+
{
|
|
508
|
+
type: "step.updated",
|
|
509
|
+
at: nowIso(),
|
|
510
|
+
stepId: String(stepCreate.stepId),
|
|
511
|
+
executionId,
|
|
512
|
+
iteration: iter,
|
|
513
|
+
status: "completed",
|
|
514
|
+
kind: "message",
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
type: "step.completed",
|
|
518
|
+
at: nowIso(),
|
|
519
|
+
stepId: String(stepCreate.stepId),
|
|
520
|
+
executionId,
|
|
521
|
+
iteration: iter,
|
|
522
|
+
status: "completed",
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
});
|
|
283
526
|
// Mark reaction event completed
|
|
284
527
|
await updateItem(params.env, reactionEventId, {
|
|
285
528
|
...(reactionEvent ?? assistantEventEffective),
|
|
286
529
|
status: "completed",
|
|
287
530
|
}, { executionId, contextId: String(currentContext.id) });
|
|
531
|
+
await emitThreadEvents({
|
|
532
|
+
silent,
|
|
533
|
+
writable,
|
|
534
|
+
events: [
|
|
535
|
+
{
|
|
536
|
+
type: "item.completed",
|
|
537
|
+
at: nowIso(),
|
|
538
|
+
itemId: String(reactionEventId),
|
|
539
|
+
contextId: String(currentContext.id),
|
|
540
|
+
threadId: resolvedThreadId,
|
|
541
|
+
executionId,
|
|
542
|
+
status: "completed",
|
|
543
|
+
},
|
|
544
|
+
],
|
|
545
|
+
});
|
|
288
546
|
await completeExecution(params.env, contextSelector, executionId, "completed");
|
|
547
|
+
await emitThreadEvents({
|
|
548
|
+
silent,
|
|
549
|
+
writable,
|
|
550
|
+
events: [
|
|
551
|
+
{
|
|
552
|
+
type: "execution.completed",
|
|
553
|
+
at: nowIso(),
|
|
554
|
+
executionId,
|
|
555
|
+
contextId: String(currentContext.id),
|
|
556
|
+
threadId: resolvedThreadId,
|
|
557
|
+
status: "completed",
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
type: "context.closed",
|
|
561
|
+
at: nowIso(),
|
|
562
|
+
contextId: String(currentContext.id),
|
|
563
|
+
threadId: resolvedThreadId,
|
|
564
|
+
status: "closed",
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
type: "thread.idle",
|
|
568
|
+
at: nowIso(),
|
|
569
|
+
threadId: resolvedThreadId,
|
|
570
|
+
status: "idle",
|
|
571
|
+
},
|
|
572
|
+
],
|
|
573
|
+
});
|
|
289
574
|
if (!silent) {
|
|
290
575
|
await closeThreadStream({ preventClose, sendFinish, writable });
|
|
291
576
|
}
|
|
@@ -298,25 +583,22 @@ export class Thread {
|
|
|
298
583
|
};
|
|
299
584
|
}
|
|
300
585
|
}
|
|
301
|
-
// Execute
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
const executionResults = await Promise.all(toolCalls.map(async (tc) => {
|
|
306
|
-
const toolDef = toolsAll[tc.toolName];
|
|
586
|
+
// Execute actions (workflow context; action implementations decide step vs workflow)
|
|
587
|
+
const actionResults = await Promise.all(actionRequests.map(async (actionRequest) => {
|
|
588
|
+
const toolDef = toolsAll[actionRequest.actionName];
|
|
307
589
|
if (!toolDef || typeof toolDef.execute !== "function") {
|
|
308
590
|
return {
|
|
309
|
-
|
|
591
|
+
actionRequest,
|
|
310
592
|
success: false,
|
|
311
593
|
output: null,
|
|
312
|
-
errorText: `
|
|
594
|
+
errorText: `Action "${actionRequest.actionName}" not found or has no execute().`,
|
|
313
595
|
};
|
|
314
596
|
}
|
|
315
597
|
try {
|
|
316
|
-
let
|
|
598
|
+
let actionInput = actionRequest.input;
|
|
317
599
|
if (toolDef?.auto === false) {
|
|
318
600
|
const { createHook, createWebhook } = await import("workflow");
|
|
319
|
-
const toolCallId = String(
|
|
601
|
+
const toolCallId = String(actionRequest.actionRef);
|
|
320
602
|
const hookToken = toolApprovalHookToken({ executionId, toolCallId });
|
|
321
603
|
const webhookToken = toolApprovalWebhookToken({ executionId, toolCallId });
|
|
322
604
|
const hook = createHook({ token: hookToken });
|
|
@@ -330,67 +612,53 @@ export class Thread {
|
|
|
330
612
|
: await approvalOrRequest.request.json().catch(() => null);
|
|
331
613
|
if (!approval || approval.approved !== true) {
|
|
332
614
|
return {
|
|
333
|
-
|
|
615
|
+
actionRequest,
|
|
334
616
|
success: false,
|
|
335
617
|
output: null,
|
|
336
618
|
errorText: approval && "comment" in approval && approval.comment
|
|
337
|
-
? `
|
|
338
|
-
: "
|
|
619
|
+
? `Action execution not approved: ${approval.comment}`
|
|
620
|
+
: "Action execution not approved",
|
|
339
621
|
};
|
|
340
622
|
}
|
|
341
623
|
if ("args" in approval && approval.args !== undefined) {
|
|
342
|
-
|
|
624
|
+
actionInput = approval.args;
|
|
343
625
|
}
|
|
344
626
|
}
|
|
345
|
-
const output = await toolDef.execute(
|
|
346
|
-
toolCallId:
|
|
627
|
+
const output = await toolDef.execute(actionInput, {
|
|
628
|
+
toolCallId: actionRequest.actionRef,
|
|
347
629
|
messages: messagesForModel,
|
|
348
630
|
eventId: reactionEventId,
|
|
349
631
|
executionId,
|
|
350
632
|
triggerEventId,
|
|
351
633
|
contextId: currentContext.id,
|
|
352
634
|
});
|
|
353
|
-
return {
|
|
635
|
+
return { actionRequest, success: true, output };
|
|
354
636
|
}
|
|
355
637
|
catch (e) {
|
|
356
638
|
return {
|
|
357
|
-
|
|
639
|
+
actionRequest,
|
|
358
640
|
success: false,
|
|
359
641
|
output: null,
|
|
360
642
|
errorText: e instanceof Error ? e.message : String(e),
|
|
361
643
|
};
|
|
362
644
|
}
|
|
363
645
|
}));
|
|
364
|
-
//
|
|
365
|
-
if (!silent) {
|
|
366
|
-
await writeToolOutputs({
|
|
367
|
-
results: executionResults.map((r) => r.success
|
|
368
|
-
? { toolCallId: r.tc.toolCallId, success: true, output: r.output }
|
|
369
|
-
: {
|
|
370
|
-
toolCallId: r.tc.toolCallId,
|
|
371
|
-
success: false,
|
|
372
|
-
errorText: r.errorText,
|
|
373
|
-
}),
|
|
374
|
-
writable,
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
// Clear action status once tool execution results have been emitted.
|
|
378
|
-
if (!silent && toolCalls.length) {
|
|
379
|
-
await writeContextSubstate({ key: null, transient: true, writable });
|
|
380
|
-
}
|
|
381
|
-
// Merge tool results into persisted parts (so next LLM call can see them)
|
|
646
|
+
// Merge action results into persisted parts (so next LLM call can see them)
|
|
382
647
|
if (reactionEvent) {
|
|
383
648
|
let parts = Array.isArray(reactionEvent.content?.parts)
|
|
384
649
|
? [...reactionEvent.content.parts]
|
|
385
650
|
: [];
|
|
386
|
-
for (const r of
|
|
387
|
-
parts = applyToolExecutionResultToParts(parts,
|
|
651
|
+
for (const r of actionResults) {
|
|
652
|
+
parts = applyToolExecutionResultToParts(parts, {
|
|
653
|
+
toolCallId: r.actionRequest.actionRef,
|
|
654
|
+
toolName: r.actionRequest.actionName,
|
|
655
|
+
}, {
|
|
388
656
|
success: Boolean(r.success),
|
|
389
657
|
result: r.output,
|
|
390
658
|
message: r.errorText,
|
|
391
659
|
});
|
|
392
660
|
}
|
|
393
|
-
|
|
661
|
+
reactionEvent = {
|
|
394
662
|
...reactionEvent,
|
|
395
663
|
content: {
|
|
396
664
|
...reactionEvent.content,
|
|
@@ -398,12 +666,11 @@ export class Thread {
|
|
|
398
666
|
},
|
|
399
667
|
status: "pending",
|
|
400
668
|
};
|
|
401
|
-
reactionEvent = await updateItem(params.env, reactionEventId, nextReactionEvent, { executionId, contextId: String(currentContext.id) });
|
|
402
669
|
}
|
|
403
670
|
// Callback for observability/integration
|
|
404
|
-
for (const r of
|
|
405
|
-
await story.opts.
|
|
406
|
-
|
|
671
|
+
for (const r of actionResults) {
|
|
672
|
+
await story.opts.onActionExecuted?.({
|
|
673
|
+
actionRequest: r.actionRequest,
|
|
407
674
|
success: r.success,
|
|
408
675
|
output: r.output,
|
|
409
676
|
errorText: r.errorText,
|
|
@@ -419,8 +686,8 @@ export class Thread {
|
|
|
419
686
|
context: updatedContext,
|
|
420
687
|
reactionEvent: reactionEvent ?? assistantEventEffective,
|
|
421
688
|
assistantEvent: assistantEventEffective,
|
|
422
|
-
|
|
423
|
-
|
|
689
|
+
actionRequests,
|
|
690
|
+
actionResults: actionResults,
|
|
424
691
|
});
|
|
425
692
|
// Persist per-iteration step outcome (tools + continue signal)
|
|
426
693
|
await updateThreadStep({
|
|
@@ -428,20 +695,120 @@ export class Thread {
|
|
|
428
695
|
stepId: stepCreate.stepId,
|
|
429
696
|
patch: {
|
|
430
697
|
status: "completed",
|
|
431
|
-
|
|
432
|
-
|
|
698
|
+
kind: actionRequests?.length ? "action_result" : "message",
|
|
699
|
+
actionName: typeof actionResults?.[0]?.actionRequest?.actionName === "string"
|
|
700
|
+
? actionResults[0].actionRequest.actionName
|
|
701
|
+
: undefined,
|
|
702
|
+
actionInput: actionResults?.[0]?.actionRequest?.input,
|
|
703
|
+
actionOutput: actionResults?.[0]?.success === true
|
|
704
|
+
? actionResults[0]?.output
|
|
705
|
+
: undefined,
|
|
706
|
+
actionError: actionResults?.[0]?.success === false
|
|
707
|
+
? String(actionResults[0]?.errorText ?? "action_execution_failed")
|
|
708
|
+
: undefined,
|
|
709
|
+
actionRequests,
|
|
710
|
+
actionResults,
|
|
433
711
|
continueLoop: continueLoop !== false,
|
|
434
712
|
},
|
|
435
713
|
executionId,
|
|
436
714
|
contextId: String(currentContext.id),
|
|
437
715
|
iteration: iter,
|
|
438
716
|
});
|
|
717
|
+
await emitThreadEvents({
|
|
718
|
+
silent,
|
|
719
|
+
writable,
|
|
720
|
+
events: [
|
|
721
|
+
{
|
|
722
|
+
type: "step.updated",
|
|
723
|
+
at: nowIso(),
|
|
724
|
+
stepId: String(stepCreate.stepId),
|
|
725
|
+
executionId,
|
|
726
|
+
iteration: iter,
|
|
727
|
+
status: "completed",
|
|
728
|
+
kind: actionRequests?.length ? "action_result" : "message",
|
|
729
|
+
actionName: typeof actionResults?.[0]?.actionRequest?.actionName === "string"
|
|
730
|
+
? actionResults[0].actionRequest.actionName
|
|
731
|
+
: undefined,
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
type: "step.completed",
|
|
735
|
+
at: nowIso(),
|
|
736
|
+
stepId: String(stepCreate.stepId),
|
|
737
|
+
executionId,
|
|
738
|
+
iteration: iter,
|
|
739
|
+
status: "completed",
|
|
740
|
+
},
|
|
741
|
+
],
|
|
742
|
+
});
|
|
743
|
+
if (continueLoop !== false && reactionEvent) {
|
|
744
|
+
reactionEvent = await updateItem(params.env, reactionEventId, {
|
|
745
|
+
...reactionEvent,
|
|
746
|
+
status: "pending",
|
|
747
|
+
}, { executionId, contextId: String(currentContext.id) });
|
|
748
|
+
await emitThreadEvents({
|
|
749
|
+
silent,
|
|
750
|
+
writable,
|
|
751
|
+
events: [
|
|
752
|
+
{
|
|
753
|
+
type: "item.updated",
|
|
754
|
+
at: nowIso(),
|
|
755
|
+
itemId: String(reactionEventId),
|
|
756
|
+
contextId: String(currentContext.id),
|
|
757
|
+
threadId: resolvedThreadId,
|
|
758
|
+
executionId,
|
|
759
|
+
status: "pending",
|
|
760
|
+
},
|
|
761
|
+
],
|
|
762
|
+
});
|
|
763
|
+
}
|
|
439
764
|
if (continueLoop === false) {
|
|
440
765
|
await updateItem(params.env, reactionEventId, {
|
|
441
766
|
...(reactionEvent ?? assistantEventEffective),
|
|
442
767
|
status: "completed",
|
|
443
768
|
}, { executionId, contextId: String(currentContext.id) });
|
|
769
|
+
await emitThreadEvents({
|
|
770
|
+
silent,
|
|
771
|
+
writable,
|
|
772
|
+
events: [
|
|
773
|
+
{
|
|
774
|
+
type: "item.completed",
|
|
775
|
+
at: nowIso(),
|
|
776
|
+
itemId: String(reactionEventId),
|
|
777
|
+
contextId: String(currentContext.id),
|
|
778
|
+
threadId: resolvedThreadId,
|
|
779
|
+
executionId,
|
|
780
|
+
status: "completed",
|
|
781
|
+
},
|
|
782
|
+
],
|
|
783
|
+
});
|
|
444
784
|
await completeExecution(params.env, contextSelector, executionId, "completed");
|
|
785
|
+
await emitThreadEvents({
|
|
786
|
+
silent,
|
|
787
|
+
writable,
|
|
788
|
+
events: [
|
|
789
|
+
{
|
|
790
|
+
type: "execution.completed",
|
|
791
|
+
at: nowIso(),
|
|
792
|
+
executionId,
|
|
793
|
+
contextId: String(currentContext.id),
|
|
794
|
+
threadId: resolvedThreadId,
|
|
795
|
+
status: "completed",
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
type: "context.closed",
|
|
799
|
+
at: nowIso(),
|
|
800
|
+
contextId: String(currentContext.id),
|
|
801
|
+
threadId: resolvedThreadId,
|
|
802
|
+
status: "closed",
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
type: "thread.idle",
|
|
806
|
+
at: nowIso(),
|
|
807
|
+
threadId: resolvedThreadId,
|
|
808
|
+
status: "idle",
|
|
809
|
+
},
|
|
810
|
+
],
|
|
811
|
+
});
|
|
445
812
|
if (!silent) {
|
|
446
813
|
await closeThreadStream({ preventClose, sendFinish, writable });
|
|
447
814
|
}
|
|
@@ -470,6 +837,20 @@ export class Thread {
|
|
|
470
837
|
executionId,
|
|
471
838
|
contextId: String(currentContext.id),
|
|
472
839
|
});
|
|
840
|
+
await emitThreadEvents({
|
|
841
|
+
silent,
|
|
842
|
+
writable,
|
|
843
|
+
events: [
|
|
844
|
+
{
|
|
845
|
+
type: "step.failed",
|
|
846
|
+
at: nowIso(),
|
|
847
|
+
stepId: String(currentStepId),
|
|
848
|
+
executionId,
|
|
849
|
+
status: "failed",
|
|
850
|
+
errorText: error instanceof Error ? error.message : String(error),
|
|
851
|
+
},
|
|
852
|
+
],
|
|
853
|
+
});
|
|
473
854
|
}
|
|
474
855
|
catch {
|
|
475
856
|
// noop
|
|
@@ -491,8 +872,6 @@ export class Thread {
|
|
|
491
872
|
const result = await this.opts.onEnd(lastEvent);
|
|
492
873
|
if (typeof result === "boolean")
|
|
493
874
|
return result;
|
|
494
|
-
if (result && typeof result === "object" && "end" in result)
|
|
495
|
-
return Boolean(result.end);
|
|
496
875
|
return true;
|
|
497
876
|
}
|
|
498
877
|
}
|