@copilotkitnext/runtime 0.0.18 → 0.0.19-threads-and-attachements.1

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/index.mjs CHANGED
@@ -1,7 +1,10 @@
1
+ // src/runtime.ts
2
+ import { logger } from "@copilotkitnext/shared";
3
+
1
4
  // package.json
2
5
  var package_default = {
3
6
  name: "@copilotkitnext/runtime",
4
- version: "0.0.18",
7
+ version: "0.0.19-threads-and-attachements.1",
5
8
  description: "Server-side runtime package for CopilotKit2",
6
9
  main: "dist/index.js",
7
10
  types: "dist/index.d.ts",
@@ -37,9 +40,9 @@ var package_default = {
37
40
  vitest: "^3.0.5"
38
41
  },
39
42
  dependencies: {
40
- "@ag-ui/client": "0.0.40-alpha.6",
41
- "@ag-ui/core": "0.0.40-alpha.6",
42
- "@ag-ui/encoder": "0.0.40-alpha.6",
43
+ "@ag-ui/client": "0.0.40-alpha.7",
44
+ "@ag-ui/core": "0.0.40-alpha.7",
45
+ "@ag-ui/encoder": "0.0.40-alpha.7",
43
46
  "@copilotkitnext/shared": "workspace:*",
44
47
  hono: "^4.6.13",
45
48
  rxjs: "7.8.1"
@@ -65,8 +68,10 @@ import {
65
68
  } from "@ag-ui/client";
66
69
  import { finalizeRunEvents } from "@copilotkitnext/shared";
67
70
  var InMemoryEventStore = class {
68
- constructor(threadId) {
71
+ constructor(threadId, resourceIds, properties) {
69
72
  this.threadId = threadId;
73
+ this.resourceIds = resourceIds;
74
+ this.properties = properties;
70
75
  }
71
76
  /** The subject that current consumers subscribe to. */
72
77
  subject = null;
@@ -86,11 +91,41 @@ var InMemoryEventStore = class {
86
91
  currentEvents = null;
87
92
  };
88
93
  var GLOBAL_STORE = /* @__PURE__ */ new Map();
94
+ function matchesScope(store, scope) {
95
+ if (scope === void 0 || scope === null) {
96
+ return true;
97
+ }
98
+ const scopeIds = Array.isArray(scope.resourceId) ? scope.resourceId : [scope.resourceId];
99
+ return scopeIds.some((scopeId) => store.resourceIds.includes(scopeId));
100
+ }
89
101
  var InMemoryAgentRunner = class extends AgentRunner {
90
102
  run(request) {
91
103
  let existingStore = GLOBAL_STORE.get(request.threadId);
92
- if (!existingStore) {
93
- existingStore = new InMemoryEventStore(request.threadId);
104
+ if (!existingStore && request.scope === null) {
105
+ throw new Error(
106
+ "Cannot create thread with null scope. Admin users must specify an explicit resourceId for the thread owner."
107
+ );
108
+ }
109
+ let resourceIds;
110
+ if (request.scope === void 0) {
111
+ resourceIds = ["global"];
112
+ } else if (request.scope === null) {
113
+ resourceIds = [];
114
+ } else if (Array.isArray(request.scope.resourceId)) {
115
+ if (request.scope.resourceId.length === 0) {
116
+ throw new Error("Invalid scope: resourceId array cannot be empty");
117
+ }
118
+ resourceIds = request.scope.resourceId;
119
+ } else {
120
+ resourceIds = [request.scope.resourceId];
121
+ }
122
+ if (existingStore) {
123
+ if (request.scope !== null && !matchesScope(existingStore, request.scope)) {
124
+ throw new Error("Unauthorized: Cannot run on thread owned by different resource");
125
+ }
126
+ resourceIds = existingStore.resourceIds;
127
+ } else {
128
+ existingStore = new InMemoryEventStore(request.threadId, resourceIds, request.scope?.properties);
94
129
  GLOBAL_STORE.set(request.threadId, existingStore);
95
130
  }
96
131
  const store = existingStore;
@@ -134,9 +169,7 @@ var InMemoryAgentRunner = class extends AgentRunner {
134
169
  if (event.type === EventType.RUN_STARTED) {
135
170
  const runStartedEvent = event;
136
171
  if (!runStartedEvent.input) {
137
- const sanitizedMessages = request.input.messages ? request.input.messages.filter(
138
- (message) => !historicMessageIds.has(message.id)
139
- ) : void 0;
172
+ const sanitizedMessages = request.input.messages ? request.input.messages.filter((message) => !historicMessageIds.has(message.id)) : void 0;
140
173
  const updatedInput = {
141
174
  ...request.input,
142
175
  ...sanitizedMessages !== void 0 ? { messages: sanitizedMessages } : {}
@@ -233,7 +266,7 @@ var InMemoryAgentRunner = class extends AgentRunner {
233
266
  connect(request) {
234
267
  const store = GLOBAL_STORE.get(request.threadId);
235
268
  const connectionSubject = new ReplaySubject(Infinity);
236
- if (!store) {
269
+ if (!store || !matchesScope(store, request.scope)) {
237
270
  connectionSubject.complete();
238
271
  return connectionSubject.asObservable();
239
272
  }
@@ -269,54 +302,256 @@ var InMemoryAgentRunner = class extends AgentRunner {
269
302
  const store = GLOBAL_STORE.get(request.threadId);
270
303
  return Promise.resolve(store?.isRunning ?? false);
271
304
  }
272
- stop(request) {
305
+ async stop(request) {
273
306
  const store = GLOBAL_STORE.get(request.threadId);
274
- if (!store || !store.isRunning) {
275
- return Promise.resolve(false);
276
- }
277
- if (store.stopRequested) {
278
- return Promise.resolve(false);
307
+ if (!store) {
308
+ return false;
279
309
  }
280
- store.stopRequested = true;
281
- store.isRunning = false;
282
- const agent = store.agent;
283
- if (!agent) {
284
- store.stopRequested = false;
310
+ if (store.isRunning) {
311
+ store.stopRequested = true;
285
312
  store.isRunning = false;
286
- return Promise.resolve(false);
313
+ const agent = store.agent;
314
+ try {
315
+ if (agent) {
316
+ agent.abortRun();
317
+ return true;
318
+ }
319
+ return false;
320
+ } catch (error) {
321
+ console.warn("Failed to abort in-memory runner:", error);
322
+ store.stopRequested = false;
323
+ store.isRunning = true;
324
+ return false;
325
+ }
287
326
  }
288
- try {
289
- agent.abortRun();
290
- return Promise.resolve(true);
291
- } catch (error) {
292
- console.error("Failed to abort agent run", error);
293
- store.stopRequested = false;
294
- store.isRunning = true;
295
- return Promise.resolve(false);
327
+ return false;
328
+ }
329
+ async listThreads(request) {
330
+ const limit = request.limit ?? 50;
331
+ const offset = request.offset ?? 0;
332
+ if (request.scope !== void 0 && request.scope !== null) {
333
+ const scopeIds = Array.isArray(request.scope.resourceId) ? request.scope.resourceId : [request.scope.resourceId];
334
+ if (scopeIds.length === 0) {
335
+ return { threads: [], total: 0 };
336
+ }
337
+ }
338
+ const threadInfos = [];
339
+ for (const [threadId, store] of GLOBAL_STORE.entries()) {
340
+ if (threadId.includes("-suggestions-")) {
341
+ continue;
342
+ }
343
+ if (!matchesScope(store, request.scope)) {
344
+ continue;
345
+ }
346
+ if (store.historicRuns.length === 0) {
347
+ continue;
348
+ }
349
+ const firstRun = store.historicRuns[0];
350
+ const lastRun = store.historicRuns[store.historicRuns.length - 1];
351
+ if (!firstRun || !lastRun) {
352
+ continue;
353
+ }
354
+ threadInfos.push({
355
+ threadId,
356
+ createdAt: firstRun.createdAt,
357
+ lastActivityAt: lastRun.createdAt,
358
+ store
359
+ });
360
+ }
361
+ threadInfos.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
362
+ const total = threadInfos.length;
363
+ const paginatedInfos = threadInfos.slice(offset, offset + limit);
364
+ const threads = paginatedInfos.map((info) => {
365
+ let firstMessage;
366
+ const firstRun = info.store.historicRuns[0];
367
+ if (firstRun) {
368
+ const textContent = firstRun.events.find((e) => e.type === EventType.TEXT_MESSAGE_CONTENT);
369
+ if (textContent?.delta) {
370
+ firstMessage = textContent.delta.substring(0, 100);
371
+ }
372
+ }
373
+ const messageIds = /* @__PURE__ */ new Set();
374
+ for (const run of info.store.historicRuns) {
375
+ for (const event of run.events) {
376
+ if ("messageId" in event && typeof event.messageId === "string") {
377
+ messageIds.add(event.messageId);
378
+ }
379
+ }
380
+ }
381
+ return {
382
+ threadId: info.threadId,
383
+ createdAt: info.createdAt,
384
+ lastActivityAt: info.lastActivityAt,
385
+ isRunning: info.store.isRunning,
386
+ messageCount: messageIds.size,
387
+ firstMessage,
388
+ resourceId: info.store.resourceIds[0] || "unknown",
389
+ // Return first for backward compatibility
390
+ properties: info.store.properties
391
+ };
392
+ });
393
+ return { threads, total };
394
+ }
395
+ async getThreadMetadata(threadId, scope) {
396
+ const store = GLOBAL_STORE.get(threadId);
397
+ if (!store || !matchesScope(store, scope) || store.historicRuns.length === 0) {
398
+ return null;
399
+ }
400
+ const firstRun = store.historicRuns[0];
401
+ const lastRun = store.historicRuns[store.historicRuns.length - 1];
402
+ if (!firstRun || !lastRun) {
403
+ return null;
404
+ }
405
+ let firstMessage;
406
+ const textContent = firstRun.events.find((e) => e.type === EventType.TEXT_MESSAGE_CONTENT);
407
+ if (textContent?.delta) {
408
+ firstMessage = textContent.delta.substring(0, 100);
409
+ }
410
+ const messageIds = /* @__PURE__ */ new Set();
411
+ for (const run of store.historicRuns) {
412
+ for (const event of run.events) {
413
+ if ("messageId" in event && typeof event.messageId === "string") {
414
+ messageIds.add(event.messageId);
415
+ }
416
+ }
417
+ }
418
+ return {
419
+ threadId,
420
+ createdAt: firstRun.createdAt,
421
+ lastActivityAt: lastRun.createdAt,
422
+ isRunning: store.isRunning,
423
+ messageCount: messageIds.size,
424
+ firstMessage,
425
+ resourceId: store.resourceIds[0] || "unknown",
426
+ // Return first for backward compatibility
427
+ properties: store.properties
428
+ };
429
+ }
430
+ async deleteThread(threadId, scope) {
431
+ const store = GLOBAL_STORE.get(threadId);
432
+ if (!store || !matchesScope(store, scope)) {
433
+ return;
434
+ }
435
+ if (store.agent) {
436
+ try {
437
+ store.agent.abortRun();
438
+ } catch (error) {
439
+ console.warn("Failed to abort agent during thread deletion:", error);
440
+ }
441
+ }
442
+ store.subject?.complete();
443
+ GLOBAL_STORE.delete(threadId);
444
+ }
445
+ /**
446
+ * Clear all threads from the global store (for testing purposes only)
447
+ * @internal
448
+ */
449
+ clearAllThreads() {
450
+ for (const [threadId, store] of GLOBAL_STORE.entries()) {
451
+ if (store.agent) {
452
+ try {
453
+ store.agent.abortRun();
454
+ } catch (error) {
455
+ console.warn("Failed to abort agent during clearAllThreads:", error);
456
+ }
457
+ }
458
+ store.subject?.complete();
459
+ GLOBAL_STORE.delete(threadId);
296
460
  }
297
461
  }
298
462
  };
299
463
 
300
464
  // src/runtime.ts
301
465
  var VERSION = package_default.version;
302
- var CopilotRuntime = class {
466
+ var CopilotRuntime = class _CopilotRuntime {
467
+ /**
468
+ * Built-in global scope for single-user apps or demos.
469
+ *
470
+ * All threads are globally accessible when using this scope.
471
+ *
472
+ * @example
473
+ * ```typescript
474
+ * new CopilotRuntime({
475
+ * agents: { myAgent },
476
+ * resolveThreadsScope: CopilotRuntime.GLOBAL_SCOPE,
477
+ * suppressResourceIdWarning: true
478
+ * });
479
+ * ```
480
+ */
481
+ static GLOBAL_SCOPE = async (context) => ({
482
+ resourceId: "global"
483
+ });
484
+ /**
485
+ * Parses the client-declared resource ID(s) from the request header.
486
+ *
487
+ * This is a utility method used internally by handlers to extract the
488
+ * `X-CopilotKit-Resource-ID` header sent by the client via `CopilotKitProvider`.
489
+ *
490
+ * **You typically don't need to call this directly** - it's automatically called
491
+ * by the runtime handlers and passed to your `resolveThreadsScope` function as
492
+ * the `clientDeclared` parameter.
493
+ *
494
+ * @param request - The incoming HTTP request
495
+ * @returns The parsed resource ID(s), or undefined if header is missing
496
+ * - Returns a string if single ID
497
+ * - Returns an array if multiple comma-separated IDs
498
+ * - Returns undefined if header not present
499
+ *
500
+ * @example
501
+ * ```typescript
502
+ * // Automatically used internally:
503
+ * const clientDeclared = CopilotRuntime.parseClientDeclaredResourceId(request);
504
+ * const scope = await runtime.resolveThreadsScope({ request, clientDeclared });
505
+ * ```
506
+ */
507
+ static parseClientDeclaredResourceId(request) {
508
+ const header = request.headers.get("X-CopilotKit-Resource-ID");
509
+ if (!header) {
510
+ return void 0;
511
+ }
512
+ const values = header.split(",").map((v) => decodeURIComponent(v.trim()));
513
+ return values.length === 1 ? values[0] : values;
514
+ }
303
515
  agents;
304
516
  transcriptionService;
305
517
  beforeRequestMiddleware;
306
518
  afterRequestMiddleware;
307
519
  runner;
520
+ resolveThreadsScope;
521
+ suppressResourceIdWarning;
308
522
  constructor({
309
523
  agents,
310
524
  transcriptionService,
311
525
  beforeRequestMiddleware,
312
526
  afterRequestMiddleware,
313
- runner
527
+ runner,
528
+ resolveThreadsScope,
529
+ suppressResourceIdWarning = false
314
530
  }) {
315
531
  this.agents = agents;
316
532
  this.transcriptionService = transcriptionService;
317
533
  this.beforeRequestMiddleware = beforeRequestMiddleware;
318
534
  this.afterRequestMiddleware = afterRequestMiddleware;
319
535
  this.runner = runner ?? new InMemoryAgentRunner();
536
+ this.resolveThreadsScope = resolveThreadsScope ?? _CopilotRuntime.GLOBAL_SCOPE;
537
+ this.suppressResourceIdWarning = suppressResourceIdWarning;
538
+ if (!resolveThreadsScope && !suppressResourceIdWarning) {
539
+ this.logGlobalScopeWarning();
540
+ }
541
+ }
542
+ logGlobalScopeWarning() {
543
+ const isProduction = process.env.NODE_ENV === "production";
544
+ if (isProduction) {
545
+ logger.error({
546
+ msg: "CopilotKit Security Warning: GLOBAL_SCOPE in production",
547
+ details: "No resolveThreadsScope configured. All threads are globally accessible to all users. Configure authentication for production: https://docs.copilotkit.ai/security/thread-scoping To suppress this warning (if intentional), set suppressResourceIdWarning: true"
548
+ });
549
+ } else {
550
+ logger.warn({
551
+ msg: "CopilotKit: Using GLOBAL_SCOPE",
552
+ details: "No resolveThreadsScope configured. All threads are globally accessible. This is fine for development, but add authentication for production: https://docs.copilotkit.ai/security/thread-scoping"
553
+ });
554
+ }
320
555
  }
321
556
  };
322
557
 
@@ -325,15 +560,9 @@ import { Hono } from "hono";
325
560
  import { cors } from "hono/cors";
326
561
 
327
562
  // src/handlers/handle-run.ts
328
- import {
329
- RunAgentInputSchema
330
- } from "@ag-ui/client";
563
+ import { RunAgentInputSchema } from "@ag-ui/client";
331
564
  import { EventEncoder } from "@ag-ui/encoder";
332
- async function handleRunAgent({
333
- runtime,
334
- request,
335
- agentId
336
- }) {
565
+ async function handleRunAgent({ runtime, request, agentId }) {
337
566
  try {
338
567
  const agents = await runtime.agents;
339
568
  if (!agents[agentId]) {
@@ -370,6 +599,23 @@ async function handleRunAgent({
370
599
  const writer = stream.writable.getWriter();
371
600
  const encoder = new EventEncoder();
372
601
  let streamClosed = false;
602
+ let subscription;
603
+ let abortListener;
604
+ const cleanupAbortListener = () => {
605
+ if (abortListener) {
606
+ request.signal.removeEventListener("abort", abortListener);
607
+ abortListener = void 0;
608
+ }
609
+ };
610
+ const closeStream = async () => {
611
+ if (!streamClosed) {
612
+ try {
613
+ await writer.close();
614
+ } catch {
615
+ }
616
+ streamClosed = true;
617
+ }
618
+ };
373
619
  (async () => {
374
620
  let input;
375
621
  try {
@@ -383,13 +629,26 @@ async function handleRunAgent({
383
629
  { status: 400 }
384
630
  );
385
631
  }
632
+ const clientDeclared = CopilotRuntime["parseClientDeclaredResourceId"](request);
633
+ const scope = await runtime.resolveThreadsScope({ request, clientDeclared });
634
+ if (scope === void 0) {
635
+ throw new Error("Unauthorized: No resource scope provided");
636
+ }
386
637
  agent.setMessages(input.messages);
387
638
  agent.setState(input.state);
388
639
  agent.threadId = input.threadId;
389
- runtime.runner.run({
640
+ const stopRunner = async () => {
641
+ try {
642
+ await runtime.runner.stop({ threadId: input.threadId });
643
+ } catch (stopError) {
644
+ console.error("Error stopping runner:", stopError);
645
+ }
646
+ };
647
+ subscription = runtime.runner.run({
390
648
  threadId: input.threadId,
391
649
  agent,
392
- input
650
+ input,
651
+ scope
393
652
  }).subscribe({
394
653
  next: async (event) => {
395
654
  if (!request.signal.aborted && !streamClosed) {
@@ -404,42 +663,39 @@ async function handleRunAgent({
404
663
  },
405
664
  error: async (error) => {
406
665
  console.error("Error running agent:", error);
407
- if (!streamClosed) {
408
- try {
409
- await writer.close();
410
- streamClosed = true;
411
- } catch {
412
- }
413
- }
666
+ cleanupAbortListener();
667
+ await closeStream();
414
668
  },
415
669
  complete: async () => {
416
- if (!streamClosed) {
417
- try {
418
- await writer.close();
419
- streamClosed = true;
420
- } catch {
421
- }
422
- }
670
+ cleanupAbortListener();
671
+ await closeStream();
423
672
  }
424
673
  });
674
+ const handleAbort = () => {
675
+ subscription?.unsubscribe();
676
+ subscription = void 0;
677
+ cleanupAbortListener();
678
+ void stopRunner();
679
+ void closeStream();
680
+ };
681
+ if (request.signal.aborted) {
682
+ handleAbort();
683
+ } else {
684
+ abortListener = handleAbort;
685
+ request.signal.addEventListener("abort", abortListener);
686
+ }
425
687
  })().catch((error) => {
426
688
  console.error("Error running agent:", error);
427
- console.error(
428
- "Error stack:",
429
- error instanceof Error ? error.stack : "No stack trace"
430
- );
689
+ console.error("Error stack:", error instanceof Error ? error.stack : "No stack trace");
431
690
  console.error("Error details:", {
432
691
  name: error instanceof Error ? error.name : "Unknown",
433
692
  message: error instanceof Error ? error.message : String(error),
434
693
  cause: error instanceof Error ? error.cause : void 0
435
694
  });
436
- if (!streamClosed) {
437
- try {
438
- writer.close();
439
- streamClosed = true;
440
- } catch {
441
- }
442
- }
695
+ subscription?.unsubscribe();
696
+ subscription = void 0;
697
+ cleanupAbortListener();
698
+ void closeStream();
443
699
  });
444
700
  return new Response(stream.readable, {
445
701
  status: 200,
@@ -451,10 +707,7 @@ async function handleRunAgent({
451
707
  });
452
708
  } catch (error) {
453
709
  console.error("Error running agent:", error);
454
- console.error(
455
- "Error stack:",
456
- error instanceof Error ? error.stack : "No stack trace"
457
- );
710
+ console.error("Error stack:", error instanceof Error ? error.stack : "No stack trace");
458
711
  console.error("Error details:", {
459
712
  name: error instanceof Error ? error.name : "Unknown",
460
713
  message: error instanceof Error ? error.message : String(error),
@@ -612,10 +865,10 @@ async function handleTranscribe({
612
865
  }
613
866
 
614
867
  // src/endpoint.ts
615
- import { logger as logger2 } from "@copilotkitnext/shared";
868
+ import { logger as logger3 } from "@copilotkitnext/shared";
616
869
 
617
870
  // src/middleware.ts
618
- import { logger } from "@copilotkitnext/shared";
871
+ import { logger as logger2 } from "@copilotkitnext/shared";
619
872
  async function callBeforeRequestMiddleware({
620
873
  runtime,
621
874
  request,
@@ -626,7 +879,7 @@ async function callBeforeRequestMiddleware({
626
879
  if (typeof mw === "function") {
627
880
  return mw({ runtime, request, path });
628
881
  }
629
- logger.warn({ mw }, "Unsupported beforeRequestMiddleware value \u2013 skipped");
882
+ logger2.warn({ mw }, "Unsupported beforeRequestMiddleware value \u2013 skipped");
630
883
  return;
631
884
  }
632
885
  async function callAfterRequestMiddleware({
@@ -639,17 +892,13 @@ async function callAfterRequestMiddleware({
639
892
  if (typeof mw === "function") {
640
893
  return mw({ runtime, response, path });
641
894
  }
642
- logger.warn({ mw }, "Unsupported afterRequestMiddleware value \u2013 skipped");
895
+ logger2.warn({ mw }, "Unsupported afterRequestMiddleware value \u2013 skipped");
643
896
  }
644
897
 
645
898
  // src/handlers/handle-connect.ts
646
899
  import { RunAgentInputSchema as RunAgentInputSchema2 } from "@ag-ui/client";
647
900
  import { EventEncoder as EventEncoder2 } from "@ag-ui/encoder";
648
- async function handleConnectAgent({
649
- runtime,
650
- request,
651
- agentId
652
- }) {
901
+ async function handleConnectAgent({ runtime, request, agentId }) {
653
902
  try {
654
903
  const agents = await runtime.agents;
655
904
  if (!agents[agentId]) {
@@ -668,6 +917,23 @@ async function handleConnectAgent({
668
917
  const writer = stream.writable.getWriter();
669
918
  const encoder = new EventEncoder2();
670
919
  let streamClosed = false;
920
+ let subscription;
921
+ let abortListener;
922
+ const cleanupAbortListener = () => {
923
+ if (abortListener) {
924
+ request.signal.removeEventListener("abort", abortListener);
925
+ abortListener = void 0;
926
+ }
927
+ };
928
+ const closeStream = async () => {
929
+ if (!streamClosed) {
930
+ try {
931
+ await writer.close();
932
+ } catch {
933
+ }
934
+ streamClosed = true;
935
+ }
936
+ };
671
937
  (async () => {
672
938
  let input;
673
939
  try {
@@ -681,8 +947,14 @@ async function handleConnectAgent({
681
947
  { status: 400 }
682
948
  );
683
949
  }
684
- runtime.runner.connect({
685
- threadId: input.threadId
950
+ const clientDeclared = CopilotRuntime["parseClientDeclaredResourceId"](request);
951
+ const scope = await runtime.resolveThreadsScope({ request, clientDeclared });
952
+ if (scope === void 0) {
953
+ throw new Error("Unauthorized: No resource scope provided");
954
+ }
955
+ subscription = runtime.runner.connect({
956
+ threadId: input.threadId,
957
+ scope
686
958
  }).subscribe({
687
959
  next: async (event) => {
688
960
  if (!request.signal.aborted && !streamClosed) {
@@ -697,42 +969,38 @@ async function handleConnectAgent({
697
969
  },
698
970
  error: async (error) => {
699
971
  console.error("Error running agent:", error);
700
- if (!streamClosed) {
701
- try {
702
- await writer.close();
703
- streamClosed = true;
704
- } catch {
705
- }
706
- }
972
+ cleanupAbortListener();
973
+ await closeStream();
707
974
  },
708
975
  complete: async () => {
709
- if (!streamClosed) {
710
- try {
711
- await writer.close();
712
- streamClosed = true;
713
- } catch {
714
- }
715
- }
976
+ cleanupAbortListener();
977
+ await closeStream();
716
978
  }
717
979
  });
980
+ const handleAbort = () => {
981
+ subscription?.unsubscribe();
982
+ subscription = void 0;
983
+ cleanupAbortListener();
984
+ void closeStream();
985
+ };
986
+ if (request.signal.aborted) {
987
+ handleAbort();
988
+ } else {
989
+ abortListener = handleAbort;
990
+ request.signal.addEventListener("abort", abortListener);
991
+ }
718
992
  })().catch((error) => {
719
993
  console.error("Error running agent:", error);
720
- console.error(
721
- "Error stack:",
722
- error instanceof Error ? error.stack : "No stack trace"
723
- );
994
+ console.error("Error stack:", error instanceof Error ? error.stack : "No stack trace");
724
995
  console.error("Error details:", {
725
996
  name: error instanceof Error ? error.name : "Unknown",
726
997
  message: error instanceof Error ? error.message : String(error),
727
998
  cause: error instanceof Error ? error.cause : void 0
728
999
  });
729
- if (!streamClosed) {
730
- try {
731
- writer.close();
732
- streamClosed = true;
733
- } catch {
734
- }
735
- }
1000
+ subscription?.unsubscribe();
1001
+ subscription = void 0;
1002
+ cleanupAbortListener();
1003
+ void closeStream();
736
1004
  });
737
1005
  return new Response(stream.readable, {
738
1006
  status: 200,
@@ -744,10 +1012,7 @@ async function handleConnectAgent({
744
1012
  });
745
1013
  } catch (error) {
746
1014
  console.error("Error running agent:", error);
747
- console.error(
748
- "Error stack:",
749
- error instanceof Error ? error.stack : "No stack trace"
750
- );
1015
+ console.error("Error stack:", error instanceof Error ? error.stack : "No stack trace");
751
1016
  console.error("Error details:", {
752
1017
  name: error instanceof Error ? error.name : "Unknown",
753
1018
  message: error instanceof Error ? error.message : String(error),
@@ -788,7 +1053,35 @@ async function handleStopAgent({
788
1053
  }
789
1054
  );
790
1055
  }
791
- const stopped = await runtime.runner.stop({ threadId });
1056
+ const clientDeclared = CopilotRuntime["parseClientDeclaredResourceId"](request);
1057
+ const scope = await runtime.resolveThreadsScope({ request, clientDeclared });
1058
+ if (scope === void 0) {
1059
+ return new Response(
1060
+ JSON.stringify({
1061
+ error: "Unauthorized",
1062
+ message: "No resource scope provided"
1063
+ }),
1064
+ {
1065
+ status: 401,
1066
+ headers: { "Content-Type": "application/json" }
1067
+ }
1068
+ );
1069
+ }
1070
+ const runner = await runtime.runner;
1071
+ const metadata = await runner.getThreadMetadata(threadId, scope);
1072
+ if (!metadata) {
1073
+ return new Response(
1074
+ JSON.stringify({
1075
+ error: "Thread not found",
1076
+ message: `Thread '${threadId}' does not exist or you don't have access`
1077
+ }),
1078
+ {
1079
+ status: 404,
1080
+ headers: { "Content-Type": "application/json" }
1081
+ }
1082
+ );
1083
+ }
1084
+ const stopped = await runner.stop({ threadId });
792
1085
  if (!stopped) {
793
1086
  return new Response(
794
1087
  JSON.stringify({
@@ -830,6 +1123,141 @@ async function handleStopAgent({
830
1123
  }
831
1124
  }
832
1125
 
1126
+ // src/handlers/handle-threads.ts
1127
+ async function handleListThreads({ runtime, request }) {
1128
+ try {
1129
+ const clientDeclared = CopilotRuntime["parseClientDeclaredResourceId"](request);
1130
+ const scope = await runtime.resolveThreadsScope({ request, clientDeclared });
1131
+ if (scope === void 0) {
1132
+ return new Response(
1133
+ JSON.stringify({
1134
+ error: "Unauthorized",
1135
+ message: "No resource scope provided"
1136
+ }),
1137
+ {
1138
+ status: 401,
1139
+ headers: { "Content-Type": "application/json" }
1140
+ }
1141
+ );
1142
+ }
1143
+ const url = new URL(request.url);
1144
+ const limitParam = url.searchParams.get("limit");
1145
+ const offsetParam = url.searchParams.get("offset");
1146
+ const parsedLimit = limitParam ? Number.parseInt(limitParam, 10) : NaN;
1147
+ const parsedOffset = offsetParam ? Number.parseInt(offsetParam, 10) : NaN;
1148
+ const limit = Math.max(1, Math.min(100, Number.isNaN(parsedLimit) ? 20 : parsedLimit));
1149
+ const offset = Math.max(0, Number.isNaN(parsedOffset) ? 0 : parsedOffset);
1150
+ const runner = await runtime.runner;
1151
+ const result = await runner.listThreads({ scope, limit, offset });
1152
+ return new Response(JSON.stringify(result), {
1153
+ status: 200,
1154
+ headers: { "Content-Type": "application/json" }
1155
+ });
1156
+ } catch (error) {
1157
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
1158
+ return new Response(
1159
+ JSON.stringify({
1160
+ error: "Failed to list threads",
1161
+ message: errorMessage
1162
+ }),
1163
+ {
1164
+ status: 500,
1165
+ headers: { "Content-Type": "application/json" }
1166
+ }
1167
+ );
1168
+ }
1169
+ }
1170
+ async function handleGetThread({ runtime, threadId, request }) {
1171
+ try {
1172
+ const clientDeclared = CopilotRuntime["parseClientDeclaredResourceId"](request);
1173
+ const scope = await runtime.resolveThreadsScope({ request, clientDeclared });
1174
+ if (scope === void 0) {
1175
+ return new Response(
1176
+ JSON.stringify({
1177
+ error: "Unauthorized",
1178
+ message: "No resource scope provided"
1179
+ }),
1180
+ {
1181
+ status: 401,
1182
+ headers: { "Content-Type": "application/json" }
1183
+ }
1184
+ );
1185
+ }
1186
+ const runner = await runtime.runner;
1187
+ const metadata = await runner.getThreadMetadata(threadId, scope);
1188
+ if (!metadata) {
1189
+ return new Response(
1190
+ JSON.stringify({
1191
+ error: "Thread not found",
1192
+ message: `Thread '${threadId}' does not exist`
1193
+ }),
1194
+ {
1195
+ status: 404,
1196
+ headers: { "Content-Type": "application/json" }
1197
+ }
1198
+ );
1199
+ }
1200
+ return new Response(JSON.stringify(metadata), {
1201
+ status: 200,
1202
+ headers: { "Content-Type": "application/json" }
1203
+ });
1204
+ } catch (error) {
1205
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
1206
+ return new Response(
1207
+ JSON.stringify({
1208
+ error: "Failed to get thread",
1209
+ message: errorMessage
1210
+ }),
1211
+ {
1212
+ status: 500,
1213
+ headers: { "Content-Type": "application/json" }
1214
+ }
1215
+ );
1216
+ }
1217
+ }
1218
+ async function handleDeleteThread({ runtime, threadId, request }) {
1219
+ if (!threadId) {
1220
+ return new Response(JSON.stringify({ error: "Thread ID required" }), {
1221
+ status: 400,
1222
+ headers: { "Content-Type": "application/json" }
1223
+ });
1224
+ }
1225
+ try {
1226
+ const clientDeclared = CopilotRuntime["parseClientDeclaredResourceId"](request);
1227
+ const scope = await runtime.resolveThreadsScope({ request, clientDeclared });
1228
+ if (scope === void 0) {
1229
+ return new Response(
1230
+ JSON.stringify({
1231
+ error: "Unauthorized",
1232
+ message: "No resource scope provided"
1233
+ }),
1234
+ {
1235
+ status: 401,
1236
+ headers: { "Content-Type": "application/json" }
1237
+ }
1238
+ );
1239
+ }
1240
+ const runner = await runtime.runner;
1241
+ await runner.deleteThread(threadId, scope);
1242
+ return new Response(JSON.stringify({ success: true }), {
1243
+ status: 200,
1244
+ headers: { "Content-Type": "application/json" }
1245
+ });
1246
+ } catch (error) {
1247
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
1248
+ return new Response(
1249
+ JSON.stringify({
1250
+ error: "Failed to delete thread",
1251
+ message: errorMessage
1252
+ }),
1253
+ {
1254
+ status: 500,
1255
+ headers: { "Content-Type": "application/json" }
1256
+ }
1257
+ );
1258
+ }
1259
+ }
1260
+
833
1261
  // src/endpoint.ts
834
1262
  function createCopilotEndpoint({ runtime, basePath }) {
835
1263
  const app = new Hono();
@@ -853,7 +1281,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
853
1281
  c.set("modifiedRequest", maybeModifiedRequest);
854
1282
  }
855
1283
  } catch (error) {
856
- logger2.error({ err: error, url: request.url, path }, "Error running before request middleware");
1284
+ logger3.error({ err: error, url: request.url, path }, "Error running before request middleware");
857
1285
  if (error instanceof Response) {
858
1286
  return error;
859
1287
  }
@@ -869,7 +1297,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
869
1297
  response,
870
1298
  path
871
1299
  }).catch((error) => {
872
- logger2.error({ err: error, url: c.req.url, path }, "Error running after request middleware");
1300
+ logger3.error({ err: error, url: c.req.url, path }, "Error running after request middleware");
873
1301
  });
874
1302
  }).post("/agent/:agentId/run", async (c) => {
875
1303
  const agentId = c.req.param("agentId");
@@ -881,7 +1309,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
881
1309
  agentId
882
1310
  });
883
1311
  } catch (error) {
884
- logger2.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
1312
+ logger3.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
885
1313
  throw error;
886
1314
  }
887
1315
  }).post("/agent/:agentId/connect", async (c) => {
@@ -894,7 +1322,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
894
1322
  agentId
895
1323
  });
896
1324
  } catch (error) {
897
- logger2.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
1325
+ logger3.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
898
1326
  throw error;
899
1327
  }
900
1328
  }).post("/agent/:agentId/stop/:threadId", async (c) => {
@@ -909,7 +1337,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
909
1337
  threadId
910
1338
  });
911
1339
  } catch (error) {
912
- logger2.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
1340
+ logger3.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
913
1341
  throw error;
914
1342
  }
915
1343
  }).get("/info", async (c) => {
@@ -920,7 +1348,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
920
1348
  request
921
1349
  });
922
1350
  } catch (error) {
923
- logger2.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
1351
+ logger3.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
924
1352
  throw error;
925
1353
  }
926
1354
  }).post("/transcribe", async (c) => {
@@ -931,7 +1359,44 @@ function createCopilotEndpoint({ runtime, basePath }) {
931
1359
  request
932
1360
  });
933
1361
  } catch (error) {
934
- logger2.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
1362
+ logger3.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
1363
+ throw error;
1364
+ }
1365
+ }).get("/threads", async (c) => {
1366
+ const request = c.get("modifiedRequest") || c.req.raw;
1367
+ try {
1368
+ return await handleListThreads({
1369
+ runtime,
1370
+ request
1371
+ });
1372
+ } catch (error) {
1373
+ logger3.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
1374
+ throw error;
1375
+ }
1376
+ }).get("/threads/:threadId", async (c) => {
1377
+ const threadId = c.req.param("threadId");
1378
+ const request = c.get("modifiedRequest") || c.req.raw;
1379
+ try {
1380
+ return await handleGetThread({
1381
+ runtime,
1382
+ request,
1383
+ threadId
1384
+ });
1385
+ } catch (error) {
1386
+ logger3.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
1387
+ throw error;
1388
+ }
1389
+ }).delete("/threads/:threadId", async (c) => {
1390
+ const threadId = c.req.param("threadId");
1391
+ const request = c.get("modifiedRequest") || c.req.raw;
1392
+ try {
1393
+ return await handleDeleteThread({
1394
+ runtime,
1395
+ request,
1396
+ threadId
1397
+ });
1398
+ } catch (error) {
1399
+ logger3.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
935
1400
  throw error;
936
1401
  }
937
1402
  }).notFound((c) => {
@@ -941,12 +1406,55 @@ function createCopilotEndpoint({ runtime, basePath }) {
941
1406
 
942
1407
  // src/runner/index.ts
943
1408
  import { finalizeRunEvents as finalizeRunEvents2 } from "@copilotkitnext/shared";
1409
+
1410
+ // src/resource-id-helpers.ts
1411
+ function validateResourceIdMatch(clientDeclared, serverAuthorized) {
1412
+ if (!clientDeclared) {
1413
+ return;
1414
+ }
1415
+ const clientIds = Array.isArray(clientDeclared) ? clientDeclared : [clientDeclared];
1416
+ const authorizedIds = Array.isArray(serverAuthorized) ? serverAuthorized : [serverAuthorized];
1417
+ const hasMatch = clientIds.some((clientId) => authorizedIds.includes(clientId));
1418
+ if (!hasMatch) {
1419
+ throw new Error("Unauthorized: Client-declared resourceId does not match authenticated user");
1420
+ }
1421
+ }
1422
+ function filterAuthorizedResourceIds(clientDeclared, serverAuthorized) {
1423
+ const authorizedIds = Array.isArray(serverAuthorized) ? serverAuthorized : [serverAuthorized];
1424
+ if (!clientDeclared) {
1425
+ return serverAuthorized;
1426
+ }
1427
+ const clientIds = Array.isArray(clientDeclared) ? clientDeclared : [clientDeclared];
1428
+ const filtered = clientIds.filter((id) => authorizedIds.includes(id));
1429
+ if (filtered.length === 0) {
1430
+ throw new Error("Unauthorized: None of the client-declared resourceIds are authorized");
1431
+ }
1432
+ return Array.isArray(clientDeclared) ? filtered : filtered[0];
1433
+ }
1434
+ function createStrictThreadScopeResolver(getUserId) {
1435
+ return async ({ request, clientDeclared }) => {
1436
+ const userId = await getUserId(request);
1437
+ validateResourceIdMatch(clientDeclared, userId);
1438
+ return { resourceId: userId };
1439
+ };
1440
+ }
1441
+ function createFilteringThreadScopeResolver(getUserResourceIds) {
1442
+ return async ({ request, clientDeclared }) => {
1443
+ const userResourceIds = await getUserResourceIds(request);
1444
+ const resourceId = filterAuthorizedResourceIds(clientDeclared, userResourceIds);
1445
+ return { resourceId };
1446
+ };
1447
+ }
944
1448
  export {
945
1449
  AgentRunner,
946
1450
  CopilotRuntime,
947
1451
  InMemoryAgentRunner,
948
1452
  VERSION,
949
1453
  createCopilotEndpoint,
950
- finalizeRunEvents2 as finalizeRunEvents
1454
+ createFilteringThreadScopeResolver,
1455
+ createStrictThreadScopeResolver,
1456
+ filterAuthorizedResourceIds,
1457
+ finalizeRunEvents2 as finalizeRunEvents,
1458
+ validateResourceIdMatch
951
1459
  };
952
1460
  //# sourceMappingURL=index.mjs.map