agent-planner-mcp 1.4.0 → 1.5.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 +4 -3
- package/SKILL.md +14 -5
- package/package.json +1 -1
- package/src/tools/bdi/intentions.js +121 -15
- package/src/tools/bdi/utility.js +2 -0
package/AGENT_GUIDE.md
CHANGED
|
@@ -42,13 +42,14 @@ get_started()
|
|
|
42
42
|
### Intentions — creation (v1.0)
|
|
43
43
|
| Tool | When |
|
|
44
44
|
|---|---|
|
|
45
|
-
| `form_intention` | Create plan + initial tree under a goal, atomically |
|
|
45
|
+
| `form_intention` | Create plan + initial tree under a goal, atomically. Declare order inline with `ref`/`depends_on` (→ `blocks` edges); warns `created_without_dependencies` |
|
|
46
46
|
| `extend_intention` | Add children under existing parent (lightweight, no queue) |
|
|
47
47
|
| `propose_research_chain` | RPI triple with 2 blocking edges in one call |
|
|
48
48
|
|
|
49
49
|
### Intentions — structural mutation (v1.0)
|
|
50
50
|
| Tool | When |
|
|
51
51
|
|---|---|
|
|
52
|
+
| `list_plans` | List plans (filter by status / visibility / workspace) |
|
|
52
53
|
| `update_plan` | Edit plan title/description/status/visibility/metadata |
|
|
53
54
|
| `update_node` | Edit any node property except status |
|
|
54
55
|
| `move_node` | Reparent within plan; cycle-safe |
|
|
@@ -148,9 +149,9 @@ Never use `add_learning(entry_type='decision')` to fake a decision queue. `queue
|
|
|
148
149
|
## Atomic patterns to remember
|
|
149
150
|
|
|
150
151
|
- `update_task` does status + log + claim release + learning in one call. Don't decompose.
|
|
151
|
-
- `claim_next_task` does suggest + claim + context. Don't decompose.
|
|
152
|
+
- `claim_next_task` does suggest + claim + context. Don't decompose. Fails closed — an empty result is structured: `reason: no_work_in_scope` (nothing left) vs `blocked_on_dep` (work remains but all of it is dependency-blocked). Don't treat empty as "done" without checking `reason`.
|
|
152
153
|
- `briefing` does goals + decisions + tasks + activity + recommendation. Don't decompose.
|
|
153
|
-
- `form_intention` creates plan + tree atomically. Don't trickle node-by-node.
|
|
154
|
+
- `form_intention` creates plan + tree atomically — and declares execution order inline via `ref`/`depends_on` (don't ship a bare hierarchy with no edges). Don't trickle node-by-node.
|
|
154
155
|
- `share_plan` does visibility + add + remove in one call. Don't fan out.
|
|
155
156
|
|
|
156
157
|
## Output discipline
|
package/SKILL.md
CHANGED
|
@@ -54,11 +54,12 @@ AgentPlanner exposes a **BDI-aligned** surface — Beliefs (state queries), Desi
|
|
|
54
54
|
- `add_learning` — record a knowledge episode for future recall
|
|
55
55
|
|
|
56
56
|
**Creation (v1.0):**
|
|
57
|
-
- `form_intention` — create a plan + initial phase/task tree under a goal, atomically
|
|
57
|
+
- `form_intention` — create a plan + initial phase/task tree under a goal, atomically. **Declare execution order inline:** give nodes a `ref` and list prerequisites in `depends_on` (refs or titles) to create `blocks` edges in the same call. Returns a `structure` summary and warns `created_without_dependencies` when a multi-task plan has no edges — don't ship a bare hierarchy with no executable ordering. Every plan it creates is provenance-stamped (`created_by: agent-planner-mcp@<version>`) for version-drift diagnosis.
|
|
58
58
|
- `extend_intention` — add children under an existing phase or task (lightweight, no decision-queue gate)
|
|
59
59
|
- `propose_research_chain` — Research → Plan → Implement triple with two blocking edges, in one call
|
|
60
60
|
|
|
61
61
|
**Structural mutation (v1.0):**
|
|
62
|
+
- `list_plans` — list plans (filter by status / visibility / workspace)
|
|
62
63
|
- `update_plan` — edit any plan property (title, description, status, visibility, metadata)
|
|
63
64
|
- `update_node` — edit any node property except status (status routes through `update_task`)
|
|
64
65
|
- `move_node` — reparent within the same plan; cycle-safe
|
|
@@ -85,7 +86,7 @@ A Workspace is a folder under an Organization that owns goals + plans — a grou
|
|
|
85
86
|
|
|
86
87
|
### Utility
|
|
87
88
|
|
|
88
|
-
- `get_started` — dynamic reference; call this if you're new to AgentPlanner
|
|
89
|
+
- `get_started` — dynamic reference; call this if you're new to AgentPlanner. Reports `mcp_version` so you can self-report your build (diagnose version drift across OpenClaw / Claude Code / local checkouts).
|
|
89
90
|
|
|
90
91
|
## Canonical workflows
|
|
91
92
|
|
|
@@ -138,6 +139,8 @@ claim_next_task({ scope: { plan_id }, dry_run: true })
|
|
|
138
139
|
claim_next_task({ scope: { plan_id } }) // dry_run defaults to false
|
|
139
140
|
```
|
|
140
141
|
|
|
142
|
+
**Fails closed.** When nothing is claimable, `claim_next_task` never hands back a dependency-blind task — it returns a structured no-work result whose `reason` distinguishes `no_work_in_scope` (nothing left to do) from `blocked_on_dep` (work remains, but every remaining task is blocked on an incomplete dependency). Check `reason` before treating an empty result as "done."
|
|
143
|
+
|
|
141
144
|
### Proposing subtasks for human approval (v0.9.1+)
|
|
142
145
|
|
|
143
146
|
For high-touch proposals (entire new directions, structural changes the human should review before they materialize), use `queue_decision` with `proposed_subtasks` — tasks only get created on `resolve_decision({action: 'approve'})`.
|
|
@@ -176,11 +179,17 @@ form_intention({
|
|
|
176
179
|
title: 'Ship new auth flow',
|
|
177
180
|
rationale: 'User-requested plan to migrate auth to passkeys',
|
|
178
181
|
tree: [
|
|
179
|
-
{ node_type: 'phase', title: 'Discovery', children: [
|
|
180
|
-
|
|
182
|
+
{ node_type: 'phase', title: 'Discovery', children: [
|
|
183
|
+
{ ref: 'research', title: 'Research passkey libraries', task_mode: 'research' },
|
|
184
|
+
]},
|
|
185
|
+
{ node_type: 'phase', title: 'Implementation', children: [
|
|
186
|
+
{ title: 'Implement passkey flow', task_mode: 'implement', depends_on: ['research'] },
|
|
187
|
+
]},
|
|
181
188
|
]
|
|
182
189
|
})
|
|
183
|
-
//
|
|
190
|
+
// Active, with a `blocks` edge (research → implement) created inline from depends_on.
|
|
191
|
+
// Response carries a `structure` summary; a multi-task plan with zero edges would
|
|
192
|
+
// return created_without_dependencies + a warning instead.
|
|
184
193
|
```
|
|
185
194
|
|
|
186
195
|
```
|
package/package.json
CHANGED
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const { asOf, formatResponse, errorResponse, isV1Unavailable } = require('./_shared');
|
|
17
|
+
const { version: PKG_VERSION } = require('../../../package.json');
|
|
18
|
+
|
|
19
|
+
// Provenance tag stamped onto every plan this server creates, so a plan stays
|
|
20
|
+
// debuggable later even if this MCP build is stale relative to the API.
|
|
21
|
+
const CLIENT_TAG = `agent-planner-mcp@${PKG_VERSION}`;
|
|
17
22
|
|
|
18
23
|
// ─────────────────────────────────────────────────────────────────────────
|
|
19
24
|
// queue_decision — real decision queue. Replaces add_learning workaround.
|
|
@@ -482,8 +487,8 @@ async function claimNextTaskHandler(args, apiClient) {
|
|
|
482
487
|
if (!fresh) {
|
|
483
488
|
try {
|
|
484
489
|
const myTasks = await apiClient.users.getMyTasks({ plan_id });
|
|
485
|
-
|
|
486
|
-
if (plan_id) tasks.filter((t) => t.plan_id === plan_id);
|
|
490
|
+
let tasks = (myTasks.tasks || myTasks || []).filter((t) => t.status === 'in_progress');
|
|
491
|
+
if (plan_id) tasks = tasks.filter((t) => t.plan_id === plan_id);
|
|
487
492
|
if (tasks[0]) {
|
|
488
493
|
chosen = tasks[0];
|
|
489
494
|
source = 'resume_in_progress';
|
|
@@ -491,20 +496,49 @@ async function claimNextTaskHandler(args, apiClient) {
|
|
|
491
496
|
} catch {}
|
|
492
497
|
}
|
|
493
498
|
|
|
494
|
-
// 2.
|
|
499
|
+
// 2. Dependency-aware suggestion via /context/suggest — the real endpoint
|
|
500
|
+
// (only ready, unclaimed tasks, in plan order). Track reachability so we
|
|
501
|
+
// can fail closed below: an empty result from a reachable endpoint means
|
|
502
|
+
// remaining work is dep-blocked, NOT "grab any not_started task".
|
|
503
|
+
let suggestReachable = false;
|
|
495
504
|
if (!chosen && plan_id) {
|
|
496
505
|
try {
|
|
497
506
|
const params = new URLSearchParams({ plan_id, limit: '1' });
|
|
498
|
-
const r = await apiClient.axiosInstance.get(`/
|
|
499
|
-
|
|
507
|
+
const r = await apiClient.axiosInstance.get(`/context/suggest?${params}`);
|
|
508
|
+
suggestReachable = true;
|
|
509
|
+
const suggested = (r.data?.suggestions || r.data?.tasks || r.data || [])[0];
|
|
500
510
|
if (suggested) {
|
|
501
511
|
chosen = suggested;
|
|
502
512
|
source = 'suggest_next_tasks';
|
|
503
513
|
}
|
|
514
|
+
} catch {
|
|
515
|
+
// Endpoint unreachable (pre-suggest self-hosted API). Leave
|
|
516
|
+
// suggestReachable=false so the last-resort blind pick can run.
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// 3. Fail-closed gate. When the dependency-aware endpoint is reachable but
|
|
521
|
+
// returned nothing, all remaining not_started work is blocked on
|
|
522
|
+
// incomplete dependencies — mirror the backend and refuse to hand out a
|
|
523
|
+
// dep-blind task. Only the truly-unreachable case falls through to (4).
|
|
524
|
+
if (!chosen && plan_id && suggestReachable) {
|
|
525
|
+
let hasNotStarted = false;
|
|
526
|
+
try {
|
|
527
|
+
const myTasks = await apiClient.users.getMyTasks({ plan_id });
|
|
528
|
+
hasNotStarted = (myTasks.tasks || myTasks || []).some((t) => t.status === 'not_started');
|
|
504
529
|
} catch {}
|
|
530
|
+
return errorResponse(
|
|
531
|
+
'not_found',
|
|
532
|
+
hasNotStarted
|
|
533
|
+
? 'All remaining tasks are blocked on incomplete dependencies'
|
|
534
|
+
: 'No actionable task found in scope',
|
|
535
|
+
{ reason: hasNotStarted ? 'blocked_on_dep' : 'no_work_in_scope' },
|
|
536
|
+
);
|
|
505
537
|
}
|
|
506
538
|
|
|
507
|
-
//
|
|
539
|
+
// 4. Last-resort blind fallback — ONLY when the dep-aware endpoint could not
|
|
540
|
+
// be reached at all (ancient self-hosted API with neither work-sessions
|
|
541
|
+
// nor /context/suggest). Against any current backend this never runs.
|
|
508
542
|
if (!chosen) {
|
|
509
543
|
try {
|
|
510
544
|
const myTasks = await apiClient.users.getMyTasks({ plan_id });
|
|
@@ -708,10 +742,14 @@ const formIntentionDefinition = {
|
|
|
708
742
|
name: 'form_intention',
|
|
709
743
|
description:
|
|
710
744
|
"Create a plan that achieves a goal, including an initial phase/task " +
|
|
711
|
-
"tree, in one call.
|
|
712
|
-
"
|
|
713
|
-
"
|
|
714
|
-
"
|
|
745
|
+
"tree, in one call. Declare execution order inline: give nodes a `ref` and " +
|
|
746
|
+
"list prerequisite refs/titles in `depends_on` to create 'blocks' edges in " +
|
|
747
|
+
"the same call — don't ship a bare hierarchy. The response returns a " +
|
|
748
|
+
"`structure` summary and warns (`created_without_dependencies`) when a " +
|
|
749
|
+
"multi-task plan has no edges. Defaults to status='active' for " +
|
|
750
|
+
"human-directed creation; pass status='draft' for autonomous loops so a " +
|
|
751
|
+
"human can review before promotion. Drafts surface in the dashboard " +
|
|
752
|
+
"pending queue and auto-promote to active when work begins on any node.",
|
|
715
753
|
inputSchema: {
|
|
716
754
|
type: 'object',
|
|
717
755
|
properties: {
|
|
@@ -740,6 +778,12 @@ const formIntentionDefinition = {
|
|
|
740
778
|
description: { type: 'string' },
|
|
741
779
|
task_mode: { type: 'string', enum: VALID_TASK_MODES, default: 'free' },
|
|
742
780
|
agent_instructions: { type: 'string' },
|
|
781
|
+
ref: { type: 'string', description: "Optional stable key so other nodes can reference this one in depends_on. Falls back to title if omitted." },
|
|
782
|
+
depends_on: {
|
|
783
|
+
type: 'array',
|
|
784
|
+
items: { type: 'string' },
|
|
785
|
+
description: "Refs (or titles) of nodes that must complete before this one. Creates a 'blocks' edge from each prerequisite to this node.",
|
|
786
|
+
},
|
|
743
787
|
children: { type: 'array' },
|
|
744
788
|
},
|
|
745
789
|
required: ['title'],
|
|
@@ -750,7 +794,10 @@ const formIntentionDefinition = {
|
|
|
750
794
|
},
|
|
751
795
|
};
|
|
752
796
|
|
|
753
|
-
|
|
797
|
+
// `ctx` (optional) collects ref/title → id maps and depends_on intents so the
|
|
798
|
+
// caller can wire dependency edges after the whole tree exists. Omitted by
|
|
799
|
+
// callers that don't support inline deps (e.g. extend_intention).
|
|
800
|
+
async function createSubtree(apiClient, planId, parentId, children, results, ctx = null) {
|
|
754
801
|
for (const child of children || []) {
|
|
755
802
|
let createdNode;
|
|
756
803
|
try {
|
|
@@ -772,8 +819,18 @@ async function createSubtree(apiClient, planId, parentId, children, results) {
|
|
|
772
819
|
continue;
|
|
773
820
|
}
|
|
774
821
|
|
|
822
|
+
if (ctx && createdNode?.id) {
|
|
823
|
+
if (child.ref) ctx.refMap.set(String(child.ref), createdNode.id);
|
|
824
|
+
const list = ctx.titleMap.get(child.title) || [];
|
|
825
|
+
list.push(createdNode.id);
|
|
826
|
+
ctx.titleMap.set(child.title, list);
|
|
827
|
+
if (Array.isArray(child.depends_on) && child.depends_on.length) {
|
|
828
|
+
ctx.edgeIntents.push({ dependsOn: child.depends_on.map(String), targetId: createdNode.id });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
775
832
|
if (child.children?.length && createdNode?.id) {
|
|
776
|
-
await createSubtree(apiClient, planId, createdNode.id, child.children, results);
|
|
833
|
+
await createSubtree(apiClient, planId, createdNode.id, child.children, results, ctx);
|
|
777
834
|
}
|
|
778
835
|
}
|
|
779
836
|
}
|
|
@@ -795,6 +852,7 @@ async function formIntentionHandler(args, apiClient) {
|
|
|
795
852
|
status,
|
|
796
853
|
visibility,
|
|
797
854
|
tree,
|
|
855
|
+
client_version: CLIENT_TAG,
|
|
798
856
|
});
|
|
799
857
|
return formatResponse({
|
|
800
858
|
...result,
|
|
@@ -837,6 +895,7 @@ async function formIntentionHandler(args, apiClient) {
|
|
|
837
895
|
title,
|
|
838
896
|
description: composedDescription,
|
|
839
897
|
status,
|
|
898
|
+
metadata: { created_by: CLIENT_TAG },
|
|
840
899
|
});
|
|
841
900
|
} catch (err) {
|
|
842
901
|
return errorResponse('create_failed', `Failed to create plan: ${err.response?.data?.error || err.message}`);
|
|
@@ -860,9 +919,48 @@ async function formIntentionHandler(args, apiClient) {
|
|
|
860
919
|
|
|
861
920
|
// 3. Create tree (top-level children parent to root via omitted parent_id).
|
|
862
921
|
const nodeResults = [];
|
|
863
|
-
|
|
922
|
+
const ctx = { refMap: new Map(), titleMap: new Map(), edgeIntents: [] };
|
|
923
|
+
await createSubtree(apiClient, plan.id, null, tree, nodeResults, ctx);
|
|
924
|
+
|
|
925
|
+
// 4. Wire inline dependency edges. depends_on:[X] on N means X blocks N.
|
|
926
|
+
const resolveRef = (ref) => {
|
|
927
|
+
if (ctx.refMap.has(ref)) return ctx.refMap.get(ref);
|
|
928
|
+
const byTitle = ctx.titleMap.get(ref);
|
|
929
|
+
return byTitle && byTitle.length === 1 ? byTitle[0] : null;
|
|
930
|
+
};
|
|
931
|
+
const dependencyWarnings = [];
|
|
932
|
+
let dependencyEdges = 0;
|
|
933
|
+
for (const intent of ctx.edgeIntents) {
|
|
934
|
+
for (const ref of intent.dependsOn) {
|
|
935
|
+
const sourceId = resolveRef(ref);
|
|
936
|
+
if (!sourceId || sourceId === intent.targetId) {
|
|
937
|
+
dependencyWarnings.push(`Unresolved, ambiguous, or self depends_on reference "${ref}"`);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
await apiClient.axiosInstance.post('/dependencies', {
|
|
942
|
+
source_node_id: sourceId,
|
|
943
|
+
target_node_id: intent.targetId,
|
|
944
|
+
dependency_type: 'blocks',
|
|
945
|
+
});
|
|
946
|
+
dependencyEdges += 1;
|
|
947
|
+
} catch (err) {
|
|
948
|
+
dependencyWarnings.push(`Edge ${ref}→task rejected: ${err.response?.data?.error || err.message}`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
864
952
|
|
|
865
|
-
|
|
953
|
+
const taskCount = nodeResults.filter((n) => n.id && (n.node_type === 'task' || n.node_type === 'milestone')).length;
|
|
954
|
+
const createdWithoutDependencies = taskCount >= 2 && dependencyEdges === 0;
|
|
955
|
+
const structure = {
|
|
956
|
+
task_count: taskCount,
|
|
957
|
+
dependency_edges: dependencyEdges,
|
|
958
|
+
created_without_dependencies: createdWithoutDependencies,
|
|
959
|
+
created_by: CLIENT_TAG,
|
|
960
|
+
};
|
|
961
|
+
if (dependencyWarnings.length) structure.dependency_warnings = dependencyWarnings;
|
|
962
|
+
|
|
963
|
+
const response = {
|
|
866
964
|
as_of: asOf(),
|
|
867
965
|
plan_id: plan.id,
|
|
868
966
|
goal_id,
|
|
@@ -871,10 +969,18 @@ async function formIntentionHandler(args, apiClient) {
|
|
|
871
969
|
nodes_created: nodeResults.filter((n) => n.id).length,
|
|
872
970
|
node_failures: nodeResults.filter((n) => n.error),
|
|
873
971
|
nodes: nodeResults,
|
|
972
|
+
structure,
|
|
874
973
|
next_step: plan.status === 'draft'
|
|
875
974
|
? "Plan created as draft. Will surface in dashboard pending for human review. Auto-promotes to active when first task moves to in_progress."
|
|
876
975
|
: "Plan active. Claim a task with claim_next_task({plan_id}) to begin work.",
|
|
877
|
-
}
|
|
976
|
+
};
|
|
977
|
+
if (createdWithoutDependencies) {
|
|
978
|
+
response.warning =
|
|
979
|
+
`Plan has ${taskCount} tasks but no dependency edges — execution order is implicit only.`;
|
|
980
|
+
response.next_required_action =
|
|
981
|
+
'Call link_intentions to add blocking edges, or confirm the tasks are order-independent.';
|
|
982
|
+
}
|
|
983
|
+
return formatResponse(response);
|
|
878
984
|
}
|
|
879
985
|
|
|
880
986
|
// ─────────────────────────────────────────────────────────────────────────
|
package/src/tools/bdi/utility.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const { asOf, formatResponse } = require('./_shared');
|
|
6
|
+
const { version: MCP_VERSION } = require('../../../package.json');
|
|
6
7
|
|
|
7
8
|
const getStartedDefinition = {
|
|
8
9
|
name: 'get_started',
|
|
@@ -21,6 +22,7 @@ const getStartedDefinition = {
|
|
|
21
22
|
async function getStartedHandler(args) {
|
|
22
23
|
return formatResponse({
|
|
23
24
|
as_of: asOf(),
|
|
25
|
+
mcp_version: MCP_VERSION,
|
|
24
26
|
overview:
|
|
25
27
|
"AgentPlanner exposes a BDI-aligned MCP surface. Tools are grouped by " +
|
|
26
28
|
"Beliefs (state queries), Desires (goals), and Intentions (committed actions). " +
|