agent-relay-server 0.17.0 → 0.18.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/docs/openapi.json +101 -1
- package/package.json +2 -2
- package/public/index.html +25 -22
- package/public/sw.js +51 -16
- package/runner/src/adapter.ts +1 -4
- package/runner/src/config.ts +1 -4
- package/scripts/orchestrator-spawn-smoke.ts +2 -1
- package/src/automations.ts +9 -33
- package/src/bus.ts +2 -17
- package/src/cli.ts +179 -3
- package/src/command-events.ts +26 -0
- package/src/config-store.ts +5 -22
- package/src/connectors.ts +1 -4
- package/src/contracts.ts +2 -8
- package/src/db.ts +14 -15
- package/src/index.ts +99 -4
- package/src/lifecycle-manager.ts +11 -24
- package/src/maintenance.ts +26 -20
- package/src/managed-policy.ts +9 -28
- package/src/mcp.ts +19 -43
- package/src/memory-broker-smoke.ts +3 -1
- package/src/memory-command-broker.ts +1 -4
- package/src/memory-http-broker.ts +1 -4
- package/src/memory-service.ts +1 -4
- package/src/memory-sqlite-broker.ts +1 -8
- package/src/provider-catalog-store.ts +3 -11
- package/src/recipe-loader.ts +1 -4
- package/src/recipe-validator.ts +1 -4
- package/src/routes.ts +249 -142
- package/src/security.ts +3 -7
- package/src/spawn-command.ts +150 -0
- package/src/sse.ts +1 -4
- package/src/steward.ts +16 -21
- package/src/upgrade.ts +3 -2
- package/src/utils.ts +38 -0
- package/src/validation.ts +28 -0
- package/src/workspace-claim.ts +29 -0
- package/src/workspace-merge.ts +21 -9
package/docs/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Relay API",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.17.0",
|
|
6
6
|
"description": "Real-time message bus for inter-agent communication. Agent-first: this spec is designed for machine consumption — agents can self-discover the full API surface via GET /api/spec.",
|
|
7
7
|
"license": {
|
|
8
8
|
"name": "MIT",
|
|
@@ -3818,6 +3818,58 @@
|
|
|
3818
3818
|
]
|
|
3819
3819
|
}
|
|
3820
3820
|
},
|
|
3821
|
+
"/api/workspaces/actions/cleanup-stale": {
|
|
3822
|
+
"post": {
|
|
3823
|
+
"operationId": "postWorkspaceCleanupStale",
|
|
3824
|
+
"summary": "Post Workspace Cleanup Stale",
|
|
3825
|
+
"tags": [
|
|
3826
|
+
"Other"
|
|
3827
|
+
],
|
|
3828
|
+
"requestBody": {
|
|
3829
|
+
"required": true,
|
|
3830
|
+
"content": {
|
|
3831
|
+
"application/json": {
|
|
3832
|
+
"schema": {
|
|
3833
|
+
"type": "object",
|
|
3834
|
+
"properties": {
|
|
3835
|
+
"repoRoot": {
|
|
3836
|
+
"type": "string"
|
|
3837
|
+
},
|
|
3838
|
+
"dryRun": {
|
|
3839
|
+
"type": "string"
|
|
3840
|
+
},
|
|
3841
|
+
"landedOnly": {
|
|
3842
|
+
"type": "string"
|
|
3843
|
+
},
|
|
3844
|
+
"offlineOwnerOnly": {
|
|
3845
|
+
"type": "string"
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
},
|
|
3852
|
+
"responses": {
|
|
3853
|
+
"200": {
|
|
3854
|
+
"description": "Success",
|
|
3855
|
+
"content": {
|
|
3856
|
+
"application/json": {}
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
},
|
|
3860
|
+
"security": [
|
|
3861
|
+
{
|
|
3862
|
+
"bearerAuth": []
|
|
3863
|
+
},
|
|
3864
|
+
{
|
|
3865
|
+
"tokenHeader": []
|
|
3866
|
+
},
|
|
3867
|
+
{
|
|
3868
|
+
"tokenQuery": []
|
|
3869
|
+
}
|
|
3870
|
+
]
|
|
3871
|
+
}
|
|
3872
|
+
},
|
|
3821
3873
|
"/api/workspaces/{id}": {
|
|
3822
3874
|
"get": {
|
|
3823
3875
|
"operationId": "getWorkspaceById",
|
|
@@ -4005,6 +4057,54 @@
|
|
|
4005
4057
|
]
|
|
4006
4058
|
}
|
|
4007
4059
|
},
|
|
4060
|
+
"/api/workspaces/{id}/diagnostics": {
|
|
4061
|
+
"get": {
|
|
4062
|
+
"operationId": "getWorkspaceDiagnostics",
|
|
4063
|
+
"summary": "Get Workspace Diagnostics",
|
|
4064
|
+
"tags": [
|
|
4065
|
+
"Other"
|
|
4066
|
+
],
|
|
4067
|
+
"parameters": [
|
|
4068
|
+
{
|
|
4069
|
+
"name": "id",
|
|
4070
|
+
"in": "path",
|
|
4071
|
+
"required": true,
|
|
4072
|
+
"schema": {
|
|
4073
|
+
"type": "string"
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
],
|
|
4077
|
+
"responses": {
|
|
4078
|
+
"200": {
|
|
4079
|
+
"description": "Success",
|
|
4080
|
+
"content": {
|
|
4081
|
+
"application/json": {}
|
|
4082
|
+
}
|
|
4083
|
+
},
|
|
4084
|
+
"404": {
|
|
4085
|
+
"description": "Not found",
|
|
4086
|
+
"content": {
|
|
4087
|
+
"application/json": {
|
|
4088
|
+
"schema": {
|
|
4089
|
+
"$ref": "#/components/schemas/Error"
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
},
|
|
4095
|
+
"security": [
|
|
4096
|
+
{
|
|
4097
|
+
"bearerAuth": []
|
|
4098
|
+
},
|
|
4099
|
+
{
|
|
4100
|
+
"tokenHeader": []
|
|
4101
|
+
},
|
|
4102
|
+
{
|
|
4103
|
+
"tokenQuery": []
|
|
4104
|
+
}
|
|
4105
|
+
]
|
|
4106
|
+
}
|
|
4107
|
+
},
|
|
4008
4108
|
"/api/workspaces/{id}/diff": {
|
|
4009
4109
|
"get": {
|
|
4010
4110
|
"operationId": "getWorkspaceDiff",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Lightweight HTTP message relay for inter-agent communication across machines",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"CONTRIBUTING.md"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"agent-relay-sdk": "0.2.
|
|
36
|
+
"agent-relay-sdk": "0.2.9"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"prepack": "bun run build:dashboard:bundle >&2",
|
package/public/index.html
CHANGED
|
@@ -10816,6 +10816,21 @@ var PAIR_STATUS_COLORS = {
|
|
|
10816
10816
|
expired: "bg-red-500/20 text-red-400"
|
|
10817
10817
|
};
|
|
10818
10818
|
//#endregion
|
|
10819
|
+
//#region ../sdk/src/types.ts
|
|
10820
|
+
/** True for a non-null, non-array object. The canonical type guard for the whole repo. */
|
|
10821
|
+
function isRecord(value) {
|
|
10822
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10823
|
+
}
|
|
10824
|
+
/**
|
|
10825
|
+
* Narrow `unknown` to a non-empty trimmed string, else `undefined`.
|
|
10826
|
+
* Settled semantics: whitespace-only is treated as empty (returns `undefined`).
|
|
10827
|
+
*/
|
|
10828
|
+
function stringValue(value) {
|
|
10829
|
+
if (typeof value !== "string") return void 0;
|
|
10830
|
+
const trimmed = value.trim();
|
|
10831
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
10832
|
+
}
|
|
10833
|
+
//#endregion
|
|
10819
10834
|
//#region src/lib/display.ts
|
|
10820
10835
|
function toTimestamp(value) {
|
|
10821
10836
|
const ts = typeof value === "number" ? value : new Date(value || 0).getTime();
|
|
@@ -10901,7 +10916,7 @@ function isAgentStale(now, agent) {
|
|
|
10901
10916
|
if (!agent?.lastSeen || agent.status === "offline") return false;
|
|
10902
10917
|
if (agent.id === "user" || agent.id === "system") return false;
|
|
10903
10918
|
const transport = agent.meta?.transport;
|
|
10904
|
-
if (isRecord
|
|
10919
|
+
if (isRecord(transport) && transport.connected === true) return false;
|
|
10905
10920
|
const lastSeenMs = new Date(agent.lastSeen).getTime();
|
|
10906
10921
|
if (!Number.isFinite(lastSeenMs)) return false;
|
|
10907
10922
|
return now - lastSeenMs > 6e4;
|
|
@@ -11060,7 +11075,7 @@ function messageMatchesChannel(message, channel) {
|
|
|
11060
11075
|
}
|
|
11061
11076
|
function providerRuntimeState(agent) {
|
|
11062
11077
|
const raw = agent?.meta?.providerState;
|
|
11063
|
-
if (!isRecord
|
|
11078
|
+
if (!isRecord(raw) || typeof raw.state !== "string") return null;
|
|
11064
11079
|
return {
|
|
11065
11080
|
state: raw.state,
|
|
11066
11081
|
reason: typeof raw.reason === "string" ? raw.reason : void 0,
|
|
@@ -11073,16 +11088,16 @@ function providerRuntimeState(agent) {
|
|
|
11073
11088
|
};
|
|
11074
11089
|
}
|
|
11075
11090
|
function providerPendingApproval(value) {
|
|
11076
|
-
if (!isRecord
|
|
11077
|
-
const choices = Array.isArray(value.choices) ? value.choices.filter(isRecord
|
|
11091
|
+
if (!isRecord(value) || typeof value.id !== "string") return void 0;
|
|
11092
|
+
const choices = Array.isArray(value.choices) ? value.choices.filter(isRecord).map((choice) => ({
|
|
11078
11093
|
id: choice.id,
|
|
11079
11094
|
label: typeof choice.label === "string" ? choice.label : String(choice.id || "")
|
|
11080
11095
|
})).filter((choice) => (choice.id === "approve" || choice.id === "approve-session" || choice.id === "deny" || choice.id === "abort") && Boolean(choice.label)) : [];
|
|
11081
|
-
const questions = Array.isArray(value.questions) ? value.questions.filter(isRecord
|
|
11096
|
+
const questions = Array.isArray(value.questions) ? value.questions.filter(isRecord).map((q) => ({
|
|
11082
11097
|
question: typeof q.question === "string" ? q.question : "",
|
|
11083
11098
|
header: typeof q.header === "string" ? q.header : void 0,
|
|
11084
11099
|
multiSelect: q.multiSelect === true,
|
|
11085
|
-
options: Array.isArray(q.options) ? q.options.filter(isRecord
|
|
11100
|
+
options: Array.isArray(q.options) ? q.options.filter(isRecord).map((o) => ({
|
|
11086
11101
|
label: typeof o.label === "string" ? o.label : String(o.label ?? ""),
|
|
11087
11102
|
description: typeof o.description === "string" ? o.description : void 0
|
|
11088
11103
|
})).filter((o) => Boolean(o.label)) : []
|
|
@@ -11110,7 +11125,7 @@ function activeSubagents(agent) {
|
|
|
11110
11125
|
label: `Subagent ${index + 1}`
|
|
11111
11126
|
})) : [];
|
|
11112
11127
|
}
|
|
11113
|
-
return raw.filter(isRecord
|
|
11128
|
+
return raw.filter(isRecord).map((item, index) => {
|
|
11114
11129
|
const id = typeof item.id === "string" && item.id ? item.id : `subagent-${index + 1}`;
|
|
11115
11130
|
const role = typeof item.role === "string" && item.role ? item.role : void 0;
|
|
11116
11131
|
return {
|
|
@@ -11256,9 +11271,6 @@ function presenceBadges(agent, attention, pair) {
|
|
|
11256
11271
|
});
|
|
11257
11272
|
return badges;
|
|
11258
11273
|
}
|
|
11259
|
-
function isRecord$2(value) {
|
|
11260
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11261
|
-
}
|
|
11262
11274
|
function emptyAttention() {
|
|
11263
11275
|
return {
|
|
11264
11276
|
unread: 0,
|
|
@@ -11525,7 +11537,7 @@ function downloadText(filename, text, type) {
|
|
|
11525
11537
|
URL.revokeObjectURL(url);
|
|
11526
11538
|
}
|
|
11527
11539
|
//#endregion
|
|
11528
|
-
//#region ../sdk/
|
|
11540
|
+
//#region ../sdk/src/provider-catalog.ts
|
|
11529
11541
|
var CLAUDE_LOW_TO_MAX = [
|
|
11530
11542
|
"low",
|
|
11531
11543
|
"medium",
|
|
@@ -108614,9 +108626,6 @@ function workspaceMode(agent) {
|
|
|
108614
108626
|
function recordValue(value) {
|
|
108615
108627
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
108616
108628
|
}
|
|
108617
|
-
function stringValue(value) {
|
|
108618
|
-
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
108619
|
-
}
|
|
108620
108629
|
function runtimeBadges(agent) {
|
|
108621
108630
|
const caps = agent.providerCapabilities;
|
|
108622
108631
|
const context = agent.context;
|
|
@@ -154067,7 +154076,7 @@ function AgentDiagnostics({ agent, orchestrators }) {
|
|
|
154067
154076
|
});
|
|
154068
154077
|
const policyName = typeof agent.meta?.policyName === "string" ? agent.meta.policyName : null;
|
|
154069
154078
|
const profileName = typeof agent.meta?.profile === "string" ? agent.meta.profile : null;
|
|
154070
|
-
const agentProfile = isRecord
|
|
154079
|
+
const agentProfile = isRecord(agent.meta?.agentProfile) ? agent.meta.agentProfile : null;
|
|
154071
154080
|
const profileProjection = isProfileProjectionReport(agentProfile?.projection) ? agentProfile.projection : null;
|
|
154072
154081
|
const managedOrch = orchestrators.find((o) => o.managedAgents.some((m) => m.agentId === agent.id));
|
|
154073
154082
|
const managedAgent = managedOrch?.managedAgents.find((m) => m.agentId === agent.id);
|
|
@@ -154558,11 +154567,8 @@ function CollapsibleSection({ title, expanded, onToggle, children }) {
|
|
|
154558
154567
|
})]
|
|
154559
154568
|
});
|
|
154560
154569
|
}
|
|
154561
|
-
function isRecord$1(value) {
|
|
154562
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
154563
|
-
}
|
|
154564
154570
|
function isProfileProjectionReport(value) {
|
|
154565
|
-
return isRecord
|
|
154571
|
+
return isRecord(value) && typeof value.profileName === "string" && typeof value.provider === "string" && typeof value.base === "string" && Array.isArray(value.entries) && Array.isArray(value.warnings) && Array.isArray(value.unsupported);
|
|
154566
154572
|
}
|
|
154567
154573
|
function KV({ label, value, className = "", mono = false }) {
|
|
154568
154574
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
@@ -155753,9 +155759,6 @@ function orchestratorForAgentTerminal(agent, session, orchestrators) {
|
|
|
155753
155759
|
if (!agent) return null;
|
|
155754
155760
|
return orchestrators.find((orchestrator) => orchestrator.managedAgents.some((managed) => managed.agentId === agent.id || managed.tmuxSession === session)) ?? null;
|
|
155755
155761
|
}
|
|
155756
|
-
function isRecord(value) {
|
|
155757
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
155758
|
-
}
|
|
155759
155762
|
//#endregion
|
|
155760
155763
|
//#region src/components/drawers/channel-detail-drawer.tsx
|
|
155761
155764
|
function ChannelDetailDrawer() {
|
package/public/sw.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const CACHE_NAME = "agent-relay-dashboard-
|
|
1
|
+
const CACHE_NAME = "agent-relay-dashboard-v2";
|
|
2
2
|
const scopeUrl = new URL(self.registration.scope);
|
|
3
3
|
const scopePath = scopeUrl.pathname.endsWith("/") ? scopeUrl.pathname : `${scopeUrl.pathname}/`;
|
|
4
4
|
const appUrl = (path) => new URL(path, self.registration.scope).toString();
|
|
@@ -11,10 +11,30 @@ const APP_SHELL = [
|
|
|
11
11
|
appUrl("icons/agent-relay-512.png"),
|
|
12
12
|
];
|
|
13
13
|
|
|
14
|
+
// The server may send the shell brotli/gzip-encoded. fetch() exposes the body
|
|
15
|
+
// already decoded but leaves Content-Encoding / Content-Length headers on the
|
|
16
|
+
// response — caching that verbatim risks a double-decode or
|
|
17
|
+
// ERR_CONTENT_LENGTH_MISMATCH on replay. Re-wrap with those headers stripped so
|
|
18
|
+
// the cached copy is clean identity bytes.
|
|
19
|
+
async function cacheNormalized(cache, key, response) {
|
|
20
|
+
const body = await response.clone().arrayBuffer();
|
|
21
|
+
const headers = new Headers(response.headers);
|
|
22
|
+
headers.delete("Content-Encoding");
|
|
23
|
+
headers.delete("Content-Length");
|
|
24
|
+
await cache.put(key, new Response(body, {
|
|
25
|
+
status: response.status,
|
|
26
|
+
statusText: response.statusText,
|
|
27
|
+
headers,
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
self.addEventListener("install", (event) => {
|
|
15
32
|
event.waitUntil(
|
|
16
33
|
caches.open(CACHE_NAME)
|
|
17
|
-
.then((cache) =>
|
|
34
|
+
.then((cache) => Promise.all(APP_SHELL.map(async (href) => {
|
|
35
|
+
const response = await fetch(href, { cache: "reload" });
|
|
36
|
+
if (response.ok) await cacheNormalized(cache, href, response);
|
|
37
|
+
})))
|
|
18
38
|
.then(() => self.skipWaiting()),
|
|
19
39
|
);
|
|
20
40
|
});
|
|
@@ -45,20 +65,35 @@ self.addEventListener("fetch", (event) => {
|
|
|
45
65
|
return;
|
|
46
66
|
}
|
|
47
67
|
|
|
68
|
+
// Stale-while-revalidate for the app shell and SPA navigations: serve the
|
|
69
|
+
// cached bundle instantly (the ~10 MB unminified shell never blocks the
|
|
70
|
+
// network on a slow/high-latency link) and refresh it in the background.
|
|
71
|
+
// SPA route navigations all resolve to index.html, so key them there to avoid
|
|
72
|
+
// fragmenting the cache by query string / deep-link path.
|
|
73
|
+
const isNavigation = request.mode === "navigate";
|
|
74
|
+
if (isNavigation || APP_SHELL.includes(url.href)) {
|
|
75
|
+
event.respondWith(staleWhileRevalidate(request, isNavigation ? appUrl("index.html") : request));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Other same-scope assets: network-first, fall back to cache when offline.
|
|
48
80
|
event.respondWith(
|
|
49
|
-
fetch(request)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
caches.open(CACHE_NAME).then((cache) => cache.put(request, copy));
|
|
54
|
-
}
|
|
55
|
-
return response;
|
|
56
|
-
})
|
|
57
|
-
.catch(async () => {
|
|
58
|
-
const cached = await caches.match(request);
|
|
59
|
-
if (cached) return cached;
|
|
60
|
-
if (request.mode === "navigate") return caches.match(appUrl("index.html"));
|
|
61
|
-
throw new Error("offline");
|
|
62
|
-
}),
|
|
81
|
+
fetch(request).catch(() => caches.match(request).then((c) => {
|
|
82
|
+
if (c) return c;
|
|
83
|
+
throw new Error("offline");
|
|
84
|
+
})),
|
|
63
85
|
);
|
|
64
86
|
});
|
|
87
|
+
|
|
88
|
+
async function staleWhileRevalidate(request, cacheKey) {
|
|
89
|
+
const cache = await caches.open(CACHE_NAME);
|
|
90
|
+
const cached = await cache.match(cacheKey);
|
|
91
|
+
const network = fetch(request)
|
|
92
|
+
.then(async (response) => {
|
|
93
|
+
if (response.ok) await cacheNormalized(cache, cacheKey, response);
|
|
94
|
+
return response;
|
|
95
|
+
})
|
|
96
|
+
.catch(() => cached);
|
|
97
|
+
// Return cache immediately when present; otherwise wait for the network.
|
|
98
|
+
return cached || network;
|
|
99
|
+
}
|
package/runner/src/adapter.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentProfile, Message } from "agent-relay-sdk";
|
|
2
|
+
import { isRecord } from "agent-relay-sdk";
|
|
2
3
|
|
|
3
4
|
export type SemanticStatus = "idle" | "busy" | "offline" | "error";
|
|
4
5
|
type ProviderWorkKind = "provider-turn" | "subagent";
|
|
@@ -160,10 +161,6 @@ export const RELAY_CONTEXT = `[agent-relay] You are connected to Agent Relay, a
|
|
|
160
161
|
|
|
161
162
|
const PROVIDER_MESSAGE_BODY_PREVIEW_CHARS = 4000;
|
|
162
163
|
|
|
163
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
164
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
165
|
-
}
|
|
166
|
-
|
|
167
164
|
function attachmentRefs(message: Message): Record<string, unknown>[] {
|
|
168
165
|
const payloadRefs = message.payload?.attachments;
|
|
169
166
|
const topLevelRefs = (message as Message & { attachments?: unknown }).attachments;
|
package/runner/src/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir, hostname } from "node:os";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
+
import { stringValue } from "agent-relay-sdk";
|
|
4
5
|
import type { ProviderConfig } from "./adapter";
|
|
5
6
|
|
|
6
7
|
interface GlobalRunnerConfig {
|
|
@@ -115,10 +116,6 @@ function readJson(path: string): Record<string, unknown> {
|
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
function stringValue(value: unknown): string | undefined {
|
|
119
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
119
|
function positiveInteger(value: unknown): number | undefined {
|
|
123
120
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
124
121
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { hostname } from "node:os";
|
|
3
|
+
import { RELAY_TOKEN_HEADER } from "agent-relay-sdk";
|
|
3
4
|
|
|
4
5
|
type Orchestrator = {
|
|
5
6
|
id: string;
|
|
@@ -44,7 +45,7 @@ relayUrl = relayUrl.replace(/\/+$/, "");
|
|
|
44
45
|
|
|
45
46
|
function headers(): Record<string, string> {
|
|
46
47
|
const h: Record<string, string> = { "Content-Type": "application/json" };
|
|
47
|
-
if (process.env.AGENT_RELAY_TOKEN) h[
|
|
48
|
+
if (process.env.AGENT_RELAY_TOKEN) h[RELAY_TOKEN_HEADER] = process.env.AGENT_RELAY_TOKEN;
|
|
48
49
|
return h;
|
|
49
50
|
}
|
|
50
51
|
|
package/src/automations.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { parseJson } from "./utils";
|
|
2
3
|
import {
|
|
3
4
|
getAgent,
|
|
4
5
|
getDb,
|
|
@@ -12,8 +13,11 @@ import {
|
|
|
12
13
|
ValidationError,
|
|
13
14
|
} from "./db";
|
|
14
15
|
import { createCommand } from "./commands-db";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
16
|
+
import { cleanString } from "./validation";
|
|
17
|
+
import { getAgentProfile, getSpawnPolicy } from "./config-store";
|
|
18
|
+
import { buildSpawnCommand, resolveSpawnModelParams } from "./spawn-command";
|
|
19
|
+
import { resolveProviderSelection, type ProviderEffort, VALID_EFFORTS } from "agent-relay-sdk/provider-catalog";
|
|
20
|
+
import { VALID_WORKSPACE_MODES } from "agent-relay-sdk";
|
|
17
21
|
import { runnerRuntimeTokenEnv } from "./runtime-tokens";
|
|
18
22
|
import type {
|
|
19
23
|
AgentCard,
|
|
@@ -43,8 +47,6 @@ const BLOCKING_RUN_STATUSES = new Set<AutomationRunStatus>(["dispatching", "wait
|
|
|
43
47
|
const OPEN_RUN_STATUSES = new Set<AutomationRunStatus>(["scheduled", "dispatching", "waiting_agent", "running"]);
|
|
44
48
|
const CLOSED_TASK_STATUS = new Set(["done", "failed", "canceled"]);
|
|
45
49
|
const MAX_CRON_SCAN_MINUTES = 366 * 24 * 60;
|
|
46
|
-
const VALID_EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const;
|
|
47
|
-
const VALID_WORKSPACE_MODES = ["isolated", "shared", "inherit"] as const;
|
|
48
50
|
const MIN_RUNTIME_BUDGET_MS = 60_000;
|
|
49
51
|
const MAX_RUNTIME_BUDGET_MS = 24 * 60 * 60 * 1000;
|
|
50
52
|
|
|
@@ -75,15 +77,6 @@ function ensureAutomationTables(): void {
|
|
|
75
77
|
initializedDb = current;
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
function parseJson<T>(value: unknown, fallback: T): T {
|
|
79
|
-
if (typeof value !== "string" || !value) return fallback;
|
|
80
|
-
try {
|
|
81
|
-
return JSON.parse(value) as T;
|
|
82
|
-
} catch {
|
|
83
|
-
return fallback;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
80
|
function rowToAutomation(row: any): Automation {
|
|
88
81
|
return {
|
|
89
82
|
id: row.id,
|
|
@@ -126,18 +119,6 @@ function rowToAutomationRun(row: any): AutomationRun {
|
|
|
126
119
|
};
|
|
127
120
|
}
|
|
128
121
|
|
|
129
|
-
function cleanString(value: unknown, field: string, opts: { required?: boolean; max?: number } = {}): string | undefined {
|
|
130
|
-
if (value === undefined || value === null) {
|
|
131
|
-
if (opts.required) throw new ValidationError(`${field} required`);
|
|
132
|
-
return undefined;
|
|
133
|
-
}
|
|
134
|
-
if (typeof value !== "string") throw new ValidationError(`${field} must be a string`);
|
|
135
|
-
const trimmed = value.trim();
|
|
136
|
-
if (opts.required && !trimmed) throw new ValidationError(`${field} required`);
|
|
137
|
-
if (opts.max && trimmed.length > opts.max) throw new ValidationError(`${field} must be ${opts.max} characters or fewer`);
|
|
138
|
-
return trimmed || undefined;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
122
|
function cleanStringArray(value: unknown, field: string): string[] | undefined {
|
|
142
123
|
if (value === undefined || value === null) return undefined;
|
|
143
124
|
if (!Array.isArray(value)) throw new ValidationError(`${field} must be an array`);
|
|
@@ -544,22 +525,17 @@ function dispatchOnDemandAutomation(
|
|
|
544
525
|
now: number,
|
|
545
526
|
): AutomationDispatchResult {
|
|
546
527
|
if (!orchestrator.providers.includes(policy.provider)) throw new ValidationError(`orchestrator ${orchestrator.id} does not have provider available: ${policy.provider}`);
|
|
547
|
-
const selection = resolveProviderSelection({ provider: policy.provider, model: policy.model, effort: policy.effort });
|
|
548
528
|
const agentProfile = policy.profile ? getAgentProfile(policy.profile)?.value : undefined;
|
|
549
529
|
const label = automationRunLabel(automation.id, run.id);
|
|
550
530
|
const command = createCommand({
|
|
551
531
|
type: "agent.spawn",
|
|
552
532
|
source: "automation",
|
|
553
533
|
target: orchestrator.agentId,
|
|
554
|
-
params: {
|
|
555
|
-
action: "spawn",
|
|
534
|
+
params: buildSpawnCommand({
|
|
556
535
|
provider: policy.provider,
|
|
557
|
-
|
|
558
|
-
providerModel: selection.providerModel,
|
|
559
|
-
effort: selection.effort,
|
|
536
|
+
modelParams: resolveSpawnModelParams(policy.provider, policy.model, policy.effort),
|
|
560
537
|
profile: policy.profile,
|
|
561
538
|
agentProfile,
|
|
562
|
-
...workspaceSpawnParams(),
|
|
563
539
|
cwd: policy.cwd || orchestrator.baseDir,
|
|
564
540
|
workspaceMode: policy.workspaceMode ?? "inherit",
|
|
565
541
|
label,
|
|
@@ -575,7 +551,7 @@ function dispatchOnDemandAutomation(
|
|
|
575
551
|
label,
|
|
576
552
|
createdBy: "automation",
|
|
577
553
|
}),
|
|
578
|
-
},
|
|
554
|
+
}),
|
|
579
555
|
});
|
|
580
556
|
const result = createRunTask(automation, run, `label:${label}`, now, {
|
|
581
557
|
targetMode: "on_demand_agent",
|
package/src/bus.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { createActivityEvent, getAgent, getDb, heartbeat, markReady, mergeAgentM
|
|
|
3
3
|
import { getOldestOutboxCursor, getOutboxCursor, replayEvents, type BusEvent } from "./bus-outbox";
|
|
4
4
|
import { emitRelayEvent, subscribeRelayEvents, type RelayEvent } from "./events";
|
|
5
5
|
import { createCommand, getCommand, updateCommand } from "./commands-db";
|
|
6
|
+
import { emitCommandEvent } from "./command-events";
|
|
6
7
|
import { getLifecycleManager } from "./lifecycle-manager";
|
|
7
8
|
import { noteAgentTimelineEvent, noteCompactionCommandCompleted } from "./compaction-watch";
|
|
8
9
|
import { applyCommandToRecipe } from "./recipe-runner";
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
type BusFrame,
|
|
14
15
|
type RegisterFrame,
|
|
15
16
|
} from "agent-relay-sdk/protocol";
|
|
17
|
+
import { isRecord, stringValue } from "agent-relay-sdk";
|
|
16
18
|
import { getComponentAuth, isComponentAuthorizedFor, isAuthorized, isOriginAllowed, unauthorized } from "./security";
|
|
17
19
|
import type { AgentCard, Command, ComponentToken, ContextState, Message, ProviderCapabilities, Task } from "./types";
|
|
18
20
|
|
|
@@ -611,15 +613,6 @@ function providerStateKey(state: Record<string, unknown> | null): string | null
|
|
|
611
613
|
return [state.state, typeof state.reason === "string" ? state.reason : ""].join(":");
|
|
612
614
|
}
|
|
613
615
|
|
|
614
|
-
function emitCommandEvent(command: Command, type: string): void {
|
|
615
|
-
emitRelayEvent({
|
|
616
|
-
type,
|
|
617
|
-
source: command.source,
|
|
618
|
-
subject: command.id,
|
|
619
|
-
data: { command },
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
|
|
623
616
|
function sendCommandResult(
|
|
624
617
|
ws: BusWebSocket,
|
|
625
618
|
commandId: string,
|
|
@@ -638,14 +631,6 @@ function sendCommandResult(
|
|
|
638
631
|
});
|
|
639
632
|
}
|
|
640
633
|
|
|
641
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
642
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
function stringValue(value: unknown): string | undefined {
|
|
646
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
634
|
function send(ws: BusWebSocket, frame: Record<string, unknown>): void {
|
|
650
635
|
ws.send(JSON.stringify(frame));
|
|
651
636
|
}
|