@desplega.ai/agent-swarm 1.75.0 → 1.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/openapi.json +973 -36
- package/package.json +2 -2
- package/src/be/db.ts +527 -9
- package/src/be/memory/raters/llm-summarizer.ts +218 -0
- package/src/be/memory/raters/llm.ts +56 -75
- package/src/be/memory/retrieval-store.ts +21 -0
- package/src/be/migrations/054_agent_harness_provider.sql +21 -0
- package/src/be/migrations/055_agent_cred_status.sql +15 -0
- package/src/be/migrations/056_drop_agent_tasks_source_check.sql +139 -0
- package/src/be/migrations/057_inbox_item_state.sql +27 -0
- package/src/be/migrations/058_task_templates.sql +31 -0
- package/src/be/swarm-config-guard.ts +24 -0
- package/src/commands/credential-wait.ts +1 -1
- package/src/commands/provider-credentials.ts +434 -0
- package/src/commands/runner.ts +229 -42
- package/src/hooks/hook.ts +115 -95
- package/src/http/agents.ts +82 -2
- package/src/http/config.ts +11 -1
- package/src/http/inbox-state.ts +89 -0
- package/src/http/index.ts +10 -0
- package/src/http/sessions.ts +86 -0
- package/src/http/status.ts +665 -0
- package/src/http/task-templates.ts +51 -0
- package/src/http/tasks.ts +85 -5
- package/src/http/users.ts +134 -0
- package/src/providers/claude-adapter.ts +5 -0
- package/src/providers/codex-adapter.ts +1 -1
- package/src/providers/index.ts +1 -1
- package/src/slack/handlers.ts +0 -1
- package/src/tests/agents-harness-provider.test.ts +333 -0
- package/src/tests/credential-check.test.ts +32 -1
- package/src/tests/credential-status-api.test.ts +42 -0
- package/src/tests/harness-provider-resolution.test.ts +242 -0
- package/src/tests/jira-sync.test.ts +1 -1
- package/src/tests/memory-rater-llm-summarizer.test.ts +317 -0
- package/src/tests/memory-rater-llm.test.ts +265 -107
- package/src/tests/migration-runner-regressions.test.ts +17 -2
- package/src/tests/sessions.test.ts +141 -0
- package/src/tests/status.test.ts +843 -0
- package/src/tests/stop-hook-task-resolution.test.ts +98 -0
- package/src/tests/template-recommendations.test.ts +148 -0
- package/src/tests/use-dismissible-card.test.ts +140 -0
- package/src/tools/swarm-config/set-config.ts +17 -1
- package/src/types.ts +117 -0
- package/src/utils/harness-provider.ts +32 -0
- package/tsconfig.json +0 -2
- package/src/providers/credentials.ts +0 -74
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.76.0",
|
|
4
4
|
"description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "desplega.sh <contact@desplega.sh>",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"@mariozechner/pi-ai": "^0.73.0",
|
|
109
109
|
"@mariozechner/pi-coding-agent": "^0.73.0",
|
|
110
110
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
111
|
-
"@openai/codex-sdk": "^0.
|
|
111
|
+
"@openai/codex-sdk": "^0.128.0",
|
|
112
112
|
"@opencode-ai/sdk": "^1.14.30",
|
|
113
113
|
"@openfort/openfort-node": "^0.9.1",
|
|
114
114
|
"@slack/bolt": "^4.6.0",
|
package/src/be/db.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { configureDbResolver } from "../prompts/resolver";
|
|
|
6
6
|
import type {
|
|
7
7
|
ActiveSession,
|
|
8
8
|
Agent,
|
|
9
|
+
AgentCredStatus,
|
|
9
10
|
AgentLog,
|
|
10
11
|
AgentLogEventType,
|
|
11
12
|
AgentMcpServer,
|
|
@@ -27,7 +28,9 @@ import type {
|
|
|
27
28
|
ContextSnapshotEventType,
|
|
28
29
|
ContextVersion,
|
|
29
30
|
CooldownConfig,
|
|
30
|
-
|
|
31
|
+
InboxItemState,
|
|
32
|
+
InboxItemStatus,
|
|
33
|
+
InboxItemType,
|
|
31
34
|
InboxMessage,
|
|
32
35
|
InboxMessageStatus,
|
|
33
36
|
InputValue,
|
|
@@ -54,6 +57,8 @@ import type {
|
|
|
54
57
|
SkillWithInstallInfo,
|
|
55
58
|
SwarmConfig,
|
|
56
59
|
SwarmRepo,
|
|
60
|
+
TaskTemplate,
|
|
61
|
+
TaskTemplateKind,
|
|
57
62
|
TriggerConfig,
|
|
58
63
|
User,
|
|
59
64
|
VersionableField,
|
|
@@ -557,6 +562,10 @@ type AgentRow = {
|
|
|
557
562
|
lastUpdatedAt: string;
|
|
558
563
|
/** JSON array of env-var names; populated only when status is `waiting_for_credentials`. */
|
|
559
564
|
credentialMissing: string | null;
|
|
565
|
+
/** Phase 1.5: per-agent harness provider pushed on worker registration. */
|
|
566
|
+
harness_provider: string | null;
|
|
567
|
+
/** Migration 055: worker-self-reported credential snapshot (JSON of AgentCredStatus). NULL = unreported. */
|
|
568
|
+
cred_status: string | null;
|
|
560
569
|
};
|
|
561
570
|
|
|
562
571
|
function rowToAgent(row: AgentRow): Agent {
|
|
@@ -578,18 +587,23 @@ function rowToAgent(row: AgentRow): Agent {
|
|
|
578
587
|
heartbeatMd: row.heartbeatMd ?? undefined,
|
|
579
588
|
lastActivityAt: row.lastActivityAt ?? undefined,
|
|
580
589
|
provider: (row.provider as ProviderName | null) ?? undefined,
|
|
590
|
+
harnessProvider: (row.harness_provider as ProviderName | null) ?? null,
|
|
581
591
|
createdAt: row.createdAt,
|
|
582
592
|
lastUpdatedAt: row.lastUpdatedAt,
|
|
583
593
|
credentialMissing: row.credentialMissing
|
|
584
594
|
? (JSON.parse(row.credentialMissing) as string[])
|
|
585
595
|
: null,
|
|
596
|
+
credStatus: row.cred_status ? (JSON.parse(row.cred_status) as AgentCredStatus) : null,
|
|
586
597
|
};
|
|
587
598
|
}
|
|
588
599
|
|
|
589
600
|
export const agentQueries = {
|
|
590
601
|
insert: () =>
|
|
591
|
-
getDb().prepare<
|
|
592
|
-
|
|
602
|
+
getDb().prepare<
|
|
603
|
+
AgentRow,
|
|
604
|
+
[string, string, number, AgentStatus, number, string | null, string | null]
|
|
605
|
+
>(
|
|
606
|
+
"INSERT INTO agents (id, name, isLead, status, maxTasks, provider, harness_provider, createdAt, lastUpdatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) RETURNING *",
|
|
593
607
|
),
|
|
594
608
|
|
|
595
609
|
getById: () => getDb().prepare<AgentRow, [string]>("SELECT * FROM agents WHERE id = ?"),
|
|
@@ -638,7 +652,15 @@ export function createAgent(
|
|
|
638
652
|
const maxTasks = agent.maxTasks ?? 1;
|
|
639
653
|
const row = agentQueries
|
|
640
654
|
.insert()
|
|
641
|
-
.get(
|
|
655
|
+
.get(
|
|
656
|
+
id,
|
|
657
|
+
agent.name,
|
|
658
|
+
agent.isLead ? 1 : 0,
|
|
659
|
+
agent.status,
|
|
660
|
+
maxTasks,
|
|
661
|
+
agent.provider ?? null,
|
|
662
|
+
agent.harnessProvider ?? null,
|
|
663
|
+
);
|
|
642
664
|
if (!row) throw new Error("Failed to create agent");
|
|
643
665
|
try {
|
|
644
666
|
createLogEntry({ eventType: "agent_joined", agentId: id, newValue: agent.status });
|
|
@@ -696,6 +718,83 @@ export function updateAgentProvider(id: string, provider: ProviderName): Agent |
|
|
|
696
718
|
return row ? rowToAgent(row) : null;
|
|
697
719
|
}
|
|
698
720
|
|
|
721
|
+
/**
|
|
722
|
+
* Phase 1.5 (cloud-personalization): set the per-agent `harness_provider`
|
|
723
|
+
* column. Pass `null` to clear. Validation against the canonical provider
|
|
724
|
+
* list happens at the API layer via `ProviderNameSchema`.
|
|
725
|
+
*
|
|
726
|
+
* Returns the updated row, or null if the agent does not exist.
|
|
727
|
+
*/
|
|
728
|
+
export function setAgentHarnessProvider(id: string, provider: ProviderName | null): Agent | null {
|
|
729
|
+
const row = getDb()
|
|
730
|
+
.prepare<AgentRow, [string | null, string]>(
|
|
731
|
+
`UPDATE agents SET harness_provider = ?, lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
732
|
+
WHERE id = ? RETURNING *`,
|
|
733
|
+
)
|
|
734
|
+
.get(provider, id);
|
|
735
|
+
return row ? rowToAgent(row) : null;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Migration 055 — write the worker-self-reported credential snapshot.
|
|
740
|
+
* Pass `null` to clear (e.g. on agent re-registration). Validation against
|
|
741
|
+
* the JSON shape happens at the API layer via `AgentCredStatusSchema`.
|
|
742
|
+
*
|
|
743
|
+
* Worker reports this alongside the existing `updateAgentCredentialState`
|
|
744
|
+
* call; we keep the writes in two functions so the dispatch pattern stays
|
|
745
|
+
* one-row-one-fact, and the PATCH handler can choose which to call based
|
|
746
|
+
* on which fields the request body carried.
|
|
747
|
+
*/
|
|
748
|
+
export function updateAgentCredStatus(
|
|
749
|
+
id: string,
|
|
750
|
+
credStatus: AgentCredStatus | null,
|
|
751
|
+
): Agent | null {
|
|
752
|
+
const json = credStatus ? JSON.stringify(credStatus) : null;
|
|
753
|
+
const row = getDb()
|
|
754
|
+
.prepare<AgentRow, [string | null, string]>(
|
|
755
|
+
`UPDATE agents SET cred_status = ?, lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
756
|
+
WHERE id = ? RETURNING *`,
|
|
757
|
+
)
|
|
758
|
+
.get(json, id);
|
|
759
|
+
return row ? rowToAgent(row) : null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Migration 055 — read all agents whose `harness_provider` matches a given
|
|
764
|
+
* provider, with their reported `cred_status`. Used by the credential-status
|
|
765
|
+
* API endpoint to roll up "is this provider working across the fleet?".
|
|
766
|
+
*
|
|
767
|
+
* Agents with NULL `cred_status` (never reported, or CRED_CHECK_DISABLE=1)
|
|
768
|
+
* are still returned — the caller surfaces them as "unreported".
|
|
769
|
+
*/
|
|
770
|
+
export function listAgentsWithCredStatusByProvider(provider: string): Agent[] {
|
|
771
|
+
const rows = getDb()
|
|
772
|
+
.prepare<AgentRow, [string]>(`SELECT * FROM agents WHERE harness_provider = ? ORDER BY name`)
|
|
773
|
+
.all(provider);
|
|
774
|
+
return rows.map(rowToAgent);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Phase 1.5 (cloud-personalization): aggregate count of registered agents
|
|
779
|
+
* by `harness_provider`. NULL rows (agents that registered before the
|
|
780
|
+
* migration or never pushed a value) are excluded — they show up in the
|
|
781
|
+
* total agent count but not here.
|
|
782
|
+
*
|
|
783
|
+
* Used by future fleet displays. Not consumed in this phase.
|
|
784
|
+
*/
|
|
785
|
+
export function getAgentHarnessProviders(): Array<{ provider: string; count: number }> {
|
|
786
|
+
const rows = getDb()
|
|
787
|
+
.prepare<{ provider: string; count: number }, []>(
|
|
788
|
+
`SELECT harness_provider AS provider, COUNT(*) AS count
|
|
789
|
+
FROM agents
|
|
790
|
+
WHERE harness_provider IS NOT NULL
|
|
791
|
+
GROUP BY harness_provider
|
|
792
|
+
ORDER BY harness_provider`,
|
|
793
|
+
)
|
|
794
|
+
.all();
|
|
795
|
+
return rows.map((r) => ({ provider: r.provider, count: r.count }));
|
|
796
|
+
}
|
|
797
|
+
|
|
699
798
|
export function updateAgentActivity(id: string): void {
|
|
700
799
|
getDb()
|
|
701
800
|
.prepare<null, [string]>(
|
|
@@ -1222,7 +1321,8 @@ export function findTaskByVcs(vcsRepo: string, vcsNumber: number): AgentTask | n
|
|
|
1222
1321
|
export const findTaskByGitHub = findTaskByVcs;
|
|
1223
1322
|
|
|
1224
1323
|
export interface TaskFilters {
|
|
1225
|
-
status
|
|
1324
|
+
/** Single status (back-compat) OR array of statuses (multi-status filter). */
|
|
1325
|
+
status?: AgentTaskStatus | AgentTaskStatus[];
|
|
1226
1326
|
agentId?: string;
|
|
1227
1327
|
search?: string;
|
|
1228
1328
|
// New filters
|
|
@@ -1232,6 +1332,10 @@ export interface TaskFilters {
|
|
|
1232
1332
|
taskType?: string;
|
|
1233
1333
|
tags?: string[];
|
|
1234
1334
|
scheduleId?: string;
|
|
1335
|
+
/** Filter to tasks whose `source` is in this list. Empty/undefined → no filter. */
|
|
1336
|
+
source?: AgentTaskSource[];
|
|
1337
|
+
/** ISO 8601 timestamp; only return tasks where createdAt >= this. */
|
|
1338
|
+
createdAfter?: string;
|
|
1235
1339
|
limit?: number;
|
|
1236
1340
|
offset?: number;
|
|
1237
1341
|
includeHeartbeat?: boolean;
|
|
@@ -1242,8 +1346,19 @@ export function getAllTasks(filters?: TaskFilters): AgentTask[] {
|
|
|
1242
1346
|
const params: (string | AgentTaskStatus)[] = [];
|
|
1243
1347
|
|
|
1244
1348
|
if (filters?.status) {
|
|
1245
|
-
|
|
1246
|
-
|
|
1349
|
+
if (Array.isArray(filters.status)) {
|
|
1350
|
+
if (filters.status.length === 1) {
|
|
1351
|
+
conditions.push("status = ?");
|
|
1352
|
+
params.push(filters.status[0]!);
|
|
1353
|
+
} else if (filters.status.length > 1) {
|
|
1354
|
+
const placeholders = filters.status.map(() => "?").join(", ");
|
|
1355
|
+
conditions.push(`status IN (${placeholders})`);
|
|
1356
|
+
for (const s of filters.status) params.push(s);
|
|
1357
|
+
}
|
|
1358
|
+
} else {
|
|
1359
|
+
conditions.push("status = ?");
|
|
1360
|
+
params.push(filters.status);
|
|
1361
|
+
}
|
|
1247
1362
|
}
|
|
1248
1363
|
|
|
1249
1364
|
if (filters?.agentId) {
|
|
@@ -1285,6 +1400,17 @@ export function getAllTasks(filters?: TaskFilters): AgentTask[] {
|
|
|
1285
1400
|
params.push(filters.scheduleId);
|
|
1286
1401
|
}
|
|
1287
1402
|
|
|
1403
|
+
if (filters?.source && filters.source.length > 0) {
|
|
1404
|
+
const placeholders = filters.source.map(() => "?").join(", ");
|
|
1405
|
+
conditions.push(`source IN (${placeholders})`);
|
|
1406
|
+
for (const s of filters.source) params.push(s);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
if (filters?.createdAfter) {
|
|
1410
|
+
conditions.push("createdAt >= ?");
|
|
1411
|
+
params.push(filters.createdAfter);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1288
1414
|
// Exclude heartbeat tasks by default
|
|
1289
1415
|
if (!filters?.includeHeartbeat) {
|
|
1290
1416
|
conditions.push("(IFNULL(taskType, '') != 'heartbeat' AND tags NOT LIKE '%\"heartbeat\"%')");
|
|
@@ -1320,8 +1446,19 @@ export function getTasksCount(filters?: Omit<TaskFilters, "limit" | "readyOnly">
|
|
|
1320
1446
|
const params: (string | AgentTaskStatus)[] = [];
|
|
1321
1447
|
|
|
1322
1448
|
if (filters?.status) {
|
|
1323
|
-
|
|
1324
|
-
|
|
1449
|
+
if (Array.isArray(filters.status)) {
|
|
1450
|
+
if (filters.status.length === 1) {
|
|
1451
|
+
conditions.push("status = ?");
|
|
1452
|
+
params.push(filters.status[0]!);
|
|
1453
|
+
} else if (filters.status.length > 1) {
|
|
1454
|
+
const placeholders = filters.status.map(() => "?").join(", ");
|
|
1455
|
+
conditions.push(`status IN (${placeholders})`);
|
|
1456
|
+
for (const s of filters.status) params.push(s);
|
|
1457
|
+
}
|
|
1458
|
+
} else {
|
|
1459
|
+
conditions.push("status = ?");
|
|
1460
|
+
params.push(filters.status);
|
|
1461
|
+
}
|
|
1325
1462
|
}
|
|
1326
1463
|
|
|
1327
1464
|
if (filters?.agentId) {
|
|
@@ -1361,6 +1498,17 @@ export function getTasksCount(filters?: Omit<TaskFilters, "limit" | "readyOnly">
|
|
|
1361
1498
|
params.push(filters.scheduleId);
|
|
1362
1499
|
}
|
|
1363
1500
|
|
|
1501
|
+
if (filters?.source && filters.source.length > 0) {
|
|
1502
|
+
const placeholders = filters.source.map(() => "?").join(", ");
|
|
1503
|
+
conditions.push(`source IN (${placeholders})`);
|
|
1504
|
+
for (const s of filters.source) params.push(s);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
if (filters?.createdAfter) {
|
|
1508
|
+
conditions.push("createdAt >= ?");
|
|
1509
|
+
params.push(filters.createdAfter);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1364
1512
|
// Exclude heartbeat tasks by default
|
|
1365
1513
|
if (!filters?.includeHeartbeat) {
|
|
1366
1514
|
conditions.push("(IFNULL(taskType, '') != 'heartbeat' AND tags NOT LIKE '%\"heartbeat\"%')");
|
|
@@ -8482,6 +8630,301 @@ export function deleteUser(id: string): boolean {
|
|
|
8482
8630
|
return result.changes > 0;
|
|
8483
8631
|
}
|
|
8484
8632
|
|
|
8633
|
+
// ============================================================================
|
|
8634
|
+
// Inbox Item State (per-user dismiss/snooze/done for action-items inbox)
|
|
8635
|
+
// ============================================================================
|
|
8636
|
+
|
|
8637
|
+
interface InboxItemStateRow {
|
|
8638
|
+
id: string;
|
|
8639
|
+
userId: string;
|
|
8640
|
+
itemType: string;
|
|
8641
|
+
itemId: string;
|
|
8642
|
+
status: string;
|
|
8643
|
+
snoozeUntil: string | null;
|
|
8644
|
+
dismissedAt: string | null;
|
|
8645
|
+
doneAt: string | null;
|
|
8646
|
+
createdAt: string;
|
|
8647
|
+
lastUpdatedAt: string;
|
|
8648
|
+
}
|
|
8649
|
+
|
|
8650
|
+
function rowToInboxItemState(row: InboxItemStateRow): InboxItemState {
|
|
8651
|
+
return {
|
|
8652
|
+
id: row.id,
|
|
8653
|
+
userId: row.userId,
|
|
8654
|
+
itemType: row.itemType as InboxItemType,
|
|
8655
|
+
itemId: row.itemId,
|
|
8656
|
+
status: row.status as InboxItemStatus,
|
|
8657
|
+
snoozeUntil: row.snoozeUntil ?? undefined,
|
|
8658
|
+
dismissedAt: row.dismissedAt ?? undefined,
|
|
8659
|
+
doneAt: row.doneAt ?? undefined,
|
|
8660
|
+
createdAt: row.createdAt,
|
|
8661
|
+
lastUpdatedAt: row.lastUpdatedAt,
|
|
8662
|
+
};
|
|
8663
|
+
}
|
|
8664
|
+
|
|
8665
|
+
export function listInboxState(opts: {
|
|
8666
|
+
userId: string;
|
|
8667
|
+
status?: InboxItemStatus;
|
|
8668
|
+
itemType?: InboxItemType;
|
|
8669
|
+
}): InboxItemState[] {
|
|
8670
|
+
const conditions: string[] = ["userId = ?"];
|
|
8671
|
+
const params: string[] = [opts.userId];
|
|
8672
|
+
|
|
8673
|
+
if (opts.status) {
|
|
8674
|
+
conditions.push("status = ?");
|
|
8675
|
+
params.push(opts.status);
|
|
8676
|
+
}
|
|
8677
|
+
if (opts.itemType) {
|
|
8678
|
+
conditions.push("itemType = ?");
|
|
8679
|
+
params.push(opts.itemType);
|
|
8680
|
+
}
|
|
8681
|
+
|
|
8682
|
+
const where = conditions.join(" AND ");
|
|
8683
|
+
return getDb()
|
|
8684
|
+
.prepare<InboxItemStateRow, string[]>(
|
|
8685
|
+
`SELECT * FROM inbox_item_state WHERE ${where} ORDER BY lastUpdatedAt DESC`,
|
|
8686
|
+
)
|
|
8687
|
+
.all(...params)
|
|
8688
|
+
.map(rowToInboxItemState);
|
|
8689
|
+
}
|
|
8690
|
+
|
|
8691
|
+
export function upsertInboxState(opts: {
|
|
8692
|
+
userId: string;
|
|
8693
|
+
itemType: InboxItemType;
|
|
8694
|
+
itemId: string;
|
|
8695
|
+
status: InboxItemStatus;
|
|
8696
|
+
snoozeUntil?: string;
|
|
8697
|
+
dismissedAt?: string;
|
|
8698
|
+
doneAt?: string;
|
|
8699
|
+
}): InboxItemState {
|
|
8700
|
+
const now = new Date().toISOString();
|
|
8701
|
+
// Auto-derive timestamps from status when not explicitly provided.
|
|
8702
|
+
const dismissedAt = opts.dismissedAt ?? (opts.status === "dismissed" ? now : null);
|
|
8703
|
+
const doneAt = opts.doneAt ?? (opts.status === "done" ? now : null);
|
|
8704
|
+
const snoozeUntil = opts.snoozeUntil ?? null;
|
|
8705
|
+
|
|
8706
|
+
// SQLite upsert via UNIQUE(userId, itemType, itemId).
|
|
8707
|
+
const row = getDb()
|
|
8708
|
+
.prepare<InboxItemStateRow, (string | null)[]>(
|
|
8709
|
+
`INSERT INTO inbox_item_state (userId, itemType, itemId, status, snoozeUntil, dismissedAt, doneAt, createdAt, lastUpdatedAt)
|
|
8710
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8711
|
+
ON CONFLICT(userId, itemType, itemId) DO UPDATE SET
|
|
8712
|
+
status = excluded.status,
|
|
8713
|
+
snoozeUntil = excluded.snoozeUntil,
|
|
8714
|
+
dismissedAt = excluded.dismissedAt,
|
|
8715
|
+
doneAt = excluded.doneAt,
|
|
8716
|
+
lastUpdatedAt = excluded.lastUpdatedAt
|
|
8717
|
+
RETURNING *`,
|
|
8718
|
+
)
|
|
8719
|
+
.get(
|
|
8720
|
+
opts.userId,
|
|
8721
|
+
opts.itemType,
|
|
8722
|
+
opts.itemId,
|
|
8723
|
+
opts.status,
|
|
8724
|
+
snoozeUntil,
|
|
8725
|
+
dismissedAt,
|
|
8726
|
+
doneAt,
|
|
8727
|
+
now,
|
|
8728
|
+
now,
|
|
8729
|
+
);
|
|
8730
|
+
if (!row) throw new Error("Failed to upsert inbox state");
|
|
8731
|
+
return rowToInboxItemState(row);
|
|
8732
|
+
}
|
|
8733
|
+
|
|
8734
|
+
// ============================================================================
|
|
8735
|
+
// Task Templates ("To start" bucket — polymorphic starters registry)
|
|
8736
|
+
// ============================================================================
|
|
8737
|
+
|
|
8738
|
+
interface TaskTemplateRow {
|
|
8739
|
+
id: string;
|
|
8740
|
+
title: string;
|
|
8741
|
+
description: string;
|
|
8742
|
+
prompt: string;
|
|
8743
|
+
kind: string;
|
|
8744
|
+
payload: string;
|
|
8745
|
+
category: string | null;
|
|
8746
|
+
tags: string;
|
|
8747
|
+
createdAt: string;
|
|
8748
|
+
}
|
|
8749
|
+
|
|
8750
|
+
function rowToTaskTemplate(row: TaskTemplateRow): TaskTemplate {
|
|
8751
|
+
let payload: Record<string, unknown> = {};
|
|
8752
|
+
try {
|
|
8753
|
+
payload = JSON.parse(row.payload);
|
|
8754
|
+
} catch {}
|
|
8755
|
+
let tags: string[] = [];
|
|
8756
|
+
try {
|
|
8757
|
+
tags = JSON.parse(row.tags);
|
|
8758
|
+
} catch {}
|
|
8759
|
+
return {
|
|
8760
|
+
id: row.id,
|
|
8761
|
+
title: row.title,
|
|
8762
|
+
description: row.description,
|
|
8763
|
+
prompt: row.prompt,
|
|
8764
|
+
kind: row.kind as TaskTemplateKind,
|
|
8765
|
+
payload,
|
|
8766
|
+
category: row.category ?? undefined,
|
|
8767
|
+
tags,
|
|
8768
|
+
createdAt: row.createdAt,
|
|
8769
|
+
};
|
|
8770
|
+
}
|
|
8771
|
+
|
|
8772
|
+
export function listTaskTemplates(opts?: {
|
|
8773
|
+
category?: string;
|
|
8774
|
+
kind?: TaskTemplateKind;
|
|
8775
|
+
query?: string;
|
|
8776
|
+
}): TaskTemplate[] {
|
|
8777
|
+
const conditions: string[] = [];
|
|
8778
|
+
const params: string[] = [];
|
|
8779
|
+
|
|
8780
|
+
if (opts?.category) {
|
|
8781
|
+
conditions.push("category = ?");
|
|
8782
|
+
params.push(opts.category);
|
|
8783
|
+
}
|
|
8784
|
+
if (opts?.kind) {
|
|
8785
|
+
conditions.push("kind = ?");
|
|
8786
|
+
params.push(opts.kind);
|
|
8787
|
+
}
|
|
8788
|
+
if (opts?.query && opts.query.trim().length > 0) {
|
|
8789
|
+
// Case-insensitive LIKE match against title OR description, single
|
|
8790
|
+
// parameter-bound WHERE clause to prevent injection.
|
|
8791
|
+
conditions.push("(LOWER(title) LIKE ? OR LOWER(description) LIKE ?)");
|
|
8792
|
+
const needle = `%${opts.query.toLowerCase()}%`;
|
|
8793
|
+
params.push(needle, needle);
|
|
8794
|
+
}
|
|
8795
|
+
|
|
8796
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
8797
|
+
return getDb()
|
|
8798
|
+
.prepare<TaskTemplateRow, string[]>(`SELECT * FROM task_templates ${where} ORDER BY createdAt`)
|
|
8799
|
+
.all(...params)
|
|
8800
|
+
.map(rowToTaskTemplate);
|
|
8801
|
+
}
|
|
8802
|
+
|
|
8803
|
+
// ============================================================================
|
|
8804
|
+
// Sessions — root task chain + recent-sessions list
|
|
8805
|
+
// ============================================================================
|
|
8806
|
+
|
|
8807
|
+
/**
|
|
8808
|
+
* Walk the parent→child chain rooted at `rootTaskId` via recursive CTE.
|
|
8809
|
+
* Returns the chain ordered by `createdAt` (so the root is first; siblings
|
|
8810
|
+
* appear in creation order; grand-children after their parents).
|
|
8811
|
+
*/
|
|
8812
|
+
export function getRootTaskChain(rootTaskId: string): AgentTask[] {
|
|
8813
|
+
const rows = getDb()
|
|
8814
|
+
.prepare<AgentTaskRow, string>(
|
|
8815
|
+
`WITH RECURSIVE chain(id) AS (
|
|
8816
|
+
SELECT id FROM agent_tasks WHERE id = ?
|
|
8817
|
+
UNION ALL
|
|
8818
|
+
SELECT t.id FROM agent_tasks t
|
|
8819
|
+
JOIN chain c ON t.parentTaskId = c.id
|
|
8820
|
+
)
|
|
8821
|
+
SELECT t.* FROM agent_tasks t
|
|
8822
|
+
JOIN chain ON chain.id = t.id
|
|
8823
|
+
ORDER BY t.createdAt`,
|
|
8824
|
+
)
|
|
8825
|
+
.all(rootTaskId);
|
|
8826
|
+
return rows.map(rowToAgentTask);
|
|
8827
|
+
}
|
|
8828
|
+
|
|
8829
|
+
export interface SessionListItem {
|
|
8830
|
+
root: AgentTask;
|
|
8831
|
+
chainTaskCount: number;
|
|
8832
|
+
lastActivityAt: string;
|
|
8833
|
+
latestStatus: AgentTaskStatus;
|
|
8834
|
+
}
|
|
8835
|
+
|
|
8836
|
+
/**
|
|
8837
|
+
* List the most recent sessions ordered by chain-wide latest activity.
|
|
8838
|
+
* A "session" here is any task with `parentTaskId IS NULL` — its descendants
|
|
8839
|
+
* (children, grand-children, …) are summarized via the recursive CTE.
|
|
8840
|
+
*
|
|
8841
|
+
* `lastActivityAt` is `MAX(t.lastUpdatedAt)` over the entire chain rooted at
|
|
8842
|
+
* the candidate task, computed as a correlated subquery so the outer ORDER
|
|
8843
|
+
* BY can sort against it.
|
|
8844
|
+
*/
|
|
8845
|
+
export function listRecentSessions(opts?: {
|
|
8846
|
+
limit?: number;
|
|
8847
|
+
offset?: number;
|
|
8848
|
+
/** Filter to root tasks whose `source` is in this list. Empty/undefined → no source filter. */
|
|
8849
|
+
source?: string[];
|
|
8850
|
+
/** Case-insensitive substring match against `r.task`. */
|
|
8851
|
+
q?: string;
|
|
8852
|
+
}): SessionListItem[] {
|
|
8853
|
+
const limit = opts?.limit ?? 25;
|
|
8854
|
+
const offset = opts?.offset ?? 0;
|
|
8855
|
+
const sources = opts?.source?.filter((s) => s.length > 0) ?? [];
|
|
8856
|
+
const q = opts?.q?.trim();
|
|
8857
|
+
|
|
8858
|
+
const conditions: string[] = ["r.parentTaskId IS NULL"];
|
|
8859
|
+
const params: (string | number)[] = [];
|
|
8860
|
+
|
|
8861
|
+
if (sources.length > 0) {
|
|
8862
|
+
conditions.push(`r.source IN (${sources.map(() => "?").join(", ")})`);
|
|
8863
|
+
params.push(...sources);
|
|
8864
|
+
}
|
|
8865
|
+
if (q && q.length > 0) {
|
|
8866
|
+
conditions.push("lower(r.task) LIKE ?");
|
|
8867
|
+
params.push(`%${q.toLowerCase()}%`);
|
|
8868
|
+
}
|
|
8869
|
+
params.push(limit, offset);
|
|
8870
|
+
|
|
8871
|
+
const rootRows = getDb()
|
|
8872
|
+
.prepare<
|
|
8873
|
+
AgentTaskRow & { __chainCount: number; __lastActivityAt: string; __latestStatus: string },
|
|
8874
|
+
typeof params
|
|
8875
|
+
>(
|
|
8876
|
+
`SELECT
|
|
8877
|
+
r.*,
|
|
8878
|
+
(SELECT COUNT(*) FROM agent_tasks d
|
|
8879
|
+
WHERE d.id IN (
|
|
8880
|
+
WITH RECURSIVE chain(id) AS (
|
|
8881
|
+
SELECT r.id
|
|
8882
|
+
UNION ALL
|
|
8883
|
+
SELECT t.id FROM agent_tasks t JOIN chain c ON t.parentTaskId = c.id
|
|
8884
|
+
)
|
|
8885
|
+
SELECT id FROM chain
|
|
8886
|
+
)
|
|
8887
|
+
) AS __chainCount,
|
|
8888
|
+
(SELECT MAX(d.lastUpdatedAt) FROM agent_tasks d
|
|
8889
|
+
WHERE d.id IN (
|
|
8890
|
+
WITH RECURSIVE chain(id) AS (
|
|
8891
|
+
SELECT r.id
|
|
8892
|
+
UNION ALL
|
|
8893
|
+
SELECT t.id FROM agent_tasks t JOIN chain c ON t.parentTaskId = c.id
|
|
8894
|
+
)
|
|
8895
|
+
SELECT id FROM chain
|
|
8896
|
+
)
|
|
8897
|
+
) AS __lastActivityAt,
|
|
8898
|
+
(SELECT d.status FROM agent_tasks d
|
|
8899
|
+
WHERE d.id IN (
|
|
8900
|
+
WITH RECURSIVE chain(id) AS (
|
|
8901
|
+
SELECT r.id
|
|
8902
|
+
UNION ALL
|
|
8903
|
+
SELECT t.id FROM agent_tasks t JOIN chain c ON t.parentTaskId = c.id
|
|
8904
|
+
)
|
|
8905
|
+
SELECT id FROM chain
|
|
8906
|
+
)
|
|
8907
|
+
ORDER BY d.lastUpdatedAt DESC
|
|
8908
|
+
LIMIT 1
|
|
8909
|
+
) AS __latestStatus
|
|
8910
|
+
FROM agent_tasks r
|
|
8911
|
+
WHERE ${conditions.join(" AND ")}
|
|
8912
|
+
ORDER BY __lastActivityAt DESC
|
|
8913
|
+
LIMIT ? OFFSET ?`,
|
|
8914
|
+
)
|
|
8915
|
+
.all(...params);
|
|
8916
|
+
|
|
8917
|
+
return rootRows.map((row) => {
|
|
8918
|
+
const { __chainCount, __lastActivityAt, __latestStatus, ...taskRow } = row;
|
|
8919
|
+
return {
|
|
8920
|
+
root: rowToAgentTask(taskRow as AgentTaskRow),
|
|
8921
|
+
chainTaskCount: __chainCount,
|
|
8922
|
+
lastActivityAt: __lastActivityAt ?? row.lastUpdatedAt,
|
|
8923
|
+
latestStatus: (__latestStatus as AgentTaskStatus) ?? row.status,
|
|
8924
|
+
};
|
|
8925
|
+
});
|
|
8926
|
+
}
|
|
8927
|
+
|
|
8485
8928
|
// ============================================================================
|
|
8486
8929
|
// Budgets, daily-spend aggregation, and budget-refusal notifications (Phase 2)
|
|
8487
8930
|
// ----------------------------------------------------------------------------
|
|
@@ -8911,3 +9354,78 @@ export function setBudgetRefusalFollowUpTaskId(
|
|
|
8911
9354
|
)
|
|
8912
9355
|
.run(followUpTaskId, taskId, date);
|
|
8913
9356
|
}
|
|
9357
|
+
|
|
9358
|
+
// ============================================================================
|
|
9359
|
+
// /status helpers — instance activity + first-task milestone
|
|
9360
|
+
// ============================================================================
|
|
9361
|
+
|
|
9362
|
+
/**
|
|
9363
|
+
* Count agents that have heartbeated within the last `minutes` minutes,
|
|
9364
|
+
* grouped by lead/worker. Used by the `workers` setup milestone on
|
|
9365
|
+
* `GET /status` to flip from `configured` → `verified` only when both a lead
|
|
9366
|
+
* and at least one worker are alive.
|
|
9367
|
+
*
|
|
9368
|
+
* "Recent" defaults to 5 minutes — a multiple of `ACTIVITY_THROTTLE_MS = 5_000`
|
|
9369
|
+
* (`src/providers/swarm-events-shared.ts:48-49`) plus margin for missed
|
|
9370
|
+
* heartbeats. Agents with `status = 'offline'` are excluded.
|
|
9371
|
+
*/
|
|
9372
|
+
export function getLiveAgentCounts(minutes: number = 5): {
|
|
9373
|
+
leads_alive: number;
|
|
9374
|
+
workers_alive: number;
|
|
9375
|
+
} {
|
|
9376
|
+
const row = getDb()
|
|
9377
|
+
.prepare<{ leads_alive: number | null; workers_alive: number | null }, [number]>(
|
|
9378
|
+
`SELECT
|
|
9379
|
+
SUM(CASE WHEN isLead = 1 THEN 1 ELSE 0 END) AS leads_alive,
|
|
9380
|
+
SUM(CASE WHEN isLead = 0 THEN 1 ELSE 0 END) AS workers_alive
|
|
9381
|
+
FROM agents
|
|
9382
|
+
WHERE lastActivityAt IS NOT NULL
|
|
9383
|
+
AND lastActivityAt >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-' || ?1 || ' minutes')
|
|
9384
|
+
AND status != 'offline'`,
|
|
9385
|
+
)
|
|
9386
|
+
.get(minutes);
|
|
9387
|
+
return {
|
|
9388
|
+
leads_alive: row?.leads_alive ?? 0,
|
|
9389
|
+
workers_alive: row?.workers_alive ?? 0,
|
|
9390
|
+
};
|
|
9391
|
+
}
|
|
9392
|
+
|
|
9393
|
+
/**
|
|
9394
|
+
* Aggregate activity numbers for `GET /status`'s `activity` block.
|
|
9395
|
+
* - `agents_online` / `leads_online`: heartbeated within the last 5 minutes.
|
|
9396
|
+
* - `recent_tasks_count`: agent_tasks rows created in the last 24 hours.
|
|
9397
|
+
*
|
|
9398
|
+
* `agents_online` reports total alive agents (leads + workers) so the home
|
|
9399
|
+
* page can show a single "online" stat without summing on the client.
|
|
9400
|
+
*/
|
|
9401
|
+
export function getInstanceActivity(): {
|
|
9402
|
+
agents_online: number;
|
|
9403
|
+
leads_online: number;
|
|
9404
|
+
recent_tasks_count: number;
|
|
9405
|
+
} {
|
|
9406
|
+
const { leads_alive, workers_alive } = getLiveAgentCounts(5);
|
|
9407
|
+
const tasksRow = getDb()
|
|
9408
|
+
.prepare<{ count: number }, []>(
|
|
9409
|
+
`SELECT COUNT(*) AS count FROM agent_tasks
|
|
9410
|
+
WHERE createdAt >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-24 hours')`,
|
|
9411
|
+
)
|
|
9412
|
+
.get();
|
|
9413
|
+
return {
|
|
9414
|
+
agents_online: leads_alive + workers_alive,
|
|
9415
|
+
leads_online: leads_alive,
|
|
9416
|
+
recent_tasks_count: tasksRow?.count ?? 0,
|
|
9417
|
+
};
|
|
9418
|
+
}
|
|
9419
|
+
|
|
9420
|
+
/**
|
|
9421
|
+
* `first_task` milestone: true once any task has reached `status = 'completed'`.
|
|
9422
|
+
* Cheap LIMIT 1 probe; the row's contents don't matter, only existence.
|
|
9423
|
+
*/
|
|
9424
|
+
export function hasFirstCompletedTask(): boolean {
|
|
9425
|
+
const row = getDb()
|
|
9426
|
+
.prepare<{ one: number }, []>(
|
|
9427
|
+
`SELECT 1 AS one FROM agent_tasks WHERE status = 'completed' LIMIT 1`,
|
|
9428
|
+
)
|
|
9429
|
+
.get();
|
|
9430
|
+
return row !== null;
|
|
9431
|
+
}
|