kernl 0.12.3 → 0.12.6

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 (100) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +18 -0
  3. package/README.md +29 -0
  4. package/dist/agent.d.ts.map +1 -1
  5. package/dist/agent.js +2 -0
  6. package/dist/kernl/kernl.d.ts +6 -0
  7. package/dist/kernl/kernl.d.ts.map +1 -1
  8. package/dist/kernl/kernl.js +19 -0
  9. package/dist/kernl/types.d.ts +6 -0
  10. package/dist/kernl/types.d.ts.map +1 -1
  11. package/dist/lib/env.d.ts +2 -2
  12. package/dist/mcp/http.d.ts.map +1 -1
  13. package/dist/mcp/http.js +1 -5
  14. package/dist/mcp/sse.d.ts.map +1 -1
  15. package/dist/mcp/sse.js +1 -5
  16. package/dist/mcp/stdio.d.ts.map +1 -1
  17. package/dist/mcp/stdio.js +1 -5
  18. package/dist/task.d.ts.map +1 -1
  19. package/dist/task.js +0 -1
  20. package/dist/thread/__tests__/thread.test.js +241 -0
  21. package/dist/thread/thread.d.ts +5 -4
  22. package/dist/thread/thread.d.ts.map +1 -1
  23. package/dist/thread/thread.js +91 -22
  24. package/dist/thread/types.d.ts +5 -0
  25. package/dist/thread/types.d.ts.map +1 -1
  26. package/dist/tool/tool.d.ts +2 -2
  27. package/dist/tool/tool.d.ts.map +1 -1
  28. package/dist/tracing/__tests__/composite.test.d.ts +2 -0
  29. package/dist/tracing/__tests__/composite.test.d.ts.map +1 -0
  30. package/dist/tracing/__tests__/composite.test.js +146 -0
  31. package/dist/tracing/__tests__/dispatch.test.d.ts +2 -0
  32. package/dist/tracing/__tests__/dispatch.test.d.ts.map +1 -0
  33. package/dist/tracing/__tests__/dispatch.test.js +160 -0
  34. package/dist/tracing/__tests__/helpers.d.ts +69 -0
  35. package/dist/tracing/__tests__/helpers.d.ts.map +1 -0
  36. package/dist/tracing/__tests__/helpers.js +109 -0
  37. package/dist/tracing/__tests__/integration.test.d.ts +2 -0
  38. package/dist/tracing/__tests__/integration.test.d.ts.map +1 -0
  39. package/dist/tracing/__tests__/integration.test.js +675 -0
  40. package/dist/tracing/__tests__/span.test.d.ts +2 -0
  41. package/dist/tracing/__tests__/span.test.d.ts.map +1 -0
  42. package/dist/tracing/__tests__/span.test.js +188 -0
  43. package/dist/tracing/dispatch.d.ts +43 -0
  44. package/dist/tracing/dispatch.d.ts.map +1 -0
  45. package/dist/tracing/dispatch.js +70 -0
  46. package/dist/tracing/index.d.ts +8 -0
  47. package/dist/tracing/index.d.ts.map +1 -0
  48. package/dist/tracing/index.js +6 -0
  49. package/dist/tracing/span.d.ts +69 -0
  50. package/dist/tracing/span.d.ts.map +1 -0
  51. package/dist/tracing/span.js +64 -0
  52. package/dist/tracing/subscriber.d.ts +53 -0
  53. package/dist/tracing/subscriber.d.ts.map +1 -0
  54. package/dist/tracing/subscriber.js +1 -0
  55. package/dist/tracing/subscribers/composite.d.ts +26 -0
  56. package/dist/tracing/subscribers/composite.d.ts.map +1 -0
  57. package/dist/tracing/subscribers/composite.js +96 -0
  58. package/dist/tracing/subscribers/console.d.ts +22 -0
  59. package/dist/tracing/subscribers/console.d.ts.map +1 -0
  60. package/dist/tracing/subscribers/console.js +82 -0
  61. package/dist/tracing/types.d.ts +77 -0
  62. package/dist/tracing/types.d.ts.map +1 -0
  63. package/dist/tracing/types.js +1 -0
  64. package/package.json +6 -2
  65. package/src/agent.ts +2 -0
  66. package/src/index.ts +1 -0
  67. package/src/kernl/kernl.ts +21 -0
  68. package/src/kernl/types.ts +7 -0
  69. package/src/mcp/http.ts +1 -9
  70. package/src/mcp/sse.ts +1 -10
  71. package/src/mcp/stdio.ts +1 -10
  72. package/src/task.ts +0 -1
  73. package/src/thread/__tests__/thread.test.ts +280 -0
  74. package/src/thread/thread.ts +111 -24
  75. package/src/thread/types.ts +5 -0
  76. package/src/tool/tool.ts +1 -1
  77. package/src/tracing/__tests__/composite.test.ts +218 -0
  78. package/src/tracing/__tests__/dispatch.test.ts +222 -0
  79. package/src/tracing/__tests__/helpers.ts +138 -0
  80. package/src/tracing/__tests__/integration.test.ts +808 -0
  81. package/src/tracing/__tests__/span.test.ts +250 -0
  82. package/src/tracing/dispatch.ts +114 -0
  83. package/src/tracing/index.ts +39 -0
  84. package/src/tracing/span.ts +115 -0
  85. package/src/tracing/subscriber.ts +62 -0
  86. package/src/tracing/subscribers/composite.ts +102 -0
  87. package/src/tracing/subscribers/console.ts +101 -0
  88. package/src/tracing/types.ts +115 -0
  89. package/dist/trace/processor.d.ts +0 -1
  90. package/dist/trace/processor.d.ts.map +0 -1
  91. package/dist/trace/processor.js +0 -1
  92. package/dist/trace/traces.d.ts +0 -1
  93. package/dist/trace/traces.d.ts.map +0 -1
  94. package/dist/trace/traces.js +0 -73
  95. package/dist/trace/utils.d.ts +0 -22
  96. package/dist/trace/utils.d.ts.map +0 -1
  97. package/dist/trace/utils.js +0 -30
  98. package/src/trace/processor.ts +0 -0
  99. package/src/trace/traces.ts +0 -86
  100. package/src/trace/utils.ts +0 -38
