@desplega.ai/agent-swarm 1.87.0 → 1.89.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 +5 -1
- package/openapi.json +53 -1
- package/package.json +6 -5
- package/plugin/skills/composio/SKILL.md +98 -0
- package/src/be/db.ts +374 -9
- package/src/be/migrations/080_skill_system_defaults.sql +8 -0
- package/src/be/migrations/081_metrics.sql +39 -0
- package/src/be/migrations/082_user_audit_fields.sql +120 -0
- package/src/be/modelsdev-cache.json +3825 -2417
- package/src/be/seed/registry.ts +3 -2
- package/src/be/seed-skills/index.ts +179 -0
- package/src/cli.tsx +51 -4
- package/src/commands/e2b-stack-wizard.tsx +394 -0
- package/src/commands/e2b.ts +1352 -53
- package/src/commands/onboard/dashboard-url.ts +29 -0
- package/src/commands/onboard/steps/post-dashboard.tsx +3 -1
- package/src/commands/onboard.tsx +3 -1
- package/src/commands/runner.ts +154 -22
- package/src/commands/x.ts +118 -0
- package/src/e2b/dispatch.ts +234 -18
- package/src/github/handlers.ts +40 -1
- package/src/heartbeat/heartbeat.ts +26 -5
- package/src/http/active-sessions.ts +32 -1
- package/src/http/auth.ts +36 -0
- package/src/http/core.ts +20 -16
- package/src/http/db-query.ts +20 -0
- package/src/http/index.ts +2 -0
- package/src/http/memory.ts +13 -1
- package/src/http/metrics.ts +447 -0
- package/src/http/operator-actor.ts +9 -0
- package/src/http/poll.ts +11 -1
- package/src/http/skills.ts +53 -0
- package/src/http/tasks.ts +4 -1
- package/src/http/webhooks.ts +75 -0
- package/src/http/workflows.ts +5 -1
- package/src/integrations/kapso/client.ts +82 -0
- package/src/memory/automatic-task-gate.ts +47 -0
- package/src/metrics/version.ts +26 -0
- package/src/prompts/base-prompt.ts +24 -1
- package/src/prompts/session-templates.ts +74 -0
- package/src/providers/claude-adapter.ts +19 -0
- package/src/providers/codex-adapter.ts +22 -0
- package/src/providers/ctx-mode-env.ts +10 -0
- package/src/providers/opencode-adapter.ts +72 -7
- package/src/server.ts +10 -1
- package/src/slack/blocks.ts +12 -4
- package/src/slack/watcher.ts +3 -3
- package/src/telemetry.ts +14 -1
- package/src/templates.d.ts +4 -0
- package/src/tests/base-prompt.test.ts +76 -0
- package/src/tests/budget-claim-gate.test.ts +26 -0
- package/src/tests/claude-adapter.test.ts +86 -1
- package/src/tests/codex-adapter.test.ts +89 -0
- package/src/tests/core-auth.test.ts +8 -1
- package/src/tests/e2b-dispatch.test.ts +603 -11
- package/src/tests/events-http.test.ts +6 -2
- package/src/tests/github-handlers-cancel-config.test.ts +262 -0
- package/src/tests/heartbeat.test.ts +84 -3
- package/src/tests/http-api-integration.test.ts +116 -1
- package/src/tests/kapso-client.test.ts +74 -1
- package/src/tests/kapso-inbound.test.ts +60 -2
- package/src/tests/metrics-http.test.ts +247 -0
- package/src/tests/opencode-adapter.test.ts +185 -30
- package/src/tests/prompt-template-session.test.ts +4 -2
- package/src/tests/runner-repo-autostash.test.ts +117 -0
- package/src/tests/runner-requester-profile.test.ts +25 -0
- package/src/tests/runner-skills-refresh.test.ts +1 -1
- package/src/tests/self-improvement.test.ts +89 -0
- package/src/tests/skill-update-scope.test.ts +88 -1
- package/src/tests/slack-blocks.test.ts +15 -0
- package/src/tests/swarm-x-tool.test.ts +90 -0
- package/src/tests/system-default-skills.test.ts +122 -0
- package/src/tests/telemetry-init.test.ts +86 -0
- package/src/tests/ui-logs-parser.test.ts +271 -0
- package/src/tests/user-token-rest-auth.test.ts +129 -0
- package/src/tests/workflow-async-v2.test.ts +23 -0
- package/src/tests/x-composio.test.ts +122 -0
- package/src/tools/create-metric.ts +191 -0
- package/src/tools/skills/skill-delete.ts +14 -0
- package/src/tools/skills/skill-update.ts +14 -0
- package/src/tools/store-progress.ts +19 -5
- package/src/tools/swarm-x.ts +116 -0
- package/src/tools/tool-config.ts +6 -0
- package/src/types.ts +121 -0
- package/src/utils/request-auth-context.ts +28 -0
- package/src/utils/skills-refresh.ts +2 -2
- package/src/workflows/engine.ts +24 -2
- package/src/workflows/executors/agent-task.ts +2 -0
- package/src/x/composio.ts +295 -0
- package/templates/skills/artifacts/config.json +1 -0
- package/templates/skills/attio-interaction/SKILL.md +279 -0
- package/templates/skills/attio-interaction/config.json +14 -0
- package/templates/skills/attio-interaction/content.md +272 -0
- package/templates/skills/kv-storage/config.json +1 -0
- package/templates/skills/pages/config.json +1 -0
- package/templates/skills/scheduled-task-resilience/config.json +1 -0
- package/templates/skills/swarm-scripts/SKILL.md +91 -0
- package/templates/skills/swarm-scripts/config.json +14 -0
- package/templates/skills/swarm-scripts/content.md +86 -0
- package/templates/skills/workflow-iterate/config.json +1 -0
- package/templates/skills/workflow-structured-output/config.json +1 -0
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the swarm-dashboard deep-link the SPA reads after a local onboard.
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: the dashboard SPA reads **camelCase** `apiUrl` / `apiKey` query
|
|
5
|
+
* params (see ui/src/hooks/use-config.ts → extractUrlParams) and silently
|
|
6
|
+
* ignores snake_case. An earlier version of these builders emitted snake_case
|
|
7
|
+
* `api_url` / `api_key`, so the auto-connect deep-link never worked. Keep these
|
|
8
|
+
* camelCase.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const DEFAULT_DASHBOARD_BASE = "https://app.agent-swarm.dev";
|
|
12
|
+
|
|
13
|
+
export type DashboardUrlParts = {
|
|
14
|
+
apiUrl: string;
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
/** Optional connection name shown in the dashboard (camelCase `name`). */
|
|
17
|
+
name?: string;
|
|
18
|
+
/** Override the dashboard base (defaults to the production app). */
|
|
19
|
+
base?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function buildOnboardDashboardUrl(parts: DashboardUrlParts): string {
|
|
23
|
+
const params = new URLSearchParams();
|
|
24
|
+
params.set("apiUrl", parts.apiUrl);
|
|
25
|
+
if (parts.apiKey) params.set("apiKey", parts.apiKey);
|
|
26
|
+
if (parts.name) params.set("name", parts.name);
|
|
27
|
+
const base = (parts.base ?? DEFAULT_DASHBOARD_BASE).replace(/\/+$/, "");
|
|
28
|
+
return `${base}?${params.toString()}`;
|
|
29
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Select } from "@inkjs/ui";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { buildOnboardDashboardUrl } from "../dashboard-url.ts";
|
|
3
4
|
import type { StepProps } from "../types.ts";
|
|
4
5
|
|
|
5
6
|
export function PostDashboardStep({ state, addLog, goToNext }: StepProps) {
|
|
6
7
|
const apiUrl = `http://localhost:${state.apiPort || 3013}`;
|
|
7
|
-
|
|
8
|
+
// camelCase params — the SPA ignores snake_case (see dashboard-url.ts).
|
|
9
|
+
const dashboardUrl = buildOnboardDashboardUrl({ apiUrl, apiKey: state.apiKey });
|
|
8
10
|
|
|
9
11
|
return (
|
|
10
12
|
<Box flexDirection="column" padding={1}>
|
package/src/commands/onboard.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { Box, Text, useApp, useInput } from "ink";
|
|
|
4
4
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
5
|
import pkg from "../../package.json";
|
|
6
6
|
import { getApiKey } from "../utils/api-key.ts";
|
|
7
|
+
import { buildOnboardDashboardUrl } from "./onboard/dashboard-url.ts";
|
|
7
8
|
import { getAgentSummary, getPresetById, PRESETS } from "./onboard/presets.ts";
|
|
8
9
|
import { CoreCredentialsStep } from "./onboard/steps/core-credentials.tsx";
|
|
9
10
|
import { CustomTemplatesStep } from "./onboard/steps/custom-templates.tsx";
|
|
@@ -244,7 +245,8 @@ export function Onboard({ dryRun = false, yes = false, preset }: OnboardProps) {
|
|
|
244
245
|
|
|
245
246
|
if (state.step === "done") {
|
|
246
247
|
const apiUrl = `http://localhost:${state.apiPort || 3013}`;
|
|
247
|
-
|
|
248
|
+
// camelCase params — the SPA ignores snake_case (see dashboard-url.ts).
|
|
249
|
+
const dashUrl = buildOnboardDashboardUrl({ apiUrl, apiKey: state.apiKey });
|
|
248
250
|
const agentCount = state.services.reduce((sum, s) => sum + s.count, 0);
|
|
249
251
|
|
|
250
252
|
return (
|
package/src/commands/runner.ts
CHANGED
|
@@ -126,14 +126,88 @@ async function readClaudeMd(clonePath: string, role: string): Promise<string | n
|
|
|
126
126
|
return null;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
export type SwarmAutostash = { ref: string; message: string };
|
|
130
|
+
|
|
131
|
+
async function listSwarmAutostashes(clonePath: string, role: string): Promise<SwarmAutostash[]> {
|
|
132
|
+
try {
|
|
133
|
+
const result = await Bun.$`cd ${clonePath} && git stash list --format=%gd%x09%s`.quiet();
|
|
134
|
+
return result
|
|
135
|
+
.text()
|
|
136
|
+
.split("\n")
|
|
137
|
+
.map((line) => line.trim())
|
|
138
|
+
.filter((line) => line.includes("swarm-autostash"))
|
|
139
|
+
.flatMap((line) => {
|
|
140
|
+
const [ref, ...messageParts] = line.split("\t");
|
|
141
|
+
if (!ref) return [];
|
|
142
|
+
return [{ ref, message: messageParts.join("\t") || line }];
|
|
143
|
+
});
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.warn(
|
|
146
|
+
`[${role}] Could not inspect git stashes for ${clonePath}: ${scrubSecrets((err as Error).message)}`,
|
|
147
|
+
);
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function refreshExistingRepoForTask(
|
|
153
|
+
repoConfig: { name: string; clonePath: string; defaultBranch: string },
|
|
154
|
+
role: string,
|
|
155
|
+
): Promise<string | null> {
|
|
156
|
+
const { name, clonePath, defaultBranch } = repoConfig;
|
|
157
|
+
const statusResult = await Bun.$`cd ${clonePath} && git status --porcelain`.quiet();
|
|
158
|
+
const statusOutput = statusResult.text().trim();
|
|
159
|
+
let stashMessage: string | null = null;
|
|
160
|
+
|
|
161
|
+
if (statusOutput !== "") {
|
|
162
|
+
stashMessage = `swarm-autostash ${defaultBranch} ${new Date().toISOString()}`;
|
|
163
|
+
try {
|
|
164
|
+
console.log(`[${role}] Auto-stashing pending work in ${name}: ${stashMessage}`);
|
|
165
|
+
await Bun.$`cd ${clonePath} && git stash push --include-untracked -m ${stashMessage}`.quiet();
|
|
166
|
+
console.log(`[${role}] Auto-stashed pending work in ${name}`);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
const errorMsg = scrubSecrets((err as Error).message);
|
|
169
|
+
console.warn(`[${role}] Could not auto-stash ${name}, skipping pull: ${errorMsg}`);
|
|
170
|
+
return `The repo "${name}" at ${clonePath} has uncommitted changes, but auto-stash failed: ${errorMsg}. A git pull was skipped to avoid losing work.`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
console.log(`[${role}] Refreshing ${name} from origin/${defaultBranch}...`);
|
|
176
|
+
const fetchSpec = `${defaultBranch}:refs/remotes/origin/${defaultBranch}`;
|
|
177
|
+
const remoteRef = `refs/remotes/origin/${defaultBranch}`;
|
|
178
|
+
await Bun.$`cd ${clonePath} && git fetch origin ${fetchSpec}`.quiet();
|
|
179
|
+
await Bun.$`cd ${clonePath} && git merge --no-edit --no-stat ${remoteRef}`.quiet();
|
|
180
|
+
console.log(`[${role}] Refreshed ${name}`);
|
|
181
|
+
return null;
|
|
182
|
+
} catch (err) {
|
|
183
|
+
const errorMsg = scrubSecrets((err as Error).message);
|
|
184
|
+
console.warn(`[${role}] Could not refresh ${name}: ${errorMsg}`);
|
|
185
|
+
try {
|
|
186
|
+
await Bun.$`cd ${clonePath} && git merge --abort`.quiet();
|
|
187
|
+
} catch {
|
|
188
|
+
// No merge in progress, or abort failed. The original refresh warning is
|
|
189
|
+
// the actionable signal; repo setup remains best-effort.
|
|
190
|
+
}
|
|
191
|
+
const stashNote = stashMessage
|
|
192
|
+
? ` Pending work was preserved in git stash "${stashMessage}".`
|
|
193
|
+
: "";
|
|
194
|
+
return `The repo "${name}" at ${clonePath} could not be refreshed from origin/${defaultBranch}: ${errorMsg}.${stashNote}`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
129
198
|
/**
|
|
130
199
|
* Ensure a repo is cloned and up-to-date for a task.
|
|
131
200
|
* Returns { clonePath, claudeMd, warning }.
|
|
132
201
|
*/
|
|
133
|
-
async function ensureRepoForTask(
|
|
202
|
+
export async function ensureRepoForTask(
|
|
134
203
|
repoConfig: { url: string; name: string; clonePath: string; defaultBranch: string },
|
|
135
204
|
role: string,
|
|
136
|
-
): Promise<{
|
|
205
|
+
): Promise<{
|
|
206
|
+
clonePath: string;
|
|
207
|
+
claudeMd: string | null;
|
|
208
|
+
warning: string | null;
|
|
209
|
+
autoStashes: SwarmAutostash[];
|
|
210
|
+
}> {
|
|
137
211
|
const { url, name, clonePath, defaultBranch } = repoConfig;
|
|
138
212
|
|
|
139
213
|
try {
|
|
@@ -158,28 +232,20 @@ async function ensureRepoForTask(
|
|
|
158
232
|
console.log(`[${role}] Cloned ${name}`);
|
|
159
233
|
} else {
|
|
160
234
|
console.log(`[${role}] Repo ${name} already cloned at ${clonePath}`);
|
|
161
|
-
|
|
162
|
-
const statusOutput = statusResult.text().trim();
|
|
163
|
-
|
|
164
|
-
if (statusOutput === "") {
|
|
165
|
-
console.log(`[${role}] Pulling ${name} (${defaultBranch})...`);
|
|
166
|
-
await Bun.$`cd ${clonePath} && git pull origin ${defaultBranch} --ff-only`.quiet();
|
|
167
|
-
console.log(`[${role}] Pulled ${name}`);
|
|
168
|
-
} else {
|
|
169
|
-
console.warn(`[${role}] Repo ${name} has uncommitted changes, skipping pull`);
|
|
170
|
-
warning = `The repo "${name}" at ${clonePath} has uncommitted changes. A git pull was skipped to avoid losing work. You may need to commit or stash changes before pulling updates.`;
|
|
171
|
-
}
|
|
235
|
+
warning = await refreshExistingRepoForTask({ name, clonePath, defaultBranch }, role);
|
|
172
236
|
}
|
|
173
237
|
|
|
174
238
|
const claudeMd = await readClaudeMd(clonePath, role);
|
|
175
|
-
|
|
239
|
+
const autoStashes = await listSwarmAutostashes(clonePath, role);
|
|
240
|
+
return { clonePath, claudeMd, warning, autoStashes };
|
|
176
241
|
} catch (err) {
|
|
177
|
-
const errorMsg = (err as Error).message;
|
|
242
|
+
const errorMsg = scrubSecrets((err as Error).message);
|
|
178
243
|
console.warn(`[${role}] Error setting up repo ${name}: ${errorMsg}`);
|
|
179
244
|
const warning = `Failed to clone/setup repo "${name}" at ${clonePath}: ${errorMsg}. The repo may not be available. You may need to clone it manually.`;
|
|
180
245
|
// Only return clonePath if the directory actually exists (clone may have failed)
|
|
181
246
|
const cloneExists = existsSync(clonePath);
|
|
182
|
-
|
|
247
|
+
const autoStashes = cloneExists ? await listSwarmAutostashes(clonePath, role) : [];
|
|
248
|
+
return { clonePath: cloneExists ? clonePath : "", claudeMd: null, warning, autoStashes };
|
|
183
249
|
}
|
|
184
250
|
}
|
|
185
251
|
|
|
@@ -1692,6 +1758,31 @@ async function cleanupActiveSessions(config: ApiConfig): Promise<void> {
|
|
|
1692
1758
|
}
|
|
1693
1759
|
}
|
|
1694
1760
|
|
|
1761
|
+
/** Reset orphaned in-progress tasks for this agent back to pending dispatch. */
|
|
1762
|
+
async function recoverOrphanedInProgressTasks(config: ApiConfig): Promise<number> {
|
|
1763
|
+
const headers: Record<string, string> = {
|
|
1764
|
+
"Content-Type": "application/json",
|
|
1765
|
+
"X-Agent-ID": config.agentId,
|
|
1766
|
+
};
|
|
1767
|
+
if (config.apiKey) headers.Authorization = `Bearer ${config.apiKey}`;
|
|
1768
|
+
try {
|
|
1769
|
+
const response = await fetch(`${config.apiUrl}/api/active-sessions/recover-orphaned-tasks`, {
|
|
1770
|
+
method: "POST",
|
|
1771
|
+
headers,
|
|
1772
|
+
body: JSON.stringify({ agentId: config.agentId, minAgeSeconds: 60 }),
|
|
1773
|
+
});
|
|
1774
|
+
if (!response.ok) {
|
|
1775
|
+
console.warn(`[runner] Failed to recover orphaned tasks: ${response.status}`);
|
|
1776
|
+
return 0;
|
|
1777
|
+
}
|
|
1778
|
+
const data = (await response.json()) as { recovered?: number };
|
|
1779
|
+
return data.recovered ?? 0;
|
|
1780
|
+
} catch (error) {
|
|
1781
|
+
console.warn(`[runner] Error recovering orphaned tasks: ${error}`);
|
|
1782
|
+
return 0;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1695
1786
|
/** Trigger a heartbeat sweep via the API (lead startup self-check) */
|
|
1696
1787
|
async function triggerHeartbeatSweep(config: ApiConfig): Promise<boolean> {
|
|
1697
1788
|
try {
|
|
@@ -1743,7 +1834,7 @@ interface Trigger {
|
|
|
1743
1834
|
text?: string;
|
|
1744
1835
|
}>;
|
|
1745
1836
|
cursorUpdates?: Array<{ channelId: string; ts: string }>; // Deferred cursor commits for channel_activity
|
|
1746
|
-
requestedBy?: { name: string; email?: string };
|
|
1837
|
+
requestedBy?: { name: string; email?: string; role?: string; notes?: string };
|
|
1747
1838
|
// Phase 4 — budget_refused fields. The server emits this envelope from
|
|
1748
1839
|
// /api/poll and MCP task-action accept when an admission gate refuses to
|
|
1749
1840
|
// let the agent claim a task. Worker reads cause + reset/spend/budget for
|
|
@@ -1766,6 +1857,26 @@ interface PollOptions {
|
|
|
1766
1857
|
since?: string; // Optional: for filtering finished tasks
|
|
1767
1858
|
}
|
|
1768
1859
|
|
|
1860
|
+
type RequesterProfile = NonNullable<Trigger["requestedBy"]>;
|
|
1861
|
+
|
|
1862
|
+
export async function buildRequesterProfilePrompt(
|
|
1863
|
+
requestedBy: RequesterProfile | undefined,
|
|
1864
|
+
): Promise<string> {
|
|
1865
|
+
if (!requestedBy?.role && !requestedBy?.notes) return "";
|
|
1866
|
+
|
|
1867
|
+
const notes = requestedBy.notes?.trim();
|
|
1868
|
+
const notesSection = notes
|
|
1869
|
+
? `\nTheir stated notes for how you should respond and act:\n${notes}`
|
|
1870
|
+
: "";
|
|
1871
|
+
const result = await resolveTemplateAsync("task.requester.profile", {
|
|
1872
|
+
requester_name: requestedBy.name,
|
|
1873
|
+
requester_role_suffix: requestedBy.role ? ` (${requestedBy.role})` : "",
|
|
1874
|
+
requester_notes_section: notesSection,
|
|
1875
|
+
});
|
|
1876
|
+
|
|
1877
|
+
return result.skipped ? "" : result.text.trim();
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1769
1880
|
/** Register agent via HTTP API */
|
|
1770
1881
|
async function registerAgent(opts: {
|
|
1771
1882
|
apiUrl: string;
|
|
@@ -3295,6 +3406,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3295
3406
|
swarmUrl,
|
|
3296
3407
|
capabilities,
|
|
3297
3408
|
traits,
|
|
3409
|
+
provider: adapter.name as ProviderName,
|
|
3298
3410
|
name: agentProfileName,
|
|
3299
3411
|
description: agentDescription,
|
|
3300
3412
|
...(traits.hasLocalEnvironment && {
|
|
@@ -3607,6 +3719,12 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3607
3719
|
// Clean up any stale active sessions from previous runs (crash recovery)
|
|
3608
3720
|
await cleanupActiveSessions(apiConfig);
|
|
3609
3721
|
console.log(`[${role}] Cleaned up stale active sessions`);
|
|
3722
|
+
const startupRecoveredOrphans = await recoverOrphanedInProgressTasks(apiConfig);
|
|
3723
|
+
if (startupRecoveredOrphans > 0) {
|
|
3724
|
+
console.log(
|
|
3725
|
+
`[${role}] Recovered ${startupRecoveredOrphans} orphaned in-progress task(s) to pending`,
|
|
3726
|
+
);
|
|
3727
|
+
}
|
|
3610
3728
|
|
|
3611
3729
|
// Fetch full agent profile to get soul/identity content
|
|
3612
3730
|
try {
|
|
@@ -4083,7 +4201,10 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
4083
4201
|
// state persists across iterations.
|
|
4084
4202
|
let consecutiveBudgetRefusals = 0;
|
|
4085
4203
|
|
|
4086
|
-
//
|
|
4204
|
+
// Throttle orphan recovery so it runs periodically while the worker is idle or under capacity.
|
|
4205
|
+
let lastOrphanRecoveryAt = 0;
|
|
4206
|
+
const ORPHAN_RECOVERY_INTERVAL_MS = 60_000;
|
|
4207
|
+
|
|
4087
4208
|
while (true) {
|
|
4088
4209
|
// Ping server on each iteration to keep status updated
|
|
4089
4210
|
await pingServer(apiConfig, role);
|
|
@@ -4179,6 +4300,16 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
4179
4300
|
|
|
4180
4301
|
// Only poll if we have capacity
|
|
4181
4302
|
if (state.activeTasks.size < state.maxConcurrent) {
|
|
4303
|
+
if (Date.now() - lastOrphanRecoveryAt > ORPHAN_RECOVERY_INTERVAL_MS) {
|
|
4304
|
+
lastOrphanRecoveryAt = Date.now();
|
|
4305
|
+
const recoveredOrphans = await recoverOrphanedInProgressTasks(apiConfig);
|
|
4306
|
+
if (recoveredOrphans > 0) {
|
|
4307
|
+
console.log(
|
|
4308
|
+
`[${role}] Recovered ${recoveredOrphans} orphaned in-progress task(s) to pending`,
|
|
4309
|
+
);
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4182
4313
|
console.log(
|
|
4183
4314
|
`[${role}] Polling for triggers (${state.activeTasks.size}/${state.maxConcurrent} active)...`,
|
|
4184
4315
|
);
|
|
@@ -4418,10 +4549,11 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
4418
4549
|
|
|
4419
4550
|
// Rebuild system prompt with per-task repo context
|
|
4420
4551
|
const taskBasePrompt = await buildSystemPrompt();
|
|
4421
|
-
const
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4552
|
+
const requesterProfilePrompt = await buildRequesterProfilePrompt(trigger.requestedBy);
|
|
4553
|
+
const taskPromptParts = [taskBasePrompt, requesterProfilePrompt, additionalSystemPrompt]
|
|
4554
|
+
.filter((part): part is string => Boolean(part))
|
|
4555
|
+
.join("\n\n");
|
|
4556
|
+
const taskSystemPrompt = taskPromptParts + cwdWarning;
|
|
4425
4557
|
|
|
4426
4558
|
iteration++;
|
|
4427
4559
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_COMPOSIO_BASE_URL,
|
|
4
|
+
executeComposioRequest,
|
|
5
|
+
formatComposioResultForCli,
|
|
6
|
+
parseComposioArgs,
|
|
7
|
+
} from "../x/composio";
|
|
8
|
+
|
|
9
|
+
interface XCommandDeps {
|
|
10
|
+
env?: Record<string, string | undefined>;
|
|
11
|
+
error?: (message: string) => void;
|
|
12
|
+
exit?: (code: number) => void;
|
|
13
|
+
fetch?: typeof fetch;
|
|
14
|
+
log?: (message: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { parseComposioArgs } from "../x/composio";
|
|
18
|
+
|
|
19
|
+
export async function runXCommand(argv: string[], deps: XCommandDeps = {}): Promise<void> {
|
|
20
|
+
const [target, ...rest] = argv;
|
|
21
|
+
const log = deps.log ?? console.log;
|
|
22
|
+
const error = deps.error ?? console.error;
|
|
23
|
+
const exit = deps.exit ?? process.exit;
|
|
24
|
+
|
|
25
|
+
if (!target || target === "help" || target === "-h" || target === "--help") {
|
|
26
|
+
printXHelp(log);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
switch (target) {
|
|
31
|
+
case "composio":
|
|
32
|
+
await runComposioCommand(rest, deps);
|
|
33
|
+
return;
|
|
34
|
+
default:
|
|
35
|
+
error(`Unknown x target: ${target}`);
|
|
36
|
+
printXHelp(error);
|
|
37
|
+
exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function runComposioCommand(argv: string[], deps: XCommandDeps = {}): Promise<void> {
|
|
42
|
+
const log = deps.log ?? console.log;
|
|
43
|
+
const error = deps.error ?? console.error;
|
|
44
|
+
const exit = deps.exit ?? process.exit;
|
|
45
|
+
const env = deps.env ?? process.env;
|
|
46
|
+
|
|
47
|
+
if (argv.length === 0 || argv[0] === "help" || argv[0] === "-h" || argv[0] === "--help") {
|
|
48
|
+
printComposioHelp(log);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let parsed: ReturnType<typeof parseComposioArgs>;
|
|
53
|
+
try {
|
|
54
|
+
parsed = parseComposioArgs(argv, env);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
error(`composio: ${scrubSecrets(errorMessage(err))}`);
|
|
57
|
+
printComposioHelp(error);
|
|
58
|
+
exit(1);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = await executeComposioRequest(parsed, { env, fetch: deps.fetch });
|
|
63
|
+
|
|
64
|
+
if (!result.ok) {
|
|
65
|
+
if (result.status > 0) error(`composio: HTTP ${result.status} ${result.statusText}`.trim());
|
|
66
|
+
else error(`composio: ${result.error ?? result.statusText}`);
|
|
67
|
+
if (result.formattedBody) error(result.formattedBody);
|
|
68
|
+
exit(1);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
log(formatComposioResultForCli(result));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function errorMessage(err: unknown): string {
|
|
76
|
+
return err instanceof Error ? err.message : String(err);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function printXHelp(log: (message: string) => void): void {
|
|
80
|
+
log(`Usage: agent-swarm x <target> [args]
|
|
81
|
+
|
|
82
|
+
Targets:
|
|
83
|
+
composio Route a request to the Composio REST API
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
agent-swarm x composio GET /tools
|
|
87
|
+
agent-swarm x composio POST /tools/execute/GITHUB_CREATE_AN_ISSUE --body '{"arguments":{}}'`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function printComposioHelp(log: (message: string) => void): void {
|
|
91
|
+
log(`Usage: agent-swarm x composio <method> <path> [options]
|
|
92
|
+
|
|
93
|
+
Routes an HTTP request to the Composio REST API.
|
|
94
|
+
|
|
95
|
+
Arguments:
|
|
96
|
+
<method> GET, POST, PUT, PATCH, DELETE, or HEAD
|
|
97
|
+
<path> API path relative to ${DEFAULT_COMPOSIO_BASE_URL}
|
|
98
|
+
|
|
99
|
+
Options:
|
|
100
|
+
--body, --data <json> JSON request body
|
|
101
|
+
-q, --query k=v Append a query parameter (repeatable)
|
|
102
|
+
-H, --header k=v Add a header (repeatable)
|
|
103
|
+
--base-url <url> Override base URL (default: COMPOSIO_BASE_URL or v3.1 API)
|
|
104
|
+
--org Use COMPOSIO_ORG_API_KEY and x-org-api-key
|
|
105
|
+
--raw Print response text without JSON pretty formatting
|
|
106
|
+
-h, --help Show this help
|
|
107
|
+
|
|
108
|
+
Environment:
|
|
109
|
+
COMPOSIO_API_KEY Project API key for x-api-key auth
|
|
110
|
+
COMPOSIO_ORG_API_KEY Optional organization key for --org
|
|
111
|
+
COMPOSIO_BASE_URL Optional API base URL override
|
|
112
|
+
|
|
113
|
+
Examples:
|
|
114
|
+
agent-swarm x composio GET /tools
|
|
115
|
+
agent-swarm x composio GET /tools --query limit=10
|
|
116
|
+
agent-swarm x composio POST /tool_router/session --body '{"user_id":"swarm"}'
|
|
117
|
+
agent-swarm x composio POST /tools/execute/GITHUB_CREATE_AN_ISSUE --body '{"arguments":{}}'`);
|
|
118
|
+
}
|