agent-relay-server 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/public/index.html +14 -10
- package/runner/src/config.ts +2 -1
- package/src/automations.ts +13 -28
- package/src/bus.ts +2 -2
- package/src/cli.ts +183 -8
- package/src/config-store.ts +9 -27
- package/src/daemon.ts +1 -4
- package/src/db.ts +9 -2
- package/src/dev.ts +1 -4
- package/src/http-body.ts +49 -0
- package/src/index.ts +2 -1
- package/src/maintenance.ts +3 -3
- package/src/mcp.ts +17 -68
- package/src/memory-broker-smoke.ts +2 -2
- package/src/memory-command-broker.ts +2 -2
- package/src/memory-http-broker.ts +2 -2
- package/src/orchestrator-lookup.ts +29 -0
- package/src/recipe-validator.ts +2 -2
- package/src/routes.ts +180 -179
- package/src/setup.ts +1 -4
- package/src/spawn-command.ts +2 -1
- package/src/steward.ts +2 -1
- package/src/upgrade.ts +88 -17
- package/src/validation.ts +54 -2
package/src/daemon.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { shellQuote } from "agent-relay-sdk/shell-utils";
|
|
2
3
|
import { constants, existsSync } from "node:fs";
|
|
3
4
|
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
5
|
import { homedir } from "node:os";
|
|
@@ -460,10 +461,6 @@ function quoteSystemdArg(value: string): string {
|
|
|
460
461
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
461
462
|
}
|
|
462
463
|
|
|
463
|
-
function shellQuote(value: string): string {
|
|
464
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
464
|
function xmlEscape(value: string): string {
|
|
468
465
|
return value
|
|
469
466
|
.replace(/&/g, "&")
|
package/src/db.ts
CHANGED
|
@@ -5079,14 +5079,21 @@ function upsertWorkspaceFromManagedAgent(agent: ManagedAgent): WorkspaceRecord |
|
|
|
5079
5079
|
// preserved and metadata is merged, not replaced.
|
|
5080
5080
|
const existing = getWorkspace(workspace.id);
|
|
5081
5081
|
const preserveStatus = existing != null && existing.status !== "active";
|
|
5082
|
+
// The branch (and advanced base) change ONLY via the relay's own land-and-continue
|
|
5083
|
+
// recycle (setWorkspaceBranch repoints to `<branch>-N` and bumps base_sha). The
|
|
5084
|
+
// runner keeps re-reporting its spawn-time branch on every heartbeat, and the
|
|
5085
|
+
// recycle returns status to "active" — so without this the next heartbeat clobbers
|
|
5086
|
+
// the repoint back to the original branch, and the next land targets a deleted
|
|
5087
|
+
// branch and strands the work (vent #62 follow-up). Trust the existing row's
|
|
5088
|
+
// branch/base over registration; only a brand-new row takes the runner's values.
|
|
5082
5089
|
return upsertWorkspace({
|
|
5083
5090
|
id: workspace.id,
|
|
5084
5091
|
repoRoot: workspace.repoRoot,
|
|
5085
5092
|
sourceCwd: workspace.sourceCwd ?? agent.cwd,
|
|
5086
5093
|
worktreePath: workspace.worktreePath,
|
|
5087
|
-
branch: workspace.branch,
|
|
5094
|
+
branch: existing?.branch ?? workspace.branch,
|
|
5088
5095
|
baseRef: workspace.baseRef,
|
|
5089
|
-
baseSha: workspace.baseSha,
|
|
5096
|
+
baseSha: existing?.baseSha ?? workspace.baseSha,
|
|
5090
5097
|
mode: workspace.mode,
|
|
5091
5098
|
requestedMode: workspace.requestedMode,
|
|
5092
5099
|
status: preserveStatus ? existing!.status : (workspace.status ?? "active"),
|
package/src/dev.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { shellQuote } from "agent-relay-sdk/shell-utils";
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
5
|
import { homedir, hostname as osHostname } from "node:os";
|
|
@@ -504,7 +505,3 @@ function quote(value: string): string {
|
|
|
504
505
|
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
|
|
505
506
|
return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
506
507
|
}
|
|
507
|
-
|
|
508
|
-
function shellQuote(value: string): string {
|
|
509
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
510
|
-
}
|
package/src/http-body.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Concatenate body chunks into a single contiguous Uint8Array. */
|
|
2
|
+
export function concatBytes(chunks: Uint8Array[]): Uint8Array {
|
|
3
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
4
|
+
const output = new Uint8Array(total);
|
|
5
|
+
let offset = 0;
|
|
6
|
+
for (const chunk of chunks) {
|
|
7
|
+
output.set(chunk, offset);
|
|
8
|
+
offset += chunk.byteLength;
|
|
9
|
+
}
|
|
10
|
+
return output;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Wrap bytes in a single-chunk ReadableStream (e.g. for artifact `storage.store`). */
|
|
14
|
+
export function bytesToStream(bytes: Uint8Array): ReadableStream<Uint8Array> {
|
|
15
|
+
return new ReadableStream<Uint8Array>({
|
|
16
|
+
start(controller) {
|
|
17
|
+
controller.enqueue(bytes);
|
|
18
|
+
controller.close();
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Drain a request body stream into a single Uint8Array, enforcing a byte cap.
|
|
25
|
+
* Returns a 413 result the moment the running total exceeds `maxBytes` (cancels
|
|
26
|
+
* the reader). Single home for the read-loop that `parseBody` (routes) and
|
|
27
|
+
* `parseJsonRpcRequest` (mcp) each open-coded — they differed only in the cap.
|
|
28
|
+
* Callers handle body-absence and decoding.
|
|
29
|
+
*/
|
|
30
|
+
export async function readBodyBytes(
|
|
31
|
+
body: ReadableStream<Uint8Array>,
|
|
32
|
+
maxBytes: number,
|
|
33
|
+
): Promise<{ ok: true; bytes: Uint8Array } | { ok: false; status: 413; error: string }> {
|
|
34
|
+
const reader = body.getReader();
|
|
35
|
+
const chunks: Uint8Array[] = [];
|
|
36
|
+
let total = 0;
|
|
37
|
+
while (true) {
|
|
38
|
+
const { done, value } = await reader.read();
|
|
39
|
+
if (done) break;
|
|
40
|
+
if (!value) continue;
|
|
41
|
+
total += value.byteLength;
|
|
42
|
+
if (total > maxBytes) {
|
|
43
|
+
await reader.cancel().catch(() => {});
|
|
44
|
+
return { ok: false, status: 413, error: `request body exceeds ${maxBytes} bytes` };
|
|
45
|
+
}
|
|
46
|
+
chunks.push(value);
|
|
47
|
+
}
|
|
48
|
+
return { ok: true, bytes: concatBytes(chunks) };
|
|
49
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from "./security";
|
|
31
31
|
import { handleCli } from "./cli";
|
|
32
32
|
import { startMaintenanceScheduler } from "./maintenance";
|
|
33
|
+
import { errMessage } from "agent-relay-sdk";
|
|
33
34
|
|
|
34
35
|
async function main(): Promise<void> {
|
|
35
36
|
const result = await handleCli(process.argv.slice(2));
|
|
@@ -393,7 +394,7 @@ function staticContentType(pathname: string): string | undefined {
|
|
|
393
394
|
|
|
394
395
|
if (import.meta.main) {
|
|
395
396
|
main().catch((error) => {
|
|
396
|
-
console.error(
|
|
397
|
+
console.error(errMessage(error));
|
|
397
398
|
process.exit(1);
|
|
398
399
|
});
|
|
399
400
|
}
|
package/src/maintenance.ts
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
import type { WorkspaceMergePreview, WorkspaceRecord, WorkspaceStatus } from "./types";
|
|
35
35
|
import { requestWorkspaceMerge } from "./workspace-merge";
|
|
36
36
|
import { workspaceActiveClaim } from "./workspace-claim";
|
|
37
|
-
import { RELAY_TOKEN_HEADER } from "agent-relay-sdk";
|
|
37
|
+
import { errMessage, RELAY_TOKEN_HEADER } from "agent-relay-sdk";
|
|
38
38
|
import { getStewardConfig } from "./config-store";
|
|
39
39
|
import { ensureRepoSteward } from "./steward";
|
|
40
40
|
import { emitRelayEvent } from "./events";
|
|
@@ -931,7 +931,7 @@ async function runDueMaintenanceJobs(): Promise<void> {
|
|
|
931
931
|
for (const row of rows) {
|
|
932
932
|
const definition = definitions.find((job) => job.id === row.id);
|
|
933
933
|
if (definition) void runJob(definition).catch((error) => {
|
|
934
|
-
console.warn(`maintenance job ${definition.id} failed: ${
|
|
934
|
+
console.warn(`maintenance job ${definition.id} failed: ${errMessage(error)}`);
|
|
935
935
|
});
|
|
936
936
|
}
|
|
937
937
|
}
|
|
@@ -999,7 +999,7 @@ async function runJob(definition: MaintenanceJobDefinition, options: { force?: b
|
|
|
999
999
|
} catch (error) {
|
|
1000
1000
|
const finishedAt = Date.now();
|
|
1001
1001
|
const durationMs = finishedAt - startedAt;
|
|
1002
|
-
const message =
|
|
1002
|
+
const message = errMessage(error);
|
|
1003
1003
|
db.query(`
|
|
1004
1004
|
UPDATE maintenance_jobs
|
|
1005
1005
|
SET last_run_at = ?, next_run_at = ?, last_duration_ms = ?, last_status = 'failed',
|
package/src/mcp.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { getArtifactStorage, maxArtifactBytes, normalizeDigest } from "./artifac
|
|
|
3
3
|
import { createCommand } from "./commands-db";
|
|
4
4
|
import { buildSpawnCommand, generateSpawnRequestId, resolveSpawnModelParams, type SpawnModelParams } from "./spawn-command";
|
|
5
5
|
import { isPathWithinBase } from "./utils";
|
|
6
|
+
import { optionalEnum } from "./validation";
|
|
7
|
+
import { listManagedOrchestratorsForAgent } from "./orchestrator-lookup";
|
|
8
|
+
import { bytesToStream, readBodyBytes } from "./http-body";
|
|
6
9
|
import { MAX_BODY_BYTES, VERSION } from "./config";
|
|
7
10
|
import { getManagedAgentState, getSpawnPolicy, listSpawnPolicies } from "./config-store";
|
|
8
11
|
import {
|
|
@@ -34,7 +37,7 @@ import {
|
|
|
34
37
|
} from "./security";
|
|
35
38
|
import type { ActivityKind, AgentCard, ArtifactKind, ArtifactSensitivity, AttachmentRef, Command, SendMessageInput, Message, SpawnApprovalMode, SpawnProvider } from "./types";
|
|
36
39
|
import { type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
37
|
-
import { isRecord, SPAWN_PROVIDERS, APPROVAL_MODES, VALID_EFFORTS } from "agent-relay-sdk";
|
|
40
|
+
import { errMessage, isRecord, SPAWN_PROVIDERS, APPROVAL_MODES, VALID_EFFORTS } from "agent-relay-sdk";
|
|
38
41
|
import { childRunnerRuntimeTokenEnv, runnerRuntimeTokenEnv } from "./runtime-tokens";
|
|
39
42
|
|
|
40
43
|
type JsonRpcId = string | number | null;
|
|
@@ -272,7 +275,7 @@ export async function postMcp(req: Request): Promise<Response> {
|
|
|
272
275
|
}
|
|
273
276
|
return Response.json(jsonRpcError(id, -32601, `unknown MCP method: ${method}`));
|
|
274
277
|
} catch (error) {
|
|
275
|
-
const message =
|
|
278
|
+
const message = errMessage(error);
|
|
276
279
|
const status = error instanceof McpAuthError ? 403 : error instanceof McpNotFoundError ? 404 : 400;
|
|
277
280
|
const code = error instanceof McpAuthError ? -32001 : error instanceof McpNotFoundError ? -32004 : -32602;
|
|
278
281
|
return Response.json(jsonRpcError(id, code, message, { status }));
|
|
@@ -284,23 +287,11 @@ async function parseJsonRpcRequest(req: Request): Promise<
|
|
|
284
287
|
| { ok: false; status: number; error: string }
|
|
285
288
|
> {
|
|
286
289
|
if (!req.body) return { ok: false, status: 400, error: "JSON-RPC body required" };
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
while (true) {
|
|
291
|
-
const { done, value } = await reader.read();
|
|
292
|
-
if (done) break;
|
|
293
|
-
if (!value) continue;
|
|
294
|
-
total += value.byteLength;
|
|
295
|
-
const maxBodyBytes = Math.max(MAX_BODY_BYTES, maxArtifactBytes() + MCP_BODY_OVERHEAD_BYTES);
|
|
296
|
-
if (total > maxBodyBytes) {
|
|
297
|
-
await reader.cancel().catch(() => {});
|
|
298
|
-
return { ok: false, status: 413, error: `request body exceeds ${maxBodyBytes} bytes` };
|
|
299
|
-
}
|
|
300
|
-
chunks.push(value);
|
|
301
|
-
}
|
|
290
|
+
const maxBodyBytes = Math.max(MAX_BODY_BYTES, maxArtifactBytes() + MCP_BODY_OVERHEAD_BYTES);
|
|
291
|
+
const read = await readBodyBytes(req.body, maxBodyBytes);
|
|
292
|
+
if (!read.ok) return read;
|
|
302
293
|
try {
|
|
303
|
-
const body = JSON.parse(new TextDecoder().decode(
|
|
294
|
+
const body = JSON.parse(new TextDecoder().decode(read.bytes)) as unknown;
|
|
304
295
|
if (!isRecord(body)) return { ok: false, status: 400, error: "JSON-RPC body must be an object" };
|
|
305
296
|
return { ok: true, request: body as JsonRpcRequest };
|
|
306
297
|
} catch {
|
|
@@ -308,17 +299,6 @@ async function parseJsonRpcRequest(req: Request): Promise<
|
|
|
308
299
|
}
|
|
309
300
|
}
|
|
310
301
|
|
|
311
|
-
function concatBytes(chunks: Uint8Array[]): Uint8Array {
|
|
312
|
-
const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
313
|
-
const output = new Uint8Array(total);
|
|
314
|
-
let offset = 0;
|
|
315
|
-
for (const chunk of chunks) {
|
|
316
|
-
output.set(chunk, offset);
|
|
317
|
-
offset += chunk.byteLength;
|
|
318
|
-
}
|
|
319
|
-
return output;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
302
|
function mcpAuthContext(req: Request): McpAuthContext {
|
|
323
303
|
const component = getComponentAuth(req);
|
|
324
304
|
if (component) return { actor: component.sub, kind: "component", scopes: component.scope, component };
|
|
@@ -354,7 +334,7 @@ async function callTool(auth: McpAuthContext, params: unknown): Promise<Record<s
|
|
|
354
334
|
auditToolCall(auth, name, "ok", result);
|
|
355
335
|
return toolResult(result);
|
|
356
336
|
} catch (error) {
|
|
357
|
-
const message =
|
|
337
|
+
const message = errMessage(error);
|
|
358
338
|
auditToolCall(auth, name, error instanceof McpAuthError ? "denied" : "error", undefined, message);
|
|
359
339
|
throw error;
|
|
360
340
|
}
|
|
@@ -732,7 +712,7 @@ function selectControlOrchestrator(input: {
|
|
|
732
712
|
return orchestrator;
|
|
733
713
|
}
|
|
734
714
|
const agent = input.agentId ? getAgent(input.agentId) : null;
|
|
735
|
-
const orchestrator = agent ? managedControlOrchestrator(agent, input) :
|
|
715
|
+
const orchestrator = agent ? managedControlOrchestrator(agent, input) : (listManagedOrchestratorsForAgent(input)[0] ?? null);
|
|
736
716
|
if (!orchestrator) throw new McpNotFoundError("no orchestrator found for agent control target");
|
|
737
717
|
if (orchestrator.status !== "online") throw new ValidationError("orchestrator is offline");
|
|
738
718
|
return orchestrator;
|
|
@@ -742,29 +722,13 @@ function managedControlOrchestrator(
|
|
|
742
722
|
agent: AgentCard,
|
|
743
723
|
input: { policyName?: string; spawnRequestId?: string; tmuxSession?: string },
|
|
744
724
|
): NonNullable<ReturnType<typeof getOrchestrator>> | null {
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
const metaSpawnRequestId = input.spawnRequestId ?? (typeof agent.meta?.spawnRequestId === "string" ? agent.meta.spawnRequestId : "");
|
|
748
|
-
return findManagedOrchestrator({
|
|
725
|
+
const str = (v: unknown): string | undefined => (typeof v === "string" ? v : undefined);
|
|
726
|
+
return listManagedOrchestratorsForAgent({
|
|
749
727
|
agentId: agent.id,
|
|
750
|
-
policyName:
|
|
751
|
-
spawnRequestId:
|
|
752
|
-
tmuxSession: input.tmuxSession ??
|
|
753
|
-
}) ?? (agent.machine ? getOrchestrator(agent.machine) : null);
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
function findManagedOrchestrator(input: {
|
|
757
|
-
agentId?: string;
|
|
758
|
-
policyName?: string;
|
|
759
|
-
spawnRequestId?: string;
|
|
760
|
-
tmuxSession?: string;
|
|
761
|
-
}): NonNullable<ReturnType<typeof getOrchestrator>> | null {
|
|
762
|
-
return listOrchestrators().find((orchestrator) => orchestrator.managedAgents.some((managed) =>
|
|
763
|
-
(!!input.agentId && managed.agentId === input.agentId) ||
|
|
764
|
-
(!!input.tmuxSession && managed.tmuxSession === input.tmuxSession) ||
|
|
765
|
-
(!!input.policyName && managed.policyName === input.policyName) ||
|
|
766
|
-
(!!input.spawnRequestId && managed.spawnRequestId === input.spawnRequestId)
|
|
767
|
-
)) ?? null;
|
|
728
|
+
policyName: input.policyName ?? str(agent.meta?.policyName),
|
|
729
|
+
spawnRequestId: input.spawnRequestId ?? str(agent.meta?.spawnRequestId),
|
|
730
|
+
tmuxSession: input.tmuxSession ?? str(agent.meta?.tmuxSession),
|
|
731
|
+
})[0] ?? (agent.machine ? getOrchestrator(agent.machine) : null);
|
|
768
732
|
}
|
|
769
733
|
|
|
770
734
|
function providerSelection(provider: SpawnProvider, args: Record<string, unknown>): SpawnModelParams {
|
|
@@ -773,15 +737,6 @@ function providerSelection(provider: SpawnProvider, args: Record<string, unknown
|
|
|
773
737
|
return resolveSpawnModelParams(provider, model, effort);
|
|
774
738
|
}
|
|
775
739
|
|
|
776
|
-
function bytesToStream(bytes: Uint8Array): ReadableStream<Uint8Array> {
|
|
777
|
-
return new ReadableStream<Uint8Array>({
|
|
778
|
-
start(controller) {
|
|
779
|
-
controller.enqueue(bytes);
|
|
780
|
-
controller.close();
|
|
781
|
-
},
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
|
-
|
|
785
740
|
function artifactBytes(args: Record<string, unknown>): Uint8Array {
|
|
786
741
|
const hasContent = args.content !== undefined && args.content !== null;
|
|
787
742
|
const hasBase64 = args.base64 !== undefined && args.base64 !== null;
|
|
@@ -956,12 +911,6 @@ function optionalBoolean(value: unknown, field: string): boolean | undefined {
|
|
|
956
911
|
return value;
|
|
957
912
|
}
|
|
958
913
|
|
|
959
|
-
function optionalEnum<T extends readonly string[]>(value: unknown, field: string, valid: T): T[number] | undefined {
|
|
960
|
-
if (value === undefined || value === null) return undefined;
|
|
961
|
-
if (typeof value !== "string" || !valid.includes(value as T[number])) throw new ValidationError(`${field} must be one of: ${valid.join(", ")}`);
|
|
962
|
-
return value as T[number];
|
|
963
|
-
}
|
|
964
|
-
|
|
965
914
|
function enumField<T extends readonly string[]>(value: unknown, field: string, valid: T): T[number] {
|
|
966
915
|
const cleaned = optionalEnum(value, field, valid);
|
|
967
916
|
if (!cleaned) throw new ValidationError(`${field} required`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RELAY_TOKEN_HEADER } from "agent-relay-sdk";
|
|
1
|
+
import { errMessage, RELAY_TOKEN_HEADER } from "agent-relay-sdk";
|
|
2
2
|
|
|
3
3
|
interface MemoryBrokerSmokeOptions {
|
|
4
4
|
baseUrl?: string;
|
|
@@ -137,7 +137,7 @@ async function step<T>(steps: MemoryBrokerSmokeStep[], name: string, detail: str
|
|
|
137
137
|
steps.push({ name, ok: true, detail });
|
|
138
138
|
return result;
|
|
139
139
|
} catch (error) {
|
|
140
|
-
const message =
|
|
140
|
+
const message = errMessage(error);
|
|
141
141
|
steps.push({ name, ok: false, detail: message });
|
|
142
142
|
throw error;
|
|
143
143
|
}
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
normalizeMemorySearchResult,
|
|
21
21
|
} from "./memory-broker-contract";
|
|
22
22
|
import { normalizeContextPackage } from "./memory-http-broker";
|
|
23
|
-
import { isRecord } from "agent-relay-sdk";
|
|
23
|
+
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
24
24
|
|
|
25
25
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
26
26
|
|
|
@@ -105,7 +105,7 @@ export class CommandMemoryBroker implements MemoryBroker {
|
|
|
105
105
|
return unwrapResult(parseJson(stdout, operation));
|
|
106
106
|
} catch (error) {
|
|
107
107
|
if (error instanceof MemoryBrokerContractError) throw error;
|
|
108
|
-
throw new MemoryBrokerContractError(`command memory broker ${operation} failed: ${
|
|
108
|
+
throw new MemoryBrokerContractError(`command memory broker ${operation} failed: ${errMessage(error)}`);
|
|
109
109
|
} finally {
|
|
110
110
|
clearTimeout(timer);
|
|
111
111
|
}
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
normalizeMemoryBrokerCapabilities,
|
|
20
20
|
normalizeMemorySearchResult,
|
|
21
21
|
} from "./memory-broker-contract";
|
|
22
|
-
import { isRecord } from "agent-relay-sdk";
|
|
22
|
+
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
23
23
|
|
|
24
24
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
25
25
|
|
|
@@ -99,7 +99,7 @@ export class HttpMemoryBroker implements MemoryBroker {
|
|
|
99
99
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
100
100
|
throw new MemoryBrokerContractError(`http memory broker ${operation} timed out after ${this.timeoutMs}ms`);
|
|
101
101
|
}
|
|
102
|
-
throw new MemoryBrokerContractError(`http memory broker ${operation} failed: ${
|
|
102
|
+
throw new MemoryBrokerContractError(`http memory broker ${operation} failed: ${errMessage(error)}`);
|
|
103
103
|
} finally {
|
|
104
104
|
clearTimeout(timer);
|
|
105
105
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { listOrchestrators } from "./db";
|
|
2
|
+
|
|
3
|
+
export interface ManagedAgentMatch {
|
|
4
|
+
agentId?: string;
|
|
5
|
+
sessionName?: string;
|
|
6
|
+
tmuxSession?: string;
|
|
7
|
+
policyName?: string;
|
|
8
|
+
spawnRequestId?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Every orchestrator that owns a managed agent matching ANY provided identity
|
|
13
|
+
* key. Single home for the managed-agent→orchestrator lookup that routes.ts and
|
|
14
|
+
* mcp.ts each open-coded. routes also matches `sessionName`; mcp never passed it,
|
|
15
|
+
* so leaving the key undefined reproduces mcp's behavior exactly. Callers layer
|
|
16
|
+
* their own online / runner-managed / machine-fallback policy on top — routes
|
|
17
|
+
* picks the first online candidate, mcp takes the first match.
|
|
18
|
+
*/
|
|
19
|
+
export function listManagedOrchestratorsForAgent(match: ManagedAgentMatch): ReturnType<typeof listOrchestrators> {
|
|
20
|
+
return listOrchestrators().filter((orch) =>
|
|
21
|
+
orch.managedAgents.some((managed) =>
|
|
22
|
+
(!!match.agentId && managed.agentId === match.agentId) ||
|
|
23
|
+
(!!match.sessionName && managed.sessionName === match.sessionName) ||
|
|
24
|
+
(!!match.tmuxSession && managed.tmuxSession === match.tmuxSession) ||
|
|
25
|
+
(!!match.policyName && managed.policyName === match.policyName) ||
|
|
26
|
+
(!!match.spawnRequestId && managed.spawnRequestId === match.spawnRequestId),
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
}
|
package/src/recipe-validator.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MemoryType, Recipe, RecipeAgent, RecipeMemoryPolicy } from "./types";
|
|
2
2
|
import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
3
|
-
import { isRecord } from "agent-relay-sdk";
|
|
3
|
+
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
4
4
|
|
|
5
5
|
class RecipeValidationError extends Error {}
|
|
6
6
|
|
|
@@ -52,7 +52,7 @@ function validateAgent(value: unknown): RecipeAgent {
|
|
|
52
52
|
try {
|
|
53
53
|
resolveProviderSelection({ provider, model, effort });
|
|
54
54
|
} catch (error) {
|
|
55
|
-
throw new RecipeValidationError(
|
|
55
|
+
throw new RecipeValidationError(errMessage(error));
|
|
56
56
|
}
|
|
57
57
|
return {
|
|
58
58
|
role: requiredString(value.role, "agent.role"),
|