@ekairos/thread 1.22.16-beta.feature-core-thread-registry-sync.0 → 1.22.18-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.
@@ -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.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,15 +145,6 @@ 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
148
  }
103
149
  // Reactor/steps always receive a stream argument.
104
150
  // In silent mode (or when no workflow writable is available), we use an in-memory sink.
@@ -114,6 +160,27 @@ export class Thread {
114
160
  : params.contextIdentifier?.key
115
161
  ? { key: params.contextIdentifier.key }
116
162
  : { id: String(currentContext.id) };
163
+ const threadId = contextThreadIdOrNull(currentContext);
164
+ const resolvedThreadId = threadId ?? String(currentContext.id);
165
+ await emitThreadEvents({
166
+ silent,
167
+ writable,
168
+ events: [
169
+ {
170
+ type: ctxResult.isNew ? "context.created" : "context.resolved",
171
+ at: nowIso(),
172
+ contextId: String(currentContext.id),
173
+ threadId: resolvedThreadId,
174
+ status: String(currentContext.status ?? "open"),
175
+ },
176
+ {
177
+ type: ctxResult.isNew ? "thread.created" : "thread.resolved",
178
+ at: nowIso(),
179
+ threadId: resolvedThreadId,
180
+ status: "idle",
181
+ },
182
+ ],
183
+ });
117
184
  if (ctxResult.isNew) {
118
185
  await story.opts.onContextCreated?.({ env: params.env, context: currentContext });
119
186
  }
@@ -123,11 +190,36 @@ export class Thread {
123
190
  contextIdentifier: contextSelector,
124
191
  triggerEvent,
125
192
  });
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
- }
193
+ await emitThreadEvents({
194
+ silent,
195
+ writable,
196
+ events: [
197
+ {
198
+ type: "item.created",
199
+ at: nowIso(),
200
+ itemId: triggerEventId,
201
+ contextId: String(currentContext.id),
202
+ threadId: resolvedThreadId,
203
+ status: "stored",
204
+ itemType: "input",
205
+ executionId,
206
+ },
207
+ {
208
+ type: "thread.streaming_started",
209
+ at: nowIso(),
210
+ threadId: resolvedThreadId,
211
+ status: "streaming",
212
+ },
213
+ {
214
+ type: "execution.created",
215
+ at: nowIso(),
216
+ executionId,
217
+ contextId: String(currentContext.id),
218
+ threadId: resolvedThreadId,
219
+ status: "executing",
220
+ },
221
+ ],
222
+ });
131
223
  let reactionEvent = null;
132
224
  // Latest persisted context state for this run (we keep it in memory; store is updated via steps).
133
225
  let updatedContext = currentContext;
