@desplega.ai/agent-swarm 1.71.2 → 1.72.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/README.md +3 -2
- package/openapi.json +994 -62
- package/package.json +2 -1
- package/src/be/budget-admission.ts +121 -0
- package/src/be/budget-refusal-notify.ts +145 -0
- package/src/be/db.ts +488 -5
- package/src/be/migrations/044_provider_meta.sql +2 -0
- package/src/be/migrations/046_budgets_and_pricing.sql +87 -0
- package/src/be/migrations/047_session_costs_cost_source.sql +16 -0
- package/src/cli.tsx +22 -1
- package/src/commands/claude-managed-setup.ts +687 -0
- package/src/commands/codex-login.ts +1 -1
- package/src/commands/runner.ts +175 -28
- package/src/commands/templates.ts +10 -6
- package/src/http/budgets.ts +219 -0
- package/src/http/index.ts +6 -0
- package/src/http/integrations.ts +134 -0
- package/src/http/poll.ts +161 -3
- package/src/http/pricing.ts +245 -0
- package/src/http/session-data.ts +54 -6
- package/src/http/tasks.ts +23 -2
- package/src/prompts/base-prompt.ts +103 -73
- package/src/prompts/session-templates.ts +43 -0
- package/src/providers/claude-adapter.ts +3 -1
- package/src/providers/claude-managed-adapter.ts +871 -0
- package/src/providers/claude-managed-models.ts +117 -0
- package/src/providers/claude-managed-swarm-events.ts +77 -0
- package/src/providers/codex-adapter.ts +3 -1
- package/src/providers/codex-skill-resolver.ts +10 -0
- package/src/providers/codex-swarm-events.ts +20 -161
- package/src/providers/devin-adapter.ts +894 -0
- package/src/providers/devin-api.ts +207 -0
- package/src/providers/devin-playbooks.ts +91 -0
- package/src/providers/devin-skill-resolver.ts +113 -0
- package/src/providers/index.ts +10 -1
- package/src/providers/pi-mono-adapter.ts +3 -1
- package/src/providers/swarm-events-shared.ts +262 -0
- package/src/providers/types.ts +26 -1
- package/src/tests/base-prompt.test.ts +199 -0
- package/src/tests/budget-admission.test.ts +338 -0
- package/src/tests/budget-claim-gate.test.ts +288 -0
- package/src/tests/budget-refusal-notification.test.ts +324 -0
- package/src/tests/budgets-routes.test.ts +331 -0
- package/src/tests/claude-managed-adapter.test.ts +1301 -0
- package/src/tests/claude-managed-setup.test.ts +325 -0
- package/src/tests/devin-adapter.test.ts +677 -0
- package/src/tests/devin-api.test.ts +339 -0
- package/src/tests/integrations-http.test.ts +211 -0
- package/src/tests/migration-046-budgets.test.ts +327 -0
- package/src/tests/pricing-routes.test.ts +315 -0
- package/src/tests/prompt-template-remaining.test.ts +4 -0
- package/src/tests/prompt-template-session.test.ts +2 -2
- package/src/tests/provider-adapter.test.ts +1 -1
- package/src/tests/runner-budget-refused.test.ts +271 -0
- package/src/tests/session-costs-codex-recompute.test.ts +386 -0
- package/src/tools/poll-task.ts +13 -2
- package/src/tools/task-action.ts +92 -2
- package/src/tools/templates.ts +29 -0
- package/src/types.ts +116 -0
- package/src/utils/budget-backoff.ts +34 -0
- package/src/utils/credentials.ts +4 -0
- package/src/utils/provider-metadata.ts +9 -0
- package/src/workflows/executors/raw-llm.ts +1 -1
- package/src/workflows/executors/validate.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.72.1",
|
|
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
|
+
}
|