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 — agent likely done
255
- this._onAgentComplete(agent);
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 = '';
@@ -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
- await telegram.bot.api.sendMessage(chatId, text).catch(() => {});
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
 
@@ -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()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-yuri",
3
- "version": "3.2.1",
3
+ "version": "3.3.0",
4
4
  "description": "Yuri — Meta-Orchestrator for Orchestrix. Drive your entire project lifecycle with natural language.",
5
5
  "main": "lib/installer.js",
6
6
  "bin": {