@copilotkitnext/runtime 0.0.17 → 0.0.19-threads-and-attachements.0

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.17",
7
+ version: "0.0.19-threads-and-attachements.0",
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"
@@ -60,137 +63,15 @@ var AgentRunner = class {
60
63
  // src/runner/in-memory.ts
61
64
  import { ReplaySubject } from "rxjs";
62
65
  import {
63
- EventType as EventType2,
66
+ EventType,
64
67
  compactEvents
65
68
  } from "@ag-ui/client";
66
-
67
- // src/runner/finalize-events.ts
68
- import { randomUUID } from "crypto";
69
- import {
70
- EventType
71
- } from "@ag-ui/client";
72
- var defaultStopMessage = "Run stopped by user";
73
- var defaultAbruptEndMessage = "Run ended without emitting a terminal event";
74
- function finalizeRunEvents(events, options = {}) {
75
- const { stopRequested = false, interruptionMessage } = options;
76
- const resolvedStopMessage = interruptionMessage ?? defaultStopMessage;
77
- const resolvedAbruptMessage = interruptionMessage && interruptionMessage !== defaultStopMessage ? interruptionMessage : defaultAbruptEndMessage;
78
- const appended = [];
79
- const openMessageIds = /* @__PURE__ */ new Set();
80
- const openToolCalls = /* @__PURE__ */ new Map();
81
- for (const event of events) {
82
- switch (event.type) {
83
- case EventType.TEXT_MESSAGE_START: {
84
- const messageId = event.messageId;
85
- if (typeof messageId === "string") {
86
- openMessageIds.add(messageId);
87
- }
88
- break;
89
- }
90
- case EventType.TEXT_MESSAGE_END: {
91
- const messageId = event.messageId;
92
- if (typeof messageId === "string") {
93
- openMessageIds.delete(messageId);
94
- }
95
- break;
96
- }
97
- case EventType.TOOL_CALL_START: {
98
- const toolCallId = event.toolCallId;
99
- if (typeof toolCallId === "string") {
100
- openToolCalls.set(toolCallId, {
101
- hasEnd: false,
102
- hasResult: false
103
- });
104
- }
105
- break;
106
- }
107
- case EventType.TOOL_CALL_END: {
108
- const toolCallId = event.toolCallId;
109
- const info = toolCallId ? openToolCalls.get(toolCallId) : void 0;
110
- if (info) {
111
- info.hasEnd = true;
112
- }
113
- break;
114
- }
115
- case EventType.TOOL_CALL_RESULT: {
116
- const toolCallId = event.toolCallId;
117
- const info = toolCallId ? openToolCalls.get(toolCallId) : void 0;
118
- if (info) {
119
- info.hasResult = true;
120
- }
121
- break;
122
- }
123
- default:
124
- break;
125
- }
126
- }
127
- const hasRunFinished = events.some((event) => event.type === EventType.RUN_FINISHED);
128
- const hasRunError = events.some((event) => event.type === EventType.RUN_ERROR);
129
- const hasTerminalEvent = hasRunFinished || hasRunError;
130
- const terminalEventMissing = !hasTerminalEvent;
131
- for (const messageId of openMessageIds) {
132
- const endEvent = {
133
- type: EventType.TEXT_MESSAGE_END,
134
- messageId
135
- };
136
- events.push(endEvent);
137
- appended.push(endEvent);
138
- }
139
- for (const [toolCallId, info] of openToolCalls) {
140
- if (!info.hasEnd) {
141
- const endEvent = {
142
- type: EventType.TOOL_CALL_END,
143
- toolCallId
144
- };
145
- events.push(endEvent);
146
- appended.push(endEvent);
147
- }
148
- if (terminalEventMissing && !info.hasResult) {
149
- const resultEvent = {
150
- type: EventType.TOOL_CALL_RESULT,
151
- toolCallId,
152
- messageId: `${toolCallId ?? randomUUID()}-result`,
153
- role: "tool",
154
- content: JSON.stringify(
155
- stopRequested ? {
156
- status: "stopped",
157
- reason: "stop_requested",
158
- message: resolvedStopMessage
159
- } : {
160
- status: "error",
161
- reason: "missing_terminal_event",
162
- message: resolvedAbruptMessage
163
- }
164
- )
165
- };
166
- events.push(resultEvent);
167
- appended.push(resultEvent);
168
- }
169
- }
170
- if (terminalEventMissing) {
171
- if (stopRequested) {
172
- const finishedEvent = {
173
- type: EventType.RUN_FINISHED
174
- };
175
- events.push(finishedEvent);
176
- appended.push(finishedEvent);
177
- } else {
178
- const errorEvent = {
179
- type: EventType.RUN_ERROR,
180
- message: resolvedAbruptMessage,
181
- code: "INCOMPLETE_STREAM"
182
- };
183
- events.push(errorEvent);
184
- appended.push(errorEvent);
185
- }
186
- }
187
- return appended;
188
- }
189
-
190
- // src/runner/in-memory.ts
69
+ import { finalizeRunEvents } from "@copilotkitnext/shared";
191
70
  var InMemoryEventStore = class {
192
- constructor(threadId) {
71
+ constructor(threadId, resourceIds, properties) {
193
72
  this.threadId = threadId;
73
+ this.resourceIds = resourceIds;
74
+ this.properties = properties;
194
75
  }
195
76
  /** The subject that current consumers subscribe to. */
196
77
  subject = null;
@@ -210,11 +91,41 @@ var InMemoryEventStore = class {
210
91
  currentEvents = null;
211
92
  };
212
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
+ }
213
101
  var InMemoryAgentRunner = class extends AgentRunner {
214
102
  run(request) {
215
103
  let existingStore = GLOBAL_STORE.get(request.threadId);
216
- if (!existingStore) {
217
- 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);
218
129
  GLOBAL_STORE.set(request.threadId, existingStore);
219
130
  }
220
131
  const store = existingStore;
@@ -234,7 +145,7 @@ var InMemoryAgentRunner = class extends AgentRunner {
234
145
  if ("messageId" in event && typeof event.messageId === "string") {
235
146
  historicMessageIds.add(event.messageId);
236
147
  }
237
- if (event.type === EventType2.RUN_STARTED) {
148
+ if (event.type === EventType.RUN_STARTED) {
238
149
  const runStarted = event;
239
150
  const messages = runStarted.input?.messages ?? [];
240
151
  for (const message of messages) {
@@ -255,12 +166,10 @@ var InMemoryAgentRunner = class extends AgentRunner {
255
166
  await request.agent.runAgent(request.input, {
256
167
  onEvent: ({ event }) => {
257
168
  let processedEvent = event;
258
- if (event.type === EventType2.RUN_STARTED) {
169
+ if (event.type === EventType.RUN_STARTED) {
259
170
  const runStartedEvent = event;
260
171
  if (!runStartedEvent.input) {
261
- const sanitizedMessages = request.input.messages ? request.input.messages.filter(
262
- (message) => !historicMessageIds.has(message.id)
263
- ) : void 0;
172
+ const sanitizedMessages = request.input.messages ? request.input.messages.filter((message) => !historicMessageIds.has(message.id)) : void 0;
264
173
  const updatedInput = {
265
174
  ...request.input,
266
175
  ...sanitizedMessages !== void 0 ? { messages: sanitizedMessages } : {}
@@ -357,7 +266,7 @@ var InMemoryAgentRunner = class extends AgentRunner {
357
266
  connect(request) {
358
267
  const store = GLOBAL_STORE.get(request.threadId);
359
268
  const connectionSubject = new ReplaySubject(Infinity);
360
- if (!store) {
269
+ if (!store || !matchesScope(store, request.scope)) {
361
270
  connectionSubject.complete();
362
271
  return connectionSubject.asObservable();
363
272
  }
@@ -393,54 +302,256 @@ var InMemoryAgentRunner = class extends AgentRunner {
393
302
  const store = GLOBAL_STORE.get(request.threadId);
394
303
  return Promise.resolve(store?.isRunning ?? false);
395
304
  }
396
- stop(request) {
305
+ async stop(request) {
397
306
  const store = GLOBAL_STORE.get(request.threadId);
398
- if (!store || !store.isRunning) {
399
- return Promise.resolve(false);
400
- }
401
- if (store.stopRequested) {
402
- return Promise.resolve(false);
307
+ if (!store) {
308
+ return false;
403
309
  }
404
- store.stopRequested = true;
405
- store.isRunning = false;
406
- const agent = store.agent;
407
- if (!agent) {
408
- store.stopRequested = false;
310
+ if (store.isRunning) {
311
+ store.stopRequested = true;
409
312
  store.isRunning = false;
410
- 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
+ }
411
326
  }
412
- try {
413
- agent.abortRun();
414
- return Promise.resolve(true);
415
- } catch (error) {
416
- console.error("Failed to abort agent run", error);
417
- store.stopRequested = false;
418
- store.isRunning = true;
419
- 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);
420
460
  }
421
461
  }
422
462
  };
423
463
 
424
464
  // src/runtime.ts
425
465
  var VERSION = package_default.version;
426
- 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
+ }
427
515
  agents;
428
516
  transcriptionService;
429
517
  beforeRequestMiddleware;
430
518
  afterRequestMiddleware;
431
519
  runner;
520
+ resolveThreadsScope;
521
+ suppressResourceIdWarning;
432
522
  constructor({
433
523
  agents,
434
524
  transcriptionService,
435
525
  beforeRequestMiddleware,
436
526
  afterRequestMiddleware,
437
- runner
527
+ runner,
528
+ resolveThreadsScope,
529
+ suppressResourceIdWarning = false
438
530
  }) {
439
531
  this.agents = agents;
440
532
  this.transcriptionService = transcriptionService;
441
533
  this.beforeRequestMiddleware = beforeRequestMiddleware;
442
534
  this.afterRequestMiddleware = afterRequestMiddleware;
443
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
+ }
444
555
  }
445
556
  };
446
557
 
@@ -449,15 +560,9 @@ import { Hono } from "hono";
449
560
  import { cors } from "hono/cors";
450
561
 
451
562
  // src/handlers/handle-run.ts
452
- import {
453
- RunAgentInputSchema
454
- } from "@ag-ui/client";
563
+ import { RunAgentInputSchema } from "@ag-ui/client";
455
564
  import { EventEncoder } from "@ag-ui/encoder";
456
- async function handleRunAgent({
457
- runtime,
458
- request,
459
- agentId
460
- }) {
565
+ async function handleRunAgent({ runtime, request, agentId }) {
461
566
  try {
462
567
  const agents = await runtime.agents;
463
568
  if (!agents[agentId]) {
@@ -494,6 +599,23 @@ async function handleRunAgent({
494
599
  const writer = stream.writable.getWriter();
495
600
  const encoder = new EventEncoder();
496
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
+ };
497
619
  (async () => {
498
620
  let input;
499
621
  try {
@@ -507,13 +629,26 @@ async function handleRunAgent({
507
629
  { status: 400 }
508
630
  );
509
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
+ }
510
637
  agent.setMessages(input.messages);
511
638
  agent.setState(input.state);
512
639
  agent.threadId = input.threadId;
513
- 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({
514
648
  threadId: input.threadId,
515
649
  agent,
516
- input
650
+ input,
651
+ scope
517
652
  }).subscribe({
518
653
  next: async (event) => {
519
654
  if (!request.signal.aborted && !streamClosed) {
@@ -528,42 +663,39 @@ async function handleRunAgent({
528
663
  },
529
664
  error: async (error) => {
530
665
  console.error("Error running agent:", error);
531
- if (!streamClosed) {
532
- try {
533
- await writer.close();
534
- streamClosed = true;
535
- } catch {
536
- }
537
- }
666
+ cleanupAbortListener();
667
+ await closeStream();
538
668
  },
539
669
  complete: async () => {
540
- if (!streamClosed) {
541
- try {
542
- await writer.close();
543
- streamClosed = true;
544
- } catch {
545
- }
546
- }
670
+ cleanupAbortListener();
671
+ await closeStream();
547
672
  }
548
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
+ }
549
687
  })().catch((error) => {
550
688
  console.error("Error running agent:", error);
551
- console.error(
552
- "Error stack:",
553
- error instanceof Error ? error.stack : "No stack trace"
554
- );
689
+ console.error("Error stack:", error instanceof Error ? error.stack : "No stack trace");
555
690
  console.error("Error details:", {
556
691
  name: error instanceof Error ? error.name : "Unknown",
557
692
  message: error instanceof Error ? error.message : String(error),
558
693
  cause: error instanceof Error ? error.cause : void 0
559
694
  });
560
- if (!streamClosed) {
561
- try {
562
- writer.close();
563
- streamClosed = true;
564
- } catch {
565
- }
566
- }
695
+ subscription?.unsubscribe();
696
+ subscription = void 0;
697
+ cleanupAbortListener();
698
+ void closeStream();
567
699
  });
568
700
  return new Response(stream.readable, {
569
701
  status: 200,
@@ -575,10 +707,7 @@ async function handleRunAgent({
575
707
  });
576
708
  } catch (error) {
577
709
  console.error("Error running agent:", error);
578
- console.error(
579
- "Error stack:",
580
- error instanceof Error ? error.stack : "No stack trace"
581
- );
710
+ console.error("Error stack:", error instanceof Error ? error.stack : "No stack trace");
582
711
  console.error("Error details:", {
583
712
  name: error instanceof Error ? error.name : "Unknown",
584
713
  message: error instanceof Error ? error.message : String(error),
@@ -736,10 +865,10 @@ async function handleTranscribe({
736
865
  }
737
866
 
738
867
  // src/endpoint.ts
739
- import { logger as logger2 } from "@copilotkitnext/shared";
868
+ import { logger as logger3 } from "@copilotkitnext/shared";
740
869
 
741
870
  // src/middleware.ts
742
- import { logger } from "@copilotkitnext/shared";
871
+ import { logger as logger2 } from "@copilotkitnext/shared";
743
872
  async function callBeforeRequestMiddleware({
744
873
  runtime,
745
874
  request,
@@ -750,7 +879,7 @@ async function callBeforeRequestMiddleware({
750
879
  if (typeof mw === "function") {
751
880
  return mw({ runtime, request, path });
752
881
  }
753
- logger.warn({ mw }, "Unsupported beforeRequestMiddleware value \u2013 skipped");
882
+ logger2.warn({ mw }, "Unsupported beforeRequestMiddleware value \u2013 skipped");
754
883
  return;
755
884
  }
756
885
  async function callAfterRequestMiddleware({
@@ -763,17 +892,13 @@ async function callAfterRequestMiddleware({
763
892
  if (typeof mw === "function") {
764
893
  return mw({ runtime, response, path });
765
894
  }
766
- logger.warn({ mw }, "Unsupported afterRequestMiddleware value \u2013 skipped");
895
+ logger2.warn({ mw }, "Unsupported afterRequestMiddleware value \u2013 skipped");
767
896
  }
768
897
 
769
898
  // src/handlers/handle-connect.ts
770
899
  import { RunAgentInputSchema as RunAgentInputSchema2 } from "@ag-ui/client";
771
900
  import { EventEncoder as EventEncoder2 } from "@ag-ui/encoder";
772
- async function handleConnectAgent({
773
- runtime,
774
- request,
775
- agentId
776
- }) {
901
+ async function handleConnectAgent({ runtime, request, agentId }) {
777
902
  try {
778
903
  const agents = await runtime.agents;
779
904
  if (!agents[agentId]) {
@@ -792,6 +917,23 @@ async function handleConnectAgent({
792
917
  const writer = stream.writable.getWriter();
793
918
  const encoder = new EventEncoder2();
794
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
+ };
795
937
  (async () => {
796
938
  let input;
797
939
  try {
@@ -805,8 +947,14 @@ async function handleConnectAgent({
805
947
  { status: 400 }
806
948
  );
807
949
  }
808
- runtime.runner.connect({
809
- 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
810
958
  }).subscribe({
811
959
  next: async (event) => {
812
960
  if (!request.signal.aborted && !streamClosed) {
@@ -821,42 +969,38 @@ async function handleConnectAgent({
821
969
  },
822
970
  error: async (error) => {
823
971
  console.error("Error running agent:", error);
824
- if (!streamClosed) {
825
- try {
826
- await writer.close();
827
- streamClosed = true;
828
- } catch {
829
- }
830
- }
972
+ cleanupAbortListener();
973
+ await closeStream();
831
974
  },
832
975
  complete: async () => {
833
- if (!streamClosed) {
834
- try {
835
- await writer.close();
836
- streamClosed = true;
837
- } catch {
838
- }
839
- }
976
+ cleanupAbortListener();
977
+ await closeStream();
840
978
  }
841
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
+ }
842
992
  })().catch((error) => {
843
993
  console.error("Error running agent:", error);
844
- console.error(
845
- "Error stack:",
846
- error instanceof Error ? error.stack : "No stack trace"
847
- );
994
+ console.error("Error stack:", error instanceof Error ? error.stack : "No stack trace");
848
995
  console.error("Error details:", {
849
996
  name: error instanceof Error ? error.name : "Unknown",
850
997
  message: error instanceof Error ? error.message : String(error),
851
998
  cause: error instanceof Error ? error.cause : void 0
852
999
  });
853
- if (!streamClosed) {
854
- try {
855
- writer.close();
856
- streamClosed = true;
857
- } catch {
858
- }
859
- }
1000
+ subscription?.unsubscribe();
1001
+ subscription = void 0;
1002
+ cleanupAbortListener();
1003
+ void closeStream();
860
1004
  });
861
1005
  return new Response(stream.readable, {
862
1006
  status: 200,
@@ -868,10 +1012,7 @@ async function handleConnectAgent({
868
1012
  });
869
1013
  } catch (error) {
870
1014
  console.error("Error running agent:", error);
871
- console.error(
872
- "Error stack:",
873
- error instanceof Error ? error.stack : "No stack trace"
874
- );
1015
+ console.error("Error stack:", error instanceof Error ? error.stack : "No stack trace");
875
1016
  console.error("Error details:", {
876
1017
  name: error instanceof Error ? error.name : "Unknown",
877
1018
  message: error instanceof Error ? error.message : String(error),
@@ -891,7 +1032,7 @@ async function handleConnectAgent({
891
1032
  }
892
1033
 
893
1034
  // src/handlers/handle-stop.ts
894
- import { EventType as EventType3 } from "@ag-ui/client";
1035
+ import { EventType as EventType2 } from "@ag-ui/client";
895
1036
  async function handleStopAgent({
896
1037
  runtime,
897
1038
  request,
@@ -912,7 +1053,35 @@ async function handleStopAgent({
912
1053
  }
913
1054
  );
914
1055
  }
915
- 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 });
916
1085
  if (!stopped) {
917
1086
  return new Response(
918
1087
  JSON.stringify({
@@ -929,7 +1098,7 @@ async function handleStopAgent({
929
1098
  JSON.stringify({
930
1099
  stopped: true,
931
1100
  interrupt: {
932
- type: EventType3.RUN_ERROR,
1101
+ type: EventType2.RUN_ERROR,
933
1102
  message: "Run stopped by user",
934
1103
  code: "STOPPED"
935
1104
  }
@@ -954,6 +1123,141 @@ async function handleStopAgent({
954
1123
  }
955
1124
  }
956
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
+
957
1261
  // src/endpoint.ts
958
1262
  function createCopilotEndpoint({ runtime, basePath }) {
959
1263
  const app = new Hono();
@@ -977,7 +1281,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
977
1281
  c.set("modifiedRequest", maybeModifiedRequest);
978
1282
  }
979
1283
  } catch (error) {
980
- 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");
981
1285
  if (error instanceof Response) {
982
1286
  return error;
983
1287
  }
@@ -993,7 +1297,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
993
1297
  response,
994
1298
  path
995
1299
  }).catch((error) => {
996
- 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");
997
1301
  });
998
1302
  }).post("/agent/:agentId/run", async (c) => {
999
1303
  const agentId = c.req.param("agentId");
@@ -1005,7 +1309,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
1005
1309
  agentId
1006
1310
  });
1007
1311
  } catch (error) {
1008
- 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");
1009
1313
  throw error;
1010
1314
  }
1011
1315
  }).post("/agent/:agentId/connect", async (c) => {
@@ -1018,7 +1322,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
1018
1322
  agentId
1019
1323
  });
1020
1324
  } catch (error) {
1021
- 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");
1022
1326
  throw error;
1023
1327
  }
1024
1328
  }).post("/agent/:agentId/stop/:threadId", async (c) => {
@@ -1033,7 +1337,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
1033
1337
  threadId
1034
1338
  });
1035
1339
  } catch (error) {
1036
- 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");
1037
1341
  throw error;
1038
1342
  }
1039
1343
  }).get("/info", async (c) => {
@@ -1044,7 +1348,7 @@ function createCopilotEndpoint({ runtime, basePath }) {
1044
1348
  request
1045
1349
  });
1046
1350
  } catch (error) {
1047
- 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");
1048
1352
  throw error;
1049
1353
  }
1050
1354
  }).post("/transcribe", async (c) => {
@@ -1055,19 +1359,102 @@ function createCopilotEndpoint({ runtime, basePath }) {
1055
1359
  request
1056
1360
  });
1057
1361
  } catch (error) {
1058
- 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");
1059
1400
  throw error;
1060
1401
  }
1061
1402
  }).notFound((c) => {
1062
1403
  return c.json({ error: "Not found" }, 404);
1063
1404
  });
1064
1405
  }
1406
+
1407
+ // src/runner/index.ts
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
+ }
1065
1448
  export {
1066
1449
  AgentRunner,
1067
1450
  CopilotRuntime,
1068
1451
  InMemoryAgentRunner,
1069
1452
  VERSION,
1070
1453
  createCopilotEndpoint,
1071
- finalizeRunEvents
1454
+ createFilteringThreadScopeResolver,
1455
+ createStrictThreadScopeResolver,
1456
+ filterAuthorizedResourceIds,
1457
+ finalizeRunEvents2 as finalizeRunEvents,
1458
+ validateResourceIdMatch
1072
1459
  };
1073
1460
  //# sourceMappingURL=index.mjs.map