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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +18 -0
- package/README.md +29 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +2 -0
- package/dist/kernl/kernl.d.ts +6 -0
- package/dist/kernl/kernl.d.ts.map +1 -1
- package/dist/kernl/kernl.js +19 -0
- package/dist/kernl/types.d.ts +6 -0
- package/dist/kernl/types.d.ts.map +1 -1
- package/dist/lib/env.d.ts +2 -2
- package/dist/mcp/http.d.ts.map +1 -1
- package/dist/mcp/http.js +1 -5
- package/dist/mcp/sse.d.ts.map +1 -1
- package/dist/mcp/sse.js +1 -5
- package/dist/mcp/stdio.d.ts.map +1 -1
- package/dist/mcp/stdio.js +1 -5
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +0 -1
- package/dist/thread/__tests__/thread.test.js +241 -0
- package/dist/thread/thread.d.ts +5 -4
- package/dist/thread/thread.d.ts.map +1 -1
- package/dist/thread/thread.js +91 -22
- package/dist/thread/types.d.ts +5 -0
- package/dist/thread/types.d.ts.map +1 -1
- package/dist/tool/tool.d.ts +2 -2
- package/dist/tool/tool.d.ts.map +1 -1
- package/dist/tracing/__tests__/composite.test.d.ts +2 -0
- package/dist/tracing/__tests__/composite.test.d.ts.map +1 -0
- package/dist/tracing/__tests__/composite.test.js +146 -0
- package/dist/tracing/__tests__/dispatch.test.d.ts +2 -0
- package/dist/tracing/__tests__/dispatch.test.d.ts.map +1 -0
- package/dist/tracing/__tests__/dispatch.test.js +160 -0
- package/dist/tracing/__tests__/helpers.d.ts +69 -0
- package/dist/tracing/__tests__/helpers.d.ts.map +1 -0
- package/dist/tracing/__tests__/helpers.js +109 -0
- package/dist/tracing/__tests__/integration.test.d.ts +2 -0
- package/dist/tracing/__tests__/integration.test.d.ts.map +1 -0
- package/dist/tracing/__tests__/integration.test.js +675 -0
- package/dist/tracing/__tests__/span.test.d.ts +2 -0
- package/dist/tracing/__tests__/span.test.d.ts.map +1 -0
- package/dist/tracing/__tests__/span.test.js +188 -0
- package/dist/tracing/dispatch.d.ts +43 -0
- package/dist/tracing/dispatch.d.ts.map +1 -0
- package/dist/tracing/dispatch.js +70 -0
- package/dist/tracing/index.d.ts +8 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +6 -0
- package/dist/tracing/span.d.ts +69 -0
- package/dist/tracing/span.d.ts.map +1 -0
- package/dist/tracing/span.js +64 -0
- package/dist/tracing/subscriber.d.ts +53 -0
- package/dist/tracing/subscriber.d.ts.map +1 -0
- package/dist/tracing/subscriber.js +1 -0
- package/dist/tracing/subscribers/composite.d.ts +26 -0
- package/dist/tracing/subscribers/composite.d.ts.map +1 -0
- package/dist/tracing/subscribers/composite.js +96 -0
- package/dist/tracing/subscribers/console.d.ts +22 -0
- package/dist/tracing/subscribers/console.d.ts.map +1 -0
- package/dist/tracing/subscribers/console.js +82 -0
- package/dist/tracing/types.d.ts +77 -0
- package/dist/tracing/types.d.ts.map +1 -0
- package/dist/tracing/types.js +1 -0
- package/package.json +6 -2
- package/src/agent.ts +2 -0
- package/src/index.ts +1 -0
- package/src/kernl/kernl.ts +21 -0
- package/src/kernl/types.ts +7 -0
- package/src/mcp/http.ts +1 -9
- package/src/mcp/sse.ts +1 -10
- package/src/mcp/stdio.ts +1 -10
- package/src/task.ts +0 -1
- package/src/thread/__tests__/thread.test.ts +280 -0
- package/src/thread/thread.ts +111 -24
- package/src/thread/types.ts +5 -0
- package/src/tool/tool.ts +1 -1
- package/src/tracing/__tests__/composite.test.ts +218 -0
- package/src/tracing/__tests__/dispatch.test.ts +222 -0
- package/src/tracing/__tests__/helpers.ts +138 -0
- package/src/tracing/__tests__/integration.test.ts +808 -0
- package/src/tracing/__tests__/span.test.ts +250 -0
- package/src/tracing/dispatch.ts +114 -0
- package/src/tracing/index.ts +39 -0
- package/src/tracing/span.ts +115 -0
- package/src/tracing/subscriber.ts +62 -0
- package/src/tracing/subscribers/composite.ts +102 -0
- package/src/tracing/subscribers/console.ts +101 -0
- package/src/tracing/types.ts +115 -0
- package/dist/trace/processor.d.ts +0 -1
- package/dist/trace/processor.d.ts.map +0 -1
- package/dist/trace/processor.js +0 -1
- package/dist/trace/traces.d.ts +0 -1
- package/dist/trace/traces.d.ts.map +0 -1
- package/dist/trace/traces.js +0 -73
- package/dist/trace/utils.d.ts +0 -22
- package/dist/trace/utils.d.ts.map +0 -1
- package/dist/trace/utils.js +0 -30
- package/src/trace/processor.ts +0 -0
- package/src/trace/traces.ts +0 -86
- package/src/trace/utils.ts +0 -38
package/src/thread/thread.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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 = {
|
|
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
|
|
297
|
-
if (
|
|
298
|
-
usage =
|
|
299
|
-
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
|
-
|
|
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
|
|
309
|
-
|
|
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
|
-
*
|
|
457
|
+
* Abort the running thread.
|
|
394
458
|
*
|
|
395
|
-
*
|
|
459
|
+
* @throws {Error} Not implemented - use AbortSignal via options instead
|
|
396
460
|
*/
|
|
397
|
-
|
|
398
|
-
|
|
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:
|
|
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:
|
|
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
|
}
|
package/src/thread/types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
});
|