agent-relay-server 0.70.1 → 0.71.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.
|
|
5
|
+
"version": "0.71.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",
|
package/package.json
CHANGED
package/src/config-store.ts
CHANGED
|
@@ -49,16 +49,18 @@ const VALID_PROFILE_FILESYSTEM_SCOPES = ["repo", "workspace", "host"] as const;
|
|
|
49
49
|
const VALID_PERMISSION_MODES = ["open", "guarded", "read-only"] as const;
|
|
50
50
|
const VALID_POLICY_MODES = ["always-on", "on-demand"] as const;
|
|
51
51
|
const VALID_MANAGED_STATUSES = ["stopped", "starting", "running", "stopping", "backoff", "failed"] as const;
|
|
52
|
+
export const DEFAULT_SPAWN_PROFILE_NAME = "default-relay";
|
|
52
53
|
const COORDINATION_CHARTER = "Coordination charter: stay at helicopter altitude; delegate implementation to spawned workers; write briefs, maintain the blackboard and plan DAG; coordinate, review, and release; do not drift into mechanical edits when a worker can do them; protect your context for coordination.";
|
|
53
54
|
const OPS_ASSISTANT_BRIEF = [
|
|
54
55
|
"Ops-assistant standing brief: execute mechanical operations on your coordinator's behalf: test suites, release commands such as `bun run release`, smoke and verification probes, gates, and host/version/status checks.",
|
|
56
|
+
"When running a self-verifying long script, especially `bun run release` (gates -> CI-watch -> deploy -> host-verify, then one final result), run it and wait for completion. Do not poll, monitor, tail logs, run parallel status/version checks, or otherwise babysit it while it runs. Trust the script; act only on its final result.",
|
|
55
57
|
"Hold and apply session-local ops conventions the coordinator gives you. Report crisp outcomes only: command/gate, pass/fail, commit/version/host evidence, and the next blocker when one exists. Do not dump logs unless asked.",
|
|
56
58
|
"Do not make strategy, design, scope, or release-timing calls. Escalate those decisions to your spawn-parent. Report operational results to your spawn-parent by default and keep their context clean.",
|
|
57
59
|
].join("\n");
|
|
58
|
-
const BUILT_IN_AGENT_PROFILE_NAMES = new Set([
|
|
60
|
+
const BUILT_IN_AGENT_PROFILE_NAMES = new Set([DEFAULT_SPAWN_PROFILE_NAME, "minimal", "isolated-research", "vanilla", "lean-worker", "coordinator", "ops-assistant", "worker", "reviewer"]);
|
|
59
61
|
|
|
60
62
|
const BUILT_IN_AGENT_PROFILES: AgentProfile[] = [
|
|
61
|
-
agentProfileDefaults({ name:
|
|
63
|
+
agentProfileDefaults({ name: DEFAULT_SPAWN_PROFILE_NAME, description: "Relay-aware spawned agent with bundled Relay context, skills, plugins, and status integration.", base: "host" }),
|
|
62
64
|
agentProfileDefaults({ name: "minimal", description: "Repo-focused spawned agent with Relay affordances disabled and host/global extras avoided where supported.", base: "minimal" }),
|
|
63
65
|
agentProfileDefaults({ name: "isolated-research", description: "Strict profile for autonomous research runs: repo context only, no Relay prompt surface, no global extras where supported.", base: "isolated" }),
|
|
64
66
|
agentProfileDefaults({ name: "vanilla", description: "Zero host bleed (#552): isolated provider home with host plugins, skills, hooks, MCP, and the CLAUDE.md/AGENTS.md cascade (repo + global) all suppressed at launch. Relay provisions any surface explicitly per profile.", base: "vanilla" }),
|
package/src/mcp/tools-spawn.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { buildSpawnTargets } from "../spawn-targets";
|
|
|
5
5
|
import { McpAuthError, McpNotFoundError } from "../mcp-errors";
|
|
6
6
|
import { parseProjectInstructions, PROJECT_INSTRUCTION_POLICY_NAMES } from "../project-instructions";
|
|
7
7
|
import { ValidationError } from "../db";
|
|
8
|
+
import { DEFAULT_SPAWN_PROFILE_NAME } from "../config-store";
|
|
8
9
|
import { optionalEnum } from "../validation";
|
|
9
10
|
import { APPROVAL_MODES, SPAWN_PROVIDERS, VALID_AGENT_LIFECYCLES, VALID_EFFORTS, VALID_WORKSPACE_MODES } from "agent-relay-sdk";
|
|
10
11
|
import type { ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
@@ -16,7 +17,7 @@ import { enumField, optionalNonNegativeInt, optionalPositiveInt, optionalString,
|
|
|
16
17
|
export const SPAWN_TOOLS: ToolDefinition[] = [
|
|
17
18
|
{
|
|
18
19
|
name: "relay_spawn_agent",
|
|
19
|
-
description:
|
|
20
|
+
description: `Spawn a long-living provider agent through Relay's orchestrator, optionally handing it its first task via \`prompt\` in the same call. Defaults to your own host (override with orchestratorId), applies profile \`${DEFAULT_SPAWN_PROFILE_NAME}\` when profile is omitted, and returns the resolved agent id once it registers. Gated: requires the command:spawn scope, granted only to agents whose profile sets maxSpawnedAgents>0, up to that live-children quota. Spawned agents cannot themselves spawn (no grandchildren).`,
|
|
20
21
|
requiredScopes: ["command:spawn"],
|
|
21
22
|
inputSchema: {
|
|
22
23
|
type: "object",
|
|
@@ -32,7 +33,7 @@ export const SPAWN_TOOLS: ToolDefinition[] = [
|
|
|
32
33
|
expectReply: { type: "boolean", description: "Require the worker to report back: it gets a pending reply obligation to you and CANNOT go idle until it delivers its result (which routes to your inbox and wakes you). Defaults true when you pass a prompt — set false for a true fire-and-forget worker that needn't report (e.g. one that only lands a branch)." },
|
|
33
34
|
systemPromptAppend: { type: "string" },
|
|
34
35
|
projectInstructions: { type: "object", description: "#409 — project instruction projection (default OFF). When set, relay composes a knowledge preamble from the spawn cwd's resolved project (own + inherited entries) and prepends it to systemPromptAppend, instead of relying on host CLAUDE.md. `policy: \"all\"` includes ingested host content; `\"vanilla\"` excludes it (manual+promoted only). Omit to keep current behavior.", properties: { policy: { type: "string", enum: PROJECT_INSTRUCTION_POLICY_NAMES } }, additionalProperties: false },
|
|
35
|
-
profile: { type: "string", description:
|
|
36
|
+
profile: { type: "string", description: `Agent profile name to apply (env, instructions, permissions, MCP/skills, spawn quota). Omit to use \`${DEFAULT_SPAWN_PROFILE_NAME}\`.` },
|
|
36
37
|
tags: { type: "array", items: { type: "string" } },
|
|
37
38
|
capabilities: { type: "array", items: { type: "string" } },
|
|
38
39
|
providerArgs: { type: "array", items: { type: "string" } },
|
|
@@ -62,7 +62,7 @@ type LandIssueRefSource = "association" | "commit" | "backlink";
|
|
|
62
62
|
|
|
63
63
|
type CloseIssueLifecycleResult =
|
|
64
64
|
| { issueRef: IssueRef; action: "closed"; source: LandIssueRefSource; landKey: string }
|
|
65
|
-
| { issueRef: IssueRef; action: "skipped"; reason: "already-closed" | "already-processed"; landKey: string }
|
|
65
|
+
| { issueRef: IssueRef; action: "skipped"; reason: "already-closed" | "already-processed" | "open-sub-issues"; landKey: string }
|
|
66
66
|
| { issueRef: IssueRef; action: "failed"; error: string; landKey: string };
|
|
67
67
|
|
|
68
68
|
type ReopenIssueLifecycleResult =
|
|
@@ -208,6 +208,16 @@ function closeAuditComment(input: LandedIssueLifecycleInput): string {
|
|
|
208
208
|
return `Closed by \`${commit}\` on merge to \`main\` - reopen if release surfaces more work.`;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
function issueLabel(ref: IssueRef, parent: IssueRef): string {
|
|
212
|
+
return ref.repo === parent.repo ? `#${ref.externalKey}` : `${ref.repo}#${ref.externalKey}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function backlinkProgressComment(input: LandedIssueLifecycleInput, parent: IssueRef, openSubIssues: IssueRef[]): string {
|
|
216
|
+
const commit = input.mergedSha ? input.mergedSha.slice(0, 12) : input.workspace.branch ?? input.workspace.id;
|
|
217
|
+
const childList = openSubIssues.map((ref) => issueLabel(ref, parent)).join(", ");
|
|
218
|
+
return `Linked landed slice \`${commit}\` to this parent issue, but Agent Relay left it open because these sub-issues are still open: ${childList}.`;
|
|
219
|
+
}
|
|
220
|
+
|
|
211
221
|
async function reconcileClosedIssueRefBeforeSkip(issueRef: IssueRef, opts: IssueLifecycleOptions): Promise<{
|
|
212
222
|
issueRef: IssueRef;
|
|
213
223
|
liveStatus?: IssueRef["status"];
|
|
@@ -240,6 +250,19 @@ async function closeIssueRefForLand(
|
|
|
240
250
|
if (reconciled.liveStatus !== "open") return { issueRef: latest, action: "skipped", reason: "already-closed", landKey: key };
|
|
241
251
|
}
|
|
242
252
|
|
|
253
|
+
const openSubIssues = source === "backlink"
|
|
254
|
+
? listIssuesForEntity("epic", latest.id).map((item) => item.issueRef).filter((ref) => ref.status === "open")
|
|
255
|
+
: [];
|
|
256
|
+
if (openSubIssues.length > 0) {
|
|
257
|
+
const comment = backlinkProgressComment(input, latest, openSubIssues);
|
|
258
|
+
try {
|
|
259
|
+
await adapterFor(latest, opts).comment(latest, comment);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
logFailure(opts, `issue lifecycle epic backlink comment failed for ${latest.id} on ${key}: ${errMessage(error)}`);
|
|
262
|
+
}
|
|
263
|
+
return { issueRef: latest, action: "skipped", reason: "open-sub-issues", landKey: key };
|
|
264
|
+
}
|
|
265
|
+
|
|
243
266
|
const comment = closeAuditComment(input);
|
|
244
267
|
try {
|
|
245
268
|
await adapterFor(latest, opts).close(latest, { comment, labels: ["fixed"] });
|
|
@@ -31,7 +31,7 @@ import { emitCommandEvent } from "../command-events";
|
|
|
31
31
|
import { buildSpawnCommand, generateSpawnRequestId, resolveSpawnModelParams } from "../spawn-command";
|
|
32
32
|
import { runnerRuntimeTokenEnv } from "../runtime-tokens";
|
|
33
33
|
import { selectSpawnOrchestrator } from "../spawn-targets";
|
|
34
|
-
import { getAgentProfile } from "../config-store";
|
|
34
|
+
import { DEFAULT_SPAWN_PROFILE_NAME, getAgentProfile } from "../config-store";
|
|
35
35
|
import { composeSpawnInstructions, type ProjectInstructionPolicy } from "../project-instructions";
|
|
36
36
|
import { autoSeedProjectForCwd } from "./project";
|
|
37
37
|
import { hasComponentScope, isComponentAuthorizedFor } from "../security";
|
|
@@ -167,10 +167,11 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
|
|
|
167
167
|
const spawnRequestId = input.spawnRequestId ?? generateSpawnRequestId();
|
|
168
168
|
|
|
169
169
|
// Resolve + validate the named agent profile server-side (was HTTP-only; the MCP path passed
|
|
170
|
-
// the name through unvalidated).
|
|
171
|
-
//
|
|
172
|
-
const
|
|
173
|
-
|
|
170
|
+
// the name through unvalidated). Omitted profile uses Relay's spawn default so coordinator
|
|
171
|
+
// spawns still get the bundled Relay context/affordances. Explicit profile always wins.
|
|
172
|
+
const profileName = input.profile ?? DEFAULT_SPAWN_PROFILE_NAME;
|
|
173
|
+
const agentProfile = withResolvedProvisioning(getAgentProfile(profileName)?.value, input.provider);
|
|
174
|
+
if (!agentProfile) throw new McpNotFoundError(input.profile ? "agent profile not found" : `default agent profile ${DEFAULT_SPAWN_PROFILE_NAME} not found`);
|
|
174
175
|
|
|
175
176
|
// --- THE GATE (uniform across transports) ---------------------------------
|
|
176
177
|
// Coarse capability: spawning requires command:spawn. The MCP dispatcher already enforced
|
|
@@ -247,7 +248,7 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
|
|
|
247
248
|
// dropped this when it unified the spawn service, silently un-spawning every default-spawner
|
|
248
249
|
// (P0 regression #364). For an agent caller, agentInitiated still forces a non-spawn-capable
|
|
249
250
|
// child regardless of profile (no grandchildren).
|
|
250
|
-
profile:
|
|
251
|
+
profile: profileName,
|
|
251
252
|
...(callerId ? { agentInitiated: true, spawnedBy: callerId } : {}),
|
|
252
253
|
...(callerTeam ? { teamId: callerTeam.id } : {}),
|
|
253
254
|
...(callerProject ? { projectId: callerProject.id } : {}),
|
|
@@ -270,7 +271,7 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
|
|
|
270
271
|
...(workspaceMode ? { workspaceMode } : {}),
|
|
271
272
|
lifecycle,
|
|
272
273
|
label: input.label,
|
|
273
|
-
profile:
|
|
274
|
+
profile: profileName,
|
|
274
275
|
agentProfile,
|
|
275
276
|
tags: input.tags ?? [],
|
|
276
277
|
capabilities: input.capabilities ?? [],
|
package/src/spawn-targets.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { countLiveSpawnedAgents, getAgent, getOrchestrator, listOrchestrators, ValidationError } from "./db";
|
|
2
|
-
import { listAgentProfiles } from "./config-store";
|
|
2
|
+
import { DEFAULT_SPAWN_PROFILE_NAME, listAgentProfiles } from "./config-store";
|
|
3
3
|
import { effectiveProviderCatalogList } from "./provider-catalog-store";
|
|
4
4
|
import { isPathWithinBase } from "./utils";
|
|
5
5
|
import { McpNotFoundError } from "./mcp-errors";
|
|
@@ -39,7 +39,7 @@ export function buildSpawnTargets(input: { callerId?: string; quota: number }):
|
|
|
39
39
|
})),
|
|
40
40
|
profiles: spawnProfilesSummary(),
|
|
41
41
|
guidance:
|
|
42
|
-
|
|
42
|
+
`Default to your own host (isSelf:true); only set orchestratorId to spawn onto another host that runs a provider/model yours doesn't. Omitting profile applies ${DEFAULT_SPAWN_PROFILE_NAME}; omitting model/effort uses the provider default (marked below). Prefer a named profile over assembling raw knobs; set spawnRequestId for idempotency. After spawning you'll be pushed agent.ready / agent.exited / agent.spawn_failed — don't block-poll.`,
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -109,7 +109,7 @@ export function spawnCapablePrimer(input: { canSpawn: boolean; quota?: number })
|
|
|
109
109
|
`You can spawn long-living child agents${quotaLabel}. Shut your own down with relay_shutdown_agent.`,
|
|
110
110
|
"- BEFORE spawning, call relay_spawn_targets: it returns the LIVE matrix of hosts × providers × models × per-model effort, your remaining quota, and named profiles. Don't guess — that tool is the discovery path.",
|
|
111
111
|
"- Default to your OWN host. Only set orchestratorId to spawn onto another host when it runs a provider/model yours doesn't, or the work must run there.",
|
|
112
|
-
|
|
112
|
+
`- Prefer a named profile (profile: "<name>") over hand-assembling provider+model+effort+approvalMode. Omitting profile applies ${DEFAULT_SPAWN_PROFILE_NAME}; omitting model/effort uses the provider default. Lean to the cheapest provider/model/effort that fits; escalate only for complex work.`,
|
|
113
113
|
"- Set spawnRequestId for idempotency so a retried spawn doesn't double-create.",
|
|
114
114
|
"- Spawn is fire-and-forget: pass waitForRegistrationMs:0 and keep working — you'll be PUSHED agent.ready when the child is genuinely ready (past onboarding), or agent.spawn_failed / agent.exited otherwise. Don't block-poll.",
|
|
115
115
|
"- Spawned children cannot themselves spawn (no grandchildren).",
|