@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.
- package/README.md +2 -2
- package/openapi.json +242 -3
- package/package.json +5 -5
- package/src/be/db.ts +152 -11
- package/src/be/memory/boot-reembed.ts +0 -1
- package/src/be/memory/providers/sqlite-store.ts +42 -25
- package/src/be/memory/raters/llm-client.ts +12 -5
- package/src/be/memory/types.ts +3 -0
- package/src/be/migrations/088_script_runs_list_indexes.sql +10 -0
- package/src/be/migrations/089_harness_variant.sql +2 -0
- 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 +3264 -1166
- 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 +2 -0
- package/src/be/seed-scripts/catalog/boot-triage.inline.ts +221 -0
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +457 -0
- package/src/be/seed-scripts/catalog/compound-insights.inline.ts +863 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +506 -0
- package/src/be/seed-scripts/index.ts +8 -7
- package/src/be/skill-sync.ts +28 -179
- package/src/commands/runner.ts +197 -10
- package/src/http/api-keys.ts +42 -0
- package/src/http/index.ts +13 -2
- package/src/http/mcp-bridge.ts +1 -1
- package/src/http/memory.ts +23 -24
- 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 +17 -6
- package/src/model-tiers.ts +140 -0
- package/src/providers/claude-adapter.ts +33 -1
- package/src/providers/claude-managed-adapter.ts +3 -0
- package/src/providers/claude-managed-models.ts +16 -0
- package/src/providers/codex-adapter.ts +8 -1
- package/src/providers/codex-models.ts +1 -0
- package/src/providers/codex-oauth/auth-json.ts +1 -0
- package/src/providers/harness-version.ts +7 -0
- package/src/providers/opencode-adapter.ts +12 -4
- package/src/providers/pi-mono-adapter.ts +90 -8
- package/src/providers/types.ts +2 -0
- package/src/scheduler/scheduler.ts +22 -34
- package/src/scripts-runtime/egress-secrets.ts +83 -0
- package/src/scripts-runtime/eval-harness.ts +4 -0
- package/src/scripts-runtime/executors/types.ts +7 -0
- package/src/scripts-runtime/loader.ts +2 -0
- package/src/server-user.ts +8 -2
- package/src/slack/channel-join.ts +41 -0
- package/src/slack/responses.ts +39 -11
- package/src/slack/watcher.ts +121 -8
- package/src/tests/additive-buffer.test.ts +0 -1
- package/src/tests/agents-list-model-display.test.ts +13 -0
- package/src/tests/api-key-tracking.test.ts +113 -0
- package/src/tests/approval-requests.test.ts +0 -6
- package/src/tests/aws-error-classifier.test.ts +148 -0
- package/src/tests/claude-managed-adapter.test.ts +12 -0
- package/src/tests/claude-managed-setup.test.ts +0 -4
- package/src/tests/codex-pool.test.ts +2 -6
- package/src/tests/context-window.test.ts +7 -0
- package/src/tests/http-api-integration.test.ts +23 -6
- package/src/tests/memory-edges.test.ts +0 -2
- package/src/tests/memory-rate-endpoint.test.ts +0 -2
- package/src/tests/memory-rater-e2e.test.ts +0 -2
- package/src/tests/memory-store.test.ts +19 -1
- package/src/tests/memory.test.ts +51 -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/reload-config.test.ts +33 -17
- package/src/tests/runner-fallback-output.test.ts +50 -0
- package/src/tests/runner-skills-refresh.test.ts +216 -46
- package/src/tests/script-runs-http.test.ts +7 -1
- package/src/tests/scripts-boot-reembed.test.ts +163 -0
- package/src/tests/scripts-embeddings.test.ts +90 -0
- package/src/tests/scripts-runtime-secret-egress.test.ts +129 -0
- package/src/tests/seed-scripts.test.ts +13 -1
- package/src/tests/seed.test.ts +26 -1
- package/src/tests/session-attach.test.ts +6 -6
- package/src/tests/session-costs-model-key-normalize.test.ts +2 -0
- package/src/tests/skill-fs-writer.test.ts +250 -0
- package/src/tests/slack-attachments-block.test.ts +0 -1
- package/src/tests/slack-blocks.test.ts +0 -1
- package/src/tests/slack-channel-join.test.ts +80 -0
- package/src/tests/slack-identity-resolution.test.ts +0 -1
- package/src/tests/slack-watcher.test.ts +66 -0
- package/src/tests/structured-output.test.ts +0 -2
- package/src/tests/use-dismissible-card.test.ts +0 -4
- 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/slack-post.ts +18 -15
- package/src/tools/slack-read.ts +9 -11
- package/src/tools/slack-reply.ts +18 -15
- package/src/tools/slack-start-thread.ts +17 -14
- package/src/tools/task-action.ts +11 -3
- package/src/types.ts +40 -0
- package/src/utils/aws-error-classifier.ts +97 -0
- package/src/utils/context-window.ts +5 -0
- package/src/utils/credentials.test.ts +68 -0
- package/src/utils/credentials.ts +66 -5
- package/src/utils/pretty-print.ts +25 -10
- package/src/utils/skill-fs-writer.ts +220 -0
- package/src/utils/skills-refresh.ts +123 -40
- package/src/workflows/engine.ts +3 -2
- 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).
|
|
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"])
|
|
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
|
|