agent-orchestrator-mcp-server 0.7.6 → 0.7.10
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-orchestrator-mcp-server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.10",
|
|
4
4
|
"description": "Local implementation of agent-orchestrator MCP server",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"stage-publish": "npm version"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
33
33
|
"zod": "^3.24.1"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
@@ -241,8 +241,8 @@ export function createIntegrationMockOrchestratorClient(initialMockData) {
|
|
|
241
241
|
if (!session) {
|
|
242
242
|
throw new Error(`API Error (404): Session not found`);
|
|
243
243
|
}
|
|
244
|
-
if (session.status !== 'needs_input') {
|
|
245
|
-
throw new Error(`API Error (422): Session must be in needs_input state to sleep`);
|
|
244
|
+
if (session.status !== 'needs_input' && session.status !== 'running') {
|
|
245
|
+
throw new Error(`API Error (422): Session must be in needs_input or running state to sleep (current: ${session.status})`);
|
|
246
246
|
}
|
|
247
247
|
session.status = 'waiting';
|
|
248
248
|
session.updated_at = new Date().toISOString();
|
|
@@ -633,7 +633,7 @@ export function createIntegrationMockOrchestratorClient(initialMockData) {
|
|
|
633
633
|
return {
|
|
634
634
|
id: 1,
|
|
635
635
|
name: data.name,
|
|
636
|
-
trigger_type: data.trigger_type,
|
|
636
|
+
trigger_type: data.trigger_type || 'schedule',
|
|
637
637
|
status: data.status || 'enabled',
|
|
638
638
|
agent_root_name: data.agent_root_name,
|
|
639
639
|
prompt_template: data.prompt_template,
|
|
@@ -642,7 +642,7 @@ export function createIntegrationMockOrchestratorClient(initialMockData) {
|
|
|
642
642
|
mcp_servers: data.mcp_servers || [],
|
|
643
643
|
configuration: data.configuration || {},
|
|
644
644
|
schedule_description: null,
|
|
645
|
-
last_session_id: null,
|
|
645
|
+
last_session_id: data.last_session_id ?? null,
|
|
646
646
|
last_triggered_at: null,
|
|
647
647
|
last_polled_at: null,
|
|
648
648
|
sessions_created_count: 0,
|
|
@@ -17,6 +17,7 @@ export declare const WakeMeUpLaterSchema: z.ZodObject<{
|
|
|
17
17
|
wake_at: string;
|
|
18
18
|
timezone?: string | undefined;
|
|
19
19
|
}>;
|
|
20
|
+
export declare function parseWakeAtToUtcMs(wakeAt: string, timezone: string): number;
|
|
20
21
|
export declare function wakeMeUpLaterTool(_server: Server, clientFactory: () => IAgentOrchestratorClient): {
|
|
21
22
|
name: string;
|
|
22
23
|
description: string;
|
|
@@ -6,6 +6,70 @@ export const WakeMeUpLaterSchema = z.object({
|
|
|
6
6
|
timezone: z.string().optional(),
|
|
7
7
|
prompt: z.string(),
|
|
8
8
|
});
|
|
9
|
+
// Reject wake-ups that resolve to ≤30 seconds in the future. Anything inside
|
|
10
|
+
// this window is effectively "now" given network latency between this tool
|
|
11
|
+
// call and the trigger being scheduled — and the past-dated case (the bug
|
|
12
|
+
// this guards against) silently fires-and-drops, leaving the session
|
|
13
|
+
// permanently asleep.
|
|
14
|
+
const WAKE_AT_GRACE_WINDOW_MS = 30 * 1000;
|
|
15
|
+
function getTimezoneOffsetMs(date, timeZone) {
|
|
16
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
17
|
+
timeZone,
|
|
18
|
+
year: 'numeric',
|
|
19
|
+
month: '2-digit',
|
|
20
|
+
day: '2-digit',
|
|
21
|
+
hour: '2-digit',
|
|
22
|
+
minute: '2-digit',
|
|
23
|
+
second: '2-digit',
|
|
24
|
+
hour12: false,
|
|
25
|
+
}).formatToParts(date);
|
|
26
|
+
const filled = {};
|
|
27
|
+
for (const part of parts) {
|
|
28
|
+
if (part.type !== 'literal')
|
|
29
|
+
filled[part.type] = part.value;
|
|
30
|
+
}
|
|
31
|
+
// hour12: false can emit "24" at midnight in some locales — normalize.
|
|
32
|
+
const hour = parseInt(filled.hour, 10) % 24;
|
|
33
|
+
const asIfUtc = Date.UTC(parseInt(filled.year, 10), parseInt(filled.month, 10) - 1, parseInt(filled.day, 10), hour, parseInt(filled.minute, 10), parseInt(filled.second, 10));
|
|
34
|
+
// Sub-second precision is dropped by Date.UTC, so the offset can be off by
|
|
35
|
+
// up to ~1s when the input has fractional seconds. That's well inside the
|
|
36
|
+
// 30s grace window, so it doesn't affect validation.
|
|
37
|
+
return asIfUtc - date.getTime();
|
|
38
|
+
}
|
|
39
|
+
// Reject inputs that don't look like a calendar+time: bare dates ("2026-04-15"),
|
|
40
|
+
// trailing offsets ("...+05:00"), and `Z` paired with a non-UTC IANA timezone
|
|
41
|
+
// (ambiguous — we'd have to pick one to honor and the other to ignore).
|
|
42
|
+
const NAIVE_DATETIME_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?Z?$/;
|
|
43
|
+
const EXPLICIT_OFFSET_REGEX = /[+-]\d{2}:?\d{2}$/;
|
|
44
|
+
// Convert a naive ISO-8601 wall-clock string in `timezone` to a UTC epoch ms.
|
|
45
|
+
// Iterates twice so DST-boundary inputs converge on the correct offset.
|
|
46
|
+
export function parseWakeAtToUtcMs(wakeAt, timezone) {
|
|
47
|
+
if (EXPLICIT_OFFSET_REGEX.test(wakeAt)) {
|
|
48
|
+
throw new Error(`wake_at must not include a UTC offset (e.g., "+05:00"); pass the wall-clock time and an IANA timezone name (e.g., "America/New_York")`);
|
|
49
|
+
}
|
|
50
|
+
if (!NAIVE_DATETIME_REGEX.test(wakeAt)) {
|
|
51
|
+
throw new Error(`wake_at must be an ISO-8601 datetime like "2026-04-15T14:30:00" (date-only and other formats are not accepted)`);
|
|
52
|
+
}
|
|
53
|
+
const hasZ = wakeAt.endsWith('Z');
|
|
54
|
+
const isUtc = timezone === 'UTC' || timezone === 'Etc/UTC';
|
|
55
|
+
if (hasZ && !isUtc) {
|
|
56
|
+
throw new Error(`wake_at ends with "Z" (UTC) but timezone is "${timezone}". Either drop the trailing "Z" or set timezone to "UTC"`);
|
|
57
|
+
}
|
|
58
|
+
const naive = hasZ ? wakeAt.slice(0, -1) : wakeAt;
|
|
59
|
+
const naiveAsUtc = new Date(naive + 'Z').getTime();
|
|
60
|
+
if (Number.isNaN(naiveAsUtc)) {
|
|
61
|
+
throw new Error(`Invalid wake_at value: "${wakeAt}"`);
|
|
62
|
+
}
|
|
63
|
+
if (isUtc) {
|
|
64
|
+
return naiveAsUtc;
|
|
65
|
+
}
|
|
66
|
+
let utcGuess = naiveAsUtc - getTimezoneOffsetMs(new Date(naiveAsUtc), timezone);
|
|
67
|
+
utcGuess = naiveAsUtc - getTimezoneOffsetMs(new Date(utcGuess), timezone);
|
|
68
|
+
return utcGuess;
|
|
69
|
+
}
|
|
70
|
+
function formatUtcInstant(ms) {
|
|
71
|
+
return new Date(ms).toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
72
|
+
}
|
|
9
73
|
function buildToolDescription() {
|
|
10
74
|
const now = new Date();
|
|
11
75
|
const utcNow = now.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
@@ -16,7 +80,7 @@ function buildToolDescription() {
|
|
|
16
80
|
- **Claude Code \`ScheduleWakeup\` tool**: Does not integrate with AO's session lifecycle — it won't transition the session to sleeping/waiting state or create an AO trigger, so AO cannot track or manage the wake-up.
|
|
17
81
|
- **Claude Code \`Monitor\` tool**: Same problem — it operates outside AO's session state management.
|
|
18
82
|
|
|
19
|
-
This tool
|
|
83
|
+
This tool creates a one-time AO wake-up trigger bound to the target session. The AO API atomically transitions the session to sleeping (waiting) state as part of creating the trigger, so AO can reclaim resources and the trigger is guaranteed to resume the correct session at the specified time.
|
|
20
84
|
|
|
21
85
|
**Current server time:** ${utcNow} (UTC). Use this as your reference point when calculating wake-up times.
|
|
22
86
|
|
|
@@ -28,16 +92,15 @@ This tool does both: it properly transitions the session to sleeping (waiting) s
|
|
|
28
92
|
- If you omit timezone, wake_at is treated as UTC.
|
|
29
93
|
|
|
30
94
|
**Parameters:**
|
|
31
|
-
- **session_id**: The session to wake up
|
|
95
|
+
- **session_id**: The session to wake up. 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.
|
|
32
96
|
- **wake_at**: ISO 8601 datetime without offset for when to wake up (e.g., "2026-04-15T14:30:00")
|
|
33
97
|
- **timezone**: IANA timezone for interpreting wake_at (default: "UTC", e.g., "America/New_York")
|
|
34
98
|
- **prompt**: The prompt to send when waking up the session
|
|
35
99
|
|
|
36
100
|
**What happens:**
|
|
37
|
-
1.
|
|
38
|
-
2.
|
|
39
|
-
3.
|
|
40
|
-
4. The trigger resumes the session with the provided prompt`;
|
|
101
|
+
1. Creates a one-time schedule trigger bound to this session that fires at the specified time.
|
|
102
|
+
2. As a side effect of creating the trigger, the AO API transitions the session to sleeping (waiting) status — immediately if currently \`needs_input\`, or after the current turn ends if currently \`running\`.
|
|
103
|
+
3. At the scheduled time, the trigger resumes the session with the provided prompt.`;
|
|
41
104
|
}
|
|
42
105
|
export function wakeMeUpLaterTool(_server, clientFactory) {
|
|
43
106
|
return {
|
|
@@ -48,7 +111,7 @@ export function wakeMeUpLaterTool(_server, clientFactory) {
|
|
|
48
111
|
properties: {
|
|
49
112
|
session_id: {
|
|
50
113
|
oneOf: [{ type: 'string' }, { type: 'number' }],
|
|
51
|
-
description: 'Session ID (numeric) or slug (string).
|
|
114
|
+
description: 'Session ID (numeric) or slug (string). Accepts sessions in needs_input or running state — from a running session, the sleep takes effect after the current turn ends.',
|
|
52
115
|
},
|
|
53
116
|
wake_at: {
|
|
54
117
|
type: 'string',
|
|
@@ -68,9 +131,42 @@ export function wakeMeUpLaterTool(_server, clientFactory) {
|
|
|
68
131
|
handler: async (args) => {
|
|
69
132
|
try {
|
|
70
133
|
const validated = WakeMeUpLaterSchema.parse(args);
|
|
71
|
-
const client = clientFactory();
|
|
72
134
|
const { session_id, wake_at, prompt } = validated;
|
|
73
135
|
const timezone = validated.timezone || 'UTC';
|
|
136
|
+
// Cheapest validation runs first (no DB/API calls). Past-dated
|
|
137
|
+
// wake_at values silently fire-and-drop in the scheduler and leave
|
|
138
|
+
// the session permanently asleep, so reject up front before any
|
|
139
|
+
// state change.
|
|
140
|
+
let wakeAtUtcMs;
|
|
141
|
+
try {
|
|
142
|
+
wakeAtUtcMs = parseWakeAtToUtcMs(wake_at, timezone);
|
|
143
|
+
}
|
|
144
|
+
catch (parseError) {
|
|
145
|
+
return {
|
|
146
|
+
content: [
|
|
147
|
+
{
|
|
148
|
+
type: 'text',
|
|
149
|
+
text: `Error: Could not parse wake_at "${wake_at}" with timezone "${timezone}": ${parseError instanceof Error ? parseError.message : 'Unknown error'}. No trigger was created and no session state was changed.`,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
isError: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const nowMs = Date.now();
|
|
156
|
+
if (wakeAtUtcMs - nowMs <= WAKE_AT_GRACE_WINDOW_MS) {
|
|
157
|
+
const wakeAtUtcStr = formatUtcInstant(wakeAtUtcMs);
|
|
158
|
+
const nowUtcStr = formatUtcInstant(nowMs);
|
|
159
|
+
return {
|
|
160
|
+
content: [
|
|
161
|
+
{
|
|
162
|
+
type: 'text',
|
|
163
|
+
text: `Error: wake_at "${wake_at}" (timezone: ${timezone}) resolves to ${wakeAtUtcStr} UTC, which is in the past or within 30 seconds of the current server time (${nowUtcStr} UTC). No trigger was created and no session state was changed. Recompute relative to the current server time shown in the tool description and call again — wake_at must be more than 30 seconds in the future.`,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
isError: true,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const client = clientFactory();
|
|
74
170
|
if (parseAllowedAgentRoots() !== null) {
|
|
75
171
|
return {
|
|
76
172
|
content: [
|
|
@@ -83,30 +179,49 @@ export function wakeMeUpLaterTool(_server, clientFactory) {
|
|
|
83
179
|
};
|
|
84
180
|
}
|
|
85
181
|
const session = await client.getSession(session_id);
|
|
86
|
-
|
|
182
|
+
// Reject states the Rails API can't auto-sleep from. `needs_input` →
|
|
183
|
+
// immediate sleep; `running` → deferred sleep via pending_sleep metadata;
|
|
184
|
+
// `waiting` → already dormant, trigger fires normally. Anything else
|
|
185
|
+
// (failed, archived) would silently no-op the auto-sleep and leave the
|
|
186
|
+
// caller with a trigger targeting a session that can't be woken.
|
|
187
|
+
const WAKEABLE_STATUSES = ['needs_input', 'running', 'waiting'];
|
|
188
|
+
if (!WAKEABLE_STATUSES.includes(session.status)) {
|
|
87
189
|
return {
|
|
88
190
|
content: [
|
|
89
191
|
{
|
|
90
192
|
type: 'text',
|
|
91
|
-
text: `Error: Session
|
|
193
|
+
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.`,
|
|
92
194
|
},
|
|
93
195
|
],
|
|
94
196
|
isError: true,
|
|
95
197
|
};
|
|
96
198
|
}
|
|
97
|
-
|
|
199
|
+
// The Rails Trigger model requires agent_root_name, but for per-session
|
|
200
|
+
// wake-up triggers (reuse_session + last_session_id + one-time schedule)
|
|
201
|
+
// the value is never used to spawn a session — the target session is
|
|
202
|
+
// always reused. Prefer the canonical metadata value. The agent_type
|
|
203
|
+
// fallback is a best-effort for pre-migration sessions without an
|
|
204
|
+
// agent_root_key; if agent_type isn't a registered agent root, the
|
|
205
|
+
// createTrigger call will fail loudly with a 422 rather than proceed
|
|
206
|
+
// with a bad value — which is what we want.
|
|
207
|
+
const agentRootName = session.metadata?.agent_root_key || session.agent_type;
|
|
98
208
|
let trigger;
|
|
99
209
|
try {
|
|
100
210
|
trigger = await client.createTrigger({
|
|
101
|
-
name: `Wake session #${
|
|
102
|
-
|
|
103
|
-
agent_root_name: session.agent_type,
|
|
211
|
+
name: `Wake session #${session.id} at ${wake_at}`,
|
|
212
|
+
agent_root_name: agentRootName,
|
|
104
213
|
prompt_template: prompt,
|
|
105
214
|
reuse_session: true,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
215
|
+
last_session_id: session.id,
|
|
216
|
+
trigger_conditions_attributes: [
|
|
217
|
+
{
|
|
218
|
+
condition_type: 'schedule',
|
|
219
|
+
configuration: {
|
|
220
|
+
scheduled_at: wake_at,
|
|
221
|
+
timezone,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
],
|
|
110
225
|
});
|
|
111
226
|
}
|
|
112
227
|
catch (triggerError) {
|
|
@@ -114,28 +229,21 @@ export function wakeMeUpLaterTool(_server, clientFactory) {
|
|
|
114
229
|
content: [
|
|
115
230
|
{
|
|
116
231
|
type: 'text',
|
|
117
|
-
text: `Error:
|
|
232
|
+
text: `Error: Trigger creation failed: ${triggerError instanceof Error ? triggerError.message : 'Unknown error'}. The session is still in its original state — no changes were made.`,
|
|
118
233
|
},
|
|
119
234
|
],
|
|
120
235
|
isError: true,
|
|
121
236
|
};
|
|
122
237
|
}
|
|
123
|
-
await client.updateSession(session_id, {
|
|
124
|
-
custom_metadata: {
|
|
125
|
-
...session.custom_metadata,
|
|
126
|
-
wake_trigger_id: trigger.id,
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
238
|
const lines = [
|
|
130
239
|
'## Wake-Up Scheduled Successfully',
|
|
131
240
|
'',
|
|
132
|
-
`- **Session ID:** ${
|
|
133
|
-
`- **Session Status:** ${sleepingSession.status}`,
|
|
241
|
+
`- **Session ID:** ${session.id}`,
|
|
134
242
|
`- **Wake At:** ${wake_at} (${timezone})`,
|
|
135
243
|
`- **Trigger ID:** ${trigger.id}`,
|
|
136
244
|
`- **Trigger Name:** ${trigger.name}`,
|
|
137
245
|
'',
|
|
138
|
-
'**You must end your conversation turn now.** The session
|
|
246
|
+
'**You must end your conversation turn now.** The session will be automatically transitioned to waiting (immediately if currently needs_input; after the current turn ends if currently running) and resumed at the scheduled time with the provided prompt.',
|
|
139
247
|
'',
|
|
140
248
|
'⚠️ **Warning:** If you do not end your conversation turn, the session may still be running when the scheduled wake-up fires. 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.',
|
|
141
249
|
];
|
package/shared/types.d.ts
CHANGED
|
@@ -251,9 +251,13 @@ export interface TriggerChannelsResponse {
|
|
|
251
251
|
num_members: number;
|
|
252
252
|
}>;
|
|
253
253
|
}
|
|
254
|
+
export interface TriggerConditionAttributes {
|
|
255
|
+
condition_type: 'slack' | 'schedule' | 'ao_event';
|
|
256
|
+
configuration: Record<string, unknown>;
|
|
257
|
+
}
|
|
254
258
|
export interface CreateTriggerRequest {
|
|
255
259
|
name: string;
|
|
256
|
-
trigger_type
|
|
260
|
+
trigger_type?: TriggerType;
|
|
257
261
|
agent_root_name: string;
|
|
258
262
|
prompt_template: string;
|
|
259
263
|
status?: TriggerStatus;
|
|
@@ -261,6 +265,8 @@ export interface CreateTriggerRequest {
|
|
|
261
265
|
reuse_session?: boolean;
|
|
262
266
|
mcp_servers?: string[];
|
|
263
267
|
configuration?: Record<string, unknown>;
|
|
268
|
+
last_session_id?: number;
|
|
269
|
+
trigger_conditions_attributes?: TriggerConditionAttributes[];
|
|
264
270
|
}
|
|
265
271
|
export interface UpdateTriggerRequest {
|
|
266
272
|
name?: string;
|