@copilotkit/runtime 1.81.0 → 1.83.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.
Files changed (156) hide show
  1. package/dist/agent/index.cjs +18 -10
  2. package/dist/agent/index.cjs.map +1 -1
  3. package/dist/agent/index.d.cts +41 -1
  4. package/dist/agent/index.d.cts.map +1 -1
  5. package/dist/agent/index.d.mts +42 -2
  6. package/dist/agent/index.d.mts.map +1 -1
  7. package/dist/agent/index.mjs +18 -10
  8. package/dist/agent/index.mjs.map +1 -1
  9. package/dist/agent/url-download.cjs +504 -0
  10. package/dist/agent/url-download.cjs.map +1 -0
  11. package/dist/agent/url-download.d.cts +80 -0
  12. package/dist/agent/url-download.d.cts.map +1 -0
  13. package/dist/agent/url-download.d.mts +80 -0
  14. package/dist/agent/url-download.d.mts.map +1 -0
  15. package/dist/agent/url-download.mjs +501 -0
  16. package/dist/agent/url-download.mjs.map +1 -0
  17. package/dist/lib/runtime/copilot-runtime.cjs +2 -1
  18. package/dist/lib/runtime/copilot-runtime.cjs.map +1 -1
  19. package/dist/lib/runtime/copilot-runtime.d.cts +5 -0
  20. package/dist/lib/runtime/copilot-runtime.d.cts.map +1 -1
  21. package/dist/lib/runtime/copilot-runtime.d.mts +5 -0
  22. package/dist/lib/runtime/copilot-runtime.d.mts.map +1 -1
  23. package/dist/lib/runtime/copilot-runtime.mjs +2 -1
  24. package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
  25. package/dist/lib/runtime/telemetry-agent-runner.cjs +4 -1
  26. package/dist/lib/runtime/telemetry-agent-runner.cjs.map +1 -1
  27. package/dist/lib/runtime/telemetry-agent-runner.d.cts +7 -1
  28. package/dist/lib/runtime/telemetry-agent-runner.d.cts.map +1 -1
  29. package/dist/lib/runtime/telemetry-agent-runner.d.mts +7 -1
  30. package/dist/lib/runtime/telemetry-agent-runner.d.mts.map +1 -1
  31. package/dist/lib/runtime/telemetry-agent-runner.mjs +4 -1
  32. package/dist/lib/runtime/telemetry-agent-runner.mjs.map +1 -1
  33. package/dist/package.cjs +1 -1
  34. package/dist/package.mjs +1 -1
  35. package/dist/v2/index.cjs +12 -3
  36. package/dist/v2/index.d.cts +5 -3
  37. package/dist/v2/index.d.mts +5 -3
  38. package/dist/v2/index.mjs +6 -5
  39. package/dist/v2/runtime/core/channel-manager.cjs +2 -1
  40. package/dist/v2/runtime/core/channel-manager.cjs.map +1 -1
  41. package/dist/v2/runtime/core/channel-manager.mjs +2 -1
  42. package/dist/v2/runtime/core/channel-manager.mjs.map +1 -1
  43. package/dist/v2/runtime/core/fetch-handler.cjs +3 -3
  44. package/dist/v2/runtime/core/fetch-handler.cjs.map +1 -1
  45. package/dist/v2/runtime/core/fetch-handler.mjs +3 -3
  46. package/dist/v2/runtime/core/fetch-handler.mjs.map +1 -1
  47. package/dist/v2/runtime/core/package-info.cjs +1 -1
  48. package/dist/v2/runtime/core/package-info.mjs +1 -1
  49. package/dist/v2/runtime/core/runtime.cjs +12 -2
  50. package/dist/v2/runtime/core/runtime.cjs.map +1 -1
  51. package/dist/v2/runtime/core/runtime.d.cts +40 -1
  52. package/dist/v2/runtime/core/runtime.d.cts.map +1 -1
  53. package/dist/v2/runtime/core/runtime.d.mts +40 -1
  54. package/dist/v2/runtime/core/runtime.d.mts.map +1 -1
  55. package/dist/v2/runtime/core/runtime.mjs +12 -2
  56. package/dist/v2/runtime/core/runtime.mjs.map +1 -1
  57. package/dist/v2/runtime/handlers/get-runtime-info.cjs +6 -6
  58. package/dist/v2/runtime/handlers/get-runtime-info.cjs.map +1 -1
  59. package/dist/v2/runtime/handlers/get-runtime-info.mjs +6 -6
  60. package/dist/v2/runtime/handlers/get-runtime-info.mjs.map +1 -1
  61. package/dist/v2/runtime/handlers/handle-connect.cjs +9 -1
  62. package/dist/v2/runtime/handlers/handle-connect.cjs.map +1 -1
  63. package/dist/v2/runtime/handlers/handle-connect.mjs +9 -1
  64. package/dist/v2/runtime/handlers/handle-connect.mjs.map +1 -1
  65. package/dist/v2/runtime/handlers/handle-inspector-learning.cjs +1 -1
  66. package/dist/v2/runtime/handlers/handle-inspector-learning.mjs +1 -1
  67. package/dist/v2/runtime/handlers/handle-run.cjs +16 -2
  68. package/dist/v2/runtime/handlers/handle-run.cjs.map +1 -1
  69. package/dist/v2/runtime/handlers/handle-run.mjs +16 -2
  70. package/dist/v2/runtime/handlers/handle-run.mjs.map +1 -1
  71. package/dist/v2/runtime/handlers/handle-stop.cjs +14 -3
  72. package/dist/v2/runtime/handlers/handle-stop.cjs.map +1 -1
  73. package/dist/v2/runtime/handlers/handle-stop.mjs +14 -3
  74. package/dist/v2/runtime/handlers/handle-stop.mjs.map +1 -1
  75. package/dist/v2/runtime/handlers/handle-trajectory-connect.cjs +1 -1
  76. package/dist/v2/runtime/handlers/handle-trajectory-connect.mjs +1 -1
  77. package/dist/v2/runtime/handlers/intelligence/annotate.cjs +2 -2
  78. package/dist/v2/runtime/handlers/intelligence/annotate.mjs +2 -2
  79. package/dist/v2/runtime/handlers/intelligence/memories.cjs +1 -1
  80. package/dist/v2/runtime/handlers/intelligence/memories.mjs +1 -1
  81. package/dist/v2/runtime/handlers/intelligence/run.cjs +7 -4
  82. package/dist/v2/runtime/handlers/intelligence/run.cjs.map +1 -1
  83. package/dist/v2/runtime/handlers/intelligence/run.mjs +7 -4
  84. package/dist/v2/runtime/handlers/intelligence/run.mjs.map +1 -1
  85. package/dist/v2/runtime/handlers/intelligence/threads.cjs +137 -31
  86. package/dist/v2/runtime/handlers/intelligence/threads.cjs.map +1 -1
  87. package/dist/v2/runtime/handlers/intelligence/threads.mjs +138 -32
  88. package/dist/v2/runtime/handlers/intelligence/threads.mjs.map +1 -1
  89. package/dist/v2/runtime/handlers/shared/agent-utils.cjs +3 -3
  90. package/dist/v2/runtime/handlers/shared/agent-utils.mjs +3 -3
  91. package/dist/v2/runtime/handlers/shared/mcp-apps-servers.cjs +2 -1
  92. package/dist/v2/runtime/handlers/shared/mcp-apps-servers.cjs.map +1 -1
  93. package/dist/v2/runtime/handlers/shared/mcp-apps-servers.mjs +3 -1
  94. package/dist/v2/runtime/handlers/shared/mcp-apps-servers.mjs.map +1 -1
  95. package/dist/v2/runtime/handlers/shared/platform-error.cjs +1 -1
  96. package/dist/v2/runtime/handlers/shared/platform-error.mjs +1 -1
  97. package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.cjs +22 -3
  98. package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.cjs.map +1 -1
  99. package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.mjs +22 -4
  100. package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.mjs.map +1 -1
  101. package/dist/v2/runtime/handlers/shared/sse-response.cjs +20 -3
  102. package/dist/v2/runtime/handlers/shared/sse-response.cjs.map +1 -1
  103. package/dist/v2/runtime/handlers/shared/sse-response.mjs +20 -3
  104. package/dist/v2/runtime/handlers/shared/sse-response.mjs.map +1 -1
  105. package/dist/v2/runtime/handlers/shared/thread-access.cjs +138 -0
  106. package/dist/v2/runtime/handlers/shared/thread-access.cjs.map +1 -0
  107. package/dist/v2/runtime/handlers/shared/thread-access.d.cts +29 -0
  108. package/dist/v2/runtime/handlers/shared/thread-access.d.cts.map +1 -0
  109. package/dist/v2/runtime/handlers/shared/thread-access.d.mts +29 -0
  110. package/dist/v2/runtime/handlers/shared/thread-access.d.mts.map +1 -0
  111. package/dist/v2/runtime/handlers/shared/thread-access.mjs +133 -0
  112. package/dist/v2/runtime/handlers/shared/thread-access.mjs.map +1 -0
  113. package/dist/v2/runtime/handlers/sse/connect.cjs +2 -1
  114. package/dist/v2/runtime/handlers/sse/connect.cjs.map +1 -1
  115. package/dist/v2/runtime/handlers/sse/connect.mjs +2 -1
  116. package/dist/v2/runtime/handlers/sse/connect.mjs.map +1 -1
  117. package/dist/v2/runtime/handlers/sse/run.cjs +23 -6
  118. package/dist/v2/runtime/handlers/sse/run.cjs.map +1 -1
  119. package/dist/v2/runtime/handlers/sse/run.mjs +23 -6
  120. package/dist/v2/runtime/handlers/sse/run.mjs.map +1 -1
  121. package/dist/v2/runtime/index.d.cts +3 -2
  122. package/dist/v2/runtime/index.d.cts.map +1 -1
  123. package/dist/v2/runtime/index.d.mts +3 -2
  124. package/dist/v2/runtime/index.d.mts.map +1 -1
  125. package/dist/v2/runtime/intelligence-platform/client.cjs +17 -4
  126. package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
  127. package/dist/v2/runtime/intelligence-platform/client.d.cts +9 -37
  128. package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
  129. package/dist/v2/runtime/intelligence-platform/client.d.mts +9 -37
  130. package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
  131. package/dist/v2/runtime/intelligence-platform/client.mjs +17 -4
  132. package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
  133. package/dist/v2/runtime/runner/agent-runner.cjs +55 -0
  134. package/dist/v2/runtime/runner/agent-runner.cjs.map +1 -1
  135. package/dist/v2/runtime/runner/agent-runner.d.cts +90 -6
  136. package/dist/v2/runtime/runner/agent-runner.d.cts.map +1 -1
  137. package/dist/v2/runtime/runner/agent-runner.d.mts +90 -6
  138. package/dist/v2/runtime/runner/agent-runner.d.mts.map +1 -1
  139. package/dist/v2/runtime/runner/agent-runner.mjs +50 -1
  140. package/dist/v2/runtime/runner/agent-runner.mjs.map +1 -1
  141. package/dist/v2/runtime/runner/in-memory.cjs +205 -25
  142. package/dist/v2/runtime/runner/in-memory.cjs.map +1 -1
  143. package/dist/v2/runtime/runner/in-memory.d.cts +112 -15
  144. package/dist/v2/runtime/runner/in-memory.d.cts.map +1 -1
  145. package/dist/v2/runtime/runner/in-memory.d.mts +112 -15
  146. package/dist/v2/runtime/runner/in-memory.d.mts.map +1 -1
  147. package/dist/v2/runtime/runner/in-memory.mjs +206 -27
  148. package/dist/v2/runtime/runner/in-memory.mjs.map +1 -1
  149. package/dist/v2/runtime/runner/index.d.cts +2 -2
  150. package/dist/v2/runtime/runner/index.d.mts +2 -2
  151. package/dist/v2/runtime/runner/index.mjs +2 -2
  152. package/dist/v2/runtime/telemetry/telemetry-client.cjs +1 -1
  153. package/dist/v2/runtime/telemetry/telemetry-client.cjs.map +1 -1
  154. package/dist/v2/runtime/telemetry/telemetry-client.mjs +1 -1
  155. package/dist/v2/runtime/telemetry/telemetry-client.mjs.map +1 -1
  156. package/package.json +6 -6
@@ -2,6 +2,7 @@ require("reflect-metadata");
2
2
  const require_runtime = require('../../../../_virtual/_rolldown/runtime.cjs');
3
3
  const require_logger = require('../../../../lib/logger.cjs');
4
4
  const require_sse_keep_alive = require('./sse-keep-alive.cjs');
5
+ const require_agent_runner = require('../../runner/agent-runner.cjs');
5
6
  const require_telemetry_client = require('../../telemetry/telemetry-client.cjs');
6
7
  let _copilotkit_shared = require("@copilotkit/shared");
7
8
  let _ag_ui_client = require("@ag-ui/client");
