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 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(task_id, depth=4) periodically to refresh as work progresses
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-planner-mcp",
3
- "version": "1.5.5",
3
+ "version": "1.5.8",
4
4
  "description": "MCP server for AgentPlanner — AI agent orchestration with planning, dependencies, knowledge graphs, and human oversight",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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
- task_id: { type: 'string' },
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: ['task_id'],
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: task_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 = safeArray(v.facts || v);
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
- return formatResponse({ as_of: asOf(), type, results: result || {} });
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
  }