@copilotkit/runtime 1.56.3 → 1.56.4-canary.1777531098
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/dist/agent/converters/tanstack.cjs +121 -25
- package/dist/agent/converters/tanstack.cjs.map +1 -1
- package/dist/agent/converters/tanstack.d.cts.map +1 -1
- package/dist/agent/converters/tanstack.d.mts.map +1 -1
- package/dist/agent/converters/tanstack.mjs +121 -25
- package/dist/agent/converters/tanstack.mjs.map +1 -1
- package/dist/lib/runtime/agent-integrations/langgraph/agent.cjs +8 -1
- package/dist/lib/runtime/agent-integrations/langgraph/agent.cjs.map +1 -1
- package/dist/lib/runtime/agent-integrations/langgraph/agent.d.cts.map +1 -1
- package/dist/lib/runtime/agent-integrations/langgraph/agent.d.mts.map +1 -1
- package/dist/lib/runtime/agent-integrations/langgraph/agent.mjs +8 -1
- package/dist/lib/runtime/agent-integrations/langgraph/agent.mjs.map +1 -1
- package/dist/package.cjs +6 -6
- package/dist/package.mjs +6 -6
- package/dist/v2/index.d.cts +2 -2
- package/dist/v2/index.d.mts +2 -2
- package/dist/v2/runtime/core/fetch-handler.cjs +2 -0
- package/dist/v2/runtime/core/fetch-handler.cjs.map +1 -1
- package/dist/v2/runtime/core/fetch-handler.d.cts.map +1 -1
- package/dist/v2/runtime/core/fetch-handler.d.mts.map +1 -1
- package/dist/v2/runtime/core/fetch-handler.mjs +2 -0
- package/dist/v2/runtime/core/fetch-handler.mjs.map +1 -1
- package/dist/v2/runtime/core/runtime.d.mts +0 -1
- package/dist/v2/runtime/core/runtime.d.mts.map +1 -1
- package/dist/v2/runtime/endpoints/express.cjs +5 -5
- package/dist/v2/runtime/endpoints/express.cjs.map +1 -1
- package/dist/v2/runtime/endpoints/express.mjs +5 -5
- package/dist/v2/runtime/endpoints/express.mjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-connect.cjs +2 -3
- package/dist/v2/runtime/handlers/handle-connect.cjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-connect.mjs +2 -3
- package/dist/v2/runtime/handlers/handle-connect.mjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/connect.cjs +21 -31
- package/dist/v2/runtime/handlers/intelligence/connect.cjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/connect.mjs +22 -31
- package/dist/v2/runtime/handlers/intelligence/connect.mjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/run.cjs +111 -26
- package/dist/v2/runtime/handlers/intelligence/run.cjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/run.mjs +111 -26
- package/dist/v2/runtime/handlers/intelligence/run.mjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/intelligence-utils.cjs +7 -3
- package/dist/v2/runtime/handlers/shared/intelligence-utils.cjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/intelligence-utils.mjs +7 -3
- package/dist/v2/runtime/handlers/shared/intelligence-utils.mjs.map +1 -1
- package/dist/v2/runtime/index.d.cts +1 -1
- package/dist/v2/runtime/index.d.mts +1 -2
- package/dist/v2/runtime/index.d.mts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.cjs +5 -2
- package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.cts +16 -18
- package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.mts +16 -18
- package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.mjs +5 -2
- package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
- package/dist/v2/runtime/runner/agent-runner.cjs.map +1 -1
- package/dist/v2/runtime/runner/agent-runner.d.cts +0 -1
- package/dist/v2/runtime/runner/agent-runner.d.cts.map +1 -1
- package/dist/v2/runtime/runner/agent-runner.d.mts +0 -1
- package/dist/v2/runtime/runner/agent-runner.d.mts.map +1 -1
- package/dist/v2/runtime/runner/agent-runner.mjs.map +1 -1
- package/dist/v2/runtime/runner/index.d.cts +1 -1
- package/dist/v2/runtime/runner/index.d.mts +1 -1
- package/dist/v2/runtime/runner/intelligence.cjs +30 -5
- package/dist/v2/runtime/runner/intelligence.cjs.map +1 -1
- package/dist/v2/runtime/runner/intelligence.d.cts +7 -1
- package/dist/v2/runtime/runner/intelligence.d.cts.map +1 -1
- package/dist/v2/runtime/runner/intelligence.d.mts +7 -1
- package/dist/v2/runtime/runner/intelligence.d.mts.map +1 -1
- package/dist/v2/runtime/runner/intelligence.mjs +30 -5
- package/dist/v2/runtime/runner/intelligence.mjs.map +1 -1
- package/dist/v2/runtime/telemetry/instance-created.cjs +33 -0
- package/dist/v2/runtime/telemetry/instance-created.cjs.map +1 -0
- package/dist/v2/runtime/telemetry/instance-created.mjs +33 -0
- package/dist/v2/runtime/telemetry/instance-created.mjs.map +1 -0
- package/dist/v2/runtime/telemetry/telemetry-client.cjs +1 -38
- package/dist/v2/runtime/telemetry/telemetry-client.cjs.map +1 -1
- package/dist/v2/runtime/telemetry/telemetry-client.mjs +1 -37
- package/dist/v2/runtime/telemetry/telemetry-client.mjs.map +1 -1
- package/package.json +7 -7
- package/src/agent/__tests__/agent-test-helpers.ts +31 -1
- package/src/agent/__tests__/converter-tanstack.test.ts +280 -0
- package/src/agent/converters/tanstack.ts +167 -10
- package/src/lib/runtime/agent-integrations/langgraph/agent.ts +8 -1
- package/src/v2/runtime/__tests__/express-fetch-bridge.test.ts +1 -1
- package/src/v2/runtime/__tests__/express-single-telemetry.integration.test.ts +65 -0
- package/src/v2/runtime/__tests__/express-telemetry.integration.test.ts +101 -0
- package/src/v2/runtime/__tests__/handle-connect.test.ts +155 -48
- package/src/v2/runtime/__tests__/handle-run.test.ts +380 -29
- package/src/v2/runtime/__tests__/hono-single-telemetry.integration.test.ts +46 -0
- package/src/v2/runtime/__tests__/hono-telemetry.integration.test.ts +99 -0
- package/src/v2/runtime/__tests__/intelligence-run-telemetry.test.ts +194 -0
- package/src/v2/runtime/__tests__/sse-response-telemetry.test.ts +108 -0
- package/src/v2/runtime/__tests__/telemetry.test.ts +0 -61
- package/src/v2/runtime/core/fetch-handler.ts +3 -0
- package/src/v2/runtime/endpoints/express.ts +9 -3
- package/src/v2/runtime/handlers/handle-connect.ts +1 -2
- package/src/v2/runtime/handlers/intelligence/connect.ts +48 -68
- package/src/v2/runtime/handlers/intelligence/run.ts +162 -21
- package/src/v2/runtime/handlers/shared/intelligence-utils.ts +21 -1
- package/src/v2/runtime/intelligence-platform/__tests__/client.test.ts +33 -39
- package/src/v2/runtime/intelligence-platform/client.ts +36 -31
- package/src/v2/runtime/runner/__tests__/intelligence-runner.test.ts +15 -7
- package/src/v2/runtime/runner/agent-runner.ts +0 -1
- package/src/v2/runtime/runner/intelligence.ts +47 -6
- package/src/v2/runtime/telemetry/__tests__/instance-created.test.ts +96 -0
- package/src/v2/runtime/telemetry/instance-created.ts +44 -0
- package/src/v2/runtime/telemetry/telemetry-client.ts +1 -57
- package/dist/v2/runtime/intelligence-platform/index.d.mts +0 -2
- package/dist/v2/runtime/telemetry/utils.cjs +0 -15
- package/dist/v2/runtime/telemetry/utils.cjs.map +0 -1
- package/dist/v2/runtime/telemetry/utils.mjs +0 -14
- package/dist/v2/runtime/telemetry/utils.mjs.map +0 -1
- package/src/v2/runtime/telemetry/utils.ts +0 -15
|
@@ -11,6 +11,12 @@ import {
|
|
|
11
11
|
tanstackToolCallStart,
|
|
12
12
|
tanstackToolCallArgs,
|
|
13
13
|
tanstackToolCallEnd,
|
|
14
|
+
tanstackToolCallResult,
|
|
15
|
+
tanstackReasoningStart,
|
|
16
|
+
tanstackReasoningMessageStart,
|
|
17
|
+
tanstackReasoningMessageContent,
|
|
18
|
+
tanstackReasoningMessageEnd,
|
|
19
|
+
tanstackReasoningEnd,
|
|
14
20
|
} from "./agent-test-helpers";
|
|
15
21
|
|
|
16
22
|
describe("TanStack AI converter (via Agent)", () => {
|
|
@@ -312,3 +318,277 @@ describe("TanStack AI converter (via Agent)", () => {
|
|
|
312
318
|
});
|
|
313
319
|
});
|
|
314
320
|
});
|
|
321
|
+
|
|
322
|
+
describe("TanStack AI converter — state tools", () => {
|
|
323
|
+
it("emits STATE_SNAPSHOT before TOOL_CALL_RESULT for AGUISendStateSnapshot", async () => {
|
|
324
|
+
const snapshot = { counter: 5, items: ["x", "y"] };
|
|
325
|
+
const agent = createAgent("tanstack", [
|
|
326
|
+
tanstackToolCallStart("call1", "AGUISendStateSnapshot"),
|
|
327
|
+
tanstackToolCallEnd("call1"),
|
|
328
|
+
tanstackToolCallResult("call1", { success: true, snapshot }),
|
|
329
|
+
]);
|
|
330
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
331
|
+
|
|
332
|
+
expectLifecycleWrapped(events);
|
|
333
|
+
|
|
334
|
+
const snapshotIdx = events.findIndex(
|
|
335
|
+
(e) => e.type === EventType.STATE_SNAPSHOT,
|
|
336
|
+
);
|
|
337
|
+
const resultIdx = events.findIndex(
|
|
338
|
+
(e) => e.type === EventType.TOOL_CALL_RESULT,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
expect(snapshotIdx).toBeGreaterThanOrEqual(0);
|
|
342
|
+
expect(resultIdx).toBeGreaterThanOrEqual(0);
|
|
343
|
+
expect(snapshotIdx).toBeLessThan(resultIdx);
|
|
344
|
+
expect(eventField<unknown>(events[snapshotIdx], "snapshot")).toEqual(
|
|
345
|
+
snapshot,
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("emits STATE_DELTA before TOOL_CALL_RESULT for AGUISendStateDelta", async () => {
|
|
350
|
+
const delta = [{ op: "replace", path: "/counter", value: 7 }];
|
|
351
|
+
const agent = createAgent("tanstack", [
|
|
352
|
+
tanstackToolCallStart("call1", "AGUISendStateDelta"),
|
|
353
|
+
tanstackToolCallEnd("call1"),
|
|
354
|
+
tanstackToolCallResult("call1", { success: true, delta }),
|
|
355
|
+
]);
|
|
356
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
357
|
+
|
|
358
|
+
expectLifecycleWrapped(events);
|
|
359
|
+
|
|
360
|
+
const deltaIdx = events.findIndex((e) => e.type === EventType.STATE_DELTA);
|
|
361
|
+
const resultIdx = events.findIndex(
|
|
362
|
+
(e) => e.type === EventType.TOOL_CALL_RESULT,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
expect(deltaIdx).toBeGreaterThanOrEqual(0);
|
|
366
|
+
expect(deltaIdx).toBeLessThan(resultIdx);
|
|
367
|
+
expect(eventField<unknown>(events[deltaIdx], "delta")).toEqual(delta);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("emits STATE_SNAPSHOT when payload arrives in raw.result instead of raw.content", async () => {
|
|
371
|
+
// Regression: serialization fell back to raw.result (`?? raw.result ?? null`)
|
|
372
|
+
// but state-tool detection only inspected raw.content, so STATE_* events
|
|
373
|
+
// were silently dropped if upstream used `result` for the state-tool body.
|
|
374
|
+
// See the "TOOL_CALL_RESULT with object content serializes to JSON" test
|
|
375
|
+
// above which already exercises the `result` field on a non-state tool.
|
|
376
|
+
const snapshot = { counter: 99 };
|
|
377
|
+
const agent = createAgent("tanstack", [
|
|
378
|
+
tanstackToolCallStart("call1", "AGUISendStateSnapshot"),
|
|
379
|
+
tanstackToolCallEnd("call1"),
|
|
380
|
+
{
|
|
381
|
+
type: "TOOL_CALL_RESULT",
|
|
382
|
+
toolCallId: "call1",
|
|
383
|
+
result: { success: true, snapshot },
|
|
384
|
+
},
|
|
385
|
+
]);
|
|
386
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
387
|
+
|
|
388
|
+
expectLifecycleWrapped(events);
|
|
389
|
+
|
|
390
|
+
const snapshotIdx = events.findIndex(
|
|
391
|
+
(e) => e.type === EventType.STATE_SNAPSHOT,
|
|
392
|
+
);
|
|
393
|
+
expect(snapshotIdx).toBeGreaterThanOrEqual(0);
|
|
394
|
+
expect(eventField<unknown>(events[snapshotIdx], "snapshot")).toEqual(
|
|
395
|
+
snapshot,
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("does NOT emit STATE_* for non-state tool results", async () => {
|
|
400
|
+
const agent = createAgent("tanstack", [
|
|
401
|
+
tanstackToolCallStart("call1", "getWeather"),
|
|
402
|
+
tanstackToolCallEnd("call1"),
|
|
403
|
+
tanstackToolCallResult("call1", {
|
|
404
|
+
snapshot: { spoofed: true },
|
|
405
|
+
delta: [{ op: "x" }],
|
|
406
|
+
}),
|
|
407
|
+
]);
|
|
408
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
409
|
+
|
|
410
|
+
expectLifecycleWrapped(events);
|
|
411
|
+
|
|
412
|
+
expect(
|
|
413
|
+
events.find(
|
|
414
|
+
(e) =>
|
|
415
|
+
e.type === EventType.STATE_SNAPSHOT ||
|
|
416
|
+
e.type === EventType.STATE_DELTA,
|
|
417
|
+
),
|
|
418
|
+
).toBeUndefined();
|
|
419
|
+
expect(
|
|
420
|
+
events.find((e) => e.type === EventType.TOOL_CALL_RESULT),
|
|
421
|
+
).toBeDefined();
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe("TanStack AI converter — reasoning", () => {
|
|
426
|
+
it("emits the full REASONING lifecycle for reasoning chunks", async () => {
|
|
427
|
+
const agent = createAgent("tanstack", [
|
|
428
|
+
tanstackReasoningStart("r1"),
|
|
429
|
+
tanstackReasoningMessageStart("r1"),
|
|
430
|
+
tanstackReasoningMessageContent("r1", "thinking"),
|
|
431
|
+
tanstackReasoningMessageEnd("r1"),
|
|
432
|
+
tanstackReasoningEnd("r1"),
|
|
433
|
+
]);
|
|
434
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
435
|
+
|
|
436
|
+
expectLifecycleWrapped(events);
|
|
437
|
+
|
|
438
|
+
// Strip the lifecycle wrap and inspect the inner sequence.
|
|
439
|
+
const inner = events.slice(1, -1).map((e) => e.type);
|
|
440
|
+
expect(inner).toEqual([
|
|
441
|
+
EventType.REASONING_START,
|
|
442
|
+
EventType.REASONING_MESSAGE_START,
|
|
443
|
+
EventType.REASONING_MESSAGE_CONTENT,
|
|
444
|
+
EventType.REASONING_MESSAGE_END,
|
|
445
|
+
EventType.REASONING_END,
|
|
446
|
+
]);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("auto-closes an open reasoning lifecycle when text starts", async () => {
|
|
450
|
+
const agent = createAgent("tanstack", [
|
|
451
|
+
tanstackReasoningStart("r1"),
|
|
452
|
+
tanstackReasoningMessageStart("r1"),
|
|
453
|
+
tanstackReasoningMessageContent("r1", "thinking"),
|
|
454
|
+
// No REASONING_MESSAGE_END / REASONING_END before text
|
|
455
|
+
tanstackTextChunk("Hi"),
|
|
456
|
+
]);
|
|
457
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
458
|
+
const types = events.map((e) => e.type);
|
|
459
|
+
|
|
460
|
+
const reasonEndIdx = types.indexOf(EventType.REASONING_END);
|
|
461
|
+
const reasonMsgEndIdx = types.indexOf(EventType.REASONING_MESSAGE_END);
|
|
462
|
+
const textIdx = types.indexOf(EventType.TEXT_MESSAGE_CHUNK);
|
|
463
|
+
|
|
464
|
+
expect(reasonMsgEndIdx).toBeGreaterThan(-1);
|
|
465
|
+
expect(reasonEndIdx).toBeGreaterThan(-1);
|
|
466
|
+
expect(reasonEndIdx).toBeLessThan(textIdx);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("auto-closes an open reasoning lifecycle when a tool call starts", async () => {
|
|
470
|
+
const agent = createAgent("tanstack", [
|
|
471
|
+
tanstackReasoningStart("r1"),
|
|
472
|
+
tanstackReasoningMessageStart("r1"),
|
|
473
|
+
tanstackReasoningMessageContent("r1", "..."),
|
|
474
|
+
tanstackToolCallStart("t1", "x"),
|
|
475
|
+
]);
|
|
476
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
477
|
+
const types = events.map((e) => e.type);
|
|
478
|
+
|
|
479
|
+
expect(types).toContain(EventType.REASONING_MESSAGE_END);
|
|
480
|
+
expect(types).toContain(EventType.REASONING_END);
|
|
481
|
+
expect(types.indexOf(EventType.REASONING_END)).toBeLessThan(
|
|
482
|
+
types.indexOf(EventType.TOOL_CALL_START),
|
|
483
|
+
);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("auto-closes when the stream ends without explicit reasoning end", async () => {
|
|
487
|
+
const agent = createAgent("tanstack", [
|
|
488
|
+
tanstackReasoningStart("r1"),
|
|
489
|
+
tanstackReasoningMessageStart("r1"),
|
|
490
|
+
tanstackReasoningMessageContent("r1", "x"),
|
|
491
|
+
]);
|
|
492
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
493
|
+
const types = events.map((e) => e.type);
|
|
494
|
+
|
|
495
|
+
expect(types).toContain(EventType.REASONING_MESSAGE_END);
|
|
496
|
+
expect(types).toContain(EventType.REASONING_END);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("emits REASONING_MESSAGE_END before REASONING_END when upstream sends END with message still open", async () => {
|
|
500
|
+
// Regression: if the converter received REASONING_END while a message
|
|
501
|
+
// was still open, it cleared run-open and emitted END only — leaving
|
|
502
|
+
// message-open true. The next non-reasoning chunk then triggered
|
|
503
|
+
// closeReasoningIfOpen, which emitted MSG_END AFTER END (wrong order).
|
|
504
|
+
// Fix: REASONING_END handler closes any open message first.
|
|
505
|
+
const agent = createAgent("tanstack", [
|
|
506
|
+
tanstackReasoningStart("r1"),
|
|
507
|
+
tanstackReasoningMessageStart("r1"),
|
|
508
|
+
tanstackReasoningMessageContent("r1", "thinking"),
|
|
509
|
+
// Upstream skips MSG_END and goes straight to END
|
|
510
|
+
tanstackReasoningEnd("r1"),
|
|
511
|
+
tanstackTextChunk("Hi"),
|
|
512
|
+
]);
|
|
513
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
514
|
+
const types = events.map((e) => e.type);
|
|
515
|
+
|
|
516
|
+
const msgEndIdx = types.indexOf(EventType.REASONING_MESSAGE_END);
|
|
517
|
+
const endIdx = types.indexOf(EventType.REASONING_END);
|
|
518
|
+
|
|
519
|
+
expect(msgEndIdx).toBeGreaterThan(-1);
|
|
520
|
+
expect(endIdx).toBeGreaterThan(-1);
|
|
521
|
+
expect(msgEndIdx).toBeLessThan(endIdx);
|
|
522
|
+
// No duplicate MSG_END or END from auto-close on the text chunk
|
|
523
|
+
expect(
|
|
524
|
+
types.filter((t) => t === EventType.REASONING_MESSAGE_END),
|
|
525
|
+
).toHaveLength(1);
|
|
526
|
+
expect(types.filter((t) => t === EventType.REASONING_END)).toHaveLength(1);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("auto-closes prior reasoning run when a new REASONING_START arrives without END", async () => {
|
|
530
|
+
// Regression: REASONING_START used to overwrite reasoningMessageId
|
|
531
|
+
// unconditionally, orphaning the prior run's MSG_END / END.
|
|
532
|
+
// Fix: REASONING_START handler calls closeReasoningIfOpen() first.
|
|
533
|
+
const agent = createAgent("tanstack", [
|
|
534
|
+
tanstackReasoningStart("r1"),
|
|
535
|
+
tanstackReasoningMessageStart("r1"),
|
|
536
|
+
tanstackReasoningMessageContent("r1", "first"),
|
|
537
|
+
// Second START with no intervening END
|
|
538
|
+
tanstackReasoningStart("r2"),
|
|
539
|
+
tanstackReasoningMessageStart("r2"),
|
|
540
|
+
tanstackReasoningMessageContent("r2", "second"),
|
|
541
|
+
tanstackReasoningMessageEnd("r2"),
|
|
542
|
+
tanstackReasoningEnd("r2"),
|
|
543
|
+
]);
|
|
544
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
545
|
+
const types = events.map((e) => e.type);
|
|
546
|
+
|
|
547
|
+
// Two complete START → MSG_START → ... → MSG_END → END sequences
|
|
548
|
+
expect(types.filter((t) => t === EventType.REASONING_START)).toHaveLength(
|
|
549
|
+
2,
|
|
550
|
+
);
|
|
551
|
+
expect(types.filter((t) => t === EventType.REASONING_END)).toHaveLength(2);
|
|
552
|
+
expect(
|
|
553
|
+
types.filter((t) => t === EventType.REASONING_MESSAGE_END),
|
|
554
|
+
).toHaveLength(2);
|
|
555
|
+
|
|
556
|
+
// First START's prior message gets closed BEFORE the second START
|
|
557
|
+
const firstEndIdx = types.indexOf(EventType.REASONING_END);
|
|
558
|
+
const secondStartIdx = types.indexOf(
|
|
559
|
+
EventType.REASONING_START,
|
|
560
|
+
firstEndIdx + 1,
|
|
561
|
+
);
|
|
562
|
+
expect(firstEndIdx).toBeLessThan(secondStartIdx);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it("does NOT duplicate REASONING_MESSAGE_END when upstream emits it explicitly before text", async () => {
|
|
566
|
+
// Regression: a single isInReasoning flag conflated message-open with
|
|
567
|
+
// run-open, so closeReasoningIfOpen on TEXT_MESSAGE_CONTENT emitted a
|
|
568
|
+
// second REASONING_MESSAGE_END after upstream's own. Track message-open
|
|
569
|
+
// and run-open separately so closeReasoningIfOpen owes only what's still
|
|
570
|
+
// open.
|
|
571
|
+
const agent = createAgent("tanstack", [
|
|
572
|
+
tanstackReasoningStart("r1"),
|
|
573
|
+
tanstackReasoningMessageStart("r1"),
|
|
574
|
+
tanstackReasoningMessageContent("r1", "thinking"),
|
|
575
|
+
tanstackReasoningMessageEnd("r1"),
|
|
576
|
+
// No explicit REASONING_END before text — closeReasoningIfOpen should
|
|
577
|
+
// emit REASONING_END but NOT a second REASONING_MESSAGE_END.
|
|
578
|
+
tanstackTextChunk("Hi"),
|
|
579
|
+
]);
|
|
580
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
581
|
+
const types = events.map((e) => e.type);
|
|
582
|
+
|
|
583
|
+
const msgEndCount = types.filter(
|
|
584
|
+
(t) => t === EventType.REASONING_MESSAGE_END,
|
|
585
|
+
).length;
|
|
586
|
+
const endCount = types.filter((t) => t === EventType.REASONING_END).length;
|
|
587
|
+
|
|
588
|
+
expect(msgEndCount).toBe(1);
|
|
589
|
+
expect(endCount).toBe(1);
|
|
590
|
+
expect(types.indexOf(EventType.REASONING_END)).toBeLessThan(
|
|
591
|
+
types.indexOf(EventType.TEXT_MESSAGE_CHUNK),
|
|
592
|
+
);
|
|
593
|
+
});
|
|
594
|
+
});
|
|
@@ -8,6 +8,13 @@ import {
|
|
|
8
8
|
ToolCallEndEvent,
|
|
9
9
|
ToolCallStartEvent,
|
|
10
10
|
ToolCallResultEvent,
|
|
11
|
+
StateSnapshotEvent,
|
|
12
|
+
StateDeltaEvent,
|
|
13
|
+
ReasoningStartEvent,
|
|
14
|
+
ReasoningMessageStartEvent,
|
|
15
|
+
ReasoningMessageContentEvent,
|
|
16
|
+
ReasoningMessageEndEvent,
|
|
17
|
+
ReasoningEndEvent,
|
|
11
18
|
} from "@ag-ui/client";
|
|
12
19
|
import { randomUUID } from "@copilotkit/shared";
|
|
13
20
|
|
|
@@ -229,6 +236,44 @@ export async function* convertTanStackStream(
|
|
|
229
236
|
abortSignal: AbortSignal,
|
|
230
237
|
): AsyncGenerator<BaseEvent> {
|
|
231
238
|
const messageId = randomUUID();
|
|
239
|
+
const toolNamesById = new Map<string, string>();
|
|
240
|
+
// Track the reasoning lifecycle at two granularities so closeReasoningIfOpen
|
|
241
|
+
// emits exactly the events still owed. A single boolean conflates the run
|
|
242
|
+
// (REASONING_START → REASONING_END) with the message
|
|
243
|
+
// (REASONING_MESSAGE_START → REASONING_MESSAGE_END) and produces a duplicate
|
|
244
|
+
// REASONING_MESSAGE_END when upstream emits MSG_END but not END before
|
|
245
|
+
// text/tools resume.
|
|
246
|
+
let reasoningRunOpen = false;
|
|
247
|
+
let reasoningMessageOpen = false;
|
|
248
|
+
let reasoningMessageId = randomUUID();
|
|
249
|
+
|
|
250
|
+
function* closeReasoningIfOpen(): Generator<BaseEvent> {
|
|
251
|
+
if (reasoningMessageOpen) {
|
|
252
|
+
reasoningMessageOpen = false;
|
|
253
|
+
const msgEnd: ReasoningMessageEndEvent = {
|
|
254
|
+
type: EventType.REASONING_MESSAGE_END,
|
|
255
|
+
messageId: reasoningMessageId,
|
|
256
|
+
};
|
|
257
|
+
yield msgEnd;
|
|
258
|
+
}
|
|
259
|
+
if (reasoningRunOpen) {
|
|
260
|
+
reasoningRunOpen = false;
|
|
261
|
+
const end: ReasoningEndEvent = {
|
|
262
|
+
type: EventType.REASONING_END,
|
|
263
|
+
messageId: reasoningMessageId,
|
|
264
|
+
};
|
|
265
|
+
yield end;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// TanStack's chat() engine runs a multi-turn agent loop: after the model
|
|
270
|
+
// returns tool calls, the engine tries to execute them and re-prompt. This
|
|
271
|
+
// produces a second round of TOOL_CALL_START / TOOL_CALL_END events that
|
|
272
|
+
// duplicate the ones from the first streaming pass. The CopilotKit runtime
|
|
273
|
+
// handles tool execution externally (via the frontend SDK), so we must stop
|
|
274
|
+
// converting events once the TanStack adapter signals the first turn is
|
|
275
|
+
// complete with RUN_FINISHED.
|
|
276
|
+
let runFinished = false;
|
|
232
277
|
|
|
233
278
|
for await (const chunk of stream) {
|
|
234
279
|
if (abortSignal.aborted) break;
|
|
@@ -236,7 +281,17 @@ export async function* convertTanStackStream(
|
|
|
236
281
|
const raw = chunk as Record<string, unknown>;
|
|
237
282
|
const type = raw.type as string;
|
|
238
283
|
|
|
239
|
-
|
|
284
|
+
// Stop converting after the first RUN_FINISHED — any subsequent events
|
|
285
|
+
// come from TanStack's internal tool-execution loop and would produce
|
|
286
|
+
// duplicate TOOL_CALL_END events that violate the ag-ui verify middleware.
|
|
287
|
+
if (type === "RUN_FINISHED") {
|
|
288
|
+
runFinished = true;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (runFinished) continue;
|
|
292
|
+
|
|
293
|
+
if (type === "TEXT_MESSAGE_CONTENT" && raw.delta != null) {
|
|
294
|
+
yield* closeReasoningIfOpen();
|
|
240
295
|
const textEvent: TextMessageChunkEvent = {
|
|
241
296
|
type: EventType.TEXT_MESSAGE_CHUNK,
|
|
242
297
|
role: "assistant",
|
|
@@ -245,6 +300,8 @@ export async function* convertTanStackStream(
|
|
|
245
300
|
};
|
|
246
301
|
yield textEvent;
|
|
247
302
|
} else if (type === "TOOL_CALL_START") {
|
|
303
|
+
yield* closeReasoningIfOpen();
|
|
304
|
+
toolNamesById.set(raw.toolCallId as string, raw.toolCallName as string);
|
|
248
305
|
const startEvent: ToolCallStartEvent = {
|
|
249
306
|
type: EventType.TOOL_CALL_START,
|
|
250
307
|
parentMessageId: messageId,
|
|
@@ -253,6 +310,7 @@ export async function* convertTanStackStream(
|
|
|
253
310
|
};
|
|
254
311
|
yield startEvent;
|
|
255
312
|
} else if (type === "TOOL_CALL_ARGS") {
|
|
313
|
+
yield* closeReasoningIfOpen();
|
|
256
314
|
const argsEvent: ToolCallArgsEvent = {
|
|
257
315
|
type: EventType.TOOL_CALL_ARGS,
|
|
258
316
|
toolCallId: raw.toolCallId as string,
|
|
@@ -260,35 +318,134 @@ export async function* convertTanStackStream(
|
|
|
260
318
|
};
|
|
261
319
|
yield argsEvent;
|
|
262
320
|
} else if (type === "TOOL_CALL_END") {
|
|
321
|
+
yield* closeReasoningIfOpen();
|
|
263
322
|
const endEvent: ToolCallEndEvent = {
|
|
264
323
|
type: EventType.TOOL_CALL_END,
|
|
265
324
|
toolCallId: raw.toolCallId as string,
|
|
266
325
|
};
|
|
267
326
|
yield endEvent;
|
|
268
327
|
} else if (type === "TOOL_CALL_RESULT") {
|
|
328
|
+
yield* closeReasoningIfOpen();
|
|
329
|
+
const toolCallId = raw.toolCallId as string;
|
|
330
|
+
const toolName = toolNamesById.get(toolCallId);
|
|
331
|
+
// Accept the payload from either `content` (canonical TanStack shape)
|
|
332
|
+
// or `result` (alternate shape used by some adapters / tests). Both
|
|
333
|
+
// state-tool detection and the final TOOL_CALL_RESULT serialization
|
|
334
|
+
// must read the same field, otherwise STATE_SNAPSHOT/STATE_DELTA can
|
|
335
|
+
// be silently dropped when upstream uses `result`.
|
|
336
|
+
const rawPayload = raw.content ?? raw.result;
|
|
337
|
+
|
|
338
|
+
const parsedContent =
|
|
339
|
+
typeof rawPayload === "string" ? safeParse(rawPayload) : rawPayload;
|
|
340
|
+
|
|
341
|
+
if (
|
|
342
|
+
toolName === "AGUISendStateSnapshot" &&
|
|
343
|
+
parsedContent &&
|
|
344
|
+
typeof parsedContent === "object" &&
|
|
345
|
+
"snapshot" in parsedContent
|
|
346
|
+
) {
|
|
347
|
+
const stateSnapshotEvent: StateSnapshotEvent = {
|
|
348
|
+
type: EventType.STATE_SNAPSHOT,
|
|
349
|
+
snapshot: (parsedContent as Record<string, unknown>).snapshot,
|
|
350
|
+
};
|
|
351
|
+
yield stateSnapshotEvent;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (
|
|
355
|
+
toolName === "AGUISendStateDelta" &&
|
|
356
|
+
parsedContent &&
|
|
357
|
+
typeof parsedContent === "object" &&
|
|
358
|
+
"delta" in parsedContent
|
|
359
|
+
) {
|
|
360
|
+
const stateDeltaEvent: StateDeltaEvent = {
|
|
361
|
+
type: EventType.STATE_DELTA,
|
|
362
|
+
delta: (parsedContent as Record<string, unknown>).delta as never,
|
|
363
|
+
};
|
|
364
|
+
yield stateDeltaEvent;
|
|
365
|
+
}
|
|
366
|
+
|
|
269
367
|
let serializedContent: string;
|
|
270
|
-
if (typeof
|
|
271
|
-
serializedContent =
|
|
368
|
+
if (typeof rawPayload === "string") {
|
|
369
|
+
serializedContent = rawPayload;
|
|
272
370
|
} else {
|
|
273
371
|
try {
|
|
274
|
-
serializedContent = JSON.stringify(
|
|
372
|
+
serializedContent = JSON.stringify(rawPayload ?? null);
|
|
275
373
|
} catch {
|
|
276
374
|
serializedContent = "[Unserializable tool result]";
|
|
277
375
|
}
|
|
278
376
|
}
|
|
377
|
+
|
|
279
378
|
const resultEvent: ToolCallResultEvent = {
|
|
280
379
|
type: EventType.TOOL_CALL_RESULT,
|
|
281
380
|
role: "tool",
|
|
282
381
|
messageId: randomUUID(),
|
|
283
|
-
toolCallId
|
|
382
|
+
toolCallId,
|
|
284
383
|
content: serializedContent,
|
|
285
384
|
};
|
|
286
385
|
yield resultEvent;
|
|
386
|
+
toolNamesById.delete(toolCallId);
|
|
387
|
+
} else if (type === "REASONING_START") {
|
|
388
|
+
// If a prior reasoning run is still open (no REASONING_END before this
|
|
389
|
+
// new START), close it cleanly first so MSG_END / END pair correctly.
|
|
390
|
+
yield* closeReasoningIfOpen();
|
|
391
|
+
reasoningRunOpen = true;
|
|
392
|
+
reasoningMessageId = (raw.messageId as string) ?? randomUUID();
|
|
393
|
+
const startEvt: ReasoningStartEvent = {
|
|
394
|
+
type: EventType.REASONING_START,
|
|
395
|
+
messageId: reasoningMessageId,
|
|
396
|
+
};
|
|
397
|
+
yield startEvt;
|
|
398
|
+
} else if (type === "REASONING_MESSAGE_START") {
|
|
399
|
+
reasoningMessageOpen = true;
|
|
400
|
+
const evt: ReasoningMessageStartEvent = {
|
|
401
|
+
type: EventType.REASONING_MESSAGE_START,
|
|
402
|
+
messageId: reasoningMessageId,
|
|
403
|
+
role: "reasoning",
|
|
404
|
+
};
|
|
405
|
+
yield evt;
|
|
406
|
+
} else if (type === "REASONING_MESSAGE_CONTENT") {
|
|
407
|
+
const evt: ReasoningMessageContentEvent = {
|
|
408
|
+
type: EventType.REASONING_MESSAGE_CONTENT,
|
|
409
|
+
messageId: reasoningMessageId,
|
|
410
|
+
delta: raw.delta as string,
|
|
411
|
+
};
|
|
412
|
+
yield evt;
|
|
413
|
+
} else if (type === "REASONING_MESSAGE_END") {
|
|
414
|
+
reasoningMessageOpen = false;
|
|
415
|
+
const evt: ReasoningMessageEndEvent = {
|
|
416
|
+
type: EventType.REASONING_MESSAGE_END,
|
|
417
|
+
messageId: reasoningMessageId,
|
|
418
|
+
};
|
|
419
|
+
yield evt;
|
|
420
|
+
} else if (type === "REASONING_END") {
|
|
421
|
+
// If upstream sends REASONING_END while a message is still open, emit
|
|
422
|
+
// the missing REASONING_MESSAGE_END FIRST so the closing pair stays in
|
|
423
|
+
// order (MSG_END before END). Otherwise the next non-reasoning chunk
|
|
424
|
+
// would trigger closeReasoningIfOpen and emit MSG_END after END.
|
|
425
|
+
if (reasoningMessageOpen) {
|
|
426
|
+
reasoningMessageOpen = false;
|
|
427
|
+
const msgEnd: ReasoningMessageEndEvent = {
|
|
428
|
+
type: EventType.REASONING_MESSAGE_END,
|
|
429
|
+
messageId: reasoningMessageId,
|
|
430
|
+
};
|
|
431
|
+
yield msgEnd;
|
|
432
|
+
}
|
|
433
|
+
reasoningRunOpen = false;
|
|
434
|
+
const evt: ReasoningEndEvent = {
|
|
435
|
+
type: EventType.REASONING_END,
|
|
436
|
+
messageId: reasoningMessageId,
|
|
437
|
+
};
|
|
438
|
+
yield evt;
|
|
287
439
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
yield* closeReasoningIfOpen();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function safeParse(value: string): unknown {
|
|
446
|
+
try {
|
|
447
|
+
return JSON.parse(value);
|
|
448
|
+
} catch {
|
|
449
|
+
return value;
|
|
293
450
|
}
|
|
294
451
|
}
|
|
@@ -154,7 +154,14 @@ export class LangGraphAgent extends AGUILangGraphAgent {
|
|
|
154
154
|
|
|
155
155
|
// @ts-ignore
|
|
156
156
|
run(input: RunAgentInput): Observable<BaseEvent> {
|
|
157
|
-
|
|
157
|
+
const enrichedInput = {
|
|
158
|
+
...input,
|
|
159
|
+
forwardedProps: {
|
|
160
|
+
...input.forwardedProps,
|
|
161
|
+
streamSubgraphs: input.forwardedProps?.streamSubgraphs ?? true,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
return super.run(enrichedInput).pipe(
|
|
158
165
|
map((processedEvent) => {
|
|
159
166
|
// Turn raw event into emit state snapshot from tool call event
|
|
160
167
|
if (processedEvent.type === EventType.RAW) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test: Express single-route adapter (deprecated convenience
|
|
3
|
+
* wrapper) + telemetry. This adapter delegates to createCopilotExpressHandler
|
|
4
|
+
* with mode: "single-route".
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
7
|
+
import type { AbstractAgent } from "@ag-ui/client";
|
|
8
|
+
import { Observable, of } from "rxjs";
|
|
9
|
+
|
|
10
|
+
import { telemetry } from "../telemetry";
|
|
11
|
+
import { createCopilotEndpointSingleRouteExpress } from "../endpoints/express-single";
|
|
12
|
+
import { CopilotRuntime } from "../core/runtime";
|
|
13
|
+
|
|
14
|
+
function makeAgent(): AbstractAgent {
|
|
15
|
+
const a: unknown = { execute: async () => ({ events: [] }) };
|
|
16
|
+
(a as { clone: () => unknown }).clone = () => makeAgent();
|
|
17
|
+
return a as AbstractAgent;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function makeRuntime() {
|
|
21
|
+
const runner = {
|
|
22
|
+
run: () =>
|
|
23
|
+
new Observable((observer) => {
|
|
24
|
+
observer.next({});
|
|
25
|
+
observer.complete();
|
|
26
|
+
return () => undefined;
|
|
27
|
+
}),
|
|
28
|
+
connect: () => of({}),
|
|
29
|
+
stop: async () => true,
|
|
30
|
+
};
|
|
31
|
+
return new CopilotRuntime({
|
|
32
|
+
agents: { default: makeAgent() },
|
|
33
|
+
runner,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("Express single-route adapter — telemetry firing (integration)", () => {
|
|
38
|
+
let captureSpy: ReturnType<typeof vi.spyOn>;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
captureSpy = vi.spyOn(telemetry, "capture").mockResolvedValue(undefined);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
captureSpy.mockRestore();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("fires instance_created on handler creation", async () => {
|
|
49
|
+
const runtime = makeRuntime();
|
|
50
|
+
createCopilotEndpointSingleRouteExpress({
|
|
51
|
+
runtime,
|
|
52
|
+
basePath: "/api/copilotkit",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await vi.waitFor(() => {
|
|
56
|
+
expect(captureSpy).toHaveBeenCalledWith(
|
|
57
|
+
"oss.runtime.instance_created",
|
|
58
|
+
expect.objectContaining({
|
|
59
|
+
agentsAmount: 1,
|
|
60
|
+
"cloud.api_key_provided": false,
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|