@copilotkit/runtime 1.56.5 → 1.57.0-canary.1778078321
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/index.cjs +20 -2
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +9 -16
- package/dist/agent/index.d.cts.map +1 -1
- package/dist/agent/index.d.mts +9 -16
- package/dist/agent/index.d.mts.map +1 -1
- package/dist/agent/index.mjs +21 -3
- package/dist/agent/index.mjs.map +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/v2/index.d.cts +3 -3
- package/dist/v2/index.d.mts +3 -3
- 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/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/run.cjs +10 -1
- package/dist/v2/runtime/handlers/intelligence/run.cjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/run.mjs +10 -1
- package/dist/v2/runtime/handlers/intelligence/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/handlers/shared/agent-utils.cjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/agent-utils.mjs.map +1 -1
- package/dist/v2/runtime/index.d.cts +3 -2
- package/dist/v2/runtime/index.d.cts.map +1 -1
- package/dist/v2/runtime/index.d.mts +3 -2
- package/dist/v2/runtime/index.d.mts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.cjs +40 -0
- package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.cts +83 -0
- package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.mts +83 -0
- package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.mjs +40 -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 +2 -2
- package/src/agent/__tests__/mcp-clients.test.ts +11 -25
- package/src/agent/index.ts +67 -32
- 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/handlers/handle-run.ts +4 -0
- package/src/v2/runtime/handlers/handle-threads.ts +3 -0
- package/src/v2/runtime/handlers/intelligence/run.ts +27 -5
- package/src/v2/runtime/handlers/intelligence/threads.ts +200 -41
- package/src/v2/runtime/handlers/shared/agent-utils.ts +4 -6
- package/src/v2/runtime/index.ts +5 -0
- package/src/v2/runtime/intelligence-platform/__tests__/intelligence-mcp-helper.test.ts +239 -0
- package/src/v2/runtime/intelligence-platform/client.ts +113 -0
- package/src/v2/runtime/runner/__tests__/in-memory-runner.test.ts +417 -3
- package/src/v2/runtime/runner/in-memory.ts +137 -51
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { Observable } from "rxjs";
|
|
2
2
|
import { describe, it, expect, vi } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
AbstractAgent,
|
|
5
|
+
BaseEvent,
|
|
6
|
+
EventType,
|
|
7
|
+
HttpAgent,
|
|
8
|
+
RunAgentInput,
|
|
9
|
+
} from "@ag-ui/client";
|
|
4
10
|
import { A2UIMiddleware } from "@ag-ui/a2ui-middleware";
|
|
5
11
|
import { handleRunAgent } from "../handlers/handle-run";
|
|
6
12
|
import { CopilotRuntime } from "../core/runtime";
|
|
7
13
|
import { IntelligenceAgentRunner } from "../runner/intelligence";
|
|
14
|
+
import { InMemoryAgentRunner } from "../runner/in-memory";
|
|
8
15
|
|
|
9
16
|
describe("handleRunAgent", () => {
|
|
10
17
|
const createMockRuntime = (
|
|
@@ -1289,4 +1296,93 @@ describe("handleRunAgent", () => {
|
|
|
1289
1296
|
}
|
|
1290
1297
|
});
|
|
1291
1298
|
});
|
|
1299
|
+
|
|
1300
|
+
describe("agentId tagging on cloned agents", () => {
|
|
1301
|
+
/**
|
|
1302
|
+
* Pins handle-run.ts:40 — `agent.agentId = agentId` is set on the clone
|
|
1303
|
+
* BEFORE the agent reaches the runner. Without it, InMemoryAgentRunner
|
|
1304
|
+
* falls back to "default" when stamping historic runs, and listThreads
|
|
1305
|
+
* returns rows with the wrong agentId. This breaks the agentId filter
|
|
1306
|
+
* in `GET /threads?agentId=...` for the local-dev fallback.
|
|
1307
|
+
*
|
|
1308
|
+
* This test runs the full flow through InMemoryAgentRunner with an
|
|
1309
|
+
* AbstractAgent whose own `agentId` field is undefined (matches the
|
|
1310
|
+
* shape after `clone()` returns a fresh instance), and asserts the
|
|
1311
|
+
* runner records the registry key, NOT "default".
|
|
1312
|
+
*/
|
|
1313
|
+
class TaggingTestAgent extends AbstractAgent {
|
|
1314
|
+
async runAgent(
|
|
1315
|
+
_input: RunAgentInput,
|
|
1316
|
+
options: { onEvent: (event: { event: BaseEvent }) => void },
|
|
1317
|
+
): Promise<void> {
|
|
1318
|
+
// Emit a single TEXT_MESSAGE_END event so the run produces at least
|
|
1319
|
+
// one event and gets persisted to historicRuns. RUN_STARTED /
|
|
1320
|
+
// RUN_FINISHED are appended by the runner itself.
|
|
1321
|
+
options.onEvent({
|
|
1322
|
+
event: {
|
|
1323
|
+
type: EventType.TEXT_MESSAGE_END,
|
|
1324
|
+
messageId: "msg-1",
|
|
1325
|
+
} as BaseEvent,
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
clone(): AbstractAgent {
|
|
1330
|
+
// The fresh clone has NO agentId — the only way the runner can know
|
|
1331
|
+
// the registry key is if handle-run.ts:40 stamps it before the run.
|
|
1332
|
+
return new TaggingTestAgent();
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
const createRunRequestForAgent = (agentId: string, threadId: string) =>
|
|
1337
|
+
new Request(`https://example.com/agent/${agentId}/run`, {
|
|
1338
|
+
method: "POST",
|
|
1339
|
+
headers: { "Content-Type": "application/json" },
|
|
1340
|
+
body: JSON.stringify({
|
|
1341
|
+
threadId,
|
|
1342
|
+
runId: `run-${threadId}`,
|
|
1343
|
+
state: {},
|
|
1344
|
+
messages: [],
|
|
1345
|
+
tools: [],
|
|
1346
|
+
context: [],
|
|
1347
|
+
forwardedProps: {},
|
|
1348
|
+
}),
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
it("propagates the registry agentId onto historic runs (NOT 'default')", async () => {
|
|
1352
|
+
const runner = new InMemoryAgentRunner();
|
|
1353
|
+
const agent = new TaggingTestAgent();
|
|
1354
|
+
const runtime = new CopilotRuntime({
|
|
1355
|
+
agents: { tagged: agent },
|
|
1356
|
+
runner,
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
// Use a unique threadId so this test does not collide with other
|
|
1360
|
+
// tests that share the InMemoryAgentRunner GLOBAL_STORE.
|
|
1361
|
+
const threadId = `thread-tagged-${Date.now()}-${Math.random()}`;
|
|
1362
|
+
|
|
1363
|
+
const response = await handleRunAgent({
|
|
1364
|
+
runtime,
|
|
1365
|
+
request: createRunRequestForAgent("tagged", threadId),
|
|
1366
|
+
agentId: "tagged",
|
|
1367
|
+
});
|
|
1368
|
+
expect(response.status).toBe(200);
|
|
1369
|
+
|
|
1370
|
+
// Drain the SSE stream so the underlying observable run completes —
|
|
1371
|
+
// historicRuns is only populated AFTER the run finalizes.
|
|
1372
|
+
const reader = response.body!.getReader();
|
|
1373
|
+
while (true) {
|
|
1374
|
+
const { done } = await reader.read();
|
|
1375
|
+
if (done) break;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
const threads = runner.listThreads();
|
|
1379
|
+
const thisThread = threads.find((t) => t.id === threadId);
|
|
1380
|
+
expect(thisThread).toBeDefined();
|
|
1381
|
+
expect(thisThread!.agentId).toBe("tagged");
|
|
1382
|
+
// Negative assertion locks the regression: a future change that drops
|
|
1383
|
+
// the `agent.agentId = agentId` line in handle-run will surface as
|
|
1384
|
+
// "default" here, not as a missing thread.
|
|
1385
|
+
expect(thisThread!.agentId).not.toBe("default");
|
|
1386
|
+
});
|
|
1387
|
+
});
|
|
1292
1388
|
});
|
|
@@ -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
|
});
|