@desplega.ai/agent-swarm 1.52.1 → 1.53.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/openapi.json +1517 -488
- package/package.json +5 -2
- package/src/be/db.ts +530 -0
- package/src/be/events.ts +322 -0
- package/src/be/migrations/021_events.sql +24 -0
- package/src/be/migrations/022_context_usage.sql +34 -0
- package/src/be/migrations/023_mcp_servers.sql +44 -0
- package/src/commands/runner.ts +348 -1
- package/src/http/context.ts +118 -0
- package/src/http/events.ts +188 -0
- package/src/http/index.ts +6 -0
- package/src/http/mcp-servers.ts +364 -0
- package/src/http/tasks.ts +33 -0
- package/src/linear/outbound.ts +8 -1
- package/src/linear/sync.ts +3 -0
- package/src/oauth/ensure-token.ts +50 -0
- package/src/prompts/base-prompt.ts +7 -0
- package/src/providers/claude-adapter.ts +156 -15
- package/src/providers/pi-mono-adapter.ts +68 -0
- package/src/providers/pi-mono-extension.ts +56 -2
- package/src/providers/pi-mono-mcp-client.ts +10 -1
- package/src/providers/types.ts +14 -1
- package/src/server.ts +19 -0
- package/src/tests/context-window.test.ts +66 -0
- package/src/tests/ensure-token.test.ts +170 -0
- package/src/tests/events-db.test.ts +314 -0
- package/src/tests/events-http.test.ts +267 -0
- package/src/tests/prompt-template-remaining.test.ts +5 -5
- package/src/tests/tool-annotations.test.ts +2 -2
- package/src/tests/vcs-tracking.test.ts +176 -0
- package/src/tests/workflow-executors.test.ts +8 -1
- package/src/tools/mcp-servers/index.ts +7 -0
- package/src/tools/mcp-servers/mcp-server-create.ts +138 -0
- package/src/tools/mcp-servers/mcp-server-delete.ts +72 -0
- package/src/tools/mcp-servers/mcp-server-get.ts +80 -0
- package/src/tools/mcp-servers/mcp-server-install.ts +110 -0
- package/src/tools/mcp-servers/mcp-server-list.ts +67 -0
- package/src/tools/mcp-servers/mcp-server-uninstall.ts +71 -0
- package/src/tools/mcp-servers/mcp-server-update.ts +120 -0
- package/src/tools/tool-config.ts +9 -0
- package/src/types.ts +153 -0
- package/src/utils/context-window.ts +41 -0
- package/src/workflows/executors/base.ts +9 -1
package/src/commands/runner.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type ProviderSession,
|
|
18
18
|
type ProviderSessionConfig,
|
|
19
19
|
} from "../providers/index.ts";
|
|
20
|
+
import { getContextWindowSize } from "../utils/context-window.ts";
|
|
20
21
|
import { resolveCredentialPools } from "../utils/credentials.ts";
|
|
21
22
|
import { prettyPrintLine, prettyPrintStderr } from "../utils/pretty-print.ts";
|
|
22
23
|
import { detectVcsProvider } from "../vcs/index.ts";
|
|
@@ -772,6 +773,8 @@ interface RunningTask {
|
|
|
772
773
|
result: ProviderResult | null;
|
|
773
774
|
/** Deferred cursor updates for channel_activity triggers — committed after success */
|
|
774
775
|
cursorUpdates?: Array<{ channelId: string; ts: string }>;
|
|
776
|
+
/** Resolved working directory for VCS detection */
|
|
777
|
+
workingDir?: string;
|
|
775
778
|
}
|
|
776
779
|
|
|
777
780
|
/** Runner state for tracking concurrent tasks */
|
|
@@ -882,6 +885,90 @@ async function saveProviderSessionId(
|
|
|
882
885
|
});
|
|
883
886
|
}
|
|
884
887
|
|
|
888
|
+
/** Cache of tasks that already have VCS linked — prevents repeated gh pr list calls */
|
|
889
|
+
const vcsDetectedTasks = new Set<string>();
|
|
890
|
+
|
|
891
|
+
/** Throttle timestamps for periodic VCS checks per task */
|
|
892
|
+
const vcsCheckTimestamps = new Map<string, number>();
|
|
893
|
+
const VCS_CHECK_INTERVAL = 60_000; // 60 seconds
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Detect if the task's working directory has an open PR for the current branch.
|
|
897
|
+
* If found, report VCS info to the API so webhook events can link back to this task.
|
|
898
|
+
*/
|
|
899
|
+
async function detectVcsForTask(
|
|
900
|
+
apiUrl: string,
|
|
901
|
+
apiKey: string,
|
|
902
|
+
taskId: string,
|
|
903
|
+
workingDir: string,
|
|
904
|
+
): Promise<void> {
|
|
905
|
+
try {
|
|
906
|
+
// 1. Check if inside a git repo
|
|
907
|
+
const isGit = await Bun.$`git -C ${workingDir} rev-parse --is-inside-work-tree`.quiet().text();
|
|
908
|
+
if (isGit.trim() !== "true") return;
|
|
909
|
+
|
|
910
|
+
// 2. Get current branch
|
|
911
|
+
const branch = (await Bun.$`git -C ${workingDir} branch --show-current`.quiet().text()).trim();
|
|
912
|
+
if (!branch || branch === "main" || branch === "master") return;
|
|
913
|
+
|
|
914
|
+
// 3. Get remote URL to determine provider and repo
|
|
915
|
+
const remoteUrl = (
|
|
916
|
+
await Bun.$`git -C ${workingDir} remote get-url origin`.quiet().text()
|
|
917
|
+
).trim();
|
|
918
|
+
|
|
919
|
+
// 4. Detect provider and check for PR/MR
|
|
920
|
+
let vcsProvider: "github" | "gitlab";
|
|
921
|
+
let prJson: string;
|
|
922
|
+
|
|
923
|
+
if (remoteUrl.includes("github.com") || remoteUrl.includes("github")) {
|
|
924
|
+
vcsProvider = "github";
|
|
925
|
+
prJson = (
|
|
926
|
+
await Bun.$`gh pr list --head ${branch} --json number,url --limit 1`.quiet().text()
|
|
927
|
+
).trim();
|
|
928
|
+
} else if (remoteUrl.includes("gitlab")) {
|
|
929
|
+
vcsProvider = "gitlab";
|
|
930
|
+
prJson = (
|
|
931
|
+
await Bun.$`glab mr list --source-branch ${branch} --json iid,web_url --per-page 1`
|
|
932
|
+
.quiet()
|
|
933
|
+
.text()
|
|
934
|
+
).trim();
|
|
935
|
+
} else {
|
|
936
|
+
return; // Unknown provider
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// 5. Parse result
|
|
940
|
+
const prs = JSON.parse(prJson);
|
|
941
|
+
if (!Array.isArray(prs) || prs.length === 0) return;
|
|
942
|
+
|
|
943
|
+
const pr = prs[0];
|
|
944
|
+
const vcsNumber = pr.number ?? pr.iid;
|
|
945
|
+
const vcsUrl = pr.url ?? pr.web_url;
|
|
946
|
+
if (!vcsNumber || !vcsUrl) return;
|
|
947
|
+
|
|
948
|
+
// 6. Extract repo from remote URL
|
|
949
|
+
const repoMatch = remoteUrl.match(/[:/]([^/]+\/[^/.]+?)(?:\.git)?$/);
|
|
950
|
+
if (!repoMatch) return;
|
|
951
|
+
const vcsRepo = repoMatch[1];
|
|
952
|
+
|
|
953
|
+
// 7. Report to API
|
|
954
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
955
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
956
|
+
|
|
957
|
+
await fetch(`${apiUrl}/api/tasks/${taskId}/vcs`, {
|
|
958
|
+
method: "PATCH",
|
|
959
|
+
headers,
|
|
960
|
+
body: JSON.stringify({ vcsProvider, vcsRepo, vcsNumber, vcsUrl }),
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
vcsDetectedTasks.add(taskId);
|
|
964
|
+
console.log(
|
|
965
|
+
`[VCS] Linked task ${taskId.slice(0, 8)} to ${vcsProvider} ${vcsRepo}#${vcsNumber}`,
|
|
966
|
+
);
|
|
967
|
+
} catch {
|
|
968
|
+
// Fire-and-forget — detection failure should never block task execution
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
885
972
|
/** Save provider session ID on the active session (for pool tasks where realTaskId is unknown) */
|
|
886
973
|
async function saveProviderSessionIdOnActiveSession(
|
|
887
974
|
apiUrl: string,
|
|
@@ -1381,6 +1468,32 @@ async function fetchEpicTaskContext(
|
|
|
1381
1468
|
}
|
|
1382
1469
|
|
|
1383
1470
|
/** Spawn a provider session without blocking - returns immediately with tracking info */
|
|
1471
|
+
/**
|
|
1472
|
+
* Extract a key field from tool arguments for event tracking.
|
|
1473
|
+
* Returns a single-entry record with the most identifying arg for the tool.
|
|
1474
|
+
*/
|
|
1475
|
+
function extractToolKey(toolName: string, args: unknown): Record<string, string | undefined> {
|
|
1476
|
+
const a = args as Record<string, unknown>;
|
|
1477
|
+
switch (toolName) {
|
|
1478
|
+
case "Read":
|
|
1479
|
+
case "Edit":
|
|
1480
|
+
case "Write":
|
|
1481
|
+
return { filePath: a.file_path as string | undefined };
|
|
1482
|
+
case "Bash":
|
|
1483
|
+
return { description: a.description as string | undefined };
|
|
1484
|
+
case "Grep":
|
|
1485
|
+
return { pattern: a.pattern as string | undefined };
|
|
1486
|
+
case "Glob":
|
|
1487
|
+
return { pattern: a.pattern as string | undefined };
|
|
1488
|
+
case "Skill":
|
|
1489
|
+
return { skillName: a.skill as string | undefined };
|
|
1490
|
+
case "Agent":
|
|
1491
|
+
return { description: a.description as string | undefined };
|
|
1492
|
+
default:
|
|
1493
|
+
return {};
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1384
1497
|
async function spawnProviderProcess(
|
|
1385
1498
|
adapter: ReturnType<typeof createProviderAdapter>,
|
|
1386
1499
|
opts: {
|
|
@@ -1439,10 +1552,61 @@ async function spawnProviderProcess(
|
|
|
1439
1552
|
const logBuffer: LogBuffer = { lines: [], lastFlush: Date.now(), partialLine: "" };
|
|
1440
1553
|
const shouldStream = opts.apiUrl && opts.runnerSessionId && opts.iteration;
|
|
1441
1554
|
|
|
1555
|
+
// Event buffer (flushes to API periodically)
|
|
1556
|
+
interface BufferedEvent {
|
|
1557
|
+
category: string;
|
|
1558
|
+
event: string;
|
|
1559
|
+
status?: string;
|
|
1560
|
+
source: string;
|
|
1561
|
+
agentId?: string;
|
|
1562
|
+
taskId?: string;
|
|
1563
|
+
sessionId?: string;
|
|
1564
|
+
parentEventId?: string;
|
|
1565
|
+
numericValue?: number;
|
|
1566
|
+
durationMs?: number;
|
|
1567
|
+
data?: Record<string, unknown>;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
const eventBuffer: BufferedEvent[] = [];
|
|
1571
|
+
const EVENT_FLUSH_INTERVAL_MS = 5000;
|
|
1572
|
+
const EVENT_BUFFER_MAX = 50;
|
|
1573
|
+
|
|
1574
|
+
function bufferEvent(evt: BufferedEvent) {
|
|
1575
|
+
eventBuffer.push(evt);
|
|
1576
|
+
if (eventBuffer.length >= EVENT_BUFFER_MAX) {
|
|
1577
|
+
flushEvents();
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
async function flushEvents() {
|
|
1582
|
+
if (eventBuffer.length === 0) return;
|
|
1583
|
+
const batch = eventBuffer.splice(0);
|
|
1584
|
+
try {
|
|
1585
|
+
await fetch(`${opts.apiUrl}/api/events/batch`, {
|
|
1586
|
+
method: "POST",
|
|
1587
|
+
headers: {
|
|
1588
|
+
"Content-Type": "application/json",
|
|
1589
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
1590
|
+
"X-Agent-ID": opts.agentId,
|
|
1591
|
+
},
|
|
1592
|
+
body: JSON.stringify({ events: batch }),
|
|
1593
|
+
});
|
|
1594
|
+
} catch {
|
|
1595
|
+
// Non-blocking — event loss is acceptable
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
const eventFlushTimer = setInterval(flushEvents, EVENT_FLUSH_INTERVAL_MS);
|
|
1600
|
+
const sessionStartTime = Date.now();
|
|
1601
|
+
|
|
1442
1602
|
// Auto-progress throttle: don't update more than once per 3 seconds
|
|
1443
1603
|
let lastProgressTime = 0;
|
|
1444
1604
|
const PROGRESS_THROTTLE_MS = 3000;
|
|
1445
1605
|
|
|
1606
|
+
// Context usage throttle: max 1 snapshot per 30 seconds
|
|
1607
|
+
let lastContextPostTime = 0;
|
|
1608
|
+
const CONTEXT_THROTTLE_MS = 30_000;
|
|
1609
|
+
|
|
1446
1610
|
session.onEvent((event) => {
|
|
1447
1611
|
switch (event.type) {
|
|
1448
1612
|
case "session_init":
|
|
@@ -1462,6 +1626,16 @@ async function spawnProviderProcess(
|
|
|
1462
1626
|
console.warn(`[runner] Failed to save provider session on active session: ${err}`),
|
|
1463
1627
|
);
|
|
1464
1628
|
}
|
|
1629
|
+
|
|
1630
|
+
// Buffer session start event
|
|
1631
|
+
bufferEvent({
|
|
1632
|
+
category: "session",
|
|
1633
|
+
event: "session.start",
|
|
1634
|
+
source: "worker",
|
|
1635
|
+
agentId: opts.agentId,
|
|
1636
|
+
taskId: effectiveTaskId,
|
|
1637
|
+
sessionId: event.sessionId,
|
|
1638
|
+
});
|
|
1465
1639
|
break;
|
|
1466
1640
|
case "tool_start": {
|
|
1467
1641
|
// Auto-progress: report tool activity as task progress (throttled)
|
|
@@ -1475,13 +1649,105 @@ async function spawnProviderProcess(
|
|
|
1475
1649
|
);
|
|
1476
1650
|
}
|
|
1477
1651
|
}
|
|
1652
|
+
|
|
1653
|
+
// Buffer tool event
|
|
1654
|
+
bufferEvent({
|
|
1655
|
+
category: "tool",
|
|
1656
|
+
event: "tool.start",
|
|
1657
|
+
source: "worker",
|
|
1658
|
+
agentId: opts.agentId,
|
|
1659
|
+
taskId: effectiveTaskId,
|
|
1660
|
+
sessionId: opts.runnerSessionId,
|
|
1661
|
+
data: {
|
|
1662
|
+
toolName: event.toolName,
|
|
1663
|
+
toolCallId: event.toolCallId,
|
|
1664
|
+
...extractToolKey(event.toolName, event.args),
|
|
1665
|
+
clientTimestamp: new Date().toISOString(),
|
|
1666
|
+
},
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
// Also emit skill event when tool is Skill
|
|
1670
|
+
if (event.toolName === "Skill") {
|
|
1671
|
+
const args = event.args as Record<string, unknown>;
|
|
1672
|
+
bufferEvent({
|
|
1673
|
+
category: "skill",
|
|
1674
|
+
event: "skill.invoke",
|
|
1675
|
+
source: "worker",
|
|
1676
|
+
agentId: opts.agentId,
|
|
1677
|
+
taskId: effectiveTaskId,
|
|
1678
|
+
sessionId: opts.runnerSessionId,
|
|
1679
|
+
data: {
|
|
1680
|
+
skillName: args.skill as string,
|
|
1681
|
+
clientTimestamp: new Date().toISOString(),
|
|
1682
|
+
},
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1478
1685
|
break;
|
|
1479
1686
|
}
|
|
1480
1687
|
case "result":
|
|
1481
1688
|
// Cost save is handled in waitForCompletion().then() to ensure
|
|
1482
1689
|
// it completes before the process exits (fire-and-forget here
|
|
1483
1690
|
// races with container shutdown).
|
|
1691
|
+
|
|
1692
|
+
// Buffer session end event
|
|
1693
|
+
bufferEvent({
|
|
1694
|
+
category: "session",
|
|
1695
|
+
event: "session.end",
|
|
1696
|
+
source: "worker",
|
|
1697
|
+
agentId: opts.agentId,
|
|
1698
|
+
taskId: effectiveTaskId,
|
|
1699
|
+
sessionId: opts.runnerSessionId,
|
|
1700
|
+
status: event.isError ? "error" : "ok",
|
|
1701
|
+
durationMs: Date.now() - sessionStartTime,
|
|
1702
|
+
data: {
|
|
1703
|
+
model: event.cost.model,
|
|
1704
|
+
totalCostUsd: event.cost.totalCostUsd,
|
|
1705
|
+
inputTokens: event.cost.inputTokens,
|
|
1706
|
+
outputTokens: event.cost.outputTokens,
|
|
1707
|
+
},
|
|
1708
|
+
});
|
|
1709
|
+
break;
|
|
1710
|
+
case "context_usage": {
|
|
1711
|
+
const now2 = Date.now();
|
|
1712
|
+
if (now2 - lastContextPostTime >= CONTEXT_THROTTLE_MS) {
|
|
1713
|
+
lastContextPostTime = now2;
|
|
1714
|
+
fetch(`${opts.apiUrl}/api/tasks/${realTaskId}/context`, {
|
|
1715
|
+
method: "POST",
|
|
1716
|
+
headers: {
|
|
1717
|
+
"Content-Type": "application/json",
|
|
1718
|
+
"X-Agent-ID": opts.agentId,
|
|
1719
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
1720
|
+
},
|
|
1721
|
+
body: JSON.stringify({
|
|
1722
|
+
eventType: "progress",
|
|
1723
|
+
sessionId: opts.runnerSessionId,
|
|
1724
|
+
contextUsedTokens: event.contextUsedTokens,
|
|
1725
|
+
contextTotalTokens: event.contextTotalTokens,
|
|
1726
|
+
contextPercent: event.contextPercent,
|
|
1727
|
+
}),
|
|
1728
|
+
}).catch(() => {});
|
|
1729
|
+
}
|
|
1730
|
+
break;
|
|
1731
|
+
}
|
|
1732
|
+
case "compaction": {
|
|
1733
|
+
// Always record compaction events (no throttle)
|
|
1734
|
+
fetch(`${opts.apiUrl}/api/tasks/${realTaskId}/context`, {
|
|
1735
|
+
method: "POST",
|
|
1736
|
+
headers: {
|
|
1737
|
+
"Content-Type": "application/json",
|
|
1738
|
+
"X-Agent-ID": opts.agentId,
|
|
1739
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
1740
|
+
},
|
|
1741
|
+
body: JSON.stringify({
|
|
1742
|
+
eventType: "compaction",
|
|
1743
|
+
sessionId: opts.runnerSessionId,
|
|
1744
|
+
preCompactTokens: event.preCompactTokens,
|
|
1745
|
+
compactTrigger: event.compactTrigger,
|
|
1746
|
+
contextTotalTokens: event.contextTotalTokens,
|
|
1747
|
+
}),
|
|
1748
|
+
}).catch(() => {});
|
|
1484
1749
|
break;
|
|
1750
|
+
}
|
|
1485
1751
|
case "raw_log":
|
|
1486
1752
|
prettyPrintLine(event.content, opts.role);
|
|
1487
1753
|
if (shouldStream) {
|
|
@@ -1510,6 +1776,10 @@ async function spawnProviderProcess(
|
|
|
1510
1776
|
|
|
1511
1777
|
// Create promise that handles completion
|
|
1512
1778
|
const promise: Promise<ProviderResult> = session.waitForCompletion().then(async (result) => {
|
|
1779
|
+
// Stop event flush timer and do a final flush
|
|
1780
|
+
clearInterval(eventFlushTimer);
|
|
1781
|
+
await flushEvents();
|
|
1782
|
+
|
|
1513
1783
|
// Final log flush
|
|
1514
1784
|
if (shouldStream && logBuffer.lines.length > 0) {
|
|
1515
1785
|
await flushLogBuffer(logBuffer, {
|
|
@@ -1562,6 +1832,25 @@ async function spawnProviderProcess(
|
|
|
1562
1832
|
}
|
|
1563
1833
|
}
|
|
1564
1834
|
|
|
1835
|
+
// Post completion context usage snapshot
|
|
1836
|
+
if (result.cost && realTaskId) {
|
|
1837
|
+
fetch(`${opts.apiUrl}/api/tasks/${realTaskId}/context`, {
|
|
1838
|
+
method: "POST",
|
|
1839
|
+
headers: {
|
|
1840
|
+
"Content-Type": "application/json",
|
|
1841
|
+
"X-Agent-ID": opts.agentId,
|
|
1842
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
1843
|
+
},
|
|
1844
|
+
body: JSON.stringify({
|
|
1845
|
+
eventType: "completion",
|
|
1846
|
+
sessionId: opts.runnerSessionId,
|
|
1847
|
+
cumulativeInputTokens: result.cost.inputTokens ?? 0,
|
|
1848
|
+
cumulativeOutputTokens: result.cost.outputTokens ?? 0,
|
|
1849
|
+
contextTotalTokens: getContextWindowSize(result.cost.model || "default"),
|
|
1850
|
+
}),
|
|
1851
|
+
}).catch(() => {});
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1565
1854
|
return result;
|
|
1566
1855
|
});
|
|
1567
1856
|
|
|
@@ -1646,6 +1935,7 @@ async function checkCompletedProcesses(
|
|
|
1646
1935
|
result: ProviderResult;
|
|
1647
1936
|
triggerType?: string;
|
|
1648
1937
|
cursorUpdates?: Array<{ channelId: string; ts: string }>;
|
|
1938
|
+
workingDir?: string;
|
|
1649
1939
|
}> = [];
|
|
1650
1940
|
|
|
1651
1941
|
for (const [taskId, task] of state.activeTasks) {
|
|
@@ -1659,18 +1949,24 @@ async function checkCompletedProcesses(
|
|
|
1659
1949
|
result: task.result,
|
|
1660
1950
|
triggerType: task.triggerType,
|
|
1661
1951
|
cursorUpdates: task.cursorUpdates,
|
|
1952
|
+
workingDir: task.workingDir,
|
|
1662
1953
|
});
|
|
1663
1954
|
}
|
|
1664
1955
|
}
|
|
1665
1956
|
|
|
1666
1957
|
// Remove completed tasks from the map and ensure they're marked as finished
|
|
1667
|
-
for (const { taskId, result, cursorUpdates } of completedTasks) {
|
|
1958
|
+
for (const { taskId, result, cursorUpdates, workingDir } of completedTasks) {
|
|
1668
1959
|
state.activeTasks.delete(taskId);
|
|
1669
1960
|
|
|
1670
1961
|
if (apiConfig) {
|
|
1671
1962
|
removeActiveSession(apiConfig, taskId);
|
|
1672
1963
|
}
|
|
1673
1964
|
|
|
1965
|
+
// Detect VCS before finishing — last chance to link a PR
|
|
1966
|
+
if (apiConfig && workingDir && !vcsDetectedTasks.has(taskId)) {
|
|
1967
|
+
await detectVcsForTask(apiConfig.apiUrl, apiConfig.apiKey, taskId, workingDir);
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1674
1970
|
// Call the finish API to ensure task status is updated
|
|
1675
1971
|
// This is idempotent - if the agent already marked it, this is a no-op
|
|
1676
1972
|
if (apiConfig) {
|
|
@@ -1820,6 +2116,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1820
2116
|
let agentProfileName: string | undefined;
|
|
1821
2117
|
let agentDescription: string | undefined;
|
|
1822
2118
|
let agentSkillsSummary: { name: string; description: string }[] | undefined;
|
|
2119
|
+
let agentMcpServersSummary: string | undefined;
|
|
1823
2120
|
|
|
1824
2121
|
// Per-task repo context — set when processing a task with githubRepo
|
|
1825
2122
|
let currentRepoContext: BasePromptArgs["repoContext"] | undefined;
|
|
@@ -1839,6 +2136,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1839
2136
|
claudeMd: agentClaudeMd,
|
|
1840
2137
|
repoContext: currentRepoContext,
|
|
1841
2138
|
skillsSummary: agentSkillsSummary,
|
|
2139
|
+
mcpServersSummary: agentMcpServersSummary,
|
|
1842
2140
|
});
|
|
1843
2141
|
};
|
|
1844
2142
|
|
|
@@ -2094,6 +2392,42 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2094
2392
|
// Non-fatal — skills are optional
|
|
2095
2393
|
}
|
|
2096
2394
|
|
|
2395
|
+
// Fetch installed MCP servers for system prompt
|
|
2396
|
+
try {
|
|
2397
|
+
const mcpServersResp = await fetch(`${apiUrl}/api/agents/${agentId}/mcp-servers`, {
|
|
2398
|
+
headers: {
|
|
2399
|
+
Authorization: `Bearer ${apiKey}`,
|
|
2400
|
+
"X-Agent-ID": agentId,
|
|
2401
|
+
},
|
|
2402
|
+
});
|
|
2403
|
+
if (mcpServersResp.ok) {
|
|
2404
|
+
const mcpServersData = (await mcpServersResp.json()) as {
|
|
2405
|
+
servers: {
|
|
2406
|
+
name: string;
|
|
2407
|
+
transport: string;
|
|
2408
|
+
description: string | null;
|
|
2409
|
+
isActive: boolean;
|
|
2410
|
+
isEnabled: boolean;
|
|
2411
|
+
}[];
|
|
2412
|
+
};
|
|
2413
|
+
const activeMcpServers = mcpServersData.servers.filter(
|
|
2414
|
+
(s) => s.isActive && s.isEnabled,
|
|
2415
|
+
);
|
|
2416
|
+
if (activeMcpServers.length > 0) {
|
|
2417
|
+
agentMcpServersSummary = activeMcpServers
|
|
2418
|
+
.map(
|
|
2419
|
+
(s) => `- **${s.name}** (${s.transport}): ${s.description || "No description"}`,
|
|
2420
|
+
)
|
|
2421
|
+
.join("\n");
|
|
2422
|
+
console.log(
|
|
2423
|
+
`[${role}] Loaded ${activeMcpServers.length} MCP servers for system prompt`,
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
} catch {
|
|
2428
|
+
// Non-fatal — MCP servers are optional
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2097
2431
|
// Rebuild system prompt with identity
|
|
2098
2432
|
basePrompt = await buildSystemPrompt();
|
|
2099
2433
|
resolvedSystemPrompt = additionalSystemPrompt
|
|
@@ -2377,6 +2711,18 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2377
2711
|
// Check for completed processes first and ensure tasks are marked as finished
|
|
2378
2712
|
await checkCompletedProcesses(state, role, apiConfig);
|
|
2379
2713
|
|
|
2714
|
+
// Periodic VCS detection for running tasks (fire-and-forget, throttled per task)
|
|
2715
|
+
const now = Date.now();
|
|
2716
|
+
for (const [taskId, task] of state.activeTasks) {
|
|
2717
|
+
if (vcsDetectedTasks.has(taskId)) continue;
|
|
2718
|
+
const lastCheck = vcsCheckTimestamps.get(taskId) ?? 0;
|
|
2719
|
+
if (now - lastCheck < VCS_CHECK_INTERVAL) continue;
|
|
2720
|
+
if (!task.workingDir) continue;
|
|
2721
|
+
|
|
2722
|
+
vcsCheckTimestamps.set(taskId, now);
|
|
2723
|
+
detectVcsForTask(apiUrl, apiKey, taskId, task.workingDir);
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2380
2726
|
// Check for cancelled tasks and signal their subprocesses
|
|
2381
2727
|
if (state.activeTasks.size > 0) {
|
|
2382
2728
|
for (const [taskId, task] of state.activeTasks) {
|
|
@@ -2678,6 +3024,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2678
3024
|
|
|
2679
3025
|
// Attach trigger metadata for logging
|
|
2680
3026
|
runningTask.triggerType = trigger.type;
|
|
3027
|
+
runningTask.workingDir = effectiveCwd;
|
|
2681
3028
|
|
|
2682
3029
|
// Attach deferred cursor updates for channel_activity triggers
|
|
2683
3030
|
if (trigger.type === "channel_activity" && trigger.cursorUpdates) {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
createContextSnapshot,
|
|
5
|
+
getContextSnapshotsByTaskId,
|
|
6
|
+
getContextSummaryByTaskId,
|
|
7
|
+
getTaskById,
|
|
8
|
+
} from "../be/db";
|
|
9
|
+
import { ContextSnapshotEventTypeSchema } from "../types";
|
|
10
|
+
import { route } from "./route-def";
|
|
11
|
+
import { json, jsonError } from "./utils";
|
|
12
|
+
|
|
13
|
+
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const postContext = route({
|
|
16
|
+
method: "post",
|
|
17
|
+
path: "/api/tasks/{id}/context",
|
|
18
|
+
pattern: ["api", "tasks", null, "context"],
|
|
19
|
+
summary: "Record a context usage snapshot for a task",
|
|
20
|
+
tags: ["Tasks"],
|
|
21
|
+
params: z.object({ id: z.string() }),
|
|
22
|
+
body: z.object({
|
|
23
|
+
eventType: ContextSnapshotEventTypeSchema,
|
|
24
|
+
sessionId: z.string(),
|
|
25
|
+
contextUsedTokens: z.number().int().min(0).optional(),
|
|
26
|
+
contextTotalTokens: z.number().int().min(0).optional(),
|
|
27
|
+
contextPercent: z.number().min(0).max(100).optional(),
|
|
28
|
+
compactTrigger: z.enum(["auto", "manual"]).optional(),
|
|
29
|
+
preCompactTokens: z.number().int().min(0).optional(),
|
|
30
|
+
cumulativeInputTokens: z.number().int().min(0).optional(),
|
|
31
|
+
cumulativeOutputTokens: z.number().int().min(0).optional(),
|
|
32
|
+
}),
|
|
33
|
+
responses: {
|
|
34
|
+
200: { description: "Snapshot recorded" },
|
|
35
|
+
400: { description: "Validation error" },
|
|
36
|
+
404: { description: "Task not found" },
|
|
37
|
+
},
|
|
38
|
+
auth: { apiKey: true, agentId: true },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const getContext = route({
|
|
42
|
+
method: "get",
|
|
43
|
+
path: "/api/tasks/{id}/context",
|
|
44
|
+
pattern: ["api", "tasks", null, "context"],
|
|
45
|
+
summary: "Get context usage history for a task",
|
|
46
|
+
tags: ["Tasks"],
|
|
47
|
+
params: z.object({ id: z.string() }),
|
|
48
|
+
query: z.object({
|
|
49
|
+
limit: z.coerce.number().int().min(1).max(500).default(100),
|
|
50
|
+
}),
|
|
51
|
+
responses: {
|
|
52
|
+
200: { description: "Context snapshot history" },
|
|
53
|
+
404: { description: "Task not found" },
|
|
54
|
+
},
|
|
55
|
+
auth: { apiKey: true },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
export async function handleContext(
|
|
61
|
+
req: IncomingMessage,
|
|
62
|
+
res: ServerResponse,
|
|
63
|
+
pathSegments: string[],
|
|
64
|
+
queryParams: URLSearchParams,
|
|
65
|
+
myAgentId: string | undefined,
|
|
66
|
+
): Promise<boolean> {
|
|
67
|
+
if (postContext.match(req.method, pathSegments)) {
|
|
68
|
+
if (!myAgentId) {
|
|
69
|
+
jsonError(res, "Missing X-Agent-ID header", 400);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const parsed = await postContext.parse(req, res, pathSegments, queryParams);
|
|
74
|
+
if (!parsed) return true;
|
|
75
|
+
|
|
76
|
+
const task = getTaskById(parsed.params.id);
|
|
77
|
+
if (!task) {
|
|
78
|
+
jsonError(res, "Task not found", 404);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const snapshot = createContextSnapshot({
|
|
83
|
+
taskId: parsed.params.id,
|
|
84
|
+
agentId: myAgentId,
|
|
85
|
+
sessionId: parsed.body.sessionId,
|
|
86
|
+
eventType: parsed.body.eventType,
|
|
87
|
+
contextUsedTokens: parsed.body.contextUsedTokens,
|
|
88
|
+
contextTotalTokens: parsed.body.contextTotalTokens,
|
|
89
|
+
contextPercent: parsed.body.contextPercent,
|
|
90
|
+
compactTrigger: parsed.body.compactTrigger,
|
|
91
|
+
preCompactTokens: parsed.body.preCompactTokens,
|
|
92
|
+
cumulativeInputTokens: parsed.body.cumulativeInputTokens,
|
|
93
|
+
cumulativeOutputTokens: parsed.body.cumulativeOutputTokens,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
json(res, { ok: true, snapshotId: snapshot.id });
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (getContext.match(req.method, pathSegments)) {
|
|
101
|
+
const parsed = await getContext.parse(req, res, pathSegments, queryParams);
|
|
102
|
+
if (!parsed) return true;
|
|
103
|
+
|
|
104
|
+
const task = getTaskById(parsed.params.id);
|
|
105
|
+
if (!task) {
|
|
106
|
+
jsonError(res, "Task not found", 404);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const snapshots = getContextSnapshotsByTaskId(parsed.params.id, parsed.query.limit);
|
|
111
|
+
const summary = getContextSummaryByTaskId(parsed.params.id);
|
|
112
|
+
|
|
113
|
+
json(res, { snapshots, summary });
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return false;
|
|
118
|
+
}
|