@desplega.ai/agent-swarm 1.92.2 → 1.94.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 (122) hide show
  1. package/README.md +2 -2
  2. package/openapi.json +242 -3
  3. package/package.json +5 -5
  4. package/src/be/db.ts +152 -11
  5. package/src/be/memory/boot-reembed.ts +0 -1
  6. package/src/be/memory/providers/sqlite-store.ts +42 -25
  7. package/src/be/memory/raters/llm-client.ts +12 -5
  8. package/src/be/memory/types.ts +3 -0
  9. package/src/be/migrations/088_script_runs_list_indexes.sql +10 -0
  10. package/src/be/migrations/089_harness_variant.sql +2 -0
  11. package/src/be/migrations/090_model_tiers.sql +2 -0
  12. package/src/be/migrations/091_seed_swarm_operations_metrics.sql +12 -0
  13. package/src/be/migrations/092_metrics_dashboard_combobox_filters.sql +68 -0
  14. package/src/be/migrations/093_slack_message_tracking.sql +6 -0
  15. package/src/be/migrations/runner.ts +52 -0
  16. package/src/be/modelsdev-cache.json +3264 -1166
  17. package/src/be/scripts/boot-reembed.ts +74 -0
  18. package/src/be/scripts/db.ts +19 -3
  19. package/src/be/seed/index.ts +1 -1
  20. package/src/be/seed/registry.ts +2 -2
  21. package/src/be/seed/runner.ts +5 -5
  22. package/src/be/seed/types.ts +6 -1
  23. package/src/be/seed-pricing.ts +2 -0
  24. package/src/be/seed-scripts/catalog/boot-triage.inline.ts +221 -0
  25. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +457 -0
  26. package/src/be/seed-scripts/catalog/compound-insights.inline.ts +863 -0
  27. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +506 -0
  28. package/src/be/seed-scripts/index.ts +8 -7
  29. package/src/be/skill-sync.ts +28 -179
  30. package/src/commands/runner.ts +197 -10
  31. package/src/http/api-keys.ts +42 -0
  32. package/src/http/index.ts +13 -2
  33. package/src/http/mcp-bridge.ts +1 -1
  34. package/src/http/memory.ts +23 -24
  35. package/src/http/metrics.ts +55 -6
  36. package/src/http/schedules.ts +16 -15
  37. package/src/http/script-runs.ts +7 -1
  38. package/src/http/scripts.ts +147 -1
  39. package/src/http/tasks.ts +17 -6
  40. package/src/model-tiers.ts +140 -0
  41. package/src/providers/claude-adapter.ts +33 -1
  42. package/src/providers/claude-managed-adapter.ts +3 -0
  43. package/src/providers/claude-managed-models.ts +16 -0
  44. package/src/providers/codex-adapter.ts +8 -1
  45. package/src/providers/codex-models.ts +1 -0
  46. package/src/providers/codex-oauth/auth-json.ts +1 -0
  47. package/src/providers/harness-version.ts +7 -0
  48. package/src/providers/opencode-adapter.ts +12 -4
  49. package/src/providers/pi-mono-adapter.ts +90 -8
  50. package/src/providers/types.ts +2 -0
  51. package/src/scheduler/scheduler.ts +22 -34
  52. package/src/scripts-runtime/egress-secrets.ts +83 -0
  53. package/src/scripts-runtime/eval-harness.ts +4 -0
  54. package/src/scripts-runtime/executors/types.ts +7 -0
  55. package/src/scripts-runtime/loader.ts +2 -0
  56. package/src/server-user.ts +8 -2
  57. package/src/slack/channel-join.ts +41 -0
  58. package/src/slack/responses.ts +39 -11
  59. package/src/slack/watcher.ts +121 -8
  60. package/src/tests/additive-buffer.test.ts +0 -1
  61. package/src/tests/agents-list-model-display.test.ts +13 -0
  62. package/src/tests/api-key-tracking.test.ts +113 -0
  63. package/src/tests/approval-requests.test.ts +0 -6
  64. package/src/tests/aws-error-classifier.test.ts +148 -0
  65. package/src/tests/claude-managed-adapter.test.ts +12 -0
  66. package/src/tests/claude-managed-setup.test.ts +0 -4
  67. package/src/tests/codex-pool.test.ts +2 -6
  68. package/src/tests/context-window.test.ts +7 -0
  69. package/src/tests/http-api-integration.test.ts +23 -6
  70. package/src/tests/memory-edges.test.ts +0 -2
  71. package/src/tests/memory-rate-endpoint.test.ts +0 -2
  72. package/src/tests/memory-rater-e2e.test.ts +0 -2
  73. package/src/tests/memory-store.test.ts +19 -1
  74. package/src/tests/memory.test.ts +51 -0
  75. package/src/tests/metrics-http.test.ts +137 -3
  76. package/src/tests/migration-046-budgets.test.ts +33 -0
  77. package/src/tests/migration-runner-regressions.test.ts +69 -0
  78. package/src/tests/model-control.test.ts +162 -46
  79. package/src/tests/opencode-adapter.test.ts +9 -0
  80. package/src/tests/pi-mono-adapter.test.ts +319 -0
  81. package/src/tests/providers/pi-cost.test.ts +9 -0
  82. package/src/tests/reload-config.test.ts +33 -17
  83. package/src/tests/runner-fallback-output.test.ts +50 -0
  84. package/src/tests/runner-skills-refresh.test.ts +216 -46
  85. package/src/tests/script-runs-http.test.ts +7 -1
  86. package/src/tests/scripts-boot-reembed.test.ts +163 -0
  87. package/src/tests/scripts-embeddings.test.ts +90 -0
  88. package/src/tests/scripts-runtime-secret-egress.test.ts +129 -0
  89. package/src/tests/seed-scripts.test.ts +13 -1
  90. package/src/tests/seed.test.ts +26 -1
  91. package/src/tests/session-attach.test.ts +6 -6
  92. package/src/tests/session-costs-model-key-normalize.test.ts +2 -0
  93. package/src/tests/skill-fs-writer.test.ts +250 -0
  94. package/src/tests/slack-attachments-block.test.ts +0 -1
  95. package/src/tests/slack-blocks.test.ts +0 -1
  96. package/src/tests/slack-channel-join.test.ts +80 -0
  97. package/src/tests/slack-identity-resolution.test.ts +0 -1
  98. package/src/tests/slack-watcher.test.ts +66 -0
  99. package/src/tests/structured-output.test.ts +0 -2
  100. package/src/tests/use-dismissible-card.test.ts +0 -4
  101. package/src/tests/workflow-agent-task.test.ts +5 -2
  102. package/src/tests/workflow-validation-port-routing.test.ts +181 -0
  103. package/src/tools/memory-get.ts +11 -0
  104. package/src/tools/memory-search.ts +18 -0
  105. package/src/tools/schedules/create-schedule.ts +71 -70
  106. package/src/tools/schedules/update-schedule.ts +43 -31
  107. package/src/tools/send-task.ts +16 -5
  108. package/src/tools/slack-post.ts +18 -15
  109. package/src/tools/slack-read.ts +9 -11
  110. package/src/tools/slack-reply.ts +18 -15
  111. package/src/tools/slack-start-thread.ts +17 -14
  112. package/src/tools/task-action.ts +11 -3
  113. package/src/types.ts +40 -0
  114. package/src/utils/aws-error-classifier.ts +97 -0
  115. package/src/utils/context-window.ts +5 -0
  116. package/src/utils/credentials.test.ts +68 -0
  117. package/src/utils/credentials.ts +66 -5
  118. package/src/utils/pretty-print.ts +25 -10
  119. package/src/utils/skill-fs-writer.ts +220 -0
  120. package/src/utils/skills-refresh.ts +123 -40
  121. package/src/workflows/engine.ts +3 -2
  122. package/src/workflows/executors/agent-task.ts +3 -1
