@desplega.ai/agent-swarm 1.93.0 → 1.95.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 +2 -2
- package/openapi.json +180 -1
- package/package.json +4 -3
- package/src/be/db.ts +74 -9
- package/src/be/migrations/090_model_tiers.sql +2 -0
- package/src/be/migrations/091_seed_swarm_operations_metrics.sql +12 -0
- package/src/be/migrations/092_metrics_dashboard_combobox_filters.sql +68 -0
- package/src/be/migrations/093_slack_message_tracking.sql +6 -0
- package/src/be/migrations/094_mcp_extra_authorize_params.sql +4 -0
- package/src/be/migrations/runner.ts +52 -0
- package/src/be/modelsdev-cache.json +2060 -198
- package/src/be/scripts/boot-reembed.ts +74 -0
- package/src/be/scripts/db.ts +19 -3
- package/src/be/seed/index.ts +1 -1
- package/src/be/seed/registry.ts +2 -2
- package/src/be/seed/runner.ts +5 -5
- package/src/be/seed/types.ts +6 -1
- package/src/be/seed-pricing.ts +1 -0
- package/src/be/seed-scripts/index.ts +3 -2
- package/src/be/skill-sync.ts +4 -4
- package/src/be/swarm-config-guard.ts +8 -0
- package/src/commands/provider-credentials.ts +14 -8
- package/src/commands/runner.ts +84 -13
- package/src/http/index.ts +13 -2
- package/src/http/mcp-oauth.ts +14 -0
- package/src/http/metrics.ts +55 -6
- package/src/http/schedules.ts +16 -15
- package/src/http/script-runs.ts +7 -1
- package/src/http/scripts.ts +147 -1
- package/src/http/tasks.ts +7 -0
- package/src/model-tiers.ts +140 -0
- package/src/oauth/mcp-wrapper.ts +14 -0
- package/src/providers/claude-managed-models.ts +9 -0
- package/src/providers/codex-skill-resolver.ts +22 -8
- package/src/providers/opencode-adapter.ts +21 -2
- package/src/providers/pi-mono-adapter.ts +143 -26
- package/src/providers/types.ts +12 -0
- package/src/scheduler/scheduler.ts +22 -34
- package/src/server-user.ts +8 -2
- package/src/slack/responses.ts +39 -11
- package/src/slack/watcher.ts +121 -8
- package/src/tests/agents-list-model-display.test.ts +13 -0
- package/src/tests/aws-error-classifier.test.ts +148 -0
- package/src/tests/claude-managed-adapter.test.ts +12 -0
- package/src/tests/context-window.test.ts +7 -0
- package/src/tests/credential-check.test.ts +185 -46
- package/src/tests/harness-provider-resolution.test.ts +23 -0
- package/src/tests/http-api-integration.test.ts +19 -0
- package/src/tests/mcp-oauth-queries.test.ts +71 -1
- package/src/tests/mcp-oauth-wrapper.test.ts +109 -0
- package/src/tests/metrics-http.test.ts +137 -3
- package/src/tests/migration-046-budgets.test.ts +33 -0
- package/src/tests/migration-runner-regressions.test.ts +69 -0
- package/src/tests/model-control.test.ts +162 -46
- package/src/tests/opencode-adapter.test.ts +38 -1
- package/src/tests/pi-mono-adapter.test.ts +319 -0
- package/src/tests/provider-command-format.test.ts +12 -0
- package/src/tests/providers/pi-cost.test.ts +9 -0
- package/src/tests/runner-fallback-output.test.ts +50 -0
- package/src/tests/scripts-boot-reembed.test.ts +163 -0
- package/src/tests/scripts-embeddings.test.ts +90 -0
- package/src/tests/seed.test.ts +26 -1
- package/src/tests/session-costs-model-key-normalize.test.ts +2 -0
- package/src/tests/skill-fs-writer.test.ts +7 -1
- package/src/tests/skill-sync.test.ts +15 -3
- package/src/tests/slack-watcher.test.ts +66 -0
- package/src/tests/workflow-agent-task.test.ts +5 -2
- package/src/tests/workflow-validation-port-routing.test.ts +181 -0
- package/src/tools/mcp-servers/mcp-server-create.ts +7 -0
- package/src/tools/mcp-servers/mcp-server-update.ts +8 -0
- package/src/tools/memory-get.ts +11 -0
- package/src/tools/memory-search.ts +18 -0
- package/src/tools/schedules/create-schedule.ts +71 -70
- package/src/tools/schedules/update-schedule.ts +43 -31
- package/src/tools/send-task.ts +16 -5
- package/src/tools/task-action.ts +11 -3
- package/src/types.ts +30 -0
- package/src/utils/aws-error-classifier.ts +97 -0
- package/src/utils/context-window.ts +2 -0
- package/src/utils/credentials.test.ts +68 -0
- package/src/utils/credentials.ts +44 -3
- package/src/utils/pretty-print.ts +25 -10
- package/src/utils/skill-fs-writer.ts +11 -3
- package/src/workflows/engine.ts +3 -2
- package/src/workflows/executors/agent-task.ts +3 -1
package/src/http/metrics.ts
CHANGED
|
@@ -48,12 +48,48 @@ function slugify(input: string): string {
|
|
|
48
48
|
|
|
49
49
|
function validateMetricDefinition(definition: unknown) {
|
|
50
50
|
const parsed = MetricDefinitionSchema.parse(definition);
|
|
51
|
+
for (const variable of parsed.variables ?? []) {
|
|
52
|
+
if (variable.optionsQuery) {
|
|
53
|
+
assertSelectOnlyQuery(variable.optionsQuery.sql);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
51
56
|
for (const widget of parsed.widgets) {
|
|
52
57
|
assertSelectOnlyQuery(widget.query.sql);
|
|
53
58
|
}
|
|
54
59
|
return parsed;
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
function resolveVariableOptionValues(variable: MetricVariable) {
|
|
63
|
+
if (!variable.optionsQuery) return variable.options ?? [];
|
|
64
|
+
assertSelectOnlyQuery(variable.optionsQuery.sql);
|
|
65
|
+
const result = executeReadOnlyQuery(variable.optionsQuery.sql, [], HARD_METRIC_MAX_ROWS);
|
|
66
|
+
return result.rows.map((row) => {
|
|
67
|
+
const record = Object.fromEntries(
|
|
68
|
+
result.columns.map((column, index) => [column, row[index] as MetricParam]),
|
|
69
|
+
);
|
|
70
|
+
const value = record[variable.optionsQuery!.valueKey];
|
|
71
|
+
const labelKey = variable.optionsQuery!.labelKey ?? variable.optionsQuery!.valueKey;
|
|
72
|
+
const label = record[labelKey] ?? value;
|
|
73
|
+
return {
|
|
74
|
+
label: label == null ? "" : String(label),
|
|
75
|
+
value: value == null ? null : value,
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveVariableOptions(metric: Metric) {
|
|
81
|
+
const optionsByKey: Record<string, Array<{ label: string; value: MetricParam }>> = {};
|
|
82
|
+
const variables = (metric.definition.variables ?? []).map((variable) => {
|
|
83
|
+
if (!variable.optionsQuery) {
|
|
84
|
+
return variable;
|
|
85
|
+
}
|
|
86
|
+
const options = resolveVariableOptionValues(variable);
|
|
87
|
+
optionsByKey[variable.key] = options;
|
|
88
|
+
return { ...variable, options };
|
|
89
|
+
});
|
|
90
|
+
return { variables, optionsByKey };
|
|
91
|
+
}
|
|
92
|
+
|
|
57
93
|
function coerceVariableValue(variable: MetricVariable, raw: unknown): MetricParam {
|
|
58
94
|
if (raw == null || raw === "") {
|
|
59
95
|
return variable.defaultValue ?? null;
|
|
@@ -71,12 +107,21 @@ function coerceVariableValue(variable: MetricVariable, raw: unknown): MetricPara
|
|
|
71
107
|
return String(raw);
|
|
72
108
|
}
|
|
73
109
|
|
|
74
|
-
function resolveMetricVariables(
|
|
110
|
+
function resolveMetricVariables(
|
|
111
|
+
metric: Metric,
|
|
112
|
+
provided: Record<string, unknown>,
|
|
113
|
+
dynamicOptionsByKey: Record<string, Array<{ label: string; value: MetricParam }>> = {},
|
|
114
|
+
) {
|
|
75
115
|
const values: Record<string, MetricParam> = {};
|
|
76
116
|
for (const variable of metric.definition.variables ?? []) {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
117
|
+
const options = dynamicOptionsByKey[variable.key] ?? variable.options;
|
|
118
|
+
const raw = provided[variable.key];
|
|
119
|
+
const value =
|
|
120
|
+
(raw == null || raw === "") && variable.defaultValue === undefined && options?.length
|
|
121
|
+
? options[0]!.value
|
|
122
|
+
: coerceVariableValue(variable, raw);
|
|
123
|
+
if (options?.length) {
|
|
124
|
+
const allowed = options.some((option) => option.value === value);
|
|
80
125
|
if (!allowed) {
|
|
81
126
|
throw new Error(`Metric variable "${variable.key}" must match one of its options`);
|
|
82
127
|
}
|
|
@@ -125,10 +170,14 @@ function runMetricWidget(widget: MetricWidget, variables: Record<string, MetricP
|
|
|
125
170
|
}
|
|
126
171
|
|
|
127
172
|
function runMetric(metric: Metric, providedVariables: Record<string, unknown> = {}) {
|
|
128
|
-
const
|
|
173
|
+
const resolved = resolveVariableOptions(metric);
|
|
174
|
+
const variables = resolveMetricVariables(metric, providedVariables, resolved.optionsByKey);
|
|
129
175
|
const widgets = metric.definition.widgets.map((widget) => runMetricWidget(widget, variables));
|
|
130
176
|
return {
|
|
131
|
-
metric
|
|
177
|
+
metric: {
|
|
178
|
+
...metric,
|
|
179
|
+
definition: { ...metric.definition, variables: resolved.variables },
|
|
180
|
+
},
|
|
132
181
|
variables,
|
|
133
182
|
widgets,
|
|
134
183
|
// Kept as the first widget result for older callers during the PR cycle.
|
package/src/http/schedules.ts
CHANGED
|
@@ -12,9 +12,8 @@ import {
|
|
|
12
12
|
updateScheduledTask,
|
|
13
13
|
} from "../be/db";
|
|
14
14
|
import { mergeScheduleTiming, validateRecurringTiming } from "../be/schedules/validate";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
15
|
+
import { ModelTierSchema, splitLegacyModelAlias } from "../model-tiers";
|
|
16
|
+
import { calculateNextRun, createStandaloneScheduleTask } from "../scheduler/scheduler";
|
|
18
17
|
import { getExecutorRegistry } from "../workflows";
|
|
19
18
|
import { handleScheduleTrigger } from "../workflows/triggers";
|
|
20
19
|
import { route } from "./route-def";
|
|
@@ -41,6 +40,7 @@ const createSchedule = route({
|
|
|
41
40
|
enabled: z.boolean().optional(),
|
|
42
41
|
timezone: z.string().optional(),
|
|
43
42
|
model: z.string().optional(),
|
|
43
|
+
modelTier: ModelTierSchema.optional(),
|
|
44
44
|
scheduleType: z.enum(["recurring", "one_time"]).optional(),
|
|
45
45
|
delayMs: z.number().int().optional(),
|
|
46
46
|
runAt: z.string().optional(),
|
|
@@ -126,6 +126,7 @@ const updateSchedule = route({
|
|
|
126
126
|
enabled: z.boolean().optional(),
|
|
127
127
|
timezone: z.string().optional(),
|
|
128
128
|
model: z.string().optional(),
|
|
129
|
+
modelTier: ModelTierSchema.nullable().optional(),
|
|
129
130
|
nextRunAt: z.string().nullable().optional(),
|
|
130
131
|
}),
|
|
131
132
|
responses: {
|
|
@@ -270,7 +271,7 @@ export async function handleSchedules(
|
|
|
270
271
|
enabled: body.enabled,
|
|
271
272
|
nextRunAt,
|
|
272
273
|
timezone: body.timezone,
|
|
273
|
-
model: body.model,
|
|
274
|
+
...splitLegacyModelAlias({ model: body.model, modelTier: body.modelTier }),
|
|
274
275
|
scheduleType: body.scheduleType,
|
|
275
276
|
});
|
|
276
277
|
|
|
@@ -333,17 +334,7 @@ export async function handleSchedules(
|
|
|
333
334
|
const now = new Date().toISOString();
|
|
334
335
|
|
|
335
336
|
const task = getDb().transaction(() => {
|
|
336
|
-
const createdTask =
|
|
337
|
-
creatorAgentId: schedule.createdByAgentId,
|
|
338
|
-
taskType: schedule.taskType,
|
|
339
|
-
tags: [...schedule.tags, "scheduled", `schedule:${schedule.name}`, "manual-run"],
|
|
340
|
-
priority: schedule.priority,
|
|
341
|
-
agentId: schedule.targetAgentId,
|
|
342
|
-
model: schedule.model,
|
|
343
|
-
scheduleId: schedule.id,
|
|
344
|
-
source: "schedule",
|
|
345
|
-
contextKey: scheduleContextKey({ scheduleId: schedule.id }),
|
|
346
|
-
});
|
|
337
|
+
const createdTask = createStandaloneScheduleTask(schedule, ["manual-run"]);
|
|
347
338
|
|
|
348
339
|
if (schedule.scheduleType === "one_time") {
|
|
349
340
|
updateScheduledTask(schedule.id, {
|
|
@@ -388,6 +379,16 @@ export async function handleSchedules(
|
|
|
388
379
|
const parsed = await updateSchedule.parse(req, res, pathSegments, queryParams);
|
|
389
380
|
if (!parsed) return true;
|
|
390
381
|
const body = parsed.body as Record<string, unknown>;
|
|
382
|
+
if (parsed.body.model !== undefined || parsed.body.modelTier !== undefined) {
|
|
383
|
+
const normalizedModel = splitLegacyModelAlias({
|
|
384
|
+
model: parsed.body.model,
|
|
385
|
+
modelTier: parsed.body.modelTier,
|
|
386
|
+
});
|
|
387
|
+
if (parsed.body.model !== undefined) body.model = normalizedModel.model ?? null;
|
|
388
|
+
if (parsed.body.modelTier !== undefined || normalizedModel.modelTier) {
|
|
389
|
+
body.modelTier = normalizedModel.modelTier ?? null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
391
392
|
|
|
392
393
|
const existing = getScheduledTaskById(parsed.params.id);
|
|
393
394
|
if (!existing) {
|
package/src/http/script-runs.ts
CHANGED
|
@@ -51,6 +51,7 @@ const createScriptRunBodySchema = z.object({
|
|
|
51
51
|
const listScriptRunsQuerySchema = z.object({
|
|
52
52
|
status: ScriptRunStatusSchema.optional(),
|
|
53
53
|
agentId: z.string().optional(),
|
|
54
|
+
scriptName: z.string().optional(),
|
|
54
55
|
limit: z.coerce.number().int().min(1).max(500).optional(),
|
|
55
56
|
offset: z.coerce.number().int().min(0).optional(),
|
|
56
57
|
});
|
|
@@ -367,12 +368,17 @@ export async function handleScriptRuns(
|
|
|
367
368
|
const opts = {
|
|
368
369
|
status: parsed.query.status,
|
|
369
370
|
agentId: parsed.query.agentId,
|
|
371
|
+
scriptName: parsed.query.scriptName,
|
|
370
372
|
limit: parsed.query.limit ?? 50,
|
|
371
373
|
offset: parsed.query.offset ?? 0,
|
|
372
374
|
};
|
|
373
375
|
json(res, {
|
|
374
376
|
runs: listScriptRuns(opts),
|
|
375
|
-
total: countScriptRuns({
|
|
377
|
+
total: countScriptRuns({
|
|
378
|
+
status: opts.status,
|
|
379
|
+
agentId: opts.agentId,
|
|
380
|
+
scriptName: opts.scriptName,
|
|
381
|
+
}),
|
|
376
382
|
});
|
|
377
383
|
return true;
|
|
378
384
|
}
|
package/src/http/scripts.ts
CHANGED
|
@@ -2,14 +2,23 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getAgentById, recordInlineScriptRun, upsertKv } from "../be/db";
|
|
4
4
|
import { createEvent } from "../be/events";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
deleteScript,
|
|
7
|
+
getScript,
|
|
8
|
+
getScriptById,
|
|
9
|
+
listScripts,
|
|
10
|
+
listScriptVersions,
|
|
11
|
+
upsertScriptByName,
|
|
12
|
+
} from "../be/scripts/db";
|
|
6
13
|
import { searchScripts } from "../be/scripts/embeddings";
|
|
7
14
|
import { extractArgsJsonSchema } from "../be/scripts/extract-schema";
|
|
8
15
|
import { SCRIPT_SDK_TYPES, SCRIPT_STDLIB_TYPES, typecheckScript } from "../be/scripts/typecheck";
|
|
9
16
|
import { extractScriptSignature } from "../scripts-runtime/extract-signature";
|
|
10
17
|
import { runScript } from "../scripts-runtime/loader";
|
|
11
18
|
import {
|
|
19
|
+
type ScriptDetail,
|
|
12
20
|
ScriptFsModeSchema,
|
|
21
|
+
type ScriptListItem,
|
|
13
22
|
type ScriptRecord,
|
|
14
23
|
type ScriptScope,
|
|
15
24
|
ScriptScopeSchema,
|
|
@@ -52,6 +61,11 @@ const searchBodySchema = z.object({
|
|
|
52
61
|
const nameParamsSchema = z.object({ name: scriptNameSchema });
|
|
53
62
|
const scopeQuerySchema = z.object({ scope: ScriptScopeSchema.default("agent") });
|
|
54
63
|
const optionalScopeQuerySchema = z.object({ scope: ScriptScopeSchema.optional() });
|
|
64
|
+
const idParamsSchema = z.object({ id: z.string().uuid() });
|
|
65
|
+
const listScriptsQuerySchema = z.object({
|
|
66
|
+
scope: ScriptScopeSchema.optional(),
|
|
67
|
+
includeScratch: z.enum(["true", "false"]).optional(),
|
|
68
|
+
});
|
|
55
69
|
|
|
56
70
|
const upsertRoute = route({
|
|
57
71
|
method: "post",
|
|
@@ -133,6 +147,74 @@ const typesRoute = route({
|
|
|
133
147
|
},
|
|
134
148
|
});
|
|
135
149
|
|
|
150
|
+
// ── Dashboard read routes ──
|
|
151
|
+
// The worker-facing routes above resolve scripts relative to the calling agent
|
|
152
|
+
// and therefore requireAgent (X-Agent-ID). The routes below are cross-scope
|
|
153
|
+
// admin reads for the dashboard: API-key auth only, no agent identity — the
|
|
154
|
+
// same model as /api/script-runs.
|
|
155
|
+
|
|
156
|
+
const listScriptsRoute = route({
|
|
157
|
+
method: "get",
|
|
158
|
+
path: "/api/scripts",
|
|
159
|
+
pattern: ["api", "scripts"],
|
|
160
|
+
operationId: "scripts_list",
|
|
161
|
+
summary: "List saved scripts",
|
|
162
|
+
description:
|
|
163
|
+
"Dashboard read: lean projection without source. Scratch scripts are excluded unless includeScratch=true.",
|
|
164
|
+
tags: ["Scripts"],
|
|
165
|
+
query: listScriptsQuerySchema,
|
|
166
|
+
responses: {
|
|
167
|
+
200: { description: "Saved scripts" },
|
|
168
|
+
400: { description: "Validation error" },
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Declared (and matched) BEFORE the by-id route: the by-id pattern
|
|
173
|
+
// ["api", "scripts", null] matches any single segment, so the literal
|
|
174
|
+
// "type-defs" segment must win first.
|
|
175
|
+
const typeDefsRoute = route({
|
|
176
|
+
method: "get",
|
|
177
|
+
path: "/api/scripts/type-defs",
|
|
178
|
+
pattern: ["api", "scripts", "type-defs"],
|
|
179
|
+
operationId: "scripts_type_defs",
|
|
180
|
+
summary: "Get script SDK and stdlib type definitions",
|
|
181
|
+
description: "Static .d.ts blobs for editor integration (e.g. Monaco extraLibs). Cacheable.",
|
|
182
|
+
tags: ["Scripts"],
|
|
183
|
+
responses: {
|
|
184
|
+
200: { description: "SDK and stdlib type definition blobs" },
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const getScriptByIdRoute = route({
|
|
189
|
+
method: "get",
|
|
190
|
+
path: "/api/scripts/{id}",
|
|
191
|
+
pattern: ["api", "scripts", null],
|
|
192
|
+
operationId: "scripts_get",
|
|
193
|
+
summary: "Get a saved script by id",
|
|
194
|
+
description: "Dashboard read: full record including source and parsed signature.",
|
|
195
|
+
tags: ["Scripts"],
|
|
196
|
+
params: idParamsSchema,
|
|
197
|
+
responses: {
|
|
198
|
+
200: { description: "Script detail" },
|
|
199
|
+
404: { description: "Script not found" },
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const listVersionsRoute = route({
|
|
204
|
+
method: "get",
|
|
205
|
+
path: "/api/scripts/{id}/versions",
|
|
206
|
+
pattern: ["api", "scripts", null, "versions"],
|
|
207
|
+
operationId: "scripts_versions",
|
|
208
|
+
summary: "List versions of a saved script",
|
|
209
|
+
description: "Dashboard read: version history, newest first.",
|
|
210
|
+
tags: ["Scripts"],
|
|
211
|
+
params: idParamsSchema,
|
|
212
|
+
responses: {
|
|
213
|
+
200: { description: "Script versions" },
|
|
214
|
+
404: { description: "Script not found" },
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
136
218
|
function requireAgent(res: ServerResponse, agentId: string | undefined) {
|
|
137
219
|
if (!agentId) {
|
|
138
220
|
jsonError(res, "X-Agent-ID required for scripts API", 400);
|
|
@@ -413,6 +495,70 @@ export async function handleScripts(
|
|
|
413
495
|
return true;
|
|
414
496
|
}
|
|
415
497
|
|
|
498
|
+
// ── Dashboard reads (no requireAgent — API-key auth only, like /api/script-runs) ──
|
|
499
|
+
|
|
500
|
+
if (listScriptsRoute.match(req.method, pathSegments)) {
|
|
501
|
+
const parsed = await listScriptsRoute.parse(req, res, pathSegments, queryParams);
|
|
502
|
+
if (!parsed) return true;
|
|
503
|
+
const scripts: ScriptListItem[] = listScripts({
|
|
504
|
+
scope: parsed.query.scope,
|
|
505
|
+
includeScratch: parsed.query.includeScratch === "true",
|
|
506
|
+
}).map((script) => ({
|
|
507
|
+
id: script.id,
|
|
508
|
+
name: script.name,
|
|
509
|
+
scope: script.scope,
|
|
510
|
+
scopeId: script.scopeId,
|
|
511
|
+
description: script.description,
|
|
512
|
+
intent: script.intent,
|
|
513
|
+
version: script.version,
|
|
514
|
+
isScratch: script.isScratch,
|
|
515
|
+
typeChecked: script.typeChecked,
|
|
516
|
+
fsMode: script.fsMode,
|
|
517
|
+
createdByAgentId: script.createdByAgentId,
|
|
518
|
+
createdAt: script.createdAt,
|
|
519
|
+
updatedAt: script.updatedAt,
|
|
520
|
+
}));
|
|
521
|
+
json(res, { scripts });
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Must be matched before getScriptByIdRoute — its ["api", "scripts", null]
|
|
526
|
+
// pattern would otherwise swallow the literal "type-defs" segment.
|
|
527
|
+
if (typeDefsRoute.match(req.method, pathSegments)) {
|
|
528
|
+
json(res, { sdkTypes: SCRIPT_SDK_TYPES, stdlibTypes: SCRIPT_STDLIB_TYPES });
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (getScriptByIdRoute.match(req.method, pathSegments)) {
|
|
533
|
+
const parsed = await getScriptByIdRoute.parse(req, res, pathSegments, queryParams);
|
|
534
|
+
if (!parsed) return true;
|
|
535
|
+
const script = getScriptById(parsed.params.id);
|
|
536
|
+
if (!script) {
|
|
537
|
+
jsonError(res, "Script not found", 404);
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
// `source` is author-supplied TS (same trust surface as script_runs.source,
|
|
541
|
+
// already served raw by GET /api/script-runs/{id}) — no env/secret material.
|
|
542
|
+
const detail: ScriptDetail = {
|
|
543
|
+
...script,
|
|
544
|
+
signature: JSON.parse(script.signatureJson) as unknown,
|
|
545
|
+
argsJsonSchema: script.argsJsonSchema ? (JSON.parse(script.argsJsonSchema) as unknown) : null,
|
|
546
|
+
};
|
|
547
|
+
json(res, { script: detail });
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (listVersionsRoute.match(req.method, pathSegments)) {
|
|
552
|
+
const parsed = await listVersionsRoute.parse(req, res, pathSegments, queryParams);
|
|
553
|
+
if (!parsed) return true;
|
|
554
|
+
if (!getScriptById(parsed.params.id)) {
|
|
555
|
+
jsonError(res, "Script not found", 404);
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
json(res, { versions: listScriptVersions(parsed.params.id) });
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
|
|
416
562
|
if (typesRoute.match(req.method, pathSegments)) {
|
|
417
563
|
const parsed = await typesRoute.parse(req, res, pathSegments, queryParams);
|
|
418
564
|
if (!parsed) return true;
|
package/src/http/tasks.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
updateTaskProgress,
|
|
24
24
|
updateTaskVcs,
|
|
25
25
|
} from "../be/db";
|
|
26
|
+
import { ModelTierSchema, splitLegacyModelAlias } from "../model-tiers";
|
|
26
27
|
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
27
28
|
import { createResumeFollowUp, createWorkerTaskFollowUp } from "../tasks/worker-follow-up";
|
|
28
29
|
import { telemetry } from "../telemetry";
|
|
@@ -91,6 +92,8 @@ const createTask = route({
|
|
|
91
92
|
outputSchema: z.record(z.string(), z.unknown()).optional(),
|
|
92
93
|
contextKey: z.string().optional(),
|
|
93
94
|
requestedByUserId: z.string().optional(),
|
|
95
|
+
model: z.string().optional(),
|
|
96
|
+
modelTier: ModelTierSchema.optional(),
|
|
94
97
|
}),
|
|
95
98
|
responses: {
|
|
96
99
|
201: { description: "Task created" },
|
|
@@ -395,6 +398,10 @@ export async function handleTasks(
|
|
|
395
398
|
outputSchema: parsed.body.outputSchema || undefined,
|
|
396
399
|
contextKey: parsed.body.contextKey || undefined,
|
|
397
400
|
requestedByUserId,
|
|
401
|
+
...splitLegacyModelAlias({
|
|
402
|
+
model: parsed.body.model,
|
|
403
|
+
modelTier: parsed.body.modelTier,
|
|
404
|
+
}),
|
|
398
405
|
});
|
|
399
406
|
|
|
400
407
|
ensure({
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ProviderName } from "./types";
|
|
3
|
+
|
|
4
|
+
export const ModelTierSchema = z.enum(["smol", "regular", "smart", "ultra"]);
|
|
5
|
+
export type ModelTier = z.infer<typeof ModelTierSchema>;
|
|
6
|
+
|
|
7
|
+
export const MODEL_TIERS = ModelTierSchema.options;
|
|
8
|
+
|
|
9
|
+
export const LEGACY_MODEL_TO_TIER: Record<string, ModelTier> = {
|
|
10
|
+
haiku: "smol",
|
|
11
|
+
sonnet: "regular",
|
|
12
|
+
opus: "smart",
|
|
13
|
+
fable: "ultra",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const MODEL_TIER_LABELS: Record<ModelTier, string> = {
|
|
17
|
+
smol: "Smol",
|
|
18
|
+
regular: "Regular",
|
|
19
|
+
smart: "Smart",
|
|
20
|
+
ultra: "Ultra",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_MODEL_TIER_MAP: Record<ProviderName, Record<ModelTier, string>> = {
|
|
24
|
+
claude: {
|
|
25
|
+
smol: "haiku",
|
|
26
|
+
regular: "sonnet",
|
|
27
|
+
smart: "opus",
|
|
28
|
+
ultra: "fable",
|
|
29
|
+
},
|
|
30
|
+
"claude-managed": {
|
|
31
|
+
smol: "claude-haiku-4-5",
|
|
32
|
+
regular: "claude-sonnet-4-6",
|
|
33
|
+
smart: "claude-opus-4-8",
|
|
34
|
+
ultra: "claude-fable-5",
|
|
35
|
+
},
|
|
36
|
+
codex: {
|
|
37
|
+
smol: "gpt-5.4-mini",
|
|
38
|
+
regular: "gpt-5.4",
|
|
39
|
+
smart: "gpt-5.5",
|
|
40
|
+
ultra: "gpt-5.5",
|
|
41
|
+
},
|
|
42
|
+
pi: {
|
|
43
|
+
smol: "openrouter/deepseek/deepseek-v4-flash",
|
|
44
|
+
regular: "openrouter/deepseek/deepseek-v4-flash",
|
|
45
|
+
smart: "openrouter/deepseek/deepseek-v4-pro",
|
|
46
|
+
ultra: "openrouter/anthropic/claude-opus-4.8",
|
|
47
|
+
},
|
|
48
|
+
opencode: {
|
|
49
|
+
smol: "openrouter/deepseek/deepseek-v4-flash",
|
|
50
|
+
regular: "openrouter/deepseek/deepseek-v4-flash",
|
|
51
|
+
smart: "openrouter/deepseek/deepseek-v4-pro",
|
|
52
|
+
ultra: "openrouter/anthropic/claude-opus-4.8",
|
|
53
|
+
},
|
|
54
|
+
devin: {
|
|
55
|
+
smol: "devin",
|
|
56
|
+
regular: "devin",
|
|
57
|
+
smart: "devin",
|
|
58
|
+
ultra: "devin",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export function parseModelTier(value: string | null | undefined): ModelTier | undefined {
|
|
63
|
+
if (!value) return undefined;
|
|
64
|
+
const normalized = value.trim().toLowerCase();
|
|
65
|
+
return ModelTierSchema.safeParse(normalized).success
|
|
66
|
+
? (normalized as ModelTier)
|
|
67
|
+
: LEGACY_MODEL_TO_TIER[normalized];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function splitLegacyModelAlias(input: {
|
|
71
|
+
model?: string | null;
|
|
72
|
+
modelTier?: string | null;
|
|
73
|
+
}): { model?: string; modelTier?: ModelTier } {
|
|
74
|
+
const explicitTier = parseModelTier(input.modelTier);
|
|
75
|
+
const model = input.model?.trim();
|
|
76
|
+
if (!model) return { modelTier: explicitTier };
|
|
77
|
+
|
|
78
|
+
const legacyTier = parseModelTier(model);
|
|
79
|
+
if (legacyTier && !explicitTier) {
|
|
80
|
+
return { modelTier: legacyTier };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
model,
|
|
85
|
+
modelTier: explicitTier,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseTierMapJson(value: string | undefined): Partial<Record<ModelTier, string>> {
|
|
90
|
+
if (!value) return {};
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(value) as unknown;
|
|
93
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
94
|
+
const result: Partial<Record<ModelTier, string>> = {};
|
|
95
|
+
for (const tier of MODEL_TIERS) {
|
|
96
|
+
const model = (parsed as Record<string, unknown>)[tier];
|
|
97
|
+
if (typeof model === "string" && model.trim()) result[tier] = model.trim();
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
} catch {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function resolveModelTier(opts: {
|
|
106
|
+
tier?: string | null;
|
|
107
|
+
harnessProvider: ProviderName;
|
|
108
|
+
env?: Record<string, string | undefined>;
|
|
109
|
+
}): string | undefined {
|
|
110
|
+
const tier = parseModelTier(opts.tier);
|
|
111
|
+
if (!tier) return undefined;
|
|
112
|
+
|
|
113
|
+
const env = opts.env ?? {};
|
|
114
|
+
const jsonOverrides = parseTierMapJson(env.MODEL_TIER_MAP);
|
|
115
|
+
const envKey = `MODEL_TIER_${tier.toUpperCase()}`;
|
|
116
|
+
const directOverride = env[envKey]?.trim();
|
|
117
|
+
if (directOverride) return directOverride;
|
|
118
|
+
if (jsonOverrides[tier]) return jsonOverrides[tier];
|
|
119
|
+
|
|
120
|
+
return DEFAULT_MODEL_TIER_MAP[opts.harnessProvider]?.[tier];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function resolveTaskModelSelection(opts: {
|
|
124
|
+
model?: string | null;
|
|
125
|
+
modelTier?: string | null;
|
|
126
|
+
harnessProvider: ProviderName;
|
|
127
|
+
env?: Record<string, string | undefined>;
|
|
128
|
+
}): { model?: string; source: "model" | "modelTier" | "none" } {
|
|
129
|
+
const model = opts.model?.trim();
|
|
130
|
+
if (model) return { model, source: "model" };
|
|
131
|
+
|
|
132
|
+
const tierModel = resolveModelTier({
|
|
133
|
+
tier: opts.modelTier,
|
|
134
|
+
harnessProvider: opts.harnessProvider,
|
|
135
|
+
env: opts.env,
|
|
136
|
+
});
|
|
137
|
+
if (tierModel) return { model: tierModel, source: "modelTier" };
|
|
138
|
+
|
|
139
|
+
return { source: "none" };
|
|
140
|
+
}
|
package/src/oauth/mcp-wrapper.ts
CHANGED
|
@@ -287,7 +287,21 @@ export async function buildAuthorizeUrl(input: BuildAuthorizeInput): Promise<Bui
|
|
|
287
287
|
url.searchParams.set("resource", input.resource);
|
|
288
288
|
|
|
289
289
|
if (input.extraParams) {
|
|
290
|
+
const RESERVED = new Set([
|
|
291
|
+
"response_type",
|
|
292
|
+
"client_id",
|
|
293
|
+
"redirect_uri",
|
|
294
|
+
"scope",
|
|
295
|
+
"state",
|
|
296
|
+
"code_challenge",
|
|
297
|
+
"code_challenge_method",
|
|
298
|
+
"resource",
|
|
299
|
+
]);
|
|
290
300
|
for (const [k, v] of Object.entries(input.extraParams)) {
|
|
301
|
+
if (RESERVED.has(k.toLowerCase())) {
|
|
302
|
+
console.warn(`[mcp-oauth] extraParams key "${k}" is reserved and skipped`);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
291
305
|
url.searchParams.set(k, v);
|
|
292
306
|
}
|
|
293
307
|
}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
/** Models supported by the managed-agents surface for the swarm worker. */
|
|
27
27
|
export const CLAUDE_MANAGED_MODELS = [
|
|
28
28
|
"claude-fable-5",
|
|
29
|
+
"claude-mythos-5",
|
|
29
30
|
"claude-sonnet-4-6",
|
|
30
31
|
"claude-opus-4-8",
|
|
31
32
|
"claude-opus-4-7",
|
|
@@ -51,6 +52,8 @@ export interface ClaudeManagedModelPricing {
|
|
|
51
52
|
* Anthropic public list pricing. Source:
|
|
52
53
|
* https://platform.claude.com/docs/en/about-claude/pricing
|
|
53
54
|
*
|
|
55
|
+
* - claude-fable-5: $10 / $50 / $1.00 / $12.50 (verified 2026-06-10)
|
|
56
|
+
* - claude-mythos-5: $10 / $50 / $1.00 / $12.50 (limited availability, verified 2026-06-10)
|
|
54
57
|
* - claude-sonnet-4-6: $3 / $15 / $0.30 / $3.75 (in / out / cache-read / cache-write)
|
|
55
58
|
* - claude-opus-4-8: $5 / $25 / $0.50 / $6.25 (verified 2026-05-28)
|
|
56
59
|
* - claude-opus-4-7: $15 / $75 / $1.50 / $18.75 (STALE — was correct at launch, Anthropic has since dropped Opus to $5/$25)
|
|
@@ -64,6 +67,12 @@ export const CLAUDE_MANAGED_MODEL_PRICING: Record<ClaudeManagedModel, ClaudeMana
|
|
|
64
67
|
cacheReadPerMillion: 1.0,
|
|
65
68
|
cacheWritePerMillion: 12.5,
|
|
66
69
|
},
|
|
70
|
+
"claude-mythos-5": {
|
|
71
|
+
inputPerMillion: 10.0,
|
|
72
|
+
outputPerMillion: 50.0,
|
|
73
|
+
cacheReadPerMillion: 1.0,
|
|
74
|
+
cacheWritePerMillion: 12.5,
|
|
75
|
+
},
|
|
67
76
|
"claude-sonnet-4-6": {
|
|
68
77
|
inputPerMillion: 3.0,
|
|
69
78
|
outputPerMillion: 15.0,
|
|
@@ -63,6 +63,21 @@ export async function resolveCodexPrompt(
|
|
|
63
63
|
prompt: string,
|
|
64
64
|
skillsDir?: string,
|
|
65
65
|
emit?: (event: ProviderEvent) => void,
|
|
66
|
+
): Promise<string> {
|
|
67
|
+
return resolveSlashSkillPrompt(prompt, {
|
|
68
|
+
providerLabel: "codex",
|
|
69
|
+
skillsDir: skillsDir ?? defaultSkillsDir(),
|
|
70
|
+
emit,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function resolveSlashSkillPrompt(
|
|
75
|
+
prompt: string,
|
|
76
|
+
opts: {
|
|
77
|
+
providerLabel: string;
|
|
78
|
+
skillsDir: string;
|
|
79
|
+
emit?: (event: ProviderEvent) => void;
|
|
80
|
+
},
|
|
66
81
|
): Promise<string> {
|
|
67
82
|
if (!prompt) {
|
|
68
83
|
return prompt;
|
|
@@ -81,15 +96,14 @@ export async function resolveCodexPrompt(
|
|
|
81
96
|
|
|
82
97
|
const commandName: string = match[1];
|
|
83
98
|
const trailingArgs: string = match[2] ?? "";
|
|
84
|
-
const
|
|
85
|
-
const skillPath = join(dir, commandName, "SKILL.md");
|
|
99
|
+
const skillPath = join(opts.skillsDir, commandName, "SKILL.md");
|
|
86
100
|
|
|
87
101
|
const file = Bun.file(skillPath);
|
|
88
102
|
const exists = await file.exists();
|
|
89
103
|
if (!exists) {
|
|
90
|
-
emit?.({
|
|
104
|
+
opts.emit?.({
|
|
91
105
|
type: "raw_stderr",
|
|
92
|
-
content: `[
|
|
106
|
+
content: `[${opts.providerLabel}] skill resolver: SKILL.md not found for /${commandName} (looked in ${skillPath})\n`,
|
|
93
107
|
});
|
|
94
108
|
return prompt;
|
|
95
109
|
}
|
|
@@ -99,17 +113,17 @@ export async function resolveCodexPrompt(
|
|
|
99
113
|
skillContent = await file.text();
|
|
100
114
|
} catch (err) {
|
|
101
115
|
const message = err instanceof Error ? err.message : String(err);
|
|
102
|
-
emit?.({
|
|
116
|
+
opts.emit?.({
|
|
103
117
|
type: "raw_stderr",
|
|
104
|
-
content: `[
|
|
118
|
+
content: `[${opts.providerLabel}] skill resolver: failed to read SKILL.md for /${commandName}: ${message}\n`,
|
|
105
119
|
});
|
|
106
120
|
return prompt;
|
|
107
121
|
}
|
|
108
122
|
|
|
109
123
|
if (skillContent.length > MAX_SKILL_CHARS) {
|
|
110
|
-
emit?.({
|
|
124
|
+
opts.emit?.({
|
|
111
125
|
type: "raw_stderr",
|
|
112
|
-
content: `[
|
|
126
|
+
content: `[${opts.providerLabel}] skill resolver: SKILL.md for /${commandName} exceeds ${MAX_SKILL_CHARS} chars (${skillContent.length}), truncating\n`,
|
|
113
127
|
});
|
|
114
128
|
skillContent = skillContent.slice(0, MAX_SKILL_CHARS);
|
|
115
129
|
}
|