agent-relay-server 0.40.0 → 0.41.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.
@@ -0,0 +1,232 @@
1
+ import { isRecord } from "agent-relay-sdk";
2
+ import { createCommand, listCommands } from "./commands-db";
3
+ import { emitCommandEvent } from "./command-events";
4
+ import { getSelfResumeConfig } from "./config-store";
5
+ import {
6
+ archiveContinuationSegment,
7
+ ensureContinuationEnvelope,
8
+ getAgent,
9
+ getContinuationEnvelope,
10
+ updateContinuationAfterCompact,
11
+ ValidationError,
12
+ } from "./db";
13
+ import { emitRelayEvent } from "./events";
14
+ import { notifySystemMessage } from "./notify";
15
+ import type { AgentCard, Command, ContinuationEnvelope, ContinuationLedgerEntry, SelfResumeMode } from "./types";
16
+
17
+ export interface CompactAndResumeInput {
18
+ agentId: string;
19
+ workingState: string;
20
+ ruledOut?: unknown;
21
+ mode?: SelfResumeMode;
22
+ objective?: string;
23
+ }
24
+
25
+ export interface CompactAndResumeResult {
26
+ ok: boolean;
27
+ mode: SelfResumeMode;
28
+ envelope: ContinuationEnvelope;
29
+ continuation?: string;
30
+ command: Command;
31
+ exhausted?: boolean;
32
+ reason?: string;
33
+ }
34
+
35
+ const VALID_RESUME_MODES = ["clear-inject", "native"] as const;
36
+
37
+ export function compactAndResume(input: CompactAndResumeInput): CompactAndResumeResult {
38
+ const agent = getAgent(input.agentId);
39
+ if (!agent) throw new ValidationError("agent not found");
40
+ const config = getSelfResumeConfig();
41
+ const mode = input.mode ?? config.defaultMode;
42
+ if (!VALID_RESUME_MODES.includes(mode)) throw new ValidationError(`mode must be one of: ${VALID_RESUME_MODES.join(", ")}`);
43
+ assertResumeSupported(agent, mode);
44
+
45
+ const now = Date.now();
46
+ const envelope = ensureContinuationEnvelope({
47
+ agentId: agent.id,
48
+ objective: input.objective,
49
+ config,
50
+ now,
51
+ });
52
+ const exhausted = budgetExhaustion(envelope, now);
53
+ if (exhausted) {
54
+ const command = createCleanStop(agent, exhausted);
55
+ notifySelfResumeBudgetExhausted(agent, envelope, exhausted);
56
+ return { ok: false, mode, envelope, command, exhausted: true, reason: exhausted };
57
+ }
58
+
59
+ const workingState = input.workingState.trim();
60
+ if (!workingState) throw new ValidationError("workingState required");
61
+ const generation = envelope.generation + 1;
62
+ const ledgerEntries = parseRuledOut(input.ruledOut, generation, now);
63
+ const archive = archiveContinuationSegment({
64
+ agentId: agent.id,
65
+ generation,
66
+ segment: workingState,
67
+ now,
68
+ });
69
+ const updated = updateContinuationAfterCompact({
70
+ agentId: agent.id,
71
+ generation,
72
+ ledgerEntries,
73
+ workingState,
74
+ archiveRefs: [archive.ref],
75
+ spentTokens: typeof agent.context?.tokensUsed === "number" ? agent.context.tokensUsed : 0,
76
+ now,
77
+ });
78
+ const continuation = renderContinuation(updated);
79
+ const command = mode === "native"
80
+ ? createNativeCompact(agent.id, generation, continuation)
81
+ : createClearThenInject(agent.id, generation, continuation);
82
+ emitCommandEvent(command, "command.requested");
83
+ return { ok: true, mode, envelope: updated, continuation, command };
84
+ }
85
+
86
+ export function enqueueContinuationInjectionAfterClear(command: Command): Command | null {
87
+ if (command.type !== "agent.clearContext" || command.status !== "succeeded") return null;
88
+ const resume = isRecord(command.params.selfResume) ? command.params.selfResume : undefined;
89
+ const continuation = typeof resume?.continuation === "string" ? resume.continuation : undefined;
90
+ if (!continuation) return null;
91
+ const generation = typeof resume?.generation === "number" ? resume.generation : undefined;
92
+ const existing = listCommands({ target: command.target, type: "agent.injectContext", limit: 500 })
93
+ .find((item) => item.correlationId === command.id);
94
+ if (existing) return null;
95
+ const inject = createCommand({
96
+ type: "agent.injectContext",
97
+ source: "system",
98
+ target: command.target,
99
+ correlationId: command.id,
100
+ params: {
101
+ reason: "self-resume-continuation",
102
+ content: continuation,
103
+ selfResume: {
104
+ generation,
105
+ afterClearCommandId: command.id,
106
+ },
107
+ },
108
+ });
109
+ emitCommandEvent(inject, "command.requested");
110
+ return inject;
111
+ }
112
+
113
+ export function renderContinuation(envelope: ContinuationEnvelope): string {
114
+ const ledger = envelope.ledger.length
115
+ ? envelope.ledger.map((entry) => `- gen ${entry.gen} @ ${new Date(entry.at).toISOString()}: ${entry.ruledOut}${entry.why ? `; why: ${entry.why}` : ""}`).join("\n")
116
+ : "- none";
117
+ return [
118
+ `You have been resumed by Agent Relay — generation ${envelope.generation}.`,
119
+ "",
120
+ "Objective (verbatim, relay-owned):",
121
+ envelope.objective,
122
+ "",
123
+ "Ruled-out ledger (append-only):",
124
+ ledger,
125
+ "",
126
+ "Working state:",
127
+ envelope.workingState,
128
+ "",
129
+ "Archive refs:",
130
+ envelope.archiveRefs.length ? envelope.archiveRefs.join("\n") : "- none",
131
+ ].join("\n");
132
+ }
133
+
134
+ function assertResumeSupported(agent: AgentCard, mode: SelfResumeMode): void {
135
+ const context = agent.providerCapabilities?.context;
136
+ const resume = context?.resume ?? (context?.clear && context?.inject ? "clear-inject" : "none");
137
+ if (resume === "none") throw new ValidationError("self-resume unsupported, fresh-session handoff only");
138
+ if (mode === "native" && resume !== "native") throw new ValidationError("native self-resume unsupported for this provider");
139
+ if (mode === "clear-inject" && !(context?.clear && context?.inject)) {
140
+ throw new ValidationError("clear-inject self-resume unsupported for this provider");
141
+ }
142
+ }
143
+
144
+ function budgetExhaustion(envelope: ContinuationEnvelope, now: number): string | null {
145
+ if (envelope.generation >= envelope.budget.maxGenerations) return "max generations exhausted";
146
+ if (Math.max(0, now - envelope.createdAt) >= envelope.budget.maxWallClockMs) return "max wall clock exhausted";
147
+ if (envelope.budget.spent.tokens >= envelope.budget.maxTokens) return "max token budget exhausted";
148
+ return null;
149
+ }
150
+
151
+ function createNativeCompact(agentId: string, generation: number, continuation: string): Command {
152
+ return createCommand({
153
+ type: "agent.compact",
154
+ source: "system",
155
+ target: agentId,
156
+ params: {
157
+ reason: "self-resume",
158
+ instructions: continuation,
159
+ selfResume: { generation, mode: "native" },
160
+ },
161
+ });
162
+ }
163
+
164
+ function createClearThenInject(agentId: string, generation: number, continuation: string): Command {
165
+ return createCommand({
166
+ type: "agent.clearContext",
167
+ source: "system",
168
+ target: agentId,
169
+ params: {
170
+ reason: "self-resume",
171
+ selfResume: { generation, mode: "clear-inject", continuation },
172
+ },
173
+ });
174
+ }
175
+
176
+ function createCleanStop(agent: AgentCard, reason: string): Command {
177
+ const command = createCommand({
178
+ type: "agent.shutdown",
179
+ source: "system",
180
+ target: agent.id,
181
+ params: {
182
+ reason: `self-resume budget exhausted: ${reason}`,
183
+ graceful: true,
184
+ timeoutMs: 10_000,
185
+ preserveRegistration: true,
186
+ selfResume: { exhausted: true },
187
+ },
188
+ });
189
+ emitCommandEvent(command, "command.requested");
190
+ return command;
191
+ }
192
+
193
+ function notifySelfResumeBudgetExhausted(agent: AgentCard, envelope: ContinuationEnvelope, reason: string): void {
194
+ const payload = {
195
+ kind: "agent.resume_budget_exhausted",
196
+ agentId: agent.id,
197
+ parent: agent.spawnedBy,
198
+ reason,
199
+ generation: envelope.generation,
200
+ };
201
+ emitRelayEvent({ type: "agent.resume_budget_exhausted", source: "server", subject: agent.id, data: payload });
202
+ if (!agent.spawnedBy) return;
203
+ notifySystemMessage(agent.spawnedBy, {
204
+ subject: "Self-resume budget exhausted",
205
+ body: `Spawned agent \`${agent.id}\` stopped cleanly: ${reason}.`,
206
+ payload,
207
+ replyExpected: false,
208
+ });
209
+ }
210
+
211
+ function parseRuledOut(value: unknown, generation: number, now: number): ContinuationLedgerEntry[] {
212
+ if (value === undefined || value === null) return [];
213
+ if (!Array.isArray(value)) throw new ValidationError("ruledOut must be an array");
214
+ return value.map((item, index) => {
215
+ if (typeof item === "string") return ledgerEntry(item, "", generation, now);
216
+ if (!isRecord(item)) throw new ValidationError(`ruledOut[${index}] must be a string or object`);
217
+ const ruledOut = typeof item.ruledOut === "string" ? item.ruledOut.trim() : "";
218
+ const why = typeof item.why === "string" ? item.why.trim() : "";
219
+ if (!ruledOut) throw new ValidationError(`ruledOut[${index}].ruledOut required`);
220
+ return ledgerEntry(ruledOut, why, generation, now);
221
+ });
222
+ }
223
+
224
+ function ledgerEntry(ruledOut: string, why: string, generation: number, now: number): ContinuationLedgerEntry {
225
+ const cleaned = ruledOut.trim();
226
+ if (!cleaned) throw new ValidationError("ruledOut entries cannot be blank");
227
+ return { gen: generation, ruledOut: cleaned, why, at: now };
228
+ }
229
+
230
+ export function continuationForAgent(agentId: string): ContinuationEnvelope | null {
231
+ return getContinuationEnvelope(agentId);
232
+ }