@desplega.ai/agent-swarm 1.71.2 → 1.72.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.
Files changed (62) hide show
  1. package/README.md +3 -2
  2. package/openapi.json +994 -62
  3. package/package.json +2 -1
  4. package/src/be/budget-admission.ts +121 -0
  5. package/src/be/budget-refusal-notify.ts +145 -0
  6. package/src/be/db.ts +488 -5
  7. package/src/be/migrations/044_provider_meta.sql +2 -0
  8. package/src/be/migrations/046_budgets_and_pricing.sql +87 -0
  9. package/src/be/migrations/047_session_costs_cost_source.sql +16 -0
  10. package/src/cli.tsx +22 -1
  11. package/src/commands/claude-managed-setup.ts +687 -0
  12. package/src/commands/codex-login.ts +1 -1
  13. package/src/commands/runner.ts +175 -28
  14. package/src/commands/templates.ts +10 -6
  15. package/src/http/budgets.ts +219 -0
  16. package/src/http/index.ts +6 -0
  17. package/src/http/integrations.ts +134 -0
  18. package/src/http/poll.ts +161 -3
  19. package/src/http/pricing.ts +245 -0
  20. package/src/http/session-data.ts +54 -6
  21. package/src/http/tasks.ts +23 -2
  22. package/src/prompts/base-prompt.ts +103 -73
  23. package/src/prompts/session-templates.ts +43 -0
  24. package/src/providers/claude-adapter.ts +3 -1
  25. package/src/providers/claude-managed-adapter.ts +871 -0
  26. package/src/providers/claude-managed-models.ts +117 -0
  27. package/src/providers/claude-managed-swarm-events.ts +77 -0
  28. package/src/providers/codex-adapter.ts +3 -1
  29. package/src/providers/codex-skill-resolver.ts +10 -0
  30. package/src/providers/codex-swarm-events.ts +20 -161
  31. package/src/providers/devin-adapter.ts +894 -0
  32. package/src/providers/devin-api.ts +207 -0
  33. package/src/providers/devin-playbooks.ts +91 -0
  34. package/src/providers/devin-skill-resolver.ts +113 -0
  35. package/src/providers/index.ts +10 -1
  36. package/src/providers/pi-mono-adapter.ts +3 -1
  37. package/src/providers/swarm-events-shared.ts +262 -0
  38. package/src/providers/types.ts +26 -1
  39. package/src/tests/base-prompt.test.ts +199 -0
  40. package/src/tests/budget-admission.test.ts +339 -0
  41. package/src/tests/budget-claim-gate.test.ts +288 -0
  42. package/src/tests/budget-refusal-notification.test.ts +324 -0
  43. package/src/tests/budgets-routes.test.ts +331 -0
  44. package/src/tests/claude-managed-adapter.test.ts +1301 -0
  45. package/src/tests/claude-managed-setup.test.ts +325 -0
  46. package/src/tests/devin-adapter.test.ts +677 -0
  47. package/src/tests/devin-api.test.ts +339 -0
  48. package/src/tests/integrations-http.test.ts +211 -0
  49. package/src/tests/migration-046-budgets.test.ts +327 -0
  50. package/src/tests/pricing-routes.test.ts +315 -0
  51. package/src/tests/prompt-template-remaining.test.ts +4 -0
  52. package/src/tests/prompt-template-session.test.ts +2 -2
  53. package/src/tests/provider-adapter.test.ts +1 -1
  54. package/src/tests/runner-budget-refused.test.ts +271 -0
  55. package/src/tests/session-costs-codex-recompute.test.ts +386 -0
  56. package/src/tools/poll-task.ts +13 -2
  57. package/src/tools/task-action.ts +92 -2
  58. package/src/tools/templates.ts +29 -0
  59. package/src/types.ts +116 -0
  60. package/src/utils/budget-backoff.ts +34 -0
  61. package/src/utils/credentials.ts +4 -0
  62. package/src/utils/provider-metadata.ts +9 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.71.2",
3
+ "version": "1.72.0",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
@@ -97,6 +97,7 @@
97
97
  },
