agent-relay-server 0.33.0 → 0.34.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,342 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { randomUUID } from "node:crypto";
3
+ import { isRecord, stringValue, isMechanicalMessageKind } from "agent-relay-sdk";
4
+ import { ORCHESTRATOR_PROTOCOL_VERSION, VERSION } from "../config.ts";
5
+ import { parseJson } from "../utils";
6
+ import { isLiveIsolatedWorkspace } from "../workspace-phase";
7
+ import {
8
+ CONTRACT_REQUIREMENTS,
9
+ contractCompatibility,
10
+ parseRuntimeCapabilities,
11
+ parseRuntimeContracts,
12
+ parseRuntimePackage,
13
+ type RuntimeContracts,
14
+ } from "../contracts";
15
+ import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "../config";
16
+ import { matchAgents } from "../agent-ref";
17
+ import { ValidationError, getDb } from "./connection.ts";
18
+ import { rowToArtifact, rowToArtifactBlob, rowToArtifactLink } from "./mappers.ts";
19
+ import type {
20
+ AgentCard,
21
+ ActivityEvent,
22
+ ActivityEventInput,
23
+ AgentKind,
24
+ AgentSessionGuard,
25
+ Artifact,
26
+ ArtifactBlob,
27
+ ArtifactKind,
28
+ ArtifactLink,
29
+ ArtifactSensitivity,
30
+ ArtifactVisibility,
31
+ AttachmentRef,
32
+ ChannelBinding,
33
+ ChannelBindingMode,
34
+ ChannelRouteTarget,
35
+ ChatHistoryImport,
36
+ ChatHistoryImportEntry,
37
+ ChannelSummary,
38
+ ChannelTargetHealth,
39
+ CreatePairInput,
40
+ HealthCheck,
41
+ HealthReport,
42
+ ManagedAgent,
43
+ ManagedSessionExitDiagnostics,
44
+ Message,
45
+ MessageDeliveryAttempt,
46
+ MessageDeliveryStatus,
47
+ Orchestrator,
48
+ OrchestratorHealth,
49
+ OrchestratorRuntimeInput,
50
+ OrchestratorStatus,
51
+ OrchestratorUpgradeState,
52
+ PairActionInput,
53
+ PairMessageInput,
54
+ PairSession,
55
+ PairStatus,
56
+ RegisterAgentInput,
57
+ ReplyObligation,
58
+ RegisterOrchestratorInput,
59
+ SendMessageInput,
60
+ PollQuery,
61
+ SpawnApprovalMode,
62
+ SpawnProvider,
63
+ Task,
64
+ TaskEvent,
65
+ TaskSeverity,
66
+ TaskStatus,
67
+ IntegrationEventInput,
68
+ IntegrationSummary,
69
+ IntegrationTaskStats,
70
+ InboxDraft,
71
+ InboxState,
72
+ InboxThreadState,
73
+ ContextSnapshot,
74
+ ContextState,
75
+ ProviderCapabilities,
76
+ TaskStatusInput,
77
+ WorkspaceMetadata,
78
+ WorkspaceRecord,
79
+ WorkspaceStatus,
80
+ } from "../types";
81
+
82
+ // --- Artifacts ---
83
+
84
+ export const VALID_ARTIFACT_KINDS = new Set<ArtifactKind>(["image", "audio", "video", "document", "archive", "other"]);
85
+ export const VALID_ARTIFACT_VISIBILITIES = new Set<ArtifactVisibility>(["private", "project", "org"]);
86
+ export const VALID_ARTIFACT_SENSITIVITIES = new Set<ArtifactSensitivity>(["public", "normal", "sensitive", "secret"]);
87
+ export const VALID_ARTIFACT_LINK_ENTITY_TYPES = new Set<ArtifactLink["entityType"]>(["message", "task", "recipeRun", "recipeStep", "channelEvent"]);
88
+ export const VALID_ARTIFACT_ROLES = new Set<NonNullable<ArtifactLink["role"]>>(["media", "patch", "report", "log", "output", "input"]);
89
+
90
+ export function artifactId(): string {
91
+ return `art_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
92
+ }
93
+
94
+ export function artifactLinkId(): string {
95
+ return `alink_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
96
+ }
97
+
98
+ export function normalizeArtifactKind(kind: unknown): ArtifactKind {
99
+ return typeof kind === "string" && VALID_ARTIFACT_KINDS.has(kind as ArtifactKind) ? kind as ArtifactKind : "other";
100
+ }
101
+
102
+ export function normalizeArtifactVisibility(visibility: unknown): ArtifactVisibility {
103
+ return typeof visibility === "string" && VALID_ARTIFACT_VISIBILITIES.has(visibility as ArtifactVisibility) ? visibility as ArtifactVisibility : "project";
104
+ }
105
+
106
+ export function normalizeArtifactSensitivity(sensitivity: unknown): ArtifactSensitivity {
107
+ return typeof sensitivity === "string" && VALID_ARTIFACT_SENSITIVITIES.has(sensitivity as ArtifactSensitivity) ? sensitivity as ArtifactSensitivity : "normal";
108
+ }
109
+
110
+ export function upsertArtifactBlob(input: {
111
+ digest: string;
112
+ storageUri: string;
113
+ mediaType: string;
114
+ size: number;
115
+ createdAt?: number;
116
+ }): ArtifactBlob {
117
+ const now = input.createdAt ?? Date.now();
118
+ getDb().query(`
119
+ INSERT INTO artifact_blobs (digest, storage_uri, media_type, size, created_at)
120
+ VALUES (?, ?, ?, ?, ?)
121
+ ON CONFLICT(digest) DO UPDATE SET
122
+ storage_uri = excluded.storage_uri,
123
+ media_type = excluded.media_type,
124
+ size = excluded.size
125
+ `).run(input.digest, input.storageUri, input.mediaType, input.size, now);
126
+ return getArtifactBlob(input.digest)!;
127
+ }
128
+
129
+ export function getArtifactBlob(digest: string): ArtifactBlob | null {
130
+ const row = getDb().query("SELECT * FROM artifact_blobs WHERE digest = ?").get(digest) as any;
131
+ return row ? rowToArtifactBlob(row) : null;
132
+ }
133
+
134
+ export function deleteArtifactBlob(digest: string): boolean {
135
+ return getDb().query("DELETE FROM artifact_blobs WHERE digest = ?").run(digest).changes > 0;
136
+ }
137
+
138
+ export function createArtifact(input: {
139
+ blobDigest: string;
140
+ mediaType: string;
141
+ kind?: ArtifactKind;
142
+ filename?: string;
143
+ size: number;
144
+ visibility?: ArtifactVisibility;
145
+ sensitivity?: ArtifactSensitivity;
146
+ createdBy: string;
147
+ expiresAt?: number;
148
+ metadata?: Record<string, unknown>;
149
+ id?: string;
150
+ }): Artifact {
151
+ if (!getArtifactBlob(input.blobDigest)) throw new ValidationError("artifact blob not found");
152
+ const id = input.id ?? artifactId();
153
+ const now = Date.now();
154
+ getDb().query(`
155
+ INSERT INTO artifacts (
156
+ id, blob_digest, media_type, kind, filename, size, visibility, sensitivity,
157
+ created_by, created_at, expires_at, metadata
158
+ )
159
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
160
+ `).run(
161
+ id,
162
+ input.blobDigest,
163
+ input.mediaType,
164
+ normalizeArtifactKind(input.kind),
165
+ input.filename ?? null,
166
+ input.size,
167
+ normalizeArtifactVisibility(input.visibility),
168
+ normalizeArtifactSensitivity(input.sensitivity),
169
+ input.createdBy,
170
+ now,
171
+ input.expiresAt ?? null,
172
+ JSON.stringify(input.metadata ?? {}),
173
+ );
174
+ return getArtifact(id)!;
175
+ }
176
+
177
+ export function getArtifact(id: string): Artifact | null {
178
+ const row = getDb().query("SELECT * FROM artifacts WHERE id = ?").get(id) as any;
179
+ if (!row) return null;
180
+ return rowToArtifact(row, listArtifactLinks(id));
181
+ }
182
+
183
+ export function listArtifacts(query: {
184
+ messageId?: number;
185
+ taskId?: number;
186
+ createdBy?: string;
187
+ limit?: number;
188
+ } = {}): Artifact[] {
189
+ const conditions: string[] = [];
190
+ const params: any[] = [];
191
+ if (query.createdBy) {
192
+ conditions.push("a.created_by = ?");
193
+ params.push(query.createdBy);
194
+ }
195
+ if (query.messageId !== undefined) {
196
+ conditions.push("EXISTS (SELECT 1 FROM artifact_links l WHERE l.artifact_id = a.id AND l.entity_type = 'message' AND l.entity_id = ?)");
197
+ params.push(String(query.messageId));
198
+ }
199
+ if (query.taskId !== undefined) {
200
+ conditions.push("EXISTS (SELECT 1 FROM artifact_links l WHERE l.artifact_id = a.id AND l.entity_type = 'task' AND l.entity_id = ?)");
201
+ params.push(String(query.taskId));
202
+ }
203
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
204
+ const limit = Math.min(Math.max(query.limit ?? 100, 1), 500);
205
+ const rows = getDb().query(`SELECT a.* FROM artifacts a ${where} ORDER BY a.created_at DESC LIMIT ?`).all(...params, limit) as any[];
206
+ return rows.map((row) => rowToArtifact(row, listArtifactLinks(row.id)));
207
+ }
208
+
209
+ export function deleteArtifact(id: string): boolean {
210
+ return getDb().transaction(() => {
211
+ return getDb().query("DELETE FROM artifacts WHERE id = ?").run(id).changes > 0;
212
+ })();
213
+ }
214
+
215
+ export function listArtifactLinks(artifactId: string): ArtifactLink[] {
216
+ return (getDb().query("SELECT * FROM artifact_links WHERE artifact_id = ? ORDER BY created_at ASC").all(artifactId) as any[]).map(rowToArtifactLink);
217
+ }
218
+
219
+ export function listArtifactsForEntity(entityType: ArtifactLink["entityType"], entityId: string | number): Artifact[] {
220
+ const rows = getDb().query(`
221
+ SELECT a.*
222
+ FROM artifacts a
223
+ JOIN artifact_links l ON l.artifact_id = a.id
224
+ WHERE l.entity_type = ? AND l.entity_id = ?
225
+ ORDER BY l.created_at ASC
226
+ `).all(entityType, String(entityId)) as any[];
227
+ return rows.map((row) => rowToArtifact(row, listArtifactLinks(row.id)));
228
+ }
229
+
230
+ export function linkArtifact(input: {
231
+ artifactId: string;
232
+ entityType: ArtifactLink["entityType"];
233
+ entityId: string | number;
234
+ role?: ArtifactLink["role"];
235
+ title?: string;
236
+ createdBy: string;
237
+ }): ArtifactLink {
238
+ if (!VALID_ARTIFACT_LINK_ENTITY_TYPES.has(input.entityType)) throw new ValidationError("invalid artifact link entity type");
239
+ if (input.role && !VALID_ARTIFACT_ROLES.has(input.role)) throw new ValidationError("invalid artifact role");
240
+ if (!getArtifact(input.artifactId)) throw new ValidationError(`artifact ${input.artifactId} not found`);
241
+ const now = Date.now();
242
+ const id = artifactLinkId();
243
+ getDb().query(`
244
+ INSERT OR IGNORE INTO artifact_links (id, artifact_id, entity_type, entity_id, role, title, created_by, created_at)
245
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
246
+ `).run(
247
+ id,
248
+ input.artifactId,
249
+ input.entityType,
250
+ String(input.entityId),
251
+ input.role ?? null,
252
+ input.title ?? null,
253
+ input.createdBy,
254
+ now,
255
+ );
256
+ const row = getDb().query(`
257
+ SELECT * FROM artifact_links
258
+ WHERE artifact_id = ? AND entity_type = ? AND entity_id = ? AND ((role IS NULL AND ? IS NULL) OR role = ?)
259
+ ORDER BY created_at DESC
260
+ LIMIT 1
261
+ `).get(input.artifactId, input.entityType, String(input.entityId), input.role ?? null, input.role ?? null) as any;
262
+ return rowToArtifactLink(row);
263
+ }
264
+
265
+ export function deleteArtifactLinksForEntity(entityType: ArtifactLink["entityType"], entityId: string | number): number {
266
+ return getDb().query("DELETE FROM artifact_links WHERE entity_type = ? AND entity_id = ?").run(entityType, String(entityId)).changes;
267
+ }
268
+
269
+ export function artifactBlobReferenceCount(digest: string): number {
270
+ const row = getDb().query("SELECT COUNT(*) AS count FROM artifacts WHERE blob_digest = ?").get(digest) as { count?: number };
271
+ return row.count ?? 0;
272
+ }
273
+
274
+ export function unreferencedArtifactBlobs(): ArtifactBlob[] {
275
+ return (getDb().query(`
276
+ SELECT b.*
277
+ FROM artifact_blobs b
278
+ WHERE NOT EXISTS (SELECT 1 FROM artifacts a WHERE a.blob_digest = b.digest)
279
+ `).all() as any[]).map(rowToArtifactBlob);
280
+ }
281
+
282
+ export function sweepArtifacts(input: { now?: number; unlinkedGraceMs?: number } = {}): { artifactIds: string[]; blobs: ArtifactBlob[] } {
283
+ const now = input.now ?? Date.now();
284
+ const unlinkedCutoff = now - (input.unlinkedGraceMs ?? 60 * 60 * 1000);
285
+ return getDb().transaction(() => {
286
+ const rows = getDb().query(`
287
+ SELECT id FROM artifacts a
288
+ WHERE (expires_at IS NOT NULL AND expires_at <= ?)
289
+ OR (created_at <= ? AND NOT EXISTS (SELECT 1 FROM artifact_links l WHERE l.artifact_id = a.id))
290
+ `).all(now, unlinkedCutoff) as Array<{ id: string }>;
291
+ for (const row of rows) getDb().query("DELETE FROM artifacts WHERE id = ?").run(row.id);
292
+ const blobs = unreferencedArtifactBlobs();
293
+ for (const blob of blobs) deleteArtifactBlob(blob.digest);
294
+ return { artifactIds: rows.map((row) => row.id), blobs };
295
+ })();
296
+ }
297
+
298
+ export function cleanAttachmentRefs(payload: Record<string, unknown> | undefined): AttachmentRef[] {
299
+ const raw = payload?.attachments;
300
+ if (!Array.isArray(raw)) return [];
301
+ const refs: AttachmentRef[] = [];
302
+ for (const [index, item] of raw.entries()) {
303
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
304
+ throw new ValidationError(`payload.attachments[${index}] must be an object`);
305
+ }
306
+ const record = item as Record<string, unknown>;
307
+ const artifactId = typeof record.artifactId === "string" ? record.artifactId.trim() : "";
308
+ if (!artifactId) continue;
309
+ const role = typeof record.role === "string" && VALID_ARTIFACT_ROLES.has(record.role as NonNullable<ArtifactLink["role"]>)
310
+ ? record.role as NonNullable<ArtifactLink["role"]>
311
+ : undefined;
312
+ refs.push({
313
+ artifactId,
314
+ kind: normalizeArtifactKind(record.kind),
315
+ role,
316
+ title: typeof record.title === "string" ? record.title.slice(0, 240) : undefined,
317
+ });
318
+ }
319
+ return refs;
320
+ }
321
+
322
+ export function validateAttachmentRefs(refs: AttachmentRef[]): void {
323
+ for (const ref of refs) {
324
+ if (!getArtifact(ref.artifactId)) throw new ValidationError(`artifact ${ref.artifactId} not found`);
325
+ }
326
+ }
327
+
328
+ export function linkAttachmentRefs(entityType: ArtifactLink["entityType"], entityId: string | number, refs: AttachmentRef[], createdBy: string): void {
329
+ for (const ref of refs) {
330
+ linkArtifact({
331
+ artifactId: ref.artifactId,
332
+ entityType,
333
+ entityId,
334
+ role: ref.role,
335
+ title: ref.title,
336
+ createdBy,
337
+ });
338
+ }
339
+ }
340
+
341
+ // --- Messages ---
342
+