@@ -135,6 +227,33 @@ export class Thread {
135
227
  const failExecution = async () => {
136
228
  try {
137
229
  await completeExecution(params.env, contextSelector, executionId, "failed");
230
+ await emitThreadEvents({
231
+ silent,
232
+ writable,
233
+ events: [
234
+ {
235
+ type: "execution.failed",
236
+ at: nowIso(),
237
+ executionId,
238
+ contextId: String(currentContext.id),
239
+ threadId: resolvedThreadId,
240
+ status: "failed",
241
+ },
242
+ {
243
+ type: "context.closed",
244
+ at: nowIso(),
245
+ contextId: String(currentContext.id),
246
+ threadId: resolvedThreadId,
247
+ status: "closed",
248
+ },
249
+ {
250
+ type: "thread.idle",
251
+ at: nowIso(),
252
+ threadId: resolvedThreadId,
253
+ status: "idle",
254
+ },
255
+ ],
256
+ });
138
257
  }
139
258
  catch {
140
259
  // noop
@@ -157,9 +276,35 @@ export class Thread {
157
276
  iteration: iter,
158
277
  });
159
278
  currentStepId = stepCreate.stepId;
279
+ await emitThreadEvents({
280
+ silent,
281
+ writable,
282
+ events: [
283
+ {
284
+ type: "step.created",
285
+ at: nowIso(),
286
+ stepId: String(stepCreate.stepId),
287
+ executionId,
288
+ iteration: iter,
289
+ status: "running",
290
+ },
291
+ ],
292
+ });
160
293
  // Hook: Thread DSL `context()` (implemented by subclasses via `initialize()`)
161
294
  const nextContent = await story.initialize(updatedContext, params.env);
162
295
  updatedContext = await updateContextContent(params.env, contextSelector, nextContent);
296
+ await emitThreadEvents({
297
+ silent,
298
+ writable,
299
+ events: [
300
+ {
301
+ type: "context.content_updated",
302
+ at: nowIso(),
303
+ contextId: String(updatedContext.id),
304
+ threadId: String(contextThreadIdOrNull(updatedContext) ?? ""),
305
+ },
306
+ ],
307
+ });
163
308
  await story.opts.onContextUpdated?.({ env: params.env, context: updatedContext });
164
309
  // Hook: Thread DSL `narrative()` (implemented by subclasses via `buildSystemPrompt()`)
165
310
  const systemPrompt = await story.buildSystemPrompt(updatedContext, params.env);
@@ -176,7 +321,7 @@ export class Thread {
176
321
  // (step id) and then a second persisted assistant message (reaction id) with the same
177
322
  // content once InstantDB updates.
178
323
  const reactor = story.getReactor(updatedContext, params.env);
179
- const { assistantEvent, toolCalls, messagesForModel } = await reactor({
324
+ const { assistantEvent, actionRequests, messagesForModel } = await reactor({
180
325
  env: params.env,
181
326
  context: updatedContext,
182
327
  contextIdentifier: contextSelector,
@@ -196,17 +341,17 @@ export class Thread {
196
341
  silent,
197
342
  writable,
198
343
  });
199
- const reviewRequests = toolCalls.length > 0
200
- ? toolCalls.flatMap((tc) => {
201
- const toolDef = toolsAll[tc.toolName];
344
+ const reviewRequests = actionRequests.length > 0
345
+ ? actionRequests.flatMap((actionRequest) => {
346
+ const toolDef = toolsAll[actionRequest.actionName];
202
347
  const auto = toolDef?.auto !== false;
203
- tc.auto = auto;
348
+ actionRequest.auto = auto;
204
349
  if (auto)
205
350
  return [];
206
351
  return [
207
352
  {
208
- toolCallId: String(tc.toolCallId),
209
- toolName: String(tc.toolName ?? ""),
353
+ toolCallId: String(actionRequest.actionRef),
354
+ toolName: String(actionRequest.actionName ?? ""),
210
355
  },
211
356
  ];
212
357
  })
@@ -232,6 +377,21 @@ export class Thread {
232
377
  contextId: String(currentContext.id),
233
378
  iteration: iter,
234
379
  });
380
+ await emitThreadEvents({
381
+ silent,
382
+ writable,
383
+ events: stepParts.map((part, idx) => ({
384
+ type: "part.created",
385
+ at: nowIso(),
386
+ partKey: `${String(stepCreate.stepId)}:${idx}`,
387
+ stepId: String(stepCreate.stepId),
388
+ idx,
389
+ partType: part && typeof part.type === "string"
390
+ ? String(part.type)
391
+ : undefined,
392
+ ...summarizePartPreview(part),
393
+ })),
394
+ });
235
395
  // Persist/append the aggregated reaction event (stable `reactionEventId` for the execution).
236
396
  if (!reactionEvent) {
237
397
  const reactionPayload = {
@@ -243,6 +403,22 @@ export class Thread {
243
403
  contextId: String(currentContext.id),
244
404
  reviewRequests,
245
405
  });
406
+ await emitThreadEvents({
407
+ silent,
408
+ writable,
409
+ events: [
410
+ {
411
+ type: "item.created",
412
+ at: nowIso(),
413
+ itemId: String(reactionEvent.id),
414
+ contextId: String(currentContext.id),
415
+ threadId: resolvedThreadId,
416
+ executionId,
417
+ status: "pending",
418
+ itemType: "output",
419
+ },
420
+ ],
421
+ });
246
422
  }
247
423
  else {
248
424
  const existingReactionParts = Array.isArray(reactionEvent.content?.parts)
@@ -260,10 +436,62 @@ export class Thread {
260
436
  status: "pending",
261
437
  };
262
438
  reactionEvent = await updateItem(params.env, reactionEvent.id, nextReactionEvent, { executionId, contextId: String(currentContext.id) });
439
+ await emitThreadEvents({
440
+ silent,
441
+ writable,
442
+ events: [
443
+ {
444
+ type: "item.updated",
445
+ at: nowIso(),
446
+ itemId: String(reactionEvent.id),
447
+ contextId: String(currentContext.id),
448
+ threadId: resolvedThreadId,
449
+ executionId,
450
+ status: "pending",
451
+ },
452
+ ],
453
+ });
263
454
  }
264
455
  story.opts.onEventCreated?.(assistantEventEffective);
456
+ const firstActionRequest = actionRequests?.[0];
457
+ await updateThreadStep({
458
+ env: params.env,
459
+ stepId: stepCreate.stepId,
460
+ patch: firstActionRequest
461
+ ? {
462
+ kind: "action_execute",
463
+ actionName: typeof firstActionRequest.actionName === "string"
464
+ ? firstActionRequest.actionName
465
+ : undefined,
466
+ actionInput: firstActionRequest.input,
467
+ }
468
+ : {
469
+ kind: "message",
470
+ },
471
+ executionId,
472
+ contextId: String(currentContext.id),
473
+ iteration: iter,
474
+ });
475
+ await emitThreadEvents({
476
+ silent,
477
+ writable,
478
+ events: [
479
+ {
480
+ type: "step.updated",
481
+ at: nowIso(),
482
+ stepId: String(stepCreate.stepId),
483
+ executionId,
484
+ iteration: iter,
485
+ status: "running",
486
+ kind: firstActionRequest ? "action_execute" : "message",
487
+ actionName: firstActionRequest && typeof firstActionRequest.actionName === "string"
488
+ ? firstActionRequest.actionName
489
+ : undefined,
490
+ },
491
+ ],
492
+ });
265
493
  // Done: no tool calls requested by the model
266
- if (!toolCalls.length) {
494
+ if (!actionRequests.length) {
267
495
  const endResult = await story.callOnEnd(assistantEventEffective);
268
496
  if (endResult) {
269
497
  // Mark iteration step completed (no tools)
@@ -272,20 +500,86 @@ export class Thread {
272
500
  stepId: stepCreate.stepId,
273
501
  patch: {
274
502
  status: "completed",
275
- toolCalls: [],
276
- toolExecutionResults: [],
503
+ kind: "message",
504
+ actionRequests: [],
505
+ actionResults: [],
277
506
  continueLoop: false,
278
507
  },
279
508
  executionId,
280
509
  contextId: String(currentContext.id),
281
510
  iteration: iter,
282
511
  });
512
+ await emitThreadEvents({
513
+ silent,
514
+ writable,
515
+ events: [
516
+ {
517
+ type: "step.updated",
518
+ at: nowIso(),
519
+ stepId: String(stepCreate.stepId),
520
+ executionId,
521
+ iteration: iter,
522
+ status: "completed",
523
+ kind: "message",
524
+ },
525
+ {
526
+ type: "step.completed",
527
+ at: nowIso(),
528
+ stepId: String(stepCreate.stepId),
529
+ executionId,
530
+ iteration: iter,
531
+ status: "completed",
532
+ },
533
+ ],
534
+ });
283
535
  // Mark reaction event completed
284
536
  await updateItem(params.env, reactionEventId, {
285
537
  ...(reactionEvent ?? assistantEventEffective),
286
538
  status: "completed",
287
539
  }, { executionId, contextId: String(currentContext.id) });
540
+ await emitThreadEvents({
541
+ silent,
542
+ writable,
543
+ events: [
544
+ {
545
+ type: "item.completed",
546
+ at: nowIso(),
547
+ itemId: String(reactionEventId),
548
+ contextId: String(currentContext.id),
549
+ threadId: resolvedThreadId,
550
+ executionId,
551
+ status: "completed",
552
+ },
553
+ ],
554
+ });
288
555
  await completeExecution(params.env, contextSelector, executionId, "completed");
556
+ await emitThreadEvents({
557
+ silent,
558
+ writable,
559
+ events: [
560
+ {
561
+ type: "execution.completed",
562
+ at: nowIso(),
563
+ executionId,
564
+ contextId: String(currentContext.id),
565
+ threadId: resolvedThreadId,
566
+ status: "completed",
567
+ },
568
+ {
569
+ type: "context.closed",
570
+ at: nowIso(),
571
+ contextId: String(currentContext.id),
572
+ threadId: resolvedThreadId,
573
+ status: "closed",
574
+ },
575
+ {
576
+ type: "thread.idle",
577
+ at: nowIso(),
578
+ threadId: resolvedThreadId,
579
+ status: "idle",
580
+ },
581
+ ],
582
+ });
289
583
  if (!silent) {
290
584
  await closeThreadStream({ preventClose, sendFinish, writable });
291
585
  }
@@ -298,25 +592,22 @@ export class Thread {
298
592
  };
299
593
  }
300
594
  }
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];
595
+ // Execute actions (workflow context; action implementations decide step vs workflow)
596
+ const actionResults = await Promise.all(actionRequests.map(async (actionRequest) => {
597
+ const toolDef = toolsAll[actionRequest.actionName];
307
598
  if (!toolDef || typeof toolDef.execute !== "function") {
308
599
  return {
309
- tc,
600
+ actionRequest,
310
601
  success: false,
311
602
  output: null,
312
- errorText: `Tool "${tc.toolName}" not found or has no execute().`,
603
+ errorText: `Action "${actionRequest.actionName}" not found or has no execute().`,
313
604
  };
314
605
  }
315
606
  try {
316
- let toolArgs = tc.args;
607
+ let actionInput = actionRequest.input;
317
608
  if (toolDef?.auto === false) {
318
609
  const { createHook, createWebhook } = await import("workflow");
319
- const toolCallId = String(tc.toolCallId);
610
+ const toolCallId = String(actionRequest.actionRef);
320
611
  const hookToken = toolApprovalHookToken({ executionId, toolCallId });
321
612
  const webhookToken = toolApprovalWebhookToken({ executionId, toolCallId });
322
613
  const hook = createHook({ token: hookToken });
@@ -330,67 +621,53 @@ export class Thread {
330
621
  : await approvalOrRequest.request.json().catch(() => null);
331
622
  if (!approval || approval.approved !== true) {
332
623
  return {
333
- tc,
624
+ actionRequest,
334
625
  success: false,
335
626
  output: null,
336
627
  errorText: approval && "comment" in approval && approval.comment
337
- ? `Tool execution not approved: ${approval.comment}`
338
- : "Tool execution not approved",
628
+ ? `Action execution not approved: ${approval.comment}`
629
+ : "Action execution not approved",
339
630
  };
340
631
  }
341
632
  if ("args" in approval && approval.args !== undefined) {
342
- toolArgs = approval.args;
633
+ actionInput = approval.args;
343
634
  }
344
635
  }
345
- const output = await toolDef.execute(toolArgs, {
346
- toolCallId: tc.toolCallId,
636
+ const output = await toolDef.execute(actionInput, {
637
+ toolCallId: actionRequest.actionRef,
347
638
  messages: messagesForModel,
348
639
  eventId: reactionEventId,
349
640
  executionId,
350
641
  triggerEventId,
351
642
  contextId: currentContext.id,
352
643
  });
353
- return { tc, success: true, output };
644
+ return { actionRequest, success: true, output };
354
645
  }
355
646
  catch (e) {
356
647
  return {
357
- tc,
648
+ actionRequest,
358
649
  success: false,
359
650
  output: null,
360
651
  errorText: e instanceof Error ? e.message : String(e),
361
652
  };
362
653
  }
363
654
  }));
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)
655
+ // Merge action results into persisted parts (so next LLM call can see them)
382
656
  if (reactionEvent) {
383
657
  let parts = Array.isArray(reactionEvent.content?.parts)
384
658
  ? [...reactionEvent.content.parts]
385
659
  : [];
386
- for (const r of executionResults) {
387
- parts = applyToolExecutionResultToParts(parts, r.tc, {
660
+ for (const r of actionResults) {
661
+ parts = applyToolExecutionResultToParts(parts, {
662
+ toolCallId: r.actionRequest.actionRef,
663
+ toolName: r.actionRequest.actionName,
664
+ }, {
388
665
  success: Boolean(r.success),
389
666
  result: r.output,
390
667
  message: r.errorText,
391
668
  });
392
669
  }
393
- const nextReactionEvent = {
670
+ reactionEvent = {
394
671
  ...reactionEvent,
395
672
  content: {
396
673
  ...reactionEvent.content,
@@ -398,12 +675,11 @@ export class Thread {
398
675
  },
399
676
  status: "pending",
400
677
  };
401
- reactionEvent = await updateItem(params.env, reactionEventId, nextReactionEvent, { executionId, contextId: String(currentContext.id) });
402
678
  }
403
679
  // Callback for observability/integration
404
- for (const r of executionResults) {
405
- await story.opts.onToolCallExecuted?.({
406
- toolCall: r.tc,
680
+ for (const r of actionResults) {
681
+ await story.opts.onActionExecuted?.({
682
+ actionRequest: r.actionRequest,
407
683
  success: r.success,
408
684
  output: r.output,
409
685
  errorText: r.errorText,
@@ -419,8 +695,8 @@ export class Thread {
419
695
  context: updatedContext,
420
696
  reactionEvent: reactionEvent ?? assistantEventEffective,
421
697
  assistantEvent: assistantEventEffective,
422
- toolCalls,
423
- toolExecutionResults: executionResults,
698
+ actionRequests,
699
+ actionResults: actionResults,
424
700
  });
425
701
  // Persist per-iteration step outcome (tools + continue signal)
426
702
  await updateThreadStep({
@@ -428,20 +704,120 @@ export class Thread {
428
704
  stepId: stepCreate.stepId,
429
705
  patch: {
430
706
  status: "completed",
431
- toolCalls,
432
- toolExecutionResults: executionResults,
707
+ kind: actionRequests?.length ? "action_result" : "message",
708
+ actionName: typeof actionResults?.[0]?.actionRequest?.actionName === "string"
709
+ ? actionResults[0].actionRequest.actionName
710
+ : undefined,
711
+ actionInput: actionResults?.[0]?.actionRequest?.input,
712
+ actionOutput: actionResults?.[0]?.success === true
713
+ ? actionResults[0]?.output
714
+ : undefined,
715
+ actionError: actionResults?.[0]?.success === false
716
+ ? String(actionResults[0]?.errorText ?? "action_execution_failed")
717
+ : undefined,
718
+ actionRequests,
719
+ actionResults,
433
720
  continueLoop: continueLoop !== false,
434
721
  },
435
722
  executionId,
436
723
  contextId: String(currentContext.id),
437
724
  iteration: iter,
438
725
  });
726
+ await emitThreadEvents({
727
+ silent,
728
+ writable,
729
+ events: [
730
+ {
731
+ type: "step.updated",
732
+ at: nowIso(),
733
+ stepId: String(stepCreate.stepId),
734
+ executionId,
735
+ iteration: iter,
736
+ status: "completed",
737
+ kind: actionRequests?.length ? "action_result" : "message",
738
+ actionName: typeof actionResults?.[0]?.actionRequest?.actionName === "string"
739
+ ? actionResults[0].actionRequest.actionName
740
+ : undefined,
741
+ },
742
+ {
743
+ type: "step.completed",
744
+ at: nowIso(),
745
+ stepId: String(stepCreate.stepId),
746
+ executionId,
747
+ iteration: iter,
748
+ status: "completed",
749
+ },
750
+ ],
751
+ });
752
+ if (continueLoop !== false && reactionEvent) {
753
+ reactionEvent = await updateItem(params.env, reactionEventId, {
754
+ ...reactionEvent,
755
+ status: "pending",
756
+ }, { executionId, contextId: String(currentContext.id) });
757
+ await emitThreadEvents({
758
+ silent,
759
+ writable,
760
+ events: [
761
+ {
762
+ type: "item.updated",
763
+ at: nowIso(),
764
+ itemId: String(reactionEventId),
765
+ contextId: String(currentContext.id),
766
+ threadId: resolvedThreadId,
767
+ executionId,
768
+ status: "pending",
769
+ },
770
+ ],
771
+ });
772
+ }
439
773
  if (continueLoop === false) {
440
774
  await updateItem(params.env, reactionEventId, {
441
775
  ...(reactionEvent ?? assistantEventEffective),
442
776
  status: "completed",
443
777
  }, { executionId, contextId: String(currentContext.id) });
778
+ await emitThreadEvents({
779
+ silent,
780
+ writable,
781
+ events: [
782
+ {
783
+ type: "item.completed",
784
+ at: nowIso(),
785
+ itemId: String(reactionEventId),
786
+ contextId: String(currentContext.id),
787
+ threadId: resolvedThreadId,
788
+ executionId,
789
+ status: "completed",
790
+ },
791
+ ],
792
+ });
444
793
  await completeExecution(params.env, contextSelector, executionId, "completed");
794
+ await emitThreadEvents({
795
+ silent,
796
+ writable,
797
+ events: [
798
+ {
799
+ type: "execution.completed",
800
+ at: nowIso(),
801
+ executionId,
802
+ contextId: String(currentContext.id),
803
+ threadId: resolvedThreadId,
804
+ status: "completed",
805
+ },
806
+ {
807
+ type: "context.closed",
808
+ at: nowIso(),
809
+ contextId: String(currentContext.id),
810
+ threadId: resolvedThreadId,
811
+ status: "closed",
812
+ },
813
+ {
814
+ type: "thread.idle",
815
+ at: nowIso(),
816
+ threadId: resolvedThreadId,
817
+ status: "idle",
818
+ },
819
+ ],
820
+ });
445
821
  if (!silent) {
446
822
  await closeThreadStream({ preventClose, sendFinish, writable });
447
823
  }
@@ -470,6 +846,20 @@ export class Thread {
470
846
  executionId,
471
847
  contextId: String(currentContext.id),
472
848
  });
849
+ await emitThreadEvents({
850
+ silent,
851
+ writable,
852
+ events: [
853
+ {
854
+ type: "step.failed",
855
+ at: nowIso(),
856
+ stepId: String(currentStepId),
857
+ executionId,
858
+ status: "failed",
859
+ errorText: error instanceof Error ? error.message : String(error),
860
+ },
861
+ ],
862
+ });
473
863
  }
474
864
  catch {
475
865
  // noop
@@ -491,8 +881,6 @@ export class Thread {
491
881
  const result = await this.opts.onEnd(lastEvent);
492
882
  if (typeof result === "boolean")
493
883
  return result;
494
- if (result && typeof result === "object" && "end" in result)
495
- return Boolean(result.end);
496
884
  return true;
497
885
  }
498
886
  }