@@ -7,6 +7,14 @@ import { Context } from "@/context";
7
7
  import type { Task } from "@/task";
8
8
  import type { ResolvedAgentResponse } from "@/guardrail";
9
9
  import type { ThreadStore } from "@/storage";
10
+ import {
11
+ span,
12
+ event,
13
+ run,
14
+ type Span,
15
+ type ModelCallSpan,
16
+ type ToolCallSpan,
17
+ } from "@/tracing";
10
18
 
11
19
  import { logger } from "@/lib/logger";
12
20
 
@@ -22,6 +30,7 @@ import {
22
30
  LanguageModelStreamEvent,
23
31
  type LanguageModelUsage,
24
32
  type LanguageModelFinishReason,
33
+ type LanguageModelResponseItem,
25
34
  } from "@kernl-sdk/protocol";
26
35
  import { randomID, filter } from "@kernl-sdk/shared/lib";
27
36
 
@@ -111,8 +120,9 @@ export class Thread<
111
120
  private history: ThreadEvent[] /* history representing the event log for the thread */;
112
121
  private tickres?: ResolvedAgentResponse<TOutput>; /* final result from terminal tick */
113
122
 
114
- private abort?: AbortController;
123
+ private _abort?: AbortSignal;
115
124
  private storage?: ThreadStore;
125
+ private _span?: Span; /* tracing span for current execution */
116
126
 
