@copilotkit/runtime 1.56.4 → 1.56.5-canary.1777671752
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 +16 -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 +17 -1
- package/dist/v2/runtime/core/fetch-handler.mjs.map +1 -1
- package/dist/v2/runtime/core/fetch-router.cjs +18 -1
- package/dist/v2/runtime/core/fetch-router.cjs.map +1 -1
- package/dist/v2/runtime/core/fetch-router.mjs +18 -1
- package/dist/v2/runtime/core/fetch-router.mjs.map +1 -1
- package/dist/v2/runtime/core/hooks.cjs.map +1 -1
- package/dist/v2/runtime/core/hooks.d.cts +8 -0
- package/dist/v2/runtime/core/hooks.d.cts.map +1 -1
- package/dist/v2/runtime/core/hooks.d.mts +8 -0
- package/dist/v2/runtime/core/hooks.d.mts.map +1 -1
- package/dist/v2/runtime/core/hooks.mjs.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-run.cjs +1 -0
- package/dist/v2/runtime/handlers/handle-run.cjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-run.mjs +1 -0
- package/dist/v2/runtime/handlers/handle-run.mjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/threads.cjs +124 -12
- package/dist/v2/runtime/handlers/intelligence/threads.cjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/threads.mjs +122 -13
- package/dist/v2/runtime/handlers/intelligence/threads.mjs.map +1 -1
- package/dist/v2/runtime/index.d.cts +1 -1
- package/dist/v2/runtime/index.d.mts +1 -1
- package/dist/v2/runtime/intelligence-platform/client.cjs +30 -0
- package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.cts +66 -0
- package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.mts +66 -0
- package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.mjs +30 -0
- package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
- package/dist/v2/runtime/runner/in-memory.cjs +94 -22
- package/dist/v2/runtime/runner/in-memory.cjs.map +1 -1
- package/dist/v2/runtime/runner/in-memory.d.cts +65 -2
- package/dist/v2/runtime/runner/in-memory.d.cts.map +1 -1
- package/dist/v2/runtime/runner/in-memory.d.mts +65 -2
- package/dist/v2/runtime/runner/in-memory.d.mts.map +1 -1
- package/dist/v2/runtime/runner/in-memory.mjs +94 -22
- package/dist/v2/runtime/runner/in-memory.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/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__/fetch-handler-validation.test.ts +68 -0
- package/src/v2/runtime/__tests__/fetch-router.test.ts +46 -0
- package/src/v2/runtime/__tests__/handle-run.test.ts +97 -1
- package/src/v2/runtime/__tests__/handle-threads.test.ts +493 -13
- package/src/v2/runtime/core/fetch-handler.ts +19 -0
- package/src/v2/runtime/core/fetch-router.ts +33 -1
- package/src/v2/runtime/core/hooks.ts +3 -0
- package/src/v2/runtime/endpoints/express.ts +9 -3
- package/src/v2/runtime/handlers/handle-run.ts +4 -0
- package/src/v2/runtime/handlers/handle-threads.ts +3 -0
- package/src/v2/runtime/handlers/intelligence/threads.ts +200 -41
- package/src/v2/runtime/intelligence-platform/client.ts +76 -0
- package/src/v2/runtime/runner/__tests__/in-memory-runner.test.ts +417 -3
- package/src/v2/runtime/runner/in-memory.ts +137 -51
|
@@ -2,12 +2,17 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
handleArchiveThread,
|
|
5
|
+
handleClearThreads,
|
|
5
6
|
handleDeleteThread,
|
|
7
|
+
handleGetThreadEvents,
|
|
8
|
+
handleGetThreadMessages,
|
|
9
|
+
handleGetThreadState,
|
|
6
10
|
handleListThreads,
|
|
7
11
|
handleSubscribeToThreads,
|
|
8
12
|
handleUpdateThread,
|
|
9
13
|
} from "../handlers/handle-threads";
|
|
10
14
|
import { CopilotRuntime } from "../core/runtime";
|
|
15
|
+
import { InMemoryAgentRunner } from "../runner/in-memory";
|
|
11
16
|
|
|
12
17
|
describe("thread handlers", () => {
|
|
13
18
|
const createIdentifyUser = () =>
|
|
@@ -47,7 +52,7 @@ describe("thread handlers", () => {
|
|
|
47
52
|
body: JSON.stringify(body),
|
|
48
53
|
});
|
|
49
54
|
|
|
50
|
-
it("returns
|
|
55
|
+
it("returns empty thread list when intelligence is not configured for listThreads", async () => {
|
|
51
56
|
const runtime = new CopilotRuntime({ agents: {} });
|
|
52
57
|
|
|
53
58
|
const response = await handleListThreads({
|
|
@@ -55,10 +60,10 @@ describe("thread handlers", () => {
|
|
|
55
60
|
request: new Request("https://example.com/threads?agentId=agent-1"),
|
|
56
61
|
});
|
|
57
62
|
|
|
58
|
-
expect(response.status).toBe(
|
|
63
|
+
expect(response.status).toBe(200);
|
|
59
64
|
await expect(response.json()).resolves.toEqual({
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
threads: [],
|
|
66
|
+
nextCursor: null,
|
|
62
67
|
});
|
|
63
68
|
});
|
|
64
69
|
|
|
@@ -143,6 +148,16 @@ describe("thread handlers", () => {
|
|
|
143
148
|
|
|
144
149
|
expect(response.status).toBe(500);
|
|
145
150
|
expect(intelligence.ɵsubscribeToThreads).not.toHaveBeenCalled();
|
|
151
|
+
// The handler must log the auth failure so an operator looking at
|
|
152
|
+
// the runtime logs sees why the request 500ed. Asserting the
|
|
153
|
+
// operation name ("identifying intelligence user") catches a
|
|
154
|
+
// regression that swaps the diagnostic for a generic placeholder.
|
|
155
|
+
// The throw originates inside `resolveIntelligenceUser`, which
|
|
156
|
+
// logs and returns 500 before `subscribeToThreads` is reached.
|
|
157
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
158
|
+
expect.stringContaining("Error identifying intelligence user"),
|
|
159
|
+
expect.any(Error),
|
|
160
|
+
);
|
|
146
161
|
} finally {
|
|
147
162
|
errorSpy.mockRestore();
|
|
148
163
|
}
|
|
@@ -357,37 +372,247 @@ describe("thread handlers", () => {
|
|
|
357
372
|
|
|
358
373
|
it("returns 422 when intelligence is not configured for thread mutations", async () => {
|
|
359
374
|
const runtime = new CopilotRuntime({ agents: {} });
|
|
360
|
-
const
|
|
361
|
-
"https://example.com/threads/thread-1",
|
|
362
|
-
|
|
363
|
-
method: "POST",
|
|
375
|
+
const buildRequest = (method: "PATCH" | "POST" | "DELETE") =>
|
|
376
|
+
new Request("https://example.com/threads/thread-1", {
|
|
377
|
+
method,
|
|
364
378
|
headers: { "Content-Type": "application/json" },
|
|
365
379
|
body: JSON.stringify({ userId: "user-1", agentId: "agent-1" }),
|
|
366
|
-
}
|
|
367
|
-
);
|
|
380
|
+
});
|
|
368
381
|
|
|
369
382
|
const updateResponse = await handleUpdateThread({
|
|
370
383
|
runtime,
|
|
371
|
-
request:
|
|
384
|
+
request: buildRequest("PATCH"),
|
|
372
385
|
threadId: "thread-1",
|
|
373
386
|
});
|
|
374
387
|
expect(updateResponse.status).toBe(422);
|
|
375
388
|
|
|
376
389
|
const archiveResponse = await handleArchiveThread({
|
|
377
390
|
runtime,
|
|
378
|
-
request:
|
|
391
|
+
request: buildRequest("POST"),
|
|
379
392
|
threadId: "thread-1",
|
|
380
393
|
});
|
|
381
394
|
expect(archiveResponse.status).toBe(422);
|
|
382
395
|
|
|
396
|
+
// Use the real DELETE method here — Request.clone() preserves the
|
|
397
|
+
// method of the original, so re-using a POST clone for the delete
|
|
398
|
+
// path silently exercises the wrong verb.
|
|
383
399
|
const deleteResponse = await handleDeleteThread({
|
|
384
400
|
runtime,
|
|
385
|
-
request:
|
|
401
|
+
request: buildRequest("DELETE"),
|
|
386
402
|
threadId: "thread-1",
|
|
387
403
|
});
|
|
388
404
|
expect(deleteResponse.status).toBe(422);
|
|
389
405
|
});
|
|
390
406
|
|
|
407
|
+
describe("handleClearThreads", () => {
|
|
408
|
+
// handleClearThreads is intentionally synchronous — it has no I/O on
|
|
409
|
+
// either branch (in-memory map mutation or platform no-op), so it
|
|
410
|
+
// returns a plain Response rather than a Promise. The other handlers
|
|
411
|
+
// in this suite are awaited because they either parse JSON or hit
|
|
412
|
+
// the Intelligence platform; this one does neither.
|
|
413
|
+
it("clears in-memory threads and returns 204 for InMemoryAgentRunner", () => {
|
|
414
|
+
const runner = new InMemoryAgentRunner();
|
|
415
|
+
const clearThreadsSpy = vi.spyOn(runner, "clearThreads");
|
|
416
|
+
const runtime = new CopilotRuntime({ agents: {}, runner });
|
|
417
|
+
|
|
418
|
+
const response = handleClearThreads({
|
|
419
|
+
runtime,
|
|
420
|
+
request: new Request("https://example.com/threads"),
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Lock the synchronous contract: a regression that starts awaiting
|
|
424
|
+
// I/O inside this handler must update the call sites that rely on
|
|
425
|
+
// the synchronous return shape. Asserting `not.toBeInstanceOf(Promise)`
|
|
426
|
+
// catches that drift at runtime.
|
|
427
|
+
expect(response).not.toBeInstanceOf(Promise);
|
|
428
|
+
expect(response.status).toBe(204);
|
|
429
|
+
expect(clearThreadsSpy).toHaveBeenCalledTimes(1);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("returns 204 without touching state when intelligence runtime is configured", () => {
|
|
433
|
+
const intelligence = { listThreads: vi.fn() };
|
|
434
|
+
const runtime = createIntelligenceRuntime({ intelligence });
|
|
435
|
+
|
|
436
|
+
const response = handleClearThreads({
|
|
437
|
+
runtime,
|
|
438
|
+
request: new Request("https://example.com/threads"),
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Same synchronous-contract guard as the in-memory branch above.
|
|
442
|
+
expect(response).not.toBeInstanceOf(Promise);
|
|
443
|
+
expect(response.status).toBe(204);
|
|
444
|
+
expect(intelligence.listThreads).not.toHaveBeenCalled();
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
describe("handleGetThreadMessages", () => {
|
|
449
|
+
it("returns messages from the in-memory runner for a known thread", async () => {
|
|
450
|
+
const runner = new InMemoryAgentRunner();
|
|
451
|
+
vi.spyOn(runner, "getThreadMessages").mockReturnValue([
|
|
452
|
+
{ id: "m1", role: "user", content: "hello" } as never,
|
|
453
|
+
{ id: "m2", role: "assistant", content: "hi there" } as never,
|
|
454
|
+
]);
|
|
455
|
+
const runtime = new CopilotRuntime({ agents: {}, runner });
|
|
456
|
+
|
|
457
|
+
const response = await handleGetThreadMessages({
|
|
458
|
+
runtime,
|
|
459
|
+
request: new Request("https://example.com/threads/thread-1/messages"),
|
|
460
|
+
threadId: "thread-1",
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
expect(response.status).toBe(200);
|
|
464
|
+
const body = await response.json();
|
|
465
|
+
expect(body.messages).toHaveLength(2);
|
|
466
|
+
expect(body.messages[0]).toMatchObject({
|
|
467
|
+
id: "m1",
|
|
468
|
+
role: "user",
|
|
469
|
+
content: "hello",
|
|
470
|
+
});
|
|
471
|
+
expect(body.messages[1]).toMatchObject({
|
|
472
|
+
id: "m2",
|
|
473
|
+
role: "assistant",
|
|
474
|
+
content: "hi there",
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("returns empty messages for an unknown threadId", async () => {
|
|
479
|
+
const runtime = new CopilotRuntime({ agents: {} });
|
|
480
|
+
|
|
481
|
+
const response = await handleGetThreadMessages({
|
|
482
|
+
runtime,
|
|
483
|
+
request: new Request(
|
|
484
|
+
"https://example.com/threads/nonexistent/messages",
|
|
485
|
+
),
|
|
486
|
+
threadId: "nonexistent",
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
expect(response.status).toBe(200);
|
|
490
|
+
const body = await response.json();
|
|
491
|
+
expect(body.messages).toEqual([]);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it("delegates to intelligence.getThreadMessages when intelligence is configured", async () => {
|
|
495
|
+
const intelligence = {
|
|
496
|
+
getThreadMessages: vi
|
|
497
|
+
.fn()
|
|
498
|
+
.mockResolvedValue({ messages: [{ id: "m1" }] }),
|
|
499
|
+
};
|
|
500
|
+
const identifyUser = createIdentifyUser();
|
|
501
|
+
const runtime = createIntelligenceRuntime({ intelligence, identifyUser });
|
|
502
|
+
|
|
503
|
+
const response = await handleGetThreadMessages({
|
|
504
|
+
runtime,
|
|
505
|
+
request: new Request("https://example.com/threads/thread-1/messages"),
|
|
506
|
+
threadId: "thread-1",
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
expect(response.status).toBe(200);
|
|
510
|
+
// The handler must propagate the platform's response body verbatim —
|
|
511
|
+
// assert it explicitly so a regression that swaps in a stubbed body
|
|
512
|
+
// (e.g. `{ messages: [] }`) is caught.
|
|
513
|
+
const body = await response.json();
|
|
514
|
+
expect(body.messages).toEqual([{ id: "m1" }]);
|
|
515
|
+
expect(intelligence.getThreadMessages).toHaveBeenCalledWith({
|
|
516
|
+
threadId: "thread-1",
|
|
517
|
+
});
|
|
518
|
+
expect(identifyUser).toHaveBeenCalledTimes(1);
|
|
519
|
+
expect(identifyUser).toHaveBeenCalledWith(
|
|
520
|
+
expect.objectContaining({ url: expect.stringContaining("thread-1") }),
|
|
521
|
+
);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it("returns 500 when identifyUser throws for getThreadMessages", async () => {
|
|
525
|
+
const intelligence = {
|
|
526
|
+
getThreadMessages: vi.fn(),
|
|
527
|
+
};
|
|
528
|
+
const runtime = createIntelligenceRuntime({
|
|
529
|
+
intelligence,
|
|
530
|
+
identifyUser: vi.fn().mockRejectedValue(new Error("auth failed")),
|
|
531
|
+
});
|
|
532
|
+
// resolveIntelligenceUser logs via console.error on rejection — silence
|
|
533
|
+
// it for the duration of this test so the suite output stays clean,
|
|
534
|
+
// matching the pattern used in the subscribe-throw test above.
|
|
535
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
const response = await handleGetThreadMessages({
|
|
539
|
+
runtime,
|
|
540
|
+
request: new Request("https://example.com/threads/thread-1/messages"),
|
|
541
|
+
threadId: "thread-1",
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
expect(response.status).toBe(500);
|
|
545
|
+
expect(intelligence.getThreadMessages).not.toHaveBeenCalled();
|
|
546
|
+
} finally {
|
|
547
|
+
errorSpy.mockRestore();
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("returns 422 when neither in-memory nor intelligence is configured", async () => {
|
|
552
|
+
// A CopilotRuntime with no runner defaults to InMemoryAgentRunner,
|
|
553
|
+
// so simulate a non-InMemory, non-intelligence setup via a custom runner stub.
|
|
554
|
+
// Use the intelligence path but omit intelligence config.
|
|
555
|
+
const runtime = createIntelligenceRuntime({ intelligence: undefined });
|
|
556
|
+
|
|
557
|
+
const response = await handleGetThreadMessages({
|
|
558
|
+
runtime,
|
|
559
|
+
request: new Request("https://example.com/threads/thread-1/messages"),
|
|
560
|
+
threadId: "thread-1",
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
expect(response.status).toBe(422);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it("maps tool-call and tool-result messages from the in-memory runner without as-never casts", async () => {
|
|
567
|
+
const runner = new InMemoryAgentRunner();
|
|
568
|
+
const messages = [
|
|
569
|
+
{
|
|
570
|
+
id: "m1",
|
|
571
|
+
role: "assistant" as const,
|
|
572
|
+
toolCalls: [
|
|
573
|
+
{
|
|
574
|
+
id: "tc-1",
|
|
575
|
+
type: "function" as const,
|
|
576
|
+
function: { name: "get_weather", arguments: '{"city":"Paris"}' },
|
|
577
|
+
},
|
|
578
|
+
],
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
id: "m2",
|
|
582
|
+
role: "tool" as const,
|
|
583
|
+
toolCallId: "tc-1",
|
|
584
|
+
content: '{"temp":18}',
|
|
585
|
+
},
|
|
586
|
+
];
|
|
587
|
+
vi.spyOn(runner, "getThreadMessages").mockReturnValue(messages);
|
|
588
|
+
const runtime = new CopilotRuntime({ agents: {}, runner });
|
|
589
|
+
|
|
590
|
+
const response = await handleGetThreadMessages({
|
|
591
|
+
runtime,
|
|
592
|
+
request: new Request("https://example.com/threads/thread-1/messages"),
|
|
593
|
+
threadId: "thread-1",
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
expect(response.status).toBe(200);
|
|
597
|
+
const body = await response.json();
|
|
598
|
+
expect(body.messages).toHaveLength(2);
|
|
599
|
+
|
|
600
|
+
const assistantMsg = body.messages[0];
|
|
601
|
+
expect(assistantMsg.role).toBe("assistant");
|
|
602
|
+
expect(assistantMsg.toolCalls).toHaveLength(1);
|
|
603
|
+
expect(assistantMsg.toolCalls[0]).toMatchObject({
|
|
604
|
+
id: "tc-1",
|
|
605
|
+
name: "get_weather",
|
|
606
|
+
args: '{"city":"Paris"}',
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
const toolResultMsg = body.messages[1];
|
|
610
|
+
expect(toolResultMsg.role).toBe("tool");
|
|
611
|
+
expect(toolResultMsg.toolCallId).toBe("tc-1");
|
|
612
|
+
expect(toolResultMsg.content).toBe('{"temp":18}');
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
391
616
|
it("returns 422 when intelligence is not configured for thread subscription", async () => {
|
|
392
617
|
const runtime = new CopilotRuntime({ agents: {} });
|
|
393
618
|
|
|
@@ -447,4 +672,259 @@ describe("thread handlers", () => {
|
|
|
447
672
|
agentId: "agent-1",
|
|
448
673
|
});
|
|
449
674
|
});
|
|
675
|
+
|
|
676
|
+
describe("handleGetThreadEvents", () => {
|
|
677
|
+
it("returns events from the in-memory runner for a known thread", async () => {
|
|
678
|
+
const runner = new InMemoryAgentRunner();
|
|
679
|
+
const fakeEvents = [
|
|
680
|
+
{ type: "RUN_STARTED", runId: "r1", threadId: "thread-1" },
|
|
681
|
+
{
|
|
682
|
+
type: "TEXT_MESSAGE_START",
|
|
683
|
+
messageId: "m1",
|
|
684
|
+
role: "assistant",
|
|
685
|
+
},
|
|
686
|
+
];
|
|
687
|
+
vi.spyOn(runner, "getThreadEvents").mockReturnValue(fakeEvents as never);
|
|
688
|
+
const runtime = new CopilotRuntime({ agents: {}, runner });
|
|
689
|
+
|
|
690
|
+
const response = await handleGetThreadEvents({
|
|
691
|
+
runtime,
|
|
692
|
+
request: new Request("https://example.com/threads/thread-1/events"),
|
|
693
|
+
threadId: "thread-1",
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
expect(response.status).toBe(200);
|
|
697
|
+
const body = await response.json();
|
|
698
|
+
expect(body.events).toHaveLength(2);
|
|
699
|
+
expect(body.events[0]).toMatchObject({ type: "RUN_STARTED" });
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it("returns empty events for an unknown threadId via the in-memory runner", async () => {
|
|
703
|
+
const runtime = new CopilotRuntime({ agents: {} });
|
|
704
|
+
|
|
705
|
+
const response = await handleGetThreadEvents({
|
|
706
|
+
runtime,
|
|
707
|
+
request: new Request("https://example.com/threads/nonexistent/events"),
|
|
708
|
+
threadId: "nonexistent",
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
expect(response.status).toBe(200);
|
|
712
|
+
const body = await response.json();
|
|
713
|
+
expect(body.events).toEqual([]);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it("delegates to intelligence.getThreadEvents when intelligence is configured", async () => {
|
|
717
|
+
// Mirrors the platform's `_inspect/threads/:id/events` response shape
|
|
718
|
+
// (Intelligence PR #144). The handler strips the platform-internal
|
|
719
|
+
// `decodeErrorRowIds` and `truncated` fields before returning, so the
|
|
720
|
+
// wire shape stays `{ events }` to match the in-memory branch.
|
|
721
|
+
const platformEvents = [
|
|
722
|
+
{ type: "RUN_STARTED", threadId: "thread-1", runId: "run-a" },
|
|
723
|
+
{ type: "TEXT_MESSAGE_CONTENT", messageId: "m1", delta: "hello" },
|
|
724
|
+
];
|
|
725
|
+
const intelligence = {
|
|
726
|
+
getThreadEvents: vi.fn().mockResolvedValue({
|
|
727
|
+
events: platformEvents,
|
|
728
|
+
decodeErrorRowIds: [],
|
|
729
|
+
truncated: false,
|
|
730
|
+
}),
|
|
731
|
+
};
|
|
732
|
+
const identifyUser = createIdentifyUser();
|
|
733
|
+
const runtime = createIntelligenceRuntime({ intelligence, identifyUser });
|
|
734
|
+
|
|
735
|
+
const response = await handleGetThreadEvents({
|
|
736
|
+
runtime,
|
|
737
|
+
request: new Request("https://example.com/threads/thread-1/events"),
|
|
738
|
+
threadId: "thread-1",
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
expect(response.status).toBe(200);
|
|
742
|
+
expect(intelligence.getThreadEvents).toHaveBeenCalledWith({
|
|
743
|
+
threadId: "thread-1",
|
|
744
|
+
});
|
|
745
|
+
expect(identifyUser).toHaveBeenCalledTimes(1);
|
|
746
|
+
const body = await response.json();
|
|
747
|
+
expect(body).toEqual({ events: platformEvents });
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it("returns 500 when intelligence.getThreadEvents throws", async () => {
|
|
751
|
+
const intelligence = {
|
|
752
|
+
getThreadEvents: vi
|
|
753
|
+
.fn()
|
|
754
|
+
.mockRejectedValue(new Error("platform unavailable")),
|
|
755
|
+
};
|
|
756
|
+
const runtime = createIntelligenceRuntime({ intelligence });
|
|
757
|
+
|
|
758
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
759
|
+
try {
|
|
760
|
+
const response = await handleGetThreadEvents({
|
|
761
|
+
runtime,
|
|
762
|
+
request: new Request("https://example.com/threads/thread-1/events"),
|
|
763
|
+
threadId: "thread-1",
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
expect(response.status).toBe(500);
|
|
767
|
+
expect(intelligence.getThreadEvents).toHaveBeenCalledTimes(1);
|
|
768
|
+
} finally {
|
|
769
|
+
errorSpy.mockRestore();
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it("returns 500 when the runner throws", async () => {
|
|
774
|
+
const runner = new InMemoryAgentRunner();
|
|
775
|
+
vi.spyOn(runner, "getThreadEvents").mockImplementation(() => {
|
|
776
|
+
throw new Error("boom");
|
|
777
|
+
});
|
|
778
|
+
const runtime = new CopilotRuntime({ agents: {}, runner });
|
|
779
|
+
|
|
780
|
+
const response = await handleGetThreadEvents({
|
|
781
|
+
runtime,
|
|
782
|
+
request: new Request("https://example.com/threads/thread-1/events"),
|
|
783
|
+
threadId: "thread-1",
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
expect(response.status).toBe(500);
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
describe("handleGetThreadState", () => {
|
|
791
|
+
it("returns the state from the in-memory runner", async () => {
|
|
792
|
+
const runner = new InMemoryAgentRunner();
|
|
793
|
+
const snapshot = { counter: 3, label: "alpha" };
|
|
794
|
+
vi.spyOn(runner, "getThreadState").mockReturnValue(snapshot as never);
|
|
795
|
+
const runtime = new CopilotRuntime({ agents: {}, runner });
|
|
796
|
+
|
|
797
|
+
const response = await handleGetThreadState({
|
|
798
|
+
runtime,
|
|
799
|
+
request: new Request("https://example.com/threads/thread-1/state"),
|
|
800
|
+
threadId: "thread-1",
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
expect(response.status).toBe(200);
|
|
804
|
+
const body = await response.json();
|
|
805
|
+
expect(body.state).toEqual(snapshot);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it("returns state:null when the runner has no snapshot for the thread", async () => {
|
|
809
|
+
const runtime = new CopilotRuntime({ agents: {} });
|
|
810
|
+
|
|
811
|
+
const response = await handleGetThreadState({
|
|
812
|
+
runtime,
|
|
813
|
+
request: new Request("https://example.com/threads/nonexistent/state"),
|
|
814
|
+
threadId: "nonexistent",
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
expect(response.status).toBe(200);
|
|
818
|
+
const body = await response.json();
|
|
819
|
+
expect(body.state).toBeNull();
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it("delegates to intelligence.getThreadState and returns the snapshot when intelligence is configured", async () => {
|
|
823
|
+
// Platform returns a discriminated `ThreadStateResult` (Intelligence
|
|
824
|
+
// PR #144). The `snapshot` arm carries the folded current state; the
|
|
825
|
+
// handler flattens it to `{ state }` so the inspector consumes the
|
|
826
|
+
// same shape as the in-memory branch.
|
|
827
|
+
const snapshot = { counter: 7, label: "intel" };
|
|
828
|
+
const intelligence = {
|
|
829
|
+
getThreadState: vi.fn().mockResolvedValue({
|
|
830
|
+
kind: "snapshot",
|
|
831
|
+
state: snapshot,
|
|
832
|
+
skippedDeltas: 0,
|
|
833
|
+
}),
|
|
834
|
+
};
|
|
835
|
+
const identifyUser = createIdentifyUser();
|
|
836
|
+
const runtime = createIntelligenceRuntime({ intelligence, identifyUser });
|
|
837
|
+
|
|
838
|
+
const response = await handleGetThreadState({
|
|
839
|
+
runtime,
|
|
840
|
+
request: new Request("https://example.com/threads/thread-1/state"),
|
|
841
|
+
threadId: "thread-1",
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
expect(response.status).toBe(200);
|
|
845
|
+
expect(intelligence.getThreadState).toHaveBeenCalledWith({
|
|
846
|
+
threadId: "thread-1",
|
|
847
|
+
});
|
|
848
|
+
expect(identifyUser).toHaveBeenCalledTimes(1);
|
|
849
|
+
const body = await response.json();
|
|
850
|
+
expect(body.state).toEqual(snapshot);
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it("returns state:null for the no-snapshot kind from intelligence", async () => {
|
|
854
|
+
const intelligence = {
|
|
855
|
+
getThreadState: vi.fn().mockResolvedValue({ kind: "no-snapshot" }),
|
|
856
|
+
};
|
|
857
|
+
const runtime = createIntelligenceRuntime({ intelligence });
|
|
858
|
+
|
|
859
|
+
const response = await handleGetThreadState({
|
|
860
|
+
runtime,
|
|
861
|
+
request: new Request("https://example.com/threads/thread-1/state"),
|
|
862
|
+
threadId: "thread-1",
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
expect(response.status).toBe(200);
|
|
866
|
+
const body = await response.json();
|
|
867
|
+
expect(body.state).toBeNull();
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
it("returns state:null for the snapshot-decode-error kind from intelligence", async () => {
|
|
871
|
+
// The platform logs the underlying decode failure server-side; from
|
|
872
|
+
// the inspector's perspective, "no readable state" is the same UX as
|
|
873
|
+
// "no snapshot yet."
|
|
874
|
+
const intelligence = {
|
|
875
|
+
getThreadState: vi
|
|
876
|
+
.fn()
|
|
877
|
+
.mockResolvedValue({ kind: "snapshot-decode-error" }),
|
|
878
|
+
};
|
|
879
|
+
const runtime = createIntelligenceRuntime({ intelligence });
|
|
880
|
+
|
|
881
|
+
const response = await handleGetThreadState({
|
|
882
|
+
runtime,
|
|
883
|
+
request: new Request("https://example.com/threads/thread-1/state"),
|
|
884
|
+
threadId: "thread-1",
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
expect(response.status).toBe(200);
|
|
888
|
+
const body = await response.json();
|
|
889
|
+
expect(body.state).toBeNull();
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
it("returns 500 when intelligence.getThreadState throws", async () => {
|
|
893
|
+
const intelligence = {
|
|
894
|
+
getThreadState: vi
|
|
895
|
+
.fn()
|
|
896
|
+
.mockRejectedValue(new Error("platform unavailable")),
|
|
897
|
+
};
|
|
898
|
+
const runtime = createIntelligenceRuntime({ intelligence });
|
|
899
|
+
|
|
900
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
901
|
+
try {
|
|
902
|
+
const response = await handleGetThreadState({
|
|
903
|
+
runtime,
|
|
904
|
+
request: new Request("https://example.com/threads/thread-1/state"),
|
|
905
|
+
threadId: "thread-1",
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
expect(response.status).toBe(500);
|
|
909
|
+
} finally {
|
|
910
|
+
errorSpy.mockRestore();
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
it("returns 500 when the runner throws", async () => {
|
|
915
|
+
const runner = new InMemoryAgentRunner();
|
|
916
|
+
vi.spyOn(runner, "getThreadState").mockImplementation(() => {
|
|
917
|
+
throw new Error("boom");
|
|
918
|
+
});
|
|
919
|
+
const runtime = new CopilotRuntime({ agents: {}, runner });
|
|
920
|
+
|
|
921
|
+
const response = await handleGetThreadState({
|
|
922
|
+
runtime,
|
|
923
|
+
request: new Request("https://example.com/threads/thread-1/state"),
|
|
924
|
+
threadId: "thread-1",
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
expect(response.status).toBe(500);
|
|
928
|
+
});
|
|
929
|
+
});
|
|
450
930
|
});
|
|
@@ -48,12 +48,15 @@ import { handleGetRuntimeInfo } from "../handlers/get-runtime-info";
|
|
|
48
48
|
import { handleTranscribe } from "../handlers/handle-transcribe";
|
|
49
49
|
import { handleDebugEvents } from "../handlers/handle-debug-events";
|
|
50
50
|
import {
|
|
51
|
+
handleClearThreads,
|
|
51
52
|
handleListThreads,
|
|
52
53
|
handleSubscribeToThreads,
|
|
53
54
|
handleUpdateThread,
|
|
54
55
|
handleArchiveThread,
|
|
55
56
|
handleDeleteThread,
|
|
56
57
|
handleGetThreadMessages,
|
|
58
|
+
handleGetThreadEvents,
|
|
59
|
+
handleGetThreadState,
|
|
57
60
|
} from "../handlers/handle-threads";
|
|
58
61
|
import {
|
|
59
62
|
parseMethodCall,
|
|
@@ -318,6 +321,8 @@ function dispatchRoute(
|
|
|
318
321
|
return handleGetRuntimeInfo({ runtime, request });
|
|
319
322
|
case "transcribe":
|
|
320
323
|
return handleTranscribe({ runtime, request });
|
|
324
|
+
case "threads/clear":
|
|
325
|
+
return Promise.resolve(handleClearThreads({ runtime, request }));
|
|
321
326
|
case "threads/list":
|
|
322
327
|
return handleListThreads({ runtime, request });
|
|
323
328
|
case "threads/subscribe":
|
|
@@ -343,6 +348,18 @@ function dispatchRoute(
|
|
|
343
348
|
request,
|
|
344
349
|
threadId: route.threadId,
|
|
345
350
|
});
|
|
351
|
+
case "threads/events":
|
|
352
|
+
return handleGetThreadEvents({
|
|
353
|
+
runtime,
|
|
354
|
+
request,
|
|
355
|
+
threadId: route.threadId,
|
|
356
|
+
});
|
|
357
|
+
case "threads/state":
|
|
358
|
+
return handleGetThreadState({
|
|
359
|
+
runtime,
|
|
360
|
+
request,
|
|
361
|
+
threadId: route.threadId,
|
|
362
|
+
});
|
|
346
363
|
case "cpk-debug-events":
|
|
347
364
|
return Promise.resolve(handleDebugEvents({ runtime, request }));
|
|
348
365
|
}
|
|
@@ -420,6 +437,8 @@ function validateHttpMethod(
|
|
|
420
437
|
case "info":
|
|
421
438
|
case "threads/list":
|
|
422
439
|
case "threads/messages":
|
|
440
|
+
case "threads/events":
|
|
441
|
+
case "threads/state":
|
|
423
442
|
case "cpk-debug-events":
|
|
424
443
|
if (method === "GET") return null;
|
|
425
444
|
return jsonResponse({ error: "Method not allowed" }, 405, {
|
|
@@ -139,6 +139,28 @@ function matchSegments(path: string): RouteInfo | null {
|
|
|
139
139
|
return { method: "threads/messages", threadId };
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// /threads/:threadId/events (3 segments)
|
|
143
|
+
if (
|
|
144
|
+
len >= 3 &&
|
|
145
|
+
segments[len - 3] === "threads" &&
|
|
146
|
+
segments[len - 1] === "events"
|
|
147
|
+
) {
|
|
148
|
+
const threadId = safeDecodeURIComponent(segments[len - 2]!);
|
|
149
|
+
if (!threadId) return null;
|
|
150
|
+
return { method: "threads/events", threadId };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// /threads/:threadId/state (3 segments)
|
|
154
|
+
if (
|
|
155
|
+
len >= 3 &&
|
|
156
|
+
segments[len - 3] === "threads" &&
|
|
157
|
+
segments[len - 1] === "state"
|
|
158
|
+
) {
|
|
159
|
+
const threadId = safeDecodeURIComponent(segments[len - 2]!);
|
|
160
|
+
if (!threadId) return null;
|
|
161
|
+
return { method: "threads/state", threadId };
|
|
162
|
+
}
|
|
163
|
+
|
|
142
164
|
// /threads/:threadId/archive (3 segments)
|
|
143
165
|
if (
|
|
144
166
|
len >= 3 &&
|
|
@@ -150,11 +172,21 @@ function matchSegments(path: string): RouteInfo | null {
|
|
|
150
172
|
return { method: "threads/archive", threadId };
|
|
151
173
|
}
|
|
152
174
|
|
|
175
|
+
// /threads/clear (2 segments) — wipe in-memory thread history
|
|
176
|
+
if (
|
|
177
|
+
len >= 2 &&
|
|
178
|
+
segments[len - 2] === "threads" &&
|
|
179
|
+
segments[len - 1] === "clear"
|
|
180
|
+
) {
|
|
181
|
+
return { method: "threads/clear" };
|
|
182
|
+
}
|
|
183
|
+
|
|
153
184
|
// /threads/:threadId (2 segments) — update or delete
|
|
154
185
|
if (
|
|
155
186
|
len >= 2 &&
|
|
156
187
|
segments[len - 2] === "threads" &&
|
|
157
|
-
segments[len - 1] !== "subscribe"
|
|
188
|
+
segments[len - 1] !== "subscribe" &&
|
|
189
|
+
segments[len - 1] !== "clear"
|
|
158
190
|
) {
|
|
159
191
|
const threadId = safeDecodeURIComponent(segments[len - 1]!);
|
|
160
192
|
if (!threadId) return null;
|
|
@@ -44,6 +44,9 @@ export type RouteInfo =
|
|
|
44
44
|
| { method: "threads/update"; threadId: string }
|
|
45
45
|
| { method: "threads/archive"; threadId: string }
|
|
46
46
|
| { method: "threads/messages"; threadId: string }
|
|
47
|
+
| { method: "threads/events"; threadId: string }
|
|
48
|
+
| { method: "threads/state"; threadId: string }
|
|
49
|
+
| { method: "threads/clear" }
|
|
47
50
|
| { method: "cpk-debug-events" };
|
|
48
51
|
|
|
49
52
|
/* ------------------------------------------------------------------------------------------------
|