@assemblyline-agents/node 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/dist/adapters.d.ts +4 -1
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +38 -18
- package/dist/adapters.js.map +1 -1
- package/dist/auth.d.ts +2 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +10 -22
- package/dist/auth.js.map +1 -1
- package/dist/http-io.d.ts.map +1 -1
- package/dist/http-io.js +14 -3
- package/dist/http-io.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +242 -33
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +2 -2
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +2 -2
- package/dist/lifecycle.js.map +1 -1
- package/dist/memory-admin-http.d.ts +27 -0
- package/dist/memory-admin-http.d.ts.map +1 -0
- package/dist/memory-admin-http.js +115 -0
- package/dist/memory-admin-http.js.map +1 -0
- package/dist/rate-limit.d.ts +4 -4
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +9 -7
- package/dist/rate-limit.js.map +1 -1
- package/dist/route-match.d.ts +4 -0
- package/dist/route-match.d.ts.map +1 -1
- package/dist/route-match.js +8 -0
- package/dist/route-match.js.map +1 -1
- package/dist/run-replay-http.d.ts +11 -0
- package/dist/run-replay-http.d.ts.map +1 -0
- package/dist/run-replay-http.js +16 -0
- package/dist/run-replay-http.js.map +1 -0
- package/dist/secrets.d.ts +19 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +58 -0
- package/dist/secrets.js.map +1 -0
- package/dist/usage-http.js +10 -10
- package/dist/usage-http.js.map +1 -1
- package/dist/workspace-admin-http.d.ts +5 -0
- package/dist/workspace-admin-http.d.ts.map +1 -0
- package/dist/workspace-admin-http.js +128 -0
- package/dist/workspace-admin-http.js.map +1 -0
- package/package.json +11 -12
package/dist/index.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { codexAgentHarness } from "@assemblyline-agents/codex";
|
|
4
|
-
import { adapter,
|
|
5
|
-
import { AssemblyLineRuntime, RunCapacityError, loadRuntimeBundle, queryAgentRuns, retryAfterSeconds, validateEvalRunOptions } from "@assemblyline-agents/runtime";
|
|
6
|
-
import {
|
|
4
|
+
import { adapter, ConnectionEventError } from "@assemblyline-agents/core";
|
|
5
|
+
import { AssemblyLineRuntime, RunReplayError, RunCapacityError, durableAgentScopeForManifest, loadRuntimeBundle, queryAgentRuns, retryAfterSeconds, validateEvalRunOptions } from "@assemblyline-agents/runtime";
|
|
6
|
+
import { deleteMemoryAdmin, listMemoryAdmin, parseMemoryAdminRoute, readMemoryAdmin } from "./memory-admin-http.js";
|
|
7
|
+
import { createBlobAdapter, createConnectionStores, createMediaProcessors, createSandboxAdapter, createSelfImprovementStores, createStateAdapter, loadRunTimeline, loadRunTimelinePage } from "./adapters.js";
|
|
7
8
|
import { apiRunCreationEnabled, evalRunsEnabled, authorizeOrSend, logHttpRequest, numberBodyValue, numberQueryValue, rateLimitOrSend, stringBodyValue, validateRuntimeHttpSecurity } from "./auth.js";
|
|
8
9
|
import { HttpRequestError, firstHeaderValue, maxRequestBodyBytes, normalizeHeaders, readBody, send, summarizeRun } from "./http-io.js";
|
|
9
10
|
import { serveRunEventStream, serveStreamingRun } from "./run-stream-http.js";
|
|
10
|
-
import {
|
|
11
|
+
import { resolveSecretsEnv } from "./secrets.js";
|
|
12
|
+
import { startRunReplay } from "./run-replay-http.js";
|
|
13
|
+
import { matchRoute, parseAgentControlRoute, parseRunFeedbackRoute, parseRunReplayRoute, parseRunResumeRoute, parseRunRoute, parseSelfImprovementRoute } from "./route-match.js";
|
|
11
14
|
import { resolveIngressRateLimit } from "./rate-limit.js";
|
|
12
15
|
import { createNodeRuntimeHandle, installSignalHandlers } from "./lifecycle.js";
|
|
13
16
|
import { DIRECT_CHANNEL, conversationBelongsToAgent, decodeConversationCursor, decodeMessageCursor, directAgentScope, directConversationPatch, directTurnFromBody, encodeConversationCursor, encodeMessageCursor, parseConversationHttpRoute } from "./direct-conversations-http.js";
|
|
14
17
|
import { usageHttpResult, usageReconciliationHttpResult } from "./usage-http.js";
|
|
18
|
+
import { isWorkspaceAdminPath, workspaceAdminHttpResult } from "./workspace-admin-http.js";
|
|
15
19
|
export { resolveProvider } from "./provider-resolver.js";
|
|
20
|
+
export { resolveSecretsEnv } from "./secrets.js";
|
|
16
21
|
export { createMemoryRateLimiterStore, parseRateLimitSpec, resolveIngressRateLimit } from "./rate-limit.js";
|
|
17
22
|
export { createNodeRuntimeHandle, installSignalHandlers } from "./lifecycle.js";
|
|
18
23
|
export function nodeAdapter(options = {}) {
|
|
@@ -34,11 +39,17 @@ export function withNodeModelHarnesses(options) {
|
|
|
34
39
|
}
|
|
35
40
|
export async function createProductionRuntimeOptions(options) {
|
|
36
41
|
const artifactRoot = resolve(options.artifactRoot);
|
|
37
|
-
const env = options.env ?? process.env;
|
|
38
42
|
const runtimeOptions = loadRuntimeBundle(artifactRoot);
|
|
39
43
|
runtimeOptions.devMode = options.devMode ?? false;
|
|
44
|
+
const env = await resolveSecretsEnv({
|
|
45
|
+
manifest: runtimeOptions.manifest,
|
|
46
|
+
env: options.env ?? process.env,
|
|
47
|
+
artifactRoot,
|
|
48
|
+
devMode: options.devMode ?? false
|
|
49
|
+
});
|
|
40
50
|
const state = await createStateAdapter(runtimeOptions, artifactRoot, env);
|
|
41
51
|
const blob = await createBlobAdapter(runtimeOptions, artifactRoot, env);
|
|
52
|
+
const mediaProcessors = await createMediaProcessors(runtimeOptions, artifactRoot, env);
|
|
42
53
|
const connectionStores = createConnectionStores(runtimeOptions, artifactRoot, env, state, options.devMode ?? false);
|
|
43
54
|
const selfImprovementStores = createSelfImprovementStores(artifactRoot, env, state);
|
|
44
55
|
const sandbox = await createSandboxAdapter(runtimeOptions, artifactRoot, env);
|
|
@@ -46,9 +57,11 @@ export async function createProductionRuntimeOptions(options) {
|
|
|
46
57
|
...runtimeOptions,
|
|
47
58
|
state,
|
|
48
59
|
blob,
|
|
60
|
+
mediaProcessors,
|
|
49
61
|
sandbox,
|
|
50
62
|
connectionGrantStore: connectionStores.grantStore,
|
|
51
63
|
connectionAuthorizationSessionStore: connectionStores.authorizationSessionStore,
|
|
64
|
+
connectionEventStore: connectionStores.eventStore,
|
|
52
65
|
skillStore: selfImprovementStores.skillStore,
|
|
53
66
|
learningStore: selfImprovementStores.learningStore,
|
|
54
67
|
scheduleStore: selfImprovementStores.scheduleStore,
|
|
@@ -56,11 +69,11 @@ export async function createProductionRuntimeOptions(options) {
|
|
|
56
69
|
env,
|
|
57
70
|
devMode: options.devMode ?? false
|
|
58
71
|
};
|
|
59
|
-
const callbackBaseUrl = env.
|
|
72
|
+
const callbackBaseUrl = env.ASSEMBLY_LINE_CONNECTION_CALLBACK_BASE_URL ?? env.ASSEMBLY_LINE_PUBLIC_URL;
|
|
60
73
|
if (callbackBaseUrl)
|
|
61
74
|
out.connectionCallbackBaseUrl = callbackBaseUrl;
|
|
62
75
|
// Production default: bound run concurrency unless the operator chose a limit.
|
|
63
|
-
if (!(options.devMode ?? false) && env.
|
|
76
|
+
if (!(options.devMode ?? false) && env.ASSEMBLY_LINE_MAX_CONCURRENT_RUNS === undefined) {
|
|
64
77
|
out.maxConcurrentRuns = 16;
|
|
65
78
|
}
|
|
66
79
|
return withNodeModelHarnesses(out);
|
|
@@ -88,7 +101,7 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
88
101
|
if (request.method === "GET" && url.pathname === "/health") {
|
|
89
102
|
if (!await authorizeOrSend(request, response, runtime, options, "public-health", url.pathname))
|
|
90
103
|
return;
|
|
91
|
-
send(response, 200,
|
|
104
|
+
send(response, 200, revisionHealth(options.manifest));
|
|
92
105
|
return;
|
|
93
106
|
}
|
|
94
107
|
if (request.method === "GET" && url.pathname === "/healthz") {
|
|
@@ -97,6 +110,7 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
97
110
|
send(response, 200, {
|
|
98
111
|
ok: true,
|
|
99
112
|
agentRevision: options.manifest.agentRevision,
|
|
113
|
+
buildRevision: options.manifest.buildRevision ?? options.manifest.agentRevision,
|
|
100
114
|
capabilities: evalRunsEnabled(runtime, options) ? ["eval-runs"] : []
|
|
101
115
|
});
|
|
102
116
|
return;
|
|
@@ -107,10 +121,10 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
107
121
|
if (!await authorizeOrSend(request, response, runtime, options, "public-health", url.pathname))
|
|
108
122
|
return;
|
|
109
123
|
if (drain.draining) {
|
|
110
|
-
send(response, 503, { ok: false, draining: true });
|
|
124
|
+
send(response, 503, { ...revisionHealth(options.manifest), ok: false, draining: true });
|
|
111
125
|
return;
|
|
112
126
|
}
|
|
113
|
-
send(response, 200,
|
|
127
|
+
send(response, 200, revisionHealth(options.manifest));
|
|
114
128
|
return;
|
|
115
129
|
}
|
|
116
130
|
if (request.method === "GET" && url.pathname === "/manifest") {
|
|
@@ -149,6 +163,25 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
149
163
|
send(response, 200, result);
|
|
150
164
|
return;
|
|
151
165
|
}
|
|
166
|
+
if (isWorkspaceAdminPath(url.pathname)) {
|
|
167
|
+
const method = request.method ?? "GET";
|
|
168
|
+
if (!["GET", "POST", "DELETE"].includes(method)) {
|
|
169
|
+
send(response, 405, { error: "Method not allowed." });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (!await authorizeOrSend(request, response, runtime, options, method === "GET" ? "admin-read" : "agent-control", url.pathname))
|
|
173
|
+
return;
|
|
174
|
+
try {
|
|
175
|
+
const body = method === "POST"
|
|
176
|
+
? (await readBody(request, maxRequestBodyBytes(runtime, options))).body
|
|
177
|
+
: {};
|
|
178
|
+
send(response, 200, await workspaceAdminHttpResult(runtime, method, url.pathname, body, url.searchParams));
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
send(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
152
185
|
const conversationRoute = parseConversationHttpRoute(url.pathname);
|
|
153
186
|
if (conversationRoute?.kind === "collection" && request.method === "GET") {
|
|
154
187
|
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
@@ -277,12 +310,21 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
277
310
|
send(response, 400, { error: "A message or attachment is required." });
|
|
278
311
|
return;
|
|
279
312
|
}
|
|
313
|
+
const workspaceId = stringBodyValue(body.workspaceId);
|
|
314
|
+
const projectId = stringBodyValue(body.projectId);
|
|
315
|
+
const tenantId = stringBodyValue(body.tenantId);
|
|
280
316
|
const runOptions = {
|
|
281
317
|
message: direct.turn.message,
|
|
282
318
|
channel: DIRECT_CHANNEL,
|
|
283
319
|
approve: direct.approve,
|
|
284
|
-
turn: direct.turn
|
|
320
|
+
turn: direct.turn,
|
|
321
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
322
|
+
...(projectId ? { projectId } : {}),
|
|
323
|
+
...(tenantId ? { tenantId } : {})
|
|
285
324
|
};
|
|
325
|
+
if (body.sandboxCredentials && typeof body.sandboxCredentials === "object" && !Array.isArray(body.sandboxCredentials)) {
|
|
326
|
+
Object.assign(runOptions, { sandboxCredentials: body.sandboxCredentials });
|
|
327
|
+
}
|
|
286
328
|
try {
|
|
287
329
|
if (direct.stream) {
|
|
288
330
|
await serveStreamingRun(runtime, response, (onRunCreated) => runtime.run({ ...runOptions, onRunCreated }), (result) => summarizeRun(result));
|
|
@@ -312,20 +354,45 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
312
354
|
send(response, 200, await queryAgentRuns(runtime.state, options.manifest, { includeDetails: false, limit }));
|
|
313
355
|
return;
|
|
314
356
|
}
|
|
357
|
+
const memoryAdminRoute = parseMemoryAdminRoute(url.pathname);
|
|
358
|
+
if (memoryAdminRoute) {
|
|
359
|
+
const agentScope = durableAgentScopeForManifest(options.manifest);
|
|
360
|
+
if (request.method === "GET") {
|
|
361
|
+
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
362
|
+
return;
|
|
363
|
+
const result = memoryAdminRoute.kind === "list"
|
|
364
|
+
? await listMemoryAdmin(runtime.state, agentScope, url.searchParams)
|
|
365
|
+
: await readMemoryAdmin(runtime.state, agentScope, memoryAdminRoute.path);
|
|
366
|
+
send(response, result.status, result.body);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (request.method === "DELETE" && memoryAdminRoute.kind === "document") {
|
|
370
|
+
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
371
|
+
return;
|
|
372
|
+
if (!await rateLimitOrSend(request, response, rateLimit, "agent-control", url.pathname))
|
|
373
|
+
return;
|
|
374
|
+
const result = await deleteMemoryAdmin(runtime.state, agentScope, memoryAdminRoute.path, url.searchParams);
|
|
375
|
+
send(response, result.status, result.body);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
send(response, 405, { error: "Method not allowed." });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
315
381
|
const selfImprovementRoute = parseSelfImprovementRoute(url.pathname);
|
|
382
|
+
const selfImprovementSurface = url.searchParams.get("surface")?.trim() || "root";
|
|
316
383
|
if (selfImprovementRoute && request.method === "GET") {
|
|
317
384
|
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
318
385
|
return;
|
|
319
386
|
if (selfImprovementRoute.kind === "pending") {
|
|
320
|
-
send(response, 200, { changes: await runtime.listPendingSkillChanges() });
|
|
387
|
+
send(response, 200, { surface: selfImprovementSurface, changes: await runtime.listPendingSkillChanges(selfImprovementSurface) });
|
|
321
388
|
return;
|
|
322
389
|
}
|
|
323
390
|
if (selfImprovementRoute.kind === "reviews") {
|
|
324
|
-
send(response, 200, { reviews: await runtime.listBackgroundReviews() });
|
|
391
|
+
send(response, 200, { surface: selfImprovementSurface, reviews: await runtime.listBackgroundReviews(selfImprovementSurface) });
|
|
325
392
|
return;
|
|
326
393
|
}
|
|
327
394
|
if (selfImprovementRoute.kind === "revisions") {
|
|
328
|
-
send(response, 200, { name: selfImprovementRoute.name, revisions: await runtime.listSkillRevisions(selfImprovementRoute.name) });
|
|
395
|
+
send(response, 200, { surface: selfImprovementSurface, name: selfImprovementRoute.name, revisions: await runtime.listSkillRevisions(selfImprovementRoute.name, selfImprovementSurface) });
|
|
329
396
|
return;
|
|
330
397
|
}
|
|
331
398
|
send(response, 405, { error: "Method not allowed." });
|
|
@@ -338,7 +405,7 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
338
405
|
return;
|
|
339
406
|
if (selfImprovementRoute.kind === "approve") {
|
|
340
407
|
try {
|
|
341
|
-
send(response, 200, await runtime.approveSkillChange(selfImprovementRoute.id));
|
|
408
|
+
send(response, 200, await runtime.approveSkillChange(selfImprovementRoute.id, selfImprovementSurface));
|
|
342
409
|
}
|
|
343
410
|
catch (error) {
|
|
344
411
|
if (error instanceof Error && /not found/i.test(error.message)) {
|
|
@@ -351,8 +418,8 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
351
418
|
}
|
|
352
419
|
if (selfImprovementRoute.kind === "reject") {
|
|
353
420
|
try {
|
|
354
|
-
await runtime.rejectSkillChange(selfImprovementRoute.id);
|
|
355
|
-
send(response, 200, { rejected: true, id: selfImprovementRoute.id });
|
|
421
|
+
await runtime.rejectSkillChange(selfImprovementRoute.id, selfImprovementSurface);
|
|
422
|
+
send(response, 200, { rejected: true, id: selfImprovementRoute.id, surface: selfImprovementSurface });
|
|
356
423
|
}
|
|
357
424
|
catch (error) {
|
|
358
425
|
if (error instanceof Error && /not found/i.test(error.message)) {
|
|
@@ -371,7 +438,7 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
371
438
|
return;
|
|
372
439
|
}
|
|
373
440
|
try {
|
|
374
|
-
send(response, 200, await runtime.restoreSkillRevision(selfImprovementRoute.name, revision));
|
|
441
|
+
send(response, 200, await runtime.restoreSkillRevision(selfImprovementRoute.name, revision, selfImprovementSurface));
|
|
375
442
|
}
|
|
376
443
|
catch (error) {
|
|
377
444
|
if (error instanceof Error && /not found/i.test(error.message)) {
|
|
@@ -476,6 +543,31 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
476
543
|
send(response, 200, { ok: true, matched: results.length, results });
|
|
477
544
|
return;
|
|
478
545
|
}
|
|
546
|
+
if (request.method === "POST" && url.pathname === "/assembly-line/connections/events/reconcile") {
|
|
547
|
+
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
548
|
+
return;
|
|
549
|
+
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
550
|
+
const principal = stringBodyValue(body.userId) && stringBodyValue(body.issuer)
|
|
551
|
+
? { type: "user", id: stringBodyValue(body.userId), issuer: stringBodyValue(body.issuer) }
|
|
552
|
+
: undefined;
|
|
553
|
+
const connection = stringBodyValue(body.connection);
|
|
554
|
+
const publicUrl = stringBodyValue(body.publicUrl);
|
|
555
|
+
const results = await runtime.reconcileConnectionEvents({
|
|
556
|
+
...(connection ? { connection } : {}),
|
|
557
|
+
...(publicUrl ? { publicUrl } : {}),
|
|
558
|
+
...(principal ? { principal } : {}),
|
|
559
|
+
force: body.force === true
|
|
560
|
+
});
|
|
561
|
+
send(response, 200, { ok: true, results });
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (request.method === "GET" && url.pathname === "/assembly-line/connections/events") {
|
|
565
|
+
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
566
|
+
return;
|
|
567
|
+
const results = await runtime.checkConnectionEvents(url.searchParams.get("connection") ?? undefined);
|
|
568
|
+
send(response, 200, { ok: true, results });
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
479
571
|
if (request.method === "GET" && url.pathname.startsWith("/runs/")) {
|
|
480
572
|
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
481
573
|
return;
|
|
@@ -493,7 +585,30 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
493
585
|
await serveRunEventStream(runtime, response, runRoute.runId, lastEventId);
|
|
494
586
|
return;
|
|
495
587
|
}
|
|
496
|
-
const
|
|
588
|
+
const paginatedTimeline = runRoute.kind === "timeline" && url.searchParams.has("limit");
|
|
589
|
+
const before = numberQueryValue(url.searchParams.get("before"));
|
|
590
|
+
const after = numberQueryValue(url.searchParams.get("after"));
|
|
591
|
+
const limit = numberQueryValue(url.searchParams.get("limit"));
|
|
592
|
+
if (paginatedTimeline && limit === undefined) {
|
|
593
|
+
send(response, 400, { error: "Run timeline limit must be a number." });
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
let timeline;
|
|
597
|
+
try {
|
|
598
|
+
timeline = paginatedTimeline
|
|
599
|
+
? await loadRunTimelinePage(runtime, runRoute.runId, {
|
|
600
|
+
...(before !== undefined ? { before } : {}),
|
|
601
|
+
...(after !== undefined ? { after } : {}),
|
|
602
|
+
limit: limit
|
|
603
|
+
})
|
|
604
|
+
: await loadRunTimeline(runtime, runRoute.runId);
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
if (!paginatedTimeline)
|
|
608
|
+
throw error;
|
|
609
|
+
send(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
497
612
|
if (!timeline) {
|
|
498
613
|
send(response, 404, { error: `Run not found: ${runRoute.runId}` });
|
|
499
614
|
return;
|
|
@@ -510,6 +625,38 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
510
625
|
return;
|
|
511
626
|
}
|
|
512
627
|
if (request.method === "POST" && url.pathname.startsWith("/runs/")) {
|
|
628
|
+
const replayRoute = parseRunReplayRoute(url.pathname);
|
|
629
|
+
if (replayRoute) {
|
|
630
|
+
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
631
|
+
return;
|
|
632
|
+
if (!await rateLimitOrSend(request, response, rateLimit, "run-create", url.pathname))
|
|
633
|
+
return;
|
|
634
|
+
try {
|
|
635
|
+
const replay = await startRunReplay(runtime, replayRoute.runId);
|
|
636
|
+
void replay.completion.catch((error) => {
|
|
637
|
+
runtime.logger.error("replay run failed after durable creation", {
|
|
638
|
+
sourceRunId: replayRoute.runId,
|
|
639
|
+
runId: replay.run.id,
|
|
640
|
+
error: error instanceof Error ? error.message : String(error)
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
send(response, 202, { runId: replay.run.id, status: replay.run.status });
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
if (error instanceof RunReplayError) {
|
|
647
|
+
send(response, error.code === "run_not_found" ? 404 : 409, { error: error.message, code: error.code });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (error instanceof RunCapacityError) {
|
|
651
|
+
send(response, 429, { error: "Run capacity exhausted. Retry later.", retryAfterMs: error.retryAfterMs }, {
|
|
652
|
+
"retry-after": String(retryAfterSeconds(error.retryAfterMs))
|
|
653
|
+
});
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
throw error;
|
|
657
|
+
}
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
513
660
|
const feedbackRoute = parseRunFeedbackRoute(url.pathname);
|
|
514
661
|
if (feedbackRoute) {
|
|
515
662
|
if (!await authorizeOrSend(request, response, runtime, options, "run-control", url.pathname))
|
|
@@ -560,7 +707,18 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
560
707
|
send(response, 409, { error: `Run ${resumeRoute.runId} is not ${expected} (status: ${run.status}).` });
|
|
561
708
|
return;
|
|
562
709
|
}
|
|
563
|
-
if (resumeRoute.kind === "cancel" &&
|
|
710
|
+
if (resumeRoute.kind === "cancel" && run.status === "cancelled") {
|
|
711
|
+
// Cancellation is an idempotent operator command. A retry may be
|
|
712
|
+
// the first response the client receives even when an earlier
|
|
713
|
+
// request already committed the terminal state.
|
|
714
|
+
send(response, 200, {
|
|
715
|
+
runId: run.id,
|
|
716
|
+
status: run.status,
|
|
717
|
+
controlIntent: run.controlIntent ?? null
|
|
718
|
+
});
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (resumeRoute.kind === "cancel" && ["completed", "failed"].includes(run.status)) {
|
|
564
722
|
send(response, 409, { error: `Run ${resumeRoute.runId} is already terminal (status: ${run.status}).` });
|
|
565
723
|
return;
|
|
566
724
|
}
|
|
@@ -601,6 +759,17 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
601
759
|
}
|
|
602
760
|
// The resume idempotency claim rejects a concurrent/replayed resume.
|
|
603
761
|
const message = error instanceof Error ? error.message : String(error);
|
|
762
|
+
if (resumeRoute.kind === "cancel" && /already terminal/i.test(message)) {
|
|
763
|
+
const latest = await runtime.state.getRun(resumeRoute.runId);
|
|
764
|
+
if (latest?.status === "cancelled") {
|
|
765
|
+
send(response, 200, {
|
|
766
|
+
runId: latest.id,
|
|
767
|
+
status: latest.status,
|
|
768
|
+
controlIntent: latest.controlIntent ?? null
|
|
769
|
+
});
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
604
773
|
if (/already in progress|not waiting|not suspended|not running|already terminal/i.test(message)) {
|
|
605
774
|
send(response, 409, { error: message });
|
|
606
775
|
return;
|
|
@@ -664,6 +833,33 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
664
833
|
send(response, result.status, result.body);
|
|
665
834
|
return;
|
|
666
835
|
}
|
|
836
|
+
const connectionEvent = /^\/assembly-line\/connections\/([^/]+)\/events\/([^/]+)$/u.exec(url.pathname);
|
|
837
|
+
if ((request.method === "GET" || request.method === "POST") && connectionEvent?.[1] && connectionEvent[2]) {
|
|
838
|
+
if (!await authorizeOrSend(request, response, runtime, options, "connection-event", url.pathname))
|
|
839
|
+
return;
|
|
840
|
+
if (!await rateLimitOrSend(request, response, rateLimit, "connection-event", url.pathname))
|
|
841
|
+
return;
|
|
842
|
+
const rawBody = request.method === "POST"
|
|
843
|
+
? (await readBody(request, maxRequestBodyBytes(runtime, options))).rawBody
|
|
844
|
+
: "";
|
|
845
|
+
try {
|
|
846
|
+
const result = await runtime.acceptConnectionEvent(decodeURIComponent(connectionEvent[1]), decodeURIComponent(connectionEvent[2]), {
|
|
847
|
+
method: request.method,
|
|
848
|
+
headers: normalizeHeaders(request.headers),
|
|
849
|
+
body: rawBody,
|
|
850
|
+
query: Object.fromEntries(url.searchParams.entries())
|
|
851
|
+
});
|
|
852
|
+
send(response, result.status, result.body ?? { ok: true, accepted: result.accepted, duplicates: result.duplicates, ignored: result.ignored }, result.headers);
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
if (error instanceof ConnectionEventError) {
|
|
856
|
+
send(response, error.status, { error: error.message });
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
throw error;
|
|
860
|
+
}
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
667
863
|
if (request.method === "POST" && url.pathname === "/runs") {
|
|
668
864
|
if (!apiRunCreationEnabled(runtime, options)) {
|
|
669
865
|
send(response, 403, { error: "API run creation is disabled." });
|
|
@@ -683,9 +879,22 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
683
879
|
Object.assign(runOptions, { toolName: body.toolName });
|
|
684
880
|
if (body.input !== undefined)
|
|
685
881
|
Object.assign(runOptions, { input: body.input });
|
|
882
|
+
if (typeof body.conversationId === "string")
|
|
883
|
+
Object.assign(runOptions, { conversationId: body.conversationId });
|
|
884
|
+
if (typeof body.projectId === "string")
|
|
885
|
+
Object.assign(runOptions, { projectId: body.projectId });
|
|
886
|
+
if (typeof body.workspaceId === "string")
|
|
887
|
+
Object.assign(runOptions, { workspaceId: body.workspaceId });
|
|
888
|
+
if (typeof body.tenantId === "string")
|
|
889
|
+
Object.assign(runOptions, { tenantId: body.tenantId });
|
|
890
|
+
if (typeof body.userId === "string")
|
|
891
|
+
Object.assign(runOptions, { userId: body.userId });
|
|
892
|
+
if (body.sandboxCredentials && typeof body.sandboxCredentials === "object" && !Array.isArray(body.sandboxCredentials)) {
|
|
893
|
+
Object.assign(runOptions, { sandboxCredentials: body.sandboxCredentials });
|
|
894
|
+
}
|
|
686
895
|
if (body.eval !== undefined) {
|
|
687
896
|
if (!evalRunsEnabled(runtime, options)) {
|
|
688
|
-
send(response, 403, { error: "Eval run options are disabled. Set
|
|
897
|
+
send(response, 403, { error: "Eval run options are disabled. Set ASSEMBLY_LINE_ENABLE_EVAL_RUNS=true or allowEvalRuns." });
|
|
689
898
|
return;
|
|
690
899
|
}
|
|
691
900
|
try {
|
|
@@ -757,14 +966,7 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
757
966
|
}
|
|
758
967
|
});
|
|
759
968
|
}
|
|
760
|
-
export async function listenNodeRuntime(
|
|
761
|
-
// Rename compatibility: make every branded env var readable under both its
|
|
762
|
-
// ASSEMBLY_LINE_* and OPENEVE_* spellings for all downstream readers.
|
|
763
|
-
const options = rawOptions.env
|
|
764
|
-
? { ...rawOptions, env: applyEnvAliases(rawOptions.env).env }
|
|
765
|
-
: rawOptions;
|
|
766
|
-
if (!rawOptions.env)
|
|
767
|
-
mirrorEnvAliasesInPlace(process.env);
|
|
969
|
+
export async function listenNodeRuntime(options) {
|
|
768
970
|
const runtime = new AssemblyLineRuntime(withNodeModelHarnesses(options));
|
|
769
971
|
runtime.boot();
|
|
770
972
|
validateRuntimeHttpSecurity(runtime, options);
|
|
@@ -783,7 +985,7 @@ export async function listenNodeRuntime(rawOptions) {
|
|
|
783
985
|
if (scheduler)
|
|
784
986
|
server.once("close", stopScheduler);
|
|
785
987
|
const env = options.env ?? runtime.env ?? process.env;
|
|
786
|
-
if (env.
|
|
988
|
+
if (env.ASSEMBLY_LINE_RUN_RECOVERY !== "false" && env.ASSEMBLY_LINE_RUN_RECOVERY !== "0") {
|
|
787
989
|
// Repair runs orphaned by a previous process before accepting new traffic.
|
|
788
990
|
await runtime.recoverIncompleteRuns().catch((error) => {
|
|
789
991
|
runtime.logger.warn("boot run recovery failed", {
|
|
@@ -818,14 +1020,21 @@ export async function listenNodeRuntime(rawOptions) {
|
|
|
818
1020
|
// Hosts like Railway send SIGTERM on every redeploy/restart. With no handler
|
|
819
1021
|
// installed, Node terminates immediately and the graceful drain never runs, so
|
|
820
1022
|
// in-flight runs are orphaned. Install the handlers by default (opt-out via
|
|
821
|
-
// option or
|
|
1023
|
+
// option or ASSEMBLY_LINE_SIGNAL_HANDLERS) so redeploys drain instead of hard-kill.
|
|
822
1024
|
if (options.installSignalHandlers !== false &&
|
|
823
|
-
env.
|
|
824
|
-
env.
|
|
1025
|
+
env.ASSEMBLY_LINE_SIGNAL_HANDLERS !== "false" &&
|
|
1026
|
+
env.ASSEMBLY_LINE_SIGNAL_HANDLERS !== "0") {
|
|
825
1027
|
installSignalHandlers(handle);
|
|
826
1028
|
}
|
|
827
1029
|
return handle;
|
|
828
1030
|
}
|
|
1031
|
+
function revisionHealth(manifest) {
|
|
1032
|
+
return {
|
|
1033
|
+
ok: true,
|
|
1034
|
+
agentRevision: manifest.agentRevision,
|
|
1035
|
+
buildRevision: manifest.buildRevision ?? manifest.agentRevision
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
829
1038
|
function once(fn) {
|
|
830
1039
|
let done = false;
|
|
831
1040
|
return () => {
|