agent-planner-mcp 1.5.9 → 1.5.12
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/package.json +1 -1
- package/src/tools/bdi/_shared.js +17 -1
- package/src/tools/bdi/desires.js +17 -8
- package/src/tools/bdi/intentions.js +61 -21
package/package.json
CHANGED
package/src/tools/bdi/_shared.js
CHANGED
|
@@ -26,6 +26,22 @@ function safeArray(value) {
|
|
|
26
26
|
return Array.isArray(value) ? value : [];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Extract the most actionable message from an axios error. The AP backend's
|
|
31
|
+
* validation errors carry a field-level `message` (e.g. "urgency: must be one
|
|
32
|
+
* of ...; options.0: Unrecognized key") plus a `details` array — far more
|
|
33
|
+
* useful than the generic top-line `error` ("Validation failed"), which is all
|
|
34
|
+
* most handlers surfaced. Prefer message → error → err.message.
|
|
35
|
+
*/
|
|
36
|
+
function apiErrorMessage(err) {
|
|
37
|
+
const data = err?.response?.data;
|
|
38
|
+
if (data) {
|
|
39
|
+
if (data.message && data.message !== data.error) return data.message;
|
|
40
|
+
if (typeof data.error === 'string') return data.error;
|
|
41
|
+
}
|
|
42
|
+
return err?.message || 'unknown error';
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
/**
|
|
30
46
|
* The web app origin (where /app/plans/:id lives), for building shareable plan
|
|
31
47
|
* links agents can post (e.g. to Slack). Derived from API_URL — the web app
|
|
@@ -56,4 +72,4 @@ function isV1Unavailable(err) {
|
|
|
56
72
|
return !(body && typeof body === 'object' && body.error);
|
|
57
73
|
}
|
|
58
74
|
|
|
59
|
-
module.exports = { asOf, formatResponse, errorResponse, safeArray, isV1Unavailable, webOrigin, planUrl };
|
|
75
|
+
module.exports = { asOf, formatResponse, errorResponse, safeArray, apiErrorMessage, isV1Unavailable, webOrigin, planUrl };
|
package/src/tools/bdi/desires.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* directly — no UI round-trip and no forced approval gate.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const { asOf, formatResponse, errorResponse, safeArray } = require('./_shared');
|
|
10
|
+
const { asOf, formatResponse, errorResponse, safeArray, apiErrorMessage } = require('./_shared');
|
|
11
11
|
|
|
12
12
|
const listGoalsDefinition = {
|
|
13
13
|
name: 'list_goals',
|
|
@@ -110,11 +110,20 @@ async function updateGoalHandler(args, apiClient) {
|
|
|
110
110
|
const applied = [];
|
|
111
111
|
const failures = [];
|
|
112
112
|
|
|
113
|
-
// Direct field updates
|
|
113
|
+
// Direct field updates. title/description/priority/status share their name
|
|
114
|
+
// with the backend, but success_criteria must be camelCased to successCriteria
|
|
115
|
+
// — the goal update schema is .strict(), so the snake_case key was rejected
|
|
116
|
+
// with a 400 (the one multi-word field, hence "description writes but
|
|
117
|
+
// success_criteria fails"). Match the create shape: wrap a bare array as
|
|
118
|
+
// { criteria: [...] }.
|
|
114
119
|
const directFields = {};
|
|
115
|
-
for (const k of ['title', 'description', 'priority', 'status'
|
|
120
|
+
for (const k of ['title', 'description', 'priority', 'status']) {
|
|
116
121
|
if (changes[k] !== undefined) directFields[k] = changes[k];
|
|
117
122
|
}
|
|
123
|
+
if (changes.success_criteria !== undefined) {
|
|
124
|
+
const sc = changes.success_criteria;
|
|
125
|
+
directFields.successCriteria = Array.isArray(sc) ? { criteria: sc } : sc;
|
|
126
|
+
}
|
|
118
127
|
// Map the public `committed` boolean onto the backend's commitment write
|
|
119
128
|
// (the API still accepts the legacy goalType field and translates it to
|
|
120
129
|
// promoted_at). committed:true ⇒ promoted, false ⇒ aspirational.
|
|
@@ -127,21 +136,21 @@ async function updateGoalHandler(args, apiClient) {
|
|
|
127
136
|
await apiClient.goals.update(goal_id, directFields);
|
|
128
137
|
applied.push('direct_fields');
|
|
129
138
|
} catch (err) {
|
|
130
|
-
failures.push({ step: 'direct_fields', error: err
|
|
139
|
+
failures.push({ step: 'direct_fields', error: apiErrorMessage(err) });
|
|
131
140
|
}
|
|
132
141
|
}
|
|
133
142
|
|
|
134
143
|
for (const planId of safeArray(changes.add_linked_plans)) {
|
|
135
144
|
try { await apiClient.goals.linkPlan(goal_id, planId); applied.push(`link_plan:${planId}`); }
|
|
136
|
-
catch (err) { failures.push({ step: `link_plan:${planId}`, error: err
|
|
145
|
+
catch (err) { failures.push({ step: `link_plan:${planId}`, error: apiErrorMessage(err) }); }
|
|
137
146
|
}
|
|
138
147
|
for (const planId of safeArray(changes.remove_linked_plans)) {
|
|
139
148
|
try { await apiClient.goals.unlinkPlan(goal_id, planId); applied.push(`unlink_plan:${planId}`); }
|
|
140
|
-
catch (err) { failures.push({ step: `unlink_plan:${planId}`, error: err
|
|
149
|
+
catch (err) { failures.push({ step: `unlink_plan:${planId}`, error: apiErrorMessage(err) }); }
|
|
141
150
|
}
|
|
142
151
|
for (const nodeId of safeArray(changes.add_achievers)) {
|
|
143
152
|
try { await apiClient.goals.addAchiever(goal_id, nodeId); applied.push(`add_achiever:${nodeId}`); }
|
|
144
|
-
catch (err) { failures.push({ step: `add_achiever:${nodeId}`, error: err
|
|
153
|
+
catch (err) { failures.push({ step: `add_achiever:${nodeId}`, error: apiErrorMessage(err) }); }
|
|
145
154
|
}
|
|
146
155
|
for (const nodeId of safeArray(changes.remove_achievers)) {
|
|
147
156
|
try {
|
|
@@ -152,7 +161,7 @@ async function updateGoalHandler(args, apiClient) {
|
|
|
152
161
|
applied.push(`remove_achiever:${nodeId}`);
|
|
153
162
|
}
|
|
154
163
|
} catch (err) {
|
|
155
|
-
failures.push({ step: `remove_achiever:${nodeId}`, error: err
|
|
164
|
+
failures.push({ step: `remove_achiever:${nodeId}`, error: apiErrorMessage(err) });
|
|
156
165
|
}
|
|
157
166
|
}
|
|
158
167
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* See ../../../docs/MCP_v1.0_FULL_SURFACE.md for design rationale.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
const { asOf, formatResponse, errorResponse, isV1Unavailable, planUrl } = require('./_shared');
|
|
16
|
+
const { asOf, formatResponse, errorResponse, apiErrorMessage, isV1Unavailable, planUrl } = require('./_shared');
|
|
17
17
|
const { version: PKG_VERSION } = require('../../../package.json');
|
|
18
18
|
|
|
19
19
|
// Provenance tag stamped onto every plan this server creates, so a plan stays
|
|
@@ -108,14 +108,37 @@ async function queueDecisionHandler(args, apiClient) {
|
|
|
108
108
|
return errorResponse('invalid_arg', 'queue_decision requires either plan_id or node_id');
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// The agent-facing urgency vocabulary (low/normal/high) differs from the
|
|
112
|
+
// backend decision schema (blocking/can_continue/informational) — map it, or
|
|
113
|
+
// every call fails strict validation.
|
|
114
|
+
const URGENCY_MAP = { low: 'informational', normal: 'can_continue', high: 'blocking' };
|
|
115
|
+
const mappedUrgency = URGENCY_MAP[urgency] || 'can_continue';
|
|
116
|
+
|
|
117
|
+
// The backend decisionOption shape is {option, pros?, cons?, recommendation?}
|
|
118
|
+
// and is .strict() — our {label, description} would be rejected. Fold
|
|
119
|
+
// description into the option text and mark the recommended one.
|
|
120
|
+
const recText = String(recommendation || '').toLowerCase();
|
|
121
|
+
const mappedOptions = Array.isArray(options)
|
|
122
|
+
? options
|
|
123
|
+
.map((o) => {
|
|
124
|
+
const label = o.label || o.option || '';
|
|
125
|
+
const option = o.description ? `${label} — ${o.description}` : label;
|
|
126
|
+
const isRecommended = label && recText.includes(label.toLowerCase());
|
|
127
|
+
return option ? { option, ...(isRecommended ? { recommendation: true } : {}) } : null;
|
|
128
|
+
})
|
|
129
|
+
.filter(Boolean)
|
|
130
|
+
: [];
|
|
131
|
+
|
|
111
132
|
const body = {
|
|
112
133
|
title,
|
|
113
134
|
context,
|
|
114
|
-
options:
|
|
115
|
-
|
|
116
|
-
|
|
135
|
+
options: mappedOptions,
|
|
136
|
+
urgency: mappedUrgency,
|
|
137
|
+
// Top-level `recommendation` is not in the strict schema — keep the agent's
|
|
138
|
+
// free-text recommendation and ask in metadata instead.
|
|
117
139
|
metadata: {
|
|
118
140
|
smallest_input_needed,
|
|
141
|
+
recommendation: recommendation || null,
|
|
119
142
|
goal_id: goal_id || null,
|
|
120
143
|
source: 'bdi.queue_decision',
|
|
121
144
|
proposed_subtasks: Array.isArray(proposed_subtasks) ? proposed_subtasks : undefined,
|
|
@@ -136,10 +159,7 @@ async function queueDecisionHandler(args, apiClient) {
|
|
|
136
159
|
title: created.title,
|
|
137
160
|
});
|
|
138
161
|
} catch (err) {
|
|
139
|
-
return errorResponse(
|
|
140
|
-
'upstream_unavailable',
|
|
141
|
-
`Failed to queue decision: ${err.response?.data?.error || err.message}`
|
|
142
|
-
);
|
|
162
|
+
return errorResponse('upstream_unavailable', `Failed to queue decision: ${apiErrorMessage(err)}`);
|
|
143
163
|
}
|
|
144
164
|
}
|
|
145
165
|
|
|
@@ -187,20 +207,20 @@ async function resolveDecisionHandler(args, apiClient) {
|
|
|
187
207
|
// Best-effort — if fetch fails, we still try to resolve.
|
|
188
208
|
}
|
|
189
209
|
|
|
210
|
+
// The backend resolve schema is strict {decision, rationale}. Encode the
|
|
211
|
+
// action (+ chosen option) into `decision` and the note into `rationale` —
|
|
212
|
+
// the previous {resolution, message, selected_option} body was rejected.
|
|
213
|
+
const decisionText = selected_option ? `${action} — ${selected_option}` : action;
|
|
190
214
|
let resolved;
|
|
191
215
|
try {
|
|
192
216
|
resolved = await apiClient.axiosInstance
|
|
193
217
|
.post(`/plans/${plan_id}/decisions/${decision_id}/resolve`, {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
selected_option: selected_option || null,
|
|
218
|
+
decision: decisionText,
|
|
219
|
+
rationale: message || undefined,
|
|
197
220
|
})
|
|
198
221
|
.then((r) => r.data);
|
|
199
222
|
} catch (err) {
|
|
200
|
-
return errorResponse(
|
|
201
|
-
'upstream_unavailable',
|
|
202
|
-
`Failed to resolve decision: ${err.response?.data?.error || err.message}`
|
|
203
|
-
);
|
|
223
|
+
return errorResponse('upstream_unavailable', `Failed to resolve decision: ${apiErrorMessage(err)}`);
|
|
204
224
|
}
|
|
205
225
|
|
|
206
226
|
// On approve, materialize any proposed_subtasks atomically (best-effort per task).
|
|
@@ -209,15 +229,21 @@ async function resolveDecisionHandler(args, apiClient) {
|
|
|
209
229
|
if (action === 'approve' && decision?.metadata?.proposed_subtasks?.length) {
|
|
210
230
|
for (const proposal of decision.metadata.proposed_subtasks) {
|
|
211
231
|
try {
|
|
232
|
+
// createNode's schema is .strict() and has no acceptance_criteria field —
|
|
233
|
+
// sending it 400s the whole subtask. Fold it into the description so the
|
|
234
|
+
// criteria survive instead of being silently dropped on approval.
|
|
235
|
+
const description = [
|
|
236
|
+
proposal.description,
|
|
237
|
+
proposal.acceptance_criteria ? `Acceptance criteria: ${proposal.acceptance_criteria}` : null,
|
|
238
|
+
].filter(Boolean).join('\n\n') || undefined;
|
|
212
239
|
const node = await apiClient.nodes.createNode(plan_id, {
|
|
213
240
|
parent_id: proposal.parent_id,
|
|
214
241
|
node_type: proposal.node_type || 'task',
|
|
215
242
|
title: proposal.title,
|
|
216
|
-
description
|
|
243
|
+
description,
|
|
217
244
|
status: 'not_started',
|
|
218
245
|
task_mode: proposal.task_mode || 'free',
|
|
219
246
|
agent_instructions: proposal.agent_instructions,
|
|
220
|
-
acceptance_criteria: proposal.acceptance_criteria,
|
|
221
247
|
});
|
|
222
248
|
created.push({ id: node.id || node.node?.id, title: proposal.title, parent_id: proposal.parent_id });
|
|
223
249
|
} catch (err) {
|
|
@@ -254,6 +280,12 @@ const STATUS_TO_LOG_TYPE = {
|
|
|
254
280
|
plan_ready: 'progress',
|
|
255
281
|
};
|
|
256
282
|
|
|
283
|
+
// The backend log endpoint accepts comment/progress/reasoning/decision/challenge.
|
|
284
|
+
// Two friendly aliases the tool historically advertised are NOT valid there and
|
|
285
|
+
// 400'd the log step — map them to the closest valid type.
|
|
286
|
+
const LOG_TYPE_ALIASES = { blocker: 'challenge', completion: 'progress' };
|
|
287
|
+
const normalizeLogType = (lt) => (lt ? (LOG_TYPE_ALIASES[lt] || lt) : lt);
|
|
288
|
+
|
|
257
289
|
const updateTaskDefinition = {
|
|
258
290
|
name: 'update_task',
|
|
259
291
|
description:
|
|
@@ -275,8 +307,9 @@ const updateTaskDefinition = {
|
|
|
275
307
|
log_message: { type: 'string', description: 'Optional progress note' },
|
|
276
308
|
log_type: {
|
|
277
309
|
type: 'string',
|
|
278
|
-
enum: ['progress', '
|
|
279
|
-
description: "Defaults from status: blocked→challenge, others→progress."
|
|
310
|
+
enum: ['progress', 'reasoning', 'decision', 'challenge', 'comment'],
|
|
311
|
+
description: "Defaults from status: blocked→challenge, others→progress. "
|
|
312
|
+
+ "Legacy 'blocker'/'completion' are accepted and mapped to challenge/progress.",
|
|
280
313
|
},
|
|
281
314
|
release_claim: {
|
|
282
315
|
type: 'boolean',
|
|
@@ -324,7 +357,7 @@ async function updateTaskHandler(args, apiClient) {
|
|
|
324
357
|
const data = await apiClient.v1.updateTask(task_id, {
|
|
325
358
|
status,
|
|
326
359
|
log_message,
|
|
327
|
-
log_type: args.log_type,
|
|
360
|
+
log_type: normalizeLogType(args.log_type),
|
|
328
361
|
release_claim,
|
|
329
362
|
add_learning,
|
|
330
363
|
});
|
|
@@ -370,7 +403,7 @@ async function updateTaskHandler(args, apiClient) {
|
|
|
370
403
|
|
|
371
404
|
// 2. Log entry
|
|
372
405
|
if (log_message) {
|
|
373
|
-
const logType = args.log_type || STATUS_TO_LOG_TYPE[status] || 'progress';
|
|
406
|
+
const logType = normalizeLogType(args.log_type) || STATUS_TO_LOG_TYPE[status] || 'progress';
|
|
374
407
|
try {
|
|
375
408
|
const log = await apiClient.logs.addLogEntry(planId, task_id, {
|
|
376
409
|
content: log_message,
|
|
@@ -698,6 +731,13 @@ async function addLearningHandler(args, apiClient) {
|
|
|
698
731
|
plan_id: scope.plan_id,
|
|
699
732
|
node_id: scope.node_id,
|
|
700
733
|
entity_type: entry_type,
|
|
734
|
+
// The episodes endpoint only persists {content,name,plan_id,node_id,metadata};
|
|
735
|
+
// the top-level entry_type/source_description were silently dropped. Carry
|
|
736
|
+
// the categorization in metadata so it survives on the stored episode.
|
|
737
|
+
metadata: {
|
|
738
|
+
entry_type,
|
|
739
|
+
source_description: source_description || 'BDI add_learning',
|
|
740
|
+
},
|
|
701
741
|
});
|
|
702
742
|
return formatResponse({
|
|
703
743
|
as_of: asOf(),
|