agent-relay-runner 0.85.0 → 0.87.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/package.json +2 -2
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/reply/SKILL.md +2 -2
- package/plugins/codex/skills/reply/SKILL.md +2 -2
- package/src/adapter.ts +4 -3
- package/src/adapters/claude-delivery.ts +5 -2
- package/src/control-server.ts +2 -1
- package/src/relay-instructions.ts +9 -1
- package/src/runner-core.ts +4 -35
- package/src/runner-insights.ts +1 -2
- package/src/session-destroy.ts +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.87.0",
|
|
4
4
|
"description": "Unified provider lifecycle runner for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"directory": "runner"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"agent-relay-sdk": "0.2.
|
|
23
|
+
"agent-relay-sdk": "0.2.66"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
|
@@ -20,11 +20,11 @@ Examples:
|
|
|
20
20
|
```bash
|
|
21
21
|
agent-relay /reply 206 "Sounds good, I'll take a look"
|
|
22
22
|
agent-relay /reply 42 "Done — the fix is in commit abc123"
|
|
23
|
-
agent-relay /reply 42 --
|
|
23
|
+
agent-relay /reply 42 --body-file .agent-relay/sessions/$AGENT_RELAY_ID/tmp/reply.md
|
|
24
24
|
agent-relay /reply 42 --body-file response.md
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Use `--
|
|
27
|
+
Use `--body-file` for long replies so shell quoting does not mangle the body. Oversized replies are automatically uploaded as an Agent Relay artifact and sent as an attached concise reply.
|
|
28
28
|
|
|
29
29
|
The relay creates this per-session scratch dir for you (it is git-ignored locally), so staging there never pollutes the working tree and never collides with other agents. No cleanup needed — the dir is reaped when your session ends.
|
|
30
30
|
|
|
@@ -20,11 +20,11 @@ Examples:
|
|
|
20
20
|
```bash
|
|
21
21
|
agent-relay /reply 206 "Sounds good, I'll take a look"
|
|
22
22
|
agent-relay /reply 42 "Done — the fix is in commit abc123"
|
|
23
|
-
agent-relay /reply 42 --
|
|
23
|
+
agent-relay /reply 42 --body-file .agent-relay/sessions/$AGENT_RELAY_ID/tmp/reply.md
|
|
24
24
|
agent-relay /reply 42 --body-file response.md
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Use `--
|
|
27
|
+
Use `--body-file` for long replies so shell quoting does not mangle the body. Oversized replies are automatically uploaded as an Agent Relay artifact and sent as an attached concise reply.
|
|
28
28
|
|
|
29
29
|
The relay creates this per-session scratch dir for you (it is git-ignored locally), so staging there never pollutes the working tree and never collides with other agents. No cleanup needed — the dir is reaped when your session ends.
|
|
30
30
|
|
package/src/adapter.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { AgentProfile, Message } from "agent-relay-sdk";
|
|
|
2
2
|
import { isRecord } from "agent-relay-sdk";
|
|
3
3
|
import type { SessionEvent } from "./session-insights";
|
|
4
4
|
import { messageBodyMaxCharsFromEnv } from "./config";
|
|
5
|
+
import { relayReplyReminderText } from "./relay-instructions";
|
|
5
6
|
|
|
6
7
|
export type SemanticStatus = "idle" | "busy" | "offline" | "error";
|
|
7
8
|
type ProviderWorkKind = "provider-turn" | "subagent" | "background-script";
|
|
@@ -204,7 +205,7 @@ export function profileIsVanillaBase(config: Pick<RunnerSpawnConfig, "agentProfi
|
|
|
204
205
|
return config.agentProfile?.base === "vanilla";
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
export const RELAY_CONTEXT = `[agent-relay] You are connected to Agent Relay, a real-time message bus between agents and users. When you receive a relay message: read it, do what it asks, and reply through the relay when a text response is needed. Use agent-relay /react <messageId> <emoji> for lightweight acknowledgement or approval. If Relay MCP tools are available, prefer relay_reply, relay_get_message, relay_get_thread, relay_send_message, relay_upload_artifact, relay_attach_artifact, relay_agent_status, relay_find_agents, relay_compact_and_resume, relay_recall, relay_spawn_agent, and relay_shutdown_agent. You never need to know or pass your own agent id — relay fills it from your token; use relay_whoami only if you need to reason about yourself. relay_compact_and_resume is for clean-seam self-resume after a context advisory: pass workingState and optional ruledOut; Relay owns the objective envelope. relay_recall searches your own archived pre-compaction segments by keyword when a discarded detail is needed. relay_spawn_targets / relay_spawn_agent / relay_shutdown_agent only appear if your profile grants spawning (a live-children quota); when present, call relay_spawn_targets FIRST for the live host/provider/model matrix + your quota, then stand up long-living child agents and shut down your own — find them later with relay_find_agents spawnedBy:me. CLI fallback: agent-relay /reply <messageId>
|
|
208
|
+
export const RELAY_CONTEXT = `[agent-relay] You are connected to Agent Relay, a real-time message bus between agents and users. When you receive a relay message: read it, do what it asks, and reply through the relay when a text response is needed. Use agent-relay /react <messageId> <emoji> for lightweight acknowledgement or approval. If Relay MCP tools are available, prefer relay_reply, relay_get_message, relay_get_thread, relay_send_message, relay_upload_artifact, relay_attach_artifact, relay_agent_status, relay_find_agents, relay_compact_and_resume, relay_recall, relay_spawn_agent, and relay_shutdown_agent. You never need to know or pass your own agent id — relay fills it from your token; use relay_whoami only if you need to reason about yourself. relay_compact_and_resume is for clean-seam self-resume after a context advisory: pass workingState and optional ruledOut; Relay owns the objective envelope. relay_recall searches your own archived pre-compaction segments by keyword when a discarded detail is needed. relay_spawn_targets / relay_spawn_agent / relay_shutdown_agent only appear if your profile grants spawning (a live-children quota); when present, call relay_spawn_targets FIRST for the live host/provider/model matrix + your quota, then stand up long-living child agents and shut down your own — find them later with relay_find_agents spawnedBy:me. For replies, prefer relay_reply or relay_send_message; CLI fallback: agent-relay /reply <messageId> "<your reply>". If a delivered message says it was truncated, fetch the full body with: agent-relay get-message <messageId>. For command details, run: agent-relay /guide`;
|
|
208
209
|
|
|
209
210
|
// #306 — deliver the FULL message body by default. Only a pathological body beyond this
|
|
210
211
|
// high cap truncates (with a get-message hint) so it can't nuke an agent's context; the 99%
|
|
@@ -353,7 +354,7 @@ export function providerMessageText(messages: Message[]): string {
|
|
|
353
354
|
`[truncated: showing first ${maxChars} of ${message.body.length} chars]`,
|
|
354
355
|
`Read full: agent-relay get-message ${message.id}`,
|
|
355
356
|
`Body only: agent-relay get-message ${message.id} --body`,
|
|
356
|
-
`Long reply: agent-relay /reply ${message.id} --
|
|
357
|
+
`Long reply CLI fallback: agent-relay /reply ${message.id} --body-file response.md`,
|
|
357
358
|
].join("\n")
|
|
358
359
|
: undefined;
|
|
359
360
|
if (isMemoryContext) {
|
|
@@ -375,7 +376,7 @@ export function providerMessageText(messages: Message[]): string {
|
|
|
375
376
|
if (replyable) {
|
|
376
377
|
sections.push([
|
|
377
378
|
"[agent-relay reply reminder]",
|
|
378
|
-
|
|
379
|
+
relayReplyReminderText(replyable.id),
|
|
379
380
|
"If you already delivered the useful response through Relay, do not send a separate status-only confirmation.",
|
|
380
381
|
"If multiple messages arrived together, cover them in one reply instead of answering each line separately.",
|
|
381
382
|
].join("\n"));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Message } from "agent-relay-sdk";
|
|
2
2
|
import { isRecord } from "agent-relay-sdk";
|
|
3
3
|
import { isNotificationMessage, NOTIFICATION_NUDGE, providerAttachmentText, providerMessageBodyMaxChars } from "../adapter";
|
|
4
|
+
import { relayReplyReminderText } from "../relay-instructions";
|
|
4
5
|
|
|
5
6
|
const REMINDER_EVERY_DELIVERIES = 5;
|
|
6
7
|
|
|
@@ -112,10 +113,12 @@ function formatMessage(message: Message, relaySurface: boolean): string {
|
|
|
112
113
|
function replyReminder(message: Message, readOnly: boolean): string {
|
|
113
114
|
const command = readOnly
|
|
114
115
|
? `agent-relay /reply ${message.id} "<your reply>"`
|
|
115
|
-
:
|
|
116
|
+
: relayReplyReminderText(message.id);
|
|
116
117
|
return [
|
|
117
118
|
"[agent-relay reply reminder]",
|
|
118
|
-
|
|
119
|
+
readOnly
|
|
120
|
+
? `If this batch needs a response, send one useful reply to the latest relevant message: ${command}`
|
|
121
|
+
: command,
|
|
119
122
|
"If you already delivered the useful response through Relay, do not send a separate status-only confirmation.",
|
|
120
123
|
"If multiple messages arrived together, cover them in one reply instead of answering each line separately.",
|
|
121
124
|
].join("\n");
|
package/src/control-server.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Message, ReplyObligation } from "agent-relay-sdk";
|
|
|
3
3
|
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
4
4
|
import type { ProviderPermissionDecisionInput, ProviderStatusEvent, SemanticStatus, TerminalAttachSpec } from "./adapter";
|
|
5
5
|
import { obligationRequiresExplicitReply } from "./reply-obligation-cache";
|
|
6
|
+
import { relayReplyActionText } from "./relay-instructions";
|
|
6
7
|
import { logger, parseLogLevel, LOG_LEVELS } from "./logger";
|
|
7
8
|
import { buildRateLimitProviderState } from "./rate-limit";
|
|
8
9
|
|
|
@@ -197,7 +198,7 @@ function replyObligationSummary(obligations: ReplyObligation[]): Record<string,
|
|
|
197
198
|
pending: true,
|
|
198
199
|
count: obligations.length,
|
|
199
200
|
obligation,
|
|
200
|
-
reason: `Pending Agent Relay response for message #${obligation.messageId} from ${obligation.from}${subject}. If it still needs an answer,
|
|
201
|
+
reason: `Pending Agent Relay response for message #${obligation.messageId} from ${obligation.from}${subject}. If it still needs an answer, use ${relayReplyActionText(obligation.messageId)}. If multiple delivered messages are part of one current request, answer once to the latest relevant message. If the useful response was already delivered through Relay, do not send a separate status-only confirmation.`,
|
|
201
202
|
};
|
|
202
203
|
}
|
|
203
204
|
|
|
@@ -3,7 +3,7 @@ export const CLAUDE_RELAY_MANUAL = `# Agent Relay
|
|
|
3
3
|
- Agent Relay messages may come from humans, channels, or other agents.
|
|
4
4
|
- Read the delivered message body and do the requested work.
|
|
5
5
|
- Reply through Relay only when the sender still needs an answer.
|
|
6
|
-
- Prefer
|
|
6
|
+
- Prefer the inline MCP tools \`relay_reply\` or \`relay_send_message\` for the actual answer to an incoming Relay message.
|
|
7
7
|
- Do not send \`/message\` with the useful answer and then a separate \`/reply\` that only says it was sent.
|
|
8
8
|
- If multiple Relay messages arrive together, answer once to the latest relevant message and cover the current request. Do not separately acknowledge stale greetings or context.
|
|
9
9
|
- If the useful response was already delivered through Relay, do not send an extra "sent", "done", or "drafts sent" confirmation unless the user explicitly asked for one.
|
|
@@ -25,6 +25,14 @@ This Claude session is running with restricted read-only Relay permissions. Do n
|
|
|
25
25
|
|
|
26
26
|
agent-relay /reply <messageId> "<your reply>"`;
|
|
27
27
|
|
|
28
|
+
export function relayReplyActionText(messageId: number): string {
|
|
29
|
+
return `relay_reply (messageId: ${messageId}, body: "<your reply>"). CLI fallback: agent-relay /reply ${messageId} "<your reply>"`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function relayReplyReminderText(messageId: number): string {
|
|
33
|
+
return `If this batch needs a response, send one useful reply to the latest relevant message with ${relayReplyActionText(messageId)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
/**
|
|
29
37
|
* Provider-agnostic caveat injected into every spawned agent that runs in an
|
|
30
38
|
* isolated workspace, regardless of which project it is working in. Isolated
|
package/src/runner-core.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { publishCapturedResponse } from "./response-capture-report";
|
|
|
28
28
|
import { runnerLaunchInjectionSessionEvents, type RunnerRelayInjectionEvent } from "./relay-injection-events";
|
|
29
29
|
import { providerTerminalSession, providerTerminalSocket } from "./process-meta";
|
|
30
30
|
import { capsFromEnv, logLevelFromEnv, mcpProxyEnabledFromEnv, orchestratorBaseDirFromEnv, orchestratorUrlFromEnv, registrationTimeoutMsFromEnv, runnerInfoFileFromEnv, runnerLogFileFromEnv, runnerOutboxDirWithInfoFallback, sessionDebugEnabled, tagsFromEnv, workspaceModeFromEnv } from "./config";
|
|
31
|
+
import { boundaryReasonForCommand, PRE_DESTROY_TIMEOUT_MS, reasonExitsRunner, type LifecycleAction, type SessionDestroyReason } from "./session-destroy";
|
|
31
32
|
import {
|
|
32
33
|
appliedAgentProfileMetadata,
|
|
33
34
|
commandTimeoutMs,
|
|
@@ -55,48 +56,13 @@ import {
|
|
|
55
56
|
type ProbeModelInfo,
|
|
56
57
|
} from "./runner-helpers";
|
|
57
58
|
|
|
58
|
-
// A destructive session transition. The runner runs end-of-session work (Insights
|
|
59
|
-
// capture, #183/#184) before the invasive operation and, during that window, presents a
|
|
60
|
-
// distinct non-addressable lifecycle state. Bus commands and provider hooks (Claude
|
|
61
|
-
// PreCompact / SessionEnd) both normalize to one of these.
|
|
62
|
-
type SessionDestroyReason = "compact" | "clear" | "restart" | "shutdown" | "kill" | "crash";
|
|
63
|
-
|
|
64
|
-
// Reasons after which the runner process won't survive to drain the durable outbox (and the
|
|
65
|
-
// per-agent outbox is never reopened once the agent is gone). For these, pre-destroy must
|
|
66
|
-
// block on delivery of the just-captured Insights datapoint, not just enqueue it (#183).
|
|
67
|
-
// `restart` (bus command) deliberately excluded: the runner stays alive and drains normally.
|
|
68
|
-
function reasonExitsRunner(reason: SessionDestroyReason): boolean {
|
|
69
|
-
return reason === "shutdown" || reason === "kill" || reason === "crash";
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// `finalizing-<reason>` is the transient pre-destroy window; the others are the executing
|
|
73
|
-
// teardown states the dashboard already renders.
|
|
74
|
-
type LifecycleAction =
|
|
75
|
-
| "shutting-down" | "killing" | "restarting"
|
|
76
|
-
| `finalizing-${SessionDestroyReason}`;
|
|
77
|
-
|
|
78
59
|
// Pre-destroy work is best-effort and must never hang teardown. Capping it keeps a slow
|
|
79
60
|
// transcript read or a wedged provider from stalling a shutdown the operator asked for.
|
|
80
|
-
const PRE_DESTROY_TIMEOUT_MS = 4_000;
|
|
81
|
-
|
|
82
61
|
// Bounded window to deliver the durable outbox before an exit-bound teardown (#183). Kept
|
|
83
62
|
// short so a wedged/down server can't stall an operator-requested shutdown for long; a
|
|
84
63
|
// row that still can't land is logged, not silently dropped.
|
|
85
64
|
const OUTBOX_FLUSH_TIMEOUT_MS = 3_000;
|
|
86
65
|
|
|
87
|
-
// Map a lifecycle bus command to its destructive boundary reason, or undefined for
|
|
88
|
-
// non-destructive commands (interrupt, inject, reconnect, permission decisions).
|
|
89
|
-
function boundaryReasonForCommand(type: string): SessionDestroyReason | undefined {
|
|
90
|
-
switch (type) {
|
|
91
|
-
case "agent.compact": return "compact";
|
|
92
|
-
case "agent.clearContext": return "clear";
|
|
93
|
-
case "agent.restart": return "restart";
|
|
94
|
-
case "agent.shutdown": return "shutdown";
|
|
95
|
-
case "agent.kill": return "kill";
|
|
96
|
-
default: return undefined;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
66
|
interface RunnerOptions {
|
|
101
67
|
provider: string;
|
|
102
68
|
model?: string;
|
|
@@ -258,6 +224,7 @@ export class AgentRunner {
|
|
|
258
224
|
private readonly pendingMessages = new Map<number, Message>();
|
|
259
225
|
private readonly activeTaskClaims = new Map<number, ActiveTaskClaim>();
|
|
260
226
|
private pendingTimelineEvent?: RunnerTimelineEvent;
|
|
227
|
+
private lastProviderEventAt?: number;
|
|
261
228
|
private pendingPromptMessageId?: number;
|
|
262
229
|
// #329: a spawn-time initial prompt whose first delivery timed out before the TUI was
|
|
263
230
|
// ready. Held (not dropped) for a ready-signal-driven re-attempt; cleared on success.
|
|
@@ -1167,6 +1134,7 @@ export class AgentRunner {
|
|
|
1167
1134
|
|
|
1168
1135
|
private setProviderStatus(update: ProviderStatusUpdate): void {
|
|
1169
1136
|
const status = typeof update === "string" ? update : update.status;
|
|
1137
|
+
this.lastProviderEventAt = Date.now();
|
|
1170
1138
|
const providerSessionId = typeof update === "string" ? undefined : update.providerSessionId;
|
|
1171
1139
|
if (providerSessionId && providerSessionId !== this.providerSessionId) return;
|
|
1172
1140
|
const reason = typeof update === "string" ? "provider-turn" : update.reason ?? "provider-turn";
|
|
@@ -1856,6 +1824,7 @@ export class AgentRunner {
|
|
|
1856
1824
|
activeWork,
|
|
1857
1825
|
activeSubagents,
|
|
1858
1826
|
activeSubagentCount: activeSubagents.length,
|
|
1827
|
+
...(this.lastProviderEventAt !== undefined ? { lastProviderEventAt: this.lastProviderEventAt } : {}),
|
|
1859
1828
|
providerState,
|
|
1860
1829
|
auth: {
|
|
1861
1830
|
profileId: this.currentTokenProfileId ?? null,
|
package/src/runner-insights.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import type { WorkspaceMetadata } from "agent-relay-sdk";
|
|
2
2
|
import type { ManagedProcess, ProviderAdapter } from "./adapter";
|
|
3
3
|
import type { Outbox } from "./outbox";
|
|
4
|
+
import type { SessionDestroyReason } from "./session-destroy";
|
|
4
5
|
import { resolveProjectName } from "./config";
|
|
5
6
|
import { computeContextRatio } from "./session-insights";
|
|
6
7
|
import { boundContinuationArchiveSegment } from "./continuation-archive";
|
|
7
8
|
|
|
8
|
-
type SessionDestroyReason = "compact" | "clear" | "restart" | "shutdown" | "kill" | "crash";
|
|
9
|
-
|
|
10
9
|
interface RunnerInsightsDeps {
|
|
11
10
|
agentId: string;
|
|
12
11
|
cwd: string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type SessionDestroyReason = "compact" | "clear" | "restart" | "shutdown" | "kill" | "crash";
|
|
2
|
+
|
|
3
|
+
export type LifecycleAction =
|
|
4
|
+
| "shutting-down" | "killing" | "restarting"
|
|
5
|
+
| `finalizing-${SessionDestroyReason}`;
|
|
6
|
+
|
|
7
|
+
export const PRE_DESTROY_TIMEOUT_MS = 4_000;
|
|
8
|
+
|
|
9
|
+
export function reasonExitsRunner(reason: SessionDestroyReason): boolean {
|
|
10
|
+
return reason === "shutdown" || reason === "kill" || reason === "crash";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function boundaryReasonForCommand(type: string): SessionDestroyReason | undefined {
|
|
14
|
+
switch (type) {
|
|
15
|
+
case "agent.compact": return "compact";
|
|
16
|
+
case "agent.clearContext": return "clear";
|
|
17
|
+
case "agent.restart": return "restart";
|
|
18
|
+
case "agent.shutdown": return "shutdown";
|
|
19
|
+
case "agent.kill": return "kill";
|
|
20
|
+
default: return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|