agent-orchestrator-mcp-server 0.7.12 → 0.7.13
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/package.json
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { IAgentOrchestratorClient } from '../orchestrator-client/orchestrator-client.js';
|
|
4
|
+
export declare const WakeMeUpWhenSessionChangesStateSchema: z.ZodObject<{
|
|
5
|
+
session_id: z.ZodUnion<[z.ZodString, z.ZodNumber]>;
|
|
6
|
+
watched_session_id: z.ZodNumber;
|
|
7
|
+
event_name: z.ZodEnum<["session_needs_input", "session_failed"]>;
|
|
8
|
+
prompt: z.ZodString;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
prompt: string;
|
|
11
|
+
session_id: string | number;
|
|
12
|
+
watched_session_id: number;
|
|
13
|
+
event_name: "session_needs_input" | "session_failed";
|
|
14
|
+
}, {
|
|
15
|
+
prompt: string;
|
|
16
|
+
session_id: string | number;
|
|
17
|
+
watched_session_id: number;
|
|
18
|
+
event_name: "session_needs_input" | "session_failed";
|
|
19
|
+
}>;
|
|
20
|
+
export declare function wakeMeUpWhenSessionChangesStateTool(_server: Server, clientFactory: () => IAgentOrchestratorClient): {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object";
|
|
25
|
+
properties: {
|
|
26
|
+
session_id: {
|
|
27
|
+
oneOf: {
|
|
28
|
+
type: string;
|
|
29
|
+
}[];
|
|
30
|
+
description: string;
|
|
31
|
+
};
|
|
32
|
+
watched_session_id: {
|
|
33
|
+
type: string;
|
|
34
|
+
description: string;
|
|
35
|
+
};
|
|
36
|
+
event_name: {
|
|
37
|
+
type: string;
|
|
38
|
+
enum: ("session_needs_input" | "session_failed")[];
|
|
39
|
+
description: string;
|
|
40
|
+
};
|
|
41
|
+
prompt: {
|
|
42
|
+
type: string;
|
|
43
|
+
description: string;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
required: string[];
|
|
47
|
+
};
|
|
48
|
+
handler: (args: unknown) => Promise<{
|
|
49
|
+
content: {
|
|
50
|
+
type: string;
|
|
51
|
+
text: string;
|
|
52
|
+
}[];
|
|
53
|
+
isError: boolean;
|
|
54
|
+
} | {
|
|
55
|
+
content: {
|
|
56
|
+
type: string;
|
|
57
|
+
text: string;
|
|
58
|
+
}[];
|
|
59
|
+
isError?: undefined;
|
|
60
|
+
}>;
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=wake-me-up-when-session-changes-state.d.ts.map
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { parseAllowedAgentRoots } from '../allowed-agent-roots.js';
|
|
3
|
+
const AO_EVENT_NAMES = ['session_needs_input', 'session_failed'];
|
|
4
|
+
export const WakeMeUpWhenSessionChangesStateSchema = z.object({
|
|
5
|
+
session_id: z.union([z.string(), z.number()]),
|
|
6
|
+
watched_session_id: z.number().int().positive(),
|
|
7
|
+
event_name: z.enum(AO_EVENT_NAMES),
|
|
8
|
+
prompt: z.string(),
|
|
9
|
+
});
|
|
10
|
+
const TOOL_DESCRIPTION = `Schedule this session to be woken up when another session transitions to \`needs_input\` or \`failed\`. The requester session is put to sleep (waiting status) and a one-time trigger fires when the watched session enters the matching state. If the requester is manually resumed before the watched session transitions, the trigger is silently consumed and won't re-fire.
|
|
11
|
+
|
|
12
|
+
This is the **state-based analog of \`wake_me_up_later\`**. Use \`wake_me_up_later\` when you know *when* to wake up (a clock time). Use this tool when you know *what event* to wake up on but not when it will happen — e.g., a subagent session you spawned will eventually need input or might fail, and you want to be the first to handle it without polling.
|
|
13
|
+
|
|
14
|
+
**IMPORTANT — Use this tool instead of polling.** When this tool is available, it is the correct way to wait on another session's state. Do NOT use these alternatives:
|
|
15
|
+
- **Repeated \`get_session\` calls in a poll loop**: wastes compute, racks up tool-call overhead, and either polls too often (waste) or too rarely (latency). The trigger fires immediately on transition with no polling latency.
|
|
16
|
+
- **\`wake_me_up_later\` with a guessed duration**: time-based wake-ups are wrong here — you don't know when the watched session will transition, and a guess is either too short (you wake up early and have to re-sleep) or too long (the watched session has been sitting in \`needs_input\` while you sleep).
|
|
17
|
+
|
|
18
|
+
**The watched session can be ANY session**, not just one the requester spawned. You can watch a peer session, a session a different agent created, or even a session run by a different user — as long as the requester knows the watched session's id.
|
|
19
|
+
|
|
20
|
+
**One-shot semantics.** The trigger auto-disables after firing. If the watched session transitions, the requester wakes up exactly once, then the trigger is gone. To wake on the next transition too, schedule another trigger from the woken-up turn.
|
|
21
|
+
|
|
22
|
+
**Important — fires on transitions, not on current state.** The trigger fires when the watched session *moves into* the target state, not when it is *already in* it. If the watched session is already \`needs_input\` when you create the trigger, the trigger waits for the next transition out and back in (which only happens if someone resumes the watched session). For \`session_failed\` on a session that is already \`failed\`, the trigger never fires — \`failed\` is terminal. This tool rejects those terminal/no-fire-possible cases up front (already-failed watched session, archived watched session, self-watch). For non-terminal "already in target state" cases, the call succeeds but is a no-op until the watched session is resumed.
|
|
23
|
+
|
|
24
|
+
**Deadline backstop pattern.** If you need to wake on the watched session's transition *or* after a max wait time (whichever comes first), create a second \`wake_me_up_later\` trigger alongside this one. First trigger to fire wins; the AO firing path resumes the requester once and the other trigger is silently dropped.
|
|
25
|
+
|
|
26
|
+
**Parameters:**
|
|
27
|
+
- **session_id**: The session to wake up (the requester). Works from either \`needs_input\` or \`running\` state — if you call this tool from within your own currently-running session, the sleep transition is recorded and takes effect after the current turn ends.
|
|
28
|
+
- **watched_session_id**: The session to watch. Must be a positive integer. The Rails API rejects unknown ids with a clear 422.
|
|
29
|
+
- **event_name**: Which transition to wake on:
|
|
30
|
+
- \`session_needs_input\`: watched session moves to \`needs_input\` (typically: it finished a turn and is waiting for the user, OR it asked a clarifying question)
|
|
31
|
+
- \`session_failed\`: watched session moves to \`failed\` (a hard error — the session crashed or was killed)
|
|
32
|
+
|
|
33
|
+
Pick the one that matches what you're actually waiting for. If you want to wake on either, create two triggers (one per event) — the first to fire wins, and the other is silently dropped on the requester's resume.
|
|
34
|
+
- **prompt**: The prompt to send when waking up the session. Include enough context that the woken-up turn knows what to do (e.g., "session #N you were watching just transitioned — check its output and decide next steps").
|
|
35
|
+
|
|
36
|
+
**What happens:**
|
|
37
|
+
1. Creates a one-time \`ao_event\` trigger bound to the requester (\`reuse_session: true\`, \`last_session_id: session_id\`) with a single condition scoped to \`watched_session_id\` and \`event_name\`.
|
|
38
|
+
2. As a side effect of creating the trigger, the AO API transitions the requester to sleeping (waiting) status — immediately if currently \`needs_input\`, or after the current turn ends if currently \`running\`.
|
|
39
|
+
3. When the watched session transitions to the matching state, the trigger fires and resumes the requester with the provided prompt. The trigger then auto-deletes (one-shot).
|
|
40
|
+
4. If the requester is manually resumed first, the pending trigger is consumed (won't fire). If the watched session is archived without ever transitioning, the trigger is cleaned up.
|
|
41
|
+
|
|
42
|
+
**You must end your conversation turn after calling this tool** so the auto-sleep can take effect. If your turn keeps running, the requester will not be in a wakeable state when the watched session transitions, and the wake-up will be silently dropped.`;
|
|
43
|
+
export function wakeMeUpWhenSessionChangesStateTool(_server, clientFactory) {
|
|
44
|
+
return {
|
|
45
|
+
name: 'wake_me_up_when_session_changes_state',
|
|
46
|
+
description: TOOL_DESCRIPTION,
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
session_id: {
|
|
51
|
+
oneOf: [{ type: 'string' }, { type: 'number' }],
|
|
52
|
+
description: 'Session ID (numeric) or slug (string) for the session to wake up (the requester). Accepts sessions in needs_input or running state — from a running session, the sleep takes effect after the current turn ends.',
|
|
53
|
+
},
|
|
54
|
+
watched_session_id: {
|
|
55
|
+
type: 'number',
|
|
56
|
+
description: 'ID of the session to watch. Must be a positive integer. The trigger fires when this session transitions to the matching event_name state.',
|
|
57
|
+
},
|
|
58
|
+
event_name: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
enum: [...AO_EVENT_NAMES],
|
|
61
|
+
description: 'Which transition to wake on: "session_needs_input" (watched session is waiting for input) or "session_failed" (watched session crashed).',
|
|
62
|
+
},
|
|
63
|
+
prompt: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'The prompt to send when waking up the requester session.',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
required: ['session_id', 'watched_session_id', 'event_name', 'prompt'],
|
|
69
|
+
},
|
|
70
|
+
handler: async (args) => {
|
|
71
|
+
try {
|
|
72
|
+
let validated;
|
|
73
|
+
try {
|
|
74
|
+
validated = WakeMeUpWhenSessionChangesStateSchema.parse(args);
|
|
75
|
+
}
|
|
76
|
+
catch (zodError) {
|
|
77
|
+
// Surface a clear validation error message instead of a raw stack.
|
|
78
|
+
const issues = zodError instanceof z.ZodError
|
|
79
|
+
? zodError.issues
|
|
80
|
+
.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
|
|
81
|
+
.join('; ')
|
|
82
|
+
: zodError instanceof Error
|
|
83
|
+
? zodError.message
|
|
84
|
+
: 'Unknown validation error';
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: 'text',
|
|
89
|
+
text: `Error: Invalid arguments — ${issues}. No trigger was created and no session state was changed.`,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
isError: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const { session_id, watched_session_id, event_name, prompt } = validated;
|
|
96
|
+
if (parseAllowedAgentRoots() !== null) {
|
|
97
|
+
return {
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: 'text',
|
|
101
|
+
text: 'Error: wake_me_up_when_session_changes_state is not allowed when ALLOWED_AGENT_ROOTS is set. Triggers cannot be created because sessions are restricted to specific preconfigured agent roots.',
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
isError: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const client = clientFactory();
|
|
108
|
+
const session = await client.getSession(session_id);
|
|
109
|
+
// The trigger fires on the requester's auto-sleep+resume cycle when the
|
|
110
|
+
// watched session transitions. If they're the same session, the
|
|
111
|
+
// requester would resume itself in a confusing self-loop. Guard up
|
|
112
|
+
// front before any state change.
|
|
113
|
+
if (typeof session.id === 'number' && session.id === watched_session_id) {
|
|
114
|
+
return {
|
|
115
|
+
content: [
|
|
116
|
+
{
|
|
117
|
+
type: 'text',
|
|
118
|
+
text: `Error: watched_session_id (${watched_session_id}) is the same as the requester session id. A session cannot watch itself for state changes — the auto-sleep would never resolve cleanly. Pass a different session id.`,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
isError: true,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// Reject states the Rails API can't auto-sleep from. `needs_input` →
|
|
125
|
+
// immediate sleep; `running` → deferred sleep via pending_sleep metadata;
|
|
126
|
+
// `waiting` → already dormant, trigger fires normally. Anything else
|
|
127
|
+
// (failed, archived) would silently no-op the auto-sleep and leave the
|
|
128
|
+
// caller with a trigger targeting a session that can't be woken.
|
|
129
|
+
const WAKEABLE_STATUSES = ['needs_input', 'running', 'waiting'];
|
|
130
|
+
if (!WAKEABLE_STATUSES.includes(session.status)) {
|
|
131
|
+
return {
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: 'text',
|
|
135
|
+
text: `Error: Session ${session.id} is in "${session.status}" state and cannot be scheduled for wake-up. Only sessions in ${WAKEABLE_STATUSES.join(', ')} can be woken up.`,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
isError: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// The Rails firing path (AoEventTriggerJob, fired from
|
|
142
|
+
// session_state_machine transition callbacks) only fires on actual
|
|
143
|
+
// *transitions* to the target state. If the watched session is already
|
|
144
|
+
// in a terminal/non-transitionable state, the trigger would never fire
|
|
145
|
+
// and the requester would sleep forever. Guard against the common
|
|
146
|
+
// footguns up front.
|
|
147
|
+
let watchedSession;
|
|
148
|
+
try {
|
|
149
|
+
watchedSession = await client.getSession(watched_session_id);
|
|
150
|
+
}
|
|
151
|
+
catch (lookupError) {
|
|
152
|
+
return {
|
|
153
|
+
content: [
|
|
154
|
+
{
|
|
155
|
+
type: 'text',
|
|
156
|
+
text: `Error: Could not look up watched session ${watched_session_id}: ${lookupError instanceof Error ? lookupError.message : 'Unknown error'}. No trigger was created and no session state was changed.`,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
isError: true,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (event_name === 'session_failed' && watchedSession.status === 'failed') {
|
|
163
|
+
return {
|
|
164
|
+
content: [
|
|
165
|
+
{
|
|
166
|
+
type: 'text',
|
|
167
|
+
text: `Error: Watched session ${watched_session_id} is already in "failed" state. The trigger fires on transitions only — a session that is already failed will not transition to failed again, so the requester would sleep forever. Inspect the failed session directly instead of waiting on it.`,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
isError: true,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (watchedSession.status === 'archived') {
|
|
174
|
+
return {
|
|
175
|
+
content: [
|
|
176
|
+
{
|
|
177
|
+
type: 'text',
|
|
178
|
+
text: `Error: Watched session ${watched_session_id} is archived and will not transition further. The trigger would never fire. Pass an active session id, or inspect the archived session directly.`,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
isError: true,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
// The Rails Trigger model requires agent_root_name, but for per-session
|
|
185
|
+
// wake-up triggers (reuse_session + last_session_id + one-time event)
|
|
186
|
+
// the value is never used to spawn a session — the target session is
|
|
187
|
+
// always reused. Prefer the canonical metadata value with agent_type as
|
|
188
|
+
// a legacy fallback (matches wake_me_up_later behavior).
|
|
189
|
+
const agentRootName = session.metadata?.agent_root_key || session.agent_type;
|
|
190
|
+
let trigger;
|
|
191
|
+
try {
|
|
192
|
+
trigger = await client.createTrigger({
|
|
193
|
+
name: `Wake session #${session.id} on ${event_name} of session #${watched_session_id}`,
|
|
194
|
+
agent_root_name: agentRootName,
|
|
195
|
+
prompt_template: prompt,
|
|
196
|
+
reuse_session: true,
|
|
197
|
+
last_session_id: session.id,
|
|
198
|
+
trigger_conditions_attributes: [
|
|
199
|
+
{
|
|
200
|
+
condition_type: 'ao_event',
|
|
201
|
+
configuration: {
|
|
202
|
+
event_name,
|
|
203
|
+
watched_session_id,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
catch (triggerError) {
|
|
210
|
+
return {
|
|
211
|
+
content: [
|
|
212
|
+
{
|
|
213
|
+
type: 'text',
|
|
214
|
+
text: `Error: Trigger creation failed: ${triggerError instanceof Error ? triggerError.message : 'Unknown error'}. The session is still in its original state — no changes were made.`,
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
isError: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const lines = [
|
|
221
|
+
'## Wake-Up Scheduled Successfully',
|
|
222
|
+
'',
|
|
223
|
+
`- **Requester Session ID:** ${session.id}`,
|
|
224
|
+
`- **Watched Session ID:** ${watched_session_id}`,
|
|
225
|
+
`- **Event:** ${event_name}`,
|
|
226
|
+
`- **Trigger ID:** ${trigger.id}`,
|
|
227
|
+
`- **Trigger Name:** ${trigger.name}`,
|
|
228
|
+
'',
|
|
229
|
+
'**You must end your conversation turn now.** The requester session will be automatically transitioned to waiting (immediately if currently needs_input; after the current turn ends if currently running) and resumed when the watched session transitions to the matching state.',
|
|
230
|
+
'',
|
|
231
|
+
'⚠️ **Warning:** If you do not end your conversation turn, the requester may still be running when the watched session transitions. A wake-up cannot be delivered to a session that is not in a wakeable (sleeping/waiting) state — it will be silently dropped, and you will never receive it.',
|
|
232
|
+
'',
|
|
233
|
+
'**One-shot:** the trigger auto-deletes after firing. If you want to wake on the next transition too, schedule another trigger from the woken-up turn.',
|
|
234
|
+
];
|
|
235
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
return {
|
|
239
|
+
content: [
|
|
240
|
+
{
|
|
241
|
+
type: 'text',
|
|
242
|
+
text: `Error scheduling state-change wake-up: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
isError: true,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
package/shared/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
//
|
|
2
|
+
// 16 tools across 4 domains + 1 composite group
|
|
3
3
|
import { quickSearchSessionsTool } from './tools/search-sessions.js';
|
|
4
4
|
import { startSessionTool } from './tools/start-session.js';
|
|
5
5
|
import { getSessionTool } from './tools/get-session.js';
|
|
@@ -12,6 +12,7 @@ import { actionNotificationTool } from './tools/action-notification.js';
|
|
|
12
12
|
import { searchTriggersTool } from './tools/search-triggers.js';
|
|
13
13
|
import { actionTriggerTool } from './tools/action-trigger.js';
|
|
14
14
|
import { wakeMeUpLaterTool } from './tools/wake-me-up-later.js';
|
|
15
|
+
import { wakeMeUpWhenSessionChangesStateTool } from './tools/wake-me-up-when-session-changes-state.js';
|
|
15
16
|
import { getSystemHealthTool } from './tools/get-system-health.js';
|
|
16
17
|
import { actionHealthTool } from './tools/action-health.js';
|
|
17
18
|
import { getTranscriptArchiveTool } from './tools/get-transcript-archive.js';
|
|
@@ -60,7 +61,7 @@ export function parseEnabledToolGroups(enabledGroupsParam) {
|
|
|
60
61
|
/**
|
|
61
62
|
* All available tools with their group assignments.
|
|
62
63
|
*
|
|
63
|
-
*
|
|
64
|
+
* 16 tools across 4 domains + 1 composite group:
|
|
64
65
|
* - quick_search_sessions: Quick title-based search/list/get sessions by ID (sessions, read)
|
|
65
66
|
* - get_session: Get detailed session info with optional logs/transcripts (sessions, read; self_session)
|
|
66
67
|
* - get_configs: Fetch all static configuration (sessions, read; self_session)
|
|
@@ -74,6 +75,7 @@ export function parseEnabledToolGroups(enabledGroupsParam) {
|
|
|
74
75
|
* - search_triggers: Search/list automation triggers (triggers, read)
|
|
75
76
|
* - action_trigger: Create, update, delete, toggle triggers (triggers, write)
|
|
76
77
|
* - wake_me_up_later: Schedule a session to be woken up at a specific time (triggers, write; self_session)
|
|
78
|
+
* - wake_me_up_when_session_changes_state: Schedule a session to be woken up when another session enters needs_input or failed (triggers, write; self_session)
|
|
77
79
|
* - get_system_health: Get system health report and CLI status (health, read)
|
|
78
80
|
* - action_health: System maintenance actions (health, write)
|
|
79
81
|
*/
|
|
@@ -145,6 +147,12 @@ const ALL_TOOLS = [
|
|
|
145
147
|
isWriteOperation: true,
|
|
146
148
|
compositeGroups: ['self_session'],
|
|
147
149
|
},
|
|
150
|
+
{
|
|
151
|
+
factory: wakeMeUpWhenSessionChangesStateTool,
|
|
152
|
+
group: 'triggers',
|
|
153
|
+
isWriteOperation: true,
|
|
154
|
+
compositeGroups: ['self_session'],
|
|
155
|
+
},
|
|
148
156
|
// Health tools - read operations
|
|
149
157
|
{ factory: getSystemHealthTool, group: 'health', isWriteOperation: false },
|
|
150
158
|
// Health tools - write operations
|