agent-planner-mcp 1.1.1 → 1.4.1
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 +16 -3
- package/README.md +2 -1
- package/SKILL.md +19 -6
- package/package.json +1 -1
- package/src/api-client.js +94 -0
- package/src/cli/local-client.js +5 -3
- package/src/tools/bdi/_shared.js +13 -1
- package/src/tools/bdi/beliefs.js +55 -10
- package/src/tools/bdi/desires.js +96 -16
- package/src/tools/bdi/index.js +3 -0
- package/src/tools/bdi/intentions.js +106 -19
- package/src/tools/bdi/utility.js +1 -0
- package/src/tools/bdi/workspaces.js +215 -0
package/AGENT_GUIDE.md
CHANGED
|
@@ -9,7 +9,7 @@ get_started()
|
|
|
9
9
|
// → Returns the BDI tool surface map and recommended workflows
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
## The
|
|
12
|
+
## The 29 tools
|
|
13
13
|
|
|
14
14
|
### Beliefs (read state)
|
|
15
15
|
| Tool | When |
|
|
@@ -26,7 +26,8 @@ get_started()
|
|
|
26
26
|
|---|---|
|
|
27
27
|
| `list_goals` | Goal list with health summary |
|
|
28
28
|
| `update_goal` | Atomic goal change (subsumes link/unlink/achievers) |
|
|
29
|
-
| `
|
|
29
|
+
| `create_goal` | Create a new top-level goal (no parent) — agents create goals directly when asked |
|
|
30
|
+
| `derive_subgoal` | Create a sub-goal under an existing parent |
|
|
30
31
|
|
|
31
32
|
### Intentions — execution
|
|
32
33
|
| Tool | When |
|
|
@@ -64,6 +65,17 @@ get_started()
|
|
|
64
65
|
| `update_member_role` | Owner-only role change |
|
|
65
66
|
| `remove_member` | Owner/admin removes non-owner member |
|
|
66
67
|
|
|
68
|
+
### Workspaces & Blueprints (v1.1)
|
|
69
|
+
A Workspace is a folder under an Organization that owns goals + plans. A Blueprint is a reusable shape that forks into a workspace as a new plan.
|
|
70
|
+
|
|
71
|
+
| Tool | When |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `list_workspaces` | List workspaces in an org (default and any user-created folders) |
|
|
74
|
+
| `create_workspace` | Create a new workspace inside an org |
|
|
75
|
+
| `list_blueprints` | List blueprints visible to user (owned + public/unlisted) |
|
|
76
|
+
| `fork_blueprint` | Instantiate a plan-scope blueprint as a new plan in a target workspace |
|
|
77
|
+
| `save_as_blueprint` | Snapshot a live plan as a reusable blueprint (excludes run-state) |
|
|
78
|
+
|
|
67
79
|
## status='active' vs status='draft' (v1.0)
|
|
68
80
|
|
|
69
81
|
The single most important decision when calling a creation tool.
|
|
@@ -128,7 +140,8 @@ form_intention({goal_id: <new>, title, rationale, status: 'draft', tree: [...]})
|
|
|
128
140
|
When in doubt between act and queue:
|
|
129
141
|
- Reversible local action (status, log, learning, edit, decompose) → **act** via `update_task`, `update_node`, `extend_intention`, `add_learning`
|
|
130
142
|
- External cost, public publish, strategy change, customer comm → **queue** via `queue_decision`
|
|
131
|
-
-
|
|
143
|
+
- A goal a human asked you to set up → just **create** it via `create_goal` (active) — no UI step, no approval gate
|
|
144
|
+
- A whole new direction or sub-goal you weren't asked for → propose as **draft** via `create_goal` / `form_intention` / `derive_subgoal` with `status='draft'`
|
|
132
145
|
|
|
133
146
|
Never use `add_learning(entry_type='decision')` to fake a decision queue. `queue_decision` is the real tool.
|
|
134
147
|
|
package/README.md
CHANGED
|
@@ -255,7 +255,8 @@ Add the same JSON config to your Cline MCP settings in VS Code.
|
|
|
255
255
|
### Desires (goals)
|
|
256
256
|
- `list_goals` — goals with health rollup
|
|
257
257
|
- `update_goal` — atomic goal update (subsumes link/unlink/achievers)
|
|
258
|
-
- `
|
|
258
|
+
- `create_goal` — create a new top-level goal (no parent)
|
|
259
|
+
- `derive_subgoal` *(v1.0)* — create a sub-goal under an existing parent
|
|
259
260
|
|
|
260
261
|
### Intentions — execution
|
|
261
262
|
- `claim_next_task` — pick + claim + load context (one call)
|
package/SKILL.md
CHANGED
|
@@ -23,7 +23,7 @@ You have access to the AgentPlanner MCP tools. AgentPlanner is a collaborative p
|
|
|
23
23
|
> - **Cursor / VS Code:** Add `npx agent-planner-mcp` to your MCP config with env vars `API_URL` and `USER_API_TOKEN`
|
|
24
24
|
> - **ChatGPT:** HTTP endpoint at `https://agentplanner.io/mcp`
|
|
25
25
|
|
|
26
|
-
## The
|
|
26
|
+
## The 29 tools, organized by intent
|
|
27
27
|
|
|
28
28
|
AgentPlanner exposes a **BDI-aligned** surface — Beliefs (state queries), Desires (goal management), Intentions (committed actions). Each tool answers one whole agentic question and returns an `as_of` ISO 8601 timestamp. v1.0.0 completes the mutation surface so humans can steer entirely through agent conversation — no UI required for normal operations.
|
|
29
29
|
|
|
@@ -40,7 +40,8 @@ AgentPlanner exposes a **BDI-aligned** surface — Beliefs (state queries), Desi
|
|
|
40
40
|
|
|
41
41
|
- `list_goals` — goals with health rollup (`{ on_track, at_risk, stale, total }`)
|
|
42
42
|
- `update_goal` — atomic goal update; subsumes link/unlink + achiever changes
|
|
43
|
-
- `
|
|
43
|
+
- `create_goal` — create a new top-level goal (no parent). Agents create goals directly when asked — no UI step. Defaults to `status='active'`.
|
|
44
|
+
- `derive_subgoal` *(v1.0)* — create a sub-goal under an existing parent (use `create_goal` for top-level).
|
|
44
45
|
|
|
45
46
|
### Intentions — what am I committing to?
|
|
46
47
|
|
|
@@ -72,6 +73,16 @@ AgentPlanner exposes a **BDI-aligned** surface — Beliefs (state queries), Desi
|
|
|
72
73
|
- `update_member_role` — owner-only role change within an org
|
|
73
74
|
- `remove_member` — owner/admin can remove non-owner members
|
|
74
75
|
|
|
76
|
+
**Workspaces and Blueprints (v1.1):**
|
|
77
|
+
|
|
78
|
+
A Workspace is a folder under an Organization that owns goals + plans — a grouping primitive so a single org isn't a flat soup of unrelated work. A Blueprint is a dehydrated, reusable shape (scope `plan` or `workspace`); forking instantiates it as a new plan inside a target workspace. v1 supports plan-scope only.
|
|
79
|
+
|
|
80
|
+
- `list_workspaces` — list workspaces in an organization
|
|
81
|
+
- `create_workspace` — create a new folder under an org (auto-slug)
|
|
82
|
+
- `list_blueprints` — list blueprints visible to user (owned + public/unlisted), filterable by scope
|
|
83
|
+
- `fork_blueprint` — instantiate a plan-scope blueprint as a new plan in a target workspace
|
|
84
|
+
- `save_as_blueprint` — snapshot a live plan as a reusable blueprint. Captures structure, agent_instructions, and dependencies; excludes statuses, claims, knowledge episodes, logs, decisions, and agent assignments
|
|
85
|
+
|
|
75
86
|
### Utility
|
|
76
87
|
|
|
77
88
|
- `get_started` — dynamic reference; call this if you're new to AgentPlanner
|
|
@@ -250,12 +261,14 @@ When a user expresses intent — "I want to launch a feature", "we need better t
|
|
|
250
261
|
2. list_goals to check if a similar goal already exists
|
|
251
262
|
3. Use update_goal({ add_linked_plans, add_achievers }) to wire it up
|
|
252
263
|
|
|
253
|
-
Goal
|
|
254
|
-
-
|
|
255
|
-
-
|
|
264
|
+
Goal commitment (`committed` boolean):
|
|
265
|
+
- committed: false — aspirational, no firm commitment to execute yet
|
|
266
|
+
- committed: true — promoted to active execution
|
|
256
267
|
```
|
|
257
268
|
|
|
258
|
-
|
|
269
|
+
Commit a goal via `update_goal({ changes: { committed: true } })`. Coherence
|
|
270
|
+
status on tasks reads as plain language: `ok` / `outdated` / `contradicted` /
|
|
271
|
+
`unchecked` (with a `coherence_message`).
|
|
259
272
|
|
|
260
273
|
## Decision queueing
|
|
261
274
|
|
package/package.json
CHANGED
package/src/api-client.js
CHANGED
|
@@ -550,6 +550,7 @@ const goals = {
|
|
|
550
550
|
const params = new URLSearchParams();
|
|
551
551
|
if (filters.organization_id) params.append('organization_id', filters.organization_id);
|
|
552
552
|
if (filters.status) params.append('status', filters.status);
|
|
553
|
+
if (filters.workspaceId) params.append('workspace_id', filters.workspaceId);
|
|
553
554
|
const response = await apiClient.get(`/goals?${params.toString()}`);
|
|
554
555
|
return response.data.goals || response.data;
|
|
555
556
|
},
|
|
@@ -744,6 +745,40 @@ const graphiti = {
|
|
|
744
745
|
}
|
|
745
746
|
};
|
|
746
747
|
|
|
748
|
+
// ─── Workspaces ───────────────────────────────────────────────
|
|
749
|
+
const workspaces = {
|
|
750
|
+
list: async ({ organizationId, includeArchived = false } = {}) => {
|
|
751
|
+
const params = new URLSearchParams();
|
|
752
|
+
if (organizationId) params.append('organization_id', organizationId);
|
|
753
|
+
if (includeArchived) params.append('include_archived', 'true');
|
|
754
|
+
return (await apiClient.get(`/workspaces?${params.toString()}`)).data;
|
|
755
|
+
},
|
|
756
|
+
get: async (workspaceId) => (await apiClient.get(`/workspaces/${workspaceId}`)).data,
|
|
757
|
+
create: async (data) => (await apiClient.post('/workspaces', data)).data,
|
|
758
|
+
update: async (workspaceId, data) => (await apiClient.patch(`/workspaces/${workspaceId}`, data)).data,
|
|
759
|
+
archive: async (workspaceId) => (await apiClient.post(`/workspaces/${workspaceId}/archive`)).data,
|
|
760
|
+
restore: async (workspaceId) => (await apiClient.post(`/workspaces/${workspaceId}/restore`)).data,
|
|
761
|
+
delete: async (workspaceId) => (await apiClient.delete(`/workspaces/${workspaceId}`)).data,
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
// ─── Blueprints ───────────────────────────────────────────────
|
|
765
|
+
const blueprints = {
|
|
766
|
+
list: async ({ scope, visibility, ownerOnly = false } = {}) => {
|
|
767
|
+
const params = new URLSearchParams();
|
|
768
|
+
if (scope) params.append('scope', scope);
|
|
769
|
+
if (visibility) params.append('visibility', visibility);
|
|
770
|
+
if (ownerOnly) params.append('owner_only', 'true');
|
|
771
|
+
const qs = params.toString();
|
|
772
|
+
return (await apiClient.get(`/blueprints${qs ? `?${qs}` : ''}`)).data;
|
|
773
|
+
},
|
|
774
|
+
get: async (blueprintId) => (await apiClient.get(`/blueprints/${blueprintId}`)).data,
|
|
775
|
+
create: async (data) => (await apiClient.post('/blueprints', data)).data,
|
|
776
|
+
update: async (blueprintId, data) => (await apiClient.patch(`/blueprints/${blueprintId}`, data)).data,
|
|
777
|
+
delete: async (blueprintId) => (await apiClient.delete(`/blueprints/${blueprintId}`)).data,
|
|
778
|
+
fork: async (blueprintId, data) => (await apiClient.post(`/blueprints/${blueprintId}/fork`, data)).data,
|
|
779
|
+
saveFromPlan: async (planId, data = {}) => (await apiClient.post(`/blueprints/from_plan/${planId}`, data)).data,
|
|
780
|
+
};
|
|
781
|
+
|
|
747
782
|
// ─── Users (my-tasks queue) ────────────────────────────────────
|
|
748
783
|
const users = {
|
|
749
784
|
getMyTasks: async (options = {}) => {
|
|
@@ -764,6 +799,22 @@ const agentLoop = {
|
|
|
764
799
|
createIntention: async (data = {}) => (await apiClient.post('/agent/intentions', data)).data,
|
|
765
800
|
};
|
|
766
801
|
|
|
802
|
+
// ─── v1 public API facades ────────────────────────────────────
|
|
803
|
+
// Server-side compositions shipped with the API v1 consolidation. Each
|
|
804
|
+
// replaces a client-side fan-out (goal_state: 5 calls → 1, recall_knowledge:
|
|
805
|
+
// up to 4 → 1, update_task: 4 → 1, share_plan: N → 1). Tool handlers fall
|
|
806
|
+
// back to the legacy fan-out when the backend predates /v1 — see
|
|
807
|
+
// isV1Unavailable() in tools/bdi/_shared.js.
|
|
808
|
+
const v1 = {
|
|
809
|
+
goalState: async (goalId) => (await apiClient.get(`/v1/goals/${goalId}/state`)).data,
|
|
810
|
+
planAnalysis: async (planId) => (await apiClient.get(`/v1/plans/${planId}/analysis`)).data,
|
|
811
|
+
knowledgeSearch: async (body = {}) => (await apiClient.post('/v1/knowledge/search', body)).data,
|
|
812
|
+
updateTask: async (nodeId, body = {}) => (await apiClient.post(`/v1/tasks/${nodeId}/update`, body)).data,
|
|
813
|
+
sharePlan: async (planId, body = {}) => (await apiClient.post(`/v1/plans/${planId}/share`, body)).data,
|
|
814
|
+
briefing: async (params = {}) => (await apiClient.get('/v1/briefing', { params })).data,
|
|
815
|
+
claimNext: async (body = {}) => (await apiClient.post('/v1/tasks/claim-next', body)).data,
|
|
816
|
+
};
|
|
817
|
+
|
|
767
818
|
// ─── Dependencies (cross-plan & external) ─────────────────────
|
|
768
819
|
const dependencies = {
|
|
769
820
|
/**
|
|
@@ -918,6 +969,7 @@ function createApiClient(token, options = {}) {
|
|
|
918
969
|
const params = new URLSearchParams();
|
|
919
970
|
if (filters.organization_id) params.append('organization_id', filters.organization_id);
|
|
920
971
|
if (filters.status) params.append('status', filters.status);
|
|
972
|
+
if (filters.workspaceId) params.append('workspace_id', filters.workspaceId);
|
|
921
973
|
const r = await client.get(`/goals?${params.toString()}`);
|
|
922
974
|
return r.data.goals || r.data;
|
|
923
975
|
},
|
|
@@ -973,6 +1025,15 @@ function createApiClient(token, options = {}) {
|
|
|
973
1025
|
listCrossPlan: async (planIds) => (await client.get('/dependencies/cross-plan', { params: { plan_ids: planIds.join(',') } })).data,
|
|
974
1026
|
createExternal: async (data) => (await client.post('/dependencies/external', data)).data,
|
|
975
1027
|
},
|
|
1028
|
+
v1: {
|
|
1029
|
+
goalState: async (goalId) => (await client.get(`/v1/goals/${goalId}/state`)).data,
|
|
1030
|
+
planAnalysis: async (planId) => (await client.get(`/v1/plans/${planId}/analysis`)).data,
|
|
1031
|
+
knowledgeSearch: async (body = {}) => (await client.post('/v1/knowledge/search', body)).data,
|
|
1032
|
+
updateTask: async (nodeId, body = {}) => (await client.post(`/v1/tasks/${nodeId}/update`, body)).data,
|
|
1033
|
+
sharePlan: async (planId, body = {}) => (await client.post(`/v1/plans/${planId}/share`, body)).data,
|
|
1034
|
+
briefing: async (params = {}) => (await client.get('/v1/briefing', { params })).data,
|
|
1035
|
+
claimNext: async (body = {}) => (await client.post('/v1/tasks/claim-next', body)).data,
|
|
1036
|
+
},
|
|
976
1037
|
users: {
|
|
977
1038
|
getMyTasks: async (options = {}) => {
|
|
978
1039
|
const params = new URLSearchParams();
|
|
@@ -988,6 +1049,36 @@ function createApiClient(token, options = {}) {
|
|
|
988
1049
|
blockWorkSession: async (sessionId, data = {}) => (await client.post(`/agent/work-sessions/${sessionId}/block`, data)).data,
|
|
989
1050
|
createIntention: async (data = {}) => (await client.post('/agent/intentions', data)).data,
|
|
990
1051
|
},
|
|
1052
|
+
workspaces: {
|
|
1053
|
+
list: async ({ organizationId, includeArchived = false } = {}) => {
|
|
1054
|
+
const params = new URLSearchParams();
|
|
1055
|
+
if (organizationId) params.append('organization_id', organizationId);
|
|
1056
|
+
if (includeArchived) params.append('include_archived', 'true');
|
|
1057
|
+
return (await client.get(`/workspaces?${params.toString()}`)).data;
|
|
1058
|
+
},
|
|
1059
|
+
get: async (workspaceId) => (await client.get(`/workspaces/${workspaceId}`)).data,
|
|
1060
|
+
create: async (data) => (await client.post('/workspaces', data)).data,
|
|
1061
|
+
update: async (workspaceId, data) => (await client.patch(`/workspaces/${workspaceId}`, data)).data,
|
|
1062
|
+
archive: async (workspaceId) => (await client.post(`/workspaces/${workspaceId}/archive`)).data,
|
|
1063
|
+
restore: async (workspaceId) => (await client.post(`/workspaces/${workspaceId}/restore`)).data,
|
|
1064
|
+
delete: async (workspaceId) => (await client.delete(`/workspaces/${workspaceId}`)).data,
|
|
1065
|
+
},
|
|
1066
|
+
blueprints: {
|
|
1067
|
+
list: async ({ scope, visibility, ownerOnly = false } = {}) => {
|
|
1068
|
+
const params = new URLSearchParams();
|
|
1069
|
+
if (scope) params.append('scope', scope);
|
|
1070
|
+
if (visibility) params.append('visibility', visibility);
|
|
1071
|
+
if (ownerOnly) params.append('owner_only', 'true');
|
|
1072
|
+
const qs = params.toString();
|
|
1073
|
+
return (await client.get(`/blueprints${qs ? `?${qs}` : ''}`)).data;
|
|
1074
|
+
},
|
|
1075
|
+
get: async (blueprintId) => (await client.get(`/blueprints/${blueprintId}`)).data,
|
|
1076
|
+
create: async (data) => (await client.post('/blueprints', data)).data,
|
|
1077
|
+
update: async (blueprintId, data) => (await client.patch(`/blueprints/${blueprintId}`, data)).data,
|
|
1078
|
+
delete: async (blueprintId) => (await client.delete(`/blueprints/${blueprintId}`)).data,
|
|
1079
|
+
fork: async (blueprintId, data) => (await client.post(`/blueprints/${blueprintId}/fork`, data)).data,
|
|
1080
|
+
saveFromPlan: async (planId, data = {}) => (await client.post(`/blueprints/from_plan/${planId}`, data)).data,
|
|
1081
|
+
},
|
|
991
1082
|
axiosInstance: client,
|
|
992
1083
|
};
|
|
993
1084
|
}
|
|
@@ -1013,12 +1104,15 @@ module.exports = {
|
|
|
1013
1104
|
tokens,
|
|
1014
1105
|
organizations,
|
|
1015
1106
|
goals,
|
|
1107
|
+
workspaces,
|
|
1108
|
+
blueprints,
|
|
1016
1109
|
context,
|
|
1017
1110
|
graphiti,
|
|
1018
1111
|
dependencies,
|
|
1019
1112
|
coherence,
|
|
1020
1113
|
users,
|
|
1021
1114
|
agentLoop,
|
|
1115
|
+
v1,
|
|
1022
1116
|
axiosInstance, // Export for direct API calls
|
|
1023
1117
|
createApiClient // Factory for per-session clients (HTTP mode)
|
|
1024
1118
|
};
|
package/src/cli/local-client.js
CHANGED
|
@@ -195,13 +195,15 @@ function renderPlanHealth(plan) {
|
|
|
195
195
|
function renderCoherenceWarning(task) {
|
|
196
196
|
if (!task || !task.coherence_status) return [];
|
|
197
197
|
const status = task.coherence_status;
|
|
198
|
-
if (status === 'clean' || status === 'unchecked') return [];
|
|
198
|
+
if (status === 'ok' || status === 'coherent' || status === 'clean' || status === 'unchecked') return [];
|
|
199
199
|
|
|
200
200
|
const out = ['## Coherence warning', ''];
|
|
201
201
|
out.push(`- Status: ${status}`);
|
|
202
|
-
|
|
202
|
+
// Accept both the public vocabulary and the legacy internal values so the
|
|
203
|
+
// CLI keeps working against older backends.
|
|
204
|
+
if (status === 'contradicted' || status === 'contradiction_detected') {
|
|
203
205
|
out.push('- Supporting knowledge contains contradictions. Run `check_contradictions` (MCP) and re-verify before acting.');
|
|
204
|
-
} else if (status === 'stale_beliefs') {
|
|
206
|
+
} else if (status === 'outdated' || status === 'stale_beliefs') {
|
|
205
207
|
out.push('- Knowledge backing this task may be outdated. Run `recall_knowledge` (MCP) to refresh before deciding.');
|
|
206
208
|
}
|
|
207
209
|
out.push('');
|
package/src/tools/bdi/_shared.js
CHANGED
|
@@ -26,4 +26,16 @@ function safeArray(value) {
|
|
|
26
26
|
return Array.isArray(value) ? value : [];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* True when an error means the backend has no /v1 surface (pre-consolidation
|
|
31
|
+
* self-hosted API). Express returns a default 404 with no structured body for
|
|
32
|
+
* unmatched routes, whereas v1 handlers always return JSON with an `error`
|
|
33
|
+
* field — so a bare 404 means "route missing", not "resource missing".
|
|
34
|
+
*/
|
|
35
|
+
function isV1Unavailable(err) {
|
|
36
|
+
if (err.response?.status !== 404) return false;
|
|
37
|
+
const body = err.response.data;
|
|
38
|
+
return !(body && typeof body === 'object' && body.error);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { asOf, formatResponse, errorResponse, safeArray, isV1Unavailable };
|
package/src/tools/bdi/beliefs.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* plan_analysis. Each answers one whole agentic question and returns `as_of`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const { asOf, formatResponse, errorResponse, safeArray } = require('./_shared');
|
|
8
|
+
const { asOf, formatResponse, errorResponse, safeArray, isV1Unavailable } = require('./_shared');
|
|
9
9
|
|
|
10
10
|
// ─────────────────────────────────────────────────────────────────────────
|
|
11
11
|
// briefing — bundled mission control state. Replaces 4 round trips.
|
|
@@ -30,15 +30,24 @@ const briefingDefinition = {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
async function briefingHandler(args, apiClient) {
|
|
33
|
+
const briefingParams = {
|
|
34
|
+
scope: args.scope,
|
|
35
|
+
goal_id: args.goal_id,
|
|
36
|
+
plan_id: args.plan_id,
|
|
37
|
+
recent_window_hours: args.recent_window_hours,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// v1 public path first; same server-side handler as /agent/briefing.
|
|
41
|
+
if (apiClient.v1) {
|
|
42
|
+
try {
|
|
43
|
+
return formatResponse(await apiClient.v1.briefing(briefingParams));
|
|
44
|
+
} catch {
|
|
45
|
+
// Fall through to the internal facade path, then the legacy fan-out.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
33
49
|
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
|
-
});
|
|
50
|
+
const response = await apiClient.axiosInstance.get('/agent/briefing', { params: briefingParams });
|
|
42
51
|
return formatResponse(response.data);
|
|
43
52
|
} catch {
|
|
44
53
|
// Fall back to the pre-facade fan-out for self-hosted older APIs.
|
|
@@ -232,6 +241,22 @@ const goalStateDefinition = {
|
|
|
232
241
|
|
|
233
242
|
async function goalStateHandler(args, apiClient) {
|
|
234
243
|
const { goal_id } = args;
|
|
244
|
+
|
|
245
|
+
// v1 facade: one server-side call replaces the 5-endpoint fan-out below.
|
|
246
|
+
// Same response shape — the server composition was ported from this handler.
|
|
247
|
+
if (apiClient.v1) {
|
|
248
|
+
try {
|
|
249
|
+
return formatResponse(await apiClient.v1.goalState(goal_id));
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if (!isV1Unavailable(err)) {
|
|
252
|
+
const status = err.response?.status;
|
|
253
|
+
if (status === 404) return errorResponse('not_found', `Goal ${goal_id} not found`);
|
|
254
|
+
if (status === 403) return errorResponse('forbidden', 'Access denied to this goal');
|
|
255
|
+
// 5xx and friends: fall through to the legacy fan-out best-effort path.
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
235
260
|
const [goalRes, qualityRes, progressRes, gapsRes, pathRes] = await Promise.allSettled([
|
|
236
261
|
apiClient.goals.get(goal_id),
|
|
237
262
|
apiClient.goals.getQuality(goal_id),
|
|
@@ -283,7 +308,7 @@ async function goalStateHandler(args, apiClient) {
|
|
|
283
308
|
as_of: asOf(),
|
|
284
309
|
goal: {
|
|
285
310
|
id: goal.id, title: goal.title, description: goal.description,
|
|
286
|
-
type: goal.type,
|
|
311
|
+
type: goal.type, committed: Boolean(goal.committed ?? goal.promotedAt ?? goal.promoted_at),
|
|
287
312
|
status: goal.status, priority: goal.priority,
|
|
288
313
|
owner_id: goal.ownerId || goal.owner_id, success_criteria: goal.successCriteria || goal.success_criteria,
|
|
289
314
|
promoted_at: goal.promotedAt || goal.promoted_at,
|
|
@@ -330,6 +355,22 @@ const recallKnowledgeDefinition = {
|
|
|
330
355
|
|
|
331
356
|
async function recallKnowledgeHandler(args, apiClient) {
|
|
332
357
|
const { query, scope = {}, since, entry_type = 'all', result_kind = 'all', max_results = 10, include_contradictions = false } = args;
|
|
358
|
+
|
|
359
|
+
// v1 facade: one server-side call replaces the 4-endpoint fan-out below.
|
|
360
|
+
if (apiClient.v1) {
|
|
361
|
+
try {
|
|
362
|
+
const data = await apiClient.v1.knowledgeSearch({
|
|
363
|
+
query, since, entry_type, result_kind, max_results, include_contradictions, ...scope,
|
|
364
|
+
});
|
|
365
|
+
return formatResponse(data);
|
|
366
|
+
} catch (err) {
|
|
367
|
+
if (!isV1Unavailable(err)) {
|
|
368
|
+
// v1 exists but errored — keep going with the legacy fan-out, which
|
|
369
|
+
// already treats every sub-call as best-effort.
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
333
374
|
const wantFacts = result_kind === 'all' || result_kind === 'facts';
|
|
334
375
|
const wantEntities = result_kind === 'all' || result_kind === 'entities';
|
|
335
376
|
const wantEpisodes = result_kind === 'all' || result_kind === 'episodes';
|
|
@@ -399,6 +440,7 @@ const listPlansDefinition = {
|
|
|
399
440
|
status: { type: 'array', items: { type: 'string' } },
|
|
400
441
|
visibility: { type: 'array', items: { type: 'string', enum: ['private', 'unlisted', 'public'] } },
|
|
401
442
|
query: { type: 'string', description: 'Substring match on title (case-insensitive)' },
|
|
443
|
+
workspace_id: { type: 'string', description: 'Scope to plans inside a single workspace' },
|
|
402
444
|
limit: { type: 'integer', default: 50 },
|
|
403
445
|
},
|
|
404
446
|
},
|
|
@@ -414,6 +456,9 @@ async function listPlansHandler(args, apiClient) {
|
|
|
414
456
|
|
|
415
457
|
if (filter.status?.length) plans = plans.filter((p) => filter.status.includes(p.status));
|
|
416
458
|
if (filter.visibility?.length) plans = plans.filter((p) => filter.visibility.includes(p.visibility));
|
|
459
|
+
if (filter.workspace_id) {
|
|
460
|
+
plans = plans.filter((p) => (p.workspace_id || p.workspaceId) === filter.workspace_id);
|
|
461
|
+
}
|
|
417
462
|
if (filter.query) {
|
|
418
463
|
const q = filter.query.toLowerCase();
|
|
419
464
|
plans = plans.filter((p) => (p.title || '').toLowerCase().includes(q));
|
package/src/tools/bdi/desires.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* BDI desires — goal management.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* link/unlink and achiever changes),
|
|
6
|
-
* under an existing parent
|
|
4
|
+
* 4 tools: list_goals (with health rollup), update_goal (atomic, subsumes
|
|
5
|
+
* link/unlink and achiever changes), create_goal (new top-level goal), and
|
|
6
|
+
* derive_subgoal (a sub-goal under an existing parent). Agents create goals
|
|
7
|
+
* directly — no UI round-trip and no forced approval gate.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
const { asOf, formatResponse, errorResponse, safeArray } = require('./_shared');
|
|
@@ -21,6 +22,7 @@ const listGoalsDefinition = {
|
|
|
21
22
|
properties: {
|
|
22
23
|
health: { type: 'array', items: { type: 'string', enum: ['on_track', 'at_risk', 'stale'] } },
|
|
23
24
|
status: { type: 'array', items: { type: 'string' } },
|
|
25
|
+
workspace_id: { type: 'string', description: 'Scope to goals inside a single workspace' },
|
|
24
26
|
include_inactive: { type: 'boolean', default: false },
|
|
25
27
|
},
|
|
26
28
|
},
|
|
@@ -32,7 +34,7 @@ async function listGoalsHandler(args, apiClient) {
|
|
|
32
34
|
const filter = args.filter || {};
|
|
33
35
|
try {
|
|
34
36
|
const [listRes, dashboardRes] = await Promise.allSettled([
|
|
35
|
-
apiClient.goals.list({ status: filter.include_inactive ? undefined : 'active' }),
|
|
37
|
+
apiClient.goals.list({ status: filter.include_inactive ? undefined : 'active', workspaceId: filter.workspace_id }),
|
|
36
38
|
apiClient.goals.getDashboard(),
|
|
37
39
|
]);
|
|
38
40
|
|
|
@@ -88,9 +90,10 @@ const updateGoalDefinition = {
|
|
|
88
90
|
description: { type: 'string' },
|
|
89
91
|
priority: { type: 'integer' },
|
|
90
92
|
status: { type: 'string' },
|
|
91
|
-
|
|
93
|
+
// Commitment: true once the goal is promoted to active execution
|
|
94
|
+
// (replaces the old desire/intention goal_type vocabulary).
|
|
95
|
+
committed: { type: 'boolean' },
|
|
92
96
|
success_criteria: {},
|
|
93
|
-
promote_to_intention: { type: 'boolean' },
|
|
94
97
|
add_linked_plans: { type: 'array', items: { type: 'string' } },
|
|
95
98
|
remove_linked_plans: { type: 'array', items: { type: 'string' } },
|
|
96
99
|
add_achievers: { type: 'array', items: { type: 'string' } },
|
|
@@ -112,8 +115,12 @@ async function updateGoalHandler(args, apiClient) {
|
|
|
112
115
|
for (const k of ['title', 'description', 'priority', 'status', 'success_criteria']) {
|
|
113
116
|
if (changes[k] !== undefined) directFields[k] = changes[k];
|
|
114
117
|
}
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
// Map the public `committed` boolean onto the backend's commitment write
|
|
119
|
+
// (the API still accepts the legacy goalType field and translates it to
|
|
120
|
+
// promoted_at). committed:true ⇒ promoted, false ⇒ aspirational.
|
|
121
|
+
if (changes.committed !== undefined) {
|
|
122
|
+
directFields.goalType = changes.committed ? 'intention' : 'desire';
|
|
123
|
+
}
|
|
117
124
|
|
|
118
125
|
if (Object.keys(directFields).length) {
|
|
119
126
|
try {
|
|
@@ -156,8 +163,8 @@ async function updateGoalHandler(args, apiClient) {
|
|
|
156
163
|
}
|
|
157
164
|
|
|
158
165
|
// ─────────────────────────────────────────────────────────────────────────
|
|
159
|
-
// derive_subgoal —
|
|
160
|
-
//
|
|
166
|
+
// derive_subgoal — create a sub-goal under an existing parent. For a new
|
|
167
|
+
// top-level goal use create_goal.
|
|
161
168
|
// ─────────────────────────────────────────────────────────────────────────
|
|
162
169
|
|
|
163
170
|
const VALID_GOAL_TYPES = ['outcome', 'constraint', 'metric', 'principle'];
|
|
@@ -166,11 +173,10 @@ const VALID_STATUSES = ['draft', 'active', 'achieved', 'paused', 'abandoned', 'a
|
|
|
166
173
|
const deriveSubgoalDefinition = {
|
|
167
174
|
name: 'derive_subgoal',
|
|
168
175
|
description:
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
"
|
|
173
|
-
"Drafts surface in the dashboard pending queue.",
|
|
176
|
+
"Create a sub-goal under an existing parent goal (parent_goal_id required). " +
|
|
177
|
+
"For a new top-level goal, use create_goal instead. Defaults to " +
|
|
178
|
+
"status='active'; pass status='draft' for autonomous loops so a human can " +
|
|
179
|
+
"review before promotion. Drafts surface in the dashboard pending queue.",
|
|
174
180
|
inputSchema: {
|
|
175
181
|
type: 'object',
|
|
176
182
|
properties: {
|
|
@@ -254,11 +260,85 @@ async function deriveSubgoalHandler(args, apiClient) {
|
|
|
254
260
|
});
|
|
255
261
|
}
|
|
256
262
|
|
|
263
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
264
|
+
// create_goal — create a top-level goal directly (no parent).
|
|
265
|
+
// Agents create goals when a human asks them to; there is no UI round-trip and
|
|
266
|
+
// no forced approval gate. Defaults to status='active'.
|
|
267
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
const createGoalDefinition = {
|
|
270
|
+
name: 'create_goal',
|
|
271
|
+
description:
|
|
272
|
+
"Create a new top-level goal (no parent). Use this when a human asks you to " +
|
|
273
|
+
"set up a goal — agents create goals directly, no UI step required. For a " +
|
|
274
|
+
"goal that contributes to an existing one, use derive_subgoal instead. " +
|
|
275
|
+
"Defaults to status='active' (live immediately); pass status='draft' only " +
|
|
276
|
+
"if you want it to sit in the pending queue for review. Lands in the user's " +
|
|
277
|
+
"active organization's default workspace unless workspace_id is given.",
|
|
278
|
+
inputSchema: {
|
|
279
|
+
type: 'object',
|
|
280
|
+
properties: {
|
|
281
|
+
title: { type: 'string', description: "The goal statement." },
|
|
282
|
+
description: { type: 'string', description: "What the goal means / context." },
|
|
283
|
+
type: {
|
|
284
|
+
type: 'string',
|
|
285
|
+
enum: VALID_GOAL_TYPES,
|
|
286
|
+
default: 'outcome',
|
|
287
|
+
description: "outcome (end state), metric (quantitative target), constraint (must-not-violate), principle (durable invariant).",
|
|
288
|
+
},
|
|
289
|
+
status: {
|
|
290
|
+
type: 'string',
|
|
291
|
+
enum: VALID_STATUSES,
|
|
292
|
+
default: 'active',
|
|
293
|
+
description: "Default 'active' (live). Pass 'draft' to propose without activating.",
|
|
294
|
+
},
|
|
295
|
+
success_criteria: {
|
|
296
|
+
type: 'array',
|
|
297
|
+
items: { type: 'string' },
|
|
298
|
+
description: "Concrete, observable conditions that mark this goal achieved.",
|
|
299
|
+
},
|
|
300
|
+
priority: { type: 'integer', minimum: 0, maximum: 10, default: 0 },
|
|
301
|
+
workspace_id: { type: 'string', description: "Optional. Target workspace; defaults to the active org's default workspace." },
|
|
302
|
+
},
|
|
303
|
+
required: ['title'],
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
async function createGoalHandler(args, apiClient) {
|
|
308
|
+
const { title, description, type = 'outcome', status = 'active', success_criteria, priority, workspace_id } = args;
|
|
309
|
+
|
|
310
|
+
const payload = { title, type, status };
|
|
311
|
+
if (description) payload.description = description;
|
|
312
|
+
if (success_criteria) payload.successCriteria = { criteria: success_criteria };
|
|
313
|
+
if (typeof priority === 'number') payload.priority = priority;
|
|
314
|
+
if (workspace_id) payload.workspaceId = workspace_id;
|
|
315
|
+
|
|
316
|
+
let goal;
|
|
317
|
+
try {
|
|
318
|
+
goal = await apiClient.goals.create(payload);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
const upstream = err.response?.data?.error || err.message;
|
|
321
|
+
return errorResponse('create_failed', `Failed to create goal: ${upstream}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return formatResponse({
|
|
325
|
+
as_of: asOf(),
|
|
326
|
+
goal_id: goal.id,
|
|
327
|
+
title: goal.title,
|
|
328
|
+
status: goal.status,
|
|
329
|
+
is_draft: goal.status === 'draft',
|
|
330
|
+
next_step: goal.status === 'draft'
|
|
331
|
+
? "Goal created as draft. Promote via update_goal({status: 'active'}) once ready."
|
|
332
|
+
: "Goal is active. Add sub-goals with derive_subgoal, or link plans via update_goal({add_linked_plans: [...]}).",
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
257
336
|
module.exports = {
|
|
258
|
-
definitions: [listGoalsDefinition, updateGoalDefinition, deriveSubgoalDefinition],
|
|
337
|
+
definitions: [listGoalsDefinition, updateGoalDefinition, createGoalDefinition, deriveSubgoalDefinition],
|
|
259
338
|
handlers: {
|
|
260
339
|
list_goals: listGoalsHandler,
|
|
261
340
|
update_goal: updateGoalHandler,
|
|
341
|
+
create_goal: createGoalHandler,
|
|
262
342
|
derive_subgoal: deriveSubgoalHandler,
|
|
263
343
|
},
|
|
264
344
|
};
|
package/src/tools/bdi/index.js
CHANGED
|
@@ -12,12 +12,14 @@ const beliefs = require('./beliefs');
|
|
|
12
12
|
const desires = require('./desires');
|
|
13
13
|
const intentions = require('./intentions');
|
|
14
14
|
const utility = require('./utility');
|
|
15
|
+
const workspaces = require('./workspaces');
|
|
15
16
|
|
|
16
17
|
const definitions = [
|
|
17
18
|
...beliefs.definitions,
|
|
18
19
|
...desires.definitions,
|
|
19
20
|
...intentions.definitions,
|
|
20
21
|
...utility.definitions,
|
|
22
|
+
...workspaces.definitions,
|
|
21
23
|
];
|
|
22
24
|
|
|
23
25
|
const handlers = {
|
|
@@ -25,6 +27,7 @@ const handlers = {
|
|
|
25
27
|
...desires.handlers,
|
|
26
28
|
...intentions.handlers,
|
|
27
29
|
...utility.handlers,
|
|
30
|
+
...workspaces.handlers,
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
const names = new Set(definitions.map((t) => t.name));
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* See ../../../docs/MCP_v1.0_FULL_SURFACE.md for design rationale.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
const { asOf, formatResponse, errorResponse } = require('./_shared');
|
|
16
|
+
const { asOf, formatResponse, errorResponse, isV1Unavailable } = require('./_shared');
|
|
17
17
|
|
|
18
18
|
// ─────────────────────────────────────────────────────────────────────────
|
|
19
19
|
// queue_decision — real decision queue. Replaces add_learning workaround.
|
|
@@ -312,6 +312,28 @@ async function updateTaskHandler(args, apiClient) {
|
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
// v1 facade: one atomic server-side call (status + log + claim release +
|
|
316
|
+
// learning) replaces the 4-endpoint fan-out below. Same response shape.
|
|
317
|
+
if (apiClient.v1) {
|
|
318
|
+
try {
|
|
319
|
+
const data = await apiClient.v1.updateTask(task_id, {
|
|
320
|
+
status,
|
|
321
|
+
log_message,
|
|
322
|
+
log_type: args.log_type,
|
|
323
|
+
release_claim,
|
|
324
|
+
add_learning,
|
|
325
|
+
});
|
|
326
|
+
return formatResponse(data);
|
|
327
|
+
} catch (err) {
|
|
328
|
+
if (!isV1Unavailable(err)) {
|
|
329
|
+
const s = err.response?.status;
|
|
330
|
+
if (s === 404) return errorResponse('not_found', `Task ${task_id} not found`);
|
|
331
|
+
if (s === 403) return errorResponse('forbidden', 'Access denied to this plan');
|
|
332
|
+
// 5xx: fall through to the legacy fan-out.
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
315
337
|
// Resolve plan_id from task if not provided.
|
|
316
338
|
if (!planId) {
|
|
317
339
|
try {
|
|
@@ -345,7 +367,7 @@ async function updateTaskHandler(args, apiClient) {
|
|
|
345
367
|
if (log_message) {
|
|
346
368
|
const logType = args.log_type || STATUS_TO_LOG_TYPE[status] || 'progress';
|
|
347
369
|
try {
|
|
348
|
-
const log = await apiClient.logs.
|
|
370
|
+
const log = await apiClient.logs.addLogEntry(planId, task_id, {
|
|
349
371
|
content: log_message,
|
|
350
372
|
log_type: logType,
|
|
351
373
|
});
|
|
@@ -427,16 +449,27 @@ async function claimNextTaskHandler(args, apiClient) {
|
|
|
427
449
|
const { scope = {}, ttl_minutes = 30, fresh = false, context_depth = 2, dry_run = false } = args;
|
|
428
450
|
const { plan_id, goal_id } = scope;
|
|
429
451
|
|
|
452
|
+
const sessionBody = {
|
|
453
|
+
plan_id,
|
|
454
|
+
goal_id,
|
|
455
|
+
ttl_minutes,
|
|
456
|
+
fresh,
|
|
457
|
+
dry_run,
|
|
458
|
+
depth: context_depth,
|
|
459
|
+
agent_id: 'mcp-agent',
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// v1 public path first; same server-side handler as /agent/work-sessions.
|
|
463
|
+
if (apiClient.v1) {
|
|
464
|
+
try {
|
|
465
|
+
return formatResponse(await apiClient.v1.claimNext(sessionBody));
|
|
466
|
+
} catch {
|
|
467
|
+
// Fall through to the internal facade path, then the legacy fan-out.
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
430
471
|
try {
|
|
431
|
-
const response = await apiClient.axiosInstance.post('/agent/work-sessions',
|
|
432
|
-
plan_id,
|
|
433
|
-
goal_id,
|
|
434
|
-
ttl_minutes,
|
|
435
|
-
fresh,
|
|
436
|
-
dry_run,
|
|
437
|
-
depth: context_depth,
|
|
438
|
-
agent_id: 'mcp-agent',
|
|
439
|
-
});
|
|
472
|
+
const response = await apiClient.axiosInstance.post('/agent/work-sessions', sessionBody);
|
|
440
473
|
return formatResponse(response.data);
|
|
441
474
|
} catch {
|
|
442
475
|
// Fall back to the pre-facade fan-out for self-hosted older APIs.
|
|
@@ -449,8 +482,8 @@ async function claimNextTaskHandler(args, apiClient) {
|
|
|
449
482
|
if (!fresh) {
|
|
450
483
|
try {
|
|
451
484
|
const myTasks = await apiClient.users.getMyTasks({ plan_id });
|
|
452
|
-
|
|
453
|
-
if (plan_id) tasks.filter((t) => t.plan_id === plan_id);
|
|
485
|
+
let tasks = (myTasks.tasks || myTasks || []).filter((t) => t.status === 'in_progress');
|
|
486
|
+
if (plan_id) tasks = tasks.filter((t) => t.plan_id === plan_id);
|
|
454
487
|
if (tasks[0]) {
|
|
455
488
|
chosen = tasks[0];
|
|
456
489
|
source = 'resume_in_progress';
|
|
@@ -458,20 +491,49 @@ async function claimNextTaskHandler(args, apiClient) {
|
|
|
458
491
|
} catch {}
|
|
459
492
|
}
|
|
460
493
|
|
|
461
|
-
// 2.
|
|
494
|
+
// 2. Dependency-aware suggestion via /context/suggest — the real endpoint
|
|
495
|
+
// (only ready, unclaimed tasks, in plan order). Track reachability so we
|
|
496
|
+
// can fail closed below: an empty result from a reachable endpoint means
|
|
497
|
+
// remaining work is dep-blocked, NOT "grab any not_started task".
|
|
498
|
+
let suggestReachable = false;
|
|
462
499
|
if (!chosen && plan_id) {
|
|
463
500
|
try {
|
|
464
501
|
const params = new URLSearchParams({ plan_id, limit: '1' });
|
|
465
|
-
const r = await apiClient.axiosInstance.get(`/
|
|
466
|
-
|
|
502
|
+
const r = await apiClient.axiosInstance.get(`/context/suggest?${params}`);
|
|
503
|
+
suggestReachable = true;
|
|
504
|
+
const suggested = (r.data?.suggestions || r.data?.tasks || r.data || [])[0];
|
|
467
505
|
if (suggested) {
|
|
468
506
|
chosen = suggested;
|
|
469
507
|
source = 'suggest_next_tasks';
|
|
470
508
|
}
|
|
509
|
+
} catch {
|
|
510
|
+
// Endpoint unreachable (pre-suggest self-hosted API). Leave
|
|
511
|
+
// suggestReachable=false so the last-resort blind pick can run.
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// 3. Fail-closed gate. When the dependency-aware endpoint is reachable but
|
|
516
|
+
// returned nothing, all remaining not_started work is blocked on
|
|
517
|
+
// incomplete dependencies — mirror the backend and refuse to hand out a
|
|
518
|
+
// dep-blind task. Only the truly-unreachable case falls through to (4).
|
|
519
|
+
if (!chosen && plan_id && suggestReachable) {
|
|
520
|
+
let hasNotStarted = false;
|
|
521
|
+
try {
|
|
522
|
+
const myTasks = await apiClient.users.getMyTasks({ plan_id });
|
|
523
|
+
hasNotStarted = (myTasks.tasks || myTasks || []).some((t) => t.status === 'not_started');
|
|
471
524
|
} catch {}
|
|
525
|
+
return errorResponse(
|
|
526
|
+
'not_found',
|
|
527
|
+
hasNotStarted
|
|
528
|
+
? 'All remaining tasks are blocked on incomplete dependencies'
|
|
529
|
+
: 'No actionable task found in scope',
|
|
530
|
+
{ reason: hasNotStarted ? 'blocked_on_dep' : 'no_work_in_scope' },
|
|
531
|
+
);
|
|
472
532
|
}
|
|
473
533
|
|
|
474
|
-
//
|
|
534
|
+
// 4. Last-resort blind fallback — ONLY when the dep-aware endpoint could not
|
|
535
|
+
// be reached at all (ancient self-hosted API with neither work-sessions
|
|
536
|
+
// nor /context/suggest). Against any current backend this never runs.
|
|
475
537
|
if (!chosen) {
|
|
476
538
|
try {
|
|
477
539
|
const myTasks = await apiClient.users.getMyTasks({ plan_id });
|
|
@@ -584,7 +646,7 @@ async function releaseTaskHandler(args, apiClient) {
|
|
|
584
646
|
let logId = null;
|
|
585
647
|
if (message) {
|
|
586
648
|
try {
|
|
587
|
-
const log = await apiClient.logs.
|
|
649
|
+
const log = await apiClient.logs.addLogEntry(planId, task_id, { content: message, log_type: 'progress' });
|
|
588
650
|
logId = log?.id || log?.log?.id;
|
|
589
651
|
} catch {}
|
|
590
652
|
}
|
|
@@ -1024,7 +1086,10 @@ async function proposeResearchChainHandler(args, apiClient) {
|
|
|
1024
1086
|
// Cycle detection happens server-side; we surface the 409 cleanly.
|
|
1025
1087
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1026
1088
|
|
|
1027
|
-
|
|
1089
|
+
// Canonical node→node dependency vocabulary (backend ring-2 consolidation).
|
|
1090
|
+
// `requires` is still accepted by the API (mapped to `blocks`) but no longer
|
|
1091
|
+
// offered here.
|
|
1092
|
+
const VALID_RELATIONS = ['blocks', 'relates_to'];
|
|
1028
1093
|
|
|
1029
1094
|
const linkIntentionsDefinition = {
|
|
1030
1095
|
name: 'link_intentions',
|
|
@@ -1480,6 +1545,28 @@ const sharePlanDefinition = {
|
|
|
1480
1545
|
|
|
1481
1546
|
async function sharePlanHandler(args, apiClient) {
|
|
1482
1547
|
const { plan_id, visibility, add_collaborators = [], remove_collaborators = [] } = args;
|
|
1548
|
+
|
|
1549
|
+
// v1 facade: one atomic server-side call replaces the per-collaborator
|
|
1550
|
+
// fan-out below. Same response shape (applied_changes + failures).
|
|
1551
|
+
if (apiClient.v1) {
|
|
1552
|
+
try {
|
|
1553
|
+
const data = await apiClient.v1.sharePlan(plan_id, {
|
|
1554
|
+
visibility,
|
|
1555
|
+
add_collaborators,
|
|
1556
|
+
remove_collaborators,
|
|
1557
|
+
});
|
|
1558
|
+
return formatResponse(data);
|
|
1559
|
+
} catch (err) {
|
|
1560
|
+
if (!isV1Unavailable(err)) {
|
|
1561
|
+
const s = err.response?.status;
|
|
1562
|
+
if (s === 400 || s === 403 || s === 404) {
|
|
1563
|
+
return errorResponse('invalid_arg', err.response?.data?.error || err.message);
|
|
1564
|
+
}
|
|
1565
|
+
// 5xx: fall through to the legacy fan-out.
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1483
1570
|
const applied = [];
|
|
1484
1571
|
const failures = [];
|
|
1485
1572
|
|
package/src/tools/bdi/utility.js
CHANGED
|
@@ -29,6 +29,7 @@ async function getStartedHandler(args) {
|
|
|
29
29
|
beliefs: ['briefing', 'list_plans', 'task_context', 'goal_state', 'recall_knowledge', 'search', 'plan_analysis'],
|
|
30
30
|
desires: ['list_goals', 'update_goal'],
|
|
31
31
|
intentions: ['claim_next_task', 'update_task', 'release_task', 'queue_decision', 'resolve_decision', 'add_learning'],
|
|
32
|
+
workspaces: ['list_workspaces', 'create_workspace', 'list_blueprints', 'fork_blueprint', 'save_as_blueprint'],
|
|
32
33
|
},
|
|
33
34
|
recommended_workflows: [
|
|
34
35
|
{
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspaces and Blueprints — organizational structure tools.
|
|
3
|
+
*
|
|
4
|
+
* Workspace = live folder under an Organization (owns goals + plans).
|
|
5
|
+
* Blueprint = dehydrated reusable shape, forks into a Workspace or Plan.
|
|
6
|
+
*
|
|
7
|
+
* v1 supports plan-scope blueprints only. See
|
|
8
|
+
* agent-planner/docs/WORKSPACE_BLUEPRINT_SKETCH.md for the design.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { asOf, formatResponse, errorResponse, safeArray } = require('./_shared');
|
|
12
|
+
|
|
13
|
+
// ─── Workspaces ──────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const listWorkspacesDefinition = {
|
|
16
|
+
name: 'list_workspaces',
|
|
17
|
+
description:
|
|
18
|
+
"List workspaces in an organization. A workspace is a folder that owns " +
|
|
19
|
+
"goals + plans. Returns archived workspaces only when include_archived=true.",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
organization_id: { type: 'string', description: "Required. Organization to scope to." },
|
|
24
|
+
include_archived: { type: 'boolean', default: false },
|
|
25
|
+
},
|
|
26
|
+
required: ['organization_id'],
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
async function listWorkspacesHandler(args, apiClient) {
|
|
31
|
+
const { organization_id, include_archived } = args;
|
|
32
|
+
try {
|
|
33
|
+
const data = await apiClient.workspaces.list({
|
|
34
|
+
organizationId: organization_id,
|
|
35
|
+
includeArchived: include_archived === true,
|
|
36
|
+
});
|
|
37
|
+
return formatResponse({
|
|
38
|
+
as_of: asOf(),
|
|
39
|
+
workspaces: safeArray(data.workspaces || data),
|
|
40
|
+
});
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return errorResponse('upstream_unavailable', `list_workspaces failed: ${err.message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const createWorkspaceDefinition = {
|
|
47
|
+
name: 'create_workspace',
|
|
48
|
+
description:
|
|
49
|
+
"Create a new workspace inside an organization. Returns the new workspace " +
|
|
50
|
+
"row. The slug is auto-generated from the title and de-duplicated within " +
|
|
51
|
+
"the org.",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
organization_id: { type: 'string' },
|
|
56
|
+
title: { type: 'string' },
|
|
57
|
+
description: { type: 'string' },
|
|
58
|
+
icon: { type: 'string', description: "Optional emoji or icon token." },
|
|
59
|
+
slug: { type: 'string', description: "Optional. Auto-generated from title if omitted." },
|
|
60
|
+
},
|
|
61
|
+
required: ['organization_id', 'title'],
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
async function createWorkspaceHandler(args, apiClient) {
|
|
66
|
+
const { organization_id, title, description, icon, slug } = args;
|
|
67
|
+
try {
|
|
68
|
+
const ws = await apiClient.workspaces.create({
|
|
69
|
+
organization_id,
|
|
70
|
+
title,
|
|
71
|
+
description,
|
|
72
|
+
icon,
|
|
73
|
+
slug,
|
|
74
|
+
});
|
|
75
|
+
return formatResponse({ as_of: asOf(), workspace: ws });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const upstream = err.response?.data?.error || err.message;
|
|
78
|
+
return errorResponse('create_failed', `create_workspace failed: ${upstream}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Blueprints ──────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
const listBlueprintsDefinition = {
|
|
85
|
+
name: 'list_blueprints',
|
|
86
|
+
description:
|
|
87
|
+
"List blueprints visible to the user (owned + public/unlisted). Filter by " +
|
|
88
|
+
"scope ('plan' or 'workspace'), visibility, or owner_only=true.",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
scope: { type: 'string', enum: ['plan', 'workspace'] },
|
|
93
|
+
visibility: { type: 'string', enum: ['private', 'public', 'unlisted'] },
|
|
94
|
+
owner_only: { type: 'boolean', default: false },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
async function listBlueprintsHandler(args, apiClient) {
|
|
100
|
+
try {
|
|
101
|
+
const data = await apiClient.blueprints.list({
|
|
102
|
+
scope: args.scope,
|
|
103
|
+
visibility: args.visibility,
|
|
104
|
+
ownerOnly: args.owner_only === true,
|
|
105
|
+
});
|
|
106
|
+
return formatResponse({
|
|
107
|
+
as_of: asOf(),
|
|
108
|
+
blueprints: safeArray(data.blueprints || data),
|
|
109
|
+
});
|
|
110
|
+
} catch (err) {
|
|
111
|
+
return errorResponse('upstream_unavailable', `list_blueprints failed: ${err.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const forkBlueprintDefinition = {
|
|
116
|
+
name: 'fork_blueprint',
|
|
117
|
+
description:
|
|
118
|
+
"Fork a plan-scope blueprint into a target workspace. Creates a new plan " +
|
|
119
|
+
"inside that workspace with the blueprint's structure (nodes, " +
|
|
120
|
+
"dependencies, agent_instructions). All node statuses reset to " +
|
|
121
|
+
"'not_started'. The new plan's forked_from_blueprint_id records lineage.",
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
blueprint_id: { type: 'string' },
|
|
126
|
+
workspace_id: { type: 'string', description: "Target workspace the new plan will land in." },
|
|
127
|
+
title: { type: 'string', description: "Optional title override for the new plan." },
|
|
128
|
+
},
|
|
129
|
+
required: ['blueprint_id', 'workspace_id'],
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
async function forkBlueprintHandler(args, apiClient) {
|
|
134
|
+
const { blueprint_id, workspace_id, title } = args;
|
|
135
|
+
try {
|
|
136
|
+
const newPlan = await apiClient.blueprints.fork(blueprint_id, {
|
|
137
|
+
workspace_id,
|
|
138
|
+
title,
|
|
139
|
+
});
|
|
140
|
+
return formatResponse({
|
|
141
|
+
as_of: asOf(),
|
|
142
|
+
plan_id: newPlan.id,
|
|
143
|
+
workspace_id,
|
|
144
|
+
forked_from_blueprint_id: newPlan.forkedFromBlueprintId || blueprint_id,
|
|
145
|
+
title: newPlan.title,
|
|
146
|
+
next_step:
|
|
147
|
+
"Plan is in status='draft'. Open it, claim a task, or promote to 'active' once ready to execute.",
|
|
148
|
+
});
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const upstream = err.response?.data?.error || err.message;
|
|
151
|
+
return errorResponse('fork_failed', `fork_blueprint failed: ${upstream}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const saveAsBlueprintDefinition = {
|
|
156
|
+
name: 'save_as_blueprint',
|
|
157
|
+
description:
|
|
158
|
+
"Snapshot a live plan as a new plan-scope blueprint. Captures structure, " +
|
|
159
|
+
"agent_instructions, and dependencies. Excludes run-state (statuses, " +
|
|
160
|
+
"claims, knowledge episodes, logs, decisions, agent assignments).",
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
plan_id: { type: 'string' },
|
|
165
|
+
title: { type: 'string', description: "Optional. Defaults to the source plan's title." },
|
|
166
|
+
description: { type: 'string' },
|
|
167
|
+
visibility: { type: 'string', enum: ['private', 'public', 'unlisted'], default: 'private' },
|
|
168
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
169
|
+
},
|
|
170
|
+
required: ['plan_id'],
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
async function saveAsBlueprintHandler(args, apiClient) {
|
|
175
|
+
const { plan_id, title, description, visibility, tags } = args;
|
|
176
|
+
try {
|
|
177
|
+
const bp = await apiClient.blueprints.saveFromPlan(plan_id, {
|
|
178
|
+
title,
|
|
179
|
+
description,
|
|
180
|
+
visibility,
|
|
181
|
+
tags,
|
|
182
|
+
});
|
|
183
|
+
return formatResponse({
|
|
184
|
+
as_of: asOf(),
|
|
185
|
+
blueprint_id: bp.id,
|
|
186
|
+
scope: bp.scope,
|
|
187
|
+
visibility: bp.visibility,
|
|
188
|
+
node_count: bp.payload?.nodes?.length ?? null,
|
|
189
|
+
dependency_count: bp.payload?.dependencies?.length ?? null,
|
|
190
|
+
next_step: bp.visibility === 'private'
|
|
191
|
+
? "Blueprint saved privately. Share it via update visibility to 'public' or 'unlisted', or fork it directly via fork_blueprint."
|
|
192
|
+
: "Blueprint published. Anyone with the link (unlisted) or via discovery (public) can fork it.",
|
|
193
|
+
});
|
|
194
|
+
} catch (err) {
|
|
195
|
+
const upstream = err.response?.data?.error || err.message;
|
|
196
|
+
return errorResponse('snapshot_failed', `save_as_blueprint failed: ${upstream}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = {
|
|
201
|
+
definitions: [
|
|
202
|
+
listWorkspacesDefinition,
|
|
203
|
+
createWorkspaceDefinition,
|
|
204
|
+
listBlueprintsDefinition,
|
|
205
|
+
forkBlueprintDefinition,
|
|
206
|
+
saveAsBlueprintDefinition,
|
|
207
|
+
],
|
|
208
|
+
handlers: {
|
|
209
|
+
list_workspaces: listWorkspacesHandler,
|
|
210
|
+
create_workspace: createWorkspaceHandler,
|
|
211
|
+
list_blueprints: listBlueprintsHandler,
|
|
212
|
+
fork_blueprint: forkBlueprintHandler,
|
|
213
|
+
save_as_blueprint: saveAsBlueprintHandler,
|
|
214
|
+
},
|
|
215
|
+
};
|