agent-planner-mcp 0.9.1 → 1.1.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/AGENT_GUIDE.md +73 -10
- package/README.md +74 -50
- package/SKILL.md +125 -17
- package/package.json +2 -1
- package/src/api-client.js +46 -2
- package/src/setup.js +5 -2
- package/src/tools/bdi/beliefs.js +128 -5
- package/src/tools/bdi/desires.js +104 -3
- package/src/tools/bdi/intentions.js +1077 -4
- package/src/tools/bdi/utility.js +1 -1
package/src/tools/bdi/beliefs.js
CHANGED
|
@@ -30,6 +30,20 @@ const briefingDefinition = {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
async function briefingHandler(args, apiClient) {
|
|
33
|
+
try {
|
|
34
|
+
const response = await apiClient.axiosInstance.get('/agent/briefing', {
|
|
35
|
+
params: {
|
|
36
|
+
scope: args.scope,
|
|
37
|
+
goal_id: args.goal_id,
|
|
38
|
+
plan_id: args.plan_id,
|
|
39
|
+
recent_window_hours: args.recent_window_hours,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
return formatResponse(response.data);
|
|
43
|
+
} catch {
|
|
44
|
+
// Fall back to the pre-facade fan-out for self-hosted older APIs.
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
const recentHours = typeof args.recent_window_hours === 'number' ? args.recent_window_hours : 24;
|
|
34
48
|
const recentSinceMs = Date.now() - recentHours * 3600 * 1000;
|
|
35
49
|
|
|
@@ -252,6 +266,19 @@ async function goalStateHandler(args, apiClient) {
|
|
|
252
266
|
direct_downstream_count: t.direct_downstream_count || 0,
|
|
253
267
|
}));
|
|
254
268
|
|
|
269
|
+
// Surface the goal's linked plans + tasks. The underlying GET /goals/:id
|
|
270
|
+
// already returns the `links` array; the previous handler discarded it,
|
|
271
|
+
// so quality.actionability could report "26 plans linked" while the
|
|
272
|
+
// response refused to name a single one. Callers had no read-side way
|
|
273
|
+
// to enumerate the plans served by a goal short of REST.
|
|
274
|
+
const links = safeArray(goal.links);
|
|
275
|
+
const linked_plans = links
|
|
276
|
+
.filter((l) => (l.linkedType || l.linked_type) === 'plan')
|
|
277
|
+
.map((l) => ({ id: l.linkedId || l.linked_id, link_id: l.id }));
|
|
278
|
+
const linked_tasks = links
|
|
279
|
+
.filter((l) => (l.linkedType || l.linked_type) === 'task')
|
|
280
|
+
.map((l) => ({ id: l.linkedId || l.linked_id, link_id: l.id }));
|
|
281
|
+
|
|
255
282
|
return formatResponse({
|
|
256
283
|
as_of: asOf(),
|
|
257
284
|
goal: {
|
|
@@ -261,6 +288,8 @@ async function goalStateHandler(args, apiClient) {
|
|
|
261
288
|
owner_id: goal.ownerId || goal.owner_id, success_criteria: goal.successCriteria || goal.success_criteria,
|
|
262
289
|
promoted_at: goal.promotedAt || goal.promoted_at,
|
|
263
290
|
},
|
|
291
|
+
linked_plans,
|
|
292
|
+
linked_tasks,
|
|
264
293
|
quality: {
|
|
265
294
|
score: quality.score, dimensions: quality.dimensions,
|
|
266
295
|
suggestions: quality.suggestions, last_assessed_at: quality.as_of,
|
|
@@ -348,6 +377,79 @@ async function recallKnowledgeHandler(args, apiClient) {
|
|
|
348
377
|
return formatResponse(out);
|
|
349
378
|
}
|
|
350
379
|
|
|
380
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
381
|
+
// list_plans — list workspace plans with optional filters.
|
|
382
|
+
// Counterpart to list_goals; previously the only ways to find a plan
|
|
383
|
+
// were knowing the UUID a priori, parsing briefing.recent_activity,
|
|
384
|
+
// or calling task_context on a known node — all bad.
|
|
385
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
const listPlansDefinition = {
|
|
388
|
+
name: 'list_plans',
|
|
389
|
+
description:
|
|
390
|
+
'List plans with optional filters by status, visibility, or text query. ' +
|
|
391
|
+
'Returns id, title, status, visibility, last update, and link counts so ' +
|
|
392
|
+
'you can pick a plan to operate on without round-tripping briefing.',
|
|
393
|
+
inputSchema: {
|
|
394
|
+
type: 'object',
|
|
395
|
+
properties: {
|
|
396
|
+
filter: {
|
|
397
|
+
type: 'object',
|
|
398
|
+
properties: {
|
|
399
|
+
status: { type: 'array', items: { type: 'string' } },
|
|
400
|
+
visibility: { type: 'array', items: { type: 'string', enum: ['private', 'unlisted', 'public'] } },
|
|
401
|
+
query: { type: 'string', description: 'Substring match on title (case-insensitive)' },
|
|
402
|
+
limit: { type: 'integer', default: 50 },
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
async function listPlansHandler(args, apiClient) {
|
|
410
|
+
const filter = args.filter || {};
|
|
411
|
+
try {
|
|
412
|
+
const raw = await apiClient.plans.getPlans();
|
|
413
|
+
let plans = Array.isArray(raw) ? raw : safeArray(raw.plans || raw);
|
|
414
|
+
|
|
415
|
+
if (filter.status?.length) plans = plans.filter((p) => filter.status.includes(p.status));
|
|
416
|
+
if (filter.visibility?.length) plans = plans.filter((p) => filter.visibility.includes(p.visibility));
|
|
417
|
+
if (filter.query) {
|
|
418
|
+
const q = filter.query.toLowerCase();
|
|
419
|
+
plans = plans.filter((p) => (p.title || '').toLowerCase().includes(q));
|
|
420
|
+
}
|
|
421
|
+
plans = plans.slice(0, filter.limit || 50);
|
|
422
|
+
|
|
423
|
+
const summary = plans.reduce(
|
|
424
|
+
(acc, p) => {
|
|
425
|
+
acc[p.status] = (acc[p.status] || 0) + 1;
|
|
426
|
+
acc.total += 1;
|
|
427
|
+
return acc;
|
|
428
|
+
},
|
|
429
|
+
{ total: 0 },
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
return formatResponse({
|
|
433
|
+
as_of: asOf(),
|
|
434
|
+
summary,
|
|
435
|
+
plans: plans.map((p) => ({
|
|
436
|
+
id: p.id,
|
|
437
|
+
title: p.title,
|
|
438
|
+
status: p.status,
|
|
439
|
+
visibility: p.visibility,
|
|
440
|
+
owner_id: p.owner_id || p.ownerId,
|
|
441
|
+
updated_at: p.updated_at || p.updatedAt,
|
|
442
|
+
// Surface progress + tether info when the API decorates the rows;
|
|
443
|
+
// listPlans on the v2 API now bulk-loads stats + goal_tethers.
|
|
444
|
+
progress: p.progress ?? p.stats?.percentage,
|
|
445
|
+
goal_tethers: p.goal_tethers,
|
|
446
|
+
})),
|
|
447
|
+
});
|
|
448
|
+
} catch (err) {
|
|
449
|
+
return errorResponse('upstream_unavailable', `list_plans failed: ${err.response?.data?.error || err.message}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
351
453
|
// ─────────────────────────────────────────────────────────────────────────
|
|
352
454
|
// search — universal text search.
|
|
353
455
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -376,13 +478,32 @@ const searchDefinition = {
|
|
|
376
478
|
|
|
377
479
|
async function searchHandler(args, apiClient) {
|
|
378
480
|
const { query, scope = 'global', scope_id, filters = {} } = args;
|
|
481
|
+
const limit = filters.limit || 20;
|
|
379
482
|
try {
|
|
380
483
|
let result;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
484
|
+
// Map MCP scopes onto the actual api-client surface. The handler
|
|
485
|
+
// previously called apiClient.search.{global,plans,inPlan,inNode}
|
|
486
|
+
// — none of those methods exist, so every invocation 500'd. The
|
|
487
|
+
// real api-client exposes globalSearch + searchPlan only; for
|
|
488
|
+
// 'plans' and 'node' we filter the global result client-side
|
|
489
|
+
// instead of hitting a (non-existent) per-bucket endpoint.
|
|
490
|
+
if (scope === 'plan') {
|
|
491
|
+
if (!scope_id) return errorResponse('invalid_arg', 'search scope=plan requires scope_id');
|
|
492
|
+
result = await apiClient.search.searchPlan(scope_id, query);
|
|
493
|
+
} else {
|
|
494
|
+
const global = await apiClient.search.globalSearch(query);
|
|
495
|
+
const all = Array.isArray(global?.results) ? global.results : [];
|
|
496
|
+
const matchScope = (r) => {
|
|
497
|
+
if (scope === 'global') return true;
|
|
498
|
+
if (scope === 'plans') return r.type === 'plan';
|
|
499
|
+
if (scope === 'node') return r.type === 'node' && (!scope_id || r.plan_id === scope_id);
|
|
500
|
+
return true;
|
|
501
|
+
};
|
|
502
|
+
const matchType = (r) => !filters.type || r.type === filters.type;
|
|
503
|
+
const matchStatus = (r) => !filters.status || r.status === filters.status;
|
|
504
|
+
const filtered = all.filter((r) => matchScope(r) && matchType(r) && matchStatus(r)).slice(0, limit);
|
|
505
|
+
result = { results: filtered, count: filtered.length, query, scope };
|
|
506
|
+
}
|
|
386
507
|
return formatResponse({ as_of: asOf(), ...(result || {}) });
|
|
387
508
|
} catch (err) {
|
|
388
509
|
return errorResponse('upstream_unavailable', `Search failed: ${err.response?.data?.error || err.message}`);
|
|
@@ -437,6 +558,7 @@ module.exports = {
|
|
|
437
558
|
taskContextDefinition,
|
|
438
559
|
goalStateDefinition,
|
|
439
560
|
recallKnowledgeDefinition,
|
|
561
|
+
listPlansDefinition,
|
|
440
562
|
searchDefinition,
|
|
441
563
|
planAnalysisDefinition,
|
|
442
564
|
],
|
|
@@ -445,6 +567,7 @@ module.exports = {
|
|
|
445
567
|
task_context: taskContextHandler,
|
|
446
568
|
goal_state: goalStateHandler,
|
|
447
569
|
recall_knowledge: recallKnowledgeHandler,
|
|
570
|
+
list_plans: listPlansHandler,
|
|
448
571
|
search: searchHandler,
|
|
449
572
|
plan_analysis: planAnalysisHandler,
|
|
450
573
|
},
|
package/src/tools/bdi/desires.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* BDI desires — goal management.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* link/unlink and achiever changes)
|
|
4
|
+
* 3 tools: list_goals (with health rollup), update_goal (atomic, subsumes
|
|
5
|
+
* link/unlink and achiever changes), derive_subgoal (propose a sub-goal
|
|
6
|
+
* under an existing parent; mandatory parent — top-level goals stay UI-only).
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
const { asOf, formatResponse, errorResponse, safeArray } = require('./_shared');
|
|
@@ -154,10 +155,110 @@ async function updateGoalHandler(args, apiClient) {
|
|
|
154
155
|
return formatResponse({ as_of: asOf(), goal_id, applied_changes: applied, failures, goal });
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
159
|
+
// derive_subgoal — propose a sub-goal under an existing parent.
|
|
160
|
+
// Top-level goals stay UI-only (strategic direction is human-set).
|
|
161
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
const VALID_GOAL_TYPES = ['outcome', 'constraint', 'metric', 'principle'];
|
|
164
|
+
const VALID_STATUSES = ['draft', 'active', 'achieved', 'paused', 'abandoned', 'archived'];
|
|
165
|
+
|
|
166
|
+
const deriveSubgoalDefinition = {
|
|
167
|
+
name: 'derive_subgoal',
|
|
168
|
+
description:
|
|
169
|
+
"Propose a sub-goal under an existing parent goal. parent_goal_id is " +
|
|
170
|
+
"mandatory — agents cannot create top-level goals (strategic direction is " +
|
|
171
|
+
"human-set). Defaults to status='active' for human-directed creation; pass " +
|
|
172
|
+
"status='draft' for autonomous loops so a human can review before promotion. " +
|
|
173
|
+
"Drafts surface in the dashboard pending queue.",
|
|
174
|
+
inputSchema: {
|
|
175
|
+
type: 'object',
|
|
176
|
+
properties: {
|
|
177
|
+
parent_goal_id: {
|
|
178
|
+
type: 'string',
|
|
179
|
+
description: "Required. The parent goal this sub-goal contributes to.",
|
|
180
|
+
},
|
|
181
|
+
title: { type: 'string' },
|
|
182
|
+
description: { type: 'string', description: "Optional extended description, appended after rationale." },
|
|
183
|
+
rationale: {
|
|
184
|
+
type: 'string',
|
|
185
|
+
description: "Why this sub-goal is needed to achieve the parent. Becomes the description; surfaces in human review.",
|
|
186
|
+
},
|
|
187
|
+
type: {
|
|
188
|
+
type: 'string',
|
|
189
|
+
enum: VALID_GOAL_TYPES,
|
|
190
|
+
default: 'outcome',
|
|
191
|
+
},
|
|
192
|
+
status: {
|
|
193
|
+
type: 'string',
|
|
194
|
+
enum: VALID_STATUSES,
|
|
195
|
+
default: 'active',
|
|
196
|
+
description: "Default 'active' for human-directed creation. Pass 'draft' when acting autonomously without explicit user direction.",
|
|
197
|
+
},
|
|
198
|
+
success_criteria: {
|
|
199
|
+
type: 'array',
|
|
200
|
+
items: { type: 'string' },
|
|
201
|
+
description: "Concrete, observable conditions that mark this sub-goal achieved.",
|
|
202
|
+
},
|
|
203
|
+
priority: { type: 'integer', default: 0 },
|
|
204
|
+
},
|
|
205
|
+
required: ['parent_goal_id', 'title', 'rationale'],
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
async function deriveSubgoalHandler(args, apiClient) {
|
|
210
|
+
const { parent_goal_id, title, description, rationale, type = 'outcome', status = 'active', success_criteria, priority } = args;
|
|
211
|
+
|
|
212
|
+
// Verify parent exists and inherit organization scope.
|
|
213
|
+
let parent;
|
|
214
|
+
try {
|
|
215
|
+
parent = await apiClient.goals.get(parent_goal_id);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return errorResponse('not_found', `Parent goal ${parent_goal_id} not found or not accessible: ${err.message}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Compose description: rationale is primary; optional description appended.
|
|
221
|
+
const composedDescription = description
|
|
222
|
+
? `${rationale}\n\n${description}`
|
|
223
|
+
: rationale;
|
|
224
|
+
|
|
225
|
+
const payload = {
|
|
226
|
+
title,
|
|
227
|
+
description: composedDescription,
|
|
228
|
+
type,
|
|
229
|
+
status,
|
|
230
|
+
parentGoalId: parent_goal_id,
|
|
231
|
+
organizationId: parent.organization_id || parent.organizationId || undefined,
|
|
232
|
+
};
|
|
233
|
+
if (success_criteria) payload.successCriteria = { criteria: success_criteria };
|
|
234
|
+
if (typeof priority === 'number') payload.priority = priority;
|
|
235
|
+
|
|
236
|
+
let goal;
|
|
237
|
+
try {
|
|
238
|
+
goal = await apiClient.goals.create(payload);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
const upstream = err.response?.data?.error || err.message;
|
|
241
|
+
return errorResponse('create_failed', `Failed to create sub-goal: ${upstream}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return formatResponse({
|
|
245
|
+
as_of: asOf(),
|
|
246
|
+
goal_id: goal.id,
|
|
247
|
+
parent_goal_id,
|
|
248
|
+
title: goal.title,
|
|
249
|
+
status: goal.status,
|
|
250
|
+
is_draft: goal.status === 'draft',
|
|
251
|
+
next_step: goal.status === 'draft'
|
|
252
|
+
? "Sub-goal created as draft. It will surface in the dashboard pending queue for human review. Promote via update_goal({status: 'active'}) once approved."
|
|
253
|
+
: "Sub-goal active. Link plans to it via update_goal({add_linked_plans: [...]}).",
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
157
257
|
module.exports = {
|
|
158
|
-
definitions: [listGoalsDefinition, updateGoalDefinition],
|
|
258
|
+
definitions: [listGoalsDefinition, updateGoalDefinition, deriveSubgoalDefinition],
|
|
159
259
|
handlers: {
|
|
160
260
|
list_goals: listGoalsHandler,
|
|
161
261
|
update_goal: updateGoalHandler,
|
|
262
|
+
derive_subgoal: deriveSubgoalHandler,
|
|
162
263
|
},
|
|
163
264
|
};
|