98
98
  "dependencies": {
99
99
  "@ai-sdk/openai": "^3.0.41",
100
+ "@anthropic-ai/sdk": "latest",
100
101
  "@asteasolutions/zod-to-openapi": "^8.0.0",
101
102
  "@desplega.ai/business-use": "^0.4.2",
102
103
  "@desplega.ai/localtunnel": "^2.2.0",
@@ -0,0 +1,121 @@
1
+ // Phase 2: budget admission predicate.
2
+ //
3
+ // Pure function. Given an `agentId` and the current UTC instant, returns
4
+ // either `{ allowed: true }` or a structured refusal envelope with the cause,
5
+ // the relevant spend/budget figures, and the next UTC midnight (`resetAt`).
6
+ //
7
+ // Callers (Phase 3 wires the three V1 gates: /api/poll pre-assigned-pending,
8
+ // /api/poll pool, MCP `task-action` `accept`) translate refusals into the
9
+ // `budget_refused` trigger envelope.
10
+
11
+ import { getBudget, getDailySpendForAgent, getDailySpendGlobal } from "./db";
12
+
13
+ export interface BudgetAdmissionAllowed {
14
+ allowed: true;
15
+ }
16
+
17
+ export interface BudgetAdmissionRefused {
18
+ allowed: false;
19
+ cause: "agent" | "global";
20
+ agentSpend?: number;
21
+ agentBudget?: number;
22
+ globalSpend?: number;
23
+ globalBudget?: number;
24
+ /** ISO 8601 of the next UTC midnight (the moment daily spend rolls over). */
25
+ resetAt: string;
26
+ }
27
+
28
+ export type BudgetAdmissionResult = BudgetAdmissionAllowed | BudgetAdmissionRefused;
29
+
30
+ /**
31
+ * Operator escape hatch (per Decision #11 in the plan): setting
32
+ * `BUDGET_ADMISSION_DISABLED=true` at process boot unconditionally returns
33
+ * `{ allowed: true }` from `canClaim`. We log a single `console.warn` per
34
+ * process so deploys with the flag still on are visible in logs.
35
+ */
36
+ let killSwitchWarned = false;
37
+
38
+ function dateUtcFrom(now: Date): string {
39
+ // `Date.toISOString()` always emits `'YYYY-MM-DDTHH:MM:SS.sssZ'` in UTC,
40
+ // so the first 10 chars are the UTC calendar day.
41
+ return now.toISOString().slice(0, 10);
42
+ }
43
+
44
+ function nextUtcMidnight(now: Date): string {
45
+ // Build the FOLLOWING UTC midnight by adding one day to the date components.
46
+ // Adding at the date-component level lets `Date.UTC` handle month and year
47
+ // rollovers automatically (e.g. April 30 → May 1, Dec 31 → Jan 1).
48
+ //
49
+ // Edge case: if `now` is itself exactly UTC midnight, this still returns
50
+ // the NEXT midnight (+24h), not the current instant — covered by tests.
51
+ const next = new Date(
52
+ Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1, 0, 0, 0, 0),
53
+ );
54
+ return next.toISOString();
55
+ }
56
+
57
+ /**
58
+ * Decide whether `agentId` is allowed to claim a new task right now. Pure —
59
+ * does not mutate any DB state. Order:
60
+ * 0. Kill-switch (`BUDGET_ADMISSION_DISABLED=true`) ⇒ allowed.
61
+ * 1. Global budget set + global daily spend ≥ ceiling ⇒ refused (`global`).
62
+ * 2. Agent budget set + agent daily spend ≥ ceiling ⇒ refused (`agent`).
63
+ * 3. Otherwise ⇒ allowed.
64
+ *
65
+ * Global is checked first by design: a tripped global budget halts the entire
66
+ * swarm regardless of any single agent's spend.
67
+ */
68
+ export function canClaim(agentId: string, nowUtc: Date): BudgetAdmissionResult {
69
+ if (process.env.BUDGET_ADMISSION_DISABLED === "true") {
70
+ if (!killSwitchWarned) {
71
+ killSwitchWarned = true;
72
+ console.warn(
73
+ "[budget-admission] BUDGET_ADMISSION_DISABLED=true — all canClaim() calls will return allowed for the lifetime of this process.",
74
+ );
75
+ }
76
+ return { allowed: true };
77
+ }
78
+
79
+ const dateUtc = dateUtcFrom(nowUtc);
80
+ const resetAt = nextUtcMidnight(nowUtc);
81
+
82
+ // 1. Global budget gate.
83
+ const globalBudget = getBudget("global", "");
84
+ if (globalBudget !== null) {
85
+ const globalSpend = getDailySpendGlobal(dateUtc);
86
+ if (globalSpend >= globalBudget.dailyBudgetUsd) {
87
+ return {
88
+ allowed: false,
89
+ cause: "global",
90
+ globalSpend,
91
+ globalBudget: globalBudget.dailyBudgetUsd,
92
+ resetAt,
93
+ };
94
+ }
95
+ }
96
+
97
+ // 2. Per-agent budget gate.
98
+ const agentBudget = getBudget("agent", agentId);
99
+ if (agentBudget !== null) {
100
+ const agentSpend = getDailySpendForAgent(agentId, dateUtc);
101
+ if (agentSpend >= agentBudget.dailyBudgetUsd) {
102
+ return {
103
+ allowed: false,
104
+ cause: "agent",
105
+ agentSpend,
106
+ agentBudget: agentBudget.dailyBudgetUsd,
107
+ resetAt,
108
+ };
109
+ }
110
+ }
111
+
112
+ return { allowed: true };
113
+ }
114
+
115
+ /**
116
+ * Test-only: reset the kill-switch warning latch so the warning fires fresh
117
+ * in subsequent test cases. Not intended for production use.
118
+ */
119
+ export function __resetKillSwitchWarnedForTests(): void {
120
+ killSwitchWarned = false;
121
+ }
@@ -0,0 +1,145 @@
1
+ // Phase 5: lead-facing notification rail for budget refusals.
2
+ //
3
+ // Centralizes the after-commit side effects shared by all three refusal
4
+ // sites (`/api/poll` pre-assigned-pending, `/api/poll` unassigned-pool,
5
+ // MCP `task-action` `accept`). The dedup row (`budget_refusal_notifications`)
6
+ // is recorded INSIDE the same transaction as the refusal — this module owns
7
+ // only the post-commit work: resolving the template body, creating the
8
+ // lead-facing follow-up task, writing the follow-up id back to the dedup
9
+ // row, and emitting `task.budget_refused` to the workflow bus.
10
+ //
11
+ // Crash window between txn commit and follow-up creation is accepted in V1
12
+ // (see plan §5.1). Operators audit via
13
+ // `SELECT ... FROM budget_refusal_notifications WHERE follow_up_task_id IS NULL`.
14
+
15
+ import { resolveTemplate } from "../prompts/resolver";
16
+ import type { AgentTask } from "../types";
17
+ import {
18
+ createTaskExtended,
19
+ getAgentById,
20
+ getLeadAgent,
21
+ setBudgetRefusalFollowUpTaskId,
22
+ } from "./db";
23
+
24
+ export interface BudgetRefusalContext {
25
+ /** The task that was refused (provides Slack context, description). */
26
+ task: Pick<AgentTask, "id" | "task" | "slackChannelId" | "slackThreadTs" | "slackUserId">;
27
+ /** Refusing agent id. */
28
+ agentId: string;
29
+ /** UTC date `YYYY-MM-DD` used as the dedup key alongside `task.id`. */
30
+ date: string;
31
+ /** Refusal cause. */
32
+ cause: "agent" | "global";
33
+ agentSpendUsd?: number;
34
+ agentBudgetUsd?: number;
35
+ globalSpendUsd?: number;
36
+ globalBudgetUsd?: number;
37
+ /** ISO 8601 of the next UTC midnight (when the daily budget resets). */
38
+ resetAt: string;
39
+ }
40
+
41
+ /**
42
+ * Format the human-readable "$X / $Y" pair shown in the lead's follow-up
43
+ * task body. Uses fixed 2-decimal formatting; falls back to `?` for
44
+ * undefined fields (shouldn't happen if `cause` is correctly populated, but
45
+ * defensive).
46
+ */
47
+ function formatSpendSummary(ctx: BudgetRefusalContext): string {
48
+ const fmt = (n: number | undefined): string => (n === undefined ? "?" : `$${n.toFixed(2)}`);
49
+ if (ctx.cause === "agent") {
50
+ return `${fmt(ctx.agentSpendUsd)} / ${fmt(ctx.agentBudgetUsd)}`;
51
+ }
52
+ return `${fmt(ctx.globalSpendUsd)} / ${fmt(ctx.globalBudgetUsd)}`;
53
+ }
54
+
55
+ /**
56
+ * After-commit side effects for a budget refusal:
57
+ *
58
+ * 1. If `inserted` (i.e. this is the first refusal of `(task.id, date)`)
59
+ * AND a lead exists, resolve the `task.budget.refused` template and
60
+ * create a follow-up task assigned to the lead. Inherit Slack context
61
+ * from the refused task so the lead can reply in-thread.
62
+ * 2. Write the new follow-up's id back into
63
+ * `budget_refusal_notifications.follow_up_task_id` for audit.
64
+ * 3. Always emit `task.budget_refused` to the workflow bus — DAG sequencing
65
+ * must react on every refusal (not just the first per day). The dedup is
66
+ * a separate concern (lead notification cadence, not workflow plumbing).
67
+ *
68
+ * Safe to invoke synchronously after the refusal transaction commits. Errors
69
+ * here are logged but do NOT propagate — the refusal envelope has already
70
+ * been returned to the worker, and a missed lead notification is recoverable
71
+ * (operator query against the dedup table) but a thrown error here would be
72
+ * useless noise on the API server.
73
+ */
74
+ export function emitBudgetRefusalSideEffects(ctx: BudgetRefusalContext, inserted: boolean): void {
75
+ // 1. Lead-facing follow-up task (first refusal of the day only).
76
+ if (inserted) {
77
+ try {
78
+ const leadAgent = getLeadAgent();
79
+ if (leadAgent) {
80
+ const refusingAgent = getAgentById(ctx.agentId);
81
+ const agentName = refusingAgent?.name || ctx.agentId.slice(0, 8);
82
+ const taskDesc = ctx.task.task.slice(0, 200);
83
+ const spendSummary = formatSpendSummary(ctx);
84
+
85
+ const resolved = resolveTemplate(
86
+ "task.budget.refused",
87
+ {
88
+ cause: ctx.cause,
89
+ agent_name: agentName,
90
+ task_desc: taskDesc,
91
+ spend_summary: spendSummary,
92
+ reset_at: ctx.resetAt,
93
+ task_id: ctx.task.id,
94
+ },
95
+ { agentId: ctx.agentId },
96
+ );
97
+
98
+ const followUp = createTaskExtended(resolved.text, {
99
+ agentId: leadAgent.id,
100
+ source: "system",
101
+ taskType: "follow-up",
102
+ parentTaskId: ctx.task.id,
103
+ slackChannelId: ctx.task.slackChannelId,
104
+ slackThreadTs: ctx.task.slackThreadTs,
105
+ slackUserId: ctx.task.slackUserId,
106
+ });
107
+
108
+ try {
109
+ setBudgetRefusalFollowUpTaskId(ctx.task.id, ctx.date, followUp.id);
110
+ } catch (err) {
111
+ console.warn(
112
+ `[budget-refusal-notify] Failed to write back follow_up_task_id for task ${ctx.task.id.slice(0, 8)} (${ctx.date}): ${err}`,
113
+ );
114
+ }
115
+
116
+ console.log(
117
+ `[budget-refusal-notify] Notified lead (${leadAgent.name}) of budget refusal — task=${ctx.task.id.slice(0, 8)} cause=${ctx.cause} agent=${agentName}`,
118
+ );
119
+ }
120
+ } catch (err) {
121
+ console.warn(
122
+ `[budget-refusal-notify] Failed to create follow-up task for budget refusal (task=${ctx.task.id.slice(0, 8)}): ${err}`,
123
+ );
124
+ }
125
+ }
126
+
127
+ // 2. Workflow event bus emit — every refusal, not just the first per day.
128
+ try {
129
+ import("../workflows/event-bus").then(({ workflowEventBus }) => {
130
+ workflowEventBus.emit("task.budget_refused", {
131
+ taskId: ctx.task.id,
132
+ agentId: ctx.agentId,
133
+ cause: ctx.cause,
134
+ agentSpendUsd: ctx.agentSpendUsd,
135
+ agentBudgetUsd: ctx.agentBudgetUsd,
136
+ globalSpendUsd: ctx.globalSpendUsd,
137
+ globalBudgetUsd: ctx.globalBudgetUsd,
138
+ resetAt: ctx.resetAt,
139
+ });
140
+ });
141
+ } catch {
142
+ // Mirror the existing emit-pattern in db.ts:1561-1571 — any failure here
143
+ // (e.g. event bus module load error) must not break the refusal path.
144
+ }
145
+ }