@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.
- 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 +339 -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/tools/task-action.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import * as z from "zod";
|
|
3
|
+
import { canClaim } from "@/be/budget-admission";
|
|
4
|
+
import {
|
|
5
|
+
type BudgetRefusalContext,
|
|
6
|
+
emitBudgetRefusalSideEffects,
|
|
7
|
+
} from "@/be/budget-refusal-notify";
|
|
3
8
|
import {
|
|
4
9
|
acceptTask,
|
|
5
10
|
checkDependencies,
|
|
@@ -14,12 +19,13 @@ import {
|
|
|
14
19
|
moveTaskFromBacklog,
|
|
15
20
|
moveTaskToBacklog,
|
|
16
21
|
reassociateSessionLogs,
|
|
22
|
+
recordBudgetRefusalNotification,
|
|
17
23
|
rejectTask,
|
|
18
24
|
releaseTask,
|
|
19
25
|
updateTaskClaudeSessionId,
|
|
20
26
|
} from "@/be/db";
|
|
21
27
|
import { createToolRegistrar } from "@/tools/utils";
|
|
22
|
-
import { AgentTaskSchema } from "@/types";
|
|
28
|
+
import { AgentTaskSchema, BudgetRefusalCauseSchema } from "@/types";
|
|
23
29
|
|
|
24
30
|
const TaskActionSchema = z.enum([
|
|
25
31
|
"create",
|
|
@@ -83,6 +89,14 @@ export const registerTaskActionTool = (server: McpServer) => {
|
|
|
83
89
|
success: z.boolean(),
|
|
84
90
|
message: z.string(),
|
|
85
91
|
task: AgentTaskSchema.optional(),
|
|
92
|
+
// Phase 3: budget-admission refusal fields. Populated only on
|
|
93
|
+
// `accept` action when the per-agent or global daily budget is blown.
|
|
94
|
+
refusalCause: BudgetRefusalCauseSchema.optional(),
|
|
95
|
+
agentSpend: z.number().optional(),
|
|
96
|
+
agentBudget: z.number().optional(),
|
|
97
|
+
globalSpend: z.number().optional(),
|
|
98
|
+
globalBudget: z.number().optional(),
|
|
99
|
+
resetAt: z.string().optional(),
|
|
86
100
|
}),
|
|
87
101
|
},
|
|
88
102
|
async (input, requestInfo, _meta) => {
|
|
@@ -241,6 +255,60 @@ export const registerTaskActionTool = (server: McpServer) => {
|
|
|
241
255
|
message: `Task "${taskId}" has unmet dependencies: ${blockedBy.join(", ")}. Cannot accept until dependencies are completed.`,
|
|
242
256
|
};
|
|
243
257
|
}
|
|
258
|
+
// Budget admission gate (Phase 3). Same in-transaction placement
|
|
259
|
+
// as the /api/poll gates so capacity AND budget share atomicity.
|
|
260
|
+
// Phase 5: record dedup row + capture side-effect context for the
|
|
261
|
+
// after-commit lead follow-up + workflow event-bus emit.
|
|
262
|
+
const admission = canClaim(agentId, new Date());
|
|
263
|
+
if (!admission.allowed) {
|
|
264
|
+
const causeMsg =
|
|
265
|
+
admission.cause === "agent"
|
|
266
|
+
? "agent daily budget exceeded"
|
|
267
|
+
: "global daily budget exceeded";
|
|
268
|
+
const utcDate = new Date().toISOString().slice(0, 10);
|
|
269
|
+
const dedup = recordBudgetRefusalNotification({
|
|
270
|
+
taskId,
|
|
271
|
+
date: utcDate,
|
|
272
|
+
agentId,
|
|
273
|
+
cause: admission.cause,
|
|
274
|
+
agentSpendUsd: admission.agentSpend,
|
|
275
|
+
agentBudgetUsd: admission.agentBudget,
|
|
276
|
+
globalSpendUsd: admission.globalSpend,
|
|
277
|
+
globalBudgetUsd: admission.globalBudget,
|
|
278
|
+
});
|
|
279
|
+
return {
|
|
280
|
+
success: false,
|
|
281
|
+
message: `Refused: ${causeMsg}. Resets at ${admission.resetAt}.`,
|
|
282
|
+
refusalCause: admission.cause,
|
|
283
|
+
...(admission.agentSpend !== undefined && { agentSpend: admission.agentSpend }),
|
|
284
|
+
...(admission.agentBudget !== undefined && { agentBudget: admission.agentBudget }),
|
|
285
|
+
...(admission.globalSpend !== undefined && { globalSpend: admission.globalSpend }),
|
|
286
|
+
...(admission.globalBudget !== undefined && {
|
|
287
|
+
globalBudget: admission.globalBudget,
|
|
288
|
+
}),
|
|
289
|
+
resetAt: admission.resetAt,
|
|
290
|
+
refusalSideEffects: {
|
|
291
|
+
context: {
|
|
292
|
+
task: {
|
|
293
|
+
id: existingTask.id,
|
|
294
|
+
task: existingTask.task,
|
|
295
|
+
slackChannelId: existingTask.slackChannelId,
|
|
296
|
+
slackThreadTs: existingTask.slackThreadTs,
|
|
297
|
+
slackUserId: existingTask.slackUserId,
|
|
298
|
+
},
|
|
299
|
+
agentId,
|
|
300
|
+
date: utcDate,
|
|
301
|
+
cause: admission.cause,
|
|
302
|
+
agentSpendUsd: admission.agentSpend,
|
|
303
|
+
agentBudgetUsd: admission.agentBudget,
|
|
304
|
+
globalSpendUsd: admission.globalSpend,
|
|
305
|
+
globalBudgetUsd: admission.globalBudget,
|
|
306
|
+
resetAt: admission.resetAt,
|
|
307
|
+
} satisfies BudgetRefusalContext,
|
|
308
|
+
inserted: dedup.inserted,
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
244
312
|
const acceptedTask = acceptTask(taskId, agentId);
|
|
245
313
|
if (!acceptedTask) {
|
|
246
314
|
return { success: false, message: `Failed to accept task "${taskId}".` };
|
|
@@ -334,11 +402,33 @@ export const registerTaskActionTool = (server: McpServer) => {
|
|
|
334
402
|
|
|
335
403
|
const result = txn();
|
|
336
404
|
|
|
405
|
+
// Phase 5: when the accept gate refused, run after-commit side
|
|
406
|
+
// effects (lead follow-up + workflow bus). The dedup row was recorded
|
|
407
|
+
// inside the txn; this just consumes the captured context.
|
|
408
|
+
if (
|
|
409
|
+
"refusalSideEffects" in result &&
|
|
410
|
+
result.refusalSideEffects &&
|
|
411
|
+
typeof result.refusalSideEffects === "object"
|
|
412
|
+
) {
|
|
413
|
+
const sideEffects = result.refusalSideEffects as {
|
|
414
|
+
context: BudgetRefusalContext;
|
|
415
|
+
inserted: boolean;
|
|
416
|
+
};
|
|
417
|
+
emitBudgetRefusalSideEffects(sideEffects.context, sideEffects.inserted);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Strip the internal-only `refusalSideEffects` field from the wire
|
|
421
|
+
// response — workers receive only the public refusal envelope.
|
|
422
|
+
const { refusalSideEffects: _omit, ...publicResult } = result as {
|
|
423
|
+
refusalSideEffects?: unknown;
|
|
424
|
+
[key: string]: unknown;
|
|
425
|
+
};
|
|
426
|
+
|
|
337
427
|
return {
|
|
338
428
|
content: [{ type: "text", text: result.message }],
|
|
339
429
|
structuredContent: {
|
|
340
430
|
yourAgentId: agentId,
|
|
341
|
-
...
|
|
431
|
+
...publicResult,
|
|
342
432
|
},
|
|
343
433
|
};
|
|
344
434
|
},
|
package/src/tools/templates.ts
CHANGED
|
@@ -84,3 +84,32 @@ Decide whether to reassign, retry, or handle the failure. Use \`get-task-details
|
|
|
84
84
|
],
|
|
85
85
|
category: "task_lifecycle",
|
|
86
86
|
});
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// Budget refusal follow-up (Phase 5: created when an agent is refused due to
|
|
90
|
+
// per-agent or global daily budget exhaustion)
|
|
91
|
+
// ============================================================================
|
|
92
|
+
|
|
93
|
+
registerTemplate({
|
|
94
|
+
eventType: "task.budget.refused",
|
|
95
|
+
header: "",
|
|
96
|
+
defaultBody: `Budget refusal \u2014 task is blocked.
|
|
97
|
+
|
|
98
|
+
Cause: {{cause}}
|
|
99
|
+
Agent: {{agent_name}}
|
|
100
|
+
Task: {{task_desc}}
|
|
101
|
+
Spend / budget: {{spend_summary}}
|
|
102
|
+
Resets at: {{reset_at}}
|
|
103
|
+
|
|
104
|
+
Decide whether to raise the budget, reassign, or wait for the daily reset.
|
|
105
|
+
Use \`get-task-details\` with taskId "{{task_id}}" for full details.`,
|
|
106
|
+
variables: [
|
|
107
|
+
{ name: "cause", description: "'agent' or 'global'" },
|
|
108
|
+
{ name: "agent_name", description: "Refusing agent name or ID prefix" },
|
|
109
|
+
{ name: "task_desc", description: "Task description (truncated to 200 chars)" },
|
|
110
|
+
{ name: "spend_summary", description: 'Formatted "$X / $Y" pair' },
|
|
111
|
+
{ name: "reset_at", description: "UTC reset time (human readable)" },
|
|
112
|
+
{ name: "task_id", description: "Original task ID" },
|
|
113
|
+
],
|
|
114
|
+
category: "task_lifecycle",
|
|
115
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -68,6 +68,32 @@ export const AgentTaskSourceSchema = z.enum([
|
|
|
68
68
|
]);
|
|
69
69
|
export type AgentTaskSource = z.infer<typeof AgentTaskSourceSchema>;
|
|
70
70
|
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Harness Provider
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// String identifiers accepted by `HARNESS_PROVIDER` and the
|
|
75
|
+
// `createProviderAdapter` factory in `src/providers/index.ts`. Keep this in
|
|
76
|
+
// sync with the factory's switch and the unknown-provider error message.
|
|
77
|
+
export const ProviderNameSchema = z.enum(["claude", "codex", "pi", "devin", "claude-managed"]);
|
|
78
|
+
export type ProviderName = z.infer<typeof ProviderNameSchema>;
|
|
79
|
+
|
|
80
|
+
export type DevinProviderMeta = {
|
|
81
|
+
sessionUrl: string;
|
|
82
|
+
maxAcuLimit?: number;
|
|
83
|
+
acuCostUsd?: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// These providers do not have metadata yet.
|
|
87
|
+
type NoProviderMeta = Record<string, never>;
|
|
88
|
+
|
|
89
|
+
export type ProviderMetaMap = {
|
|
90
|
+
devin: DevinProviderMeta;
|
|
91
|
+
claude: NoProviderMeta;
|
|
92
|
+
codex: NoProviderMeta;
|
|
93
|
+
pi: NoProviderMeta;
|
|
94
|
+
"claude-managed": NoProviderMeta;
|
|
95
|
+
};
|
|
96
|
+
|
|
71
97
|
export const AgentTaskSchema = z.object({
|
|
72
98
|
id: z.uuid(),
|
|
73
99
|
agentId: z.uuid().nullable(), // Nullable for unassigned tasks
|
|
@@ -169,6 +195,10 @@ export const AgentTaskSchema = z.object({
|
|
|
169
195
|
// agent-swarm package version at task creation time. Enables benchmarking
|
|
170
196
|
// performance across releases. Nullable for rows created before tracking was added.
|
|
171
197
|
swarmVersion: z.string().optional(),
|
|
198
|
+
|
|
199
|
+
// Provider tracking — which harness provider ran this task
|
|
200
|
+
provider: ProviderNameSchema.optional(),
|
|
201
|
+
providerMeta: z.record(z.string(), z.unknown()).optional(),
|
|
172
202
|
});
|
|
173
203
|
|
|
174
204
|
// ============================================================================
|
|
@@ -365,6 +395,11 @@ export const AgentLogEventTypeSchema = z.enum([
|
|
|
365
395
|
"service_registered",
|
|
366
396
|
"service_unregistered",
|
|
367
397
|
"service_status_change",
|
|
398
|
+
// Phase 6: budget / pricing operator-mutation audit log events
|
|
399
|
+
"budget.upserted",
|
|
400
|
+
"budget.deleted",
|
|
401
|
+
"pricing.inserted",
|
|
402
|
+
"pricing.deleted",
|
|
368
403
|
]);
|
|
369
404
|
|
|
370
405
|
export const AgentLogSchema = z.object({
|
|
@@ -396,6 +431,9 @@ export const SessionLogSchema = z.object({
|
|
|
396
431
|
export type SessionLog = z.infer<typeof SessionLogSchema>;
|
|
397
432
|
|
|
398
433
|
// Session Cost Types (aggregated cost data per session)
|
|
434
|
+
export const SessionCostSourceSchema = z.enum(["harness", "pricing-table"]);
|
|
435
|
+
export type SessionCostSource = z.infer<typeof SessionCostSourceSchema>;
|
|
436
|
+
|
|
399
437
|
export const SessionCostSchema = z.object({
|
|
400
438
|
id: z.uuid(),
|
|
401
439
|
sessionId: z.string(),
|
|
@@ -410,6 +448,10 @@ export const SessionCostSchema = z.object({
|
|
|
410
448
|
numTurns: z.number().int().min(1),
|
|
411
449
|
model: z.string(),
|
|
412
450
|
isError: z.boolean().default(false),
|
|
451
|
+
// Phase 6: where the recorded totalCostUsd came from. New rows write the
|
|
452
|
+
// actual source ('pricing-table' when the API recomputed Codex USD from DB
|
|
453
|
+
// pricing rows, 'harness' otherwise). Defaults to 'harness' for back-compat.
|
|
454
|
+
costSource: SessionCostSourceSchema.default("harness"),
|
|
413
455
|
createdAt: z.iso.datetime(),
|
|
414
456
|
});
|
|
415
457
|
|
|
@@ -1122,3 +1164,77 @@ export const ContextSnapshotSchema = z.object({
|
|
|
1122
1164
|
});
|
|
1123
1165
|
|
|
1124
1166
|
export type ContextSnapshot = z.infer<typeof ContextSnapshotSchema>;
|
|
1167
|
+
|
|
1168
|
+
// ============================================================================
|
|
1169
|
+
// Budgets + Pricing (per-agent daily cost budget — V1)
|
|
1170
|
+
// ============================================================================
|
|
1171
|
+
//
|
|
1172
|
+
// Timestamp convention for these schemas: number = epoch milliseconds (UTC).
|
|
1173
|
+
// This is a deliberate divergence from the rest of types.ts (which uses
|
|
1174
|
+
// `z.iso.datetime()` strings) so that the price-book "largest
|
|
1175
|
+
// effective_from <= now" lookup is a pure integer comparison. Matches the
|
|
1176
|
+
// SQL columns in migration 046_budgets_and_pricing.sql verbatim.
|
|
1177
|
+
|
|
1178
|
+
export const BudgetScopeSchema = z.enum(["global", "agent"]);
|
|
1179
|
+
export type BudgetScope = z.infer<typeof BudgetScopeSchema>;
|
|
1180
|
+
|
|
1181
|
+
export const BudgetSchema = z.object({
|
|
1182
|
+
scope: BudgetScopeSchema,
|
|
1183
|
+
scopeId: z.string(), // '' (empty string) for the global row
|
|
1184
|
+
dailyBudgetUsd: z.number().nonnegative(),
|
|
1185
|
+
createdAt: z.number(), // epoch ms
|
|
1186
|
+
lastUpdatedAt: z.number(), // epoch ms
|
|
1187
|
+
});
|
|
1188
|
+
export type Budget = z.infer<typeof BudgetSchema>;
|
|
1189
|
+
|
|
1190
|
+
export const PricingProviderSchema = z.enum(["claude", "codex", "pi"]);
|
|
1191
|
+
export type PricingProvider = z.infer<typeof PricingProviderSchema>;
|
|
1192
|
+
|
|
1193
|
+
export const PricingTokenClassSchema = z.enum(["input", "cached_input", "output"]);
|
|
1194
|
+
export type PricingTokenClass = z.infer<typeof PricingTokenClassSchema>;
|
|
1195
|
+
|
|
1196
|
+
export const PricingRowSchema = z.object({
|
|
1197
|
+
provider: PricingProviderSchema,
|
|
1198
|
+
model: z.string(),
|
|
1199
|
+
tokenClass: PricingTokenClassSchema,
|
|
1200
|
+
effectiveFrom: z.number().nonnegative(), // epoch ms; 0 = seed
|
|
1201
|
+
pricePerMillionUsd: z.number().nonnegative(),
|
|
1202
|
+
createdAt: z.number(), // epoch ms
|
|
1203
|
+
lastUpdatedAt: z.number(), // epoch ms
|
|
1204
|
+
});
|
|
1205
|
+
export type PricingRow = z.infer<typeof PricingRowSchema>;
|
|
1206
|
+
|
|
1207
|
+
export const BudgetRefusalCauseSchema = z.enum(["agent", "global"]);
|
|
1208
|
+
export type BudgetRefusalCause = z.infer<typeof BudgetRefusalCauseSchema>;
|
|
1209
|
+
|
|
1210
|
+
export const BudgetRefusalNotificationSchema = z.object({
|
|
1211
|
+
taskId: z.string(),
|
|
1212
|
+
date: z.string(), // 'YYYY-MM-DD' UTC
|
|
1213
|
+
agentId: z.string(),
|
|
1214
|
+
cause: BudgetRefusalCauseSchema,
|
|
1215
|
+
agentSpendUsd: z.number().nullable().optional(),
|
|
1216
|
+
agentBudgetUsd: z.number().nullable().optional(),
|
|
1217
|
+
globalSpendUsd: z.number().nullable().optional(),
|
|
1218
|
+
globalBudgetUsd: z.number().nullable().optional(),
|
|
1219
|
+
followUpTaskId: z.string().nullable().optional(),
|
|
1220
|
+
createdAt: z.number(), // epoch ms
|
|
1221
|
+
});
|
|
1222
|
+
export type BudgetRefusalNotification = z.infer<typeof BudgetRefusalNotificationSchema>;
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Phase 3 — `budget_refused` is the new variant of the `/api/poll` trigger
|
|
1226
|
+
* envelope returned when an admission gate (`canClaim`) refuses to let the
|
|
1227
|
+
* agent take a task. Older workers receiving this discriminator fall through
|
|
1228
|
+
* to default polling without back-off (degrades gracefully); Phase 4 teaches
|
|
1229
|
+
* the runner to recognize it.
|
|
1230
|
+
*/
|
|
1231
|
+
export const BudgetRefusedTriggerSchema = z.object({
|
|
1232
|
+
type: z.literal("budget_refused"),
|
|
1233
|
+
cause: BudgetRefusalCauseSchema,
|
|
1234
|
+
agentSpend: z.number().optional(),
|
|
1235
|
+
agentBudget: z.number().optional(),
|
|
1236
|
+
globalSpend: z.number().optional(),
|
|
1237
|
+
globalBudget: z.number().optional(),
|
|
1238
|
+
resetAt: z.string(), // ISO 8601, next UTC midnight
|
|
1239
|
+
});
|
|
1240
|
+
export type BudgetRefusedTrigger = z.infer<typeof BudgetRefusedTriggerSchema>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 — exponential back-off for `budget_refused` poll responses.
|
|
3
|
+
*
|
|
4
|
+
* The worker poll loop short-circuits on `trigger.type === "budget_refused"`
|
|
5
|
+
* to avoid busy-looping the API while the agent is over-budget. Each
|
|
6
|
+
* consecutive refusal doubles the sleep, capped at 5 minutes (per the
|
|
7
|
+
* scoping decision in the per-agent-daily-cost-budget plan).
|
|
8
|
+
*
|
|
9
|
+
* This module is a pure helper so it can be unit-tested in isolation
|
|
10
|
+
* without standing up the full poll loop.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Hard cap on back-off, per the plan (5 minutes). */
|
|
14
|
+
export const BUDGET_BACKOFF_CAP_MS = 5 * 60 * 1000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compute the back-off delay for the Nth consecutive `budget_refused`
|
|
18
|
+
* trigger. The first refusal sleeps `basePollMs`; each subsequent one
|
|
19
|
+
* doubles, capped at {@link BUDGET_BACKOFF_CAP_MS}.
|
|
20
|
+
*
|
|
21
|
+
* @param consecutiveRefusals - 1-indexed count of consecutive refusals
|
|
22
|
+
* (i.e. the Nth refusal in a row, including the current one).
|
|
23
|
+
* Must be >= 1; values < 1 fall back to `basePollMs`.
|
|
24
|
+
* @param basePollMs - Base poll interval (today's `PollIntervalMs`, ~2s).
|
|
25
|
+
* @returns Sleep duration in milliseconds, capped at 5 minutes.
|
|
26
|
+
*/
|
|
27
|
+
export function computeBudgetBackoffMs(consecutiveRefusals: number, basePollMs: number): number {
|
|
28
|
+
const n = Math.max(1, Math.floor(consecutiveRefusals));
|
|
29
|
+
// 2 ** (n-1) grows quickly; cap before multiplying to avoid Infinity for
|
|
30
|
+
// pathological inputs. JS handles 2 ** 30 fine but cap-first is cleaner.
|
|
31
|
+
const exponent = Math.min(30, n - 1);
|
|
32
|
+
const raw = basePollMs * 2 ** exponent;
|
|
33
|
+
return Math.min(BUDGET_BACKOFF_CAP_MS, raw);
|
|
34
|
+
}
|
package/src/utils/credentials.ts
CHANGED
|
@@ -5,6 +5,7 @@ export const CREDENTIAL_POOL_VARS = [
|
|
|
5
5
|
"OPENROUTER_API_KEY",
|
|
6
6
|
"OPENAI_API_KEY",
|
|
7
7
|
"CODEX_OAUTH",
|
|
8
|
+
"DEVIN_API_KEY",
|
|
8
9
|
] as const;
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -23,6 +24,7 @@ export const PROVIDER_CREDENTIAL_VARS: Record<string, readonly string[]> = {
|
|
|
23
24
|
// pi-mono accepts either router or anthropic keys
|
|
24
25
|
pi: ["OPENROUTER_API_KEY", "ANTHROPIC_API_KEY"],
|
|
25
26
|
codex: ["OPENAI_API_KEY", "CODEX_OAUTH"],
|
|
27
|
+
devin: ["DEVIN_API_KEY"],
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
/**
|
|
@@ -43,6 +45,8 @@ export function deriveProviderFromKeyType(keyType: string): string {
|
|
|
43
45
|
case "OPENAI_API_KEY":
|
|
44
46
|
case "CODEX_OAUTH":
|
|
45
47
|
return "codex";
|
|
48
|
+
case "DEVIN_API_KEY":
|
|
49
|
+
return "devin";
|
|
46
50
|
default:
|
|
47
51
|
return "claude";
|
|
48
52
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ProviderMetaMap, ProviderName } from "@/types";
|
|
2
|
+
|
|
3
|
+
export function parseProviderMeta<TProvider extends ProviderName>(
|
|
4
|
+
rawProvider: TProvider | null | undefined,
|
|
5
|
+
rawMeta: string | undefined | null,
|
|
6
|
+
): ProviderMetaMap[TProvider] | undefined {
|
|
7
|
+
if (!rawMeta || !rawProvider) return undefined;
|
|
8
|
+
return JSON.parse(rawMeta) as ProviderMetaMap[TProvider];
|
|
9
|
+
}
|