@inceptionstack/roundhouse 0.5.3 → 0.5.4

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": "@inceptionstack/roundhouse",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "type": "module",
5
5
  "description": "Multi-platform chat gateway that routes messages through a configured AI agent",
6
6
  "license": "MIT",
@@ -205,6 +205,23 @@ export const createPiAgentAdapter: AgentAdapterFactory = (config) => {
205
205
  } else {
206
206
  console.log(`[pi-agent] no memory extension detected — roundhouse memory will manage`);
207
207
  }
208
+
209
+ // Warn about pi extensions that bridge a chat platform directly.
210
+ // They hijack agent_start/message_update/agent_end and short-circuit
211
+ // Roundhouse's streaming pipeline — Telegram shows "typing" forever.
212
+ const conflicting = extNames.filter((n) => /pi-telegram(\b|[\/\\])/i.test(n));
213
+ if (conflicting.length > 0) {
214
+ const lines = [
215
+ "",
216
+ "\u26a0\ufe0f CONFLICT: detected pi extension(s) that bridge a chat platform directly:",
217
+ ...conflicting.map((n) => ` - ${n}`),
218
+ " Roundhouse already drives Telegram. Loading a bridge extension inside",
219
+ " the pi session causes lost replies (typing indicator without text).",
220
+ " Remove the extension from ~/.pi/agent/extensions or pi config and restart.",
221
+ "",
222
+ ];
223
+ for (const line of lines) console.warn(line);
224
+ }
208
225
  }
209
226
 
210
227
  return entry;
@@ -353,6 +370,13 @@ export const createPiAgentAdapter: AgentAdapterFactory = (config) => {
353
370
  streamEvent = { type: "tool_end", toolName: event.toolName, toolCallId: event.toolCallId, isError: event.isError };
354
371
  } else if (event.type === "turn_end") {
355
372
  streamEvent = { type: "turn_end" };
373
+ } else if (event.type === "message_end") {
374
+ // Pi records provider failures (auth, throttling, etc.) on the
375
+ // assistant message instead of throwing — surface them.
376
+ const msg = (event as any).message;
377
+ if (msg?.role === "assistant" && msg.stopReason === "error" && msg.errorMessage) {
378
+ streamEvent = { type: "model_error", message: msg.errorMessage };
379
+ }
356
380
  }
357
381
  }
358
382
 
@@ -116,6 +116,8 @@ export async function handleStreaming(
116
116
  };
117
117
 
118
118
  let hasTextInCurrentTurn = false;
119
+ let hasContentThisTurn = false;
120
+ let modelErrorPosted = false;
119
121
  let eventCount = 0;
120
122
  let drainingNotified = false;
121
123
 
@@ -125,8 +127,9 @@ export async function handleStreaming(
125
127
  break;
126
128
  }
127
129
 
130
+ eventCount++;
131
+
128
132
  if (DEBUG_STREAM) {
129
- eventCount++;
130
133
  const preview = event.type === "text_delta" ? `"${event.text.slice(0, 30)}"`
131
134
  : event.type === "custom_message" ? `${event.customType}:${event.content.slice(0, 30)}`
132
135
  : event.type === "tool_start" || event.type === "tool_end" ? event.toolName
@@ -139,12 +142,14 @@ export async function handleStreaming(
139
142
  ensureStream();
140
143
  currentPush!(event.text);
141
144
  hasTextInCurrentTurn = true;
145
+ hasContentThisTurn = true;
142
146
  break;
143
147
  }
144
148
 
145
149
  case "tool_start": {
146
150
  activeTools.set(event.toolCallId, event.toolName);
147
151
  if (!READ_ONLY_TOOLS.has(event.toolName)) usedFileModifyingTools = true;
152
+ hasContentThisTurn = true;
148
153
  if (verbose) {
149
154
  try { await thread.post(`${toolIcon(event.toolName)} Running \`${event.toolName}\`…`); } catch {}
150
155
  }
@@ -161,10 +166,22 @@ export async function handleStreaming(
161
166
  await flushCurrentStream();
162
167
  hasTextInCurrentTurn = false;
163
168
  }
169
+ hasContentThisTurn = true;
164
170
  await postWithFallback(thread, event.content);
165
171
  break;
166
172
  }
167
173
 
174
+ case "model_error": {
175
+ await flushCurrentStream();
176
+ hasTextInCurrentTurn = false;
177
+ hasContentThisTurn = true;
178
+ modelErrorPosted = true;
179
+ const safeMsg = event.message.split("\n")[0].slice(0, 400);
180
+ console.warn(`[roundhouse] model error: ${safeMsg}`);
181
+ try { await thread.post(`\u26a0\ufe0f Agent error: ${safeMsg}`); } catch {}
182
+ break;
183
+ }
184
+
168
185
  case "turn_end": {
169
186
  if (hasTextInCurrentTurn) {
170
187
  await flushCurrentStream();
@@ -207,5 +224,12 @@ export async function handleStreaming(
207
224
  await flushCurrentStream();
208
225
  }
209
226
 
227
+ // Safety net: if the entire turn produced no visible content and no error
228
+ // was already reported, notify the user so they don't stare at "typing" forever.
229
+ if (!hasContentThisTurn && !modelErrorPosted) {
230
+ console.warn(`[roundhouse] agent returned no content this turn (${eventCount} events received)`);
231
+ try { await thread.post("\u26a0\ufe0f Agent returned no response. Check roundhouse logs."); } catch {}
232
+ }
233
+
210
234
  return { usedTools: usedFileModifyingTools };
211
235
  }
package/src/types.ts CHANGED
@@ -43,7 +43,8 @@ export type AgentStreamEvent =
43
43
  | { type: "draining" }
44
44
  | { type: "drain_complete" }
45
45
  | { type: "agent_end" }
46
- | { type: "custom_message"; customType: string; content: string };
46
+ | { type: "custom_message"; customType: string; content: string }
47
+ | { type: "model_error"; message: string };
47
48
 
48
49
  // ── AdapterInfo ──────────────────────────────────────
49
50