@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
@@ -10,6 +10,39 @@ import {
10
10
  import { mergeScheduleTiming, validateRecurringTiming } from "@/be/schedules/validate";
11
11
  import { calculateNextRun } from "@/scheduler";
12
12
  import { createToolRegistrar } from "@/tools/utils";
13
+ import { ModelTierSchema, splitLegacyModelAlias } from "../../model-tiers";
14
+
15
+ export const updateScheduleInputSchema = z.object({
16
+ scheduleId: z.string().uuid().optional().describe("Schedule ID to update"),
17
+ name: z.string().optional().describe("Schedule name to update (alternative to ID)"),
18
+ newName: z.string().min(1).max(100).optional().describe("New name for the schedule"),
19
+ taskTemplate: z.string().min(1).optional().describe("New task template"),
20
+ cronExpression: z.string().nullable().optional().describe("New cron expression (null to clear)"),
21
+ intervalMs: z
22
+ .number()
23
+ .int()
24
+ .positive()
25
+ .nullable()
26
+ .optional()
27
+ .describe("New interval in milliseconds (null to clear)"),
28
+ description: z.string().optional().describe("New description"),
29
+ taskType: z.string().max(50).optional().describe("New task type"),
30
+ tags: z.array(z.string()).optional().describe("New tags"),
31
+ priority: z.number().int().min(0).max(100).optional().describe("New priority"),
32
+ targetAgentId: z.string().uuid().nullable().optional().describe("New target agent ID"),
33
+ timezone: z.string().optional().describe("New timezone"),
34
+ enabled: z.boolean().optional().describe("Enable or disable the schedule"),
35
+ model: z
36
+ .string()
37
+ .trim()
38
+ .min(1)
39
+ .nullable()
40
+ .optional()
41
+ .describe("Concrete model override for tasks created by this schedule. Set to null to clear."),
42
+ modelTier: ModelTierSchema.nullable()
43
+ .optional()
44
+ .describe("Portable model tier for tasks created by this schedule. Set to null to clear."),
45
+ });
13
46
 
14
47
  export const registerUpdateScheduleTool = (server: McpServer) => {
15
48
  createToolRegistrar(server)(
@@ -19,36 +52,7 @@ export const registerUpdateScheduleTool = (server: McpServer) => {
19
52
  annotations: { idempotentHint: true },
20
53
  description:
21
54
  "Update an existing scheduled task. Only the creator or lead agent can update schedules.",
22
- inputSchema: z.object({
23
- scheduleId: z.string().uuid().optional().describe("Schedule ID to update"),
24
- name: z.string().optional().describe("Schedule name to update (alternative to ID)"),
25
- newName: z.string().min(1).max(100).optional().describe("New name for the schedule"),
26
- taskTemplate: z.string().min(1).optional().describe("New task template"),
27
- cronExpression: z
28
- .string()
29
- .nullable()
30
- .optional()
31
- .describe("New cron expression (null to clear)"),
32
- intervalMs: z
33
- .number()
34
- .int()
35
- .positive()
36
- .nullable()
37
- .optional()
38
- .describe("New interval in milliseconds (null to clear)"),
39
- description: z.string().optional().describe("New description"),
40
- taskType: z.string().max(50).optional().describe("New task type"),
41
- tags: z.array(z.string()).optional().describe("New tags"),
42
- priority: z.number().int().min(0).max(100).optional().describe("New priority"),
43
- targetAgentId: z.string().uuid().nullable().optional().describe("New target agent ID"),
44
- timezone: z.string().optional().describe("New timezone"),
45
- enabled: z.boolean().optional().describe("Enable or disable the schedule"),
46
- model: z
47
- .enum(["haiku", "sonnet", "opus"])
48
- .nullable()
49
- .optional()
50
- .describe("Model to use for tasks created by this schedule. Set to null to clear."),
51
- }),
55
+ inputSchema: updateScheduleInputSchema,
52
56
  outputSchema: z.object({
53
57
  yourAgentId: z.string().uuid().optional(),
54
58
  success: z.boolean(),
@@ -71,6 +75,7 @@ export const registerUpdateScheduleTool = (server: McpServer) => {
71
75
  createdByAgentId: z.string().optional(),
72
76
  timezone: z.string(),
73
77
  model: z.string().optional(),
78
+ modelTier: ModelTierSchema.optional(),
74
79
  scheduleType: z.string(),
75
80
  createdAt: z.string(),
76
81
  lastUpdatedAt: z.string(),
@@ -94,6 +99,7 @@ export const registerUpdateScheduleTool = (server: McpServer) => {
94
99
  timezone,
95
100
  enabled,
96
101
  model,
102
+ modelTier,
97
103
  },
98
104
  requestInfo,
99
105
  _meta,
@@ -217,7 +223,13 @@ export const registerUpdateScheduleTool = (server: McpServer) => {
217
223
  if (targetAgentId !== undefined) updateData.targetAgentId = targetAgentId;
218
224
  if (timezone !== undefined) updateData.timezone = timezone;
219
225
  if (enabled !== undefined) updateData.enabled = enabled;
220
- if (model !== undefined) updateData.model = model;
226
+ if (model !== undefined || modelTier !== undefined) {
227
+ const normalizedModel = splitLegacyModelAlias({ model, modelTier });
228
+ if (model !== undefined) updateData.model = normalizedModel.model ?? null;
229
+ if (modelTier !== undefined || normalizedModel.modelTier) {
230
+ updateData.modelTier = normalizedModel.modelTier ?? null;
231
+ }
232
+ }
221
233
 
222
234
  // Recalculate nextRunAt based on schedule type
223
235
  if (schedule.scheduleType === "one_time") {
@@ -15,6 +15,7 @@ import { findDuplicateTask } from "@/tools/task-dedup";
15
15
  import { ownerCtx, type ToolCtx } from "@/tools/task-tool-ctx";
16
16
  import { createToolRegistrar } from "@/tools/utils";
17
17
  import { AgentTaskSchema, FollowUpConfigSchema } from "@/types";
18
+ import { ModelTierSchema, splitLegacyModelAlias } from "../model-tiers";
18
19
 
19
20
  export const sendTaskInputSchema = z.object({
20
21
  agentId: z
@@ -54,11 +55,16 @@ export const sendTaskInputSchema = z.object({
54
55
  "VCS repo identifier (e.g., 'desplega-ai/agent-swarm' for GitHub or 'group/project' for GitLab). Links the task to a registered repo for workspace context.",
55
56
  ),
56
57
  model: z
57
- .enum(["haiku", "sonnet", "opus"])
58
+ .string()
59
+ .trim()
60
+ .min(1)
58
61
  .optional()
59
62
  .describe(
60
- "Model to use for this task ('haiku', 'sonnet', or 'opus'). If not set, uses agent/global config MODEL_OVERRIDE or defaults to 'opus'.",
63
+ "Concrete model override for this task, interpreted by the assignee's harness/provider. This does not switch providers. Prefer modelTier for portable intent.",
61
64
  ),
65
+ modelTier: ModelTierSchema.optional().describe(
66
+ "Portable model tier for this task: 'smol', 'regular', 'smart', or 'ultra'. Resolved at claim/run time using the assignee's harness/provider. Legacy model shortnames map as haiku→smol, sonnet→regular, opus→smart, fable→ultra.",
67
+ ),
62
68
  allowDuplicate: z
63
69
  .boolean()
64
70
  .default(false)
@@ -111,6 +117,7 @@ export async function sendTaskHandler(
111
117
  parentTaskId,
112
118
  vcsRepo,
113
119
  model,
120
+ modelTier,
114
121
  allowDuplicate,
115
122
  slackChannelId,
116
123
  slackThreadTs,
@@ -160,6 +167,7 @@ export async function sendTaskHandler(
160
167
  }
161
168
 
162
169
  const effectiveVcsRepo = vcsRepo;
170
+ const normalizedModel = splitLegacyModelAlias({ model, modelTier });
163
171
 
164
172
  // Auto-default parentTaskId to caller's current task for tree tracking
165
173
  const effectiveParentTaskId = parentTaskId ?? sourceTaskId;
@@ -263,7 +271,8 @@ export async function sendTaskHandler(
263
271
  dir,
264
272
  parentTaskId: effectiveParentTaskId,
265
273
  vcsRepo: effectiveVcsRepo,
266
- model,
274
+ model: normalizedModel.model,
275
+ modelTier: normalizedModel.modelTier,
267
276
  slackChannelId,
268
277
  slackThreadTs,
269
278
  slackUserId,
@@ -316,7 +325,8 @@ export async function sendTaskHandler(
316
325
  dir,
317
326
  parentTaskId: effectiveParentTaskId,
318
327
  vcsRepo: effectiveVcsRepo,
319
- model,
328
+ model: normalizedModel.model,
329
+ modelTier: normalizedModel.modelTier,
320
330
  slackChannelId,
321
331
  slackThreadTs,
322
332
  slackUserId,
@@ -343,7 +353,8 @@ export async function sendTaskHandler(
343
353
  dir,
344
354
  parentTaskId: effectiveParentTaskId,
345
355
  vcsRepo: effectiveVcsRepo,
346
- model,
356
+ model: normalizedModel.model,
357
+ modelTier: normalizedModel.modelTier,
347
358
  slackChannelId,
348
359
  slackThreadTs,
349
360
  slackUserId,
@@ -2,6 +2,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as z from "zod";
3
3
  import { getAgentById } from "@/be/db";
4
4
  import { getSlackApp } from "@/slack/app";
5
+ import { withAutoJoin } from "@/slack/channel-join";
5
6
  import { markdownToSlack } from "@/slack/responses";
6
7
  import { createToolRegistrar } from "@/tools/utils";
7
8
 
@@ -68,22 +69,24 @@ export const registerSlackPostTool = (server: McpServer) => {
68
69
  try {
69
70
  const slackMessage = markdownToSlack(message);
70
71
 
71
- const result = await app.client.chat.postMessage({
72
- channel: channelId,
73
- text: slackMessage, // Fallback for notifications
74
- username: agent.name,
75
- icon_emoji: ":crown:",
76
- ...(threadTs ? { thread_ts: threadTs } : {}),
77
- blocks: [
78
- {
79
- type: "section",
80
- text: {
81
- type: "mrkdwn",
82
- text: slackMessage,
72
+ const result = await withAutoJoin(app.client, channelId, () =>
73
+ app.client.chat.postMessage({
74
+ channel: channelId,
75
+ text: slackMessage, // Fallback for notifications
76
+ username: agent.name,
77
+ icon_emoji: ":crown:",
78
+ ...(threadTs ? { thread_ts: threadTs } : {}),
79
+ blocks: [
80
+ {
81
+ type: "section",
82
+ text: {
83
+ type: "mrkdwn",
84
+ text: slackMessage,
85
+ },
83
86
  },
84
- },
85
- ],
86
- });
87
+ ],
88
+ }),
89
+ );
87
90
 
88
91
  const messageTs = result.ts;
89
92
 
@@ -2,6 +2,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as z from "zod";
3
3
  import { getAgentById, getInboxMessageById, getTaskById } from "@/be/db";
4
4
  import { getSlackApp } from "@/slack/app";
5
+ import { withAutoJoin } from "@/slack/channel-join";
5
6
  import { downloadFile } from "@/slack/files";
6
7
  import { extractSlackMessageText } from "@/slack/message-text";
7
8
  import { createToolRegistrar } from "@/tools/utils";
@@ -216,19 +217,16 @@ export const registerSlackReadTool = (server: McpServer) => {
216
217
  let rawMessages: RawMessage[] = [];
217
218
 
218
219
  if (slackThreadTs) {
219
- // Fetch thread replies
220
- const result = await client.conversations.replies({
221
- channel: slackChannelId,
222
- ts: slackThreadTs,
223
- limit,
224
- });
220
+ // Fetch thread replies — auto-join public channels on not_in_channel
221
+ const result = await withAutoJoin(client, slackChannelId, () =>
222
+ client.conversations.replies({ channel: slackChannelId, ts: slackThreadTs!, limit }),
223
+ );
225
224
  rawMessages = (result.messages || []) as RawMessage[];
226
225
  } else {
227
- // Fetch channel history
228
- const result = await client.conversations.history({
229
- channel: slackChannelId,
230
- limit,
231
- });
226
+ // Fetch channel history — auto-join public channels on not_in_channel
227
+ const result = await withAutoJoin(client, slackChannelId, () =>
228
+ client.conversations.history({ channel: slackChannelId, limit }),
229
+ );
232
230
  rawMessages = (result.messages || []) as RawMessage[];
233
231
  }
234
232
 
@@ -8,6 +8,7 @@ import {
8
8
  markTaskSlackReplySent,
9
9
  } from "@/be/db";
10
10
  import { getSlackApp } from "@/slack/app";
11
+ import { withAutoJoin } from "@/slack/channel-join";
11
12
  import { markdownToSlack } from "@/slack/responses";
12
13
  import { createToolRegistrar } from "@/tools/utils";
13
14
 
@@ -118,22 +119,24 @@ export const registerSlackReplyTool = (server: McpServer) => {
118
119
  try {
119
120
  const slackMessage = markdownToSlack(message);
120
121
 
121
- await app.client.chat.postMessage({
122
- channel: slackChannelId,
123
- thread_ts: slackThreadTs,
124
- text: slackMessage, // Fallback for notifications
125
- username: agent.name,
126
- icon_emoji: agent.isLead ? ":crown:" : ":robot_face:",
127
- blocks: [
128
- {
129
- type: "section",
130
- text: {
131
- type: "mrkdwn",
132
- text: slackMessage,
122
+ await withAutoJoin(app.client, slackChannelId, () =>
123
+ app.client.chat.postMessage({
124
+ channel: slackChannelId,
125
+ thread_ts: slackThreadTs,
126
+ text: slackMessage, // Fallback for notifications
127
+ username: agent.name,
128
+ icon_emoji: agent.isLead ? ":crown:" : ":robot_face:",
129
+ blocks: [
130
+ {
131
+ type: "section",
132
+ text: {
133
+ type: "mrkdwn",
134
+ text: slackMessage,
135
+ },
133
136
  },
134
- },
135
- ],
136
- });
137
+ ],
138
+ }),
139
+ );
137
140
 
138
141
  // After successful postMessage, mark task as having a Slack reply
139
142
  if (taskId) {
@@ -2,6 +2,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as z from "zod";
3
3
  import { getAgentById } from "@/be/db";
4
4
  import { getSlackApp } from "@/slack/app";
5
+ import { withAutoJoin } from "@/slack/channel-join";
5
6
  import { markdownToSlack } from "@/slack/responses";
6
7
  import { createToolRegistrar } from "@/tools/utils";
7
8
 
@@ -62,21 +63,23 @@ export const registerSlackStartThreadTool = (server: McpServer) => {
62
63
  try {
63
64
  const slackMessage = markdownToSlack(message);
64
65
 
65
- const result = await app.client.chat.postMessage({
66
- channel: channelId,
67
- text: slackMessage, // Fallback for notifications
68
- username: agent.name,
69
- icon_emoji: ":crown:",
70
- blocks: [
71
- {
72
- type: "section",
73
- text: {
74
- type: "mrkdwn",
75
- text: slackMessage,
66
+ const result = await withAutoJoin(app.client, channelId, () =>
67
+ app.client.chat.postMessage({
68
+ channel: channelId,
69
+ text: slackMessage, // Fallback for notifications
70
+ username: agent.name,
71
+ icon_emoji: ":crown:",
72
+ blocks: [
73
+ {
74
+ type: "section",
75
+ text: {
76
+ type: "mrkdwn",
77
+ text: slackMessage,
78
+ },
76
79
  },
77
- },
78
- ],
79
- });
80
+ ],
81
+ }),
82
+ );
80
83
 
81
84
  const ts = result.ts;
82
85
  const resolvedChannelId = result.channel ?? channelId;
@@ -28,6 +28,7 @@ import {
28
28
  import { assertOwnsTask, ownerCtx, type ToolCtx } from "@/tools/task-tool-ctx";
29
29
  import { createToolRegistrar } from "@/tools/utils";
30
30
  import { AgentTaskSchema, BudgetRefusalCauseSchema } from "@/types";
31
+ import { ModelTierSchema, splitLegacyModelAlias } from "../model-tiers";
31
32
 
32
33
  export const TaskActionSchema = z.enum([
33
34
  "create",
@@ -66,11 +67,16 @@ export const taskActionInputSchema = z.object({
66
67
  "Working directory (absolute path) for the agent to start in. Only used with 'create' action.",
67
68
  ),
68
69
  model: z
69
- .enum(["haiku", "sonnet", "opus"])
70
+ .string()
71
+ .trim()
72
+ .min(1)
70
73
  .optional()
71
74
  .describe(
72
- "Model to use for the created task ('haiku', 'sonnet', or 'opus'). Only used with 'create' action.",
75
+ "Concrete model override for the created task, interpreted by the claiming worker's harness/provider. This does not switch providers. Only used with 'create' action.",
73
76
  ),
77
+ modelTier: ModelTierSchema.optional().describe(
78
+ "Portable model tier for the created task: 'smol', 'regular', 'smart', or 'ultra'. Resolved when a worker claims/runs the task. Only used with 'create' action.",
79
+ ),
74
80
  });
75
81
 
76
82
  export const taskActionOutputSchema = z.object({
@@ -144,6 +150,7 @@ export async function taskActionHandler(
144
150
  input: TaskActionArgs,
145
151
  ): Promise<CallToolResult> {
146
152
  const { action, task, taskType, tags, priority, dependsOn, taskId, reason, dir, model } = input;
153
+ const normalizedModel = splitLegacyModelAlias({ model, modelTier: input.modelTier });
147
154
 
148
155
  if (ctx.kind === "user") {
149
156
  if (action !== "to_backlog" && action !== "from_backlog") {
@@ -229,7 +236,8 @@ export async function taskActionHandler(
229
236
  priority,
230
237
  dependsOn,
231
238
  dir,
232
- model,
239
+ model: normalizedModel.model,
240
+ modelTier: normalizedModel.modelTier,
233
241
  });
234
242
  return {
235
243
  success: true,
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as z from "zod";
2
+ import { ModelTierSchema } from "./model-tiers";
2
3
 
3
4
  // Task status - includes new unassigned and offered states
4
5
  export const AgentTaskStatusSchema = z.enum([
@@ -170,6 +171,8 @@ export const AgentTaskSchema = z.object({
170
171
  slackThreadTs: z.string().optional(),
171
172
  slackUserId: z.string().optional(),
172
173
  slackReplySent: z.boolean().default(false),
174
+ slackProgressMessageTs: z.string().optional(),
175
+ slackTreeRootMessageTs: z.string().optional(),
173
176
 
174
177
  // VCS metadata (GitHub / GitLab — provider-agnostic)
175
178
  vcsProvider: z.enum(["github", "gitlab"]).optional(),
@@ -200,7 +203,10 @@ export const AgentTaskSchema = z.object({
200
203
 
201
204
  // Model selection (optional — provider-specific; can be "opus", "gpt-4o",
202
205
  // "openrouter/openai/gpt-5-nano", etc. depending on HARNESS_PROVIDER).
206
+ // Prefer modelTier for portable task intent; model is a concrete override
207
+ // interpreted by the claiming worker's harness and never switches provider.
203
208
  model: z.string().optional(),
209
+ modelTier: ModelTierSchema.optional(),
204
210
 
205
211
  // Schedule linking (optional — set when task was created by a schedule)
206
212
  scheduleId: z.uuid().optional(),
@@ -247,6 +253,10 @@ export const AgentTaskSchema = z.object({
247
253
  provider: ProviderNameSchema.optional(),
248
254
  providerMeta: z.record(z.string(), z.unknown()).optional(),
249
255
 
256
+ // Harness variant — sub-variant within a provider (e.g. "bridge" vs "stock" for claude)
257
+ harnessVariant: z.string().optional(),
258
+ harnessVariantMeta: z.record(z.string(), z.unknown()).optional(),
259
+
250
260
  // Aggregated session cost for task list/read models. Undefined means no
251
261
  // session cost rows have been recorded for this task.
252
262
  totalCostUsd: z.number().min(0).optional(),
@@ -870,6 +880,7 @@ export const ScheduledTaskSchema = z
870
880
  lastErrorAt: z.iso.datetime().optional(),
871
881
  lastErrorMessage: z.string().optional(),
872
882
  model: z.string().optional(),
883
+ modelTier: ModelTierSchema.optional(),
873
884
  scheduleType: z.enum(["recurring", "one_time"]).default("recurring"),
874
885
  createdAt: z.iso.datetime(),
875
886
  lastUpdatedAt: z.iso.datetime(),
@@ -1371,6 +1382,7 @@ export type AgentTaskSummary = Pick<
1371
1382
  | "parentTaskId"
1372
1383
  | "scheduleId"
1373
1384
  | "model"
1385
+ | "modelTier"
1374
1386
  | "provider"
1375
1387
  | "requestedByUserId"
1376
1388
  | "progress"
@@ -1434,6 +1446,13 @@ export const MetricVariableSchema = z.object({
1434
1446
  }),
1435
1447
  )
1436
1448
  .optional(),
1449
+ optionsQuery: z
1450
+ .object({
1451
+ sql: z.string().min(1).max(10_000),
1452
+ valueKey: z.string().min(1),
1453
+ labelKey: z.string().min(1).optional(),
1454
+ })
1455
+ .optional(),
1437
1456
  });
1438
1457
  export type MetricVariable = z.infer<typeof MetricVariableSchema>;
1439
1458
 
@@ -1463,6 +1482,8 @@ export const MetricWidgetSchema = z.object({
1463
1482
  description: z.string().optional(),
1464
1483
  query: MetricQuerySchema,
1465
1484
  viz: MetricVizConfigSchema,
1485
+ colSpan: z.number().int().min(1).max(4).optional(),
1486
+ rowSpan: z.number().int().min(1).max(4).optional(),
1466
1487
  });
1467
1488
  export type MetricWidget = z.infer<typeof MetricWidgetSchema>;
1468
1489
 
@@ -1579,6 +1600,13 @@ export const ScriptRunSchema = z.object({
1579
1600
  });
1580
1601
  export type ScriptRun = z.infer<typeof ScriptRunSchema>;
1581
1602
 
1603
+ export const ScriptRunListItemSchema = ScriptRunSchema.omit({
1604
+ source: true,
1605
+ args: true,
1606
+ output: true,
1607
+ });
1608
+ export type ScriptRunListItem = z.infer<typeof ScriptRunListItemSchema>;
1609
+
1582
1610
  export const ScriptRunJournalEntrySchema = z.object({
1583
1611
  id: z.string().uuid(),
1584
1612
  runId: z.string().uuid(),
@@ -1746,6 +1774,18 @@ export const ScriptVersionRecordSchema = z.object({
1746
1774
  });
1747
1775
  export type ScriptVersionRecord = z.infer<typeof ScriptVersionRecordSchema>;
1748
1776
 
1777
+ /** Lean projection served by `GET /api/scripts` — omits `source` (payload size) and raw JSON blobs. */
1778
+ export type ScriptListItem = Omit<
1779
+ ScriptRecord,
1780
+ "source" | "signatureJson" | "argsJsonSchema" | "contentHash"
1781
+ >;
1782
+
1783
+ /** Full record served by `GET /api/scripts/{id}` — includes `source` plus parsed `signature`/`argsJsonSchema`. */
1784
+ export type ScriptDetail = Omit<ScriptRecord, "argsJsonSchema"> & {
1785
+ signature: unknown;
1786
+ argsJsonSchema: unknown;
1787
+ };
1788
+
1749
1789
  // ============================================================================
1750
1790
  // Skill Types
1751
1791
  // ============================================================================
@@ -0,0 +1,97 @@
1
+ /**
2
+ * AWS SDK error classifier for the pi-mono/Bedrock path.
3
+ *
4
+ * Provides a single shared matcher (`classifyAwsSdkError`) consumed by:
5
+ * - `src/providers/pi-mono-adapter.ts` — emits ProviderEvent {type:'error'} mid-stream
6
+ * - `src/commands/runner.ts` — backstop in the no-schema/no-progress branch
7
+ *
8
+ * The regex set lives here exactly once so the two sites never diverge.
9
+ */
10
+
11
+ export type AwsErrorCategory = "aws-auth" | "aws-throttle" | "aws-access" | "aws-model";
12
+
13
+ export interface AwsErrorClassification {
14
+ category: AwsErrorCategory;
15
+ /** Human-readable, actionable error message for the session-chat red box and task failureReason. */
16
+ message: string;
17
+ }
18
+
19
+ interface AwsErrorRule {
20
+ patterns: RegExp[];
21
+ category: AwsErrorCategory;
22
+ message: string;
23
+ }
24
+
25
+ /**
26
+ * Priority-ordered rules. The first matching rule wins, so the most critical
27
+ * category (auth) takes precedence over the more generic ones (model errors).
28
+ */
29
+ const AWS_ERROR_RULES: AwsErrorRule[] = [
30
+ {
31
+ // Expired / missing / invalid credentials
32
+ patterns: [
33
+ /ExpiredToken(?:Exception)?/,
34
+ /CredentialsProviderError/,
35
+ /Unable to locate credentials/,
36
+ /security token.*expired/i,
37
+ /expired.*security token/i,
38
+ /InvalidSignatureException/,
39
+ /UnrecognizedClientException/,
40
+ ],
41
+ category: "aws-auth",
42
+ message:
43
+ "AWS credentials have expired or are missing. " +
44
+ "Run `aws sso login` (or refresh your credentials via `aws configure`) and retry.",
45
+ },
46
+ {
47
+ // Rate limits and quota exceeded
48
+ patterns: [
49
+ /ThrottlingException/,
50
+ /TooManyRequestsException/,
51
+ /ServiceQuotaExceededException/,
52
+ /Rate exceeded/,
53
+ ],
54
+ category: "aws-throttle",
55
+ message:
56
+ "AWS Bedrock request was throttled (rate limit or quota exceeded). " +
57
+ "Wait and retry, or request a quota increase in the AWS Service Quotas console.",
58
+ },
59
+ {
60
+ // IAM / authorization denials
61
+ patterns: [/AccessDeniedException/, /not authorized to perform/i],
62
+ category: "aws-access",
63
+ message:
64
+ "AWS authorization denied for Bedrock. " +
65
+ "Verify the IAM role/user has the `bedrock:InvokeModel` permission for the target model ARN and region.",
66
+ },
67
+ {
68
+ // Bad model ID, region mismatch, model not ready
69
+ patterns: [
70
+ /ValidationException/,
71
+ /ResourceNotFoundException/,
72
+ /ModelTimeoutException/,
73
+ /ModelNotReadyException/,
74
+ ],
75
+ category: "aws-model",
76
+ message:
77
+ "AWS Bedrock model error: the model ID may be invalid, unavailable in this region, or not yet ready. " +
78
+ "Verify MODEL_OVERRIDE and the AWS region in your environment.",
79
+ },
80
+ ];
81
+
82
+ /**
83
+ * Classify an error message string against known AWS SDK error patterns.
84
+ *
85
+ * Returns the first matching `{category, message}` pair (priority order:
86
+ * aws-auth → aws-throttle → aws-access → aws-model), or `null` if no
87
+ * known AWS SDK signature is found in `text`.
88
+ */
89
+ export function classifyAwsSdkError(text: string): AwsErrorClassification | null {
90
+ if (!text) return null;
91
+ for (const rule of AWS_ERROR_RULES) {
92
+ if (rule.patterns.some((re) => re.test(text))) {
93
+ return { category: rule.category, message: rule.message };
94
+ }
95
+ }
96
+ return null;
97
+ }
@@ -26,6 +26,9 @@
26
26
  export const CONTEXT_FORMULA = "input-cache-output" as const;
27
27
 
28
28
  const CONTEXT_WINDOW_DEFAULTS: Record<string, number> = {
29
+ // Anthropic Fable / Mythos tier
30
+ "claude-fable-5": 1_000_000,
31
+ "claude-mythos-5": 1_000_000,
29
32
  // Anthropic 4.x family
30
33
  "claude-opus-4-8": 1_000_000,
31
34
  "claude-opus-4-7": 1_000_000,
@@ -45,6 +48,8 @@ const CONTEXT_WINDOW_DEFAULTS: Record<string, number> = {
45
48
  "claude-3-sonnet": 200_000,
46
49
  "claude-3-haiku": 200_000,
47
50
  // Shortnames used by the local-CLI adapter and pi-mono OpenRouter mirror.
51
+ fable: 1_000_000,
52
+ mythos: 1_000_000,
48
53
  opus: 1_000_000,
49
54
  sonnet: 1_000_000,
50
55
  haiku: 200_000,