package/src/http/index.ts CHANGED
@@ -458,10 +458,12 @@ try {
458
458
  // Seed the built-in entity catalog (scripts today; more kinds later) so
459
459
  // `script-search` & co. return useful hits from a fresh DB. Idempotent and
460
460
  // version-aware: a pristine entity updates when its source changes, a
461
- // user-modified one is preserved. See src/be/seed for the framework.
461
+ // user-modified one is preserved. Script embeddings are deferred to a
462
+ // post-listen backfill so boot doesn't block on embedding provider calls.
463
+ // See src/be/seed for the framework.
462
464
  try {
463
465
  const { runAllSeeders } = await import("../be/seed");
464
- await runAllSeeders();
466
+ await runAllSeeders({ scriptEmbeddingMode: "skip" });
465
467
  } catch (err) {
466
468
  console.error("[startup] Failed to seed built-in entities:", err);
467
469
  }
@@ -565,6 +567,15 @@ httpServer
565
567
  .catch((err) => {
566
568
  console.error("[boot-reembed] startup backfill failed (non-fatal):", err);
567
569
  });
570
+
571
+ // Background backfill: embed any scripts that were seeded without embeddings
572
+ // (scriptEmbeddingMode: "skip" during boot). Non-blocking, idempotent, no-op
573
+ // when every non-scratch script already has an embedding.
574
+ import("../be/scripts/boot-reembed")
575
+ .then(({ runBootReembedScripts }) => runBootReembedScripts())
576
+ .catch((err) => {
577
+ console.error("[boot-reembed-scripts] startup backfill failed (non-fatal):", err);
578
+ });
568
579
  })
