veryfront 0.1.63 → 0.1.65

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 (68) hide show
  1. package/esm/deno.js +1 -1
  2. package/esm/src/agent/runtime/index.d.ts +1 -0
  3. package/esm/src/agent/runtime/index.d.ts.map +1 -1
  4. package/esm/src/agent/runtime/index.js +10 -2
  5. package/esm/src/channels/control-plane.d.ts +259 -0
  6. package/esm/src/channels/control-plane.d.ts.map +1 -0
  7. package/esm/src/channels/control-plane.js +212 -0
  8. package/esm/src/channels/invoke.d.ts +71 -44
  9. package/esm/src/channels/invoke.d.ts.map +1 -1
  10. package/esm/src/channels/invoke.js +33 -114
  11. package/esm/src/integrations/endpoint-executor.d.ts +1 -0
  12. package/esm/src/integrations/endpoint-executor.d.ts.map +1 -1
  13. package/esm/src/integrations/endpoint-executor.js +44 -0
  14. package/esm/src/internal-agents/ag-ui-sse.d.ts +35 -0
  15. package/esm/src/internal-agents/ag-ui-sse.d.ts.map +1 -0
  16. package/esm/src/internal-agents/ag-ui-sse.js +263 -0
  17. package/esm/src/internal-agents/control-plane-auth.d.ts +20 -0
  18. package/esm/src/internal-agents/control-plane-auth.d.ts.map +1 -0
  19. package/esm/src/internal-agents/control-plane-auth.js +56 -0
  20. package/esm/src/internal-agents/request-body.d.ts +9 -0
  21. package/esm/src/internal-agents/request-body.d.ts.map +1 -0
  22. package/esm/src/internal-agents/request-body.js +28 -0
  23. package/esm/src/internal-agents/run-stream.d.ts +14 -0
  24. package/esm/src/internal-agents/run-stream.d.ts.map +1 -0
  25. package/esm/src/internal-agents/run-stream.js +259 -0
  26. package/esm/src/internal-agents/schema.d.ts +268 -0
  27. package/esm/src/internal-agents/schema.d.ts.map +1 -0
  28. package/esm/src/internal-agents/schema.js +71 -0
  29. package/esm/src/internal-agents/session-manager.d.ts +63 -0
  30. package/esm/src/internal-agents/session-manager.d.ts.map +1 -0
  31. package/esm/src/internal-agents/session-manager.js +258 -0
  32. package/esm/src/platform/adapters/runtime/deno/adapter.d.ts.map +1 -1
  33. package/esm/src/platform/adapters/runtime/deno/adapter.js +4 -13
  34. package/esm/src/platform/compat/process.d.ts.map +1 -1
  35. package/esm/src/platform/compat/process.js +42 -5
  36. package/esm/src/server/handlers/request/agent-run-cancel.handler.d.ts +11 -0
  37. package/esm/src/server/handlers/request/agent-run-cancel.handler.d.ts.map +1 -0
  38. package/esm/src/server/handlers/request/agent-run-cancel.handler.js +62 -0
  39. package/esm/src/server/handlers/request/agent-run-resume.handler.d.ts +11 -0
  40. package/esm/src/server/handlers/request/agent-run-resume.handler.d.ts.map +1 -0
  41. package/esm/src/server/handlers/request/agent-run-resume.handler.js +77 -0
  42. package/esm/src/server/handlers/request/agent-stream.handler.d.ts +14 -0
  43. package/esm/src/server/handlers/request/agent-stream.handler.d.ts.map +1 -0
  44. package/esm/src/server/handlers/request/agent-stream.handler.js +86 -0
  45. package/esm/src/server/handlers/request/internal-agents-list.handler.d.ts +11 -0
  46. package/esm/src/server/handlers/request/internal-agents-list.handler.d.ts.map +1 -0
  47. package/esm/src/server/handlers/request/internal-agents-list.handler.js +73 -0
  48. package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
  49. package/esm/src/server/runtime-handler/index.js +8 -0
  50. package/package.json +1 -1
  51. package/src/deno.js +1 -1
  52. package/src/src/agent/runtime/index.ts +12 -2
  53. package/src/src/channels/control-plane.ts +332 -0
  54. package/src/src/channels/invoke.ts +44 -164
  55. package/src/src/integrations/endpoint-executor.ts +51 -0
  56. package/src/src/internal-agents/ag-ui-sse.ts +327 -0
  57. package/src/src/internal-agents/control-plane-auth.ts +82 -0
  58. package/src/src/internal-agents/request-body.ts +42 -0
  59. package/src/src/internal-agents/run-stream.ts +354 -0
  60. package/src/src/internal-agents/schema.ts +102 -0
  61. package/src/src/internal-agents/session-manager.ts +358 -0
  62. package/src/src/platform/adapters/runtime/deno/adapter.ts +9 -11
  63. package/src/src/platform/compat/process.ts +56 -3
  64. package/src/src/server/handlers/request/agent-run-cancel.handler.ts +86 -0
  65. package/src/src/server/handlers/request/agent-run-resume.handler.ts +108 -0
  66. package/src/src/server/handlers/request/agent-stream.handler.ts +125 -0
  67. package/src/src/server/handlers/request/internal-agents-list.handler.ts +100 -0
  68. package/src/src/server/runtime-handler/index.ts +8 -0