@@ -42,6 +43,10 @@ function createSseEventResponse({ request, observableFactory, debugEventBus, age
42
43
  type: _ag_ui_client.EventType.RUN_ERROR,
43
44
  message: "Thread already running",
44
45
  code: _copilotkit_shared.THREAD_ALREADY_RUNNING_ERROR_CODE
46
+ } : require_agent_runner.isThreadAccessDeniedError(error) ? {
47
+ type: _ag_ui_client.EventType.RUN_ERROR,
48
+ message: "Thread access denied",
49
+ code: require_agent_runner.THREAD_ACCESS_DENIED_ERROR_CODE
45
50
  } : {
46
51
  type: _ag_ui_client.EventType.RUN_ERROR,
47
52
  message: "Agent run failed"
@@ -70,6 +75,9 @@ function createSseEventResponse({ request, observableFactory, debugEventBus, age
70
75
  };
71
76
  let subscription;
72
77
  let agentErrorReported = false;
78
+ const logAccessDenied = (error) => {
79
+ console.warn("[CopilotKit] Run refused: the thread belongs to another identity.", error instanceof Error ? error.message : String(error));
80
+ };
73
81
  const reportAgentError = (error, phase) => {
74
82
  if (agentErrorReported) return;
75
83
  agentErrorReported = true;
@@ -136,10 +144,16 @@ function createSseEventResponse({ request, observableFactory, debugEventBus, age
136
144
  }
137
145
  },
138
146
  error: async (error) => {
147
+ if (require_agent_runner.isThreadAccessDeniedError(error)) {
148
+ logAccessDenied(error);
149
+ await writeRunError(error);
150
+ await closeStream();
151
+ return;
152
+ }
139
153
  reportAgentError(error, "sse.subscription");
140
154
  if (captureTelemetry) telemetry$1.capture("oss.runtime.agent_execution_stream_errored", {
141
155
  ...executionInfo,
142
- error: error instanceof Error ? error.message : String(error)
156
+ error: "AGENT_EXECUTION_FAILED"
143
157
  });
144
158
  if (debug?.lifecycle) debugLogger.debug({ error: error instanceof Error ? error.message : String(error) }, "SSE stream errored");
145
159
  logError(error);
@@ -157,8 +171,11 @@ function createSseEventResponse({ request, observableFactory, debugEventBus, age
157
171
  });
158
172
  if (request.signal.aborted) subscription.unsubscribe();
159
173
  })().catch(async (error) => {
160
- reportAgentError(error, "sse.factory");
161
- logError(error);
174
+ if (require_agent_runner.isThreadAccessDeniedError(error)) logAccessDenied(error);
175
+ else {
176
+ reportAgentError(error, "sse.factory");
177
+ logError(error);
178
+ }
162
179
  await writeRunError(error);
163
180
  await closeStream();
164
181
  });
@@ -1 +1 @@
1
- {"version":3,"file":"sse-response.cjs","names":["THREAD_ALREADY_RUNNING_ERROR_CODE","defaultTelemetry","resolveSseKeepAliveIntervalSeconds","EventEncoder","createLogger","EventType","keepAliveSse"],"sources":["../../../../../src/v2/runtime/handlers/shared/sse-response.ts"],"sourcesContent":["import { EventType } from \"@ag-ui/client\";\nimport type { BaseEvent } from \"@ag-ui/client\";\nimport { EventEncoder } from \"@ag-ui/encoder\";\nimport type { Observable, Subscription } from \"rxjs\";\nimport {\n keepAliveSse,\n resolveSseKeepAliveIntervalSeconds,\n} from \"./sse-keep-alive\";\nimport { THREAD_ALREADY_RUNNING_ERROR_CODE } from \"@copilotkit/shared\";\nimport type { ResolvedDebugConfig } from \"@copilotkit/shared\";\nimport { createLogger } from \"../../../../v1-deprecated/lib/logger\";\nimport type { CopilotRuntimeLogger } from \"../../../../v1-deprecated/lib/logger\";\nimport { telemetry as defaultTelemetry } from \"../../telemetry\";\nimport type { TelemetryCapture } from \"../../telemetry/telemetry-client\";\nimport type { AgentExecutionResponseInfo } from \"../../telemetry/events\";\nimport type { DebugEventBus } from \"../../core/debug-event-bus\";\nimport type {\n RuntimeErrorPhase,\n RuntimeErrorReporter,\n} from \"../../core/runtime-error-reporter\";\n\n/**\n * Whether a runner rejected the run because the thread is busy. Matches the\n * `code` of {@link ThreadAlreadyRunningError}, and the plain message that\n * custom runners copied from the built-in ones still throw.\n */\nfunction isThreadAlreadyRunning(error: unknown): boolean {\n return (\n error instanceof Error &&\n ((error as { code?: unknown }).code === THREAD_ALREADY_RUNNING_ERROR_CODE ||\n error.message === \"Thread already running\")\n );\n}\n\ninterface CreateSseEventResponseParams {\n request: Request;\n observableFactory: () =>\n | Promise<Observable<BaseEvent>>\n | Observable<BaseEvent>;\n debugEventBus?: DebugEventBus;\n agentId?: string;\n debug?: ResolvedDebugConfig;\n /** Pre-created logger instance to avoid creating a new pino logger per request. */\n logger?: CopilotRuntimeLogger;\n /** Runtime-bound telemetry capture. Falls back for external direct callers. */\n telemetry?: TelemetryCapture;\n /**\n * Execution facts known before the stream opens, merged under anything the\n * upstream reports as it runs. Carries `llmHostClass`, which is knowable\n * from the agent's configuration and never appears on a streamed event.\n */\n executionSeed?: AgentExecutionResponseInfo;\n /**\n * Whether to emit `oss.runtime.agent_execution_stream_*` telemetry for this\n * stream. Defaults to `true`. The stateless `/suggest` path sets this to\n * `false`: a suggestion is a side-effect-free structured completion, not a\n * tracked run, and under `available: \"always\"` it fires often enough to flood\n * run telemetry.\n */\n captureTelemetry?: boolean;\n runtimeErrorReporter?: RuntimeErrorReporter;\n startTime?: number;\n /**\n * Seconds of silence before a `: keep-alive` SSE comment is written.\n * `0` disables the keep-alive. Resolved by\n * {@link resolveSseKeepAliveIntervalSeconds}, which supplies the default\n * and enforces the timer bound for every caller.\n */\n keepAliveIntervalSeconds?: number;\n}\n\nexport function createSseEventResponse({\n request,\n observableFactory,\n debugEventBus,\n agentId,\n debug,\n logger,\n telemetry = defaultTelemetry,\n executionSeed,\n captureTelemetry = true,\n runtimeErrorReporter,\n startTime,\n keepAliveIntervalSeconds,\n}: CreateSseEventResponseParams): Response {\n const keepAliveSeconds = resolveSseKeepAliveIntervalSeconds(\n keepAliveIntervalSeconds,\n );\n const stream = new TransformStream();\n const writer = stream.writable.getWriter();\n const encoder = new EventEncoder();\n let streamClosed = false;\n // Set once the current run's RUN_FINISHED or RUN_ERROR has been queued for\n // the client. AG-UI allows nothing after either until the next RUN_STARTED,\n // so a later failure must not add a RUN_ERROR.\n let terminalEventWritten = false;\n let debugThreadId = \"\";\n let debugRunId = \"\";\n\n const debugLogger = debug?.enabled\n ? (logger ??\n createLogger({ level: \"debug\", component: \"copilotkit-debug\" }))\n : undefined;\n\n const closeStream = async () => {\n if (!streamClosed) {\n try {\n await writer.close();\n streamClosed = true;\n } catch {\n // Stream already closed.\n }\n }\n };\n\n // Report a failure to the client as a terminal RUN_ERROR. Without it the\n // client sees an empty stream that ends normally, so its run \"succeeds\"\n // with no reply and no error (e.g. \"Thread already running\" on a busy\n // thread). Only a busy thread is named, with a code clients can branch on;\n // anything else may carry internal detail, so the client gets a generic\n // message and the full error stays in the server log.\n const writeRunError = async (error: unknown) => {\n if (terminalEventWritten || streamClosed || request.signal.aborted) {\n return;\n }\n terminalEventWritten = true;\n const event = isThreadAlreadyRunning(error)\n ? {\n type: EventType.RUN_ERROR,\n message: \"Thread already running\",\n code: THREAD_ALREADY_RUNNING_ERROR_CODE,\n }\n : { type: EventType.RUN_ERROR, message: \"Agent run failed\" };\n // The inspector sees every streamed event, so it needs this one too or\n // the run it saw start never ends there. A run that never started has\n // no ids to file it under, so it is not sent.\n if (debugEventBus && debugRunId) {\n try {\n debugEventBus.broadcast(event as BaseEvent, {\n agentId: agentId ?? \"\",\n threadId: debugThreadId,\n runId: debugRunId,\n });\n } catch (broadcastError) {\n logError(broadcastError);\n }\n }\n try {\n await writer.write(encoder.encodeBinary(event as BaseEvent));\n } catch {\n // The client is already gone; closeStream() handles the rest.\n }\n };\n\n const logError = (error: unknown) => {\n console.error(\"Error running agent:\", error);\n console.error(\n \"Error stack:\",\n error instanceof Error ? error.stack : \"No stack trace\",\n );\n console.error(\"Error details:\", {\n name: error instanceof Error ? error.name : \"Unknown\",\n message: error instanceof Error ? error.message : String(error),\n cause: error instanceof Error ? error.cause : undefined,\n });\n };\n\n let subscription: Subscription | undefined;\n let agentErrorReported = false;\n\n const reportAgentError = (error: unknown, phase: RuntimeErrorPhase) => {\n if (agentErrorReported) return;\n agentErrorReported = true;\n runtimeErrorReporter?.report({\n request,\n error,\n operation: \"agent.run\",\n agentId,\n threadId: debugThreadId || undefined,\n runId: debugRunId || undefined,\n phase,\n startTime,\n });\n };\n\n (async () => {\n const observable = await observableFactory();\n\n if (captureTelemetry) {\n telemetry.capture(\"oss.runtime.agent_execution_stream_started\", {});\n }\n\n if (debug?.lifecycle) {\n debugLogger!.debug(\"SSE stream opened\");\n }\n\n let eventCount = 0;\n let loggedEventCount = 0;\n\n // Provider/model/LangGraph facts, scraped off raw upstream events as\n // they pass and reported once the stream finishes. This moved here from\n // the v1 TelemetryAgentRunner, which wrapped the runner purely to\n // collect it and emitted its own duplicate copy of every stream event\n // to carry it. v2 callers had no equivalent and reported `{}`.\n // Seeded rather than empty: `llmHostClass` comes from the agent's config,\n // not from the wire, so nothing streamed will ever fill it in. Scraped\n // fields still win, since `collectExecutionInfo` writes over this.\n const executionInfo: AgentExecutionResponseInfo = { ...executionSeed };\n\n subscription = observable.subscribe({\n next: async (event) => {\n collectExecutionInfo(event, executionInfo);\n\n // Extract threadId/runId from RUN_STARTED\n if (event.type === \"RUN_STARTED\") {\n const e = event as { threadId?: string; runId?: string };\n debugThreadId = e.threadId ?? \"\";\n debugRunId = e.runId ?? \"\";\n }\n\n if (event.type === \"RUN_ERROR\") {\n const e = event as {\n message?: unknown;\n threadId?: string;\n runId?: string;\n };\n debugThreadId = e.threadId ?? debugThreadId;\n debugRunId = e.runId ?? debugRunId;\n reportAgentError(\n new Error(\n typeof e.message === \"string\"\n ? e.message\n : \"Runner reported a run error\",\n ),\n \"sse.subscription\",\n );\n }\n\n // Broadcast to debug listeners BEFORE the stream-closed gate below.\n // Intentional: debug subscribers (e.g. the VS Code Inspector panel)\n // should still receive trailing events after the SSE client for\n // this request closed its connection — they're independent\n // consumers observing the underlying runtime, not the request's\n // response stream.\n //\n // Wrapped in try/catch so a buggy debug subscriber can't propagate\n // an exception into this observer — if the throw reached the\n // `next` callback it would get routed to `error` by RxJS, closing\n // the SSE stream for an unrelated reason. Log via `logError` and\n // move on.\n if (debugEventBus) {\n try {\n debugEventBus.broadcast(event, {\n agentId: agentId ?? \"\",\n threadId: debugThreadId,\n runId: debugRunId,\n });\n } catch (broadcastError) {\n logError(broadcastError);\n }\n }\n\n if (!request.signal.aborted && !streamClosed) {\n try {\n eventCount++;\n if (debug?.events) {\n loggedEventCount++;\n if (debug.verbose) {\n debugLogger!.debug({ event }, \"Event emitted\");\n } else {\n debugLogger!.debug(\n { type: event.type, ...summarizeEvent(event) },\n \"Event emitted\",\n );\n }\n }\n // /connect replays finished runs before the live one, so a new\n // RUN_STARTED reopens the stream for a RUN_ERROR.\n if (event.type === EventType.RUN_STARTED) {\n terminalEventWritten = false;\n } else if (\n event.type === EventType.RUN_FINISHED ||\n event.type === EventType.RUN_ERROR\n ) {\n terminalEventWritten = true;\n }\n await writer.write(encoder.encodeBinary(event));\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n streamClosed = true;\n } else {\n // Non-abort write failures (backpressure disconnects,\n // transform-stream exceptions, …) were previously swallowed\n // silently — `streamClosed` stayed `false` and the next\n // event re-attempted a broken writer. Log and mark the\n // stream closed so we stop trying.\n logError(error);\n streamClosed = true;\n }\n }\n }\n },\n error: async (error) => {\n reportAgentError(error, \"sse.subscription\");\n if (captureTelemetry) {\n telemetry.capture(\"oss.runtime.agent_execution_stream_errored\", {\n ...executionInfo,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n if (debug?.lifecycle) {\n debugLogger!.debug(\n { error: error instanceof Error ? error.message : String(error) },\n \"SSE stream errored\",\n );\n }\n logError(error);\n await writeRunError(error);\n await closeStream();\n },\n complete: async () => {\n if (captureTelemetry) {\n telemetry.capture(\n \"oss.runtime.agent_execution_stream_ended\",\n executionInfo,\n );\n }\n if (debug?.lifecycle) {\n debugLogger!.debug(\n { eventCount, loggedEventCount },\n \"SSE stream completed\",\n );\n }\n await closeStream();\n },\n });\n\n // If the client disconnected before the subscription was created,\n // unsubscribe immediately to avoid leaking the observable.\n if (request.signal.aborted) {\n subscription.unsubscribe();\n }\n })().catch(async (error) => {\n reportAgentError(error, \"sse.factory\");\n logError(error);\n await writeRunError(error);\n await closeStream();\n });\n\n request.signal.addEventListener(\"abort\", () => {\n subscription?.unsubscribe();\n });\n\n const body =\n keepAliveSeconds > 0\n ? keepAliveSse(stream.readable, keepAliveSeconds * 1_000)\n : stream.readable;\n\n return new Response(body, {\n status: 200,\n headers: {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n },\n });\n}\n\nfunction summarizeEvent(event: BaseEvent): Record<string, unknown> {\n const e = event as any;\n const summary: Record<string, unknown> = {};\n\n if (e.messageId) summary.messageId = e.messageId;\n if (e.toolCallId) summary.toolCallId = e.toolCallId;\n if (e.toolCallName) summary.toolCallName = e.toolCallName;\n if (e.role) summary.role = e.role;\n if (e.delta != null && typeof e.delta === \"string\")\n summary.deltaLength = e.delta.length;\n if (e.snapshot && typeof e.snapshot === \"object\")\n summary.snapshotKeys = Object.keys(e.snapshot);\n if (e.delta && Array.isArray(e.delta))\n summary.operationCount = e.delta.length;\n if (e.threadId) summary.threadId = e.threadId;\n if (e.runId) summary.runId = e.runId;\n if (e.message) summary.message = e.message;\n if (e.code) summary.code = e.code;\n if (e.stepName) summary.stepName = e.stepName;\n\n return summary;\n}\n\n/**\n * Accumulate provider, model, and LangGraph facts from one upstream event.\n *\n * Mutates rather than returns so the caller keeps one record across the whole\n * stream: these arrive on different events and the last one wins. Only fields\n * the upstream actually sent are set, so an agent that reports none leaves the\n * record empty and the stream events carry `{}` as before.\n *\n * `rawEvent` is the untransformed upstream payload, present on AG-UI events\n * that wrap one. Its shape is the provider's, not ours, hence the narrowing.\n */\nfunction collectExecutionInfo(\n event: BaseEvent,\n into: AgentExecutionResponseInfo,\n): void {\n const rawEvent = (\n event as {\n rawEvent?: {\n metadata?: Record<string, unknown>;\n data?: Record<string, unknown>;\n };\n }\n ).rawEvent;\n if (!rawEvent) return;\n\n const model = (rawEvent.data as { output?: { model?: string } } | undefined)\n ?.output?.model;\n if (model) {\n into.model = model;\n // Carried forward from the v1 implementation, which set both from the\n // same field. The upstream sends no separate provider name.\n into.provider = model;\n }\n\n const metadata = rawEvent.metadata as\n | { langgraph_host?: string; langgraph_version?: string }\n | undefined;\n if (metadata?.langgraph_host) into.langGraphHost = metadata.langgraph_host;\n if (metadata?.langgraph_version) {\n into.langGraphVersion = metadata.langgraph_version;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sse-response.cjs","names":["THREAD_ALREADY_RUNNING_ERROR_CODE","defaultTelemetry","resolveSseKeepAliveIntervalSeconds","EventEncoder","createLogger","EventType","isThreadAccessDeniedError","THREAD_ACCESS_DENIED_ERROR_CODE","keepAliveSse"],"sources":["../../../../../src/v2/runtime/handlers/shared/sse-response.ts"],"sourcesContent":["import { EventType } from \"@ag-ui/client\";\nimport type { BaseEvent } from \"@ag-ui/client\";\nimport { EventEncoder } from \"@ag-ui/encoder\";\nimport type { Observable, Subscription } from \"rxjs\";\nimport {\n keepAliveSse,\n resolveSseKeepAliveIntervalSeconds,\n} from \"./sse-keep-alive\";\nimport { THREAD_ALREADY_RUNNING_ERROR_CODE } from \"@copilotkit/shared\";\nimport {\n THREAD_ACCESS_DENIED_ERROR_CODE,\n isThreadAccessDeniedError,\n} from \"../../runner/agent-runner\";\nimport type { ResolvedDebugConfig } from \"@copilotkit/shared\";\nimport { createLogger } from \"../../../../v1-deprecated/lib/logger\";\nimport type { CopilotRuntimeLogger } from \"../../../../v1-deprecated/lib/logger\";\nimport { telemetry as defaultTelemetry } from \"../../telemetry\";\nimport type { TelemetryCapture } from \"../../telemetry/telemetry-client\";\nimport type { AgentExecutionResponseInfo } from \"../../telemetry/events\";\nimport type { DebugEventBus } from \"../../core/debug-event-bus\";\nimport type {\n RuntimeErrorPhase,\n RuntimeErrorReporter,\n} from \"../../core/runtime-error-reporter\";\n\n/**\n * Whether a runner rejected the run because the thread is busy. Matches the\n * `code` of {@link ThreadAlreadyRunningError}, and the plain message that\n * custom runners copied from the built-in ones still throw.\n */\nfunction isThreadAlreadyRunning(error: unknown): boolean {\n return (\n error instanceof Error &&\n ((error as { code?: unknown }).code === THREAD_ALREADY_RUNNING_ERROR_CODE ||\n error.message === \"Thread already running\")\n );\n}\n\ninterface CreateSseEventResponseParams {\n request: Request;\n observableFactory: () =>\n | Promise<Observable<BaseEvent>>\n | Observable<BaseEvent>;\n debugEventBus?: DebugEventBus;\n agentId?: string;\n debug?: ResolvedDebugConfig;\n /** Pre-created logger instance to avoid creating a new pino logger per request. */\n logger?: CopilotRuntimeLogger;\n /** Runtime-bound telemetry capture. Falls back for external direct callers. */\n telemetry?: TelemetryCapture;\n /**\n * Execution facts known before the stream opens, merged under anything the\n * upstream reports as it runs. Carries `llmHostClass`, which is knowable\n * from the agent's configuration and never appears on a streamed event.\n */\n executionSeed?: AgentExecutionResponseInfo;\n /**\n * Whether to emit `oss.runtime.agent_execution_stream_*` telemetry for this\n * stream. Defaults to `true`. The stateless `/suggest` path sets this to\n * `false`: a suggestion is a side-effect-free structured completion, not a\n * tracked run, and under `available: \"always\"` it fires often enough to flood\n * run telemetry.\n */\n captureTelemetry?: boolean;\n runtimeErrorReporter?: RuntimeErrorReporter;\n startTime?: number;\n /**\n * Seconds of silence before a `: keep-alive` SSE comment is written.\n * `0` disables the keep-alive. Resolved by\n * {@link resolveSseKeepAliveIntervalSeconds}, which supplies the default\n * and enforces the timer bound for every caller.\n */\n keepAliveIntervalSeconds?: number;\n}\n\nexport function createSseEventResponse({\n request,\n observableFactory,\n debugEventBus,\n agentId,\n debug,\n logger,\n telemetry = defaultTelemetry,\n executionSeed,\n captureTelemetry = true,\n runtimeErrorReporter,\n startTime,\n keepAliveIntervalSeconds,\n}: CreateSseEventResponseParams): Response {\n const keepAliveSeconds = resolveSseKeepAliveIntervalSeconds(\n keepAliveIntervalSeconds,\n );\n const stream = new TransformStream();\n const writer = stream.writable.getWriter();\n const encoder = new EventEncoder();\n let streamClosed = false;\n // Set once the current run's RUN_FINISHED or RUN_ERROR has been queued for\n // the client. AG-UI allows nothing after either until the next RUN_STARTED,\n // so a later failure must not add a RUN_ERROR.\n let terminalEventWritten = false;\n let debugThreadId = \"\";\n let debugRunId = \"\";\n\n const debugLogger = debug?.enabled\n ? (logger ??\n createLogger({ level: \"debug\", component: \"copilotkit-debug\" }))\n : undefined;\n\n const closeStream = async () => {\n if (!streamClosed) {\n try {\n await writer.close();\n streamClosed = true;\n } catch {\n // Stream already closed.\n }\n }\n };\n\n // Report a failure to the client as a terminal RUN_ERROR. Without it the\n // client sees an empty stream that ends normally, so its run \"succeeds\"\n // with no reply and no error (e.g. \"Thread already running\" on a busy\n // thread). Only a busy thread is named, with a code clients can branch on;\n // anything else may carry internal detail, so the client gets a generic\n // message and the full error stays in the server log.\n const writeRunError = async (error: unknown) => {\n if (terminalEventWritten || streamClosed || request.signal.aborted) {\n return;\n }\n terminalEventWritten = true;\n const event = isThreadAlreadyRunning(error)\n ? {\n type: EventType.RUN_ERROR,\n message: \"Thread already running\",\n code: THREAD_ALREADY_RUNNING_ERROR_CODE,\n }\n : isThreadAccessDeniedError(error)\n ? {\n type: EventType.RUN_ERROR,\n message: \"Thread access denied\",\n code: THREAD_ACCESS_DENIED_ERROR_CODE,\n }\n : { type: EventType.RUN_ERROR, message: \"Agent run failed\" };\n // The inspector sees every streamed event, so it needs this one too or\n // the run it saw start never ends there. A run that never started has\n // no ids to file it under, so it is not sent.\n if (debugEventBus && debugRunId) {\n try {\n debugEventBus.broadcast(event as BaseEvent, {\n agentId: agentId ?? \"\",\n threadId: debugThreadId,\n runId: debugRunId,\n });\n } catch (broadcastError) {\n logError(broadcastError);\n }\n }\n try {\n await writer.write(encoder.encodeBinary(event as BaseEvent));\n } catch {\n // The client is already gone; closeStream() handles the rest.\n }\n };\n\n const logError = (error: unknown) => {\n console.error(\"Error running agent:\", error);\n console.error(\n \"Error stack:\",\n error instanceof Error ? error.stack : \"No stack trace\",\n );\n console.error(\"Error details:\", {\n name: error instanceof Error ? error.name : \"Unknown\",\n message: error instanceof Error ? error.message : String(error),\n cause: error instanceof Error ? error.cause : undefined,\n });\n };\n\n let subscription: Subscription | undefined;\n let agentErrorReported = false;\n\n // A refused run is an access decision, not an agent failure: log it as a\n // warning and keep it out of the error reporter.\n const logAccessDenied = (error: unknown) => {\n console.warn(\n \"[CopilotKit] Run refused: the thread belongs to another identity.\",\n error instanceof Error ? error.message : String(error),\n );\n };\n\n const reportAgentError = (error: unknown, phase: RuntimeErrorPhase) => {\n if (agentErrorReported) return;\n agentErrorReported = true;\n runtimeErrorReporter?.report({\n request,\n error,\n operation: \"agent.run\",\n agentId,\n threadId: debugThreadId || undefined,\n runId: debugRunId || undefined,\n phase,\n startTime,\n });\n };\n\n (async () => {\n const observable = await observableFactory();\n\n if (captureTelemetry) {\n telemetry.capture(\"oss.runtime.agent_execution_stream_started\", {});\n }\n\n if (debug?.lifecycle) {\n debugLogger!.debug(\"SSE stream opened\");\n }\n\n let eventCount = 0;\n let loggedEventCount = 0;\n\n // Provider/model/LangGraph facts, scraped off raw upstream events as\n // they pass and reported once the stream finishes. This moved here from\n // the v1 TelemetryAgentRunner, which wrapped the runner purely to\n // collect it and emitted its own duplicate copy of every stream event\n // to carry it. v2 callers had no equivalent and reported `{}`.\n // Seeded rather than empty: `llmHostClass` comes from the agent's config,\n // not from the wire, so nothing streamed will ever fill it in. Scraped\n // fields still win, since `collectExecutionInfo` writes over this.\n const executionInfo: AgentExecutionResponseInfo = { ...executionSeed };\n\n subscription = observable.subscribe({\n next: async (event) => {\n collectExecutionInfo(event, executionInfo);\n\n // Extract threadId/runId from RUN_STARTED\n if (event.type === \"RUN_STARTED\") {\n const e = event as { threadId?: string; runId?: string };\n debugThreadId = e.threadId ?? \"\";\n debugRunId = e.runId ?? \"\";\n }\n\n if (event.type === \"RUN_ERROR\") {\n const e = event as {\n message?: unknown;\n threadId?: string;\n runId?: string;\n };\n debugThreadId = e.threadId ?? debugThreadId;\n debugRunId = e.runId ?? debugRunId;\n reportAgentError(\n new Error(\n typeof e.message === \"string\"\n ? e.message\n : \"Runner reported a run error\",\n ),\n \"sse.subscription\",\n );\n }\n\n // Broadcast to debug listeners BEFORE the stream-closed gate below.\n // Intentional: debug subscribers (e.g. the VS Code Inspector panel)\n // should still receive trailing events after the SSE client for\n // this request closed its connection — they're independent\n // consumers observing the underlying runtime, not the request's\n // response stream.\n //\n // Wrapped in try/catch so a buggy debug subscriber can't propagate\n // an exception into this observer — if the throw reached the\n // `next` callback it would get routed to `error` by RxJS, closing\n // the SSE stream for an unrelated reason. Log via `logError` and\n // move on.\n if (debugEventBus) {\n try {\n debugEventBus.broadcast(event, {\n agentId: agentId ?? \"\",\n threadId: debugThreadId,\n runId: debugRunId,\n });\n } catch (broadcastError) {\n logError(broadcastError);\n }\n }\n\n if (!request.signal.aborted && !streamClosed) {\n try {\n eventCount++;\n if (debug?.events) {\n loggedEventCount++;\n if (debug.verbose) {\n debugLogger!.debug({ event }, \"Event emitted\");\n } else {\n debugLogger!.debug(\n { type: event.type, ...summarizeEvent(event) },\n \"Event emitted\",\n );\n }\n }\n // /connect replays finished runs before the live one, so a new\n // RUN_STARTED reopens the stream for a RUN_ERROR.\n if (event.type === EventType.RUN_STARTED) {\n terminalEventWritten = false;\n } else if (\n event.type === EventType.RUN_FINISHED ||\n event.type === EventType.RUN_ERROR\n ) {\n terminalEventWritten = true;\n }\n await writer.write(encoder.encodeBinary(event));\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n streamClosed = true;\n } else {\n // Non-abort write failures (backpressure disconnects,\n // transform-stream exceptions, …) were previously swallowed\n // silently — `streamClosed` stayed `false` and the next\n // event re-attempted a broken writer. Log and mark the\n // stream closed so we stop trying.\n logError(error);\n streamClosed = true;\n }\n }\n }\n },\n error: async (error) => {\n if (isThreadAccessDeniedError(error)) {\n logAccessDenied(error);\n await writeRunError(error);\n await closeStream();\n return;\n }\n reportAgentError(error, \"sse.subscription\");\n if (captureTelemetry) {\n // Analytics gets the outcome, not the message: provider and tool\n // errors can quote prompts, arguments or URLs. Same as Intelligence.\n telemetry.capture(\"oss.runtime.agent_execution_stream_errored\", {\n ...executionInfo,\n error: \"AGENT_EXECUTION_FAILED\",\n });\n }\n if (debug?.lifecycle) {\n debugLogger!.debug(\n { error: error instanceof Error ? error.message : String(error) },\n \"SSE stream errored\",\n );\n }\n logError(error);\n await writeRunError(error);\n await closeStream();\n },\n complete: async () => {\n if (captureTelemetry) {\n telemetry.capture(\n \"oss.runtime.agent_execution_stream_ended\",\n executionInfo,\n );\n }\n if (debug?.lifecycle) {\n debugLogger!.debug(\n { eventCount, loggedEventCount },\n \"SSE stream completed\",\n );\n }\n await closeStream();\n },\n });\n\n // If the client disconnected before the subscription was created,\n // unsubscribe immediately to avoid leaking the observable.\n if (request.signal.aborted) {\n subscription.unsubscribe();\n }\n })().catch(async (error) => {\n if (isThreadAccessDeniedError(error)) {\n logAccessDenied(error);\n } else {\n reportAgentError(error, \"sse.factory\");\n logError(error);\n }\n await writeRunError(error);\n await closeStream();\n });\n\n request.signal.addEventListener(\"abort\", () => {\n subscription?.unsubscribe();\n });\n\n const body =\n keepAliveSeconds > 0\n ? keepAliveSse(stream.readable, keepAliveSeconds * 1_000)\n : stream.readable;\n\n return new Response(body, {\n status: 200,\n headers: {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n },\n });\n}\n\nfunction summarizeEvent(event: BaseEvent): Record<string, unknown> {\n const e = event as any;\n const summary: Record<string, unknown> = {};\n\n if (e.messageId) summary.messageId = e.messageId;\n if (e.toolCallId) summary.toolCallId = e.toolCallId;\n if (e.toolCallName) summary.toolCallName = e.toolCallName;\n if (e.role) summary.role = e.role;\n if (e.delta != null && typeof e.delta === \"string\")\n summary.deltaLength = e.delta.length;\n if (e.snapshot && typeof e.snapshot === \"object\")\n summary.snapshotKeys = Object.keys(e.snapshot);\n if (e.delta && Array.isArray(e.delta))\n summary.operationCount = e.delta.length;\n if (e.threadId) summary.threadId = e.threadId;\n if (e.runId) summary.runId = e.runId;\n if (e.message) summary.message = e.message;\n if (e.code) summary.code = e.code;\n if (e.stepName) summary.stepName = e.stepName;\n\n return summary;\n}\n\n/**\n * Accumulate provider, model, and LangGraph facts from one upstream event.\n *\n * Mutates rather than returns so the caller keeps one record across the whole\n * stream: these arrive on different events and the last one wins. Only fields\n * the upstream actually sent are set, so an agent that reports none leaves the\n * record empty and the stream events carry `{}` as before.\n *\n * `rawEvent` is the untransformed upstream payload, present on AG-UI events\n * that wrap one. Its shape is the provider's, not ours, hence the narrowing.\n */\nfunction collectExecutionInfo(\n event: BaseEvent,\n into: AgentExecutionResponseInfo,\n): void {\n const rawEvent = (\n event as {\n rawEvent?: {\n metadata?: Record<string, unknown>;\n data?: Record<string, unknown>;\n };\n }\n ).rawEvent;\n if (!rawEvent) return;\n\n const model = (rawEvent.data as { output?: { model?: string } } | undefined)\n ?.output?.model;\n if (model) {\n into.model = model;\n // Carried forward from the v1 implementation, which set both from the\n // same field. The upstream sends no separate provider name.\n into.provider = model;\n }\n\n const metadata = rawEvent.metadata as\n | { langgraph_host?: string; langgraph_version?: string }\n | undefined;\n if (metadata?.langgraph_host) into.langGraphHost = metadata.langgraph_host;\n if (metadata?.langgraph_version) {\n into.langGraphVersion = metadata.langgraph_version;\n }\n}\n"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import "reflect-metadata";
2
2
  import { createLogger } from "../../../../lib/logger.mjs";
3
3
  import { keepAliveSse, resolveSseKeepAliveIntervalSeconds } from "./sse-keep-alive.mjs";
4
+ import { THREAD_ACCESS_DENIED_ERROR_CODE, isThreadAccessDeniedError } from "../../runner/agent-runner.mjs";
4
5
  import telemetry from "../../telemetry/telemetry-client.mjs";
5
6
  import { THREAD_ALREADY_RUNNING_ERROR_CODE } from "@copilotkit/shared";
6
7
  import { EventType } from "@ag-ui/client";
@@ -41,6 +42,10 @@ function createSseEventResponse({ request, observableFactory, debugEventBus, age
41
42
  type: EventType.RUN_ERROR,
42
43
  message: "Thread already running",
43
44
  code: THREAD_ALREADY_RUNNING_ERROR_CODE
45
+ } : isThreadAccessDeniedError(error) ? {
46
+ type: EventType.RUN_ERROR,
47
+ message: "Thread access denied",
48
+ code: THREAD_ACCESS_DENIED_ERROR_CODE
44
49
  } : {
45
50
  type: EventType.RUN_ERROR,
46
51
  message: "Agent run failed"
@@ -69,6 +74,9 @@ function createSseEventResponse({ request, observableFactory, debugEventBus, age
69
74
  };
70
75
  let subscription;
71
76
  let agentErrorReported = false;
77
+ const logAccessDenied = (error) => {
78
+ console.warn("[CopilotKit] Run refused: the thread belongs to another identity.", error instanceof Error ? error.message : String(error));
79
+ };
72
80
  const reportAgentError = (error, phase) => {
73
81
  if (agentErrorReported) return;
74
82
  agentErrorReported = true;
@@ -135,10 +143,16 @@ function createSseEventResponse({ request, observableFactory, debugEventBus, age
135
143
  }
136
144
  },
137
145
  error: async (error) => {
146
+ if (isThreadAccessDeniedError(error)) {
147
+ logAccessDenied(error);
148
+ await writeRunError(error);
149
+ await closeStream();
150
+ return;
151
+ }
138
152
  reportAgentError(error, "sse.subscription");
139
153
  if (captureTelemetry) telemetry$1.capture("oss.runtime.agent_execution_stream_errored", {
140
154
  ...executionInfo,
141
- error: error instanceof Error ? error.message : String(error)
155
+ error: "AGENT_EXECUTION_FAILED"
142
156
  });
143
157
  if (debug?.lifecycle) debugLogger.debug({ error: error instanceof Error ? error.message : String(error) }, "SSE stream errored");
144
158
  logError(error);
@@ -156,8 +170,11 @@ function createSseEventResponse({ request, observableFactory, debugEventBus, age
156
170
  });
157
171
  if (request.signal.aborted) subscription.unsubscribe();
158
172
  })().catch(async (error) => {
159
- reportAgentError(error, "sse.factory");
160
- logError(error);
173
+ if (isThreadAccessDeniedError(error)) logAccessDenied(error);
174
+ else {
175
+ reportAgentError(error, "sse.factory");
176
+ logError(error);
177
+ }
161
178
  await writeRunError(error);
162
179
  await closeStream();
163
180
  });
@@ -1 +1 @@
1
- {"version":3,"file":"sse-response.mjs","names":["defaultTelemetry"],"sources":["../../../../../src/v2/runtime/handlers/shared/sse-response.ts"],"sourcesContent":["import { EventType } from \"@ag-ui/client\";\nimport type { BaseEvent } from \"@ag-ui/client\";\nimport { EventEncoder } from \"@ag-ui/encoder\";\nimport type { Observable, Subscription } from \"rxjs\";\nimport {\n keepAliveSse,\n resolveSseKeepAliveIntervalSeconds,\n} from \"./sse-keep-alive\";\nimport { THREAD_ALREADY_RUNNING_ERROR_CODE } from \"@copilotkit/shared\";\nimport type { ResolvedDebugConfig } from \"@copilotkit/shared\";\nimport { createLogger } from \"../../../../v1-deprecated/lib/logger\";\nimport type { CopilotRuntimeLogger } from \"../../../../v1-deprecated/lib/logger\";\nimport { telemetry as defaultTelemetry } from \"../../telemetry\";\nimport type { TelemetryCapture } from \"../../telemetry/telemetry-client\";\nimport type { AgentExecutionResponseInfo } from \"../../telemetry/events\";\nimport type { DebugEventBus } from \"../../core/debug-event-bus\";\nimport type {\n RuntimeErrorPhase,\n RuntimeErrorReporter,\n} from \"../../core/runtime-error-reporter\";\n\n/**\n * Whether a runner rejected the run because the thread is busy. Matches the\n * `code` of {@link ThreadAlreadyRunningError}, and the plain message that\n * custom runners copied from the built-in ones still throw.\n */\nfunction isThreadAlreadyRunning(error: unknown): boolean {\n return (\n error instanceof Error &&\n ((error as { code?: unknown }).code === THREAD_ALREADY_RUNNING_ERROR_CODE ||\n error.message === \"Thread already running\")\n );\n}\n\ninterface CreateSseEventResponseParams {\n request: Request;\n observableFactory: () =>\n | Promise<Observable<BaseEvent>>\n | Observable<BaseEvent>;\n debugEventBus?: DebugEventBus;\n agentId?: string;\n debug?: ResolvedDebugConfig;\n /** Pre-created logger instance to avoid creating a new pino logger per request. */\n logger?: CopilotRuntimeLogger;\n /** Runtime-bound telemetry capture. Falls back for external direct callers. */\n telemetry?: TelemetryCapture;\n /**\n * Execution facts known before the stream opens, merged under anything the\n * upstream reports as it runs. Carries `llmHostClass`, which is knowable\n * from the agent's configuration and never appears on a streamed event.\n */\n executionSeed?: AgentExecutionResponseInfo;\n /**\n * Whether to emit `oss.runtime.agent_execution_stream_*` telemetry for this\n * stream. Defaults to `true`. The stateless `/suggest` path sets this to\n * `false`: a suggestion is a side-effect-free structured completion, not a\n * tracked run, and under `available: \"always\"` it fires often enough to flood\n * run telemetry.\n */\n captureTelemetry?: boolean;\n runtimeErrorReporter?: RuntimeErrorReporter;\n startTime?: number;\n /**\n * Seconds of silence before a `: keep-alive` SSE comment is written.\n * `0` disables the keep-alive. Resolved by\n * {@link resolveSseKeepAliveIntervalSeconds}, which supplies the default\n * and enforces the timer bound for every caller.\n */\n keepAliveIntervalSeconds?: number;\n}\n\nexport function createSseEventResponse({\n request,\n observableFactory,\n debugEventBus,\n agentId,\n debug,\n logger,\n telemetry = defaultTelemetry,\n executionSeed,\n captureTelemetry = true,\n runtimeErrorReporter,\n startTime,\n keepAliveIntervalSeconds,\n}: CreateSseEventResponseParams): Response {\n const keepAliveSeconds = resolveSseKeepAliveIntervalSeconds(\n keepAliveIntervalSeconds,\n );\n const stream = new TransformStream();\n const writer = stream.writable.getWriter();\n const encoder = new EventEncoder();\n let streamClosed = false;\n // Set once the current run's RUN_FINISHED or RUN_ERROR has been queued for\n // the client. AG-UI allows nothing after either until the next RUN_STARTED,\n // so a later failure must not add a RUN_ERROR.\n let terminalEventWritten = false;\n let debugThreadId = \"\";\n let debugRunId = \"\";\n\n const debugLogger = debug?.enabled\n ? (logger ??\n createLogger({ level: \"debug\", component: \"copilotkit-debug\" }))\n : undefined;\n\n const closeStream = async () => {\n if (!streamClosed) {\n try {\n await writer.close();\n streamClosed = true;\n } catch {\n // Stream already closed.\n }\n }\n };\n\n // Report a failure to the client as a terminal RUN_ERROR. Without it the\n // client sees an empty stream that ends normally, so its run \"succeeds\"\n // with no reply and no error (e.g. \"Thread already running\" on a busy\n // thread). Only a busy thread is named, with a code clients can branch on;\n // anything else may carry internal detail, so the client gets a generic\n // message and the full error stays in the server log.\n const writeRunError = async (error: unknown) => {\n if (terminalEventWritten || streamClosed || request.signal.aborted) {\n return;\n }\n terminalEventWritten = true;\n const event = isThreadAlreadyRunning(error)\n ? {\n type: EventType.RUN_ERROR,\n message: \"Thread already running\",\n code: THREAD_ALREADY_RUNNING_ERROR_CODE,\n }\n : { type: EventType.RUN_ERROR, message: \"Agent run failed\" };\n // The inspector sees every streamed event, so it needs this one too or\n // the run it saw start never ends there. A run that never started has\n // no ids to file it under, so it is not sent.\n if (debugEventBus && debugRunId) {\n try {\n debugEventBus.broadcast(event as BaseEvent, {\n agentId: agentId ?? \"\",\n threadId: debugThreadId,\n runId: debugRunId,\n });\n } catch (broadcastError) {\n logError(broadcastError);\n }\n }\n try {\n await writer.write(encoder.encodeBinary(event as BaseEvent));\n } catch {\n // The client is already gone; closeStream() handles the rest.\n }\n };\n\n const logError = (error: unknown) => {\n console.error(\"Error running agent:\", error);\n console.error(\n \"Error stack:\",\n error instanceof Error ? error.stack : \"No stack trace\",\n );\n console.error(\"Error details:\", {\n name: error instanceof Error ? error.name : \"Unknown\",\n message: error instanceof Error ? error.message : String(error),\n cause: error instanceof Error ? error.cause : undefined,\n });\n };\n\n let subscription: Subscription | undefined;\n let agentErrorReported = false;\n\n const reportAgentError = (error: unknown, phase: RuntimeErrorPhase) => {\n if (agentErrorReported) return;\n agentErrorReported = true;\n runtimeErrorReporter?.report({\n request,\n error,\n operation: \"agent.run\",\n agentId,\n threadId: debugThreadId || undefined,\n runId: debugRunId || undefined,\n phase,\n startTime,\n });\n };\n\n (async () => {\n const observable = await observableFactory();\n\n if (captureTelemetry) {\n telemetry.capture(\"oss.runtime.agent_execution_stream_started\", {});\n }\n\n if (debug?.lifecycle) {\n debugLogger!.debug(\"SSE stream opened\");\n }\n\n let eventCount = 0;\n let loggedEventCount = 0;\n\n // Provider/model/LangGraph facts, scraped off raw upstream events as\n // they pass and reported once the stream finishes. This moved here from\n // the v1 TelemetryAgentRunner, which wrapped the runner purely to\n // collect it and emitted its own duplicate copy of every stream event\n // to carry it. v2 callers had no equivalent and reported `{}`.\n // Seeded rather than empty: `llmHostClass` comes from the agent's config,\n // not from the wire, so nothing streamed will ever fill it in. Scraped\n // fields still win, since `collectExecutionInfo` writes over this.\n const executionInfo: AgentExecutionResponseInfo = { ...executionSeed };\n\n subscription = observable.subscribe({\n next: async (event) => {\n collectExecutionInfo(event, executionInfo);\n\n // Extract threadId/runId from RUN_STARTED\n if (event.type === \"RUN_STARTED\") {\n const e = event as { threadId?: string; runId?: string };\n debugThreadId = e.threadId ?? \"\";\n debugRunId = e.runId ?? \"\";\n }\n\n if (event.type === \"RUN_ERROR\") {\n const e = event as {\n message?: unknown;\n threadId?: string;\n runId?: string;\n };\n debugThreadId = e.threadId ?? debugThreadId;\n debugRunId = e.runId ?? debugRunId;\n reportAgentError(\n new Error(\n typeof e.message === \"string\"\n ? e.message\n : \"Runner reported a run error\",\n ),\n \"sse.subscription\",\n );\n }\n\n // Broadcast to debug listeners BEFORE the stream-closed gate below.\n // Intentional: debug subscribers (e.g. the VS Code Inspector panel)\n // should still receive trailing events after the SSE client for\n // this request closed its connection — they're independent\n // consumers observing the underlying runtime, not the request's\n // response stream.\n //\n // Wrapped in try/catch so a buggy debug subscriber can't propagate\n // an exception into this observer — if the throw reached the\n // `next` callback it would get routed to `error` by RxJS, closing\n // the SSE stream for an unrelated reason. Log via `logError` and\n // move on.\n if (debugEventBus) {\n try {\n debugEventBus.broadcast(event, {\n agentId: agentId ?? \"\",\n threadId: debugThreadId,\n runId: debugRunId,\n });\n } catch (broadcastError) {\n logError(broadcastError);\n }\n }\n\n if (!request.signal.aborted && !streamClosed) {\n try {\n eventCount++;\n if (debug?.events) {\n loggedEventCount++;\n if (debug.verbose) {\n debugLogger!.debug({ event }, \"Event emitted\");\n } else {\n debugLogger!.debug(\n { type: event.type, ...summarizeEvent(event) },\n \"Event emitted\",\n );\n }\n }\n // /connect replays finished runs before the live one, so a new\n // RUN_STARTED reopens the stream for a RUN_ERROR.\n if (event.type === EventType.RUN_STARTED) {\n terminalEventWritten = false;\n } else if (\n event.type === EventType.RUN_FINISHED ||\n event.type === EventType.RUN_ERROR\n ) {\n terminalEventWritten = true;\n }\n await writer.write(encoder.encodeBinary(event));\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n streamClosed = true;\n } else {\n // Non-abort write failures (backpressure disconnects,\n // transform-stream exceptions, …) were previously swallowed\n // silently — `streamClosed` stayed `false` and the next\n // event re-attempted a broken writer. Log and mark the\n // stream closed so we stop trying.\n logError(error);\n streamClosed = true;\n }\n }\n }\n },\n error: async (error) => {\n reportAgentError(error, \"sse.subscription\");\n if (captureTelemetry) {\n telemetry.capture(\"oss.runtime.agent_execution_stream_errored\", {\n ...executionInfo,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n if (debug?.lifecycle) {\n debugLogger!.debug(\n { error: error instanceof Error ? error.message : String(error) },\n \"SSE stream errored\",\n );\n }\n logError(error);\n await writeRunError(error);\n await closeStream();\n },\n complete: async () => {\n if (captureTelemetry) {\n telemetry.capture(\n \"oss.runtime.agent_execution_stream_ended\",\n executionInfo,\n );\n }\n if (debug?.lifecycle) {\n debugLogger!.debug(\n { eventCount, loggedEventCount },\n \"SSE stream completed\",\n );\n }\n await closeStream();\n },\n });\n\n // If the client disconnected before the subscription was created,\n // unsubscribe immediately to avoid leaking the observable.\n if (request.signal.aborted) {\n subscription.unsubscribe();\n }\n })().catch(async (error) => {\n reportAgentError(error, \"sse.factory\");\n logError(error);\n await writeRunError(error);\n await closeStream();\n });\n\n request.signal.addEventListener(\"abort\", () => {\n subscription?.unsubscribe();\n });\n\n const body =\n keepAliveSeconds > 0\n ? keepAliveSse(stream.readable, keepAliveSeconds * 1_000)\n : stream.readable;\n\n return new Response(body, {\n status: 200,\n headers: {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n },\n });\n}\n\nfunction summarizeEvent(event: BaseEvent): Record<string, unknown> {\n const e = event as any;\n const summary: Record<string, unknown> = {};\n\n if (e.messageId) summary.messageId = e.messageId;\n if (e.toolCallId) summary.toolCallId = e.toolCallId;\n if (e.toolCallName) summary.toolCallName = e.toolCallName;\n if (e.role) summary.role = e.role;\n if (e.delta != null && typeof e.delta === \"string\")\n summary.deltaLength = e.delta.length;\n if (e.snapshot && typeof e.snapshot === \"object\")\n summary.snapshotKeys = Object.keys(e.snapshot);\n if (e.delta && Array.isArray(e.delta))\n summary.operationCount = e.delta.length;\n if (e.threadId) summary.threadId = e.threadId;\n if (e.runId) summary.runId = e.runId;\n if (e.message) summary.message = e.message;\n if (e.code) summary.code = e.code;\n if (e.stepName) summary.stepName = e.stepName;\n\n return summary;\n}\n\n/**\n * Accumulate provider, model, and LangGraph facts from one upstream event.\n *\n * Mutates rather than returns so the caller keeps one record across the whole\n * stream: these arrive on different events and the last one wins. Only fields\n * the upstream actually sent are set, so an agent that reports none leaves the\n * record empty and the stream events carry `{}` as before.\n *\n * `rawEvent` is the untransformed upstream payload, present on AG-UI events\n * that wrap one. Its shape is the provider's, not ours, hence the narrowing.\n */\nfunction collectExecutionInfo(\n event: BaseEvent,\n into: AgentExecutionResponseInfo,\n): void {\n const rawEvent = (\n event as {\n rawEvent?: {\n metadata?: Record<string, unknown>;\n data?: Record<string, unknown>;\n };\n }\n ).rawEvent;\n if (!rawEvent) return;\n\n const model = (rawEvent.data as { output?: { model?: string } } | undefined)\n ?.output?.model;\n if (model) {\n into.model = model;\n // Carried forward from the v1 implementation, which set both from the\n // same field. The upstream sends no separate provider name.\n into.provider = model;\n }\n\n const metadata = rawEvent.metadata as\n | { langgraph_host?: string; langgraph_version?: string }\n | undefined;\n if (metadata?.langgraph_host) into.langGraphHost = metadata.langgraph_host;\n if (metadata?.langgraph_version) {\n into.langGraphVersion = metadata.langgraph_version;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sse-response.mjs","names":["defaultTelemetry"],"sources":["../../../../../src/v2/runtime/handlers/shared/sse-response.ts"],"sourcesContent":["import { EventType } from \"@ag-ui/client\";\nimport type { BaseEvent } from \"@ag-ui/client\";\nimport { EventEncoder } from \"@ag-ui/encoder\";\nimport type { Observable, Subscription } from \"rxjs\";\nimport {\n keepAliveSse,\n resolveSseKeepAliveIntervalSeconds,\n} from \"./sse-keep-alive\";\nimport { THREAD_ALREADY_RUNNING_ERROR_CODE } from \"@copilotkit/shared\";\nimport {\n THREAD_ACCESS_DENIED_ERROR_CODE,\n isThreadAccessDeniedError,\n} from \"../../runner/agent-runner\";\nimport type { ResolvedDebugConfig } from \"@copilotkit/shared\";\nimport { createLogger } from \"../../../../v1-deprecated/lib/logger\";\nimport type { CopilotRuntimeLogger } from \"../../../../v1-deprecated/lib/logger\";\nimport { telemetry as defaultTelemetry } from \"../../telemetry\";\nimport type { TelemetryCapture } from \"../../telemetry/telemetry-client\";\nimport type { AgentExecutionResponseInfo } from \"../../telemetry/events\";\nimport type { DebugEventBus } from \"../../core/debug-event-bus\";\nimport type {\n RuntimeErrorPhase,\n RuntimeErrorReporter,\n} from \"../../core/runtime-error-reporter\";\n\n/**\n * Whether a runner rejected the run because the thread is busy. Matches the\n * `code` of {@link ThreadAlreadyRunningError}, and the plain message that\n * custom runners copied from the built-in ones still throw.\n */\nfunction isThreadAlreadyRunning(error: unknown): boolean {\n return (\n error instanceof Error &&\n ((error as { code?: unknown }).code === THREAD_ALREADY_RUNNING_ERROR_CODE ||\n error.message === \"Thread already running\")\n );\n}\n\ninterface CreateSseEventResponseParams {\n request: Request;\n observableFactory: () =>\n | Promise<Observable<BaseEvent>>\n | Observable<BaseEvent>;\n debugEventBus?: DebugEventBus;\n agentId?: string;\n debug?: ResolvedDebugConfig;\n /** Pre-created logger instance to avoid creating a new pino logger per request. */\n logger?: CopilotRuntimeLogger;\n /** Runtime-bound telemetry capture. Falls back for external direct callers. */\n telemetry?: TelemetryCapture;\n /**\n * Execution facts known before the stream opens, merged under anything the\n * upstream reports as it runs. Carries `llmHostClass`, which is knowable\n * from the agent's configuration and never appears on a streamed event.\n */\n executionSeed?: AgentExecutionResponseInfo;\n /**\n * Whether to emit `oss.runtime.agent_execution_stream_*` telemetry for this\n * stream. Defaults to `true`. The stateless `/suggest` path sets this to\n * `false`: a suggestion is a side-effect-free structured completion, not a\n * tracked run, and under `available: \"always\"` it fires often enough to flood\n * run telemetry.\n */\n captureTelemetry?: boolean;\n runtimeErrorReporter?: RuntimeErrorReporter;\n startTime?: number;\n /**\n * Seconds of silence before a `: keep-alive` SSE comment is written.\n * `0` disables the keep-alive. Resolved by\n * {@link resolveSseKeepAliveIntervalSeconds}, which supplies the default\n * and enforces the timer bound for every caller.\n */\n keepAliveIntervalSeconds?: number;\n}\n\nexport function createSseEventResponse({\n request,\n observableFactory,\n debugEventBus,\n agentId,\n debug,\n logger,\n telemetry = defaultTelemetry,\n executionSeed,\n captureTelemetry = true,\n runtimeErrorReporter,\n startTime,\n keepAliveIntervalSeconds,\n}: CreateSseEventResponseParams): Response {\n const keepAliveSeconds = resolveSseKeepAliveIntervalSeconds(\n keepAliveIntervalSeconds,\n );\n const stream = new TransformStream();\n const writer = stream.writable.getWriter();\n const encoder = new EventEncoder();\n let streamClosed = false;\n // Set once the current run's RUN_FINISHED or RUN_ERROR has been queued for\n // the client. AG-UI allows nothing after either until the next RUN_STARTED,\n // so a later failure must not add a RUN_ERROR.\n let terminalEventWritten = false;\n let debugThreadId = \"\";\n let debugRunId = \"\";\n\n const debugLogger = debug?.enabled\n ? (logger ??\n createLogger({ level: \"debug\", component: \"copilotkit-debug\" }))\n : undefined;\n\n const closeStream = async () => {\n if (!streamClosed) {\n try {\n await writer.close();\n streamClosed = true;\n } catch {\n // Stream already closed.\n }\n }\n };\n\n // Report a failure to the client as a terminal RUN_ERROR. Without it the\n // client sees an empty stream that ends normally, so its run \"succeeds\"\n // with no reply and no error (e.g. \"Thread already running\" on a busy\n // thread). Only a busy thread is named, with a code clients can branch on;\n // anything else may carry internal detail, so the client gets a generic\n // message and the full error stays in the server log.\n const writeRunError = async (error: unknown) => {\n if (terminalEventWritten || streamClosed || request.signal.aborted) {\n return;\n }\n terminalEventWritten = true;\n const event = isThreadAlreadyRunning(error)\n ? {\n type: EventType.RUN_ERROR,\n message: \"Thread already running\",\n code: THREAD_ALREADY_RUNNING_ERROR_CODE,\n }\n : isThreadAccessDeniedError(error)\n ? {\n type: EventType.RUN_ERROR,\n message: \"Thread access denied\",\n code: THREAD_ACCESS_DENIED_ERROR_CODE,\n }\n : { type: EventType.RUN_ERROR, message: \"Agent run failed\" };\n // The inspector sees every streamed event, so it needs this one too or\n // the run it saw start never ends there. A run that never started has\n // no ids to file it under, so it is not sent.\n if (debugEventBus && debugRunId) {\n try {\n debugEventBus.broadcast(event as BaseEvent, {\n agentId: agentId ?? \"\",\n threadId: debugThreadId,\n runId: debugRunId,\n });\n } catch (broadcastError) {\n logError(broadcastError);\n }\n }\n try {\n await writer.write(encoder.encodeBinary(event as BaseEvent));\n } catch {\n // The client is already gone; closeStream() handles the rest.\n }\n };\n\n const logError = (error: unknown) => {\n console.error(\"Error running agent:\", error);\n console.error(\n \"Error stack:\",\n error instanceof Error ? error.stack : \"No stack trace\",\n );\n console.error(\"Error details:\", {\n name: error instanceof Error ? error.name : \"Unknown\",\n message: error instanceof Error ? error.message : String(error),\n cause: error instanceof Error ? error.cause : undefined,\n });\n };\n\n let subscription: Subscription | undefined;\n let agentErrorReported = false;\n\n // A refused run is an access decision, not an agent failure: log it as a\n // warning and keep it out of the error reporter.\n const logAccessDenied = (error: unknown) => {\n console.warn(\n \"[CopilotKit] Run refused: the thread belongs to another identity.\",\n error instanceof Error ? error.message : String(error),\n );\n };\n\n const reportAgentError = (error: unknown, phase: RuntimeErrorPhase) => {\n if (agentErrorReported) return;\n agentErrorReported = true;\n runtimeErrorReporter?.report({\n request,\n error,\n operation: \"agent.run\",\n agentId,\n threadId: debugThreadId || undefined,\n runId: debugRunId || undefined,\n phase,\n startTime,\n });\n };\n\n (async () => {\n const observable = await observableFactory();\n\n if (captureTelemetry) {\n telemetry.capture(\"oss.runtime.agent_execution_stream_started\", {});\n }\n\n if (debug?.lifecycle) {\n debugLogger!.debug(\"SSE stream opened\");\n }\n\n let eventCount = 0;\n let loggedEventCount = 0;\n\n // Provider/model/LangGraph facts, scraped off raw upstream events as\n // they pass and reported once the stream finishes. This moved here from\n // the v1 TelemetryAgentRunner, which wrapped the runner purely to\n // collect it and emitted its own duplicate copy of every stream event\n // to carry it. v2 callers had no equivalent and reported `{}`.\n // Seeded rather than empty: `llmHostClass` comes from the agent's config,\n // not from the wire, so nothing streamed will ever fill it in. Scraped\n // fields still win, since `collectExecutionInfo` writes over this.\n const executionInfo: AgentExecutionResponseInfo = { ...executionSeed };\n\n subscription = observable.subscribe({\n next: async (event) => {\n collectExecutionInfo(event, executionInfo);\n\n // Extract threadId/runId from RUN_STARTED\n if (event.type === \"RUN_STARTED\") {\n const e = event as { threadId?: string; runId?: string };\n debugThreadId = e.threadId ?? \"\";\n debugRunId = e.runId ?? \"\";\n }\n\n if (event.type === \"RUN_ERROR\") {\n const e = event as {\n message?: unknown;\n threadId?: string;\n runId?: string;\n };\n debugThreadId = e.threadId ?? debugThreadId;\n debugRunId = e.runId ?? debugRunId;\n reportAgentError(\n new Error(\n typeof e.message === \"string\"\n ? e.message\n : \"Runner reported a run error\",\n ),\n \"sse.subscription\",\n );\n }\n\n // Broadcast to debug listeners BEFORE the stream-closed gate below.\n // Intentional: debug subscribers (e.g. the VS Code Inspector panel)\n // should still receive trailing events after the SSE client for\n // this request closed its connection — they're independent\n // consumers observing the underlying runtime, not the request's\n // response stream.\n //\n // Wrapped in try/catch so a buggy debug subscriber can't propagate\n // an exception into this observer — if the throw reached the\n // `next` callback it would get routed to `error` by RxJS, closing\n // the SSE stream for an unrelated reason. Log via `logError` and\n // move on.\n if (debugEventBus) {\n try {\n debugEventBus.broadcast(event, {\n agentId: agentId ?? \"\",\n threadId: debugThreadId,\n runId: debugRunId,\n });\n } catch (broadcastError) {\n logError(broadcastError);\n }\n }\n\n if (!request.signal.aborted && !streamClosed) {\n try {\n eventCount++;\n if (debug?.events) {\n loggedEventCount++;\n if (debug.verbose) {\n debugLogger!.debug({ event }, \"Event emitted\");\n } else {\n debugLogger!.debug(\n { type: event.type, ...summarizeEvent(event) },\n \"Event emitted\",\n );\n }\n }\n // /connect replays finished runs before the live one, so a new\n // RUN_STARTED reopens the stream for a RUN_ERROR.\n if (event.type === EventType.RUN_STARTED) {\n terminalEventWritten = false;\n } else if (\n event.type === EventType.RUN_FINISHED ||\n event.type === EventType.RUN_ERROR\n ) {\n terminalEventWritten = true;\n }\n await writer.write(encoder.encodeBinary(event));\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n streamClosed = true;\n } else {\n // Non-abort write failures (backpressure disconnects,\n // transform-stream exceptions, …) were previously swallowed\n // silently — `streamClosed` stayed `false` and the next\n // event re-attempted a broken writer. Log and mark the\n // stream closed so we stop trying.\n logError(error);\n streamClosed = true;\n }\n }\n }\n },\n error: async (error) => {\n if (isThreadAccessDeniedError(error)) {\n logAccessDenied(error);\n await writeRunError(error);\n await closeStream();\n return;\n }\n reportAgentError(error, \"sse.subscription\");\n if (captureTelemetry) {\n // Analytics gets the outcome, not the message: provider and tool\n // errors can quote prompts, arguments or URLs. Same as Intelligence.\n telemetry.capture(\"oss.runtime.agent_execution_stream_errored\", {\n ...executionInfo,\n error: \"AGENT_EXECUTION_FAILED\",\n });\n }\n if (debug?.lifecycle) {\n debugLogger!.debug(\n { error: error instanceof Error ? error.message : String(error) },\n \"SSE stream errored\",\n );\n }\n logError(error);\n await writeRunError(error);\n await closeStream();\n },\n complete: async () => {\n if (captureTelemetry) {\n telemetry.capture(\n \"oss.runtime.agent_execution_stream_ended\",\n executionInfo,\n );\n }\n if (debug?.lifecycle) {\n debugLogger!.debug(\n { eventCount, loggedEventCount },\n \"SSE stream completed\",\n );\n }\n await closeStream();\n },\n });\n\n // If the client disconnected before the subscription was created,\n // unsubscribe immediately to avoid leaking the observable.\n if (request.signal.aborted) {\n subscription.unsubscribe();\n }\n })().catch(async (error) => {\n if (isThreadAccessDeniedError(error)) {\n logAccessDenied(error);\n } else {\n reportAgentError(error, \"sse.factory\");\n logError(error);\n }\n await writeRunError(error);\n await closeStream();\n });\n\n request.signal.addEventListener(\"abort\", () => {\n subscription?.unsubscribe();\n });\n\n const body =\n keepAliveSeconds > 0\n ? keepAliveSse(stream.readable, keepAliveSeconds * 1_000)\n : stream.readable;\n\n return new Response(body, {\n status: 200,\n headers: {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n },\n });\n}\n\nfunction summarizeEvent(event: BaseEvent): Record<string, unknown> {\n const e = event as any;\n const summary: Record<string, unknown> = {};\n\n if (e.messageId) summary.messageId = e.messageId;\n if (e.toolCallId) summary.toolCallId = e.toolCallId;\n if (e.toolCallName) summary.toolCallName = e.toolCallName;\n if (e.role) summary.role = e.role;\n if (e.delta != null && typeof e.delta === \"string\")\n summary.deltaLength = e.delta.length;\n if (e.snapshot && typeof e.snapshot === \"object\")\n summary.snapshotKeys = Object.keys(e.snapshot);\n if (e.delta && Array.isArray(e.delta))\n summary.operationCount = e.delta.length;\n if (e.threadId) summary.threadId = e.threadId;\n if (e.runId) summary.runId = e.runId;\n if (e.message) summary.message = e.message;\n if (e.code) summary.code = e.code;\n if (e.stepName) summary.stepName = e.stepName;\n\n return summary;\n}\n\n/**\n * Accumulate provider, model, and LangGraph facts from one upstream event.\n *\n * Mutates rather than returns so the caller keeps one record across the whole\n * stream: these arrive on different events and the last one wins. Only fields\n * the upstream actually sent are set, so an agent that reports none leaves the\n * record empty and the stream events carry `{}` as before.\n *\n * `rawEvent` is the untransformed upstream payload, present on AG-UI events\n * that wrap one. Its shape is the provider's, not ours, hence the narrowing.\n */\nfunction collectExecutionInfo(\n event: BaseEvent,\n into: AgentExecutionResponseInfo,\n): void {\n const rawEvent = (\n event as {\n rawEvent?: {\n metadata?: Record<string, unknown>;\n data?: Record<string, unknown>;\n };\n }\n ).rawEvent;\n if (!rawEvent) return;\n\n const model = (rawEvent.data as { output?: { model?: string } } | undefined)\n ?.output?.model;\n if (model) {\n into.model = model;\n // Carried forward from the v1 implementation, which set both from the\n // same field. The upstream sends no separate provider name.\n into.provider = model;\n }\n\n const metadata = rawEvent.metadata as\n | { langgraph_host?: string; langgraph_version?: string }\n | undefined;\n if (metadata?.langgraph_host) into.langGraphHost = metadata.langgraph_host;\n if (metadata?.langgraph_version) {\n into.langGraphVersion = metadata.langgraph_version;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,138 @@
1
+ require("reflect-metadata");
2
+ const require_agent_runner = require('../../runner/agent-runner.cjs');
3
+ const require_json_response = require('./json-response.cjs');
4
+ const require_resolve_intelligence_user = require('./resolve-intelligence-user.cjs');
5
+
6
+ //#region src/v2/runtime/handlers/shared/thread-access.ts
7
+ /** The one place that sets the default Thread access mode. */
8
+ const DEFAULT_THREAD_ACCESS = "owner";
9
+ /** Owner id of a Thread created without identity. */
10
+ const ANONYMOUS_THREAD_OWNER = "";
11
+ /**
12
+ * THE anonymous-Thread policy: which operations a caller without identity
13
+ * (no `identifyUser`) may perform, per access mode. Every such operation
14
+ * still reaches only anonymous Threads, never a Thread an identified user
15
+ * owns. To tighten the default, change the `owner` row; nothing else encodes
16
+ * this policy.
17
+ *
18
+ * Callers identified by `identifyUser` are not in this table: every operation
19
+ * is allowed and reaches only their own Threads.
20
+ */
21
+ const ANONYMOUS_THREAD_ACCESS = {
22
+ owner: {
23
+ list: false,
24
+ clear: false,
25
+ read: true,
26
+ connect: true,
27
+ run: true,
28
+ stop: true,
29
+ isRunning: true
30
+ },
31
+ open: {
32
+ list: true,
33
+ clear: true,
34
+ read: true,
35
+ connect: true,
36
+ run: true,
37
+ stop: true,
38
+ isRunning: true
39
+ }
40
+ };
41
+ const THREAD_ACCESS_NOT_CONFIGURED_MESSAGE = "Listing and clearing threads need a caller identity. Pass `identifyUser: (request) => ({ id, name })` to CopilotRuntime so each user lists and clears only their own threads, or pass `threads: { access: \"open\" }` to let every caller share threads created without identity (insecure for multi-user deployments).";
42
+ /**
43
+ * The configuration error for `identifyUser` with a runner that cannot keep
44
+ * users apart. Every Thread operation is refused rather than served unscoped.
45
+ */
46
+ function threadOwnershipUnsupportedMessage(runner) {
47
+ return `identifyUser is set, but the runner ${runner.constructor?.name || "the configured runner"} does not enforce thread ownership: it has no isThreadAccessible method. Thread routes, run, connect and stop are refused so that no user can reach another user's threads. Use InMemoryAgentRunner or SqliteAgentRunner, or implement isThreadAccessible and honor ownerId in run, connect, stop and isRunning.`;
48
+ }
49
+ /**
50
+ * Log once, at runtime construction, when `identifyUser` is set but the runner
51
+ * does not enforce thread ownership. Requests then fail with the same message.
52
+ */
53
+ function warnIfThreadOwnershipUnenforced(runtime) {
54
+ if (require_resolve_intelligence_user.hasIdentifyUser(runtime) && !require_agent_runner.enforcesThreadOwnership(runtime.runner)) console.warn(`[CopilotKit] ${threadOwnershipUnsupportedMessage(runtime.runner)}`);
55
+ }
56
+ /**
57
+ * The access mode `threads` selects. Throws on anything but an object whose
58
+ * `access` is `"owner"`, `"open"` or omitted: a typo must not silently fall
59
+ * back to the default.
60
+ */
61
+ function resolveThreadAccessMode(config) {
62
+ if (config === void 0) return DEFAULT_THREAD_ACCESS;
63
+ if (typeof config !== "object" || config === null) throw new Error("`threads` must be an object, such as `{ access: \"owner\" }`; got " + JSON.stringify(config));
64
+ const access = config.access;
65
+ if (access === void 0) return DEFAULT_THREAD_ACCESS;
66
+ if (access !== "owner" && access !== "open") throw new Error(`threads.access must be "owner" or "open"; got ${JSON.stringify(access)}`);
67
+ return access;
68
+ }
69
+ let threadListRefusalWarned = false;
70
+ /**
71
+ * Log once per process, at runtime construction, when the configuration
72
+ * refuses the Thread list and clear routes, so operators see why those
73
+ * requests get 403.
74
+ */
75
+ function warnIfThreadListRefused(runtime) {
76
+ if (threadListRefusalWarned) return;
77
+ if (!require_agent_runner.supportsLocalThreadEndpoints(runtime.runner)) return;
78
+ if (isThreadOperationAvailable(runtime, "list")) return;
79
+ threadListRefusalWarned = true;
80
+ console.warn(`[CopilotKit] ${THREAD_ACCESS_NOT_CONFIGURED_MESSAGE} Until then, GET /threads and POST /threads/clear return HTTP 403.`);
81
+ }
82
+ /**
83
+ * True when `operation` can serve requests on this runtime at all: always
84
+ * with `identifyUser` (scoped to the caller), otherwise per
85
+ * {@link ANONYMOUS_THREAD_ACCESS}.
86
+ */
87
+ function isThreadOperationAvailable(runtime, operation) {
88
+ if (require_resolve_intelligence_user.hasIdentifyUser(runtime)) return require_agent_runner.enforcesThreadOwnership(runtime.runner);
89
+ return ANONYMOUS_THREAD_ACCESS[runtime.threadAccess ?? DEFAULT_THREAD_ACCESS][operation];
90
+ }
91
+ /**
92
+ * Resolves the owner id that a local Thread request acts as, or the error
93
+ * Response that refuses it. Every local Thread handler goes through here.
94
+ *
95
+ * - With `identifyUser`: the identified user's id. Refused when the runner
96
+ * cannot enforce owners, or when identification fails.
97
+ * - Without it: the anonymous owner, when {@link ANONYMOUS_THREAD_ACCESS}
98
+ * allows `operation` in the configured mode; otherwise a 403 that names the
99
+ * options to set.
100
+ */
101
+ async function resolveLocalThreadOwner(params) {
102
+ const { runtime, request, operation } = params;
103
+ if (require_resolve_intelligence_user.hasIdentifyUser(runtime)) {
104
+ if (!require_agent_runner.enforcesThreadOwnership(runtime.runner)) return Response.json({
105
+ error: "thread_ownership_unsupported",
106
+ message: threadOwnershipUnsupportedMessage(runtime.runner)
107
+ }, { status: 500 });
108
+ const user = await require_resolve_intelligence_user.resolveIntelligenceUser({
109
+ runtime,
110
+ request
111
+ });
112
+ return user instanceof Response ? user : user.id;
113
+ }
114
+ if (!isThreadOperationAvailable(runtime, operation)) return Response.json({
115
+ error: "thread_access_not_configured",
116
+ message: THREAD_ACCESS_NOT_CONFIGURED_MESSAGE
117
+ }, { status: 403 });
118
+ return ANONYMOUS_THREAD_OWNER;
119
+ }
120
+ /**
121
+ * The response to a run on a Thread that another identity owns, whether the
122
+ * handler's check or the runner refused it. It is the same 404 and body as an
123
+ * unknown Thread on the inspect routes, so it does not say that the Thread
124
+ * belongs to someone. A run on an unknown Thread id creates the Thread, so a
125
+ * refusal still shows that the id is taken; it never shows by whom.
126
+ */
127
+ function threadAccessDeniedResponse() {
128
+ return require_json_response.errorResponse("Thread not found", 404);
129
+ }
130
+
131
+ //#endregion
132
+ exports.isThreadOperationAvailable = isThreadOperationAvailable;
133
+ exports.resolveLocalThreadOwner = resolveLocalThreadOwner;
134
+ exports.resolveThreadAccessMode = resolveThreadAccessMode;
135
+ exports.threadAccessDeniedResponse = threadAccessDeniedResponse;
136
+ exports.warnIfThreadListRefused = warnIfThreadListRefused;
137
+ exports.warnIfThreadOwnershipUnenforced = warnIfThreadOwnershipUnenforced;
138
+ //# sourceMappingURL=thread-access.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thread-access.cjs","names":["hasIdentifyUser","enforcesThreadOwnership","supportsLocalThreadEndpoints","resolveIntelligenceUser","errorResponse"],"sources":["../../../../../src/v2/runtime/handlers/shared/thread-access.ts"],"sourcesContent":["import type { CopilotRuntimeLike } from \"../../core/runtime\";\nimport type { AgentRunner } from \"../../runner/agent-runner\";\nimport {\n enforcesThreadOwnership,\n supportsLocalThreadEndpoints,\n} from \"../../runner/agent-runner\";\nimport { errorResponse } from \"./json-response\";\nimport {\n hasIdentifyUser,\n resolveIntelligenceUser,\n} from \"./resolve-intelligence-user\";\n\n/**\n * How local (non-Intelligence) Threads are shared among callers that\n * `identifyUser` does not identify. The full policy is\n * {@link ANONYMOUS_THREAD_ACCESS}.\n *\n * - `\"owner\"` (default): a Thread created without identity is a capability.\n * Whoever holds its id may read, connect to, continue and stop it, but\n * nobody may list or bulk-clear such Threads.\n * - `\"open\"`: every caller without identity shares those Threads, list and\n * clear included, as in earlier releases. Insecure for any deployment that\n * more than one person can reach.\n *\n * Neither mode loosens identity: with `identifyUser`, every operation reaches\n * only the caller's own Threads, and a Thread that an identified user created\n * is never reachable by an anonymous caller or another identity.\n */\nexport type ThreadAccessMode = \"owner\" | \"open\";\n\nexport interface ThreadsConfig {\n /** @default \"owner\" */\n access?: ThreadAccessMode;\n}\n\n/** The one place that sets the default Thread access mode. */\nexport const DEFAULT_THREAD_ACCESS: ThreadAccessMode = \"owner\";\n\n/** Owner id of a Thread created without identity. */\nexport const ANONYMOUS_THREAD_OWNER = \"\";\n\n/**\n * Every operation on a local Thread. `read` covers\n * `/threads/:id/{messages,events,state}`; `stop` covers a stop by Thread and a\n * stop by run id. `isRunning` has no route; runners check it the same way.\n */\nexport type ThreadOperation =\n | \"list\"\n | \"clear\"\n | \"read\"\n | \"connect\"\n | \"run\"\n | \"stop\"\n | \"isRunning\";\n\n/**\n * THE anonymous-Thread policy: which operations a caller without identity\n * (no `identifyUser`) may perform, per access mode. Every such operation\n * still reaches only anonymous Threads, never a Thread an identified user\n * owns. To tighten the default, change the `owner` row; nothing else encodes\n * this policy.\n *\n * Callers identified by `identifyUser` are not in this table: every operation\n * is allowed and reaches only their own Threads.\n */\nexport const ANONYMOUS_THREAD_ACCESS: Readonly<\n Record<ThreadAccessMode, Readonly<Record<ThreadOperation, boolean>>>\n> = {\n // A Thread id works as a capability for the Thread it names. Enumerating or\n // wiping every anonymous Thread needs identity or an explicit opt-in.\n owner: {\n list: false,\n clear: false,\n read: true,\n connect: true,\n run: true,\n stop: true,\n isRunning: true,\n },\n open: {\n list: true,\n clear: true,\n read: true,\n connect: true,\n run: true,\n stop: true,\n isRunning: true,\n },\n};\n\nexport const THREAD_ACCESS_NOT_CONFIGURED_MESSAGE =\n \"Listing and clearing threads need a caller identity. Pass `identifyUser: (request) => ({ id, name })` \" +\n \"to CopilotRuntime so each user lists and clears only their own threads, or pass \" +\n '`threads: { access: \"open\" }` to let every caller share threads created without identity ' +\n \"(insecure for multi-user deployments).\";\n\n/**\n * The configuration error for `identifyUser` with a runner that cannot keep\n * users apart. Every Thread operation is refused rather than served unscoped.\n */\nexport function threadOwnershipUnsupportedMessage(runner: AgentRunner): string {\n const name = runner.constructor?.name || \"the configured runner\";\n return (\n `identifyUser is set, but the runner ${name} does not enforce thread ownership: ` +\n \"it has no isThreadAccessible method. Thread routes, run, connect and stop are refused \" +\n \"so that no user can reach another user's threads. Use InMemoryAgentRunner or \" +\n \"SqliteAgentRunner, or implement isThreadAccessible and honor ownerId in run, connect, \" +\n \"stop and isRunning.\"\n );\n}\n\n/**\n * Log once, at runtime construction, when `identifyUser` is set but the runner\n * does not enforce thread ownership. Requests then fail with the same message.\n */\nexport function warnIfThreadOwnershipUnenforced(\n runtime: CopilotRuntimeLike,\n): void {\n if (hasIdentifyUser(runtime) && !enforcesThreadOwnership(runtime.runner)) {\n console.warn(\n `[CopilotKit] ${threadOwnershipUnsupportedMessage(runtime.runner)}`,\n );\n }\n}\n\n/**\n * The access mode `threads` selects. Throws on anything but an object whose\n * `access` is `\"owner\"`, `\"open\"` or omitted: a typo must not silently fall\n * back to the default.\n */\nexport function resolveThreadAccessMode(\n config: ThreadsConfig | undefined,\n): ThreadAccessMode {\n if (config === undefined) return DEFAULT_THREAD_ACCESS;\n if (typeof config !== \"object\" || config === null) {\n throw new Error(\n '`threads` must be an object, such as `{ access: \"owner\" }`; got ' +\n JSON.stringify(config),\n );\n }\n const access: unknown = config.access;\n if (access === undefined) return DEFAULT_THREAD_ACCESS;\n if (access !== \"owner\" && access !== \"open\") {\n throw new Error(\n `threads.access must be \"owner\" or \"open\"; got ${JSON.stringify(access)}`,\n );\n }\n return access;\n}\n\nlet threadListRefusalWarned = false;\n\n/** Re-arm the once-per-process startup warnings. Internal: for tests. */\nexport function ɵresetThreadAccessWarnings(): void {\n threadListRefusalWarned = false;\n}\n\n/**\n * Log once per process, at runtime construction, when the configuration\n * refuses the Thread list and clear routes, so operators see why those\n * requests get 403.\n */\nexport function warnIfThreadListRefused(runtime: CopilotRuntimeLike): void {\n if (threadListRefusalWarned) return;\n if (!supportsLocalThreadEndpoints(runtime.runner)) return;\n if (isThreadOperationAvailable(runtime, \"list\")) return;\n threadListRefusalWarned = true;\n console.warn(\n `[CopilotKit] ${THREAD_ACCESS_NOT_CONFIGURED_MESSAGE} Until then, ` +\n \"GET /threads and POST /threads/clear return HTTP 403.\",\n );\n}\n\n/**\n * True when `operation` can serve requests on this runtime at all: always\n * with `identifyUser` (scoped to the caller), otherwise per\n * {@link ANONYMOUS_THREAD_ACCESS}.\n */\nexport function isThreadOperationAvailable(\n runtime: CopilotRuntimeLike,\n operation: ThreadOperation,\n): boolean {\n // The same identity test as resolveLocalThreadOwner, so a non-function\n // value is no identity everywhere and never opens list or clear.\n if (hasIdentifyUser(runtime)) {\n return enforcesThreadOwnership(runtime.runner);\n }\n return ANONYMOUS_THREAD_ACCESS[runtime.threadAccess ?? DEFAULT_THREAD_ACCESS][\n operation\n ];\n}\n\n/**\n * Resolves the owner id that a local Thread request acts as, or the error\n * Response that refuses it. Every local Thread handler goes through here.\n *\n * - With `identifyUser`: the identified user's id. Refused when the runner\n * cannot enforce owners, or when identification fails.\n * - Without it: the anonymous owner, when {@link ANONYMOUS_THREAD_ACCESS}\n * allows `operation` in the configured mode; otherwise a 403 that names the\n * options to set.\n */\nexport async function resolveLocalThreadOwner(params: {\n runtime: CopilotRuntimeLike;\n request: Request;\n operation: ThreadOperation;\n}): Promise<string | Response> {\n const { runtime, request, operation } = params;\n if (hasIdentifyUser(runtime)) {\n // Never serve an identified request through a runner that cannot keep\n // users apart: refuse it with the configuration error instead.\n if (!enforcesThreadOwnership(runtime.runner)) {\n return Response.json(\n {\n error: \"thread_ownership_unsupported\",\n message: threadOwnershipUnsupportedMessage(runtime.runner),\n },\n { status: 500 },\n );\n }\n const user = await resolveIntelligenceUser({ runtime, request });\n return user instanceof Response ? user : user.id;\n }\n if (!isThreadOperationAvailable(runtime, operation)) {\n return Response.json(\n {\n error: \"thread_access_not_configured\",\n message: THREAD_ACCESS_NOT_CONFIGURED_MESSAGE,\n },\n { status: 403 },\n );\n }\n return ANONYMOUS_THREAD_OWNER;\n}\n\n/**\n * The response to a run on a Thread that another identity owns, whether the\n * handler's check or the runner refused it. It is the same 404 and body as an\n * unknown Thread on the inspect routes, so it does not say that the Thread\n * belongs to someone. A run on an unknown Thread id creates the Thread, so a\n * refusal still shows that the id is taken; it never shows by whom.\n */\nexport function threadAccessDeniedResponse(): Response {\n return errorResponse(\"Thread not found\", 404);\n}\n"],"mappings":";;;;;;;AAoCA,MAAa,wBAA0C;;AAGvD,MAAa,yBAAyB;;;;;;;;;;;AA0BtC,MAAa,0BAET;CAGF,OAAO;EACL,MAAM;EACN,OAAO;EACP,MAAM;EACN,SAAS;EACT,KAAK;EACL,MAAM;EACN,WAAW;EACZ;CACD,MAAM;EACJ,MAAM;EACN,OAAO;EACP,MAAM;EACN,SAAS;EACT,KAAK;EACL,MAAM;EACN,WAAW;EACZ;CACF;AAED,MAAa,uCACX;;;;;AASF,SAAgB,kCAAkC,QAA6B;AAE7E,QACE,uCAFW,OAAO,aAAa,QAAQ,wBAEK;;;;;;AAYhD,SAAgB,gCACd,SACM;AACN,KAAIA,kDAAgB,QAAQ,IAAI,CAACC,6CAAwB,QAAQ,OAAO,CACtE,SAAQ,KACN,gBAAgB,kCAAkC,QAAQ,OAAO,GAClE;;;;;;;AASL,SAAgB,wBACd,QACkB;AAClB,KAAI,WAAW,OAAW,QAAO;AACjC,KAAI,OAAO,WAAW,YAAY,WAAW,KAC3C,OAAM,IAAI,MACR,uEACE,KAAK,UAAU,OAAO,CACzB;CAEH,MAAM,SAAkB,OAAO;AAC/B,KAAI,WAAW,OAAW,QAAO;AACjC,KAAI,WAAW,WAAW,WAAW,OACnC,OAAM,IAAI,MACR,iDAAiD,KAAK,UAAU,OAAO,GACxE;AAEH,QAAO;;AAGT,IAAI,0BAA0B;;;;;;AAY9B,SAAgB,wBAAwB,SAAmC;AACzE,KAAI,wBAAyB;AAC7B,KAAI,CAACC,kDAA6B,QAAQ,OAAO,CAAE;AACnD,KAAI,2BAA2B,SAAS,OAAO,CAAE;AACjD,2BAA0B;AAC1B,SAAQ,KACN,gBAAgB,qCAAqC,oEAEtD;;;;;;;AAQH,SAAgB,2BACd,SACA,WACS;AAGT,KAAIF,kDAAgB,QAAQ,CAC1B,QAAOC,6CAAwB,QAAQ,OAAO;AAEhD,QAAO,wBAAwB,QAAQ,gBAAgB,uBACrD;;;;;;;;;;;;AAcJ,eAAsB,wBAAwB,QAIf;CAC7B,MAAM,EAAE,SAAS,SAAS,cAAc;AACxC,KAAID,kDAAgB,QAAQ,EAAE;AAG5B,MAAI,CAACC,6CAAwB,QAAQ,OAAO,CAC1C,QAAO,SAAS,KACd;GACE,OAAO;GACP,SAAS,kCAAkC,QAAQ,OAAO;GAC3D,EACD,EAAE,QAAQ,KAAK,CAChB;EAEH,MAAM,OAAO,MAAME,0DAAwB;GAAE;GAAS;GAAS,CAAC;AAChE,SAAO,gBAAgB,WAAW,OAAO,KAAK;;AAEhD,KAAI,CAAC,2BAA2B,SAAS,UAAU,CACjD,QAAO,SAAS,KACd;EACE,OAAO;EACP,SAAS;EACV,EACD,EAAE,QAAQ,KAAK,CAChB;AAEH,QAAO;;;;;;;;;AAUT,SAAgB,6BAAuC;AACrD,QAAOC,oCAAc,oBAAoB,IAAI"}
@@ -0,0 +1,29 @@
1
+
2
+ import "../../runner/agent-runner.cjs";
3
+ import "../../core/runtime.cjs";
4
+
5
+ //#region src/v2/runtime/handlers/shared/thread-access.d.ts
6
+ /**
7
+ * How local (non-Intelligence) Threads are shared among callers that
8
+ * `identifyUser` does not identify. The full policy is
9
+ * {@link ANONYMOUS_THREAD_ACCESS}.
10
+ *
11
+ * - `"owner"` (default): a Thread created without identity is a capability.
12
+ * Whoever holds its id may read, connect to, continue and stop it, but
13
+ * nobody may list or bulk-clear such Threads.
14
+ * - `"open"`: every caller without identity shares those Threads, list and
15
+ * clear included, as in earlier releases. Insecure for any deployment that
16
+ * more than one person can reach.
17
+ *
18
+ * Neither mode loosens identity: with `identifyUser`, every operation reaches
19
+ * only the caller's own Threads, and a Thread that an identified user created
20
+ * is never reachable by an anonymous caller or another identity.
21
+ */
22
+ type ThreadAccessMode = "owner" | "open";
23
+ interface ThreadsConfig {
24
+ /** @default "owner" */
25
+ access?: ThreadAccessMode;
26
+ }
27
+ //#endregion
28
+ export { ThreadAccessMode, ThreadsConfig };
29
+ //# sourceMappingURL=thread-access.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thread-access.d.cts","names":[],"sources":["../../../../../src/v2/runtime/handlers/shared/thread-access.ts"],"mappings":";;;;;;;AA4BA;;;;;AAEA;;;;;;;;;KAFY,gBAAA;AAAA,UAEK,aAAA;;EAEf,MAAA,GAAS,gBAAA;AAAA"}
@@ -0,0 +1,29 @@
1
+
2
+ import "../../runner/agent-runner.mjs";
3
+ import "../../core/runtime.mjs";
4
+
5
+ //#region src/v2/runtime/handlers/shared/thread-access.d.ts
6
+ /**
7
+ * How local (non-Intelligence) Threads are shared among callers that
8
+ * `identifyUser` does not identify. The full policy is
9
+ * {@link ANONYMOUS_THREAD_ACCESS}.
10
+ *
11
+ * - `"owner"` (default): a Thread created without identity is a capability.
12
+ * Whoever holds its id may read, connect to, continue and stop it, but
13
+ * nobody may list or bulk-clear such Threads.
14
+ * - `"open"`: every caller without identity shares those Threads, list and
15
+ * clear included, as in earlier releases. Insecure for any deployment that
16
+ * more than one person can reach.
17
+ *
18
+ * Neither mode loosens identity: with `identifyUser`, every operation reaches
19
+ * only the caller's own Threads, and a Thread that an identified user created
20
+ * is never reachable by an anonymous caller or another identity.
21
+ */
22
+ type ThreadAccessMode = "owner" | "open";
23
+ interface ThreadsConfig {
24
+ /** @default "owner" */
25
+ access?: ThreadAccessMode;
26
+ }
27
+ //#endregion
28
+ export { ThreadAccessMode, ThreadsConfig };
29
+ //# sourceMappingURL=thread-access.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thread-access.d.mts","names":[],"sources":["../../../../../src/v2/runtime/handlers/shared/thread-access.ts"],"mappings":";;;;;;;AA4BA;;;;;AAEA;;;;;;;;;KAFY,gBAAA;AAAA,UAEK,aAAA;;EAEf,MAAA,GAAS,gBAAA;AAAA"}