@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.js CHANGED
@@ -25,14 +25,21 @@ __export(index_exports, {
25
25
  InMemoryAgentRunner: () => InMemoryAgentRunner,
26
26
  VERSION: () => VERSION,
27
27
  createCopilotEndpoint: () => createCopilotEndpoint,
28
- finalizeRunEvents: () => import_shared4.finalizeRunEvents
28
+ createFilteringThreadScopeResolver: () => createFilteringThreadScopeResolver,
29
+ createStrictThreadScopeResolver: () => createStrictThreadScopeResolver,
30
+ filterAuthorizedResourceIds: () => filterAuthorizedResourceIds,
31
+ finalizeRunEvents: () => import_shared5.finalizeRunEvents,
32
+ validateResourceIdMatch: () => validateResourceIdMatch
29
33
  });
30
34
  module.exports = __toCommonJS(index_exports);
31
35
 
36
+ // src/runtime.ts
37
+ var import_shared2 = require("@copilotkitnext/shared");
38
+
32
39
  // package.json
33
40
  var package_default = {
34
41
  name: "@copilotkitnext/runtime",
35
- version: "0.0.18",
42
+ version: "0.0.19-threads-and-attachements.1",
36
43
  description: "Server-side runtime package for CopilotKit2",
37
44
  main: "dist/index.js",
38
45
  types: "dist/index.d.ts",
@@ -68,9 +75,9 @@ var package_default = {
68
75
  vitest: "^3.0.5"
69
76
  },
70
77
  dependencies: {
71
- "@ag-ui/client": "0.0.40-alpha.6",
72
- "@ag-ui/core": "0.0.40-alpha.6",
73
- "@ag-ui/encoder": "0.0.40-alpha.6",
78
+ "@ag-ui/client": "0.0.40-alpha.7",
79
+ "@ag-ui/core": "0.0.40-alpha.7",
80
+ "@ag-ui/encoder": "0.0.40-alpha.7",
74
81
  "@copilotkitnext/shared": "workspace:*",
75
82
  hono: "^4.6.13",
76
83
  rxjs: "7.8.1"
@@ -93,8 +100,10 @@ var import_rxjs = require("rxjs");
93
100
  var import_client = require("@ag-ui/client");
94
101
  var import_shared = require("@copilotkitnext/shared");
95
102
  var InMemoryEventStore = class {
96
- constructor(threadId) {
103
+ constructor(threadId, resourceIds, properties) {
97
104
  this.threadId = threadId;
105
+ this.resourceIds = resourceIds;
106
+ this.properties = properties;
98
107
  }
99
108
  /** The subject that current consumers subscribe to. */
100
109
  subject = null;
@@ -114,11 +123,41 @@ var InMemoryEventStore = class {
114
123
  currentEvents = null;
115
124
  };
116
125
  var GLOBAL_STORE = /* @__PURE__ */ new Map();
126
+ function matchesScope(store, scope) {
127
+ if (scope === void 0 || scope === null) {
128
+ return true;
129
+ }
130
+ const scopeIds = Array.isArray(scope.resourceId) ? scope.resourceId : [scope.resourceId];
131
+ return scopeIds.some((scopeId) => store.resourceIds.includes(scopeId));
132
+ }
117
133
  var InMemoryAgentRunner = class extends AgentRunner {
118
134
  run(request) {
119
135
  let existingStore = GLOBAL_STORE.get(request.threadId);
120
- if (!existingStore) {
121
- existingStore = new InMemoryEventStore(request.threadId);
136
+ if (!existingStore && request.scope === null) {
137
+ throw new Error(
138
+ "Cannot create thread with null scope. Admin users must specify an explicit resourceId for the thread owner."
139
+ );
140
+ }
141
+ let resourceIds;
142
+ if (request.scope === void 0) {
143
+ resourceIds = ["global"];
144
+ } else if (request.scope === null) {
145
+ resourceIds = [];
146
+ } else if (Array.isArray(request.scope.resourceId)) {
147
+ if (request.scope.resourceId.length === 0) {
148
+ throw new Error("Invalid scope: resourceId array cannot be empty");
149
+ }
150
+ resourceIds = request.scope.resourceId;
151
+ } else {
152
+ resourceIds = [request.scope.resourceId];
153
+ }
154
+ if (existingStore) {
155
+ if (request.scope !== null && !matchesScope(existingStore, request.scope)) {
156
+ throw new Error("Unauthorized: Cannot run on thread owned by different resource");
157
+ }
158
+ resourceIds = existingStore.resourceIds;
159
+ } else {
160
+ existingStore = new InMemoryEventStore(request.threadId, resourceIds, request.scope?.properties);
122
161
  GLOBAL_STORE.set(request.threadId, existingStore);
123
162
  }
124
163
  const store = existingStore;
@@ -162,9 +201,7 @@ var InMemoryAgentRunner = class extends AgentRunner {
162
201
  if (event.type === import_client.EventType.RUN_STARTED) {
163
202
  const runStartedEvent = event;
164
203
  if (!runStartedEvent.input) {
165
- const sanitizedMessages = request.input.messages ? request.input.messages.filter(
166
- (message) => !historicMessageIds.has(message.id)
167
- ) : void 0;
204
+ const sanitizedMessages = request.input.messages ? request.input.messages.filter((message) => !historicMessageIds.has(message.id)) : void 0;
168
205
  const updatedInput = {
169
206
  ...request.input,
170
207
  ...sanitizedMessages !== void 0 ? { messages: sanitizedMessages } : {}
@@ -261,7 +298,7 @@ var InMemoryAgentRunner = class extends AgentRunner {
261
298
  connect(request) {
262
299
  const store = GLOBAL_STORE.get(request.threadId);
263
300
  const connectionSubject = new import_rxjs.ReplaySubject(Infinity);
264
- if (!store) {
301
+ if (!store || !matchesScope(store, request.scope)) {
265
302
  connectionSubject.complete();
266
303
  return connectionSubject.asObservable();
267
304
  }
@@ -297,54 +334,256 @@ var InMemoryAgentRunner = class extends AgentRunner {
297
334
  const store = GLOBAL_STORE.get(request.threadId);
298
335
  return Promise.resolve(store?.isRunning ?? false);
299
336
  }
300
- stop(request) {
337
+ async stop(request) {
301
338
  const store = GLOBAL_STORE.get(request.threadId);
302
- if (!store || !store.isRunning) {
303
- return Promise.resolve(false);
304
- }
305
- if (store.stopRequested) {
306
- return Promise.resolve(false);
339
+ if (!store) {
340
+ return false;
307
341
  }
308
- store.stopRequested = true;
309
- store.isRunning = false;
310
- const agent = store.agent;
311
- if (!agent) {
312
- store.stopRequested = false;
342
+ if (store.isRunning) {
343
+ store.stopRequested = true;
313
344
  store.isRunning = false;
314
- return Promise.resolve(false);
345
+ const agent = store.agent;
346
+ try {
347
+ if (agent) {
348
+ agent.abortRun();
349
+ return true;
350
+ }
351
+ return false;
352
+ } catch (error) {
353
+ console.warn("Failed to abort in-memory runner:", error);
354
+ store.stopRequested = false;
355
+ store.isRunning = true;
356
+ return false;
357
+ }
315
358
  }
316
- try {
317
- agent.abortRun();
318
- return Promise.resolve(true);
319
- } catch (error) {
320
- console.error("Failed to abort agent run", error);
321
- store.stopRequested = false;
322
- store.isRunning = true;
323
- return Promise.resolve(false);
359
+ return false;
360
+ }
361
+ async listThreads(request) {
362
+ const limit = request.limit ?? 50;
363
+ const offset = request.offset ?? 0;
364
+ if (request.scope !== void 0 && request.scope !== null) {
365
+ const scopeIds = Array.isArray(request.scope.resourceId) ? request.scope.resourceId : [request.scope.resourceId];
366
+ if (scopeIds.length === 0) {
367
+ return { threads: [], total: 0 };
368
+ }
369
+ }
370
+ const threadInfos = [];
371
+ for (const [threadId, store] of GLOBAL_STORE.entries()) {
372
+ if (threadId.includes("-suggestions-")) {
373
+ continue;
374
+ }
375
+ if (!matchesScope(store, request.scope)) {
376
+ continue;
377
+ }
378
+ if (store.historicRuns.length === 0) {
379
+ continue;
380
+ }
381
+ const firstRun = store.historicRuns[0];
382
+ const lastRun = store.historicRuns[store.historicRuns.length - 1];
383
+ if (!firstRun || !lastRun) {
384
+ continue;
385
+ }
386
+ threadInfos.push({
387
+ threadId,
388
+ createdAt: firstRun.createdAt,
389
+ lastActivityAt: lastRun.createdAt,
390
+ store
391
+ });
392
+ }
393
+ threadInfos.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
394
+ const total = threadInfos.length;
395
+ const paginatedInfos = threadInfos.slice(offset, offset + limit);
396
+ const threads = paginatedInfos.map((info) => {
397
+ let firstMessage;
398
+ const firstRun = info.store.historicRuns[0];
399
+ if (firstRun) {
400
+ const textContent = firstRun.events.find((e) => e.type === import_client.EventType.TEXT_MESSAGE_CONTENT);
401
+ if (textContent?.delta) {
402
+ firstMessage = textContent.delta.substring(0, 100);
403
+ }
404
+ }
405
+ const messageIds = /* @__PURE__ */ new Set();
406
+ for (const run of info.store.historicRuns) {
407
+ for (const event of run.events) {
408
+ if ("messageId" in event && typeof event.messageId === "string") {
409
+ messageIds.add(event.messageId);
410
+ }
411
+ }
412
+ }
413
+ return {
414
+ threadId: info.threadId,
415
+ createdAt: info.createdAt,
416
+ lastActivityAt: info.lastActivityAt,
417
+ isRunning: info.store.isRunning,
418
+ messageCount: messageIds.size,
419
+ firstMessage,
420
+ resourceId: info.store.resourceIds[0] || "unknown",
421
+ // Return first for backward compatibility
422
+ properties: info.store.properties
423
+ };
424
+ });
425
+ return { threads, total };
426
+ }
427
+ async getThreadMetadata(threadId, scope) {
428
+ const store = GLOBAL_STORE.get(threadId);
429
+ if (!store || !matchesScope(store, scope) || store.historicRuns.length === 0) {
430
+ return null;
431
+ }
432
+ const firstRun = store.historicRuns[0];
433
+ const lastRun = store.historicRuns[store.historicRuns.length - 1];
434
+ if (!firstRun || !lastRun) {
435
+ return null;
436
+ }
437
+ let firstMessage;
438
+ const textContent = firstRun.events.find((e) => e.type === import_client.EventType.TEXT_MESSAGE_CONTENT);
439
+ if (textContent?.delta) {
440
+ firstMessage = textContent.delta.substring(0, 100);
441
+ }
442
+ const messageIds = /* @__PURE__ */ new Set();
443
+ for (const run of store.historicRuns) {
444
+ for (const event of run.events) {
445
+ if ("messageId" in event && typeof event.messageId === "string") {
446
+ messageIds.add(event.messageId);
447
+ }
448
+ }
449
+ }
450
+ return {
451
+ threadId,
452
+ createdAt: firstRun.createdAt,
453
+ lastActivityAt: lastRun.createdAt,
454
+ isRunning: store.isRunning,
455
+ messageCount: messageIds.size,
456
+ firstMessage,
457
+ resourceId: store.resourceIds[0] || "unknown",
458
+ // Return first for backward compatibility
459
+ properties: store.properties
460
+ };
461
+ }
462
+ async deleteThread(threadId, scope) {
463
+ const store = GLOBAL_STORE.get(threadId);
464
+ if (!store || !matchesScope(store, scope)) {
465
+ return;
466
+ }
467
+ if (store.agent) {
468
+ try {
469
+ store.agent.abortRun();
470
+ } catch (error) {
471
+ console.warn("Failed to abort agent during thread deletion:", error);
472
+ }
473
+ }
474
+ store.subject?.complete();
475
+ GLOBAL_STORE.delete(threadId);
476
+ }
477
+ /**
478
+ * Clear all threads from the global store (for testing purposes only)
479
+ * @internal
480
+ */
481
+ clearAllThreads() {
482
+ for (const [threadId, store] of GLOBAL_STORE.entries()) {
483
+ if (store.agent) {
484
+ try {
485
+ store.agent.abortRun();
486
+ } catch (error) {
487
+ console.warn("Failed to abort agent during clearAllThreads:", error);
488
+ }
489
+ }
490
+ store.subject?.complete();
491
+ GLOBAL_STORE.delete(threadId);
324
492
  }
325
493
  }
326
494
  };
327
495
 
328
496
  // src/runtime.ts
329
497
  var VERSION = package_default.version;
330
- var CopilotRuntime = class {
498
+ var CopilotRuntime = class _CopilotRuntime {
499
+ /**
500
+ * Built-in global scope for single-user apps or demos.
501
+ *
502
+ * All threads are globally accessible when using this scope.
503
+ *
504
+ * @example
505
+ * ```typescript
506
+ * new CopilotRuntime({
507
+ * agents: { myAgent },
508
+ * resolveThreadsScope: CopilotRuntime.GLOBAL_SCOPE,
509
+ * suppressResourceIdWarning: true
510
+ * });
511
+ * ```
512
+ */
513
+ static GLOBAL_SCOPE = async (context) => ({
514
+ resourceId: "global"
515
+ });
516
+ /**
517
+ * Parses the client-declared resource ID(s) from the request header.
518
+ *
519
+ * This is a utility method used internally by handlers to extract the
520
+ * `X-CopilotKit-Resource-ID` header sent by the client via `CopilotKitProvider`.
521
+ *
522
+ * **You typically don't need to call this directly** - it's automatically called
523
+ * by the runtime handlers and passed to your `resolveThreadsScope` function as
524
+ * the `clientDeclared` parameter.
525
+ *
526
+ * @param request - The incoming HTTP request
527
+ * @returns The parsed resource ID(s), or undefined if header is missing
528
+ * - Returns a string if single ID
529
+ * - Returns an array if multiple comma-separated IDs
530
+ * - Returns undefined if header not present
531
+ *
532
+ * @example
533
+ * ```typescript
534
+ * // Automatically used internally:
535
+ * const clientDeclared = CopilotRuntime.parseClientDeclaredResourceId(request);
536
+ * const scope = await runtime.resolveThreadsScope({ request, clientDeclared });
537
+ * ```
538
+ */
539
+ static parseClientDeclaredResourceId(request) {
540
+ const header = request.headers.get("X-CopilotKit-Resource-ID");
541
+ if (!header) {
542
+ return void 0;
543
+ }
544
+ const values = header.split(",").map((v) => decodeURIComponent(v.trim()));
545
+ return values.length === 1 ? values[0] : values;
546
+ }
331
547
  agents;
332
548
  transcriptionService;
333
549
  beforeRequestMiddleware;
334
550
  afterRequestMiddleware;
335
551
  runner;
552
+ resolveThreadsScope;
553
+ suppressResourceIdWarning;
336
554
  constructor({
337
555
  agents,
338
556
  transcriptionService,
339
557
  beforeRequestMiddleware,
340
558
  afterRequestMiddleware,
341
- runner
559
+ runner,
560
+ resolveThreadsScope,
561
+ suppressResourceIdWarning = false
342
562
  }) {
343
563
  this.agents = agents;
344
564
  this.transcriptionService = transcriptionService;
345
565
  this.beforeRequestMiddleware = beforeRequestMiddleware;
346
566
  this.afterRequestMiddleware = afterRequestMiddleware;
347
567
  this.runner = runner ?? new InMemoryAgentRunner();
568
+ this.resolveThreadsScope = resolveThreadsScope ?? _CopilotRuntime.GLOBAL_SCOPE;
569
+ this.suppressResourceIdWarning = suppressResourceIdWarning;
570
+ if (!resolveThreadsScope && !suppressResourceIdWarning) {
571
+ this.logGlobalScopeWarning();
572
+ }
573
+ }
574
+ logGlobalScopeWarning() {
575
+ const isProduction = process.env.NODE_ENV === "production";
576
+ if (isProduction) {
577
+ import_shared2.logger.error({
578
+ msg: "CopilotKit Security Warning: GLOBAL_SCOPE in production",
579
+ 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"
580
+ });
581
+ } else {
582
+ import_shared2.logger.warn({
583
+ msg: "CopilotKit: Using GLOBAL_SCOPE",
584
+ 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"
585
+ });
586
+ }
348
587
  }
349
588
  };
350
589
 
@@ -355,11 +594,7 @@ var import_cors = require("hono/cors");
355
594
  // src/handlers/handle-run.ts
356
595
  var import_client2 = require("@ag-ui/client");
357
596
  var import_encoder = require("@ag-ui/encoder");
358
- async function handleRunAgent({
359
- runtime,
360
- request,
361
- agentId
362
- }) {
597
+ async function handleRunAgent({ runtime, request, agentId }) {
363
598
  try {
364
599
  const agents = await runtime.agents;
365
600
  if (!agents[agentId]) {
@@ -396,6 +631,23 @@ async function handleRunAgent({
396
631
  const writer = stream.writable.getWriter();
397
632
  const encoder = new import_encoder.EventEncoder();
398
633
  let streamClosed = false;
634
+ let subscription;
635
+ let abortListener;
636
+ const cleanupAbortListener = () => {
637
+ if (abortListener) {
638
+ request.signal.removeEventListener("abort", abortListener);
639
+ abortListener = void 0;
640
+ }
641
+ };
642
+ const closeStream = async () => {
643
+ if (!streamClosed) {
644
+ try {
645
+ await writer.close();
646
+ } catch {
647
+ }
648
+ streamClosed = true;
649
+ }
650
+ };
399
651
  (async () => {
400
652
  let input;
401
653
  try {
@@ -409,13 +661,26 @@ async function handleRunAgent({
409
661
  { status: 400 }
410
662
  );
411
663
  }
664
+ const clientDeclared = CopilotRuntime["parseClientDeclaredResourceId"](request);
665
+ const scope = await runtime.resolveThreadsScope({ request, clientDeclared });
666
+ if (scope === void 0) {
667
+ throw new Error("Unauthorized: No resource scope provided");
668
+ }
412
669
  agent.setMessages(input.messages);
413
670
  agent.setState(input.state);
414
671
  agent.threadId = input.threadId;
415
- runtime.runner.run({
672
+ const stopRunner = async () => {
673
+ try {
674
+ await runtime.runner.stop({ threadId: input.threadId });
675
+ } catch (stopError) {
676
+ console.error("Error stopping runner:", stopError);
677
+ }
678
+ };
679
+ subscription = runtime.runner.run({
416
680
  threadId: input.threadId,
417
681
  agent,
418
- input
682
+ input,
683
+ scope
419
684
  }).subscribe({
420
685
  next: async (event) => {
421
686
  if (!request.signal.aborted && !streamClosed) {
@@ -430,42 +695,39 @@ async function handleRunAgent({
430
695
  },
431
696
  error: async (error) => {
432
697
  console.error("Error running agent:", error);
433
- if (!streamClosed) {
434
- try {
435
- await writer.close();
436
- streamClosed = true;
437
- } catch {
438
- }
439
- }
698
+ cleanupAbortListener();
699
+ await closeStream();
440
700
  },
441
701
  complete: async () => {
442
- if (!streamClosed) {
443
- try {
444
- await writer.close();
445
- streamClosed = true;
446
- } catch {
447
- }
448
- }
702
+ cleanupAbortListener();
703
+ await closeStream();
449
704
  }
450
705
  });
706
+ const handleAbort = () => {
707
+ subscription?.unsubscribe();
708
+ subscription = void 0;
709
+ cleanupAbortListener();
710
+ void stopRunner();
711
+ void closeStream();
712
+ };
713
+ if (request.signal.aborted) {
714
+ handleAbort();
715
+ } else {
716
+ abortListener = handleAbort;
717
+ request.signal.addEventListener("abort", abortListener);
718
+ }
451
719
  })().catch((error) => {
452
720
  console.error("Error running agent:", error);
453
- console.error(
454
- "Error stack:",
455
- error instanceof Error ? error.stack : "No stack trace"
456
- );
721
+ console.error("Error stack:", error instanceof Error ? error.stack : "No stack trace");
457
722
  console.error("Error details:", {
458
723
  name: error instanceof Error ? error.name : "Unknown",
459
724
  message: error instanceof Error ? error.message : String(error),
460
725
  cause: error instanceof Error ? error.cause : void 0
461
726
  });
462
- if (!streamClosed) {
463
- try {
464
- writer.close();
465
- streamClosed = true;
466
- } catch {
467
- }
468
- }
727
+ subscription?.unsubscribe();
728
+ subscription = void 0;
729
+ cleanupAbortListener();
730
+ void closeStream();
469
731
  });
470
732
  return new Response(stream.readable, {
471
733
  status: 200,
@@ -477,10 +739,7 @@ async function handleRunAgent({
477
739
  });
478
740
  } catch (error) {
479
741
  console.error("Error running agent:", error);
480
- console.error(
481
- "Error stack:",
482
- error instanceof Error ? error.stack : "No stack trace"
483
- );
742
+ console.error("Error stack:", error instanceof Error ? error.stack : "No stack trace");
484
743
  console.error("Error details:", {
485
744
  name: error instanceof Error ? error.name : "Unknown",
486
745
  message: error instanceof Error ? error.message : String(error),
@@ -638,10 +897,10 @@ async function handleTranscribe({
638
897
  }
639
898
 
640
899
  // src/endpoint.ts
641
- var import_shared3 = require("@copilotkitnext/shared");
900
+ var import_shared4 = require("@copilotkitnext/shared");
642
901
 
643
902
  // src/middleware.ts
644
- var import_shared2 = require("@copilotkitnext/shared");
903
+ var import_shared3 = require("@copilotkitnext/shared");
645
904
  async function callBeforeRequestMiddleware({
646
905
  runtime,
647
906
  request,
@@ -652,7 +911,7 @@ async function callBeforeRequestMiddleware({
652
911
  if (typeof mw === "function") {
653
912
  return mw({ runtime, request, path });
654
913
  }
655
- import_shared2.logger.warn({ mw }, "Unsupported beforeRequestMiddleware value \u2013 skipped");
914
+ import_shared3.logger.warn({ mw }, "Unsupported beforeRequestMiddleware value \u2013 skipped");
656
915
  return;
657
916
  }
658
917
  async function callAfterRequestMiddleware({
@@ -665,17 +924,13 @@ async function callAfterRequestMiddleware({
665
924
  if (typeof mw === "function") {
666
925
  return mw({ runtime, response, path });
667
926
  }
668
- import_shared2.logger.warn({ mw }, "Unsupported afterRequestMiddleware value \u2013 skipped");
927
+ import_shared3.logger.warn({ mw }, "Unsupported afterRequestMiddleware value \u2013 skipped");
669
928
  }
670
929
 
671
930
  // src/handlers/handle-connect.ts
672
931
  var import_client3 = require("@ag-ui/client");
673
932
  var import_encoder2 = require("@ag-ui/encoder");
674
- async function handleConnectAgent({
675
- runtime,
676
- request,
677
- agentId
678
- }) {
933
+ async function handleConnectAgent({ runtime, request, agentId }) {
679
934
  try {
680
935
  const agents = await runtime.agents;
681
936
  if (!agents[agentId]) {
@@ -694,6 +949,23 @@ async function handleConnectAgent({
694
949
  const writer = stream.writable.getWriter();
695
950
  const encoder = new import_encoder2.EventEncoder();
696
951
  let streamClosed = false;
952
+ let subscription;
953
+ let abortListener;
954
+ const cleanupAbortListener = () => {
955
+ if (abortListener) {
956
+ request.signal.removeEventListener("abort", abortListener);
957
+ abortListener = void 0;
958
+ }
959
+ };
960
+ const closeStream = async () => {
961
+ if (!streamClosed) {
962
+ try {
963
+ await writer.close();
964
+ } catch {
965
+ }
966
+ streamClosed = true;
967
+ }
968
+ };
697
969
  (async () => {
698
970
  let input;
699
971
  try {
@@ -707,8 +979,14 @@ async function handleConnectAgent({
707
979
  { status: 400 }
708
980
  );
709
981
  }
710
- runtime.runner.connect({
711
- threadId: input.threadId
982
+ const clientDeclared = CopilotRuntime["parseClientDeclaredResourceId"](request);
983
+ const scope = await runtime.resolveThreadsScope({ request, clientDeclared });
984
+ if (scope === void 0) {
985
+ throw new Error("Unauthorized: No resource scope provided");
986
+ }
987
+ subscription = runtime.runner.connect({
988
+ threadId: input.threadId,
989
+ scope
712
990
  }).subscribe({
713
991
  next: async (event) => {
714
992
  if (!request.signal.aborted && !streamClosed) {
@@ -723,42 +1001,38 @@ async function handleConnectAgent({
723
1001
  },
724
1002
  error: async (error) => {
725
1003
  console.error("Error running agent:", error);
726
- if (!streamClosed) {
727
- try {
728
- await writer.close();
729
- streamClosed = true;
730
- } catch {
731
- }
732
- }
1004
+ cleanupAbortListener();
1005
+ await closeStream();
733
1006
  },
734
1007
  complete: async () => {
735
- if (!streamClosed) {
736
- try {
737
- await writer.close();
738
- streamClosed = true;
739
- } catch {
740
- }
741
- }
1008
+ cleanupAbortListener();
1009
+ await closeStream();
742
1010
  }
743
1011
  });
1012
+ const handleAbort = () => {
1013
+ subscription?.unsubscribe();
1014
+ subscription = void 0;
1015
+ cleanupAbortListener();
1016
+ void closeStream();
1017
+ };
1018
+ if (request.signal.aborted) {
1019
+ handleAbort();
1020
+ } else {
1021
+ abortListener = handleAbort;
1022
+ request.signal.addEventListener("abort", abortListener);
1023
+ }
744
1024
  })().catch((error) => {
745
1025
  console.error("Error running agent:", error);
746
- console.error(
747
- "Error stack:",
748
- error instanceof Error ? error.stack : "No stack trace"
749
- );
1026
+ console.error("Error stack:", error instanceof Error ? error.stack : "No stack trace");
750
1027
  console.error("Error details:", {
751
1028
  name: error instanceof Error ? error.name : "Unknown",
752
1029
  message: error instanceof Error ? error.message : String(error),
753
1030
  cause: error instanceof Error ? error.cause : void 0
754
1031
  });
755
- if (!streamClosed) {
756
- try {
757
- writer.close();
758
- streamClosed = true;
759
- } catch {
760
- }
761
- }
1032
+ subscription?.unsubscribe();
1033
+ subscription = void 0;
1034
+ cleanupAbortListener();
1035
+ void closeStream();
762
1036
  });
763
1037
  return new Response(stream.readable, {
764
1038
  status: 200,
@@ -770,10 +1044,7 @@ async function handleConnectAgent({
770
1044
  });
771
1045
  } catch (error) {
772
1046
  console.error("Error running agent:", error);
773
- console.error(
774
- "Error stack:",
775
- error instanceof Error ? error.stack : "No stack trace"
776
- );
1047
+ console.error("Error stack:", error instanceof Error ? error.stack : "No stack trace");
777
1048
  console.error("Error details:", {
778
1049
  name: error instanceof Error ? error.name : "Unknown",
779
1050
  message: error instanceof Error ? error.message : String(error),
@@ -814,7 +1085,35 @@ async function handleStopAgent({
814
1085
  }
815
1086
  );
816
1087
  }
817
- const stopped = await runtime.runner.stop({ threadId });
1088
+ const clientDeclared = CopilotRuntime["parseClientDeclaredResourceId"](request);
1089
+ const scope = await runtime.resolveThreadsScope({ request, clientDeclared });
1090
+ if (scope === void 0) {
1091
+ return new Response(
1092
+ JSON.stringify({
1093
+ error: "Unauthorized",
1094
+ message: "No resource scope provided"
1095
+ }),
1096
+ {
1097
+ status: 401,
1098
+ headers: { "Content-Type": "application/json" }
1099
+ }
1100
+ );
1101
+ }
1102
+ const runner = await runtime.runner;
1103
+ const metadata = await runner.getThreadMetadata(threadId, scope);
1104
+ if (!metadata) {
1105
+ return new Response(
1106
+ JSON.stringify({
1107
+ error: "Thread not found",
1108
+ message: `Thread '${threadId}' does not exist or you don't have access`
1109
+ }),
1110
+ {
1111
+ status: 404,
1112
+ headers: { "Content-Type": "application/json" }
1113
+ }
1114
+ );
1115
+ }
1116
+ const stopped = await runner.stop({ threadId });
818
1117
  if (!stopped) {
819
1118
  return new Response(
820
1119
  JSON.stringify({
@@ -856,6 +1155,141 @@ async function handleStopAgent({
856
1155
  }
857
1156
  }
858
1157
 
1158
+ // src/handlers/handle-threads.ts
1159
+ async function handleListThreads({ runtime, request }) {
1160
+ try {
1161
+ const clientDeclared = CopilotRuntime["parseClientDeclaredResourceId"](request);
1162
+ const scope = await runtime.resolveThreadsScope({ request, clientDeclared });
1163
+ if (scope === void 0) {
1164
+ return new Response(
1165
+ JSON.stringify({
1166
+ error: "Unauthorized",
1167
+ message: "No resource scope provided"
1168
+ }),
1169
+ {
1170
+ status: 401,
1171
+ headers: { "Content-Type": "application/json" }
1172
+ }
1173
+ );
1174
+ }
1175
+ const url = new URL(request.url);
1176
+ const limitParam = url.searchParams.get("limit");
1177
+ const offsetParam = url.searchParams.get("offset");
1178
+ const parsedLimit = limitParam ? Number.parseInt(limitParam, 10) : NaN;
1179
+ const parsedOffset = offsetParam ? Number.parseInt(offsetParam, 10) : NaN;
1180
+ const limit = Math.max(1, Math.min(100, Number.isNaN(parsedLimit) ? 20 : parsedLimit));
1181
+ const offset = Math.max(0, Number.isNaN(parsedOffset) ? 0 : parsedOffset);
1182
+ const runner = await runtime.runner;
1183
+ const result = await runner.listThreads({ scope, limit, offset });
1184
+ return new Response(JSON.stringify(result), {
1185
+ status: 200,
1186
+ headers: { "Content-Type": "application/json" }
1187
+ });
1188
+ } catch (error) {
1189
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
1190
+ return new Response(
1191
+ JSON.stringify({
1192
+ error: "Failed to list threads",
1193
+ message: errorMessage
1194
+ }),
1195
+ {
1196
+ status: 500,
1197
+ headers: { "Content-Type": "application/json" }
1198
+ }
1199
+ );
1200
+ }
1201
+ }
1202
+ async function handleGetThread({ runtime, threadId, request }) {
1203
+ try {
1204
+ const clientDeclared = CopilotRuntime["parseClientDeclaredResourceId"](request);
1205
+ const scope = await runtime.resolveThreadsScope({ request, clientDeclared });
1206
+ if (scope === void 0) {
1207
+ return new Response(
1208
+ JSON.stringify({
1209
+ error: "Unauthorized",
1210
+ message: "No resource scope provided"
1211
+ }),
1212
+ {
1213
+ status: 401,
1214
+ headers: { "Content-Type": "application/json" }
1215
+ }
1216
+ );
1217
+ }
1218
+ const runner = await runtime.runner;
1219
+ const metadata = await runner.getThreadMetadata(threadId, scope);
1220
+ if (!metadata) {
1221
+ return new Response(
1222
+ JSON.stringify({
1223
+ error: "Thread not found",
1224
+ message: `Thread '${threadId}' does not exist`
1225
+ }),
1226
+ {
1227
+ status: 404,
1228
+ headers: { "Content-Type": "application/json" }
1229
+ }
1230
+ );
1231
+ }
1232
+ return new Response(JSON.stringify(metadata), {
1233
+ status: 200,
1234
+ headers: { "Content-Type": "application/json" }
1235
+ });
1236
+ } catch (error) {
1237
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
1238
+ return new Response(
1239
+ JSON.stringify({
1240
+ error: "Failed to get thread",
1241
+ message: errorMessage
1242
+ }),
1243
+ {
1244
+ status: 500,
1245
+ headers: { "Content-Type": "application/json" }
1246
+ }
1247
+ );
1248
+ }
1249
+ }
1250
+ async function handleDeleteThread({ runtime, threadId, request }) {
1251
+ if (!threadId) {
1252
+ return new Response(JSON.stringify({ error: "Thread ID required" }), {
1253
+ status: 400,
1254
+ headers: { "Content-Type": "application/json" }
1255
+ });
1256
+ }
1257
+ try {
1258
+ const clientDeclared = CopilotRuntime["parseClientDeclaredResourceId"](request);
1259
+ const scope = await runtime.resolveThreadsScope({ request, clientDeclared });
1260
+ if (scope === void 0) {
1261
+ return new Response(
1262
+ JSON.stringify({
1263
+ error: "Unauthorized",
1264
+ message: "No resource scope provided"
1265
+ }),
1266
+ {
1267
+ status: 401,
1268
+ headers: { "Content-Type": "application/json" }
1269
+ }
1270
+ );
1271
+ }
1272
+ const runner = await runtime.runner;
1273
+ await runner.deleteThread(threadId, scope);
1274
+ return new Response(JSON.stringify({ success: true }), {
1275
+ status: 200,
1276
+ headers: { "Content-Type": "application/json" }
1277
+ });
1278
+ } catch (error) {
1279
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
1280
+ return new Response(
1281
+ JSON.stringify({
1282
+ error: "Failed to delete thread",
1283
+ message: errorMessage
1284
+ }),
1285
+ {
1286
+ status: 500,
1287
+ headers: { "Content-Type": "application/json" }
1288
+ }
1289
+ );
1290
+ }
1291
+ }
1292
+
859
1293
  // src/endpoint.ts
860
1294
  function createCopilotEndpoint({ runtime, basePath }) {
861
1295
  const app = new import_hono.Hono();
@@ -879,7 +1313,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
879
1313
  c.set("modifiedRequest", maybeModifiedRequest);
880
1314
  }
881
1315
  } catch (error) {
882
- import_shared3.logger.error({ err: error, url: request.url, path }, "Error running before request middleware");
1316
+ import_shared4.logger.error({ err: error, url: request.url, path }, "Error running before request middleware");
883
1317
  if (error instanceof Response) {
884
1318
  return error;
885
1319
  }
@@ -895,7 +1329,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
895
1329
  response,
896
1330
  path
897
1331
  }).catch((error) => {
898
- import_shared3.logger.error({ err: error, url: c.req.url, path }, "Error running after request middleware");
1332
+ import_shared4.logger.error({ err: error, url: c.req.url, path }, "Error running after request middleware");
899
1333
  });
900
1334
  }).post("/agent/:agentId/run", async (c) => {
901
1335
  const agentId = c.req.param("agentId");
@@ -907,7 +1341,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
907
1341
  agentId
908
1342
  });
909
1343
  } catch (error) {
910
- import_shared3.logger.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
1344
+ import_shared4.logger.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
911
1345
  throw error;
912
1346
  }
913
1347
  }).post("/agent/:agentId/connect", async (c) => {
@@ -920,7 +1354,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
920
1354
  agentId
921
1355
  });
922
1356
  } catch (error) {
923
- import_shared3.logger.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
1357
+ import_shared4.logger.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
924
1358
  throw error;
925
1359
  }
926
1360
  }).post("/agent/:agentId/stop/:threadId", async (c) => {
@@ -935,7 +1369,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
935
1369
  threadId
936
1370
  });
937
1371
  } catch (error) {
938
- import_shared3.logger.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
1372
+ import_shared4.logger.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
939
1373
  throw error;
940
1374
  }
941
1375
  }).get("/info", async (c) => {
@@ -946,7 +1380,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
946
1380
  request
947
1381
  });
948
1382
  } catch (error) {
949
- import_shared3.logger.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
1383
+ import_shared4.logger.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
950
1384
  throw error;
951
1385
  }
952
1386
  }).post("/transcribe", async (c) => {
@@ -957,7 +1391,44 @@ function createCopilotEndpoint({ runtime, basePath }) {
957
1391
  request
958
1392
  });
959
1393
  } catch (error) {
960
- import_shared3.logger.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
1394
+ import_shared4.logger.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
1395
+ throw error;
1396
+ }
1397
+ }).get("/threads", async (c) => {
1398
+ const request = c.get("modifiedRequest") || c.req.raw;
1399
+ try {
1400
+ return await handleListThreads({
1401
+ runtime,
1402
+ request
1403
+ });
1404
+ } catch (error) {
1405
+ import_shared4.logger.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
1406
+ throw error;
1407
+ }
1408
+ }).get("/threads/:threadId", async (c) => {
1409
+ const threadId = c.req.param("threadId");
1410
+ const request = c.get("modifiedRequest") || c.req.raw;
1411
+ try {
1412
+ return await handleGetThread({
1413
+ runtime,
1414
+ request,
1415
+ threadId
1416
+ });
1417
+ } catch (error) {
1418
+ import_shared4.logger.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
1419
+ throw error;
1420
+ }
1421
+ }).delete("/threads/:threadId", async (c) => {
1422
+ const threadId = c.req.param("threadId");
1423
+ const request = c.get("modifiedRequest") || c.req.raw;
1424
+ try {
1425
+ return await handleDeleteThread({
1426
+ runtime,
1427
+ request,
1428
+ threadId
1429
+ });
1430
+ } catch (error) {
1431
+ import_shared4.logger.error({ err: error, url: request.url, path: c.req.path }, "Error running request handler");
961
1432
  throw error;
962
1433
  }
963
1434
  }).notFound((c) => {
@@ -966,7 +1437,46 @@ function createCopilotEndpoint({ runtime, basePath }) {
966
1437
  }
967
1438
 
968
1439
  // src/runner/index.ts
969
- var import_shared4 = require("@copilotkitnext/shared");
1440
+ var import_shared5 = require("@copilotkitnext/shared");
1441
+
1442
+ // src/resource-id-helpers.ts
1443
+ function validateResourceIdMatch(clientDeclared, serverAuthorized) {
1444
+ if (!clientDeclared) {
1445
+ return;
1446
+ }
1447
+ const clientIds = Array.isArray(clientDeclared) ? clientDeclared : [clientDeclared];
1448
+ const authorizedIds = Array.isArray(serverAuthorized) ? serverAuthorized : [serverAuthorized];
1449
+ const hasMatch = clientIds.some((clientId) => authorizedIds.includes(clientId));
1450
+ if (!hasMatch) {
1451
+ throw new Error("Unauthorized: Client-declared resourceId does not match authenticated user");
1452
+ }
1453
+ }
1454
+ function filterAuthorizedResourceIds(clientDeclared, serverAuthorized) {
1455
+ const authorizedIds = Array.isArray(serverAuthorized) ? serverAuthorized : [serverAuthorized];
1456
+ if (!clientDeclared) {
1457
+ return serverAuthorized;
1458
+ }
1459
+ const clientIds = Array.isArray(clientDeclared) ? clientDeclared : [clientDeclared];
1460
+ const filtered = clientIds.filter((id) => authorizedIds.includes(id));
1461
+ if (filtered.length === 0) {
1462
+ throw new Error("Unauthorized: None of the client-declared resourceIds are authorized");
1463
+ }
1464
+ return Array.isArray(clientDeclared) ? filtered : filtered[0];
1465
+ }
1466
+ function createStrictThreadScopeResolver(getUserId) {
1467
+ return async ({ request, clientDeclared }) => {
1468
+ const userId = await getUserId(request);
1469
+ validateResourceIdMatch(clientDeclared, userId);
1470
+ return { resourceId: userId };
1471
+ };
1472
+ }
1473
+ function createFilteringThreadScopeResolver(getUserResourceIds) {
1474
+ return async ({ request, clientDeclared }) => {
1475
+ const userResourceIds = await getUserResourceIds(request);
1476
+ const resourceId = filterAuthorizedResourceIds(clientDeclared, userResourceIds);
1477
+ return { resourceId };
1478
+ };
1479
+ }
970
1480
  // Annotate the CommonJS export names for ESM import in node:
971
1481
  0 && (module.exports = {
972
1482
  AgentRunner,
@@ -974,6 +1484,10 @@ var import_shared4 = require("@copilotkitnext/shared");
974
1484
  InMemoryAgentRunner,
975
1485
  VERSION,
976
1486
  createCopilotEndpoint,
977
- finalizeRunEvents
1487
+ createFilteringThreadScopeResolver,
1488
+ createStrictThreadScopeResolver,
1489
+ filterAuthorizedResourceIds,
1490
+ finalizeRunEvents,
1491
+ validateResourceIdMatch
978
1492
  });
979
1493
  //# sourceMappingURL=index.js.map