@@ -0,0 +1,358 @@
1
+ import * as dntShim from "../../_dnt.shims.js";
2
+ function stableJsonStringify(value: unknown, depth = 0): string {
3
+ if (depth > 64) {
4
+ return JSON.stringify(value);
5
+ }
6
+
7
+ if (value === null || typeof value !== "object") {
8
+ return JSON.stringify(value);
9
+ }
10
+
11
+ if (Array.isArray(value)) {
12
+ return `[${value.map((item) => stableJsonStringify(item, depth + 1)).join(",")}]`;
13
+ }
14
+
15
+ const entries = Object.entries(value as Record<string, unknown>)
16
+ .sort(([left], [right]) => left.localeCompare(right))
17
+ .map(([key, item]) => `${JSON.stringify(key)}:${stableJsonStringify(item, depth + 1)}`);
18
+
19
+ return `{${entries.join(",")}}`;
20
+ }
21
+
22
+ function createToolResultKey(result: unknown, isError: boolean): string {
23
+ return `${isError ? "1" : "0"}:${stableJsonStringify(result)}`;
24
+ }
25
+
26
+ export class AgentRunCancelledError extends Error {
27
+ constructor(message = "Run cancelled") {
28
+ super(message);
29
+ this.name = "AgentRunCancelledError";
30
+ }
31
+ }
32
+
33
+ export class AgentRunAlreadyExistsError extends Error {
34
+ constructor(runId: string) {
35
+ super(`Run "${runId}" is already active`);
36
+ this.name = "AgentRunAlreadyExistsError";
37
+ }
38
+ }
39
+
40
+ export class RunNotActiveError extends Error {
41
+ constructor(runId: string) {
42
+ super(`Run "${runId}" is not active`);
43
+ this.name = "RunNotActiveError";
44
+ }
45
+ }
46
+
47
+ export class ToolResultNotWaitingError extends Error {
48
+ constructor(runId: string, toolCallId: string) {
49
+ super(`Run "${runId}" is not waiting for tool call "${toolCallId}"`);
50
+ this.name = "ToolResultNotWaitingError";
51
+ }
52
+ }
53
+
54
+ export class ToolResultConflictError extends Error {
55
+ constructor(runId: string, toolCallId: string) {
56
+ super(`Conflicting tool result for run "${runId}" and tool call "${toolCallId}"`);
57
+ this.name = "ToolResultConflictError";
58
+ }
59
+ }
60
+
61
+ const DEFAULT_WAITING_FOR_TOOL_TTL_MS = 5 * 60 * 1000;
62
+ const DEFAULT_SESSION_TTL_MS = 15 * 60 * 1000;
63
+ const DEFAULT_MAX_CONCURRENT_SESSIONS = 100;
64
+
65
+ type SessionStatus = "running" | "waiting" | "completed" | "cancelled" | "failed";
66
+
67
+ interface SubmittedToolResult {
68
+ toolCallId: string;
69
+ result: unknown;
70
+ isError: boolean;
71
+ key: string;
72
+ }
73
+
74
+ interface WaitingToolState {
75
+ toolCallId: string;
76
+ resolve: (value: SubmittedToolResult) => void;
77
+ reject: (reason?: unknown) => void;
78
+ }
79
+
80
+ interface AgentRunSession {
81
+ runId: string;
82
+ threadId: string;
83
+ status: SessionStatus;
84
+ abortController: AbortController;
85
+ waitingTool: WaitingToolState | null;
86
+ submittedResults: Map<string, SubmittedToolResult>;
87
+ waitingTimeoutId: number | null;
88
+ sessionTimeoutId: number | null;
89
+ }
90
+
91
+ export interface SubmitToolResultOutcome {
92
+ accepted: true;
93
+ duplicate?: true;
94
+ }
95
+
96
+ export class AgentRunSessionManager {
97
+ private readonly sessions = new Map<string, AgentRunSession>();
98
+
99
+ constructor(
100
+ private readonly options: {
101
+ waitingForToolTtlMs?: number;
102
+ sessionTtlMs?: number | null;
103
+ maxConcurrentSessions?: number;
104
+ setTimeoutFn?: typeof dntShim.setTimeout;
105
+ clearTimeoutFn?: typeof clearTimeout;
106
+ } = {},
107
+ ) {}
108
+
109
+ private get waitingForToolTtlMs(): number {
110
+ return this.options.waitingForToolTtlMs ?? DEFAULT_WAITING_FOR_TOOL_TTL_MS;
111
+ }
112
+
113
+ private get sessionTtlMs(): number | null {
114
+ return this.options.sessionTtlMs ?? null;
115
+ }
116
+
117
+ private get setTimeoutFn(): typeof dntShim.setTimeout {
118
+ return this.options.setTimeoutFn ?? dntShim.dntGlobalThis.setTimeout.bind(dntShim.dntGlobalThis);
119
+ }
120
+
121
+ private get clearTimeoutFn(): typeof clearTimeout {
122
+ return this.options.clearTimeoutFn ?? globalThis.clearTimeout.bind(dntShim.dntGlobalThis);
123
+ }
124
+
125
+ private clearWaitingTimeout(session: AgentRunSession): void {
126
+ if (session.waitingTimeoutId === null) {
127
+ return;
128
+ }
129
+
130
+ this.clearTimeoutFn(session.waitingTimeoutId);
131
+ session.waitingTimeoutId = null;
132
+ }
133
+
134
+ private clearSessionTimeout(session: AgentRunSession): void {
135
+ if (session.sessionTimeoutId === null) {
136
+ return;
137
+ }
138
+
139
+ this.clearTimeoutFn(session.sessionTimeoutId);
140
+ session.sessionTimeoutId = null;
141
+ }
142
+
143
+ private scheduleSessionTimeout(session: AgentRunSession): void {
144
+ if (this.sessionTtlMs === null) {
145
+ return;
146
+ }
147
+
148
+ this.clearSessionTimeout(session);
149
+ session.sessionTimeoutId = this.setTimeoutFn(() => {
150
+ this.cancelRun(session.runId);
151
+ }, this.sessionTtlMs) as unknown as number;
152
+ }
153
+
154
+ private scheduleWaitingTimeout(session: AgentRunSession): void {
155
+ this.clearWaitingTimeout(session);
156
+ session.waitingTimeoutId = this.setTimeoutFn(() => {
157
+ this.cancelRun(session.runId);
158
+ }, this.waitingForToolTtlMs) as unknown as number;
159
+ }
160
+
161
+ private touchSession(session: AgentRunSession): void {
162
+ if (
163
+ session.status === "running" || session.status === "waiting"
164
+ ) {
165
+ this.scheduleSessionTimeout(session);
166
+ }
167
+ }
168
+
169
+ private get maxConcurrentSessions(): number {
170
+ return this.options.maxConcurrentSessions ?? DEFAULT_MAX_CONCURRENT_SESSIONS;
171
+ }
172
+
173
+ startRun(input: { runId: string; threadId: string }): AbortSignal {
174
+ const existing = this.sessions.get(input.runId);
175
+ if (existing && (existing.status === "running" || existing.status === "waiting")) {
176
+ throw new AgentRunAlreadyExistsError(input.runId);
177
+ }
178
+
179
+ if (this.sessions.size >= this.maxConcurrentSessions) {
180
+ throw new Error(
181
+ `Maximum concurrent sessions (${this.maxConcurrentSessions}) reached`,
182
+ );
183
+ }
184
+
185
+ const session: AgentRunSession = {
186
+ runId: input.runId,
187
+ threadId: input.threadId,
188
+ status: "running",
189
+ abortController: new AbortController(),
190
+ waitingTool: null,
191
+ submittedResults: new Map(),
192
+ waitingTimeoutId: null,
193
+ sessionTimeoutId: null,
194
+ };
195
+
196
+ this.sessions.set(input.runId, session);
197
+ this.touchSession(session);
198
+ return session.abortController.signal;
199
+ }
200
+
201
+ async waitForToolResult(runId: string, toolCallId: string): Promise<{
202
+ result: unknown;
203
+ isError: boolean;
204
+ }> {
205
+ const session = this.sessions.get(runId);
206
+ if (!session || session.status === "completed" || session.status === "failed") {
207
+ throw new RunNotActiveError(runId);
208
+ }
209
+
210
+ if (session.abortController.signal.aborted || session.status === "cancelled") {
211
+ throw new AgentRunCancelledError();
212
+ }
213
+
214
+ const existingResult = session.submittedResults.get(toolCallId);
215
+ if (existingResult) {
216
+ session.status = "running";
217
+ this.touchSession(session);
218
+ return { result: existingResult.result, isError: existingResult.isError };
219
+ }
220
+
221
+ if (session.waitingTool && session.waitingTool.toolCallId !== toolCallId) {
222
+ throw new ToolResultNotWaitingError(runId, toolCallId);
223
+ }
224
+
225
+ session.status = "waiting";
226
+ this.scheduleWaitingTimeout(session);
227
+ this.touchSession(session);
228
+
229
+ return await new Promise<{ result: unknown; isError: boolean }>((resolve, reject) => {
230
+ const abortHandler = () => {
231
+ this.clearWaitingTimeout(session);
232
+ session.waitingTool = null;
233
+ session.status = "cancelled";
234
+ reject(new AgentRunCancelledError());
235
+ };
236
+
237
+ session.abortController.signal.addEventListener("abort", abortHandler, { once: true });
238
+ session.waitingTool = {
239
+ toolCallId,
240
+ resolve: (value) => {
241
+ session.abortController.signal.removeEventListener("abort", abortHandler);
242
+ this.clearWaitingTimeout(session);
243
+ session.waitingTool = null;
244
+ session.status = "running";
245
+ this.touchSession(session);
246
+ resolve({ result: value.result, isError: value.isError });
247
+ },
248
+ reject: (reason) => {
249
+ session.abortController.signal.removeEventListener("abort", abortHandler);
250
+ this.clearWaitingTimeout(session);
251
+ session.waitingTool = null;
252
+ reject(reason);
253
+ },
254
+ };
255
+ });
256
+ }
257
+
258
+ submitToolResult(
259
+ runId: string,
260
+ input: { toolCallId: string; result: unknown; isError?: boolean },
261
+ ): SubmitToolResultOutcome {
262
+ const session = this.sessions.get(runId);
263
+ if (!session) {
264
+ throw new RunNotActiveError(runId);
265
+ }
266
+
267
+ const normalized: SubmittedToolResult = {
268
+ toolCallId: input.toolCallId,
269
+ result: input.result,
270
+ isError: Boolean(input.isError),
271
+ key: createToolResultKey(input.result, Boolean(input.isError)),
272
+ };
273
+
274
+ const existingResult = session.submittedResults.get(input.toolCallId);
275
+ if (existingResult) {
276
+ if (existingResult.key === normalized.key) {
277
+ return { accepted: true, duplicate: true };
278
+ }
279
+
280
+ throw new ToolResultConflictError(runId, input.toolCallId);
281
+ }
282
+
283
+ if (
284
+ session.status === "completed" || session.status === "failed" ||
285
+ session.status === "cancelled"
286
+ ) {
287
+ throw new RunNotActiveError(runId);
288
+ }
289
+
290
+ if (!session.waitingTool || session.waitingTool.toolCallId !== input.toolCallId) {
291
+ throw new ToolResultNotWaitingError(runId, input.toolCallId);
292
+ }
293
+
294
+ session.submittedResults.set(input.toolCallId, normalized);
295
+ this.touchSession(session);
296
+ session.waitingTool.resolve(normalized);
297
+ return { accepted: true };
298
+ }
299
+
300
+ cancelRun(runId: string): boolean {
301
+ const session = this.sessions.get(runId);
302
+ if (!session) {
303
+ return false;
304
+ }
305
+
306
+ if (
307
+ session.status === "completed" || session.status === "failed" ||
308
+ session.status === "cancelled"
309
+ ) {
310
+ return false;
311
+ }
312
+
313
+ session.status = "cancelled";
314
+ this.clearWaitingTimeout(session);
315
+ this.clearSessionTimeout(session);
316
+ session.abortController.abort(new AgentRunCancelledError());
317
+ session.waitingTool?.reject(new AgentRunCancelledError());
318
+ session.waitingTool = null;
319
+ this.sessions.delete(runId);
320
+ return true;
321
+ }
322
+
323
+ completeRun(runId: string): void {
324
+ const session = this.sessions.get(runId);
325
+ if (!session) return;
326
+ session.status = "completed";
327
+ this.clearWaitingTimeout(session);
328
+ this.clearSessionTimeout(session);
329
+ session.waitingTool = null;
330
+ this.sessions.delete(runId);
331
+ }
332
+
333
+ failRun(runId: string): void {
334
+ const session = this.sessions.get(runId);
335
+ if (!session) return;
336
+ session.status = "failed";
337
+ this.clearWaitingTimeout(session);
338
+ this.clearSessionTimeout(session);
339
+ session.waitingTool = null;
340
+ this.sessions.delete(runId);
341
+ }
342
+
343
+ getRunStatus(runId: string): SessionStatus | null {
344
+ return this.sessions.get(runId)?.status ?? null;
345
+ }
346
+
347
+ reset(): void {
348
+ for (const session of this.sessions.values()) {
349
+ this.clearWaitingTimeout(session);
350
+ this.clearSessionTimeout(session);
351
+ }
352
+ this.sessions.clear();
353
+ }
354
+ }
355
+
356
+ export const agentRunSessionManager = new AgentRunSessionManager({
357
+ sessionTtlMs: DEFAULT_SESSION_TTL_MS,
358
+ });
@@ -17,7 +17,12 @@ import type {
17
17
  WebSocketUpgrade,
18
18
  } from "../../base.js";
