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 CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Relay API",
5
- "version": "0.16.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.17.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.8"
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$2(transport) && transport.connected === true) return false;
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$2(raw) || typeof raw.state !== "string") return null;
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$2(value) || typeof value.id !== "string") return void 0;
11077
- const choices = Array.isArray(value.choices) ? value.choices.filter(isRecord$2).map((choice) => ({
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$2).map((q) => ({
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$2).map((o) => ({
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$2).map((item, index) => {
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/dist/provider-catalog.js
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$1(agent.meta?.agentProfile) ? agent.meta.agentProfile : null;
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$1(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);
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-v1";
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) => cache.addAll(APP_SHELL))
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
- .then((response) => {
51
- if (response.ok && APP_SHELL.includes(url.href)) {
52
- const copy = response.clone();
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
+ }
@@ -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;
@@ -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["X-Agent-Relay-Token"] = process.env.AGENT_RELAY_TOKEN;
48
+ if (process.env.AGENT_RELAY_TOKEN) h[RELAY_TOKEN_HEADER] = process.env.AGENT_RELAY_TOKEN;
48
49
  return h;
49
50
  }
50
51
 
@@ -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 { getAgentProfile, getSpawnPolicy, workspaceSpawnParams } from "./config-store";
16
- import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog";
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
- model: selection.modelAlias,
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
  }