agent-planner-mcp 0.8.0 → 0.9.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 +58 -243
- package/README.md +30 -0
- package/SKILL.md +111 -418
- package/package.json +3 -1
- package/src/tools/bdi/_shared.js +29 -0
- package/src/tools/bdi/beliefs.js +451 -0
- package/src/tools/bdi/desires.js +163 -0
- package/src/tools/bdi/index.js +46 -0
- package/src/tools/bdi/intentions.js +532 -0
- package/src/tools/bdi/utility.js +73 -0
- package/src/tools.js +34 -2629
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BDI beliefs — state queries.
|
|
3
|
+
*
|
|
4
|
+
* 6 tools: briefing, task_context, goal_state, recall_knowledge, search,
|
|
5
|
+
* plan_analysis. Each answers one whole agentic question and returns `as_of`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { asOf, formatResponse, errorResponse, safeArray } = require('./_shared');
|
|
9
|
+
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
11
|
+
// briefing — bundled mission control state. Replaces 4 round trips.
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const briefingDefinition = {
|
|
15
|
+
name: 'briefing',
|
|
16
|
+
description:
|
|
17
|
+
"Mission control state in one call. Returns goal health summary, " +
|
|
18
|
+
"pending decisions, my tasks, recent activity, and a top recommendation. " +
|
|
19
|
+
"Use this as the single read for Cowork live artifacts and the autopilot's " +
|
|
20
|
+
"first call.",
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
scope: { type: 'string', enum: ['mission_control', 'task_session', 'org'], default: 'mission_control' },
|
|
25
|
+
goal_id: { type: 'string' },
|
|
26
|
+
plan_id: { type: 'string' },
|
|
27
|
+
recent_window_hours: { type: 'number', default: 24 },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
async function briefingHandler(args, apiClient) {
|
|
33
|
+
const recentHours = typeof args.recent_window_hours === 'number' ? args.recent_window_hours : 24;
|
|
34
|
+
const recentSinceMs = Date.now() - recentHours * 3600 * 1000;
|
|
35
|
+
|
|
36
|
+
const [dashboardRes, pendingRes, myTasksRes, coherenceRes, episodesRes] = await Promise.allSettled([
|
|
37
|
+
apiClient.goals.getDashboard(),
|
|
38
|
+
apiClient.axiosInstance.get('/dashboard/pending', { params: { limit: 10 } }),
|
|
39
|
+
apiClient.users.getMyTasks(),
|
|
40
|
+
apiClient.coherence.getPending(),
|
|
41
|
+
apiClient.graphiti.getEpisodes({ max_episodes: 20 }),
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const failures = [];
|
|
45
|
+
function unwrap(settled, label, defaultValue) {
|
|
46
|
+
if (settled.status === 'fulfilled') return settled.value;
|
|
47
|
+
failures.push({ source: label, message: settled.reason?.message || String(settled.reason) });
|
|
48
|
+
return defaultValue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const dashboard = unwrap(dashboardRes, 'goals.dashboard', { goals: [] });
|
|
52
|
+
const pendingResp = unwrap(pendingRes, 'dashboard.pending', { data: { decisions: [], agent_requests: [] } });
|
|
53
|
+
const pending = pendingResp.data || pendingResp || { decisions: [], agent_requests: [] };
|
|
54
|
+
const myTasks = unwrap(myTasksRes, 'users.getMyTasks', { tasks: [] });
|
|
55
|
+
const coherencePending = unwrap(coherenceRes, 'coherence.pending', { plans: [], goals: [] });
|
|
56
|
+
const episodes = unwrap(episodesRes, 'graphiti.episodes', { episodes: { episodes: [] } });
|
|
57
|
+
|
|
58
|
+
let goals = safeArray(dashboard.goals);
|
|
59
|
+
if (args.goal_id) goals = goals.filter((g) => g.id === args.goal_id);
|
|
60
|
+
|
|
61
|
+
const goalSummary = goals.reduce(
|
|
62
|
+
(acc, g) => {
|
|
63
|
+
const h = g.health || 'on_track';
|
|
64
|
+
acc[h] = (acc[h] || 0) + 1;
|
|
65
|
+
acc.total += 1;
|
|
66
|
+
return acc;
|
|
67
|
+
},
|
|
68
|
+
{ on_track: 0, at_risk: 0, stale: 0, total: 0 }
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
let decisions = safeArray(pending.decisions);
|
|
72
|
+
if (args.plan_id) decisions = decisions.filter((d) => d.plan_id === args.plan_id);
|
|
73
|
+
|
|
74
|
+
let agentRequests = safeArray(pending.agent_requests);
|
|
75
|
+
if (args.plan_id) agentRequests = agentRequests.filter((r) => r.plan_id === args.plan_id);
|
|
76
|
+
|
|
77
|
+
const tasks = safeArray(myTasks.tasks || myTasks);
|
|
78
|
+
const myTasksBucketed = {
|
|
79
|
+
in_progress: tasks.filter((t) => t.status === 'in_progress'),
|
|
80
|
+
blocked: tasks.filter((t) => t.status === 'blocked'),
|
|
81
|
+
recently_completed: tasks.filter((t) => {
|
|
82
|
+
if (t.status !== 'completed') return false;
|
|
83
|
+
const at = t.updated_at || t.completed_at;
|
|
84
|
+
return at && new Date(at).getTime() >= recentSinceMs;
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const allEpisodes = safeArray(episodes.episodes?.episodes || episodes.episodes);
|
|
89
|
+
const recentActivity = allEpisodes
|
|
90
|
+
.filter((e) => e.created_at && new Date(e.created_at).getTime() >= recentSinceMs)
|
|
91
|
+
.map((e) => ({
|
|
92
|
+
type: 'episode',
|
|
93
|
+
ref_id: e.uuid,
|
|
94
|
+
summary: e.name || (e.content && e.content.slice(0, 200)),
|
|
95
|
+
occurred_at: e.created_at,
|
|
96
|
+
source: e.source,
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
const topRecommendation = (() => {
|
|
100
|
+
const atRisk = goals.filter((g) => g.health === 'at_risk');
|
|
101
|
+
let best = null;
|
|
102
|
+
for (const g of atRisk) {
|
|
103
|
+
for (const b of safeArray(g.bottleneck_summary)) {
|
|
104
|
+
if (!best || (b.direct_downstream_count || 0) > (best.direct_downstream_count || 0)) {
|
|
105
|
+
best = { ...b, goal_id: g.id, goal_title: g.title };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!best) return null;
|
|
110
|
+
return {
|
|
111
|
+
goal_id: best.goal_id,
|
|
112
|
+
suggested_action: `Unblock task "${best.title}" — it gates ${best.direct_downstream_count || 0} downstream task(s)`,
|
|
113
|
+
reasoning: `On at_risk goal "${best.goal_title}", this is the bottleneck with the highest direct_downstream_count`,
|
|
114
|
+
node_id: best.node_id,
|
|
115
|
+
};
|
|
116
|
+
})();
|
|
117
|
+
|
|
118
|
+
const coherencePendingList = [
|
|
119
|
+
...safeArray(coherencePending.plans).map((p) => ({
|
|
120
|
+
id: p.id,
|
|
121
|
+
type: 'plan',
|
|
122
|
+
title: p.title,
|
|
123
|
+
last_check_age_hours:
|
|
124
|
+
p.coherence_checked_at
|
|
125
|
+
? Math.round((Date.now() - new Date(p.coherence_checked_at).getTime()) / 3600 / 1000)
|
|
126
|
+
: null,
|
|
127
|
+
})),
|
|
128
|
+
...safeArray(coherencePending.goals).map((g) => ({
|
|
129
|
+
id: g.id,
|
|
130
|
+
type: 'goal',
|
|
131
|
+
title: g.title,
|
|
132
|
+
last_check_age_hours:
|
|
133
|
+
g.coherence_checked_at
|
|
134
|
+
? Math.round((Date.now() - new Date(g.coherence_checked_at).getTime()) / 3600 / 1000)
|
|
135
|
+
: null,
|
|
136
|
+
})),
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
return formatResponse({
|
|
140
|
+
as_of: asOf(),
|
|
141
|
+
scope: args.scope || 'mission_control',
|
|
142
|
+
goal_health: {
|
|
143
|
+
summary: goalSummary,
|
|
144
|
+
goals: goals.map((g) => ({
|
|
145
|
+
id: g.id,
|
|
146
|
+
title: g.title,
|
|
147
|
+
health: g.health,
|
|
148
|
+
priority: g.priority,
|
|
149
|
+
bottleneck_summary: g.bottleneck_summary,
|
|
150
|
+
last_activity: g.last_activity,
|
|
151
|
+
pending_decision_count: g.pending_decision_count,
|
|
152
|
+
})),
|
|
153
|
+
},
|
|
154
|
+
pending_decisions: decisions,
|
|
155
|
+
pending_agent_requests: agentRequests,
|
|
156
|
+
my_tasks: myTasksBucketed,
|
|
157
|
+
recent_activity: recentActivity,
|
|
158
|
+
top_recommendation: topRecommendation,
|
|
159
|
+
coherence_pending: coherencePendingList,
|
|
160
|
+
meta: { partial: failures.length > 0, failures },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
165
|
+
// task_context — single task at progressive depth (1-4).
|
|
166
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
const taskContextDefinition = {
|
|
169
|
+
name: 'task_context',
|
|
170
|
+
description:
|
|
171
|
+
"Get progressive context for a task. Depth: 1 (task only), 2 (+ neighborhood), " +
|
|
172
|
+
"3 (+ knowledge), 4 (+ extended plan/goals/transitive deps). For RPI implement " +
|
|
173
|
+
"tasks, automatically includes research+plan outputs from the chain.",
|
|
174
|
+
inputSchema: {
|
|
175
|
+
type: 'object',
|
|
176
|
+
properties: {
|
|
177
|
+
task_id: { type: 'string' },
|
|
178
|
+
depth: { type: 'integer', enum: [1, 2, 3, 4], default: 2 },
|
|
179
|
+
token_budget: { type: 'integer', default: 0 },
|
|
180
|
+
},
|
|
181
|
+
required: ['task_id'],
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
async function taskContextHandler(args, apiClient) {
|
|
186
|
+
const { task_id, depth = 2, token_budget = 0 } = args;
|
|
187
|
+
const params = new URLSearchParams({
|
|
188
|
+
node_id: task_id,
|
|
189
|
+
depth: String(depth),
|
|
190
|
+
token_budget: String(token_budget),
|
|
191
|
+
log_limit: '10',
|
|
192
|
+
include_research: 'true',
|
|
193
|
+
});
|
|
194
|
+
try {
|
|
195
|
+
const response = await apiClient.axiosInstance.get(`/context/progressive?${params}`);
|
|
196
|
+
return formatResponse({ as_of: asOf(), ...response.data });
|
|
197
|
+
} catch (err) {
|
|
198
|
+
return errorResponse('upstream_unavailable', `Failed to load task context: ${err.response?.data?.error || err.message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
203
|
+
// goal_state — single-goal deep dive. Replaces 5 separate goal reads.
|
|
204
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
const goalStateDefinition = {
|
|
207
|
+
name: 'goal_state',
|
|
208
|
+
description:
|
|
209
|
+
"Comprehensive single-goal read: details, quality assessment, progress, " +
|
|
210
|
+
"bottlenecks, knowledge gaps, pending decisions, recent activity. " +
|
|
211
|
+
"Replaces get_goal + goal_path + goal_progress + goal_knowledge_gaps + assess_goal_quality.",
|
|
212
|
+
inputSchema: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
properties: { goal_id: { type: 'string' } },
|
|
215
|
+
required: ['goal_id'],
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
async function goalStateHandler(args, apiClient) {
|
|
220
|
+
const { goal_id } = args;
|
|
221
|
+
const [goalRes, qualityRes, progressRes, gapsRes, pathRes] = await Promise.allSettled([
|
|
222
|
+
apiClient.goals.get(goal_id),
|
|
223
|
+
apiClient.goals.getQuality(goal_id),
|
|
224
|
+
apiClient.goals.getProgress(goal_id),
|
|
225
|
+
apiClient.goals.getKnowledgeGaps(goal_id),
|
|
226
|
+
apiClient.goals.getPath(goal_id),
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
const failures = [];
|
|
230
|
+
const unwrap = (s, label, def) => {
|
|
231
|
+
if (s.status === 'fulfilled') return s.value;
|
|
232
|
+
failures.push({ source: label, message: s.reason?.message });
|
|
233
|
+
return def;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const goal = unwrap(goalRes, 'goals.get', null);
|
|
237
|
+
if (!goal) return errorResponse('not_found', `Goal ${goal_id} not found`);
|
|
238
|
+
|
|
239
|
+
const quality = unwrap(qualityRes, 'goals.quality', {});
|
|
240
|
+
const progress = unwrap(progressRes, 'goals.progress', {});
|
|
241
|
+
const gaps = unwrap(gapsRes, 'goals.knowledgeGaps', { gaps: [] });
|
|
242
|
+
const path = unwrap(pathRes, 'goals.path', { tasks: [] });
|
|
243
|
+
|
|
244
|
+
const bottlenecks = safeArray(path.tasks || path)
|
|
245
|
+
.filter((t) => t.status !== 'completed')
|
|
246
|
+
.sort((a, b) => (b.direct_downstream_count || 0) - (a.direct_downstream_count || 0))
|
|
247
|
+
.slice(0, 5)
|
|
248
|
+
.map((t) => ({
|
|
249
|
+
node_id: t.id,
|
|
250
|
+
title: t.title,
|
|
251
|
+
status: t.status,
|
|
252
|
+
direct_downstream_count: t.direct_downstream_count || 0,
|
|
253
|
+
}));
|
|
254
|
+
|
|
255
|
+
return formatResponse({
|
|
256
|
+
as_of: asOf(),
|
|
257
|
+
goal: {
|
|
258
|
+
id: goal.id, title: goal.title, description: goal.description,
|
|
259
|
+
type: goal.type, goal_type: goal.goalType || goal.goal_type,
|
|
260
|
+
status: goal.status, priority: goal.priority,
|
|
261
|
+
owner_id: goal.ownerId || goal.owner_id, success_criteria: goal.successCriteria || goal.success_criteria,
|
|
262
|
+
promoted_at: goal.promotedAt || goal.promoted_at,
|
|
263
|
+
},
|
|
264
|
+
quality: {
|
|
265
|
+
score: quality.score, dimensions: quality.dimensions,
|
|
266
|
+
suggestions: quality.suggestions, last_assessed_at: quality.as_of,
|
|
267
|
+
},
|
|
268
|
+
progress: progress,
|
|
269
|
+
bottlenecks,
|
|
270
|
+
knowledge_gaps: safeArray(gaps.gaps || gaps),
|
|
271
|
+
meta: { partial: failures.length > 0, failures },
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
276
|
+
// recall_knowledge — universal knowledge query. Replaces 4 separate tools.
|
|
277
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
const recallKnowledgeDefinition = {
|
|
280
|
+
name: 'recall_knowledge',
|
|
281
|
+
description:
|
|
282
|
+
"Universal knowledge graph query. Returns facts, entities, recent episodes, " +
|
|
283
|
+
"and contradictions in one shape. Use result_kind to control payload size. " +
|
|
284
|
+
"Replaces recall_knowledge legacy + find_entities + get_recent_episodes + check_contradictions.",
|
|
285
|
+
inputSchema: {
|
|
286
|
+
type: 'object',
|
|
287
|
+
properties: {
|
|
288
|
+
query: { type: 'string', description: 'Search query — required for facts/entities, optional for episodes' },
|
|
289
|
+
scope: {
|
|
290
|
+
type: 'object',
|
|
291
|
+
properties: { plan_id: { type: 'string' }, goal_id: { type: 'string' }, node_id: { type: 'string' } },
|
|
292
|
+
},
|
|
293
|
+
since: { type: 'string', description: 'ISO 8601 — only return episodes after this' },
|
|
294
|
+
entry_type: { type: 'string', enum: ['learning', 'decision', 'progress', 'challenge', 'all'], default: 'all' },
|
|
295
|
+
result_kind: { type: 'string', enum: ['facts', 'entities', 'episodes', 'all'], default: 'all' },
|
|
296
|
+
max_results: { type: 'integer', default: 10 },
|
|
297
|
+
include_contradictions: { type: 'boolean', default: false },
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
async function recallKnowledgeHandler(args, apiClient) {
|
|
303
|
+
const { query, scope = {}, since, entry_type = 'all', result_kind = 'all', max_results = 10, include_contradictions = false } = args;
|
|
304
|
+
const wantFacts = result_kind === 'all' || result_kind === 'facts';
|
|
305
|
+
const wantEntities = result_kind === 'all' || result_kind === 'entities';
|
|
306
|
+
const wantEpisodes = result_kind === 'all' || result_kind === 'episodes';
|
|
307
|
+
|
|
308
|
+
const calls = [];
|
|
309
|
+
if (wantFacts && query) {
|
|
310
|
+
calls.push({ key: 'facts', p: apiClient.graphiti.graphSearch({ query, max_results, ...scope }) });
|
|
311
|
+
}
|
|
312
|
+
if (wantEntities && query) {
|
|
313
|
+
calls.push({ key: 'entities', p: apiClient.graphiti.searchEntities({ query, max_results }) });
|
|
314
|
+
}
|
|
315
|
+
if (wantEpisodes) {
|
|
316
|
+
calls.push({ key: 'episodes', p: apiClient.graphiti.getEpisodes({ max_episodes: Math.min(max_results * 2, 50) }) });
|
|
317
|
+
}
|
|
318
|
+
if (include_contradictions && query) {
|
|
319
|
+
calls.push({ key: 'contradictions', p: apiClient.graphiti.detectContradictions({ topic: query, ...scope }) });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const settled = await Promise.allSettled(calls.map((c) => c.p));
|
|
323
|
+
const out = { as_of: asOf(), facts: [], entities: [], episodes: [], contradictions: null, meta: { failures: [] } };
|
|
324
|
+
|
|
325
|
+
settled.forEach((s, i) => {
|
|
326
|
+
const key = calls[i].key;
|
|
327
|
+
if (s.status !== 'fulfilled') {
|
|
328
|
+
out.meta.failures.push({ source: `graphiti.${key}`, message: s.reason?.message });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const v = s.value;
|
|
332
|
+
if (key === 'facts') out.facts = safeArray(v.facts || v);
|
|
333
|
+
if (key === 'entities') out.entities = safeArray(v.entities || v);
|
|
334
|
+
if (key === 'episodes') {
|
|
335
|
+
let eps = safeArray(v.episodes?.episodes || v.episodes || v);
|
|
336
|
+
if (since) {
|
|
337
|
+
const sinceMs = new Date(since).getTime();
|
|
338
|
+
eps = eps.filter((e) => e.created_at && new Date(e.created_at).getTime() >= sinceMs);
|
|
339
|
+
}
|
|
340
|
+
if (entry_type !== 'all') {
|
|
341
|
+
eps = eps.filter((e) => (e.entry_type || e.source) === entry_type);
|
|
342
|
+
}
|
|
343
|
+
out.episodes = eps.slice(0, max_results);
|
|
344
|
+
}
|
|
345
|
+
if (key === 'contradictions') out.contradictions = v;
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return formatResponse(out);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
352
|
+
// search — universal text search.
|
|
353
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
const searchDefinition = {
|
|
356
|
+
name: 'search',
|
|
357
|
+
description: 'Text search across plans, nodes, and content. Use for finding entities by title or fragment.',
|
|
358
|
+
inputSchema: {
|
|
359
|
+
type: 'object',
|
|
360
|
+
properties: {
|
|
361
|
+
query: { type: 'string' },
|
|
362
|
+
scope: { type: 'string', enum: ['global', 'plans', 'plan', 'node'], default: 'global' },
|
|
363
|
+
scope_id: { type: 'string' },
|
|
364
|
+
filters: {
|
|
365
|
+
type: 'object',
|
|
366
|
+
properties: {
|
|
367
|
+
status: { type: 'string' },
|
|
368
|
+
type: { type: 'string' },
|
|
369
|
+
limit: { type: 'integer', default: 20 },
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
required: ['query'],
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
async function searchHandler(args, apiClient) {
|
|
378
|
+
const { query, scope = 'global', scope_id, filters = {} } = args;
|
|
379
|
+
try {
|
|
380
|
+
let result;
|
|
381
|
+
const limit = filters.limit || 20;
|
|
382
|
+
if (scope === 'global') result = await apiClient.search.global(query, { limit, ...filters });
|
|
383
|
+
else if (scope === 'plans') result = await apiClient.search.plans(query, limit);
|
|
384
|
+
else if (scope === 'plan') result = await apiClient.search.inPlan(scope_id, query, limit);
|
|
385
|
+
else if (scope === 'node') result = await apiClient.search.inNode(scope_id, query, limit);
|
|
386
|
+
return formatResponse({ as_of: asOf(), ...(result || {}) });
|
|
387
|
+
} catch (err) {
|
|
388
|
+
return errorResponse('upstream_unavailable', `Search failed: ${err.response?.data?.error || err.message}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
393
|
+
// plan_analysis — advanced reads (impact, critical_path, bottlenecks, coherence).
|
|
394
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
const planAnalysisDefinition = {
|
|
397
|
+
name: 'plan_analysis',
|
|
398
|
+
description:
|
|
399
|
+
"Advanced plan reads: impact analysis (delay/block/remove), critical path, " +
|
|
400
|
+
"bottleneck list, or coherence check.",
|
|
401
|
+
inputSchema: {
|
|
402
|
+
type: 'object',
|
|
403
|
+
properties: {
|
|
404
|
+
plan_id: { type: 'string' },
|
|
405
|
+
type: { type: 'string', enum: ['impact', 'critical_path', 'bottlenecks', 'coherence'] },
|
|
406
|
+
node_id: { type: 'string' },
|
|
407
|
+
scenario: { type: 'string', enum: ['delay', 'block', 'remove'] },
|
|
408
|
+
},
|
|
409
|
+
required: ['plan_id', 'type'],
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
async function planAnalysisHandler(args, apiClient) {
|
|
414
|
+
const { plan_id, type, node_id, scenario } = args;
|
|
415
|
+
try {
|
|
416
|
+
let result;
|
|
417
|
+
if (type === 'critical_path') {
|
|
418
|
+
result = (await apiClient.axiosInstance.get(`/plans/${plan_id}/critical-path`)).data;
|
|
419
|
+
} else if (type === 'bottlenecks') {
|
|
420
|
+
result = (await apiClient.axiosInstance.get(`/plans/${plan_id}/bottlenecks`)).data;
|
|
421
|
+
} else if (type === 'impact') {
|
|
422
|
+
if (!node_id) return errorResponse('invalid_arg', 'plan_analysis type=impact requires node_id');
|
|
423
|
+
const params = new URLSearchParams({ scenario: scenario || 'block' });
|
|
424
|
+
result = (await apiClient.axiosInstance.get(`/plans/${plan_id}/nodes/${node_id}/impact?${params}`)).data;
|
|
425
|
+
} else if (type === 'coherence') {
|
|
426
|
+
result = await apiClient.coherence.runCheck(plan_id);
|
|
427
|
+
}
|
|
428
|
+
return formatResponse({ as_of: asOf(), type, results: result || {} });
|
|
429
|
+
} catch (err) {
|
|
430
|
+
return errorResponse('upstream_unavailable', `plan_analysis failed: ${err.response?.data?.error || err.message}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
module.exports = {
|
|
435
|
+
definitions: [
|
|
436
|
+
briefingDefinition,
|
|
437
|
+
taskContextDefinition,
|
|
438
|
+
goalStateDefinition,
|
|
439
|
+
recallKnowledgeDefinition,
|
|
440
|
+
searchDefinition,
|
|
441
|
+
planAnalysisDefinition,
|
|
442
|
+
],
|
|
443
|
+
handlers: {
|
|
444
|
+
briefing: briefingHandler,
|
|
445
|
+
task_context: taskContextHandler,
|
|
446
|
+
goal_state: goalStateHandler,
|
|
447
|
+
recall_knowledge: recallKnowledgeHandler,
|
|
448
|
+
search: searchHandler,
|
|
449
|
+
plan_analysis: planAnalysisHandler,
|
|
450
|
+
},
|
|
451
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BDI desires — goal management.
|
|
3
|
+
*
|
|
4
|
+
* 2 tools: list_goals (with health rollup), update_goal (atomic, subsumes
|
|
5
|
+
* link/unlink and achiever changes).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { asOf, formatResponse, errorResponse, safeArray } = require('./_shared');
|
|
9
|
+
|
|
10
|
+
const listGoalsDefinition = {
|
|
11
|
+
name: 'list_goals',
|
|
12
|
+
description:
|
|
13
|
+
"List goals with health rollup. Returns aggregate counts (on_track/at_risk/stale) " +
|
|
14
|
+
"plus per-goal summary.",
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
filter: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
health: { type: 'array', items: { type: 'string', enum: ['on_track', 'at_risk', 'stale'] } },
|
|
22
|
+
status: { type: 'array', items: { type: 'string' } },
|
|
23
|
+
include_inactive: { type: 'boolean', default: false },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
async function listGoalsHandler(args, apiClient) {
|
|
31
|
+
const filter = args.filter || {};
|
|
32
|
+
try {
|
|
33
|
+
const [listRes, dashboardRes] = await Promise.allSettled([
|
|
34
|
+
apiClient.goals.list({ status: filter.include_inactive ? undefined : 'active' }),
|
|
35
|
+
apiClient.goals.getDashboard(),
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const goals = listRes.status === 'fulfilled' ? safeArray(listRes.value) : [];
|
|
39
|
+
const dashGoals = dashboardRes.status === 'fulfilled' ? safeArray(dashboardRes.value.goals) : [];
|
|
40
|
+
const healthByGoal = Object.fromEntries(dashGoals.map((g) => [g.id, g]));
|
|
41
|
+
|
|
42
|
+
let merged = goals.map((g) => {
|
|
43
|
+
const d = healthByGoal[g.id] || {};
|
|
44
|
+
return {
|
|
45
|
+
id: g.id,
|
|
46
|
+
title: g.title,
|
|
47
|
+
health: d.health || 'on_track',
|
|
48
|
+
priority: g.priority,
|
|
49
|
+
status: g.status,
|
|
50
|
+
owner_name: d.owner_name || g.owner_name,
|
|
51
|
+
last_activity: d.last_activity,
|
|
52
|
+
linked_plan_count: d.linked_plan_progress?.linked_plan_count,
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (filter.health?.length) merged = merged.filter((g) => filter.health.includes(g.health));
|
|
57
|
+
if (filter.status?.length) merged = merged.filter((g) => filter.status.includes(g.status));
|
|
58
|
+
|
|
59
|
+
const summary = merged.reduce(
|
|
60
|
+
(acc, g) => {
|
|
61
|
+
acc[g.health] = (acc[g.health] || 0) + 1;
|
|
62
|
+
acc.total += 1;
|
|
63
|
+
return acc;
|
|
64
|
+
},
|
|
65
|
+
{ on_track: 0, at_risk: 0, stale: 0, total: 0 }
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return formatResponse({ as_of: asOf(), summary, goals: merged });
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return errorResponse('upstream_unavailable', `list_goals failed: ${err.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const updateGoalDefinition = {
|
|
75
|
+
name: 'update_goal',
|
|
76
|
+
description:
|
|
77
|
+
"Atomic goal update. Subsumes update_goal + link_plan_to_goal + unlink_plan_from_goal " +
|
|
78
|
+
"+ add_achiever + remove_achiever. All changes apply together.",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
goal_id: { type: 'string' },
|
|
83
|
+
changes: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
title: { type: 'string' },
|
|
87
|
+
description: { type: 'string' },
|
|
88
|
+
priority: { type: 'integer' },
|
|
89
|
+
status: { type: 'string' },
|
|
90
|
+
goal_type: { type: 'string', enum: ['desire', 'intention'] },
|
|
91
|
+
success_criteria: {},
|
|
92
|
+
promote_to_intention: { type: 'boolean' },
|
|
93
|
+
add_linked_plans: { type: 'array', items: { type: 'string' } },
|
|
94
|
+
remove_linked_plans: { type: 'array', items: { type: 'string' } },
|
|
95
|
+
add_achievers: { type: 'array', items: { type: 'string' } },
|
|
96
|
+
remove_achievers: { type: 'array', items: { type: 'string' } },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
required: ['goal_id', 'changes'],
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
async function updateGoalHandler(args, apiClient) {
|
|
105
|
+
const { goal_id, changes } = args;
|
|
106
|
+
const applied = [];
|
|
107
|
+
const failures = [];
|
|
108
|
+
|
|
109
|
+
// Direct field updates
|
|
110
|
+
const directFields = {};
|
|
111
|
+
for (const k of ['title', 'description', 'priority', 'status', 'success_criteria']) {
|
|
112
|
+
if (changes[k] !== undefined) directFields[k] = changes[k];
|
|
113
|
+
}
|
|
114
|
+
if (changes.goal_type) directFields.goalType = changes.goal_type;
|
|
115
|
+
if (changes.promote_to_intention) directFields.goalType = 'intention';
|
|
116
|
+
|
|
117
|
+
if (Object.keys(directFields).length) {
|
|
118
|
+
try {
|
|
119
|
+
await apiClient.goals.update(goal_id, directFields);
|
|
120
|
+
applied.push('direct_fields');
|
|
121
|
+
} catch (err) {
|
|
122
|
+
failures.push({ step: 'direct_fields', error: err.message });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const planId of safeArray(changes.add_linked_plans)) {
|
|
127
|
+
try { await apiClient.goals.linkPlan(goal_id, planId); applied.push(`link_plan:${planId}`); }
|
|
128
|
+
catch (err) { failures.push({ step: `link_plan:${planId}`, error: err.message }); }
|
|
129
|
+
}
|
|
130
|
+
for (const planId of safeArray(changes.remove_linked_plans)) {
|
|
131
|
+
try { await apiClient.goals.unlinkPlan(goal_id, planId); applied.push(`unlink_plan:${planId}`); }
|
|
132
|
+
catch (err) { failures.push({ step: `unlink_plan:${planId}`, error: err.message }); }
|
|
133
|
+
}
|
|
134
|
+
for (const nodeId of safeArray(changes.add_achievers)) {
|
|
135
|
+
try { await apiClient.goals.addAchiever(goal_id, nodeId); applied.push(`add_achiever:${nodeId}`); }
|
|
136
|
+
catch (err) { failures.push({ step: `add_achiever:${nodeId}`, error: err.message }); }
|
|
137
|
+
}
|
|
138
|
+
for (const nodeId of safeArray(changes.remove_achievers)) {
|
|
139
|
+
try {
|
|
140
|
+
const achievers = await apiClient.goals.listAchievers(goal_id);
|
|
141
|
+
const link = safeArray(achievers.achievers || achievers).find((a) => a.source_node_id === nodeId);
|
|
142
|
+
if (link) {
|
|
143
|
+
await apiClient.goals.removeAchiever(goal_id, link.id);
|
|
144
|
+
applied.push(`remove_achiever:${nodeId}`);
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
failures.push({ step: `remove_achiever:${nodeId}`, error: err.message });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let goal = null;
|
|
152
|
+
try { goal = await apiClient.goals.get(goal_id); } catch {}
|
|
153
|
+
|
|
154
|
+
return formatResponse({ as_of: asOf(), goal_id, applied_changes: applied, failures, goal });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
definitions: [listGoalsDefinition, updateGoalDefinition],
|
|
159
|
+
handlers: {
|
|
160
|
+
list_goals: listGoalsHandler,
|
|
161
|
+
update_goal: updateGoalHandler,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BDI-aligned MCP tool surface (v0.9.0).
|
|
3
|
+
*
|
|
4
|
+
* Tools grouped by Belief / Desire / Intention namespaces. Each tool answers
|
|
5
|
+
* one whole agentic question, replaces multiple legacy calls, and emits an
|
|
6
|
+
* `as_of` ISO 8601 timestamp on success.
|
|
7
|
+
*
|
|
8
|
+
* See ../../../docs/MCP_REDESIGN_PLAN.md for full specs and rationale.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const beliefs = require('./beliefs');
|
|
12
|
+
const desires = require('./desires');
|
|
13
|
+
const intentions = require('./intentions');
|
|
14
|
+
const utility = require('./utility');
|
|
15
|
+
|
|
16
|
+
const definitions = [
|
|
17
|
+
...beliefs.definitions,
|
|
18
|
+
...desires.definitions,
|
|
19
|
+
...intentions.definitions,
|
|
20
|
+
...utility.definitions,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const handlers = {
|
|
24
|
+
...beliefs.handlers,
|
|
25
|
+
...desires.handlers,
|
|
26
|
+
...intentions.handlers,
|
|
27
|
+
...utility.handlers,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const names = new Set(definitions.map((t) => t.name));
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Dispatch a BDI tool call.
|
|
34
|
+
* @returns formatted MCP response, or undefined if the name isn't a BDI tool.
|
|
35
|
+
*/
|
|
36
|
+
async function bdiToolHandler(name, args, apiClient) {
|
|
37
|
+
if (!names.has(name)) return undefined;
|
|
38
|
+
const handler = handlers[name];
|
|
39
|
+
return handler(args || {}, apiClient);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
bdiToolDefinitions: definitions,
|
|
44
|
+
bdiToolHandler,
|
|
45
|
+
bdiToolNames: names,
|
|
46
|
+
};
|