@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.
Files changed (37) hide show
  1. package/README.md +7 -1
  2. package/dist/index.d.ts +5 -5
  3. package/dist/index.js +2 -2
  4. package/dist/react.d.ts +2 -18
  5. package/dist/react.js +2 -15
  6. package/dist/reactors/ai-sdk.chunk-map.d.ts +12 -0
  7. package/dist/reactors/ai-sdk.chunk-map.js +143 -0
  8. package/dist/reactors/ai-sdk.reactor.js +5 -1
  9. package/dist/reactors/scripted.reactor.d.ts +2 -2
  10. package/dist/reactors/scripted.reactor.js +3 -2
  11. package/dist/reactors/types.d.ts +6 -6
  12. package/dist/schema.js +10 -9
  13. package/dist/steps/do-thread-stream-step.d.ts +6 -2
  14. package/dist/steps/do-thread-stream-step.js +9 -5
  15. package/dist/steps/reaction.steps.d.ts +1 -1
  16. package/dist/steps/reaction.steps.js +48 -14
  17. package/dist/steps/store.steps.d.ts +10 -11
  18. package/dist/steps/store.steps.js +34 -77
  19. package/dist/steps/stream.steps.d.ts +3 -33
  20. package/dist/steps/stream.steps.js +7 -68
  21. package/dist/steps/trace.steps.js +1 -1
  22. package/dist/stores/instant.documents.d.ts +1 -1
  23. package/dist/stores/instant.documents.js +1 -1
  24. package/dist/stores/instant.store.d.ts +7 -3
  25. package/dist/stores/instant.store.js +32 -86
  26. package/dist/thread.contract.d.ts +16 -8
  27. package/dist/thread.contract.js +61 -19
  28. package/dist/thread.d.ts +1 -1
  29. package/dist/thread.engine.d.ts +13 -9
  30. package/dist/thread.engine.js +463 -84
  31. package/dist/thread.events.d.ts +3 -3
  32. package/dist/thread.events.js +11 -34
  33. package/dist/thread.reactor.d.ts +1 -1
  34. package/dist/thread.store.d.ts +9 -9
  35. package/dist/thread.stream.d.ts +100 -33
  36. package/dist/thread.stream.js +72 -63
  37. package/package.json +3 -2
@@ -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, writeContextSubstate, writeThreadPing, writeToolOutputs, } from "./steps/stream.steps.js";
7
- import { completeExecution, createThreadStep, emitContextIdChunk, initializeContext, saveReactionItem, saveTriggerAndCreateExecution, saveThreadPartsStep, updateThreadStep, updateContextContent, updateItem, } from "./steps/store.steps.js";
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
- // Emit a simple ping chunk early so clients can validate that streaming works end-to-end.
127
- // This should be ignored safely by clients that don't care about it.
128
- if (!silent) {
129
- await writeThreadPing({ label: "thread-start", writable });
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, toolCalls, messagesForModel } = await reactor({
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 = toolCalls.length > 0
200
- ? toolCalls.flatMap((tc) => {
201
- const toolDef = toolsAll[tc.toolName];
335
+ const reviewRequests = actionRequests.length > 0
336
+ ? actionRequests.flatMap((actionRequest) => {
337
+ const toolDef = toolsAll[actionRequest.actionName];
202
338
  const auto = toolDef?.auto !== false;
203
- tc.auto = auto;
339
+ actionRequest.auto = auto;
204
340
  if (auto)
205
341
  return [];
206
342
  return [
207
343
  {
208
- toolCallId: String(tc.toolCallId),
209
- toolName: String(tc.toolName ?? ""),
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 (!toolCalls.length) {
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
- toolCalls: [],
276
- toolExecutionResults: [],
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 tool calls (workflow context; tool implementations decide step vs workflow)
302
- if (!silent && toolCalls.length) {
303
- await writeContextSubstate({ key: "actions", transient: true, writable });
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
- tc,
591
+ actionRequest,
310
592
  success: false,
311
593
  output: null,
312
- errorText: `Tool "${tc.toolName}" not found or has no execute().`,
594
+ errorText: `Action "${actionRequest.actionName}" not found or has no execute().`,
313
595
  };
314
596
  }
315
597
  try {
316
- let toolArgs = tc.args;
598
+ let actionInput = actionRequest.input;
317
599
  if (toolDef?.auto === false) {
318
600
  const { createHook, createWebhook } = await import("workflow");
319
- const toolCallId = String(tc.toolCallId);
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
- tc,
615
+ actionRequest,
334
616
  success: false,
335
617
  output: null,
336
618
  errorText: approval && "comment" in approval && approval.comment
337
- ? `Tool execution not approved: ${approval.comment}`
338
- : "Tool execution not approved",
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
- toolArgs = approval.args;
624
+ actionInput = approval.args;
343
625
  }
344
626
  }
345
- const output = await toolDef.execute(toolArgs, {
346
- toolCallId: tc.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 { tc, success: true, output };
635
+ return { actionRequest, success: true, output };
354
636
  }
355
637
  catch (e) {
356
638
  return {
357
- tc,
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
- // Emit tool outputs to the workflow stream (step)
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 executionResults) {
387
- parts = applyToolExecutionResultToParts(parts, r.tc, {
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
- const nextReactionEvent = {
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 executionResults) {
405
- await story.opts.onToolCallExecuted?.({
406
- toolCall: r.tc,
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
- toolCalls,
423
- toolExecutionResults: executionResults,
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
- toolCalls,
432
- toolExecutionResults: executionResults,
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
  }