orchestrix-yuri 3.2.1 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -33,6 +33,9 @@ class TelegramAdapter {
|
|
|
33
33
|
chatId: String(ctx.chat.id),
|
|
34
34
|
text: ctx.message.text,
|
|
35
35
|
userName: ctx.from.first_name || ctx.from.username || 'Unknown',
|
|
36
|
+
replyToMessageId: ctx.message.reply_to_message
|
|
37
|
+
? String(ctx.message.reply_to_message.message_id)
|
|
38
|
+
: null,
|
|
36
39
|
};
|
|
37
40
|
|
|
38
41
|
try {
|
|
@@ -45,9 +45,16 @@ class PhaseOrchestrator {
|
|
|
45
45
|
this._timer = null;
|
|
46
46
|
this._lastHash = '';
|
|
47
47
|
this._stableCount = 0;
|
|
48
|
+
|
|
49
|
+
// Agent question bridging
|
|
50
|
+
this._waitingForInput = false; // true when agent asked a question
|
|
51
|
+
this._waitingMessageId = null; // Telegram message_id of the question notification
|
|
52
|
+
this._onQuestionAsked = opts.onQuestionAsked || (() => Promise.resolve(null));
|
|
53
|
+
// onQuestionAsked(text) → sends to Telegram, returns { messageId }
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
isRunning() { return this._phase !== null; }
|
|
57
|
+
isWaitingForInput() { return this._waitingForInput; }
|
|
51
58
|
|
|
52
59
|
/**
|
|
53
60
|
* Try to recover an in-progress phase from YAML state + tmux session.
|
|
@@ -239,6 +246,9 @@ class PhaseOrchestrator {
|
|
|
239
246
|
return;
|
|
240
247
|
}
|
|
241
248
|
|
|
249
|
+
// Skip polling while waiting for user input
|
|
250
|
+
if (this._waitingForInput) return;
|
|
251
|
+
|
|
242
252
|
// Check completion
|
|
243
253
|
const result = tmx.checkCompletion(this._session, agent.window, this._lastHash);
|
|
244
254
|
|
|
@@ -251,8 +261,21 @@ class PhaseOrchestrator {
|
|
|
251
261
|
this._stableCount++;
|
|
252
262
|
this._lastHash = result.hash;
|
|
253
263
|
if (this._stableCount >= 3) {
|
|
254
|
-
// Content stable for 3 polls —
|
|
255
|
-
this.
|
|
264
|
+
// Content stable for 3 polls — distinguish "done" vs "waiting for input"
|
|
265
|
+
const pane = tmx.capturePane(this._session, agent.window, 30);
|
|
266
|
+
const tail = pane.split('\n').slice(-15).join('\n');
|
|
267
|
+
|
|
268
|
+
if (/[A-Z][a-z]*ed for \d+/.test(tail)) {
|
|
269
|
+
// Completion message present — truly done
|
|
270
|
+
this._onAgentComplete(agent);
|
|
271
|
+
} else if (this._looksLikeQuestion(tail)) {
|
|
272
|
+
// Agent is asking a question — bridge to user
|
|
273
|
+
this._onAgentWaitingInput(agent, tail);
|
|
274
|
+
} else {
|
|
275
|
+
// Ambiguous — treat as done (agent may have finished without completion message)
|
|
276
|
+
log.engine(`Agent ${agent.name} stable without completion message, treating as done`);
|
|
277
|
+
this._onAgentComplete(agent);
|
|
278
|
+
}
|
|
256
279
|
return;
|
|
257
280
|
}
|
|
258
281
|
} else {
|
|
@@ -261,6 +284,82 @@ class PhaseOrchestrator {
|
|
|
261
284
|
}
|
|
262
285
|
}
|
|
263
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Detect if pane content looks like the agent is asking a question.
|
|
289
|
+
*/
|
|
290
|
+
_looksLikeQuestion(tail) {
|
|
291
|
+
// Has question mark near the end
|
|
292
|
+
if (/[??]\s*$/.test(tail) || /[??]\s*\n\s*❯/m.test(tail)) return true;
|
|
293
|
+
// Has numbered options (1. xxx 2. xxx)
|
|
294
|
+
if (/^\s*[1-9]\.\s+\S/m.test(tail) && /❯/.test(tail)) return true;
|
|
295
|
+
// Has Y/N prompt
|
|
296
|
+
if (/\(Y\/N\)/i.test(tail) || /\(y\/n\)/i.test(tail)) return true;
|
|
297
|
+
// Has "please confirm" or similar
|
|
298
|
+
if (/confirm|choose|select|which|请选择|请确认|是否/i.test(tail) && /❯/.test(tail)) return true;
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Agent is waiting for user input. Notify user via Telegram and pause polling.
|
|
304
|
+
*/
|
|
305
|
+
async _onAgentWaitingInput(agent, paneContent) {
|
|
306
|
+
this._waitingForInput = true;
|
|
307
|
+
|
|
308
|
+
// Extract the question from pane content (last meaningful lines before ❯)
|
|
309
|
+
const lines = paneContent.split('\n');
|
|
310
|
+
const promptIdx = lines.findLastIndex((l) => /❯/.test(l));
|
|
311
|
+
const questionLines = lines.slice(Math.max(0, promptIdx - 15), promptIdx).filter((l) => l.trim());
|
|
312
|
+
const question = questionLines.join('\n').trim() || '(unable to extract question)';
|
|
313
|
+
|
|
314
|
+
log.engine(`Agent ${agent.name} is asking a question, notifying user`);
|
|
315
|
+
|
|
316
|
+
const notification = `📋 **${agent.name}** is asking:\n\n${question}\n\n↩️ *Reply to this message to answer the agent.*`;
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const result = await this._onQuestionAsked(notification);
|
|
320
|
+
if (result && result.messageId) {
|
|
321
|
+
this._waitingMessageId = String(result.messageId);
|
|
322
|
+
}
|
|
323
|
+
} catch (err) {
|
|
324
|
+
log.warn(`Failed to send question notification: ${err.message}`);
|
|
325
|
+
// Auto-continue on failure
|
|
326
|
+
this._waitingForInput = false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Relay user's reply to the agent's tmux window.
|
|
332
|
+
* Called by router when user replies to the question notification.
|
|
333
|
+
*/
|
|
334
|
+
relayUserInput(text) {
|
|
335
|
+
if (!this._waitingForInput || !this._session) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const agent = PLAN_AGENTS[this._step];
|
|
340
|
+
if (!agent) return false;
|
|
341
|
+
|
|
342
|
+
log.engine(`Relaying user input to ${agent.name}: "${text.slice(0, 50)}..."`);
|
|
343
|
+
|
|
344
|
+
tmx.sendKeysWithEnter(this._session, agent.window, text);
|
|
345
|
+
|
|
346
|
+
// Resume polling
|
|
347
|
+
this._waitingForInput = false;
|
|
348
|
+
this._waitingMessageId = null;
|
|
349
|
+
this._stableCount = 0;
|
|
350
|
+
this._lastHash = '';
|
|
351
|
+
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Get the Telegram message_id of the current question notification.
|
|
357
|
+
* Used by router to check if a reply-to matches.
|
|
358
|
+
*/
|
|
359
|
+
getWaitingMessageId() {
|
|
360
|
+
return this._waitingMessageId;
|
|
361
|
+
}
|
|
362
|
+
|
|
264
363
|
_onAgentComplete(agent) {
|
|
265
364
|
this._stableCount = 0;
|
|
266
365
|
this._lastHash = '';
|
package/lib/gateway/index.js
CHANGED
|
@@ -39,12 +39,16 @@ async function startGateway(opts = {}) {
|
|
|
39
39
|
});
|
|
40
40
|
adapters.push(telegram);
|
|
41
41
|
|
|
42
|
-
// Wire proactive messaging: orchestrator → Telegram
|
|
42
|
+
// Wire proactive messaging: orchestrator → Telegram (returns { messageId })
|
|
43
43
|
router.setSendCallback(null, async (chatId, text) => {
|
|
44
44
|
try {
|
|
45
|
-
await telegram.bot.api.sendMessage(chatId, text, { parse_mode: 'Markdown' });
|
|
45
|
+
const sent = await telegram.bot.api.sendMessage(chatId, text, { parse_mode: 'Markdown' });
|
|
46
|
+
return { messageId: String(sent.message_id) };
|
|
46
47
|
} catch {
|
|
47
|
-
|
|
48
|
+
try {
|
|
49
|
+
const sent = await telegram.bot.api.sendMessage(chatId, text);
|
|
50
|
+
return { messageId: String(sent.message_id) };
|
|
51
|
+
} catch { return null; }
|
|
48
52
|
}
|
|
49
53
|
});
|
|
50
54
|
|
package/lib/gateway/router.js
CHANGED
|
@@ -54,6 +54,7 @@ class Router {
|
|
|
54
54
|
onProgress: (msg) => this._sendProactive(msg),
|
|
55
55
|
onComplete: (phase, summary) => this._sendProactive(summary),
|
|
56
56
|
onError: (phase, err) => this._sendProactive(`❌ Phase ${phase} error: ${err}`),
|
|
57
|
+
onQuestionAsked: (text) => this._sendProactiveWithId(text),
|
|
57
58
|
});
|
|
58
59
|
|
|
59
60
|
// Auto-recover any in-progress phase from previous gateway run
|
|
@@ -80,6 +81,20 @@ class Router {
|
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Send a proactive message and return { messageId } for reply-to tracking.
|
|
86
|
+
*/
|
|
87
|
+
async _sendProactiveWithId(text) {
|
|
88
|
+
if (!this._sendCallback || !this._ownerChatId) return null;
|
|
89
|
+
try {
|
|
90
|
+
const result = await this._sendCallback(this._ownerChatId, text);
|
|
91
|
+
return result; // { messageId }
|
|
92
|
+
} catch (err) {
|
|
93
|
+
log.warn(`Proactive send failed: ${err.message}`);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
/**
|
|
84
99
|
* Handle an incoming channel message.
|
|
85
100
|
*/
|
|
@@ -113,6 +128,17 @@ class Router {
|
|
|
113
128
|
return this._handleStatusQuery(msg);
|
|
114
129
|
}
|
|
115
130
|
|
|
131
|
+
// ═══ AGENT REPLY — user replied to an agent question notification ═══
|
|
132
|
+
if (msg.replyToMessageId && this.orchestrator.isWaitingForInput()) {
|
|
133
|
+
const waitingId = this.orchestrator.getWaitingMessageId();
|
|
134
|
+
if (waitingId && msg.replyToMessageId === waitingId) {
|
|
135
|
+
const relayed = this.orchestrator.relayUserInput(msg.text);
|
|
136
|
+
if (relayed) {
|
|
137
|
+
return { text: '✅ Reply sent to agent. Resuming...' };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
116
142
|
// ═══ CANCEL — stop running phase ═══
|
|
117
143
|
if (PHASE_COMMANDS.cancel.test(msg.text.trim())) {
|
|
118
144
|
if (this.orchestrator.isRunning()) {
|