agent-relay-runner 0.40.0 → 0.42.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/src/adapter.ts +7 -2
- package/src/adapters/claude.ts +14 -3
- package/src/runner.ts +61 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.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.27"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
package/src/adapter.ts
CHANGED
|
@@ -136,7 +136,8 @@ export interface ProviderAdapter {
|
|
|
136
136
|
provider: string;
|
|
137
137
|
spawn(config: RunnerSpawnConfig): Promise<ManagedProcess>;
|
|
138
138
|
shutdown(process: ManagedProcess, opts: { graceful: boolean; timeoutMs: number }): Promise<void>;
|
|
139
|
-
compact?(process: ManagedProcess): Promise<Record<string, unknown> | void>;
|
|
139
|
+
compact?(process: ManagedProcess, opts?: { instructions?: string }): Promise<Record<string, unknown> | void>;
|
|
140
|
+
compactSupportsInstructions?: boolean;
|
|
140
141
|
clearContext?(process: ManagedProcess): Promise<Record<string, unknown> | void>;
|
|
141
142
|
// Normalize the session so far into the provider-agnostic SessionEvent stream the
|
|
142
143
|
// Insights context-ratio signal (#183/#184) reduces. Called by the runner's
|
|
@@ -147,6 +148,10 @@ export interface ProviderAdapter {
|
|
|
147
148
|
// ignore it and return their accumulated log. Return null when there is nothing to
|
|
148
149
|
// measure. Best-effort: may be omitted by providers without a session view yet.
|
|
149
150
|
collectSessionEvents?(process: ManagedProcess, ctx: { transcriptPath?: string }): Promise<SessionEvent[] | null>;
|
|
151
|
+
// Full searchable transcript/archive source for destructive boundaries. The runner
|
|
152
|
+
// slices the returned stream by cursor, so adapters should return the session-so-far
|
|
153
|
+
// view when they have one.
|
|
154
|
+
collectSessionArchiveSegment?(process: ManagedProcess, ctx: { transcriptPath?: string }): Promise<string | null>;
|
|
150
155
|
// Interrupt the in-flight turn without ending the session (ESC for Claude's
|
|
151
156
|
// tmux pane, turn/interrupt for the Codex app-server). Provider-independent at
|
|
152
157
|
// the runner boundary; each adapter does what its provider actually supports.
|
|
@@ -185,7 +190,7 @@ export function profileAllowsRelayFeature(config: RunnerSpawnConfig, feature: ke
|
|
|
185
190
|
return config.agentProfile?.relay?.[feature] !== false;
|
|
186
191
|
}
|
|
187
192
|
|
|
188
|
-
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_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_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> --stdin < response.md; 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`;
|
|
193
|
+
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> --stdin < response.md; 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`;
|
|
189
194
|
|
|
190
195
|
// #306 — deliver the FULL message body by default. Only a pathological body beyond this
|
|
191
196
|
// high cap truncates (with a get-message hint) so it can't nuke an agent's context; the 99%
|
package/src/adapters/claude.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { claudeProviderMessageText } from "./claude-delivery";
|
|
|
15
15
|
|
|
16
16
|
export class ClaudeAdapter implements ProviderAdapter {
|
|
17
17
|
readonly provider = "claude";
|
|
18
|
+
readonly compactSupportsInstructions = true;
|
|
18
19
|
// #352: initial prompt is seeded as Claude's positional launch arg (buildSpawnArgs) — reliable,
|
|
19
20
|
// no send-keys/onboarding race; tells the runner to skip the redundant post-launch delivery.
|
|
20
21
|
readonly seedsInitialPromptAtLaunch = true;
|
|
@@ -55,12 +56,13 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
55
56
|
await terminateSingleProcess(process, opts);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
async compact(process: ManagedProcess): Promise<Record<string, unknown>> {
|
|
59
|
+
async compact(process: ManagedProcess, opts?: { instructions?: string }): Promise<Record<string, unknown>> {
|
|
59
60
|
const session = process.meta?.tmuxSession as string | undefined;
|
|
60
61
|
const socket = process.meta?.tmuxSocket as string | undefined;
|
|
61
62
|
if (!session || !tmuxHasSession(session, socket)) throw new Error("no active tmux session for compact");
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
const command = opts?.instructions ? `/compact ${opts.instructions}` : "/compact";
|
|
64
|
+
await submitTextToTmux(session, command, socket);
|
|
65
|
+
return { method: "tmux-inject", command };
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
async clearContext(process: ManagedProcess): Promise<Record<string, unknown>> {
|
|
@@ -84,6 +86,15 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
84
86
|
return collectClaudeSessionEvents(jsonl);
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
async collectSessionArchiveSegment(_process: ManagedProcess, ctx: { transcriptPath?: string }): Promise<string | null> {
|
|
90
|
+
if (!ctx.transcriptPath) return null;
|
|
91
|
+
try {
|
|
92
|
+
return await readFile(ctx.transcriptPath, "utf8");
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
async interrupt(process: ManagedProcess): Promise<Record<string, unknown>> {
|
|
88
99
|
const session = process.meta?.tmuxSession as string | undefined;
|
|
89
100
|
const socket = process.meta?.tmuxSocket as string | undefined;
|
package/src/runner.ts
CHANGED
|
@@ -231,6 +231,8 @@ export class AgentRunner {
|
|
|
231
231
|
// (transcript rotated, Codex buffer trimmed) resets the cursor.
|
|
232
232
|
private insightsObserved = 0;
|
|
233
233
|
private insightsCursorKey = "";
|
|
234
|
+
private archiveObservedChars = 0;
|
|
235
|
+
private archiveCursorKey = "";
|
|
234
236
|
// Memoized repo-name project id for insight observations (resolved once; involves a
|
|
235
237
|
// git toplevel lookup for direct agents). Aggregates by repo, not per-session worktree.
|
|
236
238
|
private insightProjectName?: string;
|
|
@@ -737,7 +739,9 @@ export class AgentRunner {
|
|
|
737
739
|
else if (type === "agent.reconnect") this.publishStatus();
|
|
738
740
|
else if (type === "agent.compact") {
|
|
739
741
|
if (!this.options.adapter.compact || !this.process) throw new Error("provider does not support compact");
|
|
740
|
-
providerResult = await this.options.adapter.compact(this.process
|
|
742
|
+
providerResult = await this.options.adapter.compact(this.process, {
|
|
743
|
+
instructions: typeof params.instructions === "string" ? params.instructions : undefined,
|
|
744
|
+
});
|
|
741
745
|
} else if (type === "agent.clearContext") {
|
|
742
746
|
if (!this.options.adapter.clearContext || !this.process) throw new Error("provider does not support clearContext");
|
|
743
747
|
providerResult = await this.options.adapter.clearContext(this.process);
|
|
@@ -768,7 +772,11 @@ export class AgentRunner {
|
|
|
768
772
|
} finally {
|
|
769
773
|
this.claims.finishClaim("command", commandId);
|
|
770
774
|
if (exitAfterCommand) {
|
|
771
|
-
|
|
775
|
+
if (params.preserveRegistration === true) {
|
|
776
|
+
await this.http.setStatus(this.agentId, "offline", this.options.instanceId).catch(() => {});
|
|
777
|
+
} else {
|
|
778
|
+
await this.http.deleteAgent(this.agentId).catch(() => {});
|
|
779
|
+
}
|
|
772
780
|
if (this.options.exitProcessOnShutdown !== false) {
|
|
773
781
|
setTimeout(() => void this.stop().catch((error) => {
|
|
774
782
|
logger.error("lifecycle", `stop after command failed: ${error}`);
|
|
@@ -1301,6 +1309,13 @@ export class AgentRunner {
|
|
|
1301
1309
|
});
|
|
1302
1310
|
return;
|
|
1303
1311
|
}
|
|
1312
|
+
if (record.kind === "continuation-archive") {
|
|
1313
|
+
await this.http.recordContinuationArchive({
|
|
1314
|
+
...(record.payload as Parameters<RelayHttpClient["recordContinuationArchive"]>[0]),
|
|
1315
|
+
occurredAt: record.occurredAt,
|
|
1316
|
+
});
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1304
1319
|
if (record.kind === "mcp-tool-call") {
|
|
1305
1320
|
await this.deliverBufferedMcpCall(record);
|
|
1306
1321
|
return;
|
|
@@ -1434,11 +1449,14 @@ export class AgentRunner {
|
|
|
1434
1449
|
this.publishFinalizing(reason);
|
|
1435
1450
|
try {
|
|
1436
1451
|
await Promise.race([
|
|
1437
|
-
|
|
1452
|
+
Promise.all([
|
|
1453
|
+
this.captureContextRatio(reason, opts),
|
|
1454
|
+
this.captureContinuationArchive(reason, opts),
|
|
1455
|
+
]).then(() => undefined),
|
|
1438
1456
|
new Promise<void>((resolve) => setTimeout(resolve, PRE_DESTROY_TIMEOUT_MS)),
|
|
1439
1457
|
]);
|
|
1440
1458
|
} catch (error) {
|
|
1441
|
-
this.sessionLog(`
|
|
1459
|
+
this.sessionLog(`pre-destroy capture failed: ${errMessage(error)}`);
|
|
1442
1460
|
}
|
|
1443
1461
|
// For exit-bound transitions the runner won't be alive afterward to drain the durable
|
|
1444
1462
|
// outbox, so block (bounded) on delivering what capture just enqueued. This runs before
|
|
@@ -1478,6 +1496,30 @@ export class AgentRunner {
|
|
|
1478
1496
|
return (this.insightProjectName ??= resolveProjectName(this.options.cwd, this.options.workspace));
|
|
1479
1497
|
}
|
|
1480
1498
|
|
|
1499
|
+
private async captureContinuationArchive(reason: SessionDestroyReason, opts?: { transcriptPath?: string }): Promise<void> {
|
|
1500
|
+
const adapter = this.options.adapter;
|
|
1501
|
+
if (!adapter.collectSessionArchiveSegment || !this.process) return;
|
|
1502
|
+
const transcriptPath = opts?.transcriptPath ?? this.lastTranscriptPath;
|
|
1503
|
+
const archive = await adapter.collectSessionArchiveSegment(this.process, { transcriptPath });
|
|
1504
|
+
if (!archive) return;
|
|
1505
|
+
const key = transcriptPath ?? `session:${this.providerSessionId}`;
|
|
1506
|
+
if (key !== this.archiveCursorKey || archive.length < this.archiveObservedChars) {
|
|
1507
|
+
this.archiveCursorKey = key;
|
|
1508
|
+
this.archiveObservedChars = 0;
|
|
1509
|
+
}
|
|
1510
|
+
const segment = archive.slice(this.archiveObservedChars).trim();
|
|
1511
|
+
this.archiveObservedChars = archive.length;
|
|
1512
|
+
if (!segment) return;
|
|
1513
|
+
this.outbox.enqueue({
|
|
1514
|
+
kind: "continuation-archive",
|
|
1515
|
+
payload: {
|
|
1516
|
+
agentId: this.agentId,
|
|
1517
|
+
segment,
|
|
1518
|
+
},
|
|
1519
|
+
});
|
|
1520
|
+
this.sessionLog(`continuation archive queued (${segment.length} chars, ${reason})`);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1481
1523
|
private async captureContextRatio(reason: SessionDestroyReason, opts?: { transcriptPath?: string }): Promise<void> {
|
|
1482
1524
|
const adapter = this.options.adapter;
|
|
1483
1525
|
if (!adapter.collectSessionEvents || !this.process) return;
|
|
@@ -2066,7 +2108,7 @@ export class AgentRunner {
|
|
|
2066
2108
|
const meta: Record<string, unknown> = {
|
|
2067
2109
|
providerCapabilities: runtimeProviderCapabilities(
|
|
2068
2110
|
this.options,
|
|
2069
|
-
context
|
|
2111
|
+
context,
|
|
2070
2112
|
probeModel,
|
|
2071
2113
|
),
|
|
2072
2114
|
...(terminalSession ? { tmuxSession: terminalSession } : {}),
|
|
@@ -2319,7 +2361,7 @@ interface ProbeModelInfo {
|
|
|
2319
2361
|
effort?: string;
|
|
2320
2362
|
}
|
|
2321
2363
|
|
|
2322
|
-
function runtimeProviderCapabilities(options: RunnerOptions,
|
|
2364
|
+
function runtimeProviderCapabilities(options: RunnerOptions, contextState?: ContextState, probeModel?: ProbeModelInfo): ProviderCapabilities {
|
|
2323
2365
|
const model = options.model ?? probeModel?.model;
|
|
2324
2366
|
const effort = options.effort ?? probeModel?.effort;
|
|
2325
2367
|
const modelSource = options.model ? "runtime" as const : probeModel?.model ? "provider" as const : "runtime" as const;
|
|
@@ -2349,7 +2391,7 @@ function runtimeProviderCapabilities(options: RunnerOptions, contextStats?: { so
|
|
|
2349
2391
|
confidence: "reported",
|
|
2350
2392
|
lastUpdatedAt: options.startedAt,
|
|
2351
2393
|
},
|
|
2352
|
-
...runtimeProviderContextCapabilities(options,
|
|
2394
|
+
...runtimeProviderContextCapabilities(options, contextState),
|
|
2353
2395
|
...runtimeProviderTerminalCapabilities(options),
|
|
2354
2396
|
liveSession: {
|
|
2355
2397
|
capture: true,
|
|
@@ -2418,14 +2460,22 @@ function appliedAgentProfileMetadata(provider: string, profile: AgentProfile): R
|
|
|
2418
2460
|
};
|
|
2419
2461
|
}
|
|
2420
2462
|
|
|
2421
|
-
function runtimeProviderContextCapabilities(options: RunnerOptions,
|
|
2422
|
-
const context: NonNullable<ProviderCapabilities["context"]> = {};
|
|
2423
|
-
|
|
2424
|
-
if (
|
|
2463
|
+
function runtimeProviderContextCapabilities(options: RunnerOptions, contextState?: ContextState): Pick<ProviderCapabilities, "context"> {
|
|
2464
|
+
const context: NonNullable<ProviderCapabilities["context"]> = { resume: "none" };
|
|
2465
|
+
const supportsManagedContext = options.provider === "codex" || (options.provider === "claude" && options.headless);
|
|
2466
|
+
if (contextState) {
|
|
2467
|
+
context.stats = { source: contextState.source, confidence: contextState.confidence };
|
|
2468
|
+
if (typeof contextState.tokensMax === "number" && Number.isFinite(contextState.tokensMax)) {
|
|
2469
|
+
context.windowTokens = contextState.tokensMax;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
if (supportsManagedContext) {
|
|
2425
2473
|
context.compact = true;
|
|
2426
2474
|
context.clear = true;
|
|
2427
2475
|
}
|
|
2428
2476
|
context.inject = true;
|
|
2477
|
+
if (supportsManagedContext && options.adapter.compact && options.adapter.compactSupportsInstructions) context.resume = "native";
|
|
2478
|
+
else if (supportsManagedContext && options.adapter.clearContext) context.resume = "clear-inject";
|
|
2429
2479
|
return Object.keys(context).length ? { context } : {};
|
|
2430
2480
|
}
|
|
2431
2481
|
|