agent-relay-server 0.33.0 → 0.33.1

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,339 @@
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 { normalizeTags, parseStringArray } from "./agents.ts";
18
+ import { linkAttachmentRefs, validateAttachmentRefs } from "./artifacts.ts";
19
+ import { ValidationError, getDb } from "./connection.ts";
20
+ import { sendMessage } from "./messages.ts";
21
+ import { findOpenTaskByDedupe, getTask, insertTaskEvent, taskMessageBody } from "./tasks.ts";
22
+ import type {
23
+ AgentCard,
24
+ ActivityEvent,
25
+ ActivityEventInput,
26
+ AgentKind,
27
+ AgentSessionGuard,
28
+ Artifact,
29
+ ArtifactBlob,
30
+ ArtifactKind,
31
+ ArtifactLink,
32
+ ArtifactSensitivity,
33
+ ArtifactVisibility,
34
+ AttachmentRef,
35
+ ChannelBinding,
36
+ ChannelBindingMode,
37
+ ChannelRouteTarget,
38
+ ChatHistoryImport,
39
+ ChatHistoryImportEntry,
40
+ ChannelSummary,
41
+ ChannelTargetHealth,
42
+ CreatePairInput,
43
+ HealthCheck,
44
+ HealthReport,
45
+ ManagedAgent,
46
+ ManagedSessionExitDiagnostics,
47
+ Message,
48
+ MessageDeliveryAttempt,
49
+ MessageDeliveryStatus,
50
+ Orchestrator,
51
+ OrchestratorHealth,
52
+ OrchestratorRuntimeInput,
53
+ OrchestratorStatus,
54
+ OrchestratorUpgradeState,
55
+ PairActionInput,
56
+ PairMessageInput,
57
+ PairSession,
58
+ PairStatus,
59
+ RegisterAgentInput,
60
+ ReplyObligation,
61
+ RegisterOrchestratorInput,
62
+ SendMessageInput,
63
+ PollQuery,
64
+ SpawnApprovalMode,
65
+ SpawnProvider,
66
+ Task,
67
+ TaskEvent,
68
+ TaskSeverity,
69
+ TaskStatus,
70
+ IntegrationEventInput,
71
+ IntegrationSummary,
72
+ IntegrationTaskStats,
73
+ InboxDraft,
74
+ InboxState,
75
+ InboxThreadState,
76
+ ContextSnapshot,
77
+ ContextState,
78
+ ProviderCapabilities,
79
+ TaskStatusInput,
80
+ WorkspaceMetadata,
81
+ WorkspaceRecord,
82
+ WorkspaceStatus,
83
+ } from "../types";
84
+
85
+ export function ingestIntegrationEvent(input: IntegrationEventInput, integrationName: string): { task: Task; event: TaskEvent; created: boolean; message?: Message } {
86
+ const now = Date.now();
87
+ const source = input.source ?? integrationName;
88
+ const severity = input.severity ?? "info";
89
+ const eventType = input.type ?? "event";
90
+ const targetStatus = input.status === "resolved" ? "done" : input.status;
91
+ const attachmentRefs = input.attachments ?? [];
92
+ validateAttachmentRefs(attachmentRefs);
93
+ updateIntegrationObserved(source, now);
94
+
95
+ return getDb().transaction(() => {
96
+ const existing = input.dedupeKey ? findOpenTaskByDedupe(source, input.dedupeKey) : null;
97
+ let taskId: number;
98
+ let created = false;
99
+
100
+ if (existing) {
101
+ getDb().query(`
102
+ UPDATE tasks
103
+ SET title = ?, body = ?, severity = ?, target = ?, channel = ?, external_url = ?,
104
+ occurrence_count = occurrence_count + 1, metadata = ?, updated_at = ?, last_seen_at = ?,
105
+ status = CASE WHEN ? IS NULL THEN status ELSE ? END,
106
+ result = CASE WHEN ? IN ('done', 'failed', 'canceled') THEN ? ELSE result END
107
+ WHERE id = ?
108
+ `).run(
109
+ input.title,
110
+ input.body,
111
+ severity,
112
+ input.target,
113
+ input.channel ?? null,
114
+ input.externalUrl ?? null,
115
+ JSON.stringify(input.metadata ?? existing.metadata),
116
+ now,
117
+ now,
118
+ targetStatus ?? null,
119
+ targetStatus ?? null,
120
+ targetStatus ?? null,
121
+ input.body,
122
+ existing.id,
123
+ );
124
+ taskId = existing.id;
125
+ } else {
126
+ const result = getDb().query(`
127
+ INSERT INTO tasks (source, title, body, severity, status, target, channel, dedupe_key, external_url, metadata, created_at, updated_at, last_seen_at)
128
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
129
+ `).run(
130
+ source,
131
+ input.title,
132
+ input.body,
133
+ severity,
134
+ targetStatus ?? "open",
135
+ input.target,
136
+ input.channel ?? null,
137
+ input.dedupeKey ?? null,
138
+ input.externalUrl ?? null,
139
+ JSON.stringify(input.metadata ?? {}),
140
+ now,
141
+ now,
142
+ now,
143
+ );
144
+ taskId = Number(result.lastInsertRowid);
145
+ created = true;
146
+ }
147
+
148
+ const task = getTask(taskId)!;
149
+ linkAttachmentRefs("task", task.id, attachmentRefs, source);
150
+ const event = insertTaskEvent(taskId, {
151
+ source,
152
+ type: eventType,
153
+ severity,
154
+ title: input.title,
155
+ body: input.body,
156
+ metadata: input.metadata ?? {},
157
+ }, now);
158
+
159
+ let message: Message | undefined;
160
+ if (created && task.status === "open") {
161
+ message = sendMessage({
162
+ from: "system",
163
+ to: task.target,
164
+ kind: "task",
165
+ channel: task.channel,
166
+ subject: `[${task.severity}] ${task.title}`,
167
+ body: taskMessageBody(task),
168
+ claimable: true,
169
+ payload: {
170
+ taskId: task.id,
171
+ source: task.source,
172
+ title: task.title,
173
+ status: task.status,
174
+ severity: task.severity,
175
+ dedupeKey: task.dedupeKey ?? null,
176
+ externalUrl: task.externalUrl ?? null,
177
+ ...(attachmentRefs.length ? { attachments: attachmentRefs } : {}),
178
+ },
179
+ });
180
+ getDb().query("UPDATE tasks SET message_id = ?, updated_at = ? WHERE id = ?").run(message.id, now, task.id);
181
+ }
182
+
183
+ return { task: getTask(taskId)!, event, created, message };
184
+ })();
185
+ }
186
+
187
+
188
+ export function listIntegrationTaskStats(): IntegrationTaskStats[] {
189
+ const rows = getDb().query(`
190
+ SELECT
191
+ source,
192
+ COUNT(*) AS tasks,
193
+ SUM(CASE WHEN status NOT IN ('done', 'failed', 'canceled') THEN 1 ELSE 0 END) AS open_tasks,
194
+ SUM(CASE WHEN status IN ('open', 'blocked') AND claimed_by IS NULL THEN 1 ELSE 0 END) AS waiting_tasks,
195
+ SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed_tasks,
196
+ MAX(last_seen_at) AS last_seen_at,
197
+ MAX(updated_at) AS last_updated_at
198
+ FROM tasks
199
+ GROUP BY source
200
+ ORDER BY last_seen_at DESC, source ASC
201
+ `).all() as any[];
202
+
203
+ return rows.map((row) => ({
204
+ source: row.source,
205
+ tasks: Number(row.tasks ?? 0),
206
+ openTasks: Number(row.open_tasks ?? 0),
207
+ waitingTasks: Number(row.waiting_tasks ?? 0),
208
+ failedTasks: Number(row.failed_tasks ?? 0),
209
+ lastSeenAt: row.last_seen_at ?? undefined,
210
+ lastUpdatedAt: row.last_updated_at ?? undefined,
211
+ }));
212
+ }
213
+
214
+ export type IntegrationRegistryInput = {
215
+ name: string;
216
+ displayName?: string;
217
+ description?: string;
218
+ enabled?: boolean;
219
+ scopes?: string[];
220
+ targets?: string[];
221
+ channels?: string[];
222
+ type?: string;
223
+ icon?: string;
224
+ accentColor?: string;
225
+ tags?: string[];
226
+ homepageUrl?: string;
227
+ repositoryUrl?: string;
228
+ docsUrl?: string;
229
+ manifest?: Record<string, unknown>;
230
+ source?: "api" | "env" | "observed";
231
+ };
232
+
233
+ export function rowToIntegrationRegistry(row: any): Omit<IntegrationSummary, "configured" | "observed" | "callbackConfigured" | "rateLimit" | "taskStats"> & {
234
+ source: "api" | "env" | "observed";
235
+ lastEventAt?: number;
236
+ lastTaskAt?: number;
237
+ lastAuthSuccessAt?: number;
238
+ lastAuthFailureAt?: number;
239
+ } {
240
+ return {
241
+ name: row.name,
242
+ displayName: row.display_name ?? undefined,
243
+ description: row.description ?? undefined,
244
+ enabled: row.enabled !== 0,
245
+ scopes: parseStringArray(row.scopes),
246
+ targets: parseStringArray(row.targets),
247
+ channels: parseStringArray(row.channels),
248
+ type: row.type ?? undefined,
249
+ icon: row.icon ?? undefined,
250
+ accentColor: row.accent_color ?? undefined,
251
+ tags: parseStringArray(row.tags),
252
+ homepageUrl: row.homepage_url ?? undefined,
253
+ repositoryUrl: row.repository_url ?? undefined,
254
+ docsUrl: row.docs_url ?? undefined,
255
+ manifest: parseJson(row.manifest, {}),
256
+ source: row.source ?? "api",
257
+ lastEventAt: row.last_event_at ?? undefined,
258
+ lastTaskAt: row.last_task_at ?? undefined,
259
+ lastAuthSuccessAt: row.last_auth_success_at ?? undefined,
260
+ lastAuthFailureAt: row.last_auth_failure_at ?? undefined,
261
+ };
262
+ }
263
+
264
+ export function upsertIntegrationRegistry(input: IntegrationRegistryInput): ReturnType<typeof rowToIntegrationRegistry> {
265
+ const name = stringValue(input.name);
266
+ if (!name) throw new ValidationError("integration name required");
267
+ const now = Date.now();
268
+ getDb().query(`
269
+ INSERT INTO integration_registry (
270
+ name, display_name, description, enabled, scopes, targets, channels, type, icon, accent_color,
271
+ tags, homepage_url, repository_url, docs_url, manifest, source, created_at, updated_at
272
+ )
273
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
274
+ ON CONFLICT(name) DO UPDATE SET
275
+ display_name = coalesce(excluded.display_name, integration_registry.display_name),
276
+ description = coalesce(excluded.description, integration_registry.description),
277
+ enabled = CASE WHEN integration_registry.source = 'api' AND excluded.source IN ('env', 'observed') THEN integration_registry.enabled ELSE excluded.enabled END,
278
+ scopes = CASE WHEN excluded.scopes <> '[]' THEN excluded.scopes ELSE integration_registry.scopes END,
279
+ targets = CASE WHEN excluded.targets <> '[]' THEN excluded.targets ELSE integration_registry.targets END,
280
+ channels = CASE WHEN excluded.channels <> '[]' THEN excluded.channels ELSE integration_registry.channels END,
281
+ type = coalesce(excluded.type, integration_registry.type),
282
+ icon = coalesce(excluded.icon, integration_registry.icon),
283
+ accent_color = coalesce(excluded.accent_color, integration_registry.accent_color),
284
+ tags = CASE WHEN excluded.tags <> '[]' THEN excluded.tags ELSE integration_registry.tags END,
285
+ homepage_url = coalesce(excluded.homepage_url, integration_registry.homepage_url),
286
+ repository_url = coalesce(excluded.repository_url, integration_registry.repository_url),
287
+ docs_url = coalesce(excluded.docs_url, integration_registry.docs_url),
288
+ manifest = CASE WHEN excluded.manifest <> '{}' THEN excluded.manifest ELSE integration_registry.manifest END,
289
+ source = CASE WHEN integration_registry.source = 'api' AND excluded.source IN ('env', 'observed') THEN integration_registry.source ELSE excluded.source END,
290
+ updated_at = excluded.updated_at
291
+ `).run(
292
+ name,
293
+ input.displayName ?? null,
294
+ input.description ?? null,
295
+ input.enabled === false ? 0 : 1,
296
+ JSON.stringify(input.scopes ?? []),
297
+ JSON.stringify(input.targets ?? []),
298
+ JSON.stringify(input.channels ?? []),
299
+ input.type ?? null,
300
+ input.icon ?? null,
301
+ input.accentColor ?? null,
302
+ JSON.stringify(normalizeTags(input.tags)),
303
+ input.homepageUrl ?? null,
304
+ input.repositoryUrl ?? null,
305
+ input.docsUrl ?? null,
306
+ JSON.stringify(input.manifest ?? {}),
307
+ input.source ?? "api",
308
+ now,
309
+ now,
310
+ );
311
+ return getIntegrationRegistry(name)!;
312
+ }
313
+
314
+ export function getIntegrationRegistry(name: string): ReturnType<typeof rowToIntegrationRegistry> | null {
315
+ const row = getDb().query("SELECT * FROM integration_registry WHERE name = ?").get(name) as any;
316
+ return row ? rowToIntegrationRegistry(row) : null;
317
+ }
318
+
319
+ export function listIntegrationRegistry(): ReturnType<typeof rowToIntegrationRegistry>[] {
320
+ return (getDb().query("SELECT * FROM integration_registry ORDER BY display_name COLLATE NOCASE, name COLLATE NOCASE").all() as any[]).map(rowToIntegrationRegistry);
321
+ }
322
+
323
+ export function updateIntegrationObserved(name: string, eventAt: number): void {
324
+ const now = Date.now();
325
+ getDb().query(`
326
+ INSERT INTO integration_registry (name, enabled, scopes, targets, channels, tags, manifest, source, created_at, updated_at, last_event_at, last_task_at)
327
+ VALUES (?, 1, '[]', '[]', '[]', '[]', '{}', 'observed', ?, ?, ?, ?)
328
+ ON CONFLICT(name) DO UPDATE SET
329
+ last_event_at = max(coalesce(last_event_at, 0), excluded.last_event_at),
330
+ last_task_at = max(coalesce(last_task_at, 0), excluded.last_task_at),
331
+ updated_at = excluded.updated_at
332
+ `).run(name, now, now, eventAt, eventAt);
333
+ }
334
+
335
+ export function isIntegrationRegistryEnabled(name: string): boolean {
336
+ const registry = getIntegrationRegistry(name);
337
+ return registry?.enabled !== false;
338
+ }
339
+