569
580
  .on("error", (err) => {
570
581
  console.error("HTTP Server Error:", err);
@@ -16,7 +16,7 @@ function getBridgeServer(): McpServer {
16
16
  }
17
17
 
18
18
  type RegisteredTool = {
19
- handler: Function;
19
+ handler: (argsOrExtra: unknown, extra?: unknown) => unknown | Promise<unknown>;
20
20
  inputSchema?: unknown;
21
21
  enabled?: boolean;
22
22
  };
@@ -406,6 +406,7 @@ export async function handleMemory(
406
406
 
407
407
  const { query, agentId, scope, source, sourcePath, limit, offset } = parsed.body;
408
408
  const store = getMemoryStore();
409
+ const pageLimit = Math.min(limit, 100);
409
410
  const pathNeedle = sourcePath?.trim().toLowerCase();
410
411
  const matchesPath = (p: string | null) =>
411
412
  !pathNeedle || (p?.toLowerCase().includes(pathNeedle) ?? false);
@@ -416,11 +417,14 @@ export async function handleMemory(
416
417
  const queryEmbedding = await provider.embed(query.trim());
417
418
 
418
419
  if (!queryEmbedding) {
419
- json(res, { results: [], total: 0, mode: "semantic" });
420
+ json(res, { results: [], total: 0, limit: pageLimit, offset, mode: "semantic" });
420
421
  return true;
421
422
  }
422
423
 
423
- const candidateLimit = Math.min(limit, 100) * CANDIDATE_SET_MULTIPLIER;
424
+ const candidateLimit = Math.min(
425
+ 4096,
426
+ Math.max(offset + pageLimit, pageLimit) * CANDIDATE_SET_MULTIPLIER,
427
+ );
424
428
  let candidates = store.search(queryEmbedding, agentId ?? "", {
425
429
  scope,
426
430
  limit: candidateLimit,
@@ -433,10 +437,11 @@ export async function handleMemory(
433
437
  if (pathNeedle) {
434
438
  candidates = candidates.filter((c) => matchesPath(c.sourcePath));
435
439
  }
436
- const ranked = rerank(candidates, { limit: Math.min(limit, 100) });
440
+ const ranked = rerank(candidates, { limit: candidates.length });
441
+ const page = ranked.slice(offset, offset + pageLimit);
437
442
 
438
443
  json(res, {
439
- results: ranked.map((r) => ({
444
+ results: page.map((r) => ({
440
445
  id: r.id,
441
446
  name: r.name,
442
447
  content: r.content,
@@ -457,33 +462,25 @@ export async function handleMemory(
457
462
  totalChunks: r.totalChunks,
458
463
  tags: r.tags,
459
464
  })),
460
- total: ranked.length,
465
+ total: candidates.length,
466
+ limit: pageLimit,
467
+ offset,
461
468
  mode: "semantic",
462
469
  });
463
470
  return true;
464
471
  }
465
472
 
466
- // When filtering by sourcePath, over-fetch then post-filter so the visible
467
- // page isn't gutted by the in-memory filter.
468
- const fetchLimit = pathNeedle
469
- ? Math.min(500, Math.max(limit * 10, 100))
470
- : Math.min(limit, 100);
471
- let rows = store.list(agentId ?? "", {
473
+ const listOptions = {
472
474
  scope,
473
- limit: fetchLimit,
475
+ limit: pageLimit,
474
476
  offset,
475
477
  isLead: true,
476
- });
477
- if (agentId) {
478
- rows = rows.filter((r) => r.agentId === agentId);
479
- }
480
- if (source) {
481
- rows = rows.filter((r) => r.source === source);
482
- }
483
- if (pathNeedle) {
484
- rows = rows.filter((r) => matchesPath(r.sourcePath));
485
- }
486
- rows = rows.slice(0, Math.min(limit, 100));
478
+ ownerAgentId: agentId,
479
+ source,
480
+ sourcePath: pathNeedle,
481
+ };
482
+ const rows = store.list(agentId ?? "", listOptions);
483
+ const total = store.count(agentId ?? "", listOptions);
487
484
 
488
485
  json(res, {
489
486
  results: rows.map((r) => ({
@@ -504,7 +501,9 @@ export async function handleMemory(
504
501
  totalChunks: r.totalChunks,
505
502
  tags: r.tags,
506
503
  })),
507
- total: rows.length,
504
+ total,
505
+ limit: pageLimit,
506
+ offset,
508
507
  mode: "list",
509
508
  });
510
509
  } catch (err) {
@@ -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(metric: Metric, provided: Record<string, unknown>) {
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 value = coerceVariableValue(variable, provided[variable.key]);
78
- if (variable.options?.length) {
79
- const allowed = variable.options.some((option) => option.value === value);
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 variables = resolveMetricVariables(metric, providedVariables);
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.
@@ -12,9 +12,8 @@ import {
12
12
  updateScheduledTask,
13
13
  } from "../be/db";
14
14
  import { mergeScheduleTiming, validateRecurringTiming } from "../be/schedules/validate";
15
- import { calculateNextRun } from "../scheduler/scheduler";
16
- import { scheduleContextKey } from "../tasks/context-key";
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 = createTaskWithSiblingAwareness(schedule.taskTemplate, {
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) {
@@ -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({ status: opts.status, agentId: opts.agentId }),
377
+ total: countScriptRuns({
378
+ status: opts.status,
379
+ agentId: opts.agentId,
380
+ scriptName: opts.scriptName,
381
+ }),
376
382
  });
377
383
  return true;
378
384
  }
@@ -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 { deleteScript, getScript, upsertScriptByName } from "../be/scripts/db";
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" },
@@ -98,11 +101,11 @@ const createTask = route({
98
101
  },
99
102
  });
100
103
 
101
- const updateClaudeSession = route({
104
+ const updateSession = route({
102
105
  method: "put",
103
- path: "/api/tasks/{id}/claude-session",
104
- pattern: ["api", "tasks", null, "claude-session"],
105
- summary: "Update Claude session ID for a task",
106
+ path: "/api/tasks/{id}/session",
107
+ pattern: ["api", "tasks", null, "session"],
108
+ summary: "Update provider session ID and harness metadata for a task",
106
109
  tags: ["Tasks"],
107
110
  params: z.object({ id: z.string() }),
108
111
  body: z.union([
@@ -121,6 +124,8 @@ const updateClaudeSession = route({
121
124
  provider: ProviderNameSchema.exclude(["devin"]).optional(),
122
125
  model: z.string().optional(),
123
126
  providerMeta: z.object({}).optional(),
127
+ harnessVariant: z.string().optional(),
128
+ harnessVariantMeta: z.record(z.string(), z.unknown()).optional(),
124
129
  }),
125
130
  ]),
126
131
  responses: {
@@ -393,6 +398,10 @@ export async function handleTasks(
393
398
  outputSchema: parsed.body.outputSchema || undefined,
394
399
  contextKey: parsed.body.contextKey || undefined,
395
400
  requestedByUserId,
401
+ ...splitLegacyModelAlias({
402
+ model: parsed.body.model,
403
+ modelTier: parsed.body.modelTier,
404
+ }),
396
405
  });
397
406
 
398
407
  ensure({
@@ -427,8 +436,8 @@ export async function handleTasks(
427
436
  return true;
428
437
  }
429
438
 
430
- if (updateClaudeSession.match(req.method, pathSegments)) {
431
- const parsed = await updateClaudeSession.parse(req, res, pathSegments, queryParams);
439
+ if (updateSession.match(req.method, pathSegments)) {
440
+ const parsed = await updateSession.parse(req, res, pathSegments, queryParams);
432
441
  if (!parsed) return true;
433
442
  const task = updateTaskClaudeSessionId(
434
443
  parsed.params.id,
@@ -436,6 +445,8 @@ export async function handleTasks(
436
445
  parsed.body.provider,
437
446
  parsed.body.providerMeta,
438
447
  parsed.body.model,
448
+ "harnessVariant" in parsed.body ? parsed.body.harnessVariant : undefined,
449
+ "harnessVariantMeta" in parsed.body ? parsed.body.harnessVariantMeta : undefined,
439
450
  );
440
451
  if (!task) {
441
452
  jsonError(res, "Task not found", 404);