117
127
  constructor(options: ThreadOptions<TContext, TOutput>) {
118
128
  this.tid = options.tid ?? `tid_${randomID()}`;
@@ -134,6 +144,7 @@ export class Thread<
134
144
  this.cpbuf = [];
135
145
  this.persisted = options.persisted ?? false;
136
146
  this.history = options.history ?? [];
147
+ this._abort = options.abort;
137
148
 
138
149
  // seek to latest seq (not persisted)
139
150
  if (this.history.length > 0) {
@@ -168,24 +179,42 @@ export class Thread<
168
179
  * - Exactly one thread.stop (with result on success, error on failure)
169
180
  */
170
181
  async *stream(): AsyncIterable<ThreadStreamEvent> {
171
- if (this.state === RUNNING && this.abort) {
182
+ if (this.state === RUNNING) {
172
183
  throw new Error("thread already running");
173
184
  }
174
185
 
175
186
  this.state = RUNNING;
176
- this.abort = new AbortController();
177
187
  this.tickres = undefined; // reset for this run
178
188
 
179
189
  await this.checkpoint(); /* c1: persist RUNNING state + initial input */
180
190
 
181
- this.emit("thread.start");
191
+ // create thread span (root span for this execution)
192
+ this._span = span(
193
+ {
194
+ kind: "thread",
195
+ threadId: this.tid,
196
+ agentId: this.agent.id,
197
+ namespace: this.namespace,
198
+ context: this.context.context,
199
+ },
200
+ null,
201
+ );
202
+ this._span.enter();
182
203
 
204
+ this.emit("thread.start");
183
205
  yield { kind: "stream.start" }; // always yield start immediately
184
206
 
185
207
  try {
186
208
  yield* this._execute();
209
+ this._span.record({ state: "stopped", result: this.tickres });
187
210
  this.emit("thread.stop", { state: STOPPED, result: this.tickres });
188
211
  } catch (err) {
212
+ this._span.error(err instanceof Error ? err : new Error(String(err)));
213
+ event({
214
+ kind: "thread.error",
215
+ message: err instanceof Error ? err.message : String(err),
216
+ stack: err instanceof Error ? err.stack : undefined,
217
+ });
189
218
  this.emit("thread.stop", {
190
219
  state: STOPPED,
191
220
  error: err instanceof Error ? err.message : String(err),
@@ -193,7 +222,9 @@ export class Thread<
193
222
  throw err;
194
223
  } finally {
195
224
  this.state = STOPPED;
196
- this.abort = undefined;
225
+ this._span.close();
226
+ // (TODO): questionable whether this should be undefined. perhaps a single thread should exit + resume..
227
+ this._span = undefined;
197
228
  await this.checkpoint(); /* c4: final checkpoint - persist STOPPED state */
198
229
  }
199
230
  }
@@ -208,7 +239,7 @@ export class Thread<
208
239
  for (;;) {
209
240
  let err: Error | undefined = undefined;
210
241
 
211
- if (this.abort?.signal.aborted) {
242
+ if (this._abort?.aborted) {
212
243
  return;
213
244
  }
214
245
 
@@ -286,37 +317,70 @@ export class Thread<
286
317
 
287
318
  const req = await this.prepareModelRequest(this.history);
288
319
 
320
+ const s = span<ModelCallSpan>(
321
+ {
322
+ kind: "model.call",
323
+ provider: this.model.provider,
324
+ modelId: this.model.modelId,
325
+ request: {
326
+ input: req.input,
327
+ settings: req.settings,
328
+ responseType: req.responseType,
329
+ tools: req.tools,
330
+ },
331
+ },
332
+ this._span!.id,
333
+ );
334
+ s.enter();
335
+
289
336
  this.emit("model.call.start", { settings: req.settings ?? {} });
290
337
 
338
+ // tracing / observability
339
+ const content: LanguageModelResponseItem[] = [];
291
340
  let usage: LanguageModelUsage | undefined;
292
- let finishReason: LanguageModelFinishReason = { unified: "other", raw: undefined };
341
+ let finishReason: LanguageModelFinishReason = {
342
+ unified: "other",
343
+ raw: undefined,
344
+ };
293
345
 
294
346
  try {
295
347
  if (this.model.stream) {
296
- for await (const event of this.model.stream(req)) {
297
- if (event.kind === "finish") {
298
- usage = event.usage;
299
- finishReason = event.finishReason;
348
+ for await (const e of this.model.stream(req)) {
349
+ if (e.kind === "finish") {
350
+ usage = e.usage;
351
+ finishReason = e.finishReason;
300
352
  }
301
- yield event;
353
+ if (notDelta(e)) content.push(e as LanguageModelResponseItem);
354
+ yield e;
302
355
  }
303
356
  } else {
304
357
  // fallback: blocking generate, yield events as batch
305
358
  const res = await this.model.generate(req);
306
359
  usage = res.usage;
307
360
  finishReason = res.finishReason;
308
- for (const event of res.content) {
309
- yield event;
361
+ for (const e of res.content) {
362
+ content.push(e);
363
+ yield e;
310
364
  }
311
365
  }
312
366
 
367
+ s.record({
368
+ response: {
369
+ content,
370
+ finishReason,
371
+ usage,
372
+ },
373
+ });
313
374
  this.emit("model.call.end", { finishReason, usage });
314
375
  } catch (error) {
376
+ s.error(error instanceof Error ? error : new Error(String(error)));
315
377
  this.emit("model.call.end", { finishReason: "error" });
316
378
  yield {
317
379
  kind: "error",
318
380
  error: error instanceof Error ? error : new Error(String(error)),
319
381
  };
382
+ } finally {
383
+ s.close();
320
384
  }
321
385
  }
322
386
 
@@ -390,12 +454,12 @@ export class Thread<
390
454
  }
391
455
 
392
456
  /**
393
- * Cancel the running thread
457
+ * Abort the running thread.
394
458
  *
395
- * TODO: Emit thread.stop when cancelled (neither result nor error set)
459
+ * @throws {Error} Not implemented - use AbortSignal via options instead
396
460
  */
397
- cancel() {
398
- this.abort?.abort();
461
+ abort() {
462
+ throw new Error("Not implemented: use AbortSignal via ThreadExecuteOptions");
399
463
  }
400
464
 
401
465
  /**
@@ -478,6 +542,18 @@ export class Thread<
478
542
  calls.map(async (call: ToolCall) => {
479
543
  const parsedArgs = JSON.parse(call.arguments || "{}");
480
544
 
545
+ // create tool.call span
546
+ const s = span<ToolCallSpan>(
547
+ {
548
+ kind: "tool.call",
549
+ toolId: call.toolId,
550
+ callId: call.callId,
551
+ args: parsedArgs,
552
+ },
553
+ this._span!.id,
554
+ );
555
+ s.enter();
556
+
481
557
  this.emit("tool.call.start", {
482
558
  toolId: call.toolId,
483
559
  callId: call.callId,
@@ -501,16 +577,20 @@ export class Thread<
501
577
  const ctx = new Context(this.namespace, this.context.context);
502
578
  ctx.agent = this.agent;
503
579
  ctx.approve(call.callId); // mark this call as approved
580
+
504
581
  const res = await tool.invoke(ctx, call.arguments, call.callId);
505
582
 
583
+ s.record({
584
+ state: res.state,
585
+ result: res.result,
586
+ error: res.error,
587
+ });
588
+
506
589
  this.emit("tool.call.end", {
507
590
  toolId: call.toolId,
508
591
  callId: call.callId,
509
592
  state: res.state,
510
- result:
511
- typeof res.result === "string"
512
- ? res.result
513
- : JSON.stringify(res.result),
593
+ result: res.result,
514
594
  error: res.error,
515
595
  });
516
596
 
@@ -523,11 +603,15 @@ export class Thread<
523
603
  error: res.error,
524
604
  };
525
605
  } catch (error) {
606
+ const errMsg = error instanceof Error ? error.message : String(error);
607
+ s.error(error instanceof Error ? error : new Error(errMsg));
608
+ s.record({ state: "failed", error: errMsg });
609
+
526
610
  this.emit("tool.call.end", {
527
611
  toolId: call.toolId,
528
612
  callId: call.callId,
529
613
  state: FAILED,
530
- error: error instanceof Error ? error.message : String(error),
614
+ error: errMsg,
531
615
  });
532
616
 
533
617
  return {
@@ -536,8 +620,10 @@ export class Thread<
536
620
  toolId: call.toolId,
537
621
  state: FAILED,
538
622
  result: undefined as any,
539
- error: error instanceof Error ? error.message : String(error),
623
+ error: errMsg,
540
624
  };
625
+ } finally {
626
+ s.close();
541
627
  }
542
628
  }),
543
629
  );
@@ -598,6 +684,7 @@ export class Thread<
598
684
  settings,
599
685
  tools,
600
686
  responseType,
687
+ abort: this._abort,
601
688
  };
602
689
  }
603
690
  }
@@ -219,6 +219,11 @@ export interface ThreadOptions<
219
219
  * hydrating from a store. Callers creating new threads should omit it.
220
220
  */
221
221
  persisted?: boolean;
222
+ /**
223
+ * Abort signal for cancelling thread execution.
224
+ * When aborted, the thread will stop at the next safe point.
225
+ */
226
+ abort?: AbortSignal;
222
227
  }
223
228
 
224
229
  /**
package/src/tool/tool.ts CHANGED
@@ -84,7 +84,7 @@ export class FunctionTool<
84
84
  readonly description: string;
85
85
  readonly parameters?: TParameters;
86
86
  readonly mode: "blocking" | "async";
87
- private execute: ToolExecuteFunction<TContext, TParameters, TResult>;
87
+ execute: ToolExecuteFunction<TContext, TParameters, TResult>;
88
88
 
89
89
  errorfn: ToolErrorFunction | null;
90
90
  requiresApproval: ToolApprovalFunction<TParameters>;
@@ -0,0 +1,218 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+
3
+ import { CompositeSubscriber } from "../subscribers/composite";
4
+ import { TestSubscriber } from "./helpers";
5
+
6
+ describe("CompositeSubscriber", () => {
7
+ let sub1: TestSubscriber;
8
+ let sub2: TestSubscriber;
9
+ let composite: CompositeSubscriber;
10
+
11
+ beforeEach(() => {
12
+ sub1 = new TestSubscriber();
13
+ sub2 = new TestSubscriber();
14
+ composite = new CompositeSubscriber([sub1, sub2]);
15
+ });
16
+
17
+ describe("enabled", () => {
18
+ it("should return true if any subscriber is enabled", () => {
19
+ sub1.enabledKinds = new Set(["thread"]);
20
+ sub2.enabledKinds = new Set(["model.call"]);
21
+
22
+ expect(composite.enabled({ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" })).toBe(true);
23
+ expect(composite.enabled({ kind: "model.call", provider: "test", modelId: "m1" })).toBe(true);
24
+ });
25
+
26
+ it("should return false if no subscriber is enabled", () => {
27
+ sub1.enabledKinds = new Set(["thread"]);
28
+ sub2.enabledKinds = new Set(["thread"]);
29
+
30
+ expect(composite.enabled({ kind: "model.call", provider: "test", modelId: "m1" })).toBe(false);
31
+ });
32
+
33
+ it("should return true if all subscribers are enabled (default)", () => {
34
+ expect(composite.enabled({ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" })).toBe(true);
35
+ });
36
+ });
37
+
38
+ describe("span", () => {
39
+ it("should create span in all enabled subscribers", () => {
40
+ const data = { kind: "thread" as const, threadId: "t1", agentId: "a1", namespace: "ns" };
41
+ composite.span(data, null);
42
+
43
+ expect(sub1.spans.size).toBe(1);
44
+ expect(sub2.spans.size).toBe(1);
45
+
46
+ const [, s1] = [...sub1.spans.entries()][0];
47
+ const [, s2] = [...sub2.spans.entries()][0];
48
+ expect(s1.data).toEqual(data);
49
+ expect(s2.data).toEqual(data);
50
+ });
51
+
52
+ it("should return composite span ID", () => {
53
+ const id = composite.span(
54
+ { kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" },
55
+ null,
56
+ );
57
+ expect(id).toMatch(/^composite_\d+$/);
58
+ });
59
+
60
+ it("should skip disabled subscribers", () => {
61
+ sub1.enabledKinds = new Set(["thread"]);
62
+ sub2.enabledKinds = new Set(["model.call"]); // not enabled for thread
63
+
64
+ composite.span({ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" }, null);
65
+
66
+ expect(sub1.spans.size).toBe(1);
67
+ expect(sub2.spans.size).toBe(0);
68
+ });
69
+
70
+ it("should map parent span IDs correctly", () => {
71
+ // Create parent
72
+ const parentId = composite.span(
73
+ { kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" },
74
+ null,
75
+ );
76
+
77
+ // Create child with parent
78
+ composite.span({ kind: "model.call", provider: "test", modelId: "m1" }, parentId);
79
+
80
+ // Each subscriber should have correct parent mapping
81
+ const threadSpan1 = sub1.spansOfKind("thread")[0];
82
+ const modelSpan1 = sub1.spansOfKind("model.call")[0];
83
+ expect(modelSpan1.parent).toBe(threadSpan1.id);
84
+
85
+ const threadSpan2 = sub2.spansOfKind("thread")[0];
86
+ const modelSpan2 = sub2.spansOfKind("model.call")[0];
87
+ expect(modelSpan2.parent).toBe(threadSpan2.id);
88
+ });
89
+ });
90
+
91
+ describe("enter / exit", () => {
92
+ it("should dispatch to all subscribers", () => {
93
+ const spanId = composite.span(
94
+ { kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" },
95
+ null,
96
+ );
97
+
98
+ composite.enter(spanId);
99
+ expect(sub1.entered.size).toBe(1);
100
+ expect(sub2.entered.size).toBe(1);
101
+
102
+ composite.exit(spanId);
103
+ expect(sub1.exited.size).toBe(1);
104
+ expect(sub2.exited.size).toBe(1);
105
+ });
106
+
107
+ it("should skip disabled subscribers", () => {
108
+ sub2.enabledKinds = new Set(["model.call"]); // not enabled for thread
109
+
110
+ const spanId = composite.span(
111
+ { kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" },
112
+ null,
113
+ );
114
+
115
+ composite.enter(spanId);
116
+ expect(sub1.entered.size).toBe(1);
117
+ expect(sub2.entered.size).toBe(0);
118
+ });
119
+ });
120
+
121
+ describe("record", () => {
122
+ it("should dispatch to all subscribers", () => {
123
+ const spanId = composite.span(
124
+ { kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" },
125
+ null,
126
+ );
127
+
128
+ composite.record(spanId, { state: "running" } as any);
129
+
130
+ const sub1SpanId = [...sub1.spans.keys()][0];
131
+ const sub2SpanId = [...sub2.spans.keys()][0];
132
+
133
+ expect(sub1.getRecorded(sub1SpanId)).toHaveLength(1);
134
+ expect(sub2.getRecorded(sub2SpanId)).toHaveLength(1);
135
+ });
136
+ });
137
+
138
+ describe("error", () => {
139
+ it("should dispatch to all subscribers", () => {
140
+ const spanId = composite.span(
141
+ { kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" },
142
+ null,
143
+ );
144
+
145
+ const err = new Error("test");
146
+ composite.error(spanId, err);
147
+
148
+ const sub1SpanId = [...sub1.spans.keys()][0];
149
+ const sub2SpanId = [...sub2.spans.keys()][0];
150
+
151
+ expect(sub1.errors.get(sub1SpanId)).toHaveLength(1);
152
+ expect(sub2.errors.get(sub2SpanId)).toHaveLength(1);
153
+ });
154
+ });
155
+
156
+ describe("close", () => {
157
+ it("should dispatch to all subscribers and clean up", () => {
158
+ const spanId = composite.span(
159
+ { kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" },
160
+ null,
161
+ );
162
+
163
+ composite.close(spanId);
164
+
165
+ expect(sub1.closed.size).toBe(1);
166
+ expect(sub2.closed.size).toBe(1);
167
+ });
168
+ });
169
+
170
+ describe("event", () => {
171
+ it("should dispatch to all subscribers", () => {
172
+ composite.event({ kind: "thread.error", message: "test" }, null);
173
+
174
+ expect(sub1.events).toHaveLength(1);
175
+ expect(sub2.events).toHaveLength(1);
176
+ });
177
+
178
+ it("should map parent span IDs correctly", () => {
179
+ const spanId = composite.span(
180
+ { kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" },
181
+ null,
182
+ );
183
+
184
+ composite.event({ kind: "thread.error", message: "test" }, spanId);
185
+
186
+ const sub1SpanId = [...sub1.spans.keys()][0];
187
+ const sub2SpanId = [...sub2.spans.keys()][0];
188
+
189
+ expect(sub1.events[0].parent).toBe(sub1SpanId);
190
+ expect(sub2.events[0].parent).toBe(sub2SpanId);
191
+ });
192
+
193
+ it("should handle null parent", () => {
194
+ composite.event({ kind: "thread.error", message: "test" }, null);
195
+
196
+ expect(sub1.events[0].parent).toBeNull();
197
+ expect(sub2.events[0].parent).toBeNull();
198
+ });
199
+ });
200
+
201
+ describe("flush", () => {
202
+ it("should flush all subscribers", async () => {
203
+ await composite.flush();
204
+
205
+ expect(sub1.calls.some((c) => c.method === "flush")).toBe(true);
206
+ expect(sub2.calls.some((c) => c.method === "flush")).toBe(true);
207
+ });
208
+ });
209
+
210
+ describe("shutdown", () => {
211
+ it("should shutdown all subscribers", async () => {
212
+ await composite.shutdown(5000);
213
+
214
+ expect(sub1.calls.some((c) => c.method === "shutdown")).toBe(true);
215
+ expect(sub2.calls.some((c) => c.method === "shutdown")).toBe(true);
216
+ });
217
+ });
218
+ });