reflectt-node 0.1.8 → 0.1.11
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/defaults/TEAM-ROLES.yaml +317 -5
- package/dist/agent-config.d.ts +51 -0
- package/dist/agent-config.d.ts.map +1 -0
- package/dist/agent-config.js +129 -0
- package/dist/agent-config.js.map +1 -0
- package/dist/agent-config.test.d.ts +2 -0
- package/dist/agent-config.test.d.ts.map +1 -0
- package/dist/agent-config.test.js +91 -0
- package/dist/agent-config.test.js.map +1 -0
- package/dist/agent-memories.d.ts +58 -0
- package/dist/agent-memories.d.ts.map +1 -0
- package/dist/agent-memories.js +168 -0
- package/dist/agent-memories.js.map +1 -0
- package/dist/agent-memories.test.d.ts +2 -0
- package/dist/agent-memories.test.d.ts.map +1 -0
- package/dist/agent-memories.test.js +327 -0
- package/dist/agent-memories.test.js.map +1 -0
- package/dist/agent-messaging.d.ts +50 -0
- package/dist/agent-messaging.d.ts.map +1 -0
- package/dist/agent-messaging.js +103 -0
- package/dist/agent-messaging.js.map +1 -0
- package/dist/agent-messaging.test.d.ts +2 -0
- package/dist/agent-messaging.test.d.ts.map +1 -0
- package/dist/agent-messaging.test.js +105 -0
- package/dist/agent-messaging.test.js.map +1 -0
- package/dist/agent-runs.d.ts +158 -0
- package/dist/agent-runs.d.ts.map +1 -0
- package/dist/agent-runs.js +514 -0
- package/dist/agent-runs.js.map +1 -0
- package/dist/agent-runs.test.d.ts +2 -0
- package/dist/agent-runs.test.d.ts.map +1 -0
- package/dist/agent-runs.test.js +386 -0
- package/dist/agent-runs.test.js.map +1 -0
- package/dist/approval-queue.test.d.ts +2 -0
- package/dist/approval-queue.test.d.ts.map +1 -0
- package/dist/approval-queue.test.js +118 -0
- package/dist/approval-queue.test.js.map +1 -0
- package/dist/artifact-store.d.ts +55 -0
- package/dist/artifact-store.d.ts.map +1 -0
- package/dist/artifact-store.js +128 -0
- package/dist/artifact-store.js.map +1 -0
- package/dist/artifact-store.test.d.ts +2 -0
- package/dist/artifact-store.test.d.ts.map +1 -0
- package/dist/artifact-store.test.js +119 -0
- package/dist/artifact-store.test.js.map +1 -0
- package/dist/boardHealthWorker.d.ts +28 -0
- package/dist/boardHealthWorker.d.ts.map +1 -1
- package/dist/boardHealthWorker.js +33 -1
- package/dist/boardHealthWorker.js.map +1 -1
- package/dist/canvas-input.test.d.ts +2 -0
- package/dist/canvas-input.test.d.ts.map +1 -0
- package/dist/canvas-input.test.js +96 -0
- package/dist/canvas-input.test.js.map +1 -0
- package/dist/canvas-render.test.d.ts +2 -0
- package/dist/canvas-render.test.d.ts.map +1 -0
- package/dist/canvas-render.test.js +95 -0
- package/dist/canvas-render.test.js.map +1 -0
- package/dist/capabilities/browser.d.ts +75 -0
- package/dist/capabilities/browser.d.ts.map +1 -0
- package/dist/capabilities/browser.js +172 -0
- package/dist/capabilities/browser.js.map +1 -0
- package/dist/channels.d.ts +1 -1
- package/dist/cli.js +4 -2
- package/dist/cli.js.map +1 -1
- package/dist/cloud.d.ts +2 -0
- package/dist/cloud.d.ts.map +1 -1
- package/dist/cloud.js +21 -1
- package/dist/cloud.js.map +1 -1
- package/dist/cost-enforcement.d.ts +38 -0
- package/dist/cost-enforcement.d.ts.map +1 -0
- package/dist/cost-enforcement.js +84 -0
- package/dist/cost-enforcement.js.map +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +131 -0
- package/dist/db.js.map +1 -1
- package/dist/e2e-loop-proof.test.d.ts +2 -0
- package/dist/e2e-loop-proof.test.d.ts.map +1 -0
- package/dist/e2e-loop-proof.test.js +104 -0
- package/dist/e2e-loop-proof.test.js.map +1 -0
- package/dist/email-sms-send.test.d.ts +2 -0
- package/dist/email-sms-send.test.d.ts.map +1 -0
- package/dist/email-sms-send.test.js +96 -0
- package/dist/email-sms-send.test.js.map +1 -0
- package/dist/events.d.ts +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -1
- package/dist/fingerprint.d.ts.map +1 -1
- package/dist/fingerprint.js +5 -10
- package/dist/fingerprint.js.map +1 -1
- package/dist/github-webhook-chat.d.ts +75 -0
- package/dist/github-webhook-chat.d.ts.map +1 -0
- package/dist/github-webhook-chat.js +108 -0
- package/dist/github-webhook-chat.js.map +1 -0
- package/dist/handoff-state.test.d.ts +2 -0
- package/dist/handoff-state.test.d.ts.map +1 -0
- package/dist/handoff-state.test.js +102 -0
- package/dist/handoff-state.test.js.map +1 -0
- package/dist/health.d.ts +9 -0
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +18 -0
- package/dist/health.js.map +1 -1
- package/dist/host-error-correlation.d.ts +65 -0
- package/dist/host-error-correlation.d.ts.map +1 -0
- package/dist/host-error-correlation.js +123 -0
- package/dist/host-error-correlation.js.map +1 -0
- package/dist/index.js +39 -10
- package/dist/index.js.map +1 -1
- package/dist/notificationDedupeGuard.d.ts +4 -0
- package/dist/notificationDedupeGuard.d.ts.map +1 -1
- package/dist/notificationDedupeGuard.js +8 -4
- package/dist/notificationDedupeGuard.js.map +1 -1
- package/dist/presence.d.ts +37 -5
- package/dist/presence.d.ts.map +1 -1
- package/dist/presence.js +127 -16
- package/dist/presence.js.map +1 -1
- package/dist/review-sla.d.ts +9 -0
- package/dist/review-sla.d.ts.map +1 -0
- package/dist/review-sla.js +51 -0
- package/dist/review-sla.js.map +1 -0
- package/dist/routing-enforcement.test.d.ts +2 -0
- package/dist/routing-enforcement.test.d.ts.map +1 -0
- package/dist/routing-enforcement.test.js +86 -0
- package/dist/routing-enforcement.test.js.map +1 -0
- package/dist/run-retention.test.d.ts +2 -0
- package/dist/run-retention.test.d.ts.map +1 -0
- package/dist/run-retention.test.js +57 -0
- package/dist/run-retention.test.js.map +1 -0
- package/dist/run-stream.test.d.ts +2 -0
- package/dist/run-stream.test.d.ts.map +1 -0
- package/dist/run-stream.test.js +70 -0
- package/dist/run-stream.test.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1229 -75
- package/dist/server.js.map +1 -1
- package/dist/tasks.d.ts.map +1 -1
- package/dist/tasks.js +45 -0
- package/dist/tasks.js.map +1 -1
- package/dist/todoHoardingGuard.d.ts +17 -0
- package/dist/todoHoardingGuard.d.ts.map +1 -1
- package/dist/todoHoardingGuard.js +25 -2
- package/dist/todoHoardingGuard.js.map +1 -1
- package/dist/webhook-storage.d.ts +50 -0
- package/dist/webhook-storage.d.ts.map +1 -0
- package/dist/webhook-storage.js +102 -0
- package/dist/webhook-storage.js.map +1 -0
- package/dist/webhook-storage.test.d.ts +2 -0
- package/dist/webhook-storage.test.d.ts.map +1 -0
- package/dist/webhook-storage.test.js +86 -0
- package/dist/webhook-storage.test.js.map +1 -0
- package/dist/workflow-templates.d.ts +44 -0
- package/dist/workflow-templates.d.ts.map +1 -0
- package/dist/workflow-templates.js +154 -0
- package/dist/workflow-templates.js.map +1 -0
- package/dist/workflow-templates.test.d.ts +2 -0
- package/dist/workflow-templates.test.d.ts.map +1 -0
- package/dist/workflow-templates.test.js +76 -0
- package/dist/workflow-templates.test.js.map +1 -0
- package/package.json +3 -1
- package/public/dashboard.js +11 -0
- package/public/design-tokens-platform.md +118 -0
- package/public/design-tokens.css +195 -0
- package/public/docs.md +127 -2
package/dist/server.js
CHANGED
|
@@ -69,9 +69,9 @@ import { researchManager } from './research.js';
|
|
|
69
69
|
import { wsHeartbeat } from './ws-heartbeat.js';
|
|
70
70
|
import { getBuildInfo } from './buildInfo.js';
|
|
71
71
|
import { appendStoredLog, readStoredLogs, getStoredLogPath } from './logStore.js';
|
|
72
|
-
import { getAgentRoles, getAgentRolesSource, loadAgentRoles, startConfigWatch, suggestAssignee, suggestReviewer, checkWipCap, saveAgentRoles,
|
|
72
|
+
import { getAgentRoles, getAgentRolesSource, loadAgentRoles, startConfigWatch, suggestAssignee, suggestReviewer, checkWipCap, saveAgentRoles, getAgentRole, getAgentAliases, setAgentDisplayName, resolveAgentMention } from './assignment.js';
|
|
73
73
|
import { initTelemetry, trackRequest as trackTelemetryRequest, trackError as trackTelemetryError, trackTaskEvent, getSnapshot as getTelemetrySnapshot, getTelemetryConfig } from './telemetry.js';
|
|
74
|
-
import {
|
|
74
|
+
import { recordUsageBatch, getUsageSummary, getUsageByAgent, getUsageByModel, getUsageByTask, getDailySpendByModel, getAvgCostByLane, getAvgCostByAgent, setCap, listCaps, deleteCap, checkCaps, getRoutingSuggestions, estimateCost, ensureUsageTables } from './usage-tracking.js';
|
|
75
75
|
import { getTeamConfigHealth } from './team-config.js';
|
|
76
76
|
import { SecretVault } from './secrets.js';
|
|
77
77
|
import { initGitHubActorAuth, resolveGitHubTokenForActor } from './github-actor-auth.js';
|
|
@@ -81,6 +81,7 @@ import { createGitHubIdentityProvider } from './github-identity.js';
|
|
|
81
81
|
import { getProvisioningManager } from './provisioning.js';
|
|
82
82
|
import { getWebhookDeliveryManager } from './webhooks.js';
|
|
83
83
|
import { enrichWebhookPayload } from './github-webhook-attribution.js';
|
|
84
|
+
import { formatGitHubEvent } from './github-webhook-chat.js';
|
|
84
85
|
import { exportBundle, importBundle } from './portability.js';
|
|
85
86
|
import { getNotificationManager } from './notifications.js';
|
|
86
87
|
import { getConnectivityManager } from './connectivity.js';
|
|
@@ -275,6 +276,13 @@ function normalizeConfiguredModel(value) {
|
|
|
275
276
|
error: `Unknown model identifier "${raw}". Allowed aliases: ${Object.keys(MODEL_ALIASES).join(', ')} or provider/model format.`,
|
|
276
277
|
};
|
|
277
278
|
}
|
|
279
|
+
// ── Handoff state schema (max 3 columns per COO rule) ─────────────
|
|
280
|
+
const VALID_HANDOFF_DECISIONS = ['approved', 'rejected', 'needs_changes', 'escalated'];
|
|
281
|
+
const HandoffStateSchema = z.object({
|
|
282
|
+
reviewed_by: z.string().min(1),
|
|
283
|
+
decision: z.enum(VALID_HANDOFF_DECISIONS),
|
|
284
|
+
next_owner: z.string().min(1).optional(),
|
|
285
|
+
}).strict();
|
|
278
286
|
const UpdateTaskSchema = z.object({
|
|
279
287
|
title: z.string().min(1).optional(),
|
|
280
288
|
description: z.string().optional(),
|
|
@@ -4506,6 +4514,46 @@ export async function createServer() {
|
|
|
4506
4514
|
matchType: resolved.matchType,
|
|
4507
4515
|
};
|
|
4508
4516
|
});
|
|
4517
|
+
// ── Task handoff state ─────────────────────────────────────────────
|
|
4518
|
+
app.get('/tasks/:id/handoff', async (request, reply) => {
|
|
4519
|
+
const resolved = taskManager.resolveTaskId(request.params.id);
|
|
4520
|
+
if (!resolved.task) {
|
|
4521
|
+
reply.code(404);
|
|
4522
|
+
return { error: 'Task not found' };
|
|
4523
|
+
}
|
|
4524
|
+
const meta = resolved.task.metadata;
|
|
4525
|
+
const handoff = meta?.handoff_state ?? null;
|
|
4526
|
+
return {
|
|
4527
|
+
taskId: resolved.resolvedId,
|
|
4528
|
+
status: resolved.task.status,
|
|
4529
|
+
handoff_state: handoff,
|
|
4530
|
+
};
|
|
4531
|
+
});
|
|
4532
|
+
app.put('/tasks/:id/handoff', async (request, reply) => {
|
|
4533
|
+
const resolved = taskManager.resolveTaskId(request.params.id);
|
|
4534
|
+
if (!resolved.task || !resolved.resolvedId) {
|
|
4535
|
+
reply.code(404);
|
|
4536
|
+
return { error: 'Task not found' };
|
|
4537
|
+
}
|
|
4538
|
+
const body = request.body;
|
|
4539
|
+
const result = HandoffStateSchema.safeParse(body);
|
|
4540
|
+
if (!result.success) {
|
|
4541
|
+
reply.code(422);
|
|
4542
|
+
return {
|
|
4543
|
+
error: `Invalid handoff_state: ${result.error.issues.map(i => i.message).join(', ')}`,
|
|
4544
|
+
hint: 'Required: reviewed_by (string), decision (approved|rejected|needs_changes|escalated). Optional: next_owner (string).',
|
|
4545
|
+
};
|
|
4546
|
+
}
|
|
4547
|
+
const existingMeta = (resolved.task.metadata || {});
|
|
4548
|
+
taskManager.updateTask(resolved.resolvedId, {
|
|
4549
|
+
metadata: { ...existingMeta, handoff_state: result.data },
|
|
4550
|
+
});
|
|
4551
|
+
return {
|
|
4552
|
+
success: true,
|
|
4553
|
+
taskId: resolved.resolvedId,
|
|
4554
|
+
handoff_state: result.data,
|
|
4555
|
+
};
|
|
4556
|
+
});
|
|
4509
4557
|
// Task artifact visibility — resolves artifact paths and checks accessibility
|
|
4510
4558
|
app.get('/tasks/:id/artifacts', async (request, reply) => {
|
|
4511
4559
|
const resolved = resolveTaskFromParam(request.params.id, reply);
|
|
@@ -5672,7 +5720,9 @@ export async function createServer() {
|
|
|
5672
5720
|
|| data.metadata?.skip_dedup === true
|
|
5673
5721
|
|| data.metadata?.is_test === true;
|
|
5674
5722
|
if (!skipDedup && data.assignee) {
|
|
5675
|
-
|
|
5723
|
+
// 60-second window targets gateway reconnect double-fire (typical gap: <10s)
|
|
5724
|
+
// without blocking legitimate same-title task creation later in the day.
|
|
5725
|
+
const TASK_DEDUP_WINDOW_MS = 60_000; // 60 seconds
|
|
5676
5726
|
const cutoff = Date.now() - TASK_DEDUP_WINDOW_MS;
|
|
5677
5727
|
const normalizedTitle = data.title.trim().toLowerCase();
|
|
5678
5728
|
const activeTasks = taskManager.listTasks({ includeTest: true }).filter(t => t.status !== 'done'
|
|
@@ -5680,16 +5730,15 @@ export async function createServer() {
|
|
|
5680
5730
|
&& t.createdAt >= cutoff
|
|
5681
5731
|
&& t.title.trim().toLowerCase() === normalizedTitle);
|
|
5682
5732
|
if (activeTasks.length > 0) {
|
|
5733
|
+
// Return 200 with the existing task (collapse, not reject).
|
|
5734
|
+
// Agents that create-on-reconnect receive a success response identical
|
|
5735
|
+
// to what they'd get from a new creation — no retry loop triggered.
|
|
5683
5736
|
const existing = activeTasks[0];
|
|
5684
|
-
reply.code(409);
|
|
5685
5737
|
return {
|
|
5686
|
-
success:
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
status:
|
|
5690
|
-
existing_id: existing.id,
|
|
5691
|
-
existing_status: existing.status,
|
|
5692
|
-
hint: `Task "${existing.title}" already exists for ${data.assignee} (${existing.id}, status: ${existing.status}). Created ${Math.round((Date.now() - existing.createdAt) / 60000)}m ago.`,
|
|
5738
|
+
success: true,
|
|
5739
|
+
task: existing,
|
|
5740
|
+
deduplicated: true,
|
|
5741
|
+
hint: `Duplicate suppressed — task "${existing.title}" already exists for ${data.assignee} (${existing.id}, status: ${existing.status}, created ${Math.round((Date.now() - existing.createdAt) / 1000)}s ago).`,
|
|
5693
5742
|
};
|
|
5694
5743
|
}
|
|
5695
5744
|
}
|
|
@@ -6082,6 +6131,14 @@ export async function createServer() {
|
|
|
6082
6131
|
const pruned = boardHealthWorker.pruneAuditLog(maxAgeDays);
|
|
6083
6132
|
return { success: true, pruned };
|
|
6084
6133
|
});
|
|
6134
|
+
app.post('/board-health/quiet-window', async () => {
|
|
6135
|
+
boardHealthWorker.resetQuietWindow();
|
|
6136
|
+
return {
|
|
6137
|
+
success: true,
|
|
6138
|
+
quietUntil: Date.now() + (boardHealthWorker.getStatus().config?.restartQuietWindowMs ?? 300_000),
|
|
6139
|
+
message: 'Quiet window reset — ready-queue alerts suppressed for restart window',
|
|
6140
|
+
};
|
|
6141
|
+
});
|
|
6085
6142
|
// ── Agent change feed ─────────────────────────────────────────────────
|
|
6086
6143
|
app.get('/feed/:agent', async (request) => {
|
|
6087
6144
|
const { agent } = request.params;
|
|
@@ -6366,6 +6423,22 @@ export async function createServer() {
|
|
|
6366
6423
|
mergedMeta.reopened_from = existing.status;
|
|
6367
6424
|
}
|
|
6368
6425
|
}
|
|
6426
|
+
// ── Handoff state validation ──
|
|
6427
|
+
if (mergedMeta.handoff_state && typeof mergedMeta.handoff_state === 'object') {
|
|
6428
|
+
const handoffResult = HandoffStateSchema.safeParse(mergedMeta.handoff_state);
|
|
6429
|
+
if (!handoffResult.success) {
|
|
6430
|
+
reply.code(422);
|
|
6431
|
+
return {
|
|
6432
|
+
success: false,
|
|
6433
|
+
error: `Invalid handoff_state: ${handoffResult.error.issues.map(i => i.message).join(', ')}`,
|
|
6434
|
+
code: 'INVALID_HANDOFF_STATE',
|
|
6435
|
+
hint: 'handoff_state must have: reviewed_by (string), decision (approved|rejected|needs_changes|escalated), optional next_owner (string). Max 3 fields per COO rule.',
|
|
6436
|
+
gate: 'handoff_state',
|
|
6437
|
+
};
|
|
6438
|
+
}
|
|
6439
|
+
// Stamp validated handoff
|
|
6440
|
+
mergedMeta.handoff_state = handoffResult.data;
|
|
6441
|
+
}
|
|
6369
6442
|
// ── Cancel reason gate: require cancel_reason when transitioning to cancelled ──
|
|
6370
6443
|
if (parsed.status === 'cancelled') {
|
|
6371
6444
|
const meta = (incomingMeta ?? {});
|
|
@@ -6818,20 +6891,24 @@ export async function createServer() {
|
|
|
6818
6891
|
if (task.assignee) {
|
|
6819
6892
|
if (parsed.status === 'done') {
|
|
6820
6893
|
presenceManager.recordActivity(task.assignee, 'task_completed');
|
|
6821
|
-
presenceManager.updatePresence(task.assignee, 'working');
|
|
6894
|
+
presenceManager.updatePresence(task.assignee, 'working', null);
|
|
6822
6895
|
trackTaskEvent('completed');
|
|
6823
6896
|
}
|
|
6824
6897
|
else if (parsed.status === 'doing') {
|
|
6825
|
-
presenceManager.updatePresence(task.assignee, 'working');
|
|
6898
|
+
presenceManager.updatePresence(task.assignee, 'working', task.id);
|
|
6826
6899
|
}
|
|
6827
6900
|
else if (parsed.status === 'blocked') {
|
|
6828
|
-
presenceManager.updatePresence(task.assignee, 'blocked');
|
|
6901
|
+
presenceManager.updatePresence(task.assignee, 'blocked', task.id);
|
|
6829
6902
|
}
|
|
6830
6903
|
else if (parsed.status === 'validating') {
|
|
6831
|
-
presenceManager.updatePresence(task.assignee, 'reviewing');
|
|
6904
|
+
presenceManager.updatePresence(task.assignee, 'reviewing', task.id);
|
|
6832
6905
|
}
|
|
6833
6906
|
}
|
|
6834
6907
|
// ── Reviewer notification: @mention reviewer when task enters validating ──
|
|
6908
|
+
// NOTE: A dedup_key is set here so the inline chat dedup guard suppresses
|
|
6909
|
+
// any duplicate reviewRequested send that may arrive via the statusNotifTargets
|
|
6910
|
+
// loop below for the same task+transition. Without it, two messages fire for
|
|
6911
|
+
// every todo→validating transition (this direct send + the loop send).
|
|
6835
6912
|
if (parsed.status === 'validating' && existing.status !== 'validating' && existing.reviewer) {
|
|
6836
6913
|
const taskMeta = task.metadata;
|
|
6837
6914
|
const prUrl = taskMeta?.review_handoff?.pr_url
|
|
@@ -6848,6 +6925,7 @@ export async function createServer() {
|
|
|
6848
6925
|
taskId: task.id,
|
|
6849
6926
|
reviewer: existing.reviewer,
|
|
6850
6927
|
prUrl: prUrl || undefined,
|
|
6928
|
+
dedup_key: `review-requested:${task.id}:${task.updatedAt}`,
|
|
6851
6929
|
},
|
|
6852
6930
|
}).catch(() => { }); // Non-blocking
|
|
6853
6931
|
}
|
|
@@ -6971,13 +7049,17 @@ export async function createServer() {
|
|
|
6971
7049
|
// Dedupe guard: prevent stale/out-of-order notification events
|
|
6972
7050
|
const { shouldEmitNotification } = await import('./notificationDedupeGuard.js');
|
|
6973
7051
|
for (const target of statusNotifTargets) {
|
|
6974
|
-
// Check dedupe guard before emitting
|
|
7052
|
+
// Check dedupe guard before emitting.
|
|
7053
|
+
// Pass targetAgent so each recipient gets an independent cursor — prevents
|
|
7054
|
+
// the first recipient's cursor update from suppressing later recipients for
|
|
7055
|
+
// the same event (e.g. assignee + reviewer both getting taskCompleted on 'done').
|
|
6975
7056
|
const dedupeCheck = shouldEmitNotification({
|
|
6976
7057
|
taskId: task.id,
|
|
6977
7058
|
eventUpdatedAt: task.updatedAt,
|
|
6978
7059
|
eventStatus: parsed.status,
|
|
6979
7060
|
currentTaskStatus: task.status,
|
|
6980
7061
|
currentTaskUpdatedAt: task.updatedAt,
|
|
7062
|
+
targetAgent: target.agent,
|
|
6981
7063
|
});
|
|
6982
7064
|
if (!dedupeCheck.emit) {
|
|
6983
7065
|
console.log(`[NotifDedupe] Suppressed: ${dedupeCheck.reason}`);
|
|
@@ -6990,7 +7072,13 @@ export async function createServer() {
|
|
|
6990
7072
|
message: `Task ${task.id} → ${parsed.status}`,
|
|
6991
7073
|
});
|
|
6992
7074
|
if (routing.shouldNotify) {
|
|
6993
|
-
// Route through inbox/chat based on delivery method preference
|
|
7075
|
+
// Route through inbox/chat based on delivery method preference.
|
|
7076
|
+
// For reviewRequested, set a dedup_key matching the direct send above so the
|
|
7077
|
+
// inline chat dedup suppresses this copy (the direct send fires first with a
|
|
7078
|
+
// richer payload including PR URL and `to:` routing).
|
|
7079
|
+
const dedupKey = target.type === 'reviewRequested'
|
|
7080
|
+
? `review-requested:${task.id}:${task.updatedAt}`
|
|
7081
|
+
: undefined;
|
|
6994
7082
|
chatManager.sendMessage({
|
|
6995
7083
|
from: 'system',
|
|
6996
7084
|
content: `@${target.agent} [${target.type}:${task.id}] ${task.title} → ${parsed.status}`,
|
|
@@ -7001,6 +7089,7 @@ export async function createServer() {
|
|
|
7001
7089
|
status: parsed.status,
|
|
7002
7090
|
updatedAt: task.updatedAt,
|
|
7003
7091
|
deliveryMethod: routing.deliveryMethod,
|
|
7092
|
+
...(dedupKey ? { dedup_key: dedupKey } : {}),
|
|
7004
7093
|
},
|
|
7005
7094
|
}).catch(() => { }); // Non-blocking
|
|
7006
7095
|
}
|
|
@@ -7071,6 +7160,27 @@ export async function createServer() {
|
|
|
7071
7160
|
};
|
|
7072
7161
|
app.get('/agents', async () => buildRoleRegistryPayload());
|
|
7073
7162
|
app.get('/agents/roles', async () => buildRoleRegistryPayload());
|
|
7163
|
+
// Host-native identity resolution — resolves agent by name, alias, or display name
|
|
7164
|
+
// without requiring the OpenClaw gateway. Merges YAML roles + agent_config table.
|
|
7165
|
+
app.get('/agents/:name/identity', async (request) => {
|
|
7166
|
+
const { name } = request.params;
|
|
7167
|
+
const resolved = resolveAgentMention(name);
|
|
7168
|
+
const role = resolved ? getAgentRole(resolved) : getAgentRole(name);
|
|
7169
|
+
if (!role) {
|
|
7170
|
+
return { found: false, query: name, hint: 'Agent not found in YAML roles or config' };
|
|
7171
|
+
}
|
|
7172
|
+
return {
|
|
7173
|
+
found: true,
|
|
7174
|
+
agentId: role.name,
|
|
7175
|
+
displayName: role.displayName ?? role.name,
|
|
7176
|
+
role: role.role,
|
|
7177
|
+
description: role.description ?? null,
|
|
7178
|
+
aliases: role.aliases ?? [],
|
|
7179
|
+
affinityTags: role.affinityTags ?? [],
|
|
7180
|
+
wipCap: role.wipCap,
|
|
7181
|
+
source: 'yaml',
|
|
7182
|
+
};
|
|
7183
|
+
});
|
|
7074
7184
|
// Team-scoped alias for assignment-engine consumers
|
|
7075
7185
|
app.get('/team/roles', async () => {
|
|
7076
7186
|
const payload = buildRoleRegistryPayload();
|
|
@@ -7494,55 +7604,7 @@ export async function createServer() {
|
|
|
7494
7604
|
};
|
|
7495
7605
|
});
|
|
7496
7606
|
// ── Approval Queue ──────────────────────────────────────────────────
|
|
7497
|
-
|
|
7498
|
-
// Tasks in 'todo' that were auto-assigned (have suggestedAgent in metadata) or need assignment review
|
|
7499
|
-
const allTasks = taskManager.listTasks({});
|
|
7500
|
-
const todoTasks = allTasks.filter(t => t.status === 'todo');
|
|
7501
|
-
const items = todoTasks.map(t => {
|
|
7502
|
-
const task = t;
|
|
7503
|
-
const meta = task.metadata || {};
|
|
7504
|
-
const title = task.title || '';
|
|
7505
|
-
const tags = Array.isArray(task.tags) ? task.tags : [];
|
|
7506
|
-
const doneCriteria = Array.isArray(task.done_criteria) ? task.done_criteria : [];
|
|
7507
|
-
// Score all agents for this task
|
|
7508
|
-
const roles = getAgentRoles();
|
|
7509
|
-
const agentOptions = roles.map(agent => {
|
|
7510
|
-
const wipCount = allTasks.filter(at => at.status === 'doing' && (at.assignee || '').toLowerCase() === agent.name).length;
|
|
7511
|
-
const s = scoreAssignment(agent, { title, tags, done_criteria: doneCriteria }, wipCount);
|
|
7512
|
-
return {
|
|
7513
|
-
agentId: agent.name,
|
|
7514
|
-
name: agent.name,
|
|
7515
|
-
confidenceScore: Math.max(0, Math.min(1, s.score)),
|
|
7516
|
-
affinityTags: agent.affinityTags,
|
|
7517
|
-
};
|
|
7518
|
-
}).sort((a, b) => b.confidenceScore - a.confidenceScore);
|
|
7519
|
-
const topAgent = agentOptions[0];
|
|
7520
|
-
const suggestedAgent = task.assignee || topAgent?.agentId || null;
|
|
7521
|
-
const confidenceScore = topAgent?.confidenceScore || 0;
|
|
7522
|
-
const confidenceReason = topAgent && topAgent.confidenceScore > 0
|
|
7523
|
-
? `${topAgent.name}: affinity match on ${topAgent.affinityTags.slice(0, 3).join(', ')}`
|
|
7524
|
-
: 'No strong affinity match';
|
|
7525
|
-
return {
|
|
7526
|
-
taskId: task.id,
|
|
7527
|
-
title,
|
|
7528
|
-
description: task.description || '',
|
|
7529
|
-
priority: task.priority || 'P3',
|
|
7530
|
-
suggestedAgent,
|
|
7531
|
-
confidenceScore,
|
|
7532
|
-
confidenceReason,
|
|
7533
|
-
agentOptions,
|
|
7534
|
-
status: 'pending',
|
|
7535
|
-
};
|
|
7536
|
-
});
|
|
7537
|
-
const highConfidence = items.filter(i => i.confidenceScore >= 0.85);
|
|
7538
|
-
const needsReview = items.filter(i => i.confidenceScore < 0.85);
|
|
7539
|
-
return {
|
|
7540
|
-
items: [...highConfidence, ...needsReview],
|
|
7541
|
-
total: items.length,
|
|
7542
|
-
highConfidenceCount: highConfidence.length,
|
|
7543
|
-
needsReviewCount: needsReview.length,
|
|
7544
|
-
};
|
|
7545
|
-
});
|
|
7607
|
+
// Note: GET /approval-queue is defined below near /approval-queue/:approvalId/decide
|
|
7546
7608
|
app.post('/approval-queue/:taskId/approve', async (request, reply) => {
|
|
7547
7609
|
const body = request.body;
|
|
7548
7610
|
const taskId = request.params.taskId;
|
|
@@ -7698,6 +7760,88 @@ export async function createServer() {
|
|
|
7698
7760
|
warnings: result.warnings,
|
|
7699
7761
|
};
|
|
7700
7762
|
});
|
|
7763
|
+
// ── Presence Layer canvas state ─────────────────────────────────────
|
|
7764
|
+
// Agent emits canvas_render state transitions for the Presence Layer.
|
|
7765
|
+
// Deterministic event types. No "AI can emit anything" protocol.
|
|
7766
|
+
const CANVAS_STATES = ['floor', 'listening', 'thinking', 'rendering', 'ambient', 'decision', 'urgent', 'handoff'];
|
|
7767
|
+
const SENSOR_VALUES = [null, 'mic', 'camera', 'mic+camera'];
|
|
7768
|
+
const CanvasRenderSchema = z.object({
|
|
7769
|
+
state: z.enum(CANVAS_STATES),
|
|
7770
|
+
sensors: z.enum(['mic', 'camera', 'mic+camera']).nullable().default(null),
|
|
7771
|
+
agentId: z.string().min(1),
|
|
7772
|
+
payload: z.object({
|
|
7773
|
+
text: z.string().optional(),
|
|
7774
|
+
media: z.unknown().optional(),
|
|
7775
|
+
decision: z.object({
|
|
7776
|
+
question: z.string(),
|
|
7777
|
+
context: z.string().optional(),
|
|
7778
|
+
decisionId: z.string(),
|
|
7779
|
+
expiresAt: z.number().optional(),
|
|
7780
|
+
autoAction: z.string().optional(),
|
|
7781
|
+
}).optional(),
|
|
7782
|
+
agents: z.array(z.object({
|
|
7783
|
+
name: z.string(),
|
|
7784
|
+
state: z.string(),
|
|
7785
|
+
task: z.string().optional(),
|
|
7786
|
+
})).optional(),
|
|
7787
|
+
summary: z.object({
|
|
7788
|
+
headline: z.string(),
|
|
7789
|
+
items: z.array(z.string()).optional(),
|
|
7790
|
+
cost: z.string().optional(),
|
|
7791
|
+
duration: z.string().optional(),
|
|
7792
|
+
}).optional(),
|
|
7793
|
+
}).default({}),
|
|
7794
|
+
});
|
|
7795
|
+
// Current state per agent — in-memory, not persisted
|
|
7796
|
+
const canvasStateMap = new Map();
|
|
7797
|
+
// POST /canvas/state — agent emits a state transition
|
|
7798
|
+
app.post('/canvas/state', async (request, reply) => {
|
|
7799
|
+
const result = CanvasRenderSchema.safeParse(request.body);
|
|
7800
|
+
if (!result.success) {
|
|
7801
|
+
reply.code(422);
|
|
7802
|
+
return {
|
|
7803
|
+
error: `Invalid canvas state: ${result.error.issues.map(i => i.message).join(', ')}`,
|
|
7804
|
+
hint: `state must be one of: ${CANVAS_STATES.join(', ')}`,
|
|
7805
|
+
validStates: CANVAS_STATES,
|
|
7806
|
+
};
|
|
7807
|
+
}
|
|
7808
|
+
const { state, sensors, agentId, payload } = result.data;
|
|
7809
|
+
const now = Date.now();
|
|
7810
|
+
// Store current state
|
|
7811
|
+
canvasStateMap.set(agentId, { state, sensors, payload, updatedAt: now });
|
|
7812
|
+
// Emit canvas_render event over SSE
|
|
7813
|
+
eventBus.emit({
|
|
7814
|
+
id: `crender-${now}-${Math.random().toString(36).slice(2, 8)}`,
|
|
7815
|
+
type: 'canvas_render',
|
|
7816
|
+
timestamp: now,
|
|
7817
|
+
data: { state, sensors, agentId, payload },
|
|
7818
|
+
});
|
|
7819
|
+
return { success: true, state, agentId, timestamp: now };
|
|
7820
|
+
});
|
|
7821
|
+
// GET /canvas/state — current state for all agents (or one)
|
|
7822
|
+
app.get('/canvas/state', async (request) => {
|
|
7823
|
+
const query = request.query;
|
|
7824
|
+
if (query.agentId) {
|
|
7825
|
+
const entry = canvasStateMap.get(query.agentId);
|
|
7826
|
+
return entry ?? { state: 'floor', sensors: null, payload: {}, updatedAt: null };
|
|
7827
|
+
}
|
|
7828
|
+
const all = {};
|
|
7829
|
+
for (const [id, entry] of canvasStateMap) {
|
|
7830
|
+
all[id] = entry;
|
|
7831
|
+
}
|
|
7832
|
+
return { agents: all, count: canvasStateMap.size };
|
|
7833
|
+
});
|
|
7834
|
+
// GET /canvas/states — valid state + sensor values (discovery)
|
|
7835
|
+
app.get('/canvas/states', async () => ({
|
|
7836
|
+
states: CANVAS_STATES,
|
|
7837
|
+
sensors: SENSOR_VALUES,
|
|
7838
|
+
schema: {
|
|
7839
|
+
state: 'floor | listening | thinking | rendering | ambient | decision | urgent | handoff',
|
|
7840
|
+
sensors: 'null | mic | camera | mic+camera (non-dismissable trust indicator)',
|
|
7841
|
+
agentId: 'required — which agent is driving the canvas',
|
|
7842
|
+
payload: 'optional — text, media, decision, agents, summary',
|
|
7843
|
+
},
|
|
7844
|
+
}));
|
|
7701
7845
|
// GET /canvas/slots — current active slots
|
|
7702
7846
|
app.get('/canvas/slots', async () => {
|
|
7703
7847
|
return {
|
|
@@ -9272,6 +9416,26 @@ export async function createServer() {
|
|
|
9272
9416
|
const allDrops = chatManager.getDropStats();
|
|
9273
9417
|
const agentDrops = allDrops[agent];
|
|
9274
9418
|
const focusSummary = getFocusSummary();
|
|
9419
|
+
// Boot context: recent memories + active run (survives restart)
|
|
9420
|
+
let bootMemories = [];
|
|
9421
|
+
let activeRun = null;
|
|
9422
|
+
try {
|
|
9423
|
+
const { listMemories } = await import('./agent-memories.js');
|
|
9424
|
+
const memories = listMemories({ agentId: agent, limit: 5 });
|
|
9425
|
+
bootMemories = memories.map(m => ({
|
|
9426
|
+
key: m.key, content: m.content.slice(0, 200),
|
|
9427
|
+
namespace: m.namespace, updatedAt: m.updatedAt,
|
|
9428
|
+
}));
|
|
9429
|
+
}
|
|
9430
|
+
catch { /* agent-memories not available */ }
|
|
9431
|
+
try {
|
|
9432
|
+
const { getActiveAgentRun } = await import('./agent-runs.js');
|
|
9433
|
+
const run = getActiveAgentRun(agent, 'default');
|
|
9434
|
+
if (run) {
|
|
9435
|
+
activeRun = { id: run.id, objective: run.objective, status: run.status, startedAt: run.startedAt };
|
|
9436
|
+
}
|
|
9437
|
+
}
|
|
9438
|
+
catch { /* agent-runs not available */ }
|
|
9275
9439
|
return {
|
|
9276
9440
|
agent, ts: Date.now(),
|
|
9277
9441
|
active: slim(activeTask), next: pauseStatus.paused ? null : slim(nextTask),
|
|
@@ -9281,6 +9445,12 @@ export async function createServer() {
|
|
|
9281
9445
|
...(focusSummary ? { focus: focusSummary } : {}),
|
|
9282
9446
|
...(agentDrops ? { drops: { total: agentDrops.total, rolling_1h: agentDrops.rolling_1h } } : {}),
|
|
9283
9447
|
...(pauseStatus.paused ? { paused: true, pauseMessage: pauseStatus.message, resumesAt: pauseStatus.entry?.pausedUntil ?? null } : {}),
|
|
9448
|
+
...(bootMemories.length > 0 ? { memories: bootMemories } : {}),
|
|
9449
|
+
...(activeRun ? { run: activeRun } : {}),
|
|
9450
|
+
...(() => {
|
|
9451
|
+
const p = presenceManager.getAllPresence().find(p => p.agent === agent);
|
|
9452
|
+
return p?.waiting ? { waiting: p.waiting } : {};
|
|
9453
|
+
})(),
|
|
9284
9454
|
action: pauseStatus.paused ? `PAUSED: ${pauseStatus.message}`
|
|
9285
9455
|
: activeTask ? `Continue ${activeTask.id}`
|
|
9286
9456
|
: nextTask ? `Claim ${nextTask.id}`
|
|
@@ -9288,6 +9458,21 @@ export async function createServer() {
|
|
|
9288
9458
|
: 'HEARTBEAT_OK',
|
|
9289
9459
|
};
|
|
9290
9460
|
});
|
|
9461
|
+
// ── Agent Waiting State ──────────────────────────────────────────────
|
|
9462
|
+
// Agents signal they're blocked on human input. Shows in heartbeat + presence.
|
|
9463
|
+
app.post('/agents/:agent/waiting', async (request, reply) => {
|
|
9464
|
+
const agent = String(request.params.agent || '').trim().toLowerCase();
|
|
9465
|
+
const body = request.body ?? {};
|
|
9466
|
+
if (!body.reason)
|
|
9467
|
+
return reply.code(400).send({ error: 'reason is required' });
|
|
9468
|
+
presenceManager.setWaiting(agent, { reason: body.reason, waitingFor: body.waitingFor, taskId: body.taskId, expiresAt: body.expiresAt });
|
|
9469
|
+
return { success: true, agent, status: 'waiting', waiting: { reason: body.reason, waitingFor: body.waitingFor, taskId: body.taskId, expiresAt: body.expiresAt } };
|
|
9470
|
+
});
|
|
9471
|
+
app.delete('/agents/:agent/waiting', async (request) => {
|
|
9472
|
+
const agent = String(request.params.agent || '').trim().toLowerCase();
|
|
9473
|
+
presenceManager.clearWaiting(agent);
|
|
9474
|
+
return { success: true, agent, status: 'idle' };
|
|
9475
|
+
});
|
|
9291
9476
|
// ── Bootstrap: dynamic agent config generation ──────────────────────
|
|
9292
9477
|
app.get('/bootstrap/heartbeat/:agent', async (request) => {
|
|
9293
9478
|
const agent = String(request.params.agent || '').trim().toLowerCase();
|
|
@@ -9351,6 +9536,7 @@ If your heartbeat shows **no active task** and **no next task**:
|
|
|
9351
9536
|
- Do not load full chat history.
|
|
9352
9537
|
- Do not post plan-only updates.
|
|
9353
9538
|
- If nothing changed and no direct action is required, reply \`HEARTBEAT_OK\`.
|
|
9539
|
+
- **Decision authority:** Team owns product/arch/process decisions. Escalate credentials, legal, and vision decisions to the admin/owner. See \`decision_authority\` block in \`defaults/TEAM-ROLES.yaml\` for the full list.
|
|
9354
9540
|
`;
|
|
9355
9541
|
// Stable hash for change detection (agents can cache and compare)
|
|
9356
9542
|
const { createHash } = await import('node:crypto');
|
|
@@ -10455,17 +10641,12 @@ If your heartbeat shows **no active task** and **no next task**:
|
|
|
10455
10641
|
return { success: false, error: 'agent and model are required' };
|
|
10456
10642
|
}
|
|
10457
10643
|
const event = recordUsage({
|
|
10458
|
-
|
|
10459
|
-
task_id: body.task_id,
|
|
10644
|
+
agentId: body.agent,
|
|
10460
10645
|
model: body.model,
|
|
10461
|
-
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
estimated_cost_usd: body.estimated_cost_usd != null ? Number(body.estimated_cost_usd) : undefined,
|
|
10465
|
-
category: body.category || 'other',
|
|
10646
|
+
inputTokens: Number(body.input_tokens) || 0,
|
|
10647
|
+
outputTokens: Number(body.output_tokens) || 0,
|
|
10648
|
+
cost: body.estimated_cost_usd != null ? Number(body.estimated_cost_usd) : 0,
|
|
10466
10649
|
timestamp: Number(body.timestamp) || Date.now(),
|
|
10467
|
-
team_id: body.team_id,
|
|
10468
|
-
metadata: body.metadata,
|
|
10469
10650
|
});
|
|
10470
10651
|
return { success: true, event };
|
|
10471
10652
|
});
|
|
@@ -11027,6 +11208,21 @@ If your heartbeat shows **no active task** and **no next task**:
|
|
|
11027
11208
|
});
|
|
11028
11209
|
events.push(event);
|
|
11029
11210
|
}
|
|
11211
|
+
// Post GitHub events to the 'github' chat channel with remapped mentions.
|
|
11212
|
+
// Pass enrichedBody (not body) so formatGitHubEvent has access to
|
|
11213
|
+
// _reflectt_attribution and can mention the correct agent (@link not @kai).
|
|
11214
|
+
if (provider === 'github') {
|
|
11215
|
+
const ghEventType = request.headers['x-github-event'] || eventType;
|
|
11216
|
+
const chatMessage = formatGitHubEvent(ghEventType, enrichedBody);
|
|
11217
|
+
if (chatMessage) {
|
|
11218
|
+
chatManager.sendMessage({
|
|
11219
|
+
from: 'github',
|
|
11220
|
+
content: chatMessage,
|
|
11221
|
+
channel: 'github',
|
|
11222
|
+
metadata: { source: 'github-webhook', eventType: ghEventType, delivery: request.headers['x-github-delivery'] },
|
|
11223
|
+
}).catch(() => { }); // non-blocking
|
|
11224
|
+
}
|
|
11225
|
+
}
|
|
11030
11226
|
reply.code(202);
|
|
11031
11227
|
return { success: true, accepted: events.length, events: events.map(e => ({ id: e.id, idempotencyKey: e.idempotencyKey, status: e.status })) };
|
|
11032
11228
|
});
|
|
@@ -11989,6 +12185,964 @@ If your heartbeat shows **no active task** and **no next task**:
|
|
|
11989
12185
|
});
|
|
11990
12186
|
// Start hourly auto-snapshot for alert-preflight daily metrics
|
|
11991
12187
|
startAutoSnapshot();
|
|
12188
|
+
// ─── Browser capability routes ───────────────────────────────────────────────
|
|
12189
|
+
const browser = await import('./capabilities/browser.js');
|
|
12190
|
+
app.get('/browser/config', async () => {
|
|
12191
|
+
return browser.getBrowserConfig();
|
|
12192
|
+
});
|
|
12193
|
+
app.post('/browser/sessions', async (request, reply) => {
|
|
12194
|
+
try {
|
|
12195
|
+
const body = request.body;
|
|
12196
|
+
if (!body?.agent)
|
|
12197
|
+
return reply.code(400).send({ error: 'agent is required' });
|
|
12198
|
+
const session = await browser.createSession({
|
|
12199
|
+
agent: body.agent,
|
|
12200
|
+
url: body.url,
|
|
12201
|
+
headless: body.headless,
|
|
12202
|
+
viewport: body.viewport,
|
|
12203
|
+
});
|
|
12204
|
+
const { _stagehand, _page, _idleTimer, ...safe } = session;
|
|
12205
|
+
return reply.code(201).send(safe);
|
|
12206
|
+
}
|
|
12207
|
+
catch (err) {
|
|
12208
|
+
const status = err.message?.includes('Max concurrent') || err.message?.includes('exceeded max') ? 429 : 500;
|
|
12209
|
+
return reply.code(status).send({ error: err.message });
|
|
12210
|
+
}
|
|
12211
|
+
});
|
|
12212
|
+
app.get('/browser/sessions', async () => {
|
|
12213
|
+
return { sessions: browser.listSessions() };
|
|
12214
|
+
});
|
|
12215
|
+
app.get('/browser/sessions/:id', async (request, reply) => {
|
|
12216
|
+
const session = browser.getSession(request.params.id);
|
|
12217
|
+
if (!session)
|
|
12218
|
+
return reply.code(404).send({ error: 'Session not found' });
|
|
12219
|
+
const { _stagehand, _page, _idleTimer, ...safe } = session;
|
|
12220
|
+
return safe;
|
|
12221
|
+
});
|
|
12222
|
+
app.delete('/browser/sessions/:id', async (request, reply) => {
|
|
12223
|
+
await browser.closeSession(request.params.id);
|
|
12224
|
+
return { ok: true };
|
|
12225
|
+
});
|
|
12226
|
+
app.post('/browser/sessions/:id/act', async (request, reply) => {
|
|
12227
|
+
try {
|
|
12228
|
+
const body = request.body;
|
|
12229
|
+
if (!body?.instruction)
|
|
12230
|
+
return reply.code(400).send({ error: 'instruction is required' });
|
|
12231
|
+
const result = await browser.act(request.params.id, body.instruction);
|
|
12232
|
+
return result;
|
|
12233
|
+
}
|
|
12234
|
+
catch (err) {
|
|
12235
|
+
return reply.code(err.message?.includes('No active') ? 404 : 500).send({ error: err.message });
|
|
12236
|
+
}
|
|
12237
|
+
});
|
|
12238
|
+
app.post('/browser/sessions/:id/extract', async (request, reply) => {
|
|
12239
|
+
try {
|
|
12240
|
+
const body = request.body;
|
|
12241
|
+
if (!body?.instruction)
|
|
12242
|
+
return reply.code(400).send({ error: 'instruction is required' });
|
|
12243
|
+
const result = await browser.extract(request.params.id, body.instruction, body.schema);
|
|
12244
|
+
return result;
|
|
12245
|
+
}
|
|
12246
|
+
catch (err) {
|
|
12247
|
+
return reply.code(err.message?.includes('No active') ? 404 : 500).send({ error: err.message });
|
|
12248
|
+
}
|
|
12249
|
+
});
|
|
12250
|
+
app.post('/browser/sessions/:id/observe', async (request, reply) => {
|
|
12251
|
+
try {
|
|
12252
|
+
const body = request.body;
|
|
12253
|
+
if (!body?.instruction)
|
|
12254
|
+
return reply.code(400).send({ error: 'instruction is required' });
|
|
12255
|
+
const result = await browser.observe(request.params.id, body.instruction);
|
|
12256
|
+
return result;
|
|
12257
|
+
}
|
|
12258
|
+
catch (err) {
|
|
12259
|
+
return reply.code(err.message?.includes('No active') ? 404 : 500).send({ error: err.message });
|
|
12260
|
+
}
|
|
12261
|
+
});
|
|
12262
|
+
app.post('/browser/sessions/:id/navigate', async (request, reply) => {
|
|
12263
|
+
try {
|
|
12264
|
+
const body = request.body;
|
|
12265
|
+
if (!body?.url)
|
|
12266
|
+
return reply.code(400).send({ error: 'url is required' });
|
|
12267
|
+
const result = await browser.navigate(request.params.id, body.url);
|
|
12268
|
+
return result;
|
|
12269
|
+
}
|
|
12270
|
+
catch (err) {
|
|
12271
|
+
return reply.code(err.message?.includes('No active') ? 404 : 500).send({ error: err.message });
|
|
12272
|
+
}
|
|
12273
|
+
});
|
|
12274
|
+
app.get('/browser/sessions/:id/screenshot', async (request, reply) => {
|
|
12275
|
+
try {
|
|
12276
|
+
const result = await browser.screenshot(request.params.id);
|
|
12277
|
+
return result;
|
|
12278
|
+
}
|
|
12279
|
+
catch (err) {
|
|
12280
|
+
return reply.code(err.message?.includes('No active') ? 404 : 500).send({ error: err.message });
|
|
12281
|
+
}
|
|
12282
|
+
});
|
|
12283
|
+
// ── Agent Runs & Events ──────────────────────────────────────────────────
|
|
12284
|
+
const { createAgentRun, updateAgentRun, getAgentRun, getActiveAgentRun, listAgentRuns, appendAgentEvent, listAgentEvents, VALID_RUN_STATUSES, } = await import('./agent-runs.js');
|
|
12285
|
+
// Create a new agent run
|
|
12286
|
+
app.post('/agents/:agentId/runs', async (request, reply) => {
|
|
12287
|
+
const { agentId } = request.params;
|
|
12288
|
+
const body = request.body;
|
|
12289
|
+
if (!body?.objective)
|
|
12290
|
+
return reply.code(400).send({ error: 'objective is required' });
|
|
12291
|
+
const teamId = body.teamId ?? 'default';
|
|
12292
|
+
try {
|
|
12293
|
+
const run = createAgentRun(agentId, teamId, body.objective, {
|
|
12294
|
+
taskId: body.taskId,
|
|
12295
|
+
parentRunId: body.parentRunId,
|
|
12296
|
+
});
|
|
12297
|
+
return reply.code(201).send(run);
|
|
12298
|
+
}
|
|
12299
|
+
catch (err) {
|
|
12300
|
+
return reply.code(500).send({ error: err.message });
|
|
12301
|
+
}
|
|
12302
|
+
});
|
|
12303
|
+
// Update an agent run (status, context, artifacts)
|
|
12304
|
+
app.patch('/agents/:agentId/runs/:runId', async (request, reply) => {
|
|
12305
|
+
const { runId } = request.params;
|
|
12306
|
+
const body = request.body;
|
|
12307
|
+
if (body?.status && !VALID_RUN_STATUSES.includes(body.status)) {
|
|
12308
|
+
return reply.code(400).send({ error: `Invalid status. Valid: ${VALID_RUN_STATUSES.join(', ')}` });
|
|
12309
|
+
}
|
|
12310
|
+
try {
|
|
12311
|
+
const run = updateAgentRun(runId, {
|
|
12312
|
+
status: body?.status,
|
|
12313
|
+
contextSnapshot: body?.contextSnapshot,
|
|
12314
|
+
artifacts: body?.artifacts,
|
|
12315
|
+
});
|
|
12316
|
+
if (!run)
|
|
12317
|
+
return reply.code(404).send({ error: 'Run not found' });
|
|
12318
|
+
return run;
|
|
12319
|
+
}
|
|
12320
|
+
catch (err) {
|
|
12321
|
+
return reply.code(500).send({ error: err.message });
|
|
12322
|
+
}
|
|
12323
|
+
});
|
|
12324
|
+
// List agent runs
|
|
12325
|
+
app.get('/agents/:agentId/runs', async (request, reply) => {
|
|
12326
|
+
const { agentId } = request.params;
|
|
12327
|
+
const query = request.query;
|
|
12328
|
+
const teamId = query.teamId ?? 'default';
|
|
12329
|
+
const limit = query.limit ? parseInt(query.limit, 10) : undefined;
|
|
12330
|
+
return listAgentRuns(agentId, teamId, { status: query.status, limit });
|
|
12331
|
+
});
|
|
12332
|
+
// Get active run for an agent
|
|
12333
|
+
app.get('/agents/:agentId/runs/current', async (request, reply) => {
|
|
12334
|
+
const { agentId } = request.params;
|
|
12335
|
+
const query = request.query;
|
|
12336
|
+
const teamId = query.teamId ?? 'default';
|
|
12337
|
+
const run = getActiveAgentRun(agentId, teamId);
|
|
12338
|
+
if (!run)
|
|
12339
|
+
return reply.code(404).send({ error: 'No active run' });
|
|
12340
|
+
return run;
|
|
12341
|
+
});
|
|
12342
|
+
// Append an event
|
|
12343
|
+
const { validateRoutingSemantics } = await import('./agent-runs.js');
|
|
12344
|
+
// GET /events/routing/validate — check if a payload passes routing semantics
|
|
12345
|
+
app.post('/events/routing/validate', async (request) => {
|
|
12346
|
+
const body = request.body;
|
|
12347
|
+
if (!body?.eventType)
|
|
12348
|
+
return { valid: false, errors: ['eventType is required'], warnings: [] };
|
|
12349
|
+
return validateRoutingSemantics(body.eventType, body.payload ?? {});
|
|
12350
|
+
});
|
|
12351
|
+
app.post('/agents/:agentId/events', async (request, reply) => {
|
|
12352
|
+
const { agentId } = request.params;
|
|
12353
|
+
const body = request.body;
|
|
12354
|
+
if (!body?.eventType)
|
|
12355
|
+
return reply.code(400).send({ error: 'eventType is required' });
|
|
12356
|
+
try {
|
|
12357
|
+
const event = appendAgentEvent({
|
|
12358
|
+
agentId,
|
|
12359
|
+
runId: body.runId,
|
|
12360
|
+
eventType: body.eventType,
|
|
12361
|
+
payload: body.payload,
|
|
12362
|
+
enforceRouting: body.enforceRouting,
|
|
12363
|
+
});
|
|
12364
|
+
return reply.code(201).send(event);
|
|
12365
|
+
}
|
|
12366
|
+
catch (err) {
|
|
12367
|
+
if (err.message.includes('Routing semantics violation')) {
|
|
12368
|
+
return reply.code(422).send({ error: err.message, hint: 'Actionable events require: action_required (string), urgency (low|normal|high|critical), owner (string). Optional: expires_at (number).' });
|
|
12369
|
+
}
|
|
12370
|
+
return reply.code(500).send({ error: err.message });
|
|
12371
|
+
const message = String(err?.message || err);
|
|
12372
|
+
if (message.includes('rationale')) {
|
|
12373
|
+
return reply.code(400).send({ error: message });
|
|
12374
|
+
}
|
|
12375
|
+
return reply.code(500).send({ error: message });
|
|
12376
|
+
}
|
|
12377
|
+
});
|
|
12378
|
+
// List agent events
|
|
12379
|
+
app.get('/agents/:agentId/events', async (request, reply) => {
|
|
12380
|
+
const { agentId } = request.params;
|
|
12381
|
+
const query = request.query;
|
|
12382
|
+
return listAgentEvents({
|
|
12383
|
+
agentId,
|
|
12384
|
+
runId: query.runId,
|
|
12385
|
+
eventType: query.type,
|
|
12386
|
+
since: query.since ? parseInt(query.since, 10) : undefined,
|
|
12387
|
+
limit: query.limit ? parseInt(query.limit, 10) : undefined,
|
|
12388
|
+
});
|
|
12389
|
+
});
|
|
12390
|
+
// ── Run Event Stream (SSE) ─────────────────────────────────────────────
|
|
12391
|
+
// Real-time SSE stream for run events. Canvas subscribes here instead of polling.
|
|
12392
|
+
// GET /agents/:agentId/runs/:runId/stream — stream events for a specific run
|
|
12393
|
+
// GET /agents/:agentId/stream — stream all events for an agent
|
|
12394
|
+
app.get('/agents/:agentId/runs/:runId/stream', async (request, reply) => {
|
|
12395
|
+
const { agentId, runId } = request.params;
|
|
12396
|
+
const run = getAgentRun(runId);
|
|
12397
|
+
if (!run) {
|
|
12398
|
+
reply.code(404);
|
|
12399
|
+
return { error: 'Run not found' };
|
|
12400
|
+
}
|
|
12401
|
+
reply.raw.writeHead(200, {
|
|
12402
|
+
'Content-Type': 'text/event-stream',
|
|
12403
|
+
'Cache-Control': 'no-cache',
|
|
12404
|
+
'Connection': 'keep-alive',
|
|
12405
|
+
'X-Accel-Buffering': 'no',
|
|
12406
|
+
});
|
|
12407
|
+
// Send current run state as initial snapshot
|
|
12408
|
+
reply.raw.write(`event: snapshot\ndata: ${JSON.stringify({ run, events: listAgentEvents({ runId, limit: 20 }) })}\n\n`);
|
|
12409
|
+
// Subscribe to eventBus for this run's events
|
|
12410
|
+
const listenerId = `run-stream-${runId}-${Date.now()}`;
|
|
12411
|
+
let closed = false;
|
|
12412
|
+
eventBus.on(listenerId, (event) => {
|
|
12413
|
+
if (closed)
|
|
12414
|
+
return;
|
|
12415
|
+
const data = event.data;
|
|
12416
|
+
// Forward events that match this agent or run
|
|
12417
|
+
if (data && (data.runId === runId || data.agentId === agentId)) {
|
|
12418
|
+
try {
|
|
12419
|
+
reply.raw.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
12420
|
+
}
|
|
12421
|
+
catch { /* connection closed */ }
|
|
12422
|
+
}
|
|
12423
|
+
});
|
|
12424
|
+
// Heartbeat
|
|
12425
|
+
const heartbeat = setInterval(() => {
|
|
12426
|
+
if (closed) {
|
|
12427
|
+
clearInterval(heartbeat);
|
|
12428
|
+
return;
|
|
12429
|
+
}
|
|
12430
|
+
try {
|
|
12431
|
+
reply.raw.write(`:heartbeat\n\n`);
|
|
12432
|
+
}
|
|
12433
|
+
catch {
|
|
12434
|
+
clearInterval(heartbeat);
|
|
12435
|
+
}
|
|
12436
|
+
}, 15_000);
|
|
12437
|
+
// Cleanup
|
|
12438
|
+
request.raw.on('close', () => {
|
|
12439
|
+
closed = true;
|
|
12440
|
+
eventBus.off(listenerId);
|
|
12441
|
+
clearInterval(heartbeat);
|
|
12442
|
+
});
|
|
12443
|
+
});
|
|
12444
|
+
// Stream all events for an agent
|
|
12445
|
+
app.get('/agents/:agentId/stream', async (request, reply) => {
|
|
12446
|
+
const { agentId } = request.params;
|
|
12447
|
+
reply.raw.writeHead(200, {
|
|
12448
|
+
'Content-Type': 'text/event-stream',
|
|
12449
|
+
'Cache-Control': 'no-cache',
|
|
12450
|
+
'Connection': 'keep-alive',
|
|
12451
|
+
'X-Accel-Buffering': 'no',
|
|
12452
|
+
});
|
|
12453
|
+
// Send recent events as snapshot
|
|
12454
|
+
const recentEvents = listAgentEvents({ agentId, limit: 20 });
|
|
12455
|
+
const activeRun = getActiveAgentRun(agentId, 'default');
|
|
12456
|
+
reply.raw.write(`event: snapshot\ndata: ${JSON.stringify({ activeRun, events: recentEvents })}\n\n`);
|
|
12457
|
+
const listenerId = `agent-stream-${agentId}-${Date.now()}`;
|
|
12458
|
+
let closed = false;
|
|
12459
|
+
eventBus.on(listenerId, (event) => {
|
|
12460
|
+
if (closed)
|
|
12461
|
+
return;
|
|
12462
|
+
const data = event.data;
|
|
12463
|
+
if (data && data.agentId === agentId) {
|
|
12464
|
+
try {
|
|
12465
|
+
reply.raw.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
12466
|
+
}
|
|
12467
|
+
catch { /* connection closed */ }
|
|
12468
|
+
}
|
|
12469
|
+
});
|
|
12470
|
+
const heartbeat = setInterval(() => {
|
|
12471
|
+
if (closed) {
|
|
12472
|
+
clearInterval(heartbeat);
|
|
12473
|
+
return;
|
|
12474
|
+
}
|
|
12475
|
+
try {
|
|
12476
|
+
reply.raw.write(`:heartbeat\n\n`);
|
|
12477
|
+
}
|
|
12478
|
+
catch {
|
|
12479
|
+
clearInterval(heartbeat);
|
|
12480
|
+
}
|
|
12481
|
+
}, 15_000);
|
|
12482
|
+
request.raw.on('close', () => {
|
|
12483
|
+
closed = true;
|
|
12484
|
+
eventBus.off(listenerId);
|
|
12485
|
+
clearInterval(heartbeat);
|
|
12486
|
+
});
|
|
12487
|
+
});
|
|
12488
|
+
// ── Workflow Templates ─────────────────────────────────────────────────
|
|
12489
|
+
const { listWorkflowTemplates, getWorkflowTemplate, runWorkflow } = await import('./workflow-templates.js');
|
|
12490
|
+
// GET /workflows — list available workflow templates
|
|
12491
|
+
app.get('/workflows', async () => ({ templates: listWorkflowTemplates() }));
|
|
12492
|
+
// GET /workflows/:id — get template details
|
|
12493
|
+
app.get('/workflows/:id', async (request, reply) => {
|
|
12494
|
+
const template = getWorkflowTemplate(request.params.id);
|
|
12495
|
+
if (!template) {
|
|
12496
|
+
reply.code(404);
|
|
12497
|
+
return { error: 'Template not found' };
|
|
12498
|
+
}
|
|
12499
|
+
return {
|
|
12500
|
+
id: template.id,
|
|
12501
|
+
name: template.name,
|
|
12502
|
+
description: template.description,
|
|
12503
|
+
steps: template.steps.map(s => ({ name: s.name, description: s.description })),
|
|
12504
|
+
};
|
|
12505
|
+
});
|
|
12506
|
+
// POST /workflows/:id/run — execute a workflow
|
|
12507
|
+
app.post('/workflows/:id/run', async (request, reply) => {
|
|
12508
|
+
const template = getWorkflowTemplate(request.params.id);
|
|
12509
|
+
if (!template) {
|
|
12510
|
+
reply.code(404);
|
|
12511
|
+
return { error: 'Template not found' };
|
|
12512
|
+
}
|
|
12513
|
+
const body = request.body ?? {};
|
|
12514
|
+
const agentId = body.agentId ?? 'link';
|
|
12515
|
+
const teamId = body.teamId ?? 'default';
|
|
12516
|
+
const result = await runWorkflow(template, agentId, teamId, body);
|
|
12517
|
+
return result;
|
|
12518
|
+
});
|
|
12519
|
+
// ── Agent Messaging (Host-native) ─────────────────────────────────────
|
|
12520
|
+
// Local agent-to-agent messaging. Replaces gateway for same-Host agents.
|
|
12521
|
+
const { sendAgentMessage, listAgentMessages, listSentMessages, markMessagesRead, getUnreadCount, listChannelMessages } = await import('./agent-messaging.js');
|
|
12522
|
+
// Send message
|
|
12523
|
+
app.post('/agents/:agentId/messages/send', async (request, reply) => {
|
|
12524
|
+
const { agentId } = request.params;
|
|
12525
|
+
const body = request.body;
|
|
12526
|
+
if (!body?.to)
|
|
12527
|
+
return reply.code(400).send({ error: 'to (recipient agent) is required' });
|
|
12528
|
+
if (!body?.content)
|
|
12529
|
+
return reply.code(400).send({ error: 'content is required' });
|
|
12530
|
+
const msg = sendAgentMessage({
|
|
12531
|
+
fromAgent: agentId,
|
|
12532
|
+
toAgent: body.to,
|
|
12533
|
+
channel: body.channel,
|
|
12534
|
+
content: body.content,
|
|
12535
|
+
metadata: body.metadata,
|
|
12536
|
+
});
|
|
12537
|
+
// Emit event for SSE subscribers
|
|
12538
|
+
eventBus.emit({
|
|
12539
|
+
id: `amsg-evt-${Date.now()}`,
|
|
12540
|
+
type: 'message_posted',
|
|
12541
|
+
timestamp: Date.now(),
|
|
12542
|
+
data: { messageId: msg.id, from: agentId, to: body.to, channel: msg.channel },
|
|
12543
|
+
});
|
|
12544
|
+
return reply.code(201).send(msg);
|
|
12545
|
+
});
|
|
12546
|
+
// Inbox
|
|
12547
|
+
app.get('/agents/:agentId/messages', async (request) => {
|
|
12548
|
+
const { agentId } = request.params;
|
|
12549
|
+
const query = request.query;
|
|
12550
|
+
return {
|
|
12551
|
+
messages: listAgentMessages({
|
|
12552
|
+
agentId,
|
|
12553
|
+
channel: query.channel,
|
|
12554
|
+
unreadOnly: query.unread === 'true',
|
|
12555
|
+
since: query.since ? parseInt(query.since, 10) : undefined,
|
|
12556
|
+
limit: query.limit ? parseInt(query.limit, 10) : undefined,
|
|
12557
|
+
}),
|
|
12558
|
+
unreadCount: getUnreadCount(agentId),
|
|
12559
|
+
};
|
|
12560
|
+
});
|
|
12561
|
+
// Sent
|
|
12562
|
+
app.get('/agents/:agentId/messages/sent', async (request) => {
|
|
12563
|
+
const { agentId } = request.params;
|
|
12564
|
+
const query = request.query;
|
|
12565
|
+
return { messages: listSentMessages(agentId, query.limit ? parseInt(query.limit, 10) : undefined) };
|
|
12566
|
+
});
|
|
12567
|
+
// Mark read
|
|
12568
|
+
app.post('/agents/:agentId/messages/read', async (request) => {
|
|
12569
|
+
const { agentId } = request.params;
|
|
12570
|
+
const body = request.body ?? {};
|
|
12571
|
+
const marked = markMessagesRead(agentId, body.messageIds);
|
|
12572
|
+
return { marked };
|
|
12573
|
+
});
|
|
12574
|
+
// Channel messages
|
|
12575
|
+
app.get('/messages/channel/:channel', async (request) => {
|
|
12576
|
+
const { channel } = request.params;
|
|
12577
|
+
const query = request.query;
|
|
12578
|
+
return {
|
|
12579
|
+
messages: listChannelMessages(channel, {
|
|
12580
|
+
since: query.since ? parseInt(query.since, 10) : undefined,
|
|
12581
|
+
limit: query.limit ? parseInt(query.limit, 10) : undefined,
|
|
12582
|
+
}),
|
|
12583
|
+
};
|
|
12584
|
+
});
|
|
12585
|
+
// ── Run Retention / Archive ────────────────────────────────────────────
|
|
12586
|
+
const { applyRunRetention, getRetentionStats } = await import('./agent-runs.js');
|
|
12587
|
+
// GET /runs/retention/stats — preview what retention policy would do
|
|
12588
|
+
app.get('/runs/retention/stats', async (request) => {
|
|
12589
|
+
const query = request.query;
|
|
12590
|
+
return getRetentionStats({
|
|
12591
|
+
maxAgeDays: query.maxAgeDays ? parseInt(query.maxAgeDays, 10) : undefined,
|
|
12592
|
+
maxCompletedRuns: query.maxCompletedRuns ? parseInt(query.maxCompletedRuns, 10) : undefined,
|
|
12593
|
+
});
|
|
12594
|
+
});
|
|
12595
|
+
// POST /runs/retention/apply — apply retention policy
|
|
12596
|
+
app.post('/runs/retention/apply', async (request) => {
|
|
12597
|
+
const body = request.body ?? {};
|
|
12598
|
+
return applyRunRetention({
|
|
12599
|
+
policy: {
|
|
12600
|
+
maxAgeDays: body.maxAgeDays,
|
|
12601
|
+
maxCompletedRuns: body.maxCompletedRuns,
|
|
12602
|
+
deleteArchived: body.deleteArchived,
|
|
12603
|
+
},
|
|
12604
|
+
agentId: body.agentId,
|
|
12605
|
+
dryRun: body.dryRun,
|
|
12606
|
+
});
|
|
12607
|
+
});
|
|
12608
|
+
// ── Artifact Store (Host-native) ──────────────────────────────────────
|
|
12609
|
+
const { storeArtifact, getArtifact, readArtifactContent, listArtifacts, deleteArtifact, getStorageUsage } = await import('./artifact-store.js');
|
|
12610
|
+
// Upload artifact
|
|
12611
|
+
app.post('/agents/:agentId/artifacts', async (request, reply) => {
|
|
12612
|
+
const { agentId } = request.params;
|
|
12613
|
+
const body = request.body;
|
|
12614
|
+
if (!body?.name)
|
|
12615
|
+
return reply.code(400).send({ error: 'name is required' });
|
|
12616
|
+
if (!body?.content)
|
|
12617
|
+
return reply.code(400).send({ error: 'content is required' });
|
|
12618
|
+
const contentBuf = body.encoding === 'base64' ? Buffer.from(body.content, 'base64') : Buffer.from(body.content);
|
|
12619
|
+
const art = storeArtifact({ agentId, name: body.name, content: contentBuf, mimeType: body.mimeType, runId: body.runId, taskId: body.taskId, metadata: body.metadata });
|
|
12620
|
+
return reply.code(201).send(art);
|
|
12621
|
+
});
|
|
12622
|
+
// List artifacts
|
|
12623
|
+
app.get('/agents/:agentId/artifacts', async (request) => {
|
|
12624
|
+
const { agentId } = request.params;
|
|
12625
|
+
const query = request.query;
|
|
12626
|
+
return {
|
|
12627
|
+
artifacts: listArtifacts({ agentId, runId: query.runId, taskId: query.taskId, limit: query.limit ? parseInt(query.limit, 10) : undefined }),
|
|
12628
|
+
usage: getStorageUsage(agentId),
|
|
12629
|
+
};
|
|
12630
|
+
});
|
|
12631
|
+
// Get artifact metadata
|
|
12632
|
+
app.get('/artifacts/:artifactId', async (request, reply) => {
|
|
12633
|
+
const { artifactId } = request.params;
|
|
12634
|
+
const art = getArtifact(artifactId);
|
|
12635
|
+
if (!art)
|
|
12636
|
+
return reply.code(404).send({ error: 'Artifact not found' });
|
|
12637
|
+
return art;
|
|
12638
|
+
});
|
|
12639
|
+
// Download artifact content
|
|
12640
|
+
app.get('/artifacts/:artifactId/content', async (request, reply) => {
|
|
12641
|
+
const { artifactId } = request.params;
|
|
12642
|
+
const content = readArtifactContent(artifactId);
|
|
12643
|
+
if (!content)
|
|
12644
|
+
return reply.code(404).send({ error: 'Artifact not found or file missing' });
|
|
12645
|
+
const art = getArtifact(artifactId);
|
|
12646
|
+
return reply.type(art.mimeType).send(content);
|
|
12647
|
+
});
|
|
12648
|
+
// Delete artifact
|
|
12649
|
+
app.delete('/artifacts/:artifactId', async (request, reply) => {
|
|
12650
|
+
const { artifactId } = request.params;
|
|
12651
|
+
const deleted = deleteArtifact(artifactId);
|
|
12652
|
+
if (!deleted)
|
|
12653
|
+
return reply.code(404).send({ error: 'Artifact not found' });
|
|
12654
|
+
return { deleted: true };
|
|
12655
|
+
});
|
|
12656
|
+
// Storage usage
|
|
12657
|
+
app.get('/agents/:agentId/storage', async (request) => {
|
|
12658
|
+
const { agentId } = request.params;
|
|
12659
|
+
return getStorageUsage(agentId);
|
|
12660
|
+
});
|
|
12661
|
+
// ── Webhook Storage ──────────────────────────────────────────────────
|
|
12662
|
+
const { storeWebhookPayload, getWebhookPayload, listWebhookPayloads, markPayloadProcessed, getUnprocessedCount, purgeOldPayloads } = await import('./webhook-storage.js');
|
|
12663
|
+
// Ingest webhook payload
|
|
12664
|
+
app.post('/webhooks/ingest', async (request, reply) => {
|
|
12665
|
+
const body = request.body;
|
|
12666
|
+
if (!body?.source)
|
|
12667
|
+
return reply.code(400).send({ error: 'source is required' });
|
|
12668
|
+
if (!body?.eventType)
|
|
12669
|
+
return reply.code(400).send({ error: 'eventType is required' });
|
|
12670
|
+
if (!body?.body)
|
|
12671
|
+
return reply.code(400).send({ error: 'body (payload) is required' });
|
|
12672
|
+
const headers = {};
|
|
12673
|
+
for (const [k, v] of Object.entries(request.headers)) {
|
|
12674
|
+
if (typeof v === 'string')
|
|
12675
|
+
headers[k] = v;
|
|
12676
|
+
}
|
|
12677
|
+
const payload = storeWebhookPayload({ source: body.source, eventType: body.eventType, agentId: body.agentId, body: body.body, headers });
|
|
12678
|
+
return reply.code(201).send(payload);
|
|
12679
|
+
});
|
|
12680
|
+
// List payloads
|
|
12681
|
+
app.get('/webhooks/payloads', async (request) => {
|
|
12682
|
+
const query = request.query;
|
|
12683
|
+
return {
|
|
12684
|
+
payloads: listWebhookPayloads({
|
|
12685
|
+
source: query.source,
|
|
12686
|
+
agentId: query.agentId,
|
|
12687
|
+
unprocessedOnly: query.unprocessed === 'true',
|
|
12688
|
+
since: query.since ? parseInt(query.since, 10) : undefined,
|
|
12689
|
+
limit: query.limit ? parseInt(query.limit, 10) : undefined,
|
|
12690
|
+
}),
|
|
12691
|
+
unprocessedCount: getUnprocessedCount({ source: query.source, agentId: query.agentId }),
|
|
12692
|
+
};
|
|
12693
|
+
});
|
|
12694
|
+
// Get single payload
|
|
12695
|
+
app.get('/webhooks/payloads/:payloadId', async (request, reply) => {
|
|
12696
|
+
const { payloadId } = request.params;
|
|
12697
|
+
const payload = getWebhookPayload(payloadId);
|
|
12698
|
+
if (!payload)
|
|
12699
|
+
return reply.code(404).send({ error: 'Payload not found' });
|
|
12700
|
+
return payload;
|
|
12701
|
+
});
|
|
12702
|
+
// Mark processed
|
|
12703
|
+
app.post('/webhooks/payloads/:payloadId/process', async (request, reply) => {
|
|
12704
|
+
const { payloadId } = request.params;
|
|
12705
|
+
const marked = markPayloadProcessed(payloadId);
|
|
12706
|
+
if (!marked)
|
|
12707
|
+
return reply.code(404).send({ error: 'Payload not found or already processed' });
|
|
12708
|
+
return { processed: true };
|
|
12709
|
+
});
|
|
12710
|
+
// Purge old processed payloads
|
|
12711
|
+
app.post('/webhooks/purge', async (request) => {
|
|
12712
|
+
const body = request.body ?? {};
|
|
12713
|
+
const deleted = purgeOldPayloads(body.maxAgeDays ?? 30);
|
|
12714
|
+
return { deleted };
|
|
12715
|
+
});
|
|
12716
|
+
// ── Approval Routing ────────────────────────────────────────────────────
|
|
12717
|
+
const { listPendingApprovals, listApprovalQueue, submitApprovalDecision, } = await import('./agent-runs.js');
|
|
12718
|
+
// List pending approvals (review_requested events needing action)
|
|
12719
|
+
app.get('/approvals/pending', async (request) => {
|
|
12720
|
+
const query = request.query;
|
|
12721
|
+
return listPendingApprovals({
|
|
12722
|
+
agentId: query.agentId,
|
|
12723
|
+
limit: query.limit ? parseInt(query.limit, 10) : undefined,
|
|
12724
|
+
});
|
|
12725
|
+
});
|
|
12726
|
+
// Dedicated approval queue — unified view of everything needing human decision.
|
|
12727
|
+
// Answers: what needs decision, who owns it, when it expires, what happens if ignored.
|
|
12728
|
+
app.get('/approval-queue', async (request) => {
|
|
12729
|
+
const query = request.query;
|
|
12730
|
+
const items = listApprovalQueue({
|
|
12731
|
+
agentId: query.agentId,
|
|
12732
|
+
category: query.category === 'review' || query.category === 'agent_action' ? query.category : undefined,
|
|
12733
|
+
includeExpired: query.includeExpired === 'true',
|
|
12734
|
+
limit: query.limit ? parseInt(query.limit, 10) : undefined,
|
|
12735
|
+
});
|
|
12736
|
+
return {
|
|
12737
|
+
items,
|
|
12738
|
+
count: items.length,
|
|
12739
|
+
hasExpired: items.some(i => i.isExpired),
|
|
12740
|
+
};
|
|
12741
|
+
});
|
|
12742
|
+
// Submit agent-action approval (approve_requested events)
|
|
12743
|
+
app.post('/approval-queue/:approvalId/decide', async (request, reply) => {
|
|
12744
|
+
const { approvalId } = request.params;
|
|
12745
|
+
const body = request.body;
|
|
12746
|
+
if (!body?.decision || !['approve', 'reject', 'defer'].includes(body.decision)) {
|
|
12747
|
+
return reply.code(400).send({ error: 'decision must be "approve", "reject", or "defer"' });
|
|
12748
|
+
}
|
|
12749
|
+
if (!body?.actor) {
|
|
12750
|
+
return reply.code(400).send({ error: 'actor is required' });
|
|
12751
|
+
}
|
|
12752
|
+
try {
|
|
12753
|
+
const result = submitApprovalDecision({
|
|
12754
|
+
eventId: approvalId,
|
|
12755
|
+
decision: body.decision,
|
|
12756
|
+
reviewer: body.actor,
|
|
12757
|
+
comment: body.comment,
|
|
12758
|
+
});
|
|
12759
|
+
// Emit canvas_input event so Presence Layer updates
|
|
12760
|
+
eventBus.emit({
|
|
12761
|
+
id: `aq-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
12762
|
+
type: 'canvas_input',
|
|
12763
|
+
timestamp: Date.now(),
|
|
12764
|
+
data: {
|
|
12765
|
+
action: 'decision',
|
|
12766
|
+
approvalId,
|
|
12767
|
+
decision: body.decision,
|
|
12768
|
+
actor: body.actor,
|
|
12769
|
+
},
|
|
12770
|
+
});
|
|
12771
|
+
return result;
|
|
12772
|
+
}
|
|
12773
|
+
catch (err) {
|
|
12774
|
+
return reply.code(err.message.includes('not found') ? 404 : 400).send({ error: err.message });
|
|
12775
|
+
}
|
|
12776
|
+
});
|
|
12777
|
+
// Submit approval decision
|
|
12778
|
+
app.post('/approvals/:eventId/decide', async (request, reply) => {
|
|
12779
|
+
const { eventId } = request.params;
|
|
12780
|
+
const body = request.body;
|
|
12781
|
+
if (!body?.decision || !['approve', 'reject'].includes(body.decision)) {
|
|
12782
|
+
return reply.code(400).send({ error: 'decision must be "approve" or "reject"' });
|
|
12783
|
+
}
|
|
12784
|
+
if (!body?.reviewer) {
|
|
12785
|
+
return reply.code(400).send({ error: 'reviewer is required' });
|
|
12786
|
+
}
|
|
12787
|
+
try {
|
|
12788
|
+
const result = submitApprovalDecision({
|
|
12789
|
+
eventId,
|
|
12790
|
+
decision: body.decision,
|
|
12791
|
+
reviewer: body.reviewer,
|
|
12792
|
+
comment: body.comment,
|
|
12793
|
+
rationale: body.rationale,
|
|
12794
|
+
});
|
|
12795
|
+
return result;
|
|
12796
|
+
}
|
|
12797
|
+
catch (err) {
|
|
12798
|
+
return reply.code(err.message.includes('not found') ? 404 : 400).send({ error: err.message });
|
|
12799
|
+
}
|
|
12800
|
+
});
|
|
12801
|
+
// ── Canvas Input ──────────────────────────────────────────────────────
|
|
12802
|
+
// Human → agent control seam for the Presence Layer.
|
|
12803
|
+
// Payload is intentionally small per COO spec: action + target + actor.
|
|
12804
|
+
const CANVAS_INPUT_ACTIONS = ['decision', 'interrupt', 'pause', 'resume', 'mute', 'unmute'];
|
|
12805
|
+
const CanvasInputSchema = z.object({
|
|
12806
|
+
action: z.enum(CANVAS_INPUT_ACTIONS),
|
|
12807
|
+
targetRunId: z.string().optional(), // which run to act on
|
|
12808
|
+
decisionId: z.string().optional(), // for decision actions
|
|
12809
|
+
choice: z.enum(['approve', 'deny', 'defer']).optional(), // for decision actions
|
|
12810
|
+
actor: z.string().min(1), // who made this input
|
|
12811
|
+
comment: z.string().optional(), // optional rationale
|
|
12812
|
+
});
|
|
12813
|
+
app.post('/canvas/input', async (request, reply) => {
|
|
12814
|
+
const body = request.body;
|
|
12815
|
+
const result = CanvasInputSchema.safeParse(body);
|
|
12816
|
+
if (!result.success) {
|
|
12817
|
+
reply.code(422);
|
|
12818
|
+
return {
|
|
12819
|
+
error: `Invalid canvas input: ${result.error.issues.map(i => i.message).join(', ')}`,
|
|
12820
|
+
hint: 'Required: action (decision|interrupt|pause|resume|mute|unmute), actor. Optional: targetRunId, decisionId, choice, comment.',
|
|
12821
|
+
};
|
|
12822
|
+
}
|
|
12823
|
+
const input = result.data;
|
|
12824
|
+
const now = Date.now();
|
|
12825
|
+
// Route by action type
|
|
12826
|
+
if (input.action === 'decision') {
|
|
12827
|
+
if (!input.decisionId || !input.choice) {
|
|
12828
|
+
reply.code(422);
|
|
12829
|
+
return { error: 'Decision action requires decisionId and choice (approve|deny|defer)' };
|
|
12830
|
+
}
|
|
12831
|
+
// Emit canvas_input event for SSE subscribers
|
|
12832
|
+
eventBus.emit({ id: `cinput-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, type: "canvas_input", timestamp: Date.now(), data: {
|
|
12833
|
+
action: input.action,
|
|
12834
|
+
decisionId: input.decisionId,
|
|
12835
|
+
choice: input.choice,
|
|
12836
|
+
actor: input.actor,
|
|
12837
|
+
comment: input.comment,
|
|
12838
|
+
timestamp: now,
|
|
12839
|
+
} });
|
|
12840
|
+
return {
|
|
12841
|
+
success: true,
|
|
12842
|
+
action: 'decision',
|
|
12843
|
+
decisionId: input.decisionId,
|
|
12844
|
+
choice: input.choice,
|
|
12845
|
+
actor: input.actor,
|
|
12846
|
+
timestamp: now,
|
|
12847
|
+
};
|
|
12848
|
+
}
|
|
12849
|
+
if (input.action === 'interrupt' || input.action === 'pause') {
|
|
12850
|
+
// Update active run if specified
|
|
12851
|
+
const runId = input.targetRunId;
|
|
12852
|
+
if (runId) {
|
|
12853
|
+
try {
|
|
12854
|
+
updateAgentRun(runId, {
|
|
12855
|
+
status: input.action === 'interrupt' ? 'cancelled' : 'blocked',
|
|
12856
|
+
});
|
|
12857
|
+
}
|
|
12858
|
+
catch { /* run may not exist — still emit event */ }
|
|
12859
|
+
}
|
|
12860
|
+
eventBus.emit({ id: `cinput-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, type: "canvas_input", timestamp: Date.now(), data: {
|
|
12861
|
+
action: input.action,
|
|
12862
|
+
targetRunId: runId || null,
|
|
12863
|
+
actor: input.actor,
|
|
12864
|
+
timestamp: now,
|
|
12865
|
+
} });
|
|
12866
|
+
return {
|
|
12867
|
+
success: true,
|
|
12868
|
+
action: input.action,
|
|
12869
|
+
targetRunId: runId || null,
|
|
12870
|
+
actor: input.actor,
|
|
12871
|
+
timestamp: now,
|
|
12872
|
+
};
|
|
12873
|
+
}
|
|
12874
|
+
if (input.action === 'resume') {
|
|
12875
|
+
const runId = input.targetRunId;
|
|
12876
|
+
if (runId) {
|
|
12877
|
+
try {
|
|
12878
|
+
updateAgentRun(runId, { status: 'working' });
|
|
12879
|
+
}
|
|
12880
|
+
catch { /* run may not exist */ }
|
|
12881
|
+
}
|
|
12882
|
+
eventBus.emit({ id: `cinput-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, type: "canvas_input", timestamp: Date.now(), data: {
|
|
12883
|
+
action: 'resume',
|
|
12884
|
+
targetRunId: runId || null,
|
|
12885
|
+
actor: input.actor,
|
|
12886
|
+
timestamp: now,
|
|
12887
|
+
} });
|
|
12888
|
+
return { success: true, action: 'resume', targetRunId: runId || null, actor: input.actor, timestamp: now };
|
|
12889
|
+
}
|
|
12890
|
+
// Mute/unmute — emit event only, no state change needed
|
|
12891
|
+
eventBus.emit({ id: `cinput-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, type: "canvas_input", timestamp: Date.now(), data: {
|
|
12892
|
+
action: input.action,
|
|
12893
|
+
actor: input.actor,
|
|
12894
|
+
timestamp: now,
|
|
12895
|
+
} });
|
|
12896
|
+
return { success: true, action: input.action, actor: input.actor, timestamp: now };
|
|
12897
|
+
});
|
|
12898
|
+
// GET /canvas/input/schema — discovery endpoint
|
|
12899
|
+
app.get('/canvas/input/schema', async () => ({
|
|
12900
|
+
actions: CANVAS_INPUT_ACTIONS,
|
|
12901
|
+
schema: {
|
|
12902
|
+
action: 'decision | interrupt | pause | resume | mute | unmute',
|
|
12903
|
+
targetRunId: 'optional — which run to act on',
|
|
12904
|
+
decisionId: 'required for decision action — approval event ID',
|
|
12905
|
+
choice: 'required for decision — approve | deny | defer',
|
|
12906
|
+
actor: 'required — who made this input',
|
|
12907
|
+
comment: 'optional — rationale',
|
|
12908
|
+
},
|
|
12909
|
+
}));
|
|
12910
|
+
// ── Email / SMS relay ──────────────────────────────────────────────────
|
|
12911
|
+
async function cloudRelay(path, body, reply) {
|
|
12912
|
+
const cloudUrl = process.env.REFLECTT_CLOUD_URL;
|
|
12913
|
+
const hostToken = process.env.REFLECTT_HOST_TOKEN;
|
|
12914
|
+
if (!cloudUrl || !hostToken) {
|
|
12915
|
+
reply.code(503);
|
|
12916
|
+
return { error: 'Not connected to cloud. Configure REFLECTT_CLOUD_URL and REFLECTT_HOST_TOKEN.' };
|
|
12917
|
+
}
|
|
12918
|
+
try {
|
|
12919
|
+
const res = await fetch(`${cloudUrl}${path}`, {
|
|
12920
|
+
method: 'POST',
|
|
12921
|
+
headers: {
|
|
12922
|
+
'Content-Type': 'application/json',
|
|
12923
|
+
Authorization: `Bearer ${hostToken}`,
|
|
12924
|
+
},
|
|
12925
|
+
body: JSON.stringify(body),
|
|
12926
|
+
});
|
|
12927
|
+
const data = await res.json().catch(() => ({}));
|
|
12928
|
+
if (!res.ok) {
|
|
12929
|
+
reply.code(res.status);
|
|
12930
|
+
return data;
|
|
12931
|
+
}
|
|
12932
|
+
return data;
|
|
12933
|
+
}
|
|
12934
|
+
catch (err) {
|
|
12935
|
+
reply.code(502);
|
|
12936
|
+
return { error: `Cloud relay failed: ${err.message}` };
|
|
12937
|
+
}
|
|
12938
|
+
}
|
|
12939
|
+
// Send email via cloud relay
|
|
12940
|
+
app.post('/email/send', async (request, reply) => {
|
|
12941
|
+
const body = request.body;
|
|
12942
|
+
const from = typeof body.from === 'string' ? body.from.trim() : '';
|
|
12943
|
+
const to = body.to;
|
|
12944
|
+
const subject = typeof body.subject === 'string' ? body.subject.trim() : '';
|
|
12945
|
+
if (!from)
|
|
12946
|
+
return reply.code(400).send({ error: 'from is required' });
|
|
12947
|
+
if (!to)
|
|
12948
|
+
return reply.code(400).send({ error: 'to is required' });
|
|
12949
|
+
if (!subject)
|
|
12950
|
+
return reply.code(400).send({ error: 'subject is required' });
|
|
12951
|
+
if (!body.html && !body.text)
|
|
12952
|
+
return reply.code(400).send({ error: 'html or text body is required' });
|
|
12953
|
+
// Use host-relay endpoint — authenticates with host credential, uses host's own teamId server-side
|
|
12954
|
+
const hostId = process.env.REFLECTT_HOST_ID;
|
|
12955
|
+
const relayPath = hostId ? `/api/hosts/${encodeURIComponent(hostId)}/relay/email` : '/api/hosts/relay/email';
|
|
12956
|
+
return cloudRelay(relayPath, {
|
|
12957
|
+
from,
|
|
12958
|
+
to,
|
|
12959
|
+
subject,
|
|
12960
|
+
html: body.html,
|
|
12961
|
+
text: body.text,
|
|
12962
|
+
replyTo: body.replyTo,
|
|
12963
|
+
cc: body.cc,
|
|
12964
|
+
bcc: body.bcc,
|
|
12965
|
+
agent: body.agentId || body.agent || 'unknown',
|
|
12966
|
+
}, reply);
|
|
12967
|
+
});
|
|
12968
|
+
// Send SMS via cloud relay
|
|
12969
|
+
app.post('/sms/send', async (request, reply) => {
|
|
12970
|
+
const body = request.body;
|
|
12971
|
+
const to = typeof body.to === 'string' ? body.to.trim() : '';
|
|
12972
|
+
const msgBody = typeof body.body === 'string' ? body.body.trim() : '';
|
|
12973
|
+
if (!to)
|
|
12974
|
+
return reply.code(400).send({ error: 'to is required (phone number)' });
|
|
12975
|
+
if (!msgBody)
|
|
12976
|
+
return reply.code(400).send({ error: 'body is required' });
|
|
12977
|
+
const hostIdSms = process.env.REFLECTT_HOST_ID;
|
|
12978
|
+
const smsRelayPath = hostIdSms ? `/api/hosts/${encodeURIComponent(hostIdSms)}/relay/sms` : '/api/hosts/relay/sms';
|
|
12979
|
+
return cloudRelay(smsRelayPath, {
|
|
12980
|
+
to,
|
|
12981
|
+
body: msgBody,
|
|
12982
|
+
from: body.from,
|
|
12983
|
+
agent: body.agentId || body.agent || 'unknown',
|
|
12984
|
+
}, reply);
|
|
12985
|
+
});
|
|
12986
|
+
// ── Agent Config ──────────────────────────────────────────────────────
|
|
12987
|
+
// Per-agent model preference, cost cap, and settings.
|
|
12988
|
+
// This is the policy anchor for cost enforcement.
|
|
12989
|
+
const { getAgentConfig, listAgentConfigs, setAgentConfig, deleteAgentConfig, checkCostCap } = await import('./agent-config.js');
|
|
12990
|
+
// GET /agents/:agentId/config — get config for an agent
|
|
12991
|
+
app.get('/agents/:agentId/config', async (request) => {
|
|
12992
|
+
const config = getAgentConfig(request.params.agentId);
|
|
12993
|
+
return config ?? { agentId: request.params.agentId, configured: false };
|
|
12994
|
+
});
|
|
12995
|
+
// PUT /agents/:agentId/config — upsert config for an agent
|
|
12996
|
+
app.put('/agents/:agentId/config', async (request, reply) => {
|
|
12997
|
+
const body = request.body ?? {};
|
|
12998
|
+
try {
|
|
12999
|
+
const config = setAgentConfig(request.params.agentId, {
|
|
13000
|
+
teamId: typeof body.teamId === 'string' ? body.teamId : undefined,
|
|
13001
|
+
model: body.model !== undefined ? body.model : undefined,
|
|
13002
|
+
fallbackModel: body.fallbackModel !== undefined ? body.fallbackModel : undefined,
|
|
13003
|
+
costCapDaily: body.costCapDaily !== undefined ? body.costCapDaily : undefined,
|
|
13004
|
+
costCapMonthly: body.costCapMonthly !== undefined ? body.costCapMonthly : undefined,
|
|
13005
|
+
maxTokensPerCall: body.maxTokensPerCall !== undefined ? body.maxTokensPerCall : undefined,
|
|
13006
|
+
settings: body.settings !== undefined ? body.settings : undefined,
|
|
13007
|
+
});
|
|
13008
|
+
return config;
|
|
13009
|
+
}
|
|
13010
|
+
catch (err) {
|
|
13011
|
+
reply.code(400);
|
|
13012
|
+
return { error: err.message };
|
|
13013
|
+
}
|
|
13014
|
+
});
|
|
13015
|
+
// DELETE /agents/:agentId/config — remove config for an agent
|
|
13016
|
+
app.delete('/agents/:agentId/config', async (request, reply) => {
|
|
13017
|
+
const deleted = deleteAgentConfig(request.params.agentId);
|
|
13018
|
+
if (!deleted) {
|
|
13019
|
+
reply.code(404);
|
|
13020
|
+
return { error: 'Config not found' };
|
|
13021
|
+
}
|
|
13022
|
+
return { success: true };
|
|
13023
|
+
});
|
|
13024
|
+
// GET /agent-configs — list all agent configs
|
|
13025
|
+
app.get('/agent-configs', async (request) => {
|
|
13026
|
+
const query = request.query;
|
|
13027
|
+
return { configs: listAgentConfigs({ teamId: query.teamId }) };
|
|
13028
|
+
});
|
|
13029
|
+
// GET /agents/:agentId/cost-check — runtime cost enforcement check
|
|
13030
|
+
// Used by the runtime before making model calls.
|
|
13031
|
+
app.get('/agents/:agentId/cost-check', async (request) => {
|
|
13032
|
+
const query = request.query;
|
|
13033
|
+
const dailySpend = query.dailySpend ? parseFloat(query.dailySpend) : 0;
|
|
13034
|
+
const monthlySpend = query.monthlySpend ? parseFloat(query.monthlySpend) : 0;
|
|
13035
|
+
return checkCostCap(request.params.agentId, dailySpend, monthlySpend);
|
|
13036
|
+
});
|
|
13037
|
+
// ── Cost-Policy Enforcement Middleware ──────────────────────────────────
|
|
13038
|
+
const { enforcePolicy, recordUsage, getDailySpend, getMonthlySpend, purgeUsageLog, ensureUsageLogTable, } = await import('./cost-enforcement.js');
|
|
13039
|
+
ensureUsageLogTable();
|
|
13040
|
+
// POST /agents/:agentId/enforce-cost — runtime enforcement before model calls
|
|
13041
|
+
app.post('/agents/:agentId/enforce-cost', async (request, reply) => {
|
|
13042
|
+
const result = enforcePolicy(request.params.agentId);
|
|
13043
|
+
const status = result.action === 'deny' ? 403 : 200;
|
|
13044
|
+
return reply.code(status).send(result);
|
|
13045
|
+
});
|
|
13046
|
+
// GET /agents/:agentId/spend — current daily + monthly spend
|
|
13047
|
+
app.get('/agents/:agentId/spend', async (request) => {
|
|
13048
|
+
const { agentId } = request.params;
|
|
13049
|
+
return {
|
|
13050
|
+
agentId,
|
|
13051
|
+
dailySpend: getDailySpend(agentId),
|
|
13052
|
+
monthlySpend: getMonthlySpend(agentId),
|
|
13053
|
+
};
|
|
13054
|
+
});
|
|
13055
|
+
// POST /usage/record — record a usage event
|
|
13056
|
+
app.post('/usage/record', async (request, reply) => {
|
|
13057
|
+
const body = request.body;
|
|
13058
|
+
if (!body?.agentId)
|
|
13059
|
+
return reply.code(400).send({ error: 'agentId is required' });
|
|
13060
|
+
if (!body?.model)
|
|
13061
|
+
return reply.code(400).send({ error: 'model is required' });
|
|
13062
|
+
if (typeof body.cost !== 'number')
|
|
13063
|
+
return reply.code(400).send({ error: 'cost is required (number)' });
|
|
13064
|
+
recordUsage({
|
|
13065
|
+
agentId: body.agentId,
|
|
13066
|
+
model: body.model,
|
|
13067
|
+
inputTokens: body.inputTokens ?? 0,
|
|
13068
|
+
outputTokens: body.outputTokens ?? 0,
|
|
13069
|
+
cost: body.cost,
|
|
13070
|
+
timestamp: Date.now(),
|
|
13071
|
+
});
|
|
13072
|
+
return reply.code(201).send({ ok: true });
|
|
13073
|
+
});
|
|
13074
|
+
// POST /usage/purge — purge old usage records
|
|
13075
|
+
app.post('/usage/purge', async (request) => {
|
|
13076
|
+
const body = request.body;
|
|
13077
|
+
const deleted = purgeUsageLog(body?.maxAgeDays ?? 90);
|
|
13078
|
+
return { deleted };
|
|
13079
|
+
});
|
|
13080
|
+
// ── Agent Memories ─────────────────────────────────────────────────────
|
|
13081
|
+
const { setMemory, getMemory, listMemories, deleteMemory, deleteMemoryById, purgeExpiredMemories, countMemories, } = await import('./agent-memories.js');
|
|
13082
|
+
// Set (create or update) a memory
|
|
13083
|
+
app.put('/agents/:agentId/memories', async (request, reply) => {
|
|
13084
|
+
const { agentId } = request.params;
|
|
13085
|
+
const body = request.body;
|
|
13086
|
+
if (!body?.key)
|
|
13087
|
+
return reply.code(400).send({ error: 'key is required' });
|
|
13088
|
+
if (body.content === undefined || body.content === null)
|
|
13089
|
+
return reply.code(400).send({ error: 'content is required' });
|
|
13090
|
+
try {
|
|
13091
|
+
const memory = setMemory({
|
|
13092
|
+
agentId,
|
|
13093
|
+
namespace: body.namespace,
|
|
13094
|
+
key: body.key,
|
|
13095
|
+
content: body.content,
|
|
13096
|
+
tags: body.tags,
|
|
13097
|
+
expiresAt: body.expiresAt,
|
|
13098
|
+
});
|
|
13099
|
+
return reply.code(200).send(memory);
|
|
13100
|
+
}
|
|
13101
|
+
catch (err) {
|
|
13102
|
+
return reply.code(500).send({ error: err.message });
|
|
13103
|
+
}
|
|
13104
|
+
});
|
|
13105
|
+
// Get a specific memory by key
|
|
13106
|
+
app.get('/agents/:agentId/memories/:key', async (request, reply) => {
|
|
13107
|
+
const { agentId, key } = request.params;
|
|
13108
|
+
const query = request.query;
|
|
13109
|
+
const memory = getMemory(agentId, key, query.namespace);
|
|
13110
|
+
if (!memory)
|
|
13111
|
+
return reply.code(404).send({ error: 'Memory not found' });
|
|
13112
|
+
return memory;
|
|
13113
|
+
});
|
|
13114
|
+
// List memories for an agent
|
|
13115
|
+
app.get('/agents/:agentId/memories', async (request, reply) => {
|
|
13116
|
+
const { agentId } = request.params;
|
|
13117
|
+
const query = request.query;
|
|
13118
|
+
return listMemories({
|
|
13119
|
+
agentId,
|
|
13120
|
+
namespace: query.namespace,
|
|
13121
|
+
tag: query.tag,
|
|
13122
|
+
search: query.search,
|
|
13123
|
+
limit: query.limit ? parseInt(query.limit, 10) : undefined,
|
|
13124
|
+
});
|
|
13125
|
+
});
|
|
13126
|
+
// Delete a memory by key
|
|
13127
|
+
app.delete('/agents/:agentId/memories/:key', async (request, reply) => {
|
|
13128
|
+
const { agentId, key } = request.params;
|
|
13129
|
+
const query = request.query;
|
|
13130
|
+
const deleted = deleteMemory(agentId, key, query.namespace);
|
|
13131
|
+
if (!deleted)
|
|
13132
|
+
return reply.code(404).send({ error: 'Memory not found' });
|
|
13133
|
+
return { deleted: true };
|
|
13134
|
+
});
|
|
13135
|
+
// Count memories
|
|
13136
|
+
app.get('/agents/:agentId/memories/count', async (request, reply) => {
|
|
13137
|
+
const { agentId } = request.params;
|
|
13138
|
+
const query = request.query;
|
|
13139
|
+
return { count: countMemories(agentId, query.namespace) };
|
|
13140
|
+
});
|
|
13141
|
+
// Purge expired memories (housekeeping)
|
|
13142
|
+
app.post('/agents/memories/purge', async (_request, reply) => {
|
|
13143
|
+
const purged = purgeExpiredMemories();
|
|
13144
|
+
return { purged };
|
|
13145
|
+
});
|
|
11992
13146
|
return app;
|
|
11993
13147
|
}
|
|
11994
13148
|
//# sourceMappingURL=server.js.map
|