@desplega.ai/agent-swarm 1.93.0 → 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.
- package/README.md +2 -2
- package/openapi.json +180 -1
- package/package.json +1 -1
- package/src/be/db.ts +63 -7
- package/src/be/migrations/090_model_tiers.sql +2 -0
- package/src/be/migrations/091_seed_swarm_operations_metrics.sql +12 -0
- package/src/be/migrations/092_metrics_dashboard_combobox_filters.sql +68 -0
- package/src/be/migrations/093_slack_message_tracking.sql +6 -0
- package/src/be/migrations/runner.ts +52 -0
- package/src/be/modelsdev-cache.json +2060 -198
- package/src/be/scripts/boot-reembed.ts +74 -0
- package/src/be/scripts/db.ts +19 -3
- package/src/be/seed/index.ts +1 -1
- package/src/be/seed/registry.ts +2 -2
- package/src/be/seed/runner.ts +5 -5
- package/src/be/seed/types.ts +6 -1
- package/src/be/seed-pricing.ts +1 -0
- package/src/be/seed-scripts/index.ts +3 -2
- package/src/commands/runner.ts +83 -13
- package/src/http/index.ts +13 -2
- package/src/http/metrics.ts +55 -6
- package/src/http/schedules.ts +16 -15
- package/src/http/script-runs.ts +7 -1
- package/src/http/scripts.ts +147 -1
- package/src/http/tasks.ts +7 -0
- package/src/model-tiers.ts +140 -0
- package/src/providers/claude-managed-models.ts +9 -0
- package/src/providers/opencode-adapter.ts +1 -0
- package/src/providers/pi-mono-adapter.ts +78 -6
- package/src/scheduler/scheduler.ts +22 -34
- package/src/server-user.ts +8 -2
- package/src/slack/responses.ts +39 -11
- package/src/slack/watcher.ts +121 -8
- package/src/tests/agents-list-model-display.test.ts +13 -0
- package/src/tests/aws-error-classifier.test.ts +148 -0
- package/src/tests/claude-managed-adapter.test.ts +12 -0
- package/src/tests/context-window.test.ts +7 -0
- package/src/tests/http-api-integration.test.ts +19 -0
- package/src/tests/metrics-http.test.ts +137 -3
- package/src/tests/migration-046-budgets.test.ts +33 -0
- package/src/tests/migration-runner-regressions.test.ts +69 -0
- package/src/tests/model-control.test.ts +162 -46
- package/src/tests/opencode-adapter.test.ts +9 -0
- package/src/tests/pi-mono-adapter.test.ts +319 -0
- package/src/tests/providers/pi-cost.test.ts +9 -0
- package/src/tests/runner-fallback-output.test.ts +50 -0
- package/src/tests/scripts-boot-reembed.test.ts +163 -0
- package/src/tests/scripts-embeddings.test.ts +90 -0
- package/src/tests/seed.test.ts +26 -1
- package/src/tests/session-costs-model-key-normalize.test.ts +2 -0
- package/src/tests/slack-watcher.test.ts +66 -0
- package/src/tests/workflow-agent-task.test.ts +5 -2
- package/src/tests/workflow-validation-port-routing.test.ts +181 -0
- package/src/tools/memory-get.ts +11 -0
- package/src/tools/memory-search.ts +18 -0
- package/src/tools/schedules/create-schedule.ts +71 -70
- package/src/tools/schedules/update-schedule.ts +43 -31
- package/src/tools/send-task.ts +16 -5
- package/src/tools/task-action.ts +11 -3
- package/src/types.ts +29 -0
- package/src/utils/aws-error-classifier.ts +97 -0
- package/src/utils/context-window.ts +2 -0
- package/src/utils/credentials.test.ts +68 -0
- package/src/utils/credentials.ts +44 -3
- package/src/utils/pretty-print.ts +25 -10
- package/src/workflows/engine.ts +3 -2
- package/src/workflows/executors/agent-task.ts +3 -1
package/src/tests/seed.test.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { unlink } from "node:fs/promises";
|
|
3
3
|
import { closeDb, initDb } from "../be/db";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getSeedState,
|
|
6
|
+
runSeeder,
|
|
7
|
+
type Seeder,
|
|
8
|
+
type SeederRunOptions,
|
|
9
|
+
type SeedItem,
|
|
10
|
+
} from "../be/seed";
|
|
5
11
|
|
|
6
12
|
const TEST_DB_PATH = "./test-seed.sqlite";
|
|
7
13
|
|
|
@@ -137,6 +143,25 @@ describe("seeder harness — versioning rule", () => {
|
|
|
137
143
|
expect(upstream.get("y")).toBe("h-pre-existing");
|
|
138
144
|
});
|
|
139
145
|
|
|
146
|
+
test("runner passes opts through to apply()", async () => {
|
|
147
|
+
const capturedOpts: (SeederRunOptions | undefined)[] = [];
|
|
148
|
+
const source = new Map([["a", "h-a1"]]);
|
|
149
|
+
const upstream = new Map<string, string>();
|
|
150
|
+
const seeder: Seeder<SeedItem> = {
|
|
151
|
+
kind: "opts-passthrough-test",
|
|
152
|
+
items: () => [...source.entries()].map(([key, contentHash]) => ({ key, contentHash })),
|
|
153
|
+
upstreamHash: (item) => upstream.get(item.key) ?? null,
|
|
154
|
+
apply: (item, _action, opts) => {
|
|
155
|
+
capturedOpts.push(opts);
|
|
156
|
+
upstream.set(item.key, item.contentHash);
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
await runSeeder(seeder, { quiet: true, scriptEmbeddingMode: "skip" });
|
|
161
|
+
expect(capturedOpts).toHaveLength(1);
|
|
162
|
+
expect(capturedOpts[0]?.scriptEmbeddingMode).toBe("skip");
|
|
163
|
+
});
|
|
164
|
+
|
|
140
165
|
test("a throwing apply is captured per-item without aborting the run", async () => {
|
|
141
166
|
const source = new Map([
|
|
142
167
|
["ok", "h-ok"],
|
|
@@ -134,6 +134,8 @@ describe("normalizeModelKey()", () => {
|
|
|
134
134
|
|
|
135
135
|
test("is a no-op for canonical claude ids", () => {
|
|
136
136
|
expect(normalizeModelKey("claude", "claude-opus-4-7")).toBe("claude-opus-4-7");
|
|
137
|
+
expect(normalizeModelKey("claude", "claude-fable-5")).toBe("claude-fable-5");
|
|
138
|
+
expect(normalizeModelKey("claude", "claude-mythos-5")).toBe("claude-mythos-5");
|
|
137
139
|
});
|
|
138
140
|
|
|
139
141
|
test("is idempotent", () => {
|
|
@@ -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", () => {
|
|
@@ -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).
|
|
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
|
});
|
package/src/tools/memory-get.ts
CHANGED
|
@@ -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:
|
|
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", "fable"])
|
|
80
|
-
.optional()
|
|
81
|
-
.describe(
|
|
82
|
-
"Model to use for tasks created by this schedule ('haiku', 'sonnet', 'opus', or 'fable'). 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
|
|