@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
@@ -9,12 +9,16 @@ import {
9
9
  getChildTasks,
10
10
  getCompletedSlackTasks,
11
11
  getInProgressSlackTasks,
12
+ getTaskById,
12
13
  initDb,
13
14
  insertTaskAttachment,
15
+ setSlackMessageTracking,
14
16
  startTask,
17
+ updateTaskProgress,
15
18
  } from "../be/db";
16
19
  import {
17
20
  _getLastRenderedTree,
21
+ _getTaskMessages,
18
22
  _getTaskToTree,
19
23
  _getTreeLastUpdateTime,
20
24
  _getTreeMessages,
@@ -114,6 +118,68 @@ describe("watcher DB queries", () => {
114
118
  startTaskWatcher(60000);
115
119
  stopTaskWatcher();
116
120
  });
121
+
122
+ test("rehydrates tree message tracking from in-progress tasks after restart", () => {
123
+ const agent = createAgent({ name: "WatcherHydrateTreeAgent", isLead: false, status: "idle" });
124
+ const task = createTaskExtended("watcher hydrate tree test", {
125
+ agentId: agent.id,
126
+ source: "slack",
127
+ slackChannelId: "C_HYDRATE_TREE",
128
+ slackThreadTs: "1919191919.000001",
129
+ slackUserId: "U_HYDRATE_TREE",
130
+ });
131
+ startTask(task.id);
132
+
133
+ const messageTs = "1919191919.000002";
134
+ registerTreeMessage(task.id, "C_HYDRATE_TREE", "1919191919.000001", messageTs);
135
+
136
+ expect(getTaskById(task.id)!.slackTreeRootMessageTs).toBe(messageTs);
137
+
138
+ _getTreeMessages().clear();
139
+ _getTaskToTree().clear();
140
+ _getTaskMessages().clear();
141
+
142
+ startTaskWatcher(60000);
143
+ stopTaskWatcher();
144
+
145
+ const tree = _getTreeMessages().get(messageTs);
146
+ expect(tree).toBeDefined();
147
+ expect(tree!.channelId).toBe("C_HYDRATE_TREE");
148
+ expect(tree!.threadTs).toBe("1919191919.000001");
149
+ expect(tree!.rootTaskIds.has(task.id)).toBe(true);
150
+ expect(_getTaskToTree().get(task.id)).toBe(messageTs);
151
+ expect(_getTaskMessages().get(task.id)?.messageTs).toBe(messageTs);
152
+ });
153
+
154
+ test("rehydrates flat progress message tracking from in-progress tasks after restart", () => {
155
+ const agent = createAgent({ name: "WatcherHydrateFlatAgent", isLead: false, status: "idle" });
156
+ const task = createTaskExtended("watcher hydrate flat test", {
157
+ agentId: agent.id,
158
+ source: "slack",
159
+ slackChannelId: "C_HYDRATE_FLAT",
160
+ slackThreadTs: "2020202020.000001",
161
+ slackUserId: "U_HYDRATE_FLAT",
162
+ });
163
+ updateTaskProgress(task.id, "Halfway there");
164
+
165
+ const messageTs = "2020202020.000002";
166
+ setSlackMessageTracking(task.id, { slackProgressMessageTs: messageTs });
167
+
168
+ _getTreeMessages().clear();
169
+ _getTaskToTree().clear();
170
+ _getTaskMessages().clear();
171
+
172
+ startTaskWatcher(60000);
173
+ stopTaskWatcher();
174
+
175
+ expect(_getTreeMessages().has(messageTs)).toBe(false);
176
+ expect(_getTaskToTree().has(task.id)).toBe(false);
177
+ expect(_getTaskMessages().get(task.id)).toEqual({
178
+ channelId: "C_HYDRATE_FLAT",
179
+ threadTs: "2020202020.000001",
180
+ messageTs,
181
+ });
182
+ });
117
183
  });
118
184
 
