@desplega.ai/agent-swarm 1.74.4 → 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 +1264 -46
- package/package.json +2 -2
- package/src/be/db.ts +563 -9
- package/src/be/memory/edges-store.ts +69 -0
- package/src/be/memory/providers/sqlite-store.ts +4 -0
- package/src/be/memory/raters/explicit-self.ts +22 -0
- package/src/be/memory/raters/implicit-citation.ts +44 -0
- package/src/be/memory/raters/llm-client.ts +172 -0
- package/src/be/memory/raters/llm-summarizer.ts +218 -0
- package/src/be/memory/raters/llm.ts +375 -0
- package/src/be/memory/raters/noop.ts +14 -0
- package/src/be/memory/raters/registry.ts +86 -0
- package/src/be/memory/raters/retrieval.ts +88 -0
- package/src/be/memory/raters/run-server-raters.ts +97 -0
- package/src/be/memory/raters/store.ts +228 -0
- package/src/be/memory/raters/types.ts +101 -0
- package/src/be/memory/reranker.ts +32 -2
- package/src/be/memory/retrieval-store.ts +116 -0
- package/src/be/memory/types.ts +3 -0
- package/src/be/migrations/051_memory_posteriors_and_retrieval.sql +67 -0
- package/src/be/migrations/052_memory_edges.sql +36 -0
- package/src/be/migrations/053_agent_waiting_for_credentials_status.sql +61 -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 +186 -0
- package/src/commands/provider-credentials.ts +434 -0
- package/src/commands/runner.ts +253 -21
- package/src/hooks/hook.ts +143 -66
- package/src/http/agents.ts +191 -1
- package/src/http/config.ts +11 -1
- package/src/http/core.ts +5 -0
- package/src/http/inbox-state.ts +89 -0
- package/src/http/index.ts +10 -0
- package/src/http/memory.ts +230 -1
- 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/prompts/memories.ts +62 -0
- package/src/providers/claude-adapter.ts +22 -0
- package/src/providers/claude-managed-adapter.ts +24 -0
- package/src/providers/codex-adapter.ts +43 -1
- package/src/providers/devin-adapter.ts +18 -0
- package/src/providers/index.ts +7 -0
- package/src/providers/opencode-adapter.ts +60 -0
- package/src/providers/pi-mono-adapter.ts +71 -0
- package/src/providers/types.ts +34 -0
- package/src/server.ts +2 -0
- package/src/slack/handlers.ts +0 -1
- package/src/tests/agents-harness-provider.test.ts +333 -0
- package/src/tests/credential-check.test.ts +367 -0
- package/src/tests/credential-status-api.test.ts +223 -0
- package/src/tests/credential-status-routing.test.ts +150 -0
- package/src/tests/credential-wait.test.ts +282 -0
- package/src/tests/harness-provider-resolution.test.ts +242 -0
- package/src/tests/jira-sync.test.ts +1 -1
- package/src/tests/memory-edges.test.ts +722 -0
- package/src/tests/memory-rate-endpoint.test.ts +330 -0
- package/src/tests/memory-rate-tool.test.ts +252 -0
- package/src/tests/memory-rater-e2e.test.ts +578 -0
- package/src/tests/memory-rater-implicit-citation.test.ts +304 -0
- package/src/tests/memory-rater-llm-summarizer.test.ts +317 -0
- package/src/tests/memory-rater-llm.test.ts +964 -0
- package/src/tests/memory-rater-store.test.ts +249 -0
- package/src/tests/memory-reranker.test.ts +161 -2
- package/src/tests/migration-runner-regressions.test.ts +17 -2
- package/src/tests/mocks/mock-llm-rater-client.ts +35 -0
- package/src/tests/run-server-raters.test.ts +291 -0
- 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/tool-annotations.test.ts +2 -2
- package/src/tests/use-dismissible-card.test.ts +140 -0
- package/src/tools/memory-rate.ts +166 -0
- package/src/tools/memory-search.ts +18 -0
- package/src/tools/store-progress.ts +37 -0
- package/src/tools/swarm-config/set-config.ts +17 -1
- package/src/tools/tool-config.ts +1 -0
- package/src/types.ts +122 -1
- package/src/utils/harness-provider.ts +32 -0
- package/tsconfig.json +0 -2
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,
|
|
@@ -555,6 +560,12 @@ type AgentRow = {
|
|
|
555
560
|
provider: string | null;
|
|
556
561
|
createdAt: string;
|
|
557
562
|
lastUpdatedAt: string;
|
|
563
|
+
/** JSON array of env-var names; populated only when status is `waiting_for_credentials`. */
|
|
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;
|
|
558
569
|
};
|
|
559
570
|
|
|
560
571
|
function rowToAgent(row: AgentRow): Agent {
|
|
@@ -576,15 +587,23 @@ function rowToAgent(row: AgentRow): Agent {
|
|
|
576
587
|
heartbeatMd: row.heartbeatMd ?? undefined,
|
|
577
588
|
lastActivityAt: row.lastActivityAt ?? undefined,
|
|
578
589
|
provider: (row.provider as ProviderName | null) ?? undefined,
|
|
590
|
+
harnessProvider: (row.harness_provider as ProviderName | null) ?? null,
|
|
579
591
|
createdAt: row.createdAt,
|
|
580
592
|
lastUpdatedAt: row.lastUpdatedAt,
|
|
593
|
+
credentialMissing: row.credentialMissing
|
|
594
|
+
? (JSON.parse(row.credentialMissing) as string[])
|
|
595
|
+
: null,
|
|
596
|
+
credStatus: row.cred_status ? (JSON.parse(row.cred_status) as AgentCredStatus) : null,
|
|
581
597
|
};
|
|
582
598
|
}
|
|
583
599
|
|
|
584
600
|
export const agentQueries = {
|
|
585
601
|
insert: () =>
|
|
586
|
-
getDb().prepare<
|
|
587
|
-
|
|
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 *",
|
|
588
607
|
),
|
|
589
608
|
|
|
590
609
|
getById: () => getDb().prepare<AgentRow, [string]>("SELECT * FROM agents WHERE id = ?"),
|
|
@@ -596,9 +615,36 @@ export const agentQueries = {
|
|
|
596
615
|
"UPDATE agents SET status = ?, lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ? RETURNING *",
|
|
597
616
|
),
|
|
598
617
|
|
|
618
|
+
updateCredentialState: () =>
|
|
619
|
+
getDb().prepare<AgentRow, [AgentStatus, string | null, string]>(
|
|
620
|
+
"UPDATE agents SET status = ?, credentialMissing = ?, lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ? RETURNING *",
|
|
621
|
+
),
|
|
622
|
+
|
|
599
623
|
delete: () => getDb().prepare<null, [string]>("DELETE FROM agents WHERE id = ?"),
|
|
600
624
|
};
|
|
601
625
|
|
|
626
|
+
/**
|
|
627
|
+
* Phase 3 of the worker credential safe-loop plan.
|
|
628
|
+
*
|
|
629
|
+
* `ready=true` clears the waiting state — the agent transitions to `idle`
|
|
630
|
+
* and the dispatcher will start handing it tasks again.
|
|
631
|
+
*
|
|
632
|
+
* `ready=false` parks the agent on `waiting_for_credentials` with the env-var
|
|
633
|
+
* names it's blocked on. The capacity dispatch query already filters
|
|
634
|
+
* `status === 'idle'` so the new value is implicitly excluded with no other
|
|
635
|
+
* code change.
|
|
636
|
+
*/
|
|
637
|
+
export function updateAgentCredentialState(
|
|
638
|
+
agentId: string,
|
|
639
|
+
ready: boolean,
|
|
640
|
+
missing: string[] | null,
|
|
641
|
+
): Agent | null {
|
|
642
|
+
const status: AgentStatus = ready ? "idle" : "waiting_for_credentials";
|
|
643
|
+
const missingJson = ready ? null : missing && missing.length > 0 ? JSON.stringify(missing) : null;
|
|
644
|
+
const row = agentQueries.updateCredentialState().get(status, missingJson, agentId);
|
|
645
|
+
return row ? rowToAgent(row) : null;
|
|
646
|
+
}
|
|
647
|
+
|
|
602
648
|
export function createAgent(
|
|
603
649
|
agent: Omit<Agent, "id" | "createdAt" | "lastUpdatedAt"> & { id?: string },
|
|
604
650
|
): Agent {
|
|
@@ -606,7 +652,15 @@ export function createAgent(
|
|
|
606
652
|
const maxTasks = agent.maxTasks ?? 1;
|
|
607
653
|
const row = agentQueries
|
|
608
654
|
.insert()
|
|
609
|
-
.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
|
+
);
|
|
610
664
|
if (!row) throw new Error("Failed to create agent");
|
|
611
665
|
try {
|
|
612
666
|
createLogEntry({ eventType: "agent_joined", agentId: id, newValue: agent.status });
|
|
@@ -664,6 +718,83 @@ export function updateAgentProvider(id: string, provider: ProviderName): Agent |
|
|
|
664
718
|
return row ? rowToAgent(row) : null;
|
|
665
719
|
}
|
|
666
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
|
+
|
|
667
798
|
export function updateAgentActivity(id: string): void {
|
|
668
799
|
getDb()
|
|
669
800
|
.prepare<null, [string]>(
|
|
@@ -774,6 +905,10 @@ export function getRemainingCapacity(agentId: string): number {
|
|
|
774
905
|
export function updateAgentStatusFromCapacity(agentId: string): void {
|
|
775
906
|
const agent = getAgentById(agentId);
|
|
776
907
|
if (!agent || agent.status === "offline") return;
|
|
908
|
+
// `waiting_for_credentials` is owned by the worker's credential-wait
|
|
909
|
+
// tick — task-completion shouldn't accidentally promote a blocked agent
|
|
910
|
+
// back to idle.
|
|
911
|
+
if (agent.status === "waiting_for_credentials") return;
|
|
777
912
|
|
|
778
913
|
const activeCount = getActiveTaskCount(agentId);
|
|
779
914
|
const newStatus = activeCount > 0 ? "busy" : "idle";
|
|
@@ -1186,7 +1321,8 @@ export function findTaskByVcs(vcsRepo: string, vcsNumber: number): AgentTask | n
|
|
|
1186
1321
|
export const findTaskByGitHub = findTaskByVcs;
|
|
1187
1322
|
|
|
1188
1323
|
export interface TaskFilters {
|
|
1189
|
-
status
|
|
1324
|
+
/** Single status (back-compat) OR array of statuses (multi-status filter). */
|
|
1325
|
+
status?: AgentTaskStatus | AgentTaskStatus[];
|
|
1190
1326
|
agentId?: string;
|
|
1191
1327
|
search?: string;
|
|
1192
1328
|
// New filters
|
|
@@ -1196,6 +1332,10 @@ export interface TaskFilters {
|
|
|
1196
1332
|
taskType?: string;
|
|
1197
1333
|
tags?: string[];
|
|
1198
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;
|
|
1199
1339
|
limit?: number;
|
|
1200
1340
|
offset?: number;
|
|
1201
1341
|
includeHeartbeat?: boolean;
|
|
@@ -1206,8 +1346,19 @@ export function getAllTasks(filters?: TaskFilters): AgentTask[] {
|
|
|
1206
1346
|
const params: (string | AgentTaskStatus)[] = [];
|
|
1207
1347
|
|
|
1208
1348
|
if (filters?.status) {
|
|
1209
|
-
|
|
1210
|
-
|
|
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
|
+
}
|
|
1211
1362
|
}
|
|
1212
1363
|
|
|
1213
1364
|
if (filters?.agentId) {
|
|
@@ -1249,6 +1400,17 @@ export function getAllTasks(filters?: TaskFilters): AgentTask[] {
|
|
|
1249
1400
|
params.push(filters.scheduleId);
|
|
1250
1401
|
}
|
|
1251
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
|
+
|
|
1252
1414
|
// Exclude heartbeat tasks by default
|
|
1253
1415
|
if (!filters?.includeHeartbeat) {
|
|
1254
1416
|
conditions.push("(IFNULL(taskType, '') != 'heartbeat' AND tags NOT LIKE '%\"heartbeat\"%')");
|
|
@@ -1284,8 +1446,19 @@ export function getTasksCount(filters?: Omit<TaskFilters, "limit" | "readyOnly">
|
|
|
1284
1446
|
const params: (string | AgentTaskStatus)[] = [];
|
|
1285
1447
|
|
|
1286
1448
|
if (filters?.status) {
|
|
1287
|
-
|
|
1288
|
-
|
|
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
|
+
}
|
|
1289
1462
|
}
|
|
1290
1463
|
|
|
1291
1464
|
if (filters?.agentId) {
|
|
@@ -1325,6 +1498,17 @@ export function getTasksCount(filters?: Omit<TaskFilters, "limit" | "readyOnly">
|
|
|
1325
1498
|
params.push(filters.scheduleId);
|
|
1326
1499
|
}
|
|
1327
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
|
+
|
|
1328
1512
|
// Exclude heartbeat tasks by default
|
|
1329
1513
|
if (!filters?.includeHeartbeat) {
|
|
1330
1514
|
conditions.push("(IFNULL(taskType, '') != 'heartbeat' AND tags NOT LIKE '%\"heartbeat\"%')");
|
|
@@ -8446,6 +8630,301 @@ export function deleteUser(id: string): boolean {
|
|
|
8446
8630
|
return result.changes > 0;
|
|
8447
8631
|
}
|
|
8448
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
|
+
|
|
8449
8928
|
// ============================================================================
|
|
8450
8929
|
// Budgets, daily-spend aggregation, and budget-refusal notifications (Phase 2)
|
|
8451
8930
|
// ----------------------------------------------------------------------------
|
|
@@ -8875,3 +9354,78 @@ export function setBudgetRefusalFollowUpTaskId(
|
|
|
8875
9354
|
)
|
|
8876
9355
|
.run(followUpTaskId, taskId, date);
|
|
8877
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
|
+
}
|