agent-planner-mcp 1.5.5 → 1.5.8
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/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/tools/bdi/beliefs.js +63 -6
package/SKILL.md
CHANGED
|
@@ -123,7 +123,7 @@ The `update_task` call is atomic — status change, log entry, claim release, an
|
|
|
123
123
|
|
|
124
124
|
```
|
|
125
125
|
1. claim_next_task(scope={ plan_id }, ttl_minutes=30) → exclusive ownership
|
|
126
|
-
2. task_context(
|
|
126
|
+
2. task_context(node_id, depth=4) periodically to refresh as work progresses
|
|
127
127
|
3. update_task(...) for state transitions
|
|
128
128
|
4. release_task(task_id, message='handoff to teammate') for explicit handoff
|
|
129
129
|
```
|
package/package.json
CHANGED
package/src/tools/bdi/beliefs.js
CHANGED
|
@@ -7,6 +7,21 @@
|
|
|
7
7
|
|
|
8
8
|
const { asOf, formatResponse, errorResponse, safeArray, isV1Unavailable, planUrl } = require('./_shared');
|
|
9
9
|
|
|
10
|
+
// A Graphiti fact is superseded once it has an `expired_at`, or an `invalid_at`
|
|
11
|
+
// that is in the past — the temporal graph has replaced it with a newer truth.
|
|
12
|
+
// recall_knowledge used to return these inline with current facts, undistinguished,
|
|
13
|
+
// so an agent could act on stale knowledge. Tag each fact `status` and sort
|
|
14
|
+
// current-first so the valid facts read first.
|
|
15
|
+
function annotateFacts(facts, nowMs = Date.now()) {
|
|
16
|
+
const list = safeArray(facts).map((f) => {
|
|
17
|
+
const invalidMs = f.invalid_at ? new Date(f.invalid_at).getTime() : null;
|
|
18
|
+
const superseded = Boolean(f.expired_at) || (invalidMs != null && invalidMs <= nowMs);
|
|
19
|
+
return { ...f, status: superseded ? 'superseded' : 'current' };
|
|
20
|
+
});
|
|
21
|
+
list.sort((a, b) => (a.status === b.status ? 0 : a.status === 'current' ? -1 : 1));
|
|
22
|
+
return list;
|
|
23
|
+
}
|
|
24
|
+
|
|
10
25
|
// ─────────────────────────────────────────────────────────────────────────
|
|
11
26
|
// briefing — bundled mission control state. Replaces 4 round trips.
|
|
12
27
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -197,18 +212,26 @@ const taskContextDefinition = {
|
|
|
197
212
|
inputSchema: {
|
|
198
213
|
type: 'object',
|
|
199
214
|
properties: {
|
|
200
|
-
|
|
215
|
+
// `node_id` is the canonical name (matches every other tool + the skill
|
|
216
|
+
// docs). `task_id` is kept as an accepted alias for back-compat.
|
|
217
|
+
node_id: { type: 'string', description: 'The task/node id to load context for.' },
|
|
218
|
+
task_id: { type: 'string', description: 'Alias for node_id (back-compat).' },
|
|
201
219
|
depth: { type: 'integer', enum: [1, 2, 3, 4], default: 2 },
|
|
202
220
|
token_budget: { type: 'integer', default: 0 },
|
|
203
221
|
},
|
|
204
|
-
required
|
|
222
|
+
// Neither is strictly required at the schema level because either name is
|
|
223
|
+
// accepted; the handler validates that one was supplied with a clear error.
|
|
205
224
|
},
|
|
206
225
|
};
|
|
207
226
|
|
|
208
227
|
async function taskContextHandler(args, apiClient) {
|
|
209
|
-
const { task_id, depth = 2, token_budget = 0 } = args;
|
|
228
|
+
const { node_id, task_id, depth = 2, token_budget = 0 } = args;
|
|
229
|
+
const nodeId = node_id || task_id;
|
|
230
|
+
if (!nodeId) {
|
|
231
|
+
return errorResponse('invalid_arg', 'task_context requires node_id (the task/node id).');
|
|
232
|
+
}
|
|
210
233
|
const params = new URLSearchParams({
|
|
211
|
-
node_id:
|
|
234
|
+
node_id: nodeId,
|
|
212
235
|
depth: String(depth),
|
|
213
236
|
token_budget: String(token_budget),
|
|
214
237
|
log_limit: '10',
|
|
@@ -335,6 +358,8 @@ const recallKnowledgeDefinition = {
|
|
|
335
358
|
description:
|
|
336
359
|
"Universal knowledge graph query. Returns facts, entities, recent episodes, " +
|
|
337
360
|
"and contradictions in one shape. Use result_kind to control payload size. " +
|
|
361
|
+
"Each fact carries status: 'current' or 'superseded' (the graph has since " +
|
|
362
|
+
"replaced it) — facts are sorted current-first; prefer current facts. " +
|
|
338
363
|
"Replaces recall_knowledge legacy + find_entities + get_recent_episodes + check_contradictions.",
|
|
339
364
|
inputSchema: {
|
|
340
365
|
type: 'object',
|
|
@@ -355,6 +380,9 @@ const recallKnowledgeDefinition = {
|
|
|
355
380
|
|
|
356
381
|
async function recallKnowledgeHandler(args, apiClient) {
|
|
357
382
|
const { query, scope = {}, since, entry_type = 'all', result_kind = 'all', max_results = 10, include_contradictions = false } = args;
|
|
383
|
+
if (!query || !String(query).trim()) {
|
|
384
|
+
return errorResponse('invalid_arg', 'recall_knowledge requires a non-empty query string');
|
|
385
|
+
}
|
|
358
386
|
|
|
359
387
|
// v1 facade: one server-side call replaces the 4-endpoint fan-out below.
|
|
360
388
|
if (apiClient.v1) {
|
|
@@ -362,6 +390,11 @@ async function recallKnowledgeHandler(args, apiClient) {
|
|
|
362
390
|
const data = await apiClient.v1.knowledgeSearch({
|
|
363
391
|
query, since, entry_type, result_kind, max_results, include_contradictions, ...scope,
|
|
364
392
|
});
|
|
393
|
+
if (data && Array.isArray(data.facts)) {
|
|
394
|
+
data.facts = annotateFacts(data.facts);
|
|
395
|
+
const superseded = data.facts.filter((f) => f.status === 'superseded').length;
|
|
396
|
+
data.meta = { ...(data.meta || {}), superseded_fact_count: superseded };
|
|
397
|
+
}
|
|
365
398
|
return formatResponse(data);
|
|
366
399
|
} catch (err) {
|
|
367
400
|
if (!isV1Unavailable(err)) {
|
|
@@ -399,7 +432,7 @@ async function recallKnowledgeHandler(args, apiClient) {
|
|
|
399
432
|
return;
|
|
400
433
|
}
|
|
401
434
|
const v = s.value;
|
|
402
|
-
if (key === 'facts') out.facts =
|
|
435
|
+
if (key === 'facts') out.facts = annotateFacts(v.facts || v);
|
|
403
436
|
if (key === 'entities') out.entities = safeArray(v.entities || v);
|
|
404
437
|
if (key === 'episodes') {
|
|
405
438
|
let eps = safeArray(v.episodes?.episodes || v.episodes || v);
|
|
@@ -415,6 +448,7 @@ async function recallKnowledgeHandler(args, apiClient) {
|
|
|
415
448
|
if (key === 'contradictions') out.contradictions = v;
|
|
416
449
|
});
|
|
417
450
|
|
|
451
|
+
out.meta.superseded_fact_count = out.facts.filter((f) => f.status === 'superseded').length;
|
|
418
452
|
return formatResponse(out);
|
|
419
453
|
}
|
|
420
454
|
|
|
@@ -531,6 +565,9 @@ const searchDefinition = {
|
|
|
531
565
|
|
|
532
566
|
async function searchHandler(args, apiClient) {
|
|
533
567
|
const { query, scope = 'global', scope_id, filters = {} } = args;
|
|
568
|
+
if (!query || !String(query).trim()) {
|
|
569
|
+
return errorResponse('invalid_arg', 'search requires a non-empty query string');
|
|
570
|
+
}
|
|
534
571
|
const limit = filters.limit || 20;
|
|
535
572
|
try {
|
|
536
573
|
let result;
|
|
@@ -593,8 +630,17 @@ const planAnalysisDefinition = {
|
|
|
593
630
|
},
|
|
594
631
|
};
|
|
595
632
|
|
|
633
|
+
const PLAN_ANALYSIS_TYPES = ['impact', 'critical_path', 'bottlenecks', 'coherence'];
|
|
634
|
+
|
|
596
635
|
async function planAnalysisHandler(args, apiClient) {
|
|
597
636
|
const { plan_id, type, node_id, scenario } = args;
|
|
637
|
+
// The hosted MCP transport does not enforce `required`, so a missing/unknown
|
|
638
|
+
// `type` would otherwise fall through every branch and return an empty
|
|
639
|
+
// `results: {}` with no explanation. Validate explicitly with a clear error.
|
|
640
|
+
if (!plan_id) return errorResponse('invalid_arg', 'plan_analysis requires plan_id');
|
|
641
|
+
if (!type || !PLAN_ANALYSIS_TYPES.includes(type)) {
|
|
642
|
+
return errorResponse('invalid_arg', `plan_analysis requires type (one of: ${PLAN_ANALYSIS_TYPES.join(', ')})`);
|
|
643
|
+
}
|
|
598
644
|
try {
|
|
599
645
|
let result;
|
|
600
646
|
if (type === 'critical_path') {
|
|
@@ -608,7 +654,18 @@ async function planAnalysisHandler(args, apiClient) {
|
|
|
608
654
|
} else if (type === 'coherence') {
|
|
609
655
|
result = await apiClient.coherence.runCheck(plan_id);
|
|
610
656
|
}
|
|
611
|
-
|
|
657
|
+
const payload = { as_of: asOf(), type, results: result || {} };
|
|
658
|
+
// Critical-path / bottleneck analysis is driven by `blocks` dependency
|
|
659
|
+
// edges. A flat (edgeless) plan returns empty here — without a hint the
|
|
660
|
+
// agent can't tell "nothing blocking" from "this plan has no edges at all".
|
|
661
|
+
const emptyCriticalPath = type === 'critical_path' && !(result?.nodes?.length);
|
|
662
|
+
const emptyBottlenecks = type === 'bottlenecks'
|
|
663
|
+
&& !((Array.isArray(result) ? result : result?.bottlenecks)?.length);
|
|
664
|
+
if (emptyCriticalPath || emptyBottlenecks) {
|
|
665
|
+
payload.note = 'No dependency edges drive this result — the plan may be flat (no blocks edges). '
|
|
666
|
+
+ 'Use task_context (depth 2+) to read its tasks, or add dependencies with link_intentions.';
|
|
667
|
+
}
|
|
668
|
+
return formatResponse(payload);
|
|
612
669
|
} catch (err) {
|
|
613
670
|
return errorResponse('upstream_unavailable', `plan_analysis failed: ${err.response?.data?.error || err.message}`);
|
|
614
671
|
}
|