119
185
  describe("getChildTasks", () => {
@@ -233,7 +233,6 @@ describe("AgentTaskConfigSchema — outputSchema", () => {
233
233
  test("accepts outputSchema in config", async () => {
234
234
  const { AgentTaskExecutor } = await import("../workflows/executors/agent-task");
235
235
  const executor = new AgentTaskExecutor({
236
- // biome-ignore lint/suspicious/noExplicitAny: mock DB for test
237
236
  db: {} as any,
238
237
  eventBus: { emit: () => {}, on: () => {}, off: () => {} },
239
238
  interpolate: (t: string) => t,
@@ -257,7 +256,6 @@ describe("AgentTaskConfigSchema — outputSchema", () => {
257
256
  test("accepts followUpConfig in config", async () => {
258
257
  const { AgentTaskExecutor } = await import("../workflows/executors/agent-task");
259
258
  const executor = new AgentTaskExecutor({
260
- // biome-ignore lint/suspicious/noExplicitAny: mock DB for test
261
259
  db: {} as any,
262
260
  eventBus: { emit: () => {}, on: () => {}, off: () => {} },
263
261
  interpolate: (t: string) => t,
@@ -49,7 +49,6 @@ class MemoryStorage {
49
49
 
50
50
  afterEach(() => {
51
51
  // Clean up the global between tests so leakage can't mask bugs.
52
- // biome-ignore lint/suspicious/noExplicitAny: test-only shim
53
52
  delete (globalThis as any).localStorage;
54
53
  });
55
54
 
@@ -82,7 +81,6 @@ describe("deriveStorageKey", () => {
82
81
  describe("dismiss / restore round-trip via localStorage shape", () => {
83
82
  test("dismiss writes '1' under the namespaced key; restore removes it", () => {
84
83
  const storage = new MemoryStorage();
85
- // biome-ignore lint/suspicious/noExplicitAny: test-only shim
86
84
  (globalThis as any).localStorage = storage;
87
85
 
88
86
  const key = deriveStorageKey("http://localhost:3013", "home-welcome");
@@ -101,7 +99,6 @@ describe("dismiss / restore round-trip via localStorage shape", () => {
101
99
 
102
100
  test("namespace isolation: dismissing on apiUrl A does not affect apiUrl B", () => {
103
101
  const storage = new MemoryStorage();
104
- // biome-ignore lint/suspicious/noExplicitAny: test-only shim
105
102
  (globalThis as any).localStorage = storage;
106
103
 
107
104
  const keyA = deriveStorageKey("http://a.local:3013", "home-welcome");
@@ -118,7 +115,6 @@ describe("graceful failure when localStorage throws", () => {
118
115
  test("setItem throw is swallowed by the hook's try/catch contract", () => {
119
116
  const storage = new MemoryStorage();
120
117
  storage.setThrowOnSet(true);
121
- // biome-ignore lint/suspicious/noExplicitAny: test-only shim
122
118
  (globalThis as any).localStorage = storage;
123
119
 
124
120
  const key = deriveStorageKey("http://localhost:3013", "home-welcome");
@@ -94,13 +94,14 @@ afterAll(async () => {
94
94
  // ─── Tests ───────────────────────────────────────────────────
95
95
 
96
96
  describe("AgentTaskExecutor — workspace scoping", () => {
97
- test("config schema accepts dir, vcsRepo, model, parentTaskId", () => {
97
+ test("config schema accepts dir, vcsRepo, model, modelTier, parentTaskId", () => {
98
98
  const executor = new AgentTaskExecutor(mockDeps);
99
99
  const config = {
100
100
  template: "Do something",
101
101
  dir: "/workspace/repos/my-project",
102
102
  vcsRepo: "org/repo",
103
103
  model: "sonnet",
104
+ modelTier: "smart",
104
105
  parentTaskId: "f1b14078-5df1-457d-88a2-33f1d3e621fd",
105
106
  };
106
107
  const parsed = executor.configSchema.safeParse(config);
@@ -109,6 +110,7 @@ describe("AgentTaskExecutor — workspace scoping", () => {
109
110
  expect(parsed.data.dir).toBe("/workspace/repos/my-project");
110
111
  expect(parsed.data.vcsRepo).toBe("org/repo");
111
112
  expect(parsed.data.model).toBe("sonnet");
113
+ expect(parsed.data.modelTier).toBe("smart");
112
114
  expect(parsed.data.parentTaskId).toBe("f1b14078-5df1-457d-88a2-33f1d3e621fd");
113
115
  }
114
116
  });
@@ -166,7 +168,8 @@ describe("AgentTaskExecutor — workspace scoping", () => {
166
168
  expect(task).toBeDefined();
167
169
  expect(task!.dir).toBe("/workspace/repos/agent-swarm");
168
170
  expect(task!.vcsRepo).toBe("desplega-ai/agent-swarm");
169
- expect(task!.model).toBe("sonnet");
171
+ expect(task!.model).toBeUndefined();
172
+ expect(task!.modelTier).toBe("regular");
170
173
  expect(task!.source).toBe("workflow");
171
174
  });
172
175
 
@@ -47,6 +47,29 @@ class ObjectOutputExecutor extends BaseExecutor<
47
47
  }
48
48
  }
49
49
 
50
+ /**
51
+ * Executor that mimics an agent-task review output shape.
52
+ * Used for regression coverage around aliased property-match fields.
53
+ */
54
+ class ReviewOutputExecutor extends BaseExecutor<
55
+ typeof ReviewOutputExecutor.schema,
56
+ typeof ReviewOutputExecutor.outSchema
57
+ > {
58
+ static readonly schema = z.object({ verdict: z.string() });
59
+ static readonly outSchema = z.object({ taskOutput: z.object({ verdict: z.string() }) });
60
+
61
+ readonly type = "review-output";
62
+ readonly mode = "instant" as const;
63
+ readonly configSchema = ReviewOutputExecutor.schema;
64
+ readonly outputSchema = ReviewOutputExecutor.outSchema;
65
+
66
+ protected async execute(
67
+ config: z.infer<typeof ReviewOutputExecutor.schema>,
68
+ ): Promise<ExecutorResult<z.infer<typeof ReviewOutputExecutor.outSchema>>> {
69
+ return { status: "success", output: { taskOutput: { verdict: config.verdict } } };
70
+ }
71
+ }
72
+
50
73
  /**
51
74
  * Terminal executor that just succeeds. Used for leaf nodes.
52
75
  */
@@ -77,6 +100,7 @@ const mockDeps: ExecutorDependencies = {
77
100
  function createTestRegistry(): ExecutorRegistry {
78
101
  const registry = new ExecutorRegistry();
79
102
  registry.register(new ObjectOutputExecutor(mockDeps));
103
+ registry.register(new ReviewOutputExecutor(mockDeps));
80
104
  registry.register(new NoopExecutor(mockDeps));
81
105
  registry.register(new PropertyMatchExecutor(mockDeps));
82
106
  return registry;
@@ -218,4 +242,161 @@ describe("Validation Port Routing", () => {
218
242
  const run = getWorkflowRun(runId);
219
243
  expect(run!.status).toBe("completed");
220
244
  });
245
+
246
+ test("property-match node resolves fields through node.inputs aliases", async () => {
247
+ const workflow = makeWorkflow({
248
+ nodes: [
249
+ {
250
+ id: "review-step",
251
+ type: "object-output",
252
+ config: { approved: true },
253
+ next: "check",
254
+ },
255
+ {
256
+ id: "check",
257
+ type: "property-match",
258
+ inputs: { review: "review-step" },
259
+ config: {
260
+ conditions: [{ field: "review.approved", op: "eq", value: true }],
261
+ },
262
+ next: { true: "on-pass", false: "on-fail" },
263
+ },
264
+ {
265
+ id: "on-pass",
266
+ type: "noop",
267
+ config: {},
268
+ },
269
+ {
270
+ id: "on-fail",
271
+ type: "noop",
272
+ config: {},
273
+ },
274
+ ],
275
+ });
276
+
277
+ const runId = await startWorkflowExecution(workflow, {}, registry);
278
+ const steps = getWorkflowRunStepsByRunId(runId);
279
+ const nodeIds = steps.map((s) => s.nodeId);
280
+
281
+ expect(nodeIds).toContain("check");
282
+ expect(nodeIds).toContain("on-pass");
283
+ expect(nodeIds).not.toContain("on-fail");
284
+ });
285
+
286
+ test("property-match node keeps raw context path fallback with node.inputs present", async () => {
287
+ const workflow = makeWorkflow({
288
+ nodes: [
289
+ {
290
+ id: "review-step",
291
+ type: "object-output",
292
+ config: { approved: true },
293
+ next: "check",
294
+ },
295
+ {
296
+ id: "check",
297
+ type: "property-match",
298
+ inputs: { review: "review-step" },
299
+ config: {
300
+ conditions: [{ field: "review-step.approved", op: "eq", value: true }],
301
+ },
302
+ next: { true: "on-pass", false: "on-fail" },
303
+ },
304
+ {
305
+ id: "on-pass",
306
+ type: "noop",
307
+ config: {},
308
+ },
309
+ {
310
+ id: "on-fail",
311
+ type: "noop",
312
+ config: {},
313
+ },
314
+ ],
315
+ });
316
+
317
+ const runId = await startWorkflowExecution(workflow, {}, registry);
318
+ const steps = getWorkflowRunStepsByRunId(runId);
319
+ const nodeIds = steps.map((s) => s.nodeId);
320
+
321
+ expect(nodeIds).toContain("on-pass");
322
+ expect(nodeIds).not.toContain("on-fail");
323
+ });
324
+
325
+ test("property-match handles DES-294 review.taskOutput.verdict alias shape", async () => {
326
+ const workflow = makeWorkflow({
327
+ nodes: [
328
+ {
329
+ id: "review-step",
330
+ type: "review-output",
331
+ config: { verdict: "continue" },
332
+ next: "halt-check",
333
+ },
334
+ {
335
+ id: "halt-check",
336
+ type: "property-match",
337
+ inputs: { review: "review-step" },
338
+ config: {
339
+ conditions: [{ field: "review.taskOutput.verdict", op: "eq", value: "continue" }],
340
+ },
341
+ next: { true: "continue-flow", false: "false-halt" },
342
+ },
343
+ {
344
+ id: "continue-flow",
345
+ type: "noop",
346
+ config: {},
347
+ },
348
+ {
349
+ id: "false-halt",
350
+ type: "noop",
351
+ config: {},
352
+ },
353
+ ],
354
+ });
355
+
356
+ const runId = await startWorkflowExecution(workflow, {}, registry);
357
+ const steps = getWorkflowRunStepsByRunId(runId);
358
+ const nodeIds = steps.map((s) => s.nodeId);
359
+
360
+ expect(nodeIds).toContain("continue-flow");
361
+ expect(nodeIds).not.toContain("false-halt");
362
+ });
363
+
364
+ test("property-match validation resolves fields through node.inputs aliases", async () => {
365
+ const workflow = makeWorkflow({
366
+ nodes: [
367
+ {
368
+ id: "check",
369
+ type: "object-output",
370
+ inputs: { review: "trigger.review" },
371
+ config: { approved: true },
372
+ next: "after-check",
373
+ validation: {
374
+ executor: "property-match",
375
+ config: {
376
+ conditions: [{ field: "review.taskOutput.verdict", op: "eq", value: "continue" }],
377
+ },
378
+ mustPass: true,
379
+ },
380
+ },
381
+ {
382
+ id: "after-check",
383
+ type: "noop",
384
+ config: {},
385
+ },
386
+ ],
387
+ });
388
+
389
+ const runId = await startWorkflowExecution(
390
+ workflow,
391
+ { review: { taskOutput: { verdict: "continue" } } },
392
+ registry,
393
+ );
394
+ const steps = getWorkflowRunStepsByRunId(runId);
395
+ const nodeIds = steps.map((s) => s.nodeId);
396
+ const run = getWorkflowRun(runId);
397
+
398
+ expect(run!.status).toBe("completed");
399
+ expect(nodeIds).toContain("check");
400
+ expect(nodeIds).toContain("after-check");
401
+ });
221
402
  });
@@ -2,8 +2,11 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as z from "zod";
3
3
  import { getMemoryStore } from "@/be/memory";
4
4
  import { createToolRegistrar } from "@/tools/utils";
5
+ import type { AgentMemorySource } from "@/types";
5
6
  import { AgentMemorySchema } from "@/types";
6
7
 
8
+ const NUDGE_ELIGIBLE_SOURCES: ReadonlySet<AgentMemorySource> = new Set(["manual", "file_index"]);
9
+
7
10
  export const registerMemoryGetTool = (server: McpServer) => {
8
11
  createToolRegistrar(server)(
9
12
  "memory-get",
@@ -21,6 +24,7 @@ export const registerMemoryGetTool = (server: McpServer) => {
21
24
  success: z.boolean(),
22
25
  message: z.string(),
23
26
  memory: AgentMemorySchema.optional(),
27
+ rateHint: z.string().optional(),
24
28
  }),
25
29
  },
26
30
  async ({ memoryId }, requestInfo, _meta) => {
@@ -37,6 +41,12 @@ export const registerMemoryGetTool = (server: McpServer) => {
37
41
  };
38
42
  }
39
43
 
44
+ const inTaskContext = !!requestInfo.sourceTaskId;
45
+ const rateHint =
46
+ inTaskContext && NUDGE_ELIGIBLE_SOURCES.has(memory.source as AgentMemorySource)
47
+ ? `memory_rate(id="${memory.id}", useful=true|false)`
48
+ : undefined;
49
+
40
50
  return {
41
51
  content: [
42
52
  {
@@ -49,6 +59,7 @@ export const registerMemoryGetTool = (server: McpServer) => {
49
59
  success: true,
50
60
  message: `Memory "${memory.name}" retrieved.`,
51
61
  memory,
62
+ rateHint,
52
63
  },
53
64
  };
54
65
  },
@@ -6,8 +6,15 @@ import { CANDIDATE_SET_MULTIPLIER } from "@/be/memory/constants";
6
6
  import { recordRetrievals } from "@/be/memory/raters/retrieval";
7
7
  import { rerank } from "@/be/memory/reranker";
8
8
  import { createToolRegistrar } from "@/tools/utils";
9
+ import type { AgentMemorySource } from "@/types";
9
10
  import { AgentMemoryScopeSchema, AgentMemorySourceSchema } from "@/types";
10
11
 
12
+ const NUDGE_ELIGIBLE_SOURCES: ReadonlySet<AgentMemorySource> = new Set(["manual", "file_index"]);
13
+
14
+ function rateHintFor(memoryId: string): string {
15
+ return `memory_rate(id="${memoryId}", useful=true|false)`;
16
+ }
17
+
11
18
  export const registerMemorySearchTool = (server: McpServer) => {
12
19
  createToolRegistrar(server)(
13
20
  "memory-search",
@@ -42,9 +49,11 @@ export const registerMemorySearchTool = (server: McpServer) => {
42
49
  scope: AgentMemoryScopeSchema,
43
50
  similarity: z.number().optional(),
44
51
  createdAt: z.string(),
52
+ rateHint: z.string().optional(),
45
53
  }),
46
54
  )
47
55
  .optional(),
56
+ _ratingNudge: z.string().optional(),
48
57
  }),
49
58
  },
50
59
  async ({ query, scope, limit, source }, requestInfo, _meta) => {
@@ -94,6 +103,7 @@ export const registerMemorySearchTool = (server: McpServer) => {
94
103
  }
95
104
  }
96
105
 
106
+ const inTaskContext = !!requestInfo.sourceTaskId;
97
107
  const mapped = ranked.map((r) => ({
98
108
  id: r.id,
99
109
  name: r.name,
@@ -102,8 +112,15 @@ export const registerMemorySearchTool = (server: McpServer) => {
102
112
  scope: r.scope,
103
113
  similarity: r.similarity,
104
114
  createdAt: r.createdAt,
115
+ ...(inTaskContext && NUDGE_ELIGIBLE_SOURCES.has(r.source as AgentMemorySource)
116
+ ? { rateHint: rateHintFor(r.id) }
117
+ : {}),
105
118
  }));
106
119
 
120
+ const nudgeCount = mapped.filter((r) => r.rateHint).length;
121
+ const _ratingNudge =
122
+ nudgeCount > 0 ? "Rate memories that help or mislead you with memory_rate." : undefined;
123
+
107
124
  return {
108
125
  content: [
109
126
  {
@@ -116,6 +133,7 @@ export const registerMemorySearchTool = (server: McpServer) => {
116
133
  success: true,
117
134
  message: `Found ${mapped.length} memories matching "${query}".`,
118
135
  results: mapped,
136
+ _ratingNudge,
119
137
  },
120
138
  };
121
139
  }
@@ -4,6 +4,71 @@ import * as z from "zod";
4
4
  import { createScheduledTask, getAgentById, getScheduledTaskByName } from "@/be/db";
5
5
  import { calculateNextRun } from "@/scheduler";
6
6
  import { createToolRegistrar } from "@/tools/utils";
7
+ import { ModelTierSchema, splitLegacyModelAlias } from "../../model-tiers";
8
+
9
+ export const createScheduleInputSchema = z.object({
10
+ name: z.string().min(1).max(100).describe("Unique name for the schedule (e.g., 'daily-cleanup')"),
11
+ taskTemplate: z.string().min(1).describe("The task description that will be created each time"),
12
+ scheduleType: z
13
+ .enum(["recurring", "one_time"])
14
+ .default("recurring")
15
+ .optional()
16
+ .describe("Schedule type: 'recurring' (default) or 'one_time'"),
17
+ cronExpression: z
18
+ .string()
19
+ .optional()
20
+ .describe("Cron expression for recurring schedules (e.g., '0 9 * * *')"),
21
+ intervalMs: z
22
+ .number()
23
+ .int()
24
+ .positive()
25
+ .optional()
26
+ .describe("Interval in milliseconds for recurring schedules (e.g., 3600000 for hourly)"),
27
+ delayMs: z
28
+ .number()
29
+ .int()
30
+ .positive()
31
+ .optional()
32
+ .describe("Delay in milliseconds for one-time schedules (e.g., 1800000 for 30 min)"),
33
+ runAt: z
34
+ .string()
35
+ .datetime()
36
+ .optional()
37
+ .describe("ISO datetime for one-time schedules (e.g., '2026-03-06T15:00:00Z')"),
38
+ description: z.string().optional().describe("Human-readable description of the schedule"),
39
+ taskType: z.string().max(50).optional().describe("Task type (e.g., 'maintenance', 'report')"),
40
+ tags: z.array(z.string()).optional().describe("Tags to apply to created tasks"),
41
+ priority: z
42
+ .number()
43
+ .int()
44
+ .min(0)
45
+ .max(100)
46
+ .default(50)
47
+ .optional()
48
+ .describe("Task priority 0-100 (default: 50)"),
49
+ targetAgentId: z
50
+ .string()
51
+ .uuid()
52
+ .optional()
53
+ .describe("Agent to assign tasks to (omit for task pool)"),
54
+ timezone: z.string().default("UTC").optional().describe("Timezone for cron schedules"),
55
+ enabled: z
56
+ .boolean()
57
+ .default(true)
58
+ .optional()
59
+ .describe("Whether the schedule is enabled (default: true)"),
60
+ model: z
61
+ .string()
62
+ .trim()
63
+ .min(1)
64
+ .optional()
65
+ .describe(
66
+ "Concrete model override for tasks created by this schedule. Interpreted by each assignee's harness/provider and does not switch providers. Prefer modelTier for portable intent.",
67
+ ),
68
+ modelTier: ModelTierSchema.optional().describe(
69
+ "Portable model tier for tasks created by this schedule: 'smol', 'regular', 'smart', or 'ultra'. Resolved by each assignee's harness/provider at run time.",
70
+ ),
71
+ });
7
72
 
8
73
  export const registerCreateScheduleTool = (server: McpServer) => {
9
74
  createToolRegistrar(server)(
@@ -13,75 +78,7 @@ export const registerCreateScheduleTool = (server: McpServer) => {
13
78
  annotations: { destructiveHint: false },
14
79
  description:
15
80
  "Create a new scheduled task. For recurring: provide cronExpression or intervalMs. For one-time: provide delayMs or runAt with scheduleType 'one_time'.",
16
- inputSchema: z.object({
17
- name: z
18
- .string()
19
- .min(1)
20
- .max(100)
21
- .describe("Unique name for the schedule (e.g., 'daily-cleanup')"),
22
- taskTemplate: z
23
- .string()
24
- .min(1)
25
- .describe("The task description that will be created each time"),
26
- scheduleType: z
27
- .enum(["recurring", "one_time"])
28
- .default("recurring")
29
- .optional()
30
- .describe("Schedule type: 'recurring' (default) or 'one_time'"),
31
- cronExpression: z
32
- .string()
33
- .optional()
34
- .describe("Cron expression for recurring schedules (e.g., '0 9 * * *')"),
35
- intervalMs: z
36
- .number()
37
- .int()
38
- .positive()
39
- .optional()
40
- .describe("Interval in milliseconds for recurring schedules (e.g., 3600000 for hourly)"),
41
- delayMs: z
42
- .number()
43
- .int()
44
- .positive()
45
- .optional()
46
- .describe("Delay in milliseconds for one-time schedules (e.g., 1800000 for 30 min)"),
47
- runAt: z
48
- .string()
49
- .datetime()
50
- .optional()
51
- .describe("ISO datetime for one-time schedules (e.g., '2026-03-06T15:00:00Z')"),
52
- description: z.string().optional().describe("Human-readable description of the schedule"),
53
- taskType: z
54
- .string()
55
- .max(50)
56
- .optional()
57
- .describe("Task type (e.g., 'maintenance', 'report')"),
58
- tags: z.array(z.string()).optional().describe("Tags to apply to created tasks"),
59
- priority: z
60
- .number()
61
- .int()
62
- .min(0)
63
- .max(100)
64
- .default(50)
65
- .optional()
66
- .describe("Task priority 0-100 (default: 50)"),
67
- targetAgentId: z
68
- .string()
69
- .uuid()
70
- .optional()
71
- .describe("Agent to assign tasks to (omit for task pool)"),
72
- timezone: z.string().default("UTC").optional().describe("Timezone for cron schedules"),
73
- enabled: z
74
- .boolean()
75
- .default(true)
76
- .optional()
77
- .describe("Whether the schedule is enabled (default: true)"),
78
- model: z
79
- .enum(["haiku", "sonnet", "opus"])
80
- .optional()
81
- .describe(
82
- "Model to use for tasks created by this schedule ('haiku', 'sonnet', or 'opus'). If not set, uses agent/global config or defaults to 'opus'.",
83
- ),
84
- }),
81
+ inputSchema: createScheduleInputSchema,
85
82
  outputSchema: z.object({
86
83
  yourAgentId: z.string().uuid().optional(),
87
84
  success: z.boolean(),
@@ -104,6 +101,7 @@ export const registerCreateScheduleTool = (server: McpServer) => {
104
101
  createdByAgentId: z.string().optional(),
105
102
  timezone: z.string(),
106
103
  model: z.string().optional(),
104
+ modelTier: ModelTierSchema.optional(),
107
105
  scheduleType: z.string(),
108
106
  createdAt: z.string(),
109
107
  lastUpdatedAt: z.string(),
@@ -128,6 +126,7 @@ export const registerCreateScheduleTool = (server: McpServer) => {
128
126
  timezone,
129
127
  enabled,
130
128
  model,
129
+ modelTier,
131
130
  },
132
131
  requestInfo,
133
132
  _meta,
@@ -270,6 +269,7 @@ export const registerCreateScheduleTool = (server: McpServer) => {
270
269
  }
271
270
 
272
271
  try {
272
+ const normalizedModel = splitLegacyModelAlias({ model, modelTier });
273
273
  // Calculate initial nextRunAt
274
274
  let nextRunAt: string | undefined;
275
275
  if (enabled === false) {
@@ -299,7 +299,8 @@ export const registerCreateScheduleTool = (server: McpServer) => {
299
299
  enabled,
300
300
  nextRunAt,
301
301
  createdByAgentId: requestInfo.agentId,
302
- model,
302
+ model: normalizedModel.model,
303
+ modelTier: normalizedModel.modelTier,
303
304
  scheduleType: scheduleType ?? "recurring",
304
305
  });
305
306