@desplega.ai/agent-swarm 1.79.4 → 1.80.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openapi.json +496 -32
- package/package.json +14 -6
- package/src/artifact-sdk/server.ts +2 -1
- package/src/be/db.ts +102 -31
- package/src/be/migrations/063_cost_context_schema_relax.sql +133 -0
- package/src/be/migrations/064_scripts.sql +39 -0
- package/src/be/migrations/065_script_embeddings.sql +7 -0
- package/src/be/pricing-normalize.ts +81 -0
- package/src/be/scripts/db.ts +391 -0
- package/src/be/scripts/embeddings.ts +231 -0
- package/src/be/scripts/maintenance.ts +9 -0
- package/src/be/scripts/typecheck.ts +193 -0
- package/src/be/seed-pricing.ts +293 -0
- package/src/cli.tsx +22 -5
- package/src/commands/artifact.ts +3 -2
- package/src/commands/claude-managed-setup.ts +21 -4
- package/src/commands/codex-login.ts +5 -3
- package/src/commands/onboard.tsx +2 -1
- package/src/commands/runner.ts +663 -246
- package/src/commands/setup.tsx +5 -3
- package/src/hooks/hook.ts +4 -3
- package/src/http/context.ts +6 -2
- package/src/http/index.ts +126 -68
- package/src/http/memory.ts +28 -0
- package/src/http/openapi.ts +1 -0
- package/src/http/page-proxy.ts +2 -1
- package/src/http/route-def.ts +1 -0
- package/src/http/schedules.ts +37 -0
- package/src/http/scripts.ts +381 -0
- package/src/http/session-data.ts +74 -23
- package/src/linear/outbound.ts +9 -2
- package/src/otel-impl.ts +200 -0
- package/src/otel.ts +132 -0
- package/src/providers/claude-adapter.ts +52 -6
- package/src/providers/claude-managed-adapter.ts +43 -17
- package/src/providers/claude-managed-pricing.ts +34 -0
- package/src/providers/codex-adapter.ts +38 -27
- package/src/providers/codex-models.ts +22 -3
- package/src/providers/devin-adapter.ts +11 -0
- package/src/providers/opencode-adapter.ts +31 -7
- package/src/providers/pi-mono-adapter.ts +39 -7
- package/src/providers/pricing-sources.md +52 -0
- package/src/providers/swarm-events-shared.ts +8 -4
- package/src/providers/types.ts +33 -10
- package/src/scripts-runtime/ctx.ts +23 -0
- package/src/scripts-runtime/eval-harness.ts +39 -0
- package/src/scripts-runtime/executors/native.ts +229 -0
- package/src/scripts-runtime/executors/registry.ts +16 -0
- package/src/scripts-runtime/executors/types.ts +63 -0
- package/src/scripts-runtime/extract-signature.ts +81 -0
- package/src/scripts-runtime/import-allowlist.ts +109 -0
- package/src/scripts-runtime/loader.ts +96 -0
- package/src/scripts-runtime/redacted.ts +48 -0
- package/src/scripts-runtime/sdk-allowlist.ts +29 -0
- package/src/scripts-runtime/stdlib/fetch.ts +46 -0
- package/src/scripts-runtime/stdlib/glob.ts +8 -0
- package/src/scripts-runtime/stdlib/grep.ts +34 -0
- package/src/scripts-runtime/stdlib/index.ts +16 -0
- package/src/scripts-runtime/stdlib/table.ts +17 -0
- package/src/scripts-runtime/swarm-config.ts +35 -0
- package/src/scripts-runtime/swarm-sdk.ts +197 -0
- package/src/scripts-runtime/types/stdlib.d.ts +104 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
- package/src/server.ts +18 -0
- package/src/tests/api-key.test.ts +33 -0
- package/src/tests/claude-managed-adapter.test.ts +17 -3
- package/src/tests/claude-managed-setup.test.ts +10 -1
- package/src/tests/codex-adapter.test.ts +20 -19
- package/src/tests/codex-login.test.ts +1 -1
- package/src/tests/context-snapshot.test.ts +2 -2
- package/src/tests/context-window.test.ts +65 -1
- package/src/tests/devin-adapter.test.ts +2 -0
- package/src/tests/http/context-routes.test.ts +161 -0
- package/src/tests/linear-outbound-sync.test.ts +109 -0
- package/src/tests/mcp-tools.test.ts +69 -0
- package/src/tests/migration-063-schema-relax.test.ts +109 -0
- package/src/tests/opencode-adapter.test.ts +146 -1
- package/src/tests/otel-impl-secret-scrubbing.test.ts +33 -0
- package/src/tests/pages-view-count.test.ts +30 -5
- package/src/tests/providers/codex-cost.test.ts +18 -0
- package/src/tests/providers/opencode-cost.test.ts +74 -0
- package/src/tests/providers/pi-cost.test.ts +128 -0
- package/src/tests/redacted.test.ts +29 -0
- package/src/tests/runner-tool-spans.test.ts +268 -0
- package/src/tests/script-executor-conformance.test.ts +142 -0
- package/src/tests/script-executor-registry.test.ts +17 -0
- package/src/tests/scripts-db.test.ts +329 -0
- package/src/tests/scripts-embeddings.test.ts +291 -0
- package/src/tests/scripts-extract-signature.test.ts +47 -0
- package/src/tests/scripts-http.test.ts +350 -0
- package/src/tests/scripts-import-allowlist.test.ts +55 -0
- package/src/tests/scripts-mcp-e2e.test.ts +269 -0
- package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
- package/src/tests/scripts-runtime.test.ts +289 -0
- package/src/tests/sdk-allowlist.test.ts +59 -0
- package/src/tests/secret-scrubber.test.ts +54 -1
- package/src/tests/session-costs-codex-recompute.test.ts +35 -22
- package/src/tests/session-costs-model-key-normalize.test.ts +271 -0
- package/src/tests/session-costs-recompute-all-providers.test.ts +170 -0
- package/src/tests/store-progress-cost.test.ts +6 -1
- package/src/tests/swarm-config.test.ts +38 -0
- package/src/tests/tool-annotations.test.ts +2 -2
- package/src/tests/tool-call-progress.test.ts +30 -0
- package/src/tests/workflow-e2e.test.ts +218 -0
- package/src/tests/workflow-executors.test.ts +32 -2
- package/src/tests/workflow-input-redaction.test.ts +232 -0
- package/src/tests/workflow-swarm-script.test.ts +273 -0
- package/src/tools/memory-rate.ts +2 -1
- package/src/tools/script-common.ts +88 -0
- package/src/tools/script-delete.ts +35 -0
- package/src/tools/script-query-types.ts +37 -0
- package/src/tools/script-run.ts +43 -0
- package/src/tools/script-search.ts +32 -0
- package/src/tools/script-upsert.ts +43 -0
- package/src/tools/store-progress.ts +16 -60
- package/src/tools/tool-config.ts +7 -0
- package/src/tools/utils.ts +65 -12
- package/src/types.ts +122 -10
- package/src/utils/api-key.ts +28 -0
- package/src/utils/context-window.ts +104 -4
- package/src/utils/page-session.ts +8 -6
- package/src/utils/secret-scrubber.ts +29 -1
- package/src/workflows/engine.ts +12 -4
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/script.ts +12 -1
- package/src/workflows/executors/swarm-script.ts +170 -0
- package/src/workflows/input.ts +65 -0
- package/src/workflows/recovery.ts +31 -3
- package/src/workflows/resume.ts +43 -5
package/src/commands/runner.ts
CHANGED
|
@@ -2,6 +2,16 @@ import { existsSync, statSync } from "node:fs";
|
|
|
2
2
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { ensure, initialize } from "@desplega.ai/business-use";
|
|
4
4
|
import type { TemplateResponse } from "../../templates/schema.ts";
|
|
5
|
+
import {
|
|
6
|
+
type Attributes,
|
|
7
|
+
initOtel,
|
|
8
|
+
injectTraceContext,
|
|
9
|
+
isPollTracingEnabled,
|
|
10
|
+
type SwarmSpan,
|
|
11
|
+
startSpan,
|
|
12
|
+
withSpan,
|
|
13
|
+
withSpanContext,
|
|
14
|
+
} from "../otel.ts";
|
|
5
15
|
import { type BasePromptArgs, getBasePrompt } from "../prompts/base-prompt.ts";
|
|
6
16
|
import {
|
|
7
17
|
generateDefaultClaudeMd,
|
|
@@ -15,12 +25,14 @@ import { authJsonToCredentialSelection } from "../providers/codex-oauth/auth-jso
|
|
|
15
25
|
import {
|
|
16
26
|
type CostData,
|
|
17
27
|
createProviderAdapter,
|
|
28
|
+
type ProviderEvent,
|
|
18
29
|
type ProviderResult,
|
|
19
30
|
type ProviderSession,
|
|
20
31
|
type ProviderSessionConfig,
|
|
21
32
|
} from "../providers/index.ts";
|
|
22
33
|
import { initTelemetry, telemetry } from "../telemetry.ts";
|
|
23
34
|
import type { ProviderName, RepoGuidelines } from "../types.ts";
|
|
35
|
+
import { getApiKey } from "../utils/api-key.ts";
|
|
24
36
|
import { computeBudgetBackoffMs } from "../utils/budget-backoff.ts";
|
|
25
37
|
import { getContextWindowSize } from "../utils/context-window.ts";
|
|
26
38
|
import { type CredentialSelection, resolveCredentialPools } from "../utils/credentials.ts";
|
|
@@ -402,6 +414,12 @@ const SWARM_TOOL_LABELS: Record<string, string | null> = {
|
|
|
402
414
|
"list-services": "🔌 Listing services",
|
|
403
415
|
"unregister-service": "🔌 Unregistering service",
|
|
404
416
|
"update-service-status": "🔌 Updating service status",
|
|
417
|
+
// Reusable scripts
|
|
418
|
+
"script-search": "📜 Searching scripts",
|
|
419
|
+
"script-run": "📜 Running script",
|
|
420
|
+
"script-upsert": "📜 Saving script",
|
|
421
|
+
"script-delete": "📜 Deleting script",
|
|
422
|
+
"script-query-types": "📜 Reading script types",
|
|
405
423
|
};
|
|
406
424
|
|
|
407
425
|
/** Convert kebab-case to sentence case: "get-task-details" → "Get task details" */
|
|
@@ -477,13 +495,11 @@ export function toolCallToProgress(toolName: string, args: unknown): string | nu
|
|
|
477
495
|
}
|
|
478
496
|
|
|
479
497
|
// Pi-mono exposes tools from the built-in swarm MCP endpoint as bare
|
|
480
|
-
// names ("store-progress", "send-task", ...), not as mcp__ names.
|
|
498
|
+
// names ("store-progress", "send-task", "script-run", ...), not as mcp__ names.
|
|
481
499
|
// Treat those names as agent-swarm tools so activity stays readable.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (label) return label;
|
|
486
|
-
}
|
|
500
|
+
const label = SWARM_TOOL_LABELS[toolName];
|
|
501
|
+
if (label === null) return null;
|
|
502
|
+
if (label) return label;
|
|
487
503
|
|
|
488
504
|
return `🔧 ${toolName}`;
|
|
489
505
|
}
|
|
@@ -1503,6 +1519,25 @@ async function registerAgent(opts: {
|
|
|
1503
1519
|
|
|
1504
1520
|
/** Poll for triggers via HTTP API */
|
|
1505
1521
|
async function pollForTrigger(opts: PollOptions): Promise<Trigger | null> {
|
|
1522
|
+
if (!isPollTracingEnabled()) {
|
|
1523
|
+
return pollForTriggerOnce(opts);
|
|
1524
|
+
}
|
|
1525
|
+
return withSpan(
|
|
1526
|
+
"worker.poll",
|
|
1527
|
+
async (span) => {
|
|
1528
|
+
const trigger = await pollForTriggerOnce(opts);
|
|
1529
|
+
span.setAttribute("agentswarm.poll.result", trigger ? trigger.type : "empty");
|
|
1530
|
+
return trigger;
|
|
1531
|
+
},
|
|
1532
|
+
{
|
|
1533
|
+
"agent.id": opts.agentId,
|
|
1534
|
+
"agentswarm.worker.poll_timeout_ms": opts.pollTimeout,
|
|
1535
|
+
"agentswarm.worker.poll_interval_ms": opts.pollInterval,
|
|
1536
|
+
},
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
async function pollForTriggerOnce(opts: PollOptions): Promise<Trigger | null> {
|
|
1506
1541
|
const startTime = Date.now();
|
|
1507
1542
|
const headers: Record<string, string> = {
|
|
1508
1543
|
"X-Agent-ID": opts.agentId,
|
|
@@ -1510,6 +1545,7 @@ async function pollForTrigger(opts: PollOptions): Promise<Trigger | null> {
|
|
|
1510
1545
|
if (opts.apiKey) {
|
|
1511
1546
|
headers.Authorization = `Bearer ${opts.apiKey}`;
|
|
1512
1547
|
}
|
|
1548
|
+
injectTraceContext(headers);
|
|
1513
1549
|
|
|
1514
1550
|
while (Date.now() - startTime < opts.pollTimeout) {
|
|
1515
1551
|
try {
|
|
@@ -1739,6 +1775,198 @@ function extractToolKey(toolName: string, args: unknown): Record<string, string
|
|
|
1739
1775
|
}
|
|
1740
1776
|
}
|
|
1741
1777
|
|
|
1778
|
+
const OTEL_PREVIEW_LIMIT = 500;
|
|
1779
|
+
|
|
1780
|
+
function telemetryPreview(value: unknown): string | undefined {
|
|
1781
|
+
if (value === undefined) return undefined;
|
|
1782
|
+
try {
|
|
1783
|
+
const serialized = typeof value === "string" ? value : JSON.stringify(value);
|
|
1784
|
+
if (!serialized) return undefined;
|
|
1785
|
+
const scrubbed = scrubSecrets(serialized);
|
|
1786
|
+
return scrubbed.length > OTEL_PREVIEW_LIMIT
|
|
1787
|
+
? `${scrubbed.slice(0, OTEL_PREVIEW_LIMIT)}...`
|
|
1788
|
+
: scrubbed;
|
|
1789
|
+
} catch {
|
|
1790
|
+
return "[unserializable]";
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
type ToolTelemetry = {
|
|
1795
|
+
kind: "mcp" | "harness" | "skill" | "agent" | "shell" | "file" | "unknown";
|
|
1796
|
+
name: string;
|
|
1797
|
+
normalizedName: string;
|
|
1798
|
+
mcpServer?: string;
|
|
1799
|
+
mcpTool?: string;
|
|
1800
|
+
};
|
|
1801
|
+
|
|
1802
|
+
function classifyTool(toolName: string, args: unknown): ToolTelemetry {
|
|
1803
|
+
const argRecord = args && typeof args === "object" ? (args as Record<string, unknown>) : {};
|
|
1804
|
+
|
|
1805
|
+
if (toolName.startsWith("mcp__")) {
|
|
1806
|
+
const [, server, ...toolParts] = toolName.split("__");
|
|
1807
|
+
const mcpTool = toolParts.join("__") || undefined;
|
|
1808
|
+
return {
|
|
1809
|
+
kind: "mcp",
|
|
1810
|
+
name: toolName,
|
|
1811
|
+
normalizedName: server && mcpTool ? `${server}.${mcpTool}` : toolName,
|
|
1812
|
+
mcpServer: server,
|
|
1813
|
+
mcpTool,
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
if (typeof argRecord.server === "string" && typeof argRecord.tool === "string") {
|
|
1818
|
+
return {
|
|
1819
|
+
kind: "mcp",
|
|
1820
|
+
name: toolName,
|
|
1821
|
+
normalizedName: `${argRecord.server}.${argRecord.tool}`,
|
|
1822
|
+
mcpServer: argRecord.server,
|
|
1823
|
+
mcpTool: argRecord.tool,
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
if (toolName.includes(":")) {
|
|
1828
|
+
const [server, ...toolParts] = toolName.split(":");
|
|
1829
|
+
const mcpTool = toolParts.join(":") || undefined;
|
|
1830
|
+
return {
|
|
1831
|
+
kind: "mcp",
|
|
1832
|
+
name: toolName,
|
|
1833
|
+
normalizedName: server && mcpTool ? `${server}.${mcpTool}` : toolName,
|
|
1834
|
+
mcpServer: server,
|
|
1835
|
+
mcpTool,
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
switch (toolName) {
|
|
1840
|
+
case "Bash":
|
|
1841
|
+
case "bash":
|
|
1842
|
+
case "command_execution":
|
|
1843
|
+
return { kind: "shell", name: toolName, normalizedName: toolName };
|
|
1844
|
+
case "Read":
|
|
1845
|
+
case "Edit":
|
|
1846
|
+
case "Write":
|
|
1847
|
+
case "Delete":
|
|
1848
|
+
case "Grep":
|
|
1849
|
+
case "Glob":
|
|
1850
|
+
case "file_change":
|
|
1851
|
+
return { kind: "file", name: toolName, normalizedName: toolName };
|
|
1852
|
+
case "Skill":
|
|
1853
|
+
return { kind: "skill", name: toolName, normalizedName: toolName };
|
|
1854
|
+
case "Agent":
|
|
1855
|
+
return { kind: "agent", name: toolName, normalizedName: toolName };
|
|
1856
|
+
default:
|
|
1857
|
+
return { kind: "harness", name: toolName, normalizedName: toolName };
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
function providerEventAttributes(event: ProviderEvent): Attributes {
|
|
1862
|
+
switch (event.type) {
|
|
1863
|
+
case "session_init":
|
|
1864
|
+
return {
|
|
1865
|
+
"agentswarm.provider.session_id": event.sessionId,
|
|
1866
|
+
"agentswarm.provider.name": event.provider,
|
|
1867
|
+
"agentswarm.provider.meta_preview": telemetryPreview(event.providerMeta),
|
|
1868
|
+
};
|
|
1869
|
+
case "message":
|
|
1870
|
+
return {
|
|
1871
|
+
"gen_ai.message.role": event.role,
|
|
1872
|
+
"gen_ai.message.content_preview": telemetryPreview(event.content),
|
|
1873
|
+
};
|
|
1874
|
+
case "tool_start":
|
|
1875
|
+
case "tool_end": {
|
|
1876
|
+
const tool = classifyTool(
|
|
1877
|
+
event.toolName,
|
|
1878
|
+
event.type === "tool_start" ? event.args : undefined,
|
|
1879
|
+
);
|
|
1880
|
+
return {
|
|
1881
|
+
"agentswarm.tool.name": tool.name,
|
|
1882
|
+
"agentswarm.tool.normalized_name": tool.normalizedName,
|
|
1883
|
+
"agentswarm.tool.kind": tool.kind,
|
|
1884
|
+
"agentswarm.tool.call_id": event.toolCallId,
|
|
1885
|
+
"mcp.server.name": tool.mcpServer,
|
|
1886
|
+
"mcp.tool.name": tool.mcpTool,
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
case "result":
|
|
1890
|
+
return {
|
|
1891
|
+
"agentswarm.session.outcome": event.isError ? "error" : "ok",
|
|
1892
|
+
"agentswarm.error.category": event.errorCategory,
|
|
1893
|
+
"gen_ai.response.model": event.cost.model,
|
|
1894
|
+
"gen_ai.usage.input_tokens": event.cost.inputTokens ?? 0,
|
|
1895
|
+
"gen_ai.usage.output_tokens": event.cost.outputTokens ?? 0,
|
|
1896
|
+
"agentswarm.cost.total_usd": event.cost.totalCostUsd ?? 0,
|
|
1897
|
+
};
|
|
1898
|
+
case "error":
|
|
1899
|
+
return {
|
|
1900
|
+
"agentswarm.error.category": event.category,
|
|
1901
|
+
"exception.message": telemetryPreview(event.message),
|
|
1902
|
+
};
|
|
1903
|
+
case "progress":
|
|
1904
|
+
return { "agentswarm.progress.message": telemetryPreview(event.message) };
|
|
1905
|
+
case "context_usage":
|
|
1906
|
+
return {
|
|
1907
|
+
"agentswarm.context.used_tokens": event.contextUsedTokens ?? undefined,
|
|
1908
|
+
"agentswarm.context.total_tokens": event.contextTotalTokens ?? undefined,
|
|
1909
|
+
"agentswarm.context.percent": event.contextPercent ?? undefined,
|
|
1910
|
+
"gen_ai.usage.output_tokens": event.outputTokens ?? undefined,
|
|
1911
|
+
};
|
|
1912
|
+
case "compaction":
|
|
1913
|
+
return {
|
|
1914
|
+
"agentswarm.compaction.trigger": event.compactTrigger,
|
|
1915
|
+
"agentswarm.compaction.pre_tokens": event.preCompactTokens,
|
|
1916
|
+
"agentswarm.context.total_tokens": event.contextTotalTokens,
|
|
1917
|
+
};
|
|
1918
|
+
case "custom":
|
|
1919
|
+
return {
|
|
1920
|
+
"agentswarm.provider.event_name": event.name,
|
|
1921
|
+
"agentswarm.provider.event_data_preview": telemetryPreview(event.data),
|
|
1922
|
+
};
|
|
1923
|
+
case "raw_log":
|
|
1924
|
+
case "raw_stderr":
|
|
1925
|
+
return {};
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
/**
|
|
1930
|
+
* Entry shape for the `activeToolSpans` map maintained by `runWithSession`.
|
|
1931
|
+
* Exported for unit tests that exercise `implicitCloseActiveToolSpans`.
|
|
1932
|
+
*/
|
|
1933
|
+
export type ActiveToolSpanEntry = {
|
|
1934
|
+
span: SwarmSpan;
|
|
1935
|
+
startedAt: number;
|
|
1936
|
+
};
|
|
1937
|
+
|
|
1938
|
+
/**
|
|
1939
|
+
* Closes any still-open tool spans (both `worker.tool` and `worker.mcp.tool`)
|
|
1940
|
+
* in `activeToolSpans` at the assistant-message boundary, tagging them with
|
|
1941
|
+
* `agentswarm.tool.implicit_close=true` and removing them from the map.
|
|
1942
|
+
*
|
|
1943
|
+
* The Claude SDK adapter doesn't emit per-tool completion events for any
|
|
1944
|
+
* tool kind, MCP or harness-side — so the assistant-message boundary serves
|
|
1945
|
+
* as an implicit `tool_end` for everything. Spans closed here are still
|
|
1946
|
+
* successful (code 1, OK); the boundary is just a signal that the prior
|
|
1947
|
+
* tool turn is done.
|
|
1948
|
+
*
|
|
1949
|
+
* Returns the number of spans closed (handy for tests / metrics).
|
|
1950
|
+
*/
|
|
1951
|
+
export function implicitCloseActiveToolSpans(
|
|
1952
|
+
activeToolSpans: Map<string, ActiveToolSpanEntry>,
|
|
1953
|
+
now: number = Date.now(),
|
|
1954
|
+
): number {
|
|
1955
|
+
let closed = 0;
|
|
1956
|
+
for (const [toolCallId, active] of activeToolSpans) {
|
|
1957
|
+
active.span.setAttributes({
|
|
1958
|
+
"agentswarm.tool.duration_ms": now - active.startedAt,
|
|
1959
|
+
"agentswarm.tool.implicit_close": true,
|
|
1960
|
+
"agentswarm.tool.call_id": toolCallId,
|
|
1961
|
+
});
|
|
1962
|
+
active.span.setStatus({ code: 1 });
|
|
1963
|
+
active.span.end();
|
|
1964
|
+
activeToolSpans.delete(toolCallId);
|
|
1965
|
+
closed++;
|
|
1966
|
+
}
|
|
1967
|
+
return closed;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1742
1970
|
async function spawnProviderProcess(
|
|
1743
1971
|
adapter: ReturnType<typeof createProviderAdapter>,
|
|
1744
1972
|
opts: {
|
|
@@ -1806,7 +2034,25 @@ async function spawnProviderProcess(
|
|
|
1806
2034
|
env: freshEnv as Record<string, string>,
|
|
1807
2035
|
};
|
|
1808
2036
|
|
|
1809
|
-
const session = await
|
|
2037
|
+
const session = await withSpan(
|
|
2038
|
+
"worker.session.create",
|
|
2039
|
+
async (span) => {
|
|
2040
|
+
const createdSession = await adapter.createSession(config);
|
|
2041
|
+
span.setAttribute("agentswarm.provider.session_id", createdSession.sessionId || "pending");
|
|
2042
|
+
return createdSession;
|
|
2043
|
+
},
|
|
2044
|
+
{
|
|
2045
|
+
"agent.id": opts.agentId,
|
|
2046
|
+
"agentswarm.task.id": effectiveTaskId,
|
|
2047
|
+
"agentswarm.task.real_id": realTaskId,
|
|
2048
|
+
"agentswarm.agent.role": opts.role,
|
|
2049
|
+
"agentswarm.harness_provider": opts.harnessProvider,
|
|
2050
|
+
"gen_ai.request.model": model || undefined,
|
|
2051
|
+
"agentswarm.session.cwd": config.cwd,
|
|
2052
|
+
"agentswarm.session.vcs_repo": opts.vcsRepo,
|
|
2053
|
+
"agentswarm.session.additional_args_count": opts.additionalArgs?.length ?? 0,
|
|
2054
|
+
},
|
|
2055
|
+
);
|
|
1810
2056
|
const initialModelReport = buildLatestModelReport({
|
|
1811
2057
|
model,
|
|
1812
2058
|
taskModel: opts.model,
|
|
@@ -1884,6 +2130,25 @@ async function spawnProviderProcess(
|
|
|
1884
2130
|
|
|
1885
2131
|
const eventFlushTimer = setInterval(flushEvents, EVENT_FLUSH_INTERVAL_MS);
|
|
1886
2132
|
const sessionStartTime = Date.now();
|
|
2133
|
+
let providerSessionId = session.sessionId;
|
|
2134
|
+
const activeToolSpans = new Map<
|
|
2135
|
+
string,
|
|
2136
|
+
{
|
|
2137
|
+
span: SwarmSpan;
|
|
2138
|
+
startedAt: number;
|
|
2139
|
+
}
|
|
2140
|
+
>();
|
|
2141
|
+
const sessionSpan = startSpan("worker.session", {
|
|
2142
|
+
"agent.id": opts.agentId,
|
|
2143
|
+
"agentswarm.task.id": effectiveTaskId,
|
|
2144
|
+
"agentswarm.task.real_id": realTaskId,
|
|
2145
|
+
"agentswarm.agent.role": opts.role,
|
|
2146
|
+
"agentswarm.harness_provider": opts.harnessProvider,
|
|
2147
|
+
"agentswarm.provider.session_id": providerSessionId,
|
|
2148
|
+
"agentswarm.session.cwd": config.cwd,
|
|
2149
|
+
"agentswarm.session.vcs_repo": opts.vcsRepo,
|
|
2150
|
+
"gen_ai.request.model": model || undefined,
|
|
2151
|
+
});
|
|
1887
2152
|
|
|
1888
2153
|
// Auto-progress throttle: don't update more than once per 3 seconds
|
|
1889
2154
|
let lastProgressTime = 0;
|
|
@@ -1892,130 +2157,248 @@ async function spawnProviderProcess(
|
|
|
1892
2157
|
let lastContextPostTime = 0;
|
|
1893
2158
|
const CONTEXT_THROTTLE_MS = 30_000;
|
|
1894
2159
|
|
|
1895
|
-
session
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
break;
|
|
1931
|
-
case "tool_start": {
|
|
1932
|
-
// Auto-progress: report tool activity as task progress (throttled)
|
|
1933
|
-
const now = Date.now();
|
|
1934
|
-
if (effectiveTaskId && opts.apiUrl && now - lastProgressTime >= PROGRESS_THROTTLE_MS) {
|
|
1935
|
-
const progress = toolCallToProgress(event.toolName, event.args);
|
|
1936
|
-
if (progress) {
|
|
1937
|
-
lastProgressTime = now;
|
|
1938
|
-
updateProgressViaAPI(opts.apiUrl, opts.apiKey, effectiveTaskId, progress).catch(
|
|
1939
|
-
() => {},
|
|
2160
|
+
// Phase 10: accumulate per-turn output tokens across the session so progress
|
|
2161
|
+
// snapshots carry a real `cumulativeOutputTokens` (was 0 until completion).
|
|
2162
|
+
let cumulativeProgressOutputTokens = 0;
|
|
2163
|
+
|
|
2164
|
+
session.onEvent((event) =>
|
|
2165
|
+
withSpanContext(sessionSpan, () => {
|
|
2166
|
+
sessionSpan.addEvent(`provider.${event.type}`, providerEventAttributes(event));
|
|
2167
|
+
switch (event.type) {
|
|
2168
|
+
case "session_init":
|
|
2169
|
+
providerSessionId = event.sessionId;
|
|
2170
|
+
sessionSpan.setAttributes({
|
|
2171
|
+
"agentswarm.provider.session_id": providerSessionId,
|
|
2172
|
+
"agentswarm.provider.name": event.provider,
|
|
2173
|
+
"agentswarm.provider.meta_preview": telemetryPreview(event.providerMeta),
|
|
2174
|
+
});
|
|
2175
|
+
if (realTaskId) {
|
|
2176
|
+
saveProviderSessionId(
|
|
2177
|
+
opts.apiUrl,
|
|
2178
|
+
opts.apiKey,
|
|
2179
|
+
realTaskId,
|
|
2180
|
+
event.sessionId,
|
|
2181
|
+
event.provider,
|
|
2182
|
+
event.providerMeta,
|
|
2183
|
+
model,
|
|
2184
|
+
).catch((err) => console.warn(`[runner] Failed to save session ID: ${err}`));
|
|
2185
|
+
} else {
|
|
2186
|
+
// Pool task: save provider session ID on active session so it can be
|
|
2187
|
+
// propagated to the real task when the agent claims one
|
|
2188
|
+
saveProviderSessionIdOnActiveSession(
|
|
2189
|
+
opts.apiUrl,
|
|
2190
|
+
opts.apiKey,
|
|
2191
|
+
effectiveTaskId,
|
|
2192
|
+
event.sessionId,
|
|
2193
|
+
).catch((err) =>
|
|
2194
|
+
console.warn(`[runner] Failed to save provider session on active session: ${err}`),
|
|
1940
2195
|
);
|
|
1941
2196
|
}
|
|
2197
|
+
|
|
2198
|
+
// Buffer session start event
|
|
2199
|
+
bufferEvent({
|
|
2200
|
+
category: "session",
|
|
2201
|
+
event: "session.start",
|
|
2202
|
+
source: "worker",
|
|
2203
|
+
agentId: opts.agentId,
|
|
2204
|
+
taskId: effectiveTaskId,
|
|
2205
|
+
sessionId: event.sessionId,
|
|
2206
|
+
});
|
|
2207
|
+
break;
|
|
2208
|
+
case "message": {
|
|
2209
|
+
// Assistant-message boundary acts as an implicit tool_end for ALL
|
|
2210
|
+
// still-open tool spans (both `worker.tool` and `worker.mcp.tool`).
|
|
2211
|
+
// The Claude SDK adapter doesn't emit per-tool completion events for
|
|
2212
|
+
// any tool kind, so without this their spans would only close at
|
|
2213
|
+
// session shutdown via `closeActiveToolSpans` and report wall-clock
|
|
2214
|
+
// duration from tool_start to session end.
|
|
2215
|
+
if (event.role === "assistant") {
|
|
2216
|
+
implicitCloseActiveToolSpans(activeToolSpans);
|
|
2217
|
+
}
|
|
2218
|
+
break;
|
|
1942
2219
|
}
|
|
2220
|
+
case "tool_start": {
|
|
2221
|
+
const tool = classifyTool(event.toolName, event.args);
|
|
2222
|
+
const toolSpan = startSpan(tool.kind === "mcp" ? "worker.mcp.tool" : "worker.tool", {
|
|
2223
|
+
"agent.id": opts.agentId,
|
|
2224
|
+
"agentswarm.task.id": effectiveTaskId,
|
|
2225
|
+
"agentswarm.task.real_id": realTaskId,
|
|
2226
|
+
"agentswarm.agent.role": opts.role,
|
|
2227
|
+
"agentswarm.harness_provider": opts.harnessProvider,
|
|
2228
|
+
"agentswarm.provider.session_id": providerSessionId,
|
|
2229
|
+
"agentswarm.tool.name": tool.name,
|
|
2230
|
+
"agentswarm.tool.normalized_name": tool.normalizedName,
|
|
2231
|
+
"agentswarm.tool.kind": tool.kind,
|
|
2232
|
+
"agentswarm.tool.call_id": event.toolCallId,
|
|
2233
|
+
"mcp.server.name": tool.mcpServer,
|
|
2234
|
+
"mcp.tool.name": tool.mcpTool,
|
|
2235
|
+
"agentswarm.tool.args_preview": telemetryPreview(event.args),
|
|
2236
|
+
});
|
|
2237
|
+
activeToolSpans.set(event.toolCallId, {
|
|
2238
|
+
span: toolSpan,
|
|
2239
|
+
startedAt: Date.now(),
|
|
2240
|
+
});
|
|
1943
2241
|
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
...extractToolKey(event.toolName, event.args),
|
|
1956
|
-
clientTimestamp: new Date().toISOString(),
|
|
1957
|
-
},
|
|
1958
|
-
});
|
|
2242
|
+
// Auto-progress: report tool activity as task progress (throttled)
|
|
2243
|
+
const now = Date.now();
|
|
2244
|
+
if (effectiveTaskId && opts.apiUrl && now - lastProgressTime >= PROGRESS_THROTTLE_MS) {
|
|
2245
|
+
const progress = toolCallToProgress(event.toolName, event.args);
|
|
2246
|
+
if (progress) {
|
|
2247
|
+
lastProgressTime = now;
|
|
2248
|
+
updateProgressViaAPI(opts.apiUrl, opts.apiKey, effectiveTaskId, progress).catch(
|
|
2249
|
+
() => {},
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
1959
2253
|
|
|
1960
|
-
|
|
1961
|
-
if (event.toolName === "Skill") {
|
|
1962
|
-
const args = event.args as Record<string, unknown>;
|
|
2254
|
+
// Buffer tool event
|
|
1963
2255
|
bufferEvent({
|
|
1964
|
-
category: "
|
|
1965
|
-
event: "
|
|
2256
|
+
category: "tool",
|
|
2257
|
+
event: "tool.start",
|
|
1966
2258
|
source: "worker",
|
|
1967
2259
|
agentId: opts.agentId,
|
|
1968
2260
|
taskId: effectiveTaskId,
|
|
1969
2261
|
sessionId: opts.runnerSessionId,
|
|
1970
2262
|
data: {
|
|
1971
|
-
|
|
2263
|
+
toolName: event.toolName,
|
|
2264
|
+
toolCallId: event.toolCallId,
|
|
2265
|
+
...extractToolKey(event.toolName, event.args),
|
|
1972
2266
|
clientTimestamp: new Date().toISOString(),
|
|
1973
2267
|
},
|
|
1974
2268
|
});
|
|
2269
|
+
|
|
2270
|
+
// Also emit skill event when tool is Skill
|
|
2271
|
+
if (event.toolName === "Skill") {
|
|
2272
|
+
const args = event.args as Record<string, unknown>;
|
|
2273
|
+
bufferEvent({
|
|
2274
|
+
category: "skill",
|
|
2275
|
+
event: "skill.invoke",
|
|
2276
|
+
source: "worker",
|
|
2277
|
+
agentId: opts.agentId,
|
|
2278
|
+
taskId: effectiveTaskId,
|
|
2279
|
+
sessionId: opts.runnerSessionId,
|
|
2280
|
+
data: {
|
|
2281
|
+
skillName: args.skill as string,
|
|
2282
|
+
clientTimestamp: new Date().toISOString(),
|
|
2283
|
+
},
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
break;
|
|
1975
2287
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
2288
|
+
case "tool_end": {
|
|
2289
|
+
const active = activeToolSpans.get(event.toolCallId);
|
|
2290
|
+
const now = Date.now();
|
|
2291
|
+
if (active) {
|
|
2292
|
+
active.span.setAttributes({
|
|
2293
|
+
"agentswarm.tool.duration_ms": now - active.startedAt,
|
|
2294
|
+
"agentswarm.tool.result_preview": telemetryPreview(event.result),
|
|
2295
|
+
});
|
|
2296
|
+
active.span.setStatus({ code: 1 });
|
|
2297
|
+
active.span.end();
|
|
2298
|
+
activeToolSpans.delete(event.toolCallId);
|
|
2299
|
+
} else {
|
|
2300
|
+
const tool = classifyTool(event.toolName, undefined);
|
|
2301
|
+
const span = startSpan(tool.kind === "mcp" ? "worker.mcp.tool" : "worker.tool", {
|
|
2302
|
+
"agent.id": opts.agentId,
|
|
2303
|
+
"agentswarm.task.id": effectiveTaskId,
|
|
2304
|
+
"agentswarm.task.real_id": realTaskId,
|
|
2305
|
+
"agentswarm.agent.role": opts.role,
|
|
2306
|
+
"agentswarm.harness_provider": opts.harnessProvider,
|
|
2307
|
+
"agentswarm.provider.session_id": providerSessionId,
|
|
2308
|
+
"agentswarm.tool.name": tool.name,
|
|
2309
|
+
"agentswarm.tool.normalized_name": tool.normalizedName,
|
|
2310
|
+
"agentswarm.tool.kind": tool.kind,
|
|
2311
|
+
"agentswarm.tool.call_id": event.toolCallId,
|
|
2312
|
+
"mcp.server.name": tool.mcpServer,
|
|
2313
|
+
"mcp.tool.name": tool.mcpTool,
|
|
2314
|
+
"agentswarm.tool.result_preview": telemetryPreview(event.result),
|
|
2315
|
+
"agentswarm.tool.missing_start": true,
|
|
2316
|
+
});
|
|
2317
|
+
span.end();
|
|
2318
|
+
}
|
|
2319
|
+
break;
|
|
2320
|
+
}
|
|
2321
|
+
case "result":
|
|
2322
|
+
{
|
|
2323
|
+
const latestModel = buildLatestModelReport({
|
|
2324
|
+
model: event.cost.model,
|
|
2325
|
+
taskModel: opts.model,
|
|
2326
|
+
configModel,
|
|
2327
|
+
taskId: realTaskId,
|
|
2328
|
+
harnessProvider: opts.harnessProvider,
|
|
2329
|
+
});
|
|
2330
|
+
if (latestModel) {
|
|
2331
|
+
reportLatestModel(opts.apiUrl, opts.apiKey, opts.agentId, latestModel).catch((err) =>
|
|
2332
|
+
console.warn(`[runner] Failed to report latest model: ${err}`),
|
|
2333
|
+
);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
// Cost save is handled in waitForCompletion().then() to ensure
|
|
2337
|
+
// it completes before the process exits (fire-and-forget here
|
|
2338
|
+
// races with container shutdown).
|
|
2339
|
+
|
|
2340
|
+
// Buffer session end event
|
|
2341
|
+
bufferEvent({
|
|
2342
|
+
category: "session",
|
|
2343
|
+
event: "session.end",
|
|
2344
|
+
source: "worker",
|
|
2345
|
+
agentId: opts.agentId,
|
|
2346
|
+
taskId: effectiveTaskId,
|
|
2347
|
+
sessionId: opts.runnerSessionId,
|
|
2348
|
+
status: event.isError ? "error" : "ok",
|
|
2349
|
+
durationMs: Date.now() - sessionStartTime,
|
|
2350
|
+
data: {
|
|
2351
|
+
model: event.cost.model,
|
|
2352
|
+
totalCostUsd: event.cost.totalCostUsd,
|
|
2353
|
+
inputTokens: event.cost.inputTokens,
|
|
2354
|
+
outputTokens: event.cost.outputTokens,
|
|
2355
|
+
},
|
|
1986
2356
|
});
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
2357
|
+
break;
|
|
2358
|
+
case "error":
|
|
2359
|
+
sessionSpan.setStatus({
|
|
2360
|
+
code: 2,
|
|
2361
|
+
message: event.message,
|
|
2362
|
+
});
|
|
2363
|
+
break;
|
|
2364
|
+
case "context_usage": {
|
|
2365
|
+
const now2 = Date.now();
|
|
2366
|
+
if (now2 - lastContextPostTime >= CONTEXT_THROTTLE_MS) {
|
|
2367
|
+
lastContextPostTime = now2;
|
|
2368
|
+
// Phase 10: track cumulative output tokens on the worker side and
|
|
2369
|
+
// forward them on every progress snapshot. Previously these were
|
|
2370
|
+
// 0 until the `completion` snapshot at session end, so the
|
|
2371
|
+
// dashboard's "tokens consumed" line was a flat zero throughout.
|
|
2372
|
+
cumulativeProgressOutputTokens += event.outputTokens ?? 0;
|
|
2373
|
+
// For inputs we don't get a per-turn delta on the `context_usage`
|
|
2374
|
+
// event (the unified formula bakes it into contextUsedTokens), so
|
|
2375
|
+
// we report the latest contextUsedTokens as the running input proxy.
|
|
2376
|
+
// The DB column is `cumulativeInputTokens` but the semantic on
|
|
2377
|
+
// progress rows is "running used-tokens" — both inputs and outputs
|
|
2378
|
+
// contribute, exact decomposition lives on the cost row.
|
|
2379
|
+
fetch(`${opts.apiUrl}/api/tasks/${realTaskId}/context`, {
|
|
2380
|
+
method: "POST",
|
|
2381
|
+
headers: {
|
|
2382
|
+
"Content-Type": "application/json",
|
|
2383
|
+
"X-Agent-ID": opts.agentId,
|
|
2384
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
2385
|
+
},
|
|
2386
|
+
body: JSON.stringify({
|
|
2387
|
+
eventType: "progress",
|
|
2388
|
+
sessionId: opts.runnerSessionId,
|
|
2389
|
+
contextUsedTokens: event.contextUsedTokens,
|
|
2390
|
+
contextTotalTokens: event.contextTotalTokens,
|
|
2391
|
+
contextPercent: event.contextPercent,
|
|
2392
|
+
cumulativeInputTokens: event.contextUsedTokens,
|
|
2393
|
+
cumulativeOutputTokens: cumulativeProgressOutputTokens,
|
|
2394
|
+
contextFormula: event.contextFormula,
|
|
2395
|
+
}),
|
|
2396
|
+
}).catch(() => {});
|
|
1991
2397
|
}
|
|
2398
|
+
break;
|
|
1992
2399
|
}
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
// races with container shutdown).
|
|
1996
|
-
|
|
1997
|
-
// Buffer session end event
|
|
1998
|
-
bufferEvent({
|
|
1999
|
-
category: "session",
|
|
2000
|
-
event: "session.end",
|
|
2001
|
-
source: "worker",
|
|
2002
|
-
agentId: opts.agentId,
|
|
2003
|
-
taskId: effectiveTaskId,
|
|
2004
|
-
sessionId: opts.runnerSessionId,
|
|
2005
|
-
status: event.isError ? "error" : "ok",
|
|
2006
|
-
durationMs: Date.now() - sessionStartTime,
|
|
2007
|
-
data: {
|
|
2008
|
-
model: event.cost.model,
|
|
2009
|
-
totalCostUsd: event.cost.totalCostUsd,
|
|
2010
|
-
inputTokens: event.cost.inputTokens,
|
|
2011
|
-
outputTokens: event.cost.outputTokens,
|
|
2012
|
-
},
|
|
2013
|
-
});
|
|
2014
|
-
break;
|
|
2015
|
-
case "context_usage": {
|
|
2016
|
-
const now2 = Date.now();
|
|
2017
|
-
if (now2 - lastContextPostTime >= CONTEXT_THROTTLE_MS) {
|
|
2018
|
-
lastContextPostTime = now2;
|
|
2400
|
+
case "compaction": {
|
|
2401
|
+
// Always record compaction events (no throttle)
|
|
2019
2402
|
fetch(`${opts.apiUrl}/api/tasks/${realTaskId}/context`, {
|
|
2020
2403
|
method: "POST",
|
|
2021
2404
|
headers: {
|
|
@@ -2024,153 +2407,186 @@ async function spawnProviderProcess(
|
|
|
2024
2407
|
Authorization: `Bearer ${opts.apiKey}`,
|
|
2025
2408
|
},
|
|
2026
2409
|
body: JSON.stringify({
|
|
2027
|
-
eventType: "
|
|
2410
|
+
eventType: "compaction",
|
|
2028
2411
|
sessionId: opts.runnerSessionId,
|
|
2029
|
-
|
|
2412
|
+
preCompactTokens: event.preCompactTokens,
|
|
2413
|
+
compactTrigger: event.compactTrigger,
|
|
2030
2414
|
contextTotalTokens: event.contextTotalTokens,
|
|
2031
|
-
contextPercent: event.contextPercent,
|
|
2032
2415
|
}),
|
|
2033
2416
|
}).catch(() => {});
|
|
2417
|
+
break;
|
|
2034
2418
|
}
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
}).catch(() => {});
|
|
2054
|
-
break;
|
|
2055
|
-
}
|
|
2056
|
-
case "raw_log":
|
|
2057
|
-
prettyPrintLine(event.content, opts.role);
|
|
2058
|
-
if (shouldStream) {
|
|
2059
|
-
logBuffer.lines.push(event.content);
|
|
2060
|
-
const shouldFlush =
|
|
2061
|
-
logBuffer.lines.length >= LOG_BUFFER_SIZE ||
|
|
2062
|
-
Date.now() - logBuffer.lastFlush >= LOG_FLUSH_INTERVAL_MS;
|
|
2063
|
-
if (shouldFlush) {
|
|
2064
|
-
flushLogBuffer(logBuffer, {
|
|
2065
|
-
apiUrl: opts.apiUrl,
|
|
2066
|
-
apiKey: opts.apiKey,
|
|
2067
|
-
agentId: opts.agentId,
|
|
2068
|
-
sessionId: opts.runnerSessionId,
|
|
2069
|
-
iteration: opts.iteration,
|
|
2070
|
-
taskId: effectiveTaskId,
|
|
2071
|
-
cli: adapter.name,
|
|
2072
|
-
}).catch(() => {});
|
|
2419
|
+
case "raw_log":
|
|
2420
|
+
prettyPrintLine(event.content, opts.role);
|
|
2421
|
+
if (shouldStream) {
|
|
2422
|
+
logBuffer.lines.push(event.content);
|
|
2423
|
+
const shouldFlush =
|
|
2424
|
+
logBuffer.lines.length >= LOG_BUFFER_SIZE ||
|
|
2425
|
+
Date.now() - logBuffer.lastFlush >= LOG_FLUSH_INTERVAL_MS;
|
|
2426
|
+
if (shouldFlush) {
|
|
2427
|
+
flushLogBuffer(logBuffer, {
|
|
2428
|
+
apiUrl: opts.apiUrl,
|
|
2429
|
+
apiKey: opts.apiKey,
|
|
2430
|
+
agentId: opts.agentId,
|
|
2431
|
+
sessionId: opts.runnerSessionId,
|
|
2432
|
+
iteration: opts.iteration,
|
|
2433
|
+
taskId: effectiveTaskId,
|
|
2434
|
+
cli: adapter.name,
|
|
2435
|
+
}).catch(() => {});
|
|
2436
|
+
}
|
|
2073
2437
|
}
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2438
|
+
break;
|
|
2439
|
+
case "raw_stderr":
|
|
2440
|
+
prettyPrintStderr(event.content, opts.role);
|
|
2441
|
+
break;
|
|
2442
|
+
|
|
2443
|
+
case "progress": {
|
|
2444
|
+
if (effectiveTaskId && opts.apiUrl) {
|
|
2445
|
+
const now = Date.now();
|
|
2446
|
+
if (now - lastProgressTime >= PROGRESS_THROTTLE_MS) {
|
|
2447
|
+
lastProgressTime = now;
|
|
2448
|
+
updateProgressViaAPI(opts.apiUrl, opts.apiKey, effectiveTaskId, event.message).catch(
|
|
2449
|
+
() => {},
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2088
2452
|
}
|
|
2453
|
+
break;
|
|
2089
2454
|
}
|
|
2090
|
-
break;
|
|
2091
2455
|
}
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2456
|
+
}),
|
|
2457
|
+
);
|
|
2094
2458
|
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
// Final log flush
|
|
2102
|
-
if (shouldStream && logBuffer.lines.length > 0) {
|
|
2103
|
-
await flushLogBuffer(logBuffer, {
|
|
2104
|
-
apiUrl: opts.apiUrl,
|
|
2105
|
-
apiKey: opts.apiKey,
|
|
2106
|
-
agentId: opts.agentId,
|
|
2107
|
-
sessionId: opts.runnerSessionId,
|
|
2108
|
-
iteration: opts.iteration,
|
|
2109
|
-
taskId: effectiveTaskId,
|
|
2110
|
-
cli: adapter.name,
|
|
2459
|
+
function closeActiveToolSpans(status: "ok" | "error", message?: string) {
|
|
2460
|
+
for (const [toolCallId, active] of activeToolSpans) {
|
|
2461
|
+
active.span.setAttributes({
|
|
2462
|
+
"agentswarm.tool.duration_ms": Date.now() - active.startedAt,
|
|
2463
|
+
"agentswarm.tool.unclosed": true,
|
|
2464
|
+
"agentswarm.tool.call_id": toolCallId,
|
|
2111
2465
|
});
|
|
2466
|
+
if (status === "error") {
|
|
2467
|
+
active.span.setStatus({ code: 2, message: message || "session ended before tool_end" });
|
|
2468
|
+
}
|
|
2469
|
+
active.span.end();
|
|
2470
|
+
activeToolSpans.delete(toolCallId);
|
|
2112
2471
|
}
|
|
2472
|
+
}
|
|
2113
2473
|
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2474
|
+
// Create promise that handles completion
|
|
2475
|
+
const promise: Promise<ProviderResult> = session
|
|
2476
|
+
.waitForCompletion()
|
|
2477
|
+
.then((result) =>
|
|
2478
|
+
withSpanContext(sessionSpan, async () => {
|
|
2479
|
+
// Stop event flush timer and do a final flush
|
|
2480
|
+
clearInterval(eventFlushTimer);
|
|
2481
|
+
await flushEvents();
|
|
2482
|
+
|
|
2483
|
+
// Final log flush
|
|
2484
|
+
if (shouldStream && logBuffer.lines.length > 0) {
|
|
2485
|
+
await flushLogBuffer(logBuffer, {
|
|
2486
|
+
apiUrl: opts.apiUrl,
|
|
2487
|
+
apiKey: opts.apiKey,
|
|
2488
|
+
agentId: opts.agentId,
|
|
2489
|
+
sessionId: opts.runnerSessionId,
|
|
2490
|
+
iteration: opts.iteration,
|
|
2491
|
+
taskId: effectiveTaskId,
|
|
2492
|
+
cli: adapter.name,
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2123
2495
|
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2496
|
+
// Error logging for non-zero exit
|
|
2497
|
+
if (result.exitCode !== 0) {
|
|
2498
|
+
const errorLog = {
|
|
2499
|
+
timestamp: new Date().toISOString(),
|
|
2500
|
+
iteration: opts.iteration,
|
|
2501
|
+
exitCode: result.exitCode,
|
|
2502
|
+
taskId: effectiveTaskId,
|
|
2503
|
+
error: true,
|
|
2504
|
+
};
|
|
2128
2505
|
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
} else {
|
|
2134
|
-
console.warn(
|
|
2135
|
-
`[${opts.role}] Task ${effectiveTaskId.slice(0, 8)} exited with code ${result.exitCode}. YOLO mode - continuing...`,
|
|
2136
|
-
);
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2506
|
+
const errorsFile = `${logDir}/errors.jsonl`;
|
|
2507
|
+
const errorsFileRef = Bun.file(errorsFile);
|
|
2508
|
+
const existingErrors = (await errorsFileRef.exists()) ? await errorsFileRef.text() : "";
|
|
2509
|
+
await Bun.write(errorsFile, `${existingErrors}${JSON.stringify(errorLog)}\n`);
|
|
2139
2510
|
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2511
|
+
if (!isYolo) {
|
|
2512
|
+
console.error(
|
|
2513
|
+
`[${opts.role}] Task ${effectiveTaskId.slice(0, 8)} exited with code ${result.exitCode}.`,
|
|
2514
|
+
);
|
|
2515
|
+
} else {
|
|
2516
|
+
console.warn(
|
|
2517
|
+
`[${opts.role}] Task ${effectiveTaskId.slice(0, 8)} exited with code ${result.exitCode}. YOLO mode - continuing...`,
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2152
2521
|
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2522
|
+
// Save cost data (awaited to ensure it completes before container exits)
|
|
2523
|
+
if (result.cost) {
|
|
2524
|
+
sessionSpan.setAttributes({
|
|
2525
|
+
"gen_ai.response.model": result.cost.model,
|
|
2526
|
+
"gen_ai.usage.input_tokens": result.cost.inputTokens ?? 0,
|
|
2527
|
+
"gen_ai.usage.output_tokens": result.cost.outputTokens ?? 0,
|
|
2528
|
+
"agentswarm.cost.total_usd": result.cost.totalCostUsd ?? 0,
|
|
2529
|
+
});
|
|
2530
|
+
try {
|
|
2531
|
+
await saveCostData(
|
|
2532
|
+
{ ...result.cost, taskId: realTaskId, sessionId: opts.runnerSessionId },
|
|
2533
|
+
opts.apiUrl,
|
|
2534
|
+
opts.apiKey,
|
|
2535
|
+
);
|
|
2536
|
+
} catch (err) {
|
|
2537
|
+
console.warn(`[runner] Failed to save cost: ${err}`);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2171
2540
|
|
|
2172
|
-
|
|
2173
|
-
|
|
2541
|
+
// Post completion context usage snapshot
|
|
2542
|
+
if (result.cost && realTaskId) {
|
|
2543
|
+
fetch(`${opts.apiUrl}/api/tasks/${realTaskId}/context`, {
|
|
2544
|
+
method: "POST",
|
|
2545
|
+
headers: {
|
|
2546
|
+
"Content-Type": "application/json",
|
|
2547
|
+
"X-Agent-ID": opts.agentId,
|
|
2548
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
2549
|
+
},
|
|
2550
|
+
body: JSON.stringify({
|
|
2551
|
+
eventType: "completion",
|
|
2552
|
+
sessionId: opts.runnerSessionId,
|
|
2553
|
+
cumulativeInputTokens: result.cost.inputTokens ?? 0,
|
|
2554
|
+
cumulativeOutputTokens: result.cost.outputTokens ?? 0,
|
|
2555
|
+
contextTotalTokens: getContextWindowSize(result.cost.model || "default"),
|
|
2556
|
+
}),
|
|
2557
|
+
}).catch(() => {});
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
sessionSpan.setAttributes({
|
|
2561
|
+
"agentswarm.session.duration_ms": Date.now() - sessionStartTime,
|
|
2562
|
+
"agentswarm.session.exit_code": result.exitCode,
|
|
2563
|
+
"agentswarm.session.outcome": result.exitCode === 0 ? "ok" : "error",
|
|
2564
|
+
});
|
|
2565
|
+
if (result.exitCode !== 0) {
|
|
2566
|
+
sessionSpan.setStatus({
|
|
2567
|
+
code: 2,
|
|
2568
|
+
message: result.failureReason || `Provider exited with ${result.exitCode}`,
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
closeActiveToolSpans(result.exitCode === 0 ? "ok" : "error", result.failureReason);
|
|
2572
|
+
sessionSpan.end();
|
|
2573
|
+
|
|
2574
|
+
return result;
|
|
2575
|
+
}),
|
|
2576
|
+
)
|
|
2577
|
+
.catch((error) =>
|
|
2578
|
+
withSpanContext(sessionSpan, () => {
|
|
2579
|
+
clearInterval(eventFlushTimer);
|
|
2580
|
+
sessionSpan.recordException(error);
|
|
2581
|
+
sessionSpan.setStatus({
|
|
2582
|
+
code: 2,
|
|
2583
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2584
|
+
});
|
|
2585
|
+
closeActiveToolSpans("error", error instanceof Error ? error.message : String(error));
|
|
2586
|
+
sessionSpan.end();
|
|
2587
|
+
throw error;
|
|
2588
|
+
}),
|
|
2589
|
+
);
|
|
2174
2590
|
|
|
2175
2591
|
// Build credential info for rate limit tracking
|
|
2176
2592
|
const primarySelection = credentialSelections[0] ?? oauthSelection;
|
|
@@ -2402,6 +2818,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2402
2818
|
|
|
2403
2819
|
// Initialize Business-Use SDK for worker-side instrumentation
|
|
2404
2820
|
initialize();
|
|
2821
|
+
await initOtel(role);
|
|
2405
2822
|
|
|
2406
2823
|
const sessionId = process.env.SESSION_ID || crypto.randomUUID().slice(0, 8);
|
|
2407
2824
|
const baseLogDir = opts.logsDir || process.env.LOG_DIR || "/logs";
|
|
@@ -2417,7 +2834,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2417
2834
|
|
|
2418
2835
|
const apiUrl = process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
|
|
2419
2836
|
const swarmUrl = process.env.SWARM_URL || "localhost";
|
|
2420
|
-
const apiKey =
|
|
2837
|
+
const apiKey = getApiKey();
|
|
2421
2838
|
|
|
2422
2839
|
// Resolve the boot harness provider from swarm_config (repo > agent > global,
|
|
2423
2840
|
// overlaid on top of `process.env`). This is what selects the adapter for
|
|
@@ -2443,8 +2860,8 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2443
2860
|
let adapter = createProviderAdapter(bootProvider);
|
|
2444
2861
|
|
|
2445
2862
|
// Configure HTTP-based template resolution (workers resolve via API, not local DB)
|
|
2446
|
-
if (
|
|
2447
|
-
configureHttpResolver(apiUrl,
|
|
2863
|
+
if (apiKey) {
|
|
2864
|
+
configureHttpResolver(apiUrl, apiKey);
|
|
2448
2865
|
}
|
|
2449
2866
|
|
|
2450
2867
|
// Initialize anonymized telemetry (opt-out via ANONYMIZED_TELEMETRY=false).
|
|
@@ -2455,7 +2872,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2455
2872
|
// skips telemetry instead of minting a fresh `install_<hex>` ID per
|
|
2456
2873
|
// restart, which floods prod metrics with phantom installs.
|
|
2457
2874
|
{
|
|
2458
|
-
const telemetryApiKey =
|
|
2875
|
+
const telemetryApiKey = apiKey || undefined;
|
|
2459
2876
|
await initTelemetry(
|
|
2460
2877
|
"worker",
|
|
2461
2878
|
async (key) => {
|