wolfpack-mcp 1.0.99 → 1.0.101
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/dist/agentObserveTools.js +157 -0
- package/dist/agentObserveTools.test.js +99 -0
- package/dist/agentSelfTools.js +6 -2
- package/dist/client.js +45 -4
- package/dist/index.js +15 -1740
- package/dist/procedureTools.js +4 -268
- package/dist/toolCatalogue.js +2156 -0
- package/dist/toolCatalogue.test.js +96 -0
- package/package.json +1 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watching other agents work (#2388).
|
|
3
|
+
*
|
|
4
|
+
* Registered only when the API key has the `agent_observer` capability, and
|
|
5
|
+
* stdio only — like the agent-self, agent-memory and agent-builder families,
|
|
6
|
+
* these are tools an agent uses about the fleet it runs in, not tools an
|
|
7
|
+
* integration calls over the remote transport, so they have no catalogue entry
|
|
8
|
+
* and no generated remote copy to drift from.
|
|
9
|
+
*
|
|
10
|
+
* The backend decides what comes back: a session is visible to an observer only
|
|
11
|
+
* where the observer is a member of a project the run was scoped to. Nothing in
|
|
12
|
+
* this file can widen that, and the descriptions say so plainly — a coach that
|
|
13
|
+
* believes it can see the whole fleet reports "nothing is stuck" about projects
|
|
14
|
+
* it was never shown.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
function text(data) {
|
|
18
|
+
return JSON.stringify(data, null, 2);
|
|
19
|
+
}
|
|
20
|
+
const PROJECT_BOUND = 'You see a session only if it ran in a project you are a member of — never the whole fleet, ' +
|
|
21
|
+
'and never work done in someone else’s My Space.';
|
|
22
|
+
export const AGENT_OBSERVE_TOOLS = [
|
|
23
|
+
{
|
|
24
|
+
name: 'list_observed_sessions',
|
|
25
|
+
description: "List other agents' sessions across the projects you are a member of, newest first. " +
|
|
26
|
+
'Returns status, trigger, timings, error text, token use and the projects each run was ' +
|
|
27
|
+
'scoped to — no transcripts, no container logs, no system prompts. ' +
|
|
28
|
+
'To find work that may be stuck, pass status "running" with a `since` an hour or two back ' +
|
|
29
|
+
'and look at what has been going longest. ' +
|
|
30
|
+
PROJECT_BOUND,
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
status: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: 'Only sessions in this status: pending, starting, running, stopping, stopped or failed',
|
|
37
|
+
},
|
|
38
|
+
agent_profile_id: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'Only sessions of this agent (UUID). Not found if it is not one you may observe.',
|
|
41
|
+
},
|
|
42
|
+
since: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: 'Only sessions started at or after this ISO 8601 timestamp',
|
|
45
|
+
},
|
|
46
|
+
limit: { type: 'number', description: 'Max sessions to return (default 20)' },
|
|
47
|
+
offset: { type: 'number', description: 'Skip first N sessions (default 0)' },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'get_observed_session',
|
|
53
|
+
description: 'Read one observed session by id. Same fields as the listing. ' +
|
|
54
|
+
'Not found if the session ran in no project of yours.',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
session_id: { type: 'string', description: 'The session UUID' },
|
|
59
|
+
},
|
|
60
|
+
required: ['session_id'],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'get_observed_session_events',
|
|
65
|
+
description: "Read an observed session's activity log: which tools ran, in order, with the one-line " +
|
|
66
|
+
'summary each hook wrote. This is how a run looks from outside — the same tool repeating, ' +
|
|
67
|
+
'or nothing at all for a long time, is what "stuck" looks like. ' +
|
|
68
|
+
'Tool arguments and results are not included.',
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
session_id: { type: 'string', description: 'The session UUID' },
|
|
73
|
+
after: { type: 'string', description: 'Only events after this event id (for polling)' },
|
|
74
|
+
limit: { type: 'number', description: 'Max events to return (default 100)' },
|
|
75
|
+
},
|
|
76
|
+
required: ['session_id'],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'get_observed_session_conversation',
|
|
81
|
+
description: "Read an observed session's conversation. Requires the separate " +
|
|
82
|
+
'mcp:agents:observe_transcripts permission, which most observers do not have and do not ' +
|
|
83
|
+
'need: a transcript carries repository contents, shell output and the agent’s system ' +
|
|
84
|
+
'prompt verbatim. Judge a run from its status and its events first.',
|
|
85
|
+
inputSchema: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
session_id: { type: 'string', description: 'The session UUID' },
|
|
89
|
+
limit: { type: 'number', description: 'Max history entries to return' },
|
|
90
|
+
offset: { type: 'number', description: 'Skip first N history entries' },
|
|
91
|
+
},
|
|
92
|
+
required: ['session_id'],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
const ListObservedSessionsSchema = z.object({
|
|
97
|
+
status: z.string().optional(),
|
|
98
|
+
agent_profile_id: z.string().optional(),
|
|
99
|
+
since: z.string().optional(),
|
|
100
|
+
limit: z.number().optional(),
|
|
101
|
+
offset: z.number().optional(),
|
|
102
|
+
});
|
|
103
|
+
const SessionIdSchema = z.object({
|
|
104
|
+
session_id: z.string(),
|
|
105
|
+
after: z.string().optional(),
|
|
106
|
+
limit: z.number().optional(),
|
|
107
|
+
offset: z.number().optional(),
|
|
108
|
+
});
|
|
109
|
+
export async function handleAgentObserveTool(name, args, client) {
|
|
110
|
+
switch (name) {
|
|
111
|
+
case 'list_observed_sessions': {
|
|
112
|
+
const parsed = ListObservedSessionsSchema.parse(args || {});
|
|
113
|
+
const sessions = await client.listObservedSessions({
|
|
114
|
+
status: parsed.status,
|
|
115
|
+
agentProfileId: parsed.agent_profile_id,
|
|
116
|
+
since: parsed.since,
|
|
117
|
+
limit: parsed.limit,
|
|
118
|
+
offset: parsed.offset,
|
|
119
|
+
});
|
|
120
|
+
if (sessions.length === 0) {
|
|
121
|
+
return {
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: 'text',
|
|
125
|
+
text: 'No sessions match, among the agents and projects you can observe.',
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return { content: [{ type: 'text', text: text(sessions) }] };
|
|
131
|
+
}
|
|
132
|
+
case 'get_observed_session': {
|
|
133
|
+
const parsed = SessionIdSchema.parse(args);
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: 'text', text: text(await client.getObservedSession(parsed.session_id)) }],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
case 'get_observed_session_events': {
|
|
139
|
+
const parsed = SessionIdSchema.parse(args);
|
|
140
|
+
const events = await client.getObservedSessionEvents(parsed.session_id, {
|
|
141
|
+
after: parsed.after,
|
|
142
|
+
limit: parsed.limit,
|
|
143
|
+
});
|
|
144
|
+
return { content: [{ type: 'text', text: text(events) }] };
|
|
145
|
+
}
|
|
146
|
+
case 'get_observed_session_conversation': {
|
|
147
|
+
const parsed = SessionIdSchema.parse(args);
|
|
148
|
+
const conversation = await client.getObservedSessionConversation(parsed.session_id, {
|
|
149
|
+
limit: parsed.limit,
|
|
150
|
+
offset: parsed.offset,
|
|
151
|
+
});
|
|
152
|
+
return { content: [{ type: 'text', text: text(conversation) }] };
|
|
153
|
+
}
|
|
154
|
+
default:
|
|
155
|
+
throw new Error(`Unknown agent observe tool: ${name}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { AGENT_OBSERVE_TOOLS, handleAgentObserveTool } from './agentObserveTools.js';
|
|
3
|
+
import { AGENT_SELF_TOOLS, AGENT_MEMORY_TOOLS } from './agentSelfTools.js';
|
|
4
|
+
import { remoteTools, stdioTools } from './toolCatalogue.js';
|
|
5
|
+
/**
|
|
6
|
+
* #2388 — the observation family's exposure decision, and the shape of the
|
|
7
|
+
* calls it makes.
|
|
8
|
+
*
|
|
9
|
+
* Two things are worth a test here and the rest is the backend's:
|
|
10
|
+
*
|
|
11
|
+
* - **It is stdio only.** The catalogue is what the remote transport serves,
|
|
12
|
+
* and a tool that reached it would be answering an integration key over
|
|
13
|
+
* HTTP rather than an agent in a container. The other agent families made
|
|
14
|
+
* the same decision; this asserts it rather than trusting a comment.
|
|
15
|
+
* - **No agent id is required anywhere.** The whole design is that an observer
|
|
16
|
+
* names a session, not an agent whose sessions it wants — the backend
|
|
17
|
+
* decides reach from the observer's own project memberships. A required
|
|
18
|
+
* `agent_id` would be the first step back towards "read this agent".
|
|
19
|
+
*/
|
|
20
|
+
const names = (tools) => tools.map((t) => t.name);
|
|
21
|
+
describe('the observe tool family', () => {
|
|
22
|
+
it('is the four tools the observe scopes buy', () => {
|
|
23
|
+
expect(names(AGENT_OBSERVE_TOOLS).sort()).toEqual([
|
|
24
|
+
'get_observed_session',
|
|
25
|
+
'get_observed_session_conversation',
|
|
26
|
+
'get_observed_session_events',
|
|
27
|
+
'list_observed_sessions',
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
it('is not in the catalogue, so the remote transport never serves it', () => {
|
|
31
|
+
const remote = names(remoteTools());
|
|
32
|
+
const everyStdioCatalogueTool = names(stdioTools(['procedures']));
|
|
33
|
+
for (const tool of names(AGENT_OBSERVE_TOOLS)) {
|
|
34
|
+
expect(remote).not.toContain(tool);
|
|
35
|
+
expect(everyStdioCatalogueTool).not.toContain(tool);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
it('does not collide with the other stdio-only families', () => {
|
|
39
|
+
const all = [
|
|
40
|
+
...names(AGENT_OBSERVE_TOOLS),
|
|
41
|
+
...names(AGENT_SELF_TOOLS),
|
|
42
|
+
...names(AGENT_MEMORY_TOOLS),
|
|
43
|
+
];
|
|
44
|
+
expect(new Set(all).size).toBe(all.length);
|
|
45
|
+
});
|
|
46
|
+
it('requires a session id and never an agent id', () => {
|
|
47
|
+
for (const tool of AGENT_OBSERVE_TOOLS) {
|
|
48
|
+
const required = tool.inputSchema.required ?? [];
|
|
49
|
+
expect(required).not.toContain('agent_id');
|
|
50
|
+
expect(required).not.toContain('agent_profile_id');
|
|
51
|
+
if (tool.name !== 'list_observed_sessions') {
|
|
52
|
+
expect(required).toEqual(['session_id']);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
// The listing's whole point is the cross-agent question, so the two parameters
|
|
57
|
+
// a "what is stuck right now?" call is made of have to be on it.
|
|
58
|
+
it('offers status and since on the listing', () => {
|
|
59
|
+
const listing = AGENT_OBSERVE_TOOLS.find((t) => t.name === 'list_observed_sessions');
|
|
60
|
+
expect(Object.keys(listing.inputSchema.properties)).toEqual(expect.arrayContaining(['status', 'since', 'agent_profile_id', 'limit', 'offset']));
|
|
61
|
+
});
|
|
62
|
+
// A coach that reads an empty list as "the fleet is healthy" is worse than one
|
|
63
|
+
// with no tool at all, so the description says what the list is bounded by and
|
|
64
|
+
// the empty answer says it again.
|
|
65
|
+
it('tells the reader what it is not being shown', () => {
|
|
66
|
+
const listing = AGENT_OBSERVE_TOOLS.find((t) => t.name === 'list_observed_sessions');
|
|
67
|
+
expect(listing.description).toContain('a project you are a member of');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('the handler', () => {
|
|
71
|
+
const client = {
|
|
72
|
+
listObservedSessions: async () => [],
|
|
73
|
+
getObservedSession: async (id) => ({ id }),
|
|
74
|
+
getObservedSessionEvents: async () => [{ eventType: 'stop' }],
|
|
75
|
+
getObservedSessionConversation: async () => ({ history: [] }),
|
|
76
|
+
};
|
|
77
|
+
it('says so plainly when nothing matches, rather than printing an empty array', async () => {
|
|
78
|
+
const result = await handleAgentObserveTool('list_observed_sessions', {}, client);
|
|
79
|
+
expect(result.content[0].text).toContain('you can observe');
|
|
80
|
+
});
|
|
81
|
+
it('passes the snake_case arguments through under the client’s names', async () => {
|
|
82
|
+
const seen = [];
|
|
83
|
+
const recording = {
|
|
84
|
+
listObservedSessions: async (options) => {
|
|
85
|
+
seen.push(options);
|
|
86
|
+
return [{ id: 'session-1' }];
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
await handleAgentObserveTool('list_observed_sessions', { status: 'running', agent_profile_id: 'agent-1', since: '2026-09-15T07:00:00Z' }, recording);
|
|
90
|
+
expect(seen[0]).toMatchObject({
|
|
91
|
+
status: 'running',
|
|
92
|
+
agentProfileId: 'agent-1',
|
|
93
|
+
since: '2026-09-15T07:00:00Z',
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
it('refuses a name that is not one of its own', async () => {
|
|
97
|
+
await expect(handleAgentObserveTool('stop_observed_session', {}, client)).rejects.toThrow(/Unknown agent observe tool/);
|
|
98
|
+
});
|
|
99
|
+
});
|
package/dist/agentSelfTools.js
CHANGED
|
@@ -31,7 +31,8 @@ export const AGENT_SELF_TOOLS = [
|
|
|
31
31
|
export const AGENT_MEMORY_TOOLS = [
|
|
32
32
|
{
|
|
33
33
|
name: 'list_memories',
|
|
34
|
-
description: 'List
|
|
34
|
+
description: 'List the persistent memory entries this session can see: your global ones, plus those scoped to the projects this session runs in. ' +
|
|
35
|
+
'Memory persists across sessions and is scoped to you. ' +
|
|
35
36
|
'Use this to recall observations, patterns, and notes from previous work.',
|
|
36
37
|
inputSchema: { type: 'object', properties: {} },
|
|
37
38
|
},
|
|
@@ -50,7 +51,10 @@ export const AGENT_MEMORY_TOOLS = [
|
|
|
50
51
|
name: 'save_memory',
|
|
51
52
|
description: 'Save a persistent memory entry by key. Creates the entry if it does not exist, or updates it if it does. ' +
|
|
52
53
|
'Use this to persist observations, learnings, and notes across sessions. ' +
|
|
53
|
-
'To clear a memory entry, save it with empty content.'
|
|
54
|
+
'To clear a memory entry, save it with empty content. ' +
|
|
55
|
+
'Scope: a session running in a single project saves to that project, and only sessions in that project read it back. ' +
|
|
56
|
+
'A session spanning several projects saves globally — every later session of yours reads it, in every project. ' +
|
|
57
|
+
'Do not save anything from one project that should not be read in another.',
|
|
54
58
|
inputSchema: {
|
|
55
59
|
type: 'object',
|
|
56
60
|
properties: {
|
package/dist/client.js
CHANGED
|
@@ -435,8 +435,10 @@ export class WolfpackClient {
|
|
|
435
435
|
const { teamSlug, ...rest } = data;
|
|
436
436
|
return this.api.post('/wiki-pages', { ...rest, teamSlug });
|
|
437
437
|
}
|
|
438
|
-
|
|
439
|
-
|
|
438
|
+
// No teamSlug: the route takes none, and a wiki slug already carries its
|
|
439
|
+
// project prefix, so the page resolves without one.
|
|
440
|
+
async updateWikiPage(pageId, data) {
|
|
441
|
+
return this.api.patch(`/wiki-pages/${encodeURIComponent(pageId)}`, data);
|
|
440
442
|
}
|
|
441
443
|
// Journal Entry methods
|
|
442
444
|
async listJournalEntries(options) {
|
|
@@ -854,8 +856,7 @@ export class WolfpackClient {
|
|
|
854
856
|
}
|
|
855
857
|
// ─── Agent Builder: Skills (read) ──────────────────────────────────────────
|
|
856
858
|
async listAgentSkills(agentId, orgSlug) {
|
|
857
|
-
|
|
858
|
-
return agent?.skills ?? [];
|
|
859
|
+
return this.api.get(this.withOrgSlug(`/skills/agent-assignments/${agentId}`, orgSlug));
|
|
859
860
|
}
|
|
860
861
|
async listOrgSkills(orgSlug) {
|
|
861
862
|
return this.api.get(this.withOrgSlug('/agents/org-skills', orgSlug));
|
|
@@ -988,6 +989,46 @@ export class WolfpackClient {
|
|
|
988
989
|
async saveMemory(key, content) {
|
|
989
990
|
return this.api.put(`/self/memories/${encodeURIComponent(key)}`, { content });
|
|
990
991
|
}
|
|
992
|
+
// ─── Agent observation (#2388) ─────────────────────────────────────────────
|
|
993
|
+
// No agent id is required on any of these and none can be widened from here:
|
|
994
|
+
// the backend answers with the runs whose projects the caller is a member of,
|
|
995
|
+
// and with a 404 for anything else.
|
|
996
|
+
async listObservedSessions(options) {
|
|
997
|
+
const params = new URLSearchParams();
|
|
998
|
+
if (options?.status)
|
|
999
|
+
params.append('status', options.status);
|
|
1000
|
+
if (options?.agentProfileId)
|
|
1001
|
+
params.append('agentProfileId', options.agentProfileId);
|
|
1002
|
+
if (options?.since)
|
|
1003
|
+
params.append('since', options.since);
|
|
1004
|
+
if (options?.limit !== undefined)
|
|
1005
|
+
params.append('limit', options.limit.toString());
|
|
1006
|
+
if (options?.offset !== undefined)
|
|
1007
|
+
params.append('offset', options.offset.toString());
|
|
1008
|
+
const q = params.toString();
|
|
1009
|
+
return this.api.get(`/observe/sessions${q ? `?${q}` : ''}`);
|
|
1010
|
+
}
|
|
1011
|
+
async getObservedSession(sessionId) {
|
|
1012
|
+
return this.api.get(`/observe/sessions/${encodeURIComponent(sessionId)}`);
|
|
1013
|
+
}
|
|
1014
|
+
async getObservedSessionEvents(sessionId, options) {
|
|
1015
|
+
const params = new URLSearchParams();
|
|
1016
|
+
if (options?.after)
|
|
1017
|
+
params.append('after', options.after);
|
|
1018
|
+
if (options?.limit !== undefined)
|
|
1019
|
+
params.append('limit', options.limit.toString());
|
|
1020
|
+
const q = params.toString();
|
|
1021
|
+
return this.api.get(`/observe/sessions/${encodeURIComponent(sessionId)}/events${q ? `?${q}` : ''}`);
|
|
1022
|
+
}
|
|
1023
|
+
async getObservedSessionConversation(sessionId, options) {
|
|
1024
|
+
const params = new URLSearchParams();
|
|
1025
|
+
if (options?.limit !== undefined)
|
|
1026
|
+
params.append('limit', options.limit.toString());
|
|
1027
|
+
if (options?.offset !== undefined)
|
|
1028
|
+
params.append('offset', options.offset.toString());
|
|
1029
|
+
const q = params.toString();
|
|
1030
|
+
return this.api.get(`/observe/sessions/${encodeURIComponent(sessionId)}/conversation${q ? `?${q}` : ''}`);
|
|
1031
|
+
}
|
|
991
1032
|
// ─── Browser control (#2288) ───────────────────────────────────────────────
|
|
992
1033
|
// No chat is named on any of these: the backend derives it from the session
|
|
993
1034
|
// this key is bound to, so the agent cannot address another visitor's page.
|