19
19
  import { serverLogger } from "../../../../utils/index.js";
20
- import { getEnvOverlayStorage } from "../../../compat/process.js";
20
+ import {
21
+ env as getEnvObject,
22
+ getEnv,
23
+ getEnvOverlayStorage,
24
+ setEnv,
25
+ } from "../../../compat/process.js";
21
26
  import {
22
27
  createFileWatcher,
23
28
  createWatcherIterator,
@@ -272,22 +277,15 @@ class DenoFileSystemAdapter implements FileSystemAdapter {
272
277
 
273
278
  class DenoEnvironmentAdapter implements EnvironmentAdapter {
274
279
  get(key: string): string | undefined {
275
- if (typeof dntShim.Deno === "undefined" || typeof dntShim.Deno.env === "undefined") return undefined;
276
- return dntShim.Deno.env.get(key);
280
+ return getEnv(key);
277
281
  }
278
282
 
279
283
  set(key: string, value: string): void {
280
- if (typeof dntShim.Deno === "undefined" || typeof dntShim.Deno.env === "undefined") {
281
- throw NOT_SUPPORTED.create({
282
- detail: "DenoEnvironmentAdapter.set() can only be used in Deno runtime",
283
- });
284
- }
285
- dntShim.Deno.env.set(key, value);
284
+ setEnv(key, value);
286
285
  }
287
286
 
288
287
  toObject(): Record<string, string> {
289
- if (typeof dntShim.Deno === "undefined" || typeof dntShim.Deno.env === "undefined") return {};
290
- return dntShim.Deno.env.toObject();
288
+ return getEnvObject();
291
289
  }
292
290
  }
293
291
 
@@ -57,10 +57,46 @@ export function chdir(directory: string): void {
57
57
  throw new Error("chdir() is not supported in this runtime");
58
58
  }
59
59
 
60
+ type EnvOverlayValue = string | null;
61
+ type EnvOverlayStore = Map<string, EnvOverlayValue>;
62
+
63
+ function getEnvOverlayStore(): EnvOverlayStore | null {
64
+ const storage = getEnvOverlayStorage();
65
+ const store = storage?.getStore();
66
+ return store instanceof Map ? store as EnvOverlayStore : null;
67
+ }
68
+
69
+ function getOverlayEnvValue(
70
+ store: EnvOverlayStore | null,
71
+ key: string,
72
+ ): { hasValue: boolean; value: string | undefined } {
73
+ if (!store?.has(key)) {
74
+ return { hasValue: false, value: undefined };
75
+ }
76
+
77
+ const value = store.get(key);
78
+ return { hasValue: true, value: value ?? undefined };
79
+ }
80
+
60
81
  export function env(): Record<string, string> {
61
- if (IS_DENO) return dntShim.Deno.env.toObject();
62
- if (runtimeProcess) return runtimeProcess.env as Record<string, string>;
63
- return {};
82
+ const base = IS_DENO
83
+ ? dntShim.Deno.env.toObject()
84
+ : runtimeProcess
85
+ ? { ...runtimeProcess.env } as Record<string, string>
86
+ : {};
87
+
88
+ const overlay = getEnvOverlayStore();
89
+ if (!overlay) return base;
90
+
91
+ for (const [key, value] of overlay.entries()) {
92
+ if (value === null) {
93
+ delete base[key];
94
+ continue;
95
+ }
96
+ base[key] = value;
97
+ }
98
+
99
+ return base;
64
100
  }
65
101
 
66
102
  /**
@@ -68,6 +104,11 @@ export function env(): Record<string, string> {
68
104
  * Use this for framework-owned runtime configuration that should not be shadowed by tenant env.
69
105
  */
70
106
  export function getHostEnv(key: string): string | undefined {
107
+ const overlayResult = getOverlayEnvValue(getEnvOverlayStore(), key);
108
+ if (overlayResult.hasValue) {
109
+ return overlayResult.value;
110
+ }
111
+
71
112
  if (IS_DENO) return dntShim.Deno.env.get(key);
72
113
  if (runtimeProcess) return runtimeProcess.env[key];
73
114
  return undefined;
@@ -184,6 +225,12 @@ export function getEnvBoolean(
184
225
  }
185
226
 
186
227
  export function setEnv(key: string, value: string): void {
228
+ const overlay = getEnvOverlayStore();
229
+ if (overlay) {
230
+ overlay.set(key, value);
231
+ return;
232
+ }
233
+
187
234
  if (IS_DENO) {
188
235
  dntShim.Deno.env.set(key, value);
189
236
  return;
@@ -196,6 +243,12 @@ export function setEnv(key: string, value: string): void {
196
243
  }
197
244
 
198
245
  export function deleteEnv(key: string): void {
246
+ const overlay = getEnvOverlayStore();
247
+ if (overlay) {
248
+ overlay.set(key, null);
249
+ return;
250
+ }
251
+
199
252
  if (IS_DENO) {
200
253
  dntShim.Deno.env.delete(key);
201
254
  return;
@@ -0,0 +1,86 @@
1
+ import * as dntShim from "../../../../_dnt.shims.js";
2
+ import {
3
+ ControlPlaneRequestError,
4
+ verifyControlPlaneRequest,
5
+ } from "../../../internal-agents/control-plane-auth.js";
6
+ import {
7
+ type AgentRunSessionManager,
8
+ agentRunSessionManager,
9
+ } from "../../../internal-agents/session-manager.js";
10
+ import {
11
+ INTERNAL_AGENT_CONTROL_PLANE_MAX_BODY_BYTES,
12
+ InternalAgentRequestBodyTooLargeError,
13
+ readInternalAgentRequestBody,
14
+ } from "../../../internal-agents/request-body.js";
15
+ import { BaseHandler } from "../response/base.js";
16
+ import type { HandlerContext, HandlerMetadata, HandlerPriority, HandlerResult } from "../types.js";
17
+ import { PRIORITY_MEDIUM_API } from "../../../utils/constants/index.js";
18
+
19
+ const CANCEL_PATH_REGEX = /^\/internal\/agents\/runs\/([^/]+)$/;
20
+
21
+ function getRunId(pathname: string): string | null {
22
+ return CANCEL_PATH_REGEX.exec(pathname)?.[1] ?? null;
23
+ }
24
+
25
+ export class AgentRunCancelHandler extends BaseHandler {
26
+ metadata: HandlerMetadata = {
27
+ name: "AgentRunCancelHandler",
28
+ priority: PRIORITY_MEDIUM_API as HandlerPriority,
29
+ patterns: [{ pattern: "/internal/agents/runs/", prefix: true, method: "DELETE" }],
30
+ };
31
+
32
+ constructor(private readonly sessionManager: AgentRunSessionManager = agentRunSessionManager) {
33
+ super();
34
+ }
35
+
36
+ async handle(req: dntShim.Request, ctx: HandlerContext): Promise<HandlerResult> {
37
+ if (!this.shouldHandle(req, ctx)) {
38
+ return this.continue();
39
+ }
40
+
41
+ const runId = getRunId(new URL(req.url).pathname);
42
+ if (!runId) {
43
+ return this.continue();
44
+ }
45
+
46
+ return this.withProxyContext(ctx, async () => {
47
+ const builder = this.createResponseBuilder(ctx)
48
+ .withCORS(req, ctx.securityConfig?.cors)
49
+ .withSecurity(ctx.securityConfig ?? undefined, req);
50
+
51
+ try {
52
+ const rawBody = await readInternalAgentRequestBody(
53
+ req,
54
+ INTERNAL_AGENT_CONTROL_PLANE_MAX_BODY_BYTES,
55
+ );
56
+ await verifyControlPlaneRequest(req, ctx, rawBody, {
57
+ expectedSubject: runId,
58
+ expectedSurface: "studio",
59
+ });
60
+
61
+ const accepted = this.sessionManager.cancelRun(runId);
62
+ if (accepted) {
63
+ return this.respond(builder.json({ accepted: true }, 202));
64
+ }
65
+
66
+ return this.respond(builder.build(null, 204));
67
+ } catch (error) {
68
+ if (error instanceof InternalAgentRequestBodyTooLargeError) {
69
+ return this.respond(builder.json({ error: error.message }, error.status));
70
+ }
71
+
72
+ if (error instanceof ControlPlaneRequestError) {
73
+ return this.respond(builder.json({ error: error.message }, error.status));
74
+ }
75
+
76
+ this.logWarn("Internal agent run cancel failed", {
77
+ error: error instanceof Error ? error.message : String(error),
78
+ runId,
79
+ projectId: ctx.projectId,
80
+ projectSlug: ctx.projectSlug,
81
+ });
82
+ return this.respond(builder.json({ error: "Internal cancel failed" }, 500));
83
+ }
84
+ });
85
+ }
86
+ }
@@ -0,0 +1,108 @@
1
+ import * as dntShim from "../../../../_dnt.shims.js";
2
+ import {
3
+ ControlPlaneRequestError,
4
+ verifyControlPlaneRequest,
5
+ } from "../../../internal-agents/control-plane-auth.js";
6
+ import {
7
+ type AgentRunSessionManager,
8
+ agentRunSessionManager,
9
+ RunNotActiveError,
10
+ ToolResultConflictError,
11
+ ToolResultNotWaitingError,
12
+ } from "../../../internal-agents/session-manager.js";
13
+ import {
14
+ INTERNAL_AGENT_CONTROL_PLANE_MAX_BODY_BYTES,
15
+ InternalAgentRequestBodyTooLargeError,
16
+ readInternalAgentRequestBody,
17
+ } from "../../../internal-agents/request-body.js";
18
+ import { ResumeSignalSchema } from "../../../internal-agents/schema.js";
19
+ import { BaseHandler } from "../response/base.js";
20
+ import type { HandlerContext, HandlerMetadata, HandlerPriority, HandlerResult } from "../types.js";
21
+ import { PRIORITY_MEDIUM_API } from "../../../utils/constants/index.js";
22
+
23
+ const RESUME_PATH_REGEX = /^\/internal\/agents\/runs\/([^/]+)\/resume$/;
24
+
25
+ function getRunId(pathname: string): string | null {
26
+ return RESUME_PATH_REGEX.exec(pathname)?.[1] ?? null;
27
+ }
28
+
29
+ export class AgentRunResumeHandler extends BaseHandler {
30
+ metadata: HandlerMetadata = {
31
+ name: "AgentRunResumeHandler",
32
+ priority: PRIORITY_MEDIUM_API as HandlerPriority,
33
+ patterns: [{ pattern: "/internal/agents/runs/", prefix: true, method: "POST" }],
34
+ };
35
+
36
+ constructor(private readonly sessionManager: AgentRunSessionManager = agentRunSessionManager) {
37
+ super();
38
+ }
39
+
40
+ async handle(req: dntShim.Request, ctx: HandlerContext): Promise<HandlerResult> {
41
+ if (!this.shouldHandle(req, ctx)) {
42
+ return this.continue();
43
+ }
44
+
45
+ const runId = getRunId(new URL(req.url).pathname);
46
+ if (!runId) {
47
+ return this.continue();
48
+ }
49
+
50
+ return this.withProxyContext(ctx, async () => {
51
+ const builder = this.createResponseBuilder(ctx)
52
+ .withCORS(req, ctx.securityConfig?.cors)
53
+ .withSecurity(ctx.securityConfig ?? undefined, req);
54
+
55
+ try {
56
+ const rawBody = await readInternalAgentRequestBody(
57
+ req,
58
+ INTERNAL_AGENT_CONTROL_PLANE_MAX_BODY_BYTES,
59
+ );
60
+ await verifyControlPlaneRequest(req, ctx, rawBody, {
61
+ expectedSubject: runId,
62
+ expectedSurface: "studio",
63
+ });
64
+
65
+ const signal = ResumeSignalSchema.parse(JSON.parse(rawBody));
66
+ const outcome = this.sessionManager.submitToolResult(runId, {
67
+ toolCallId: signal.toolCallId,
68
+ result: signal.result,
69
+ isError: signal.isError,
70
+ });
71
+
72
+ return this.respond(builder.json(outcome, 200));
73
+ } catch (error) {
74
+ if (error instanceof InternalAgentRequestBodyTooLargeError) {
75
+ return this.respond(builder.json({ error: error.message }, error.status));
76
+ }
77
+
78
+ if (error instanceof ControlPlaneRequestError) {
79
+ return this.respond(builder.json({ error: error.message }, error.status));
80
+ }
81
+
82
+ if (error instanceof SyntaxError || (error instanceof Error && error.name === "ZodError")) {
83
+ return this.respond(builder.json({ error: "Invalid resume request" }, 400));
84
+ }
85
+
86
+ if (error instanceof ToolResultConflictError) {
87
+ return this.respond(builder.json({ error: "TOOL_RESULT_CONFLICT" }, 409));
88
+ }
89
+
90
+ if (error instanceof ToolResultNotWaitingError) {
91
+ return this.respond(builder.json({ error: "TOOL_RESULT_NOT_WAITING" }, 409));
92
+ }
93
+
94
+ if (error instanceof RunNotActiveError) {
95
+ return this.respond(builder.json({ error: "RUN_NOT_ACTIVE" }, 410));
96
+ }
97
+
98
+ this.logWarn("Internal agent run resume failed", {
99
+ error: error instanceof Error ? error.message : String(error),
100
+ runId,
101
+ projectId: ctx.projectId,
102
+ projectSlug: ctx.projectSlug,
103
+ });
104
+ return this.respond(builder.json({ error: "Internal resume failed" }, 500));
105
+ }
106
+ });
107
+ }
108
+ }