@inceptionstack/roundhouse 0.5.2 → 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/architecture.md +94 -32
- package/package.json +1 -1
- package/src/agents/kiro/kiro-adapter.ts +8 -1
- package/src/agents/pi/message-format.ts +87 -0
- package/src/agents/pi/pi-adapter.ts +33 -72
- package/src/cli/agent-command.ts +210 -0
- package/src/cli/cli.ts +63 -305
- package/src/cli/cron-commands.ts +258 -0
- package/src/cli/cron.ts +26 -267
- package/src/cli/launchd.ts +1 -1
- package/src/cli/service-manager.ts +192 -0
- package/src/cli/setup/args.ts +109 -0
- package/src/cli/setup/flows.ts +273 -0
- package/src/cli/setup/helpers.ts +66 -0
- package/src/cli/setup/index.ts +7 -0
- package/src/cli/setup/runtime.ts +109 -0
- package/src/cli/setup/steps.ts +617 -0
- package/src/cli/setup/types.ts +52 -0
- package/src/cli/setup.ts +79 -1275
- package/src/cli/shell.ts +49 -0
- package/src/cli/systemd.ts +6 -33
- package/src/config.ts +67 -53
- package/src/gateway/attachments.ts +147 -0
- package/src/gateway/commands.ts +371 -0
- package/src/gateway/helpers.ts +104 -0
- package/src/gateway/index.ts +11 -0
- package/src/gateway/streaming.ts +235 -0
- package/src/gateway.ts +212 -763
- package/src/types.ts +16 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gateway/streaming.ts — Agent stream event handler
|
|
3
|
+
*
|
|
4
|
+
* Processes the async stream of agent events and routes them:
|
|
5
|
+
* - text_delta → collected per-turn, sent via thread.handleStream()
|
|
6
|
+
* - tool_start/end → compact status messages (verbose mode)
|
|
7
|
+
* - turn_end → flush current stream, start fresh
|
|
8
|
+
* - custom_message → flush and post as separate message
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { AgentStreamEvent } from "../types";
|
|
12
|
+
import { READ_ONLY_TOOLS } from "../memory/types";
|
|
13
|
+
import { isTelegramThread, handleTelegramHtmlStream } from "../telegram-html";
|
|
14
|
+
import { DEBUG_STREAM } from "../util";
|
|
15
|
+
import { toolIcon } from "./helpers";
|
|
16
|
+
|
|
17
|
+
// ── Text Stream Factory ──────────────────────────────
|
|
18
|
+
|
|
19
|
+
export function createTextStream(): {
|
|
20
|
+
iterable: AsyncIterable<string>;
|
|
21
|
+
push: (text: string) => void;
|
|
22
|
+
finish: () => void;
|
|
23
|
+
} {
|
|
24
|
+
let buffer = "";
|
|
25
|
+
let resolve: ((value: IteratorResult<string>) => void) | null = null;
|
|
26
|
+
let done = false;
|
|
27
|
+
|
|
28
|
+
const iterable: AsyncIterable<string> = {
|
|
29
|
+
[Symbol.asyncIterator]() {
|
|
30
|
+
return {
|
|
31
|
+
async next(): Promise<IteratorResult<string>> {
|
|
32
|
+
if (buffer) {
|
|
33
|
+
const chunk = buffer;
|
|
34
|
+
buffer = "";
|
|
35
|
+
return { value: chunk, done: false };
|
|
36
|
+
}
|
|
37
|
+
if (done) return { value: undefined as any, done: true };
|
|
38
|
+
return new Promise((r) => { resolve = r; });
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
iterable,
|
|
46
|
+
push(text: string) {
|
|
47
|
+
if (resolve) {
|
|
48
|
+
const r = resolve;
|
|
49
|
+
resolve = null;
|
|
50
|
+
r({ value: text, done: false });
|
|
51
|
+
} else {
|
|
52
|
+
buffer += text;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
finish() {
|
|
56
|
+
done = true;
|
|
57
|
+
resolve?.({ value: undefined as any, done: true });
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Stream Handler ───────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export interface StreamContext {
|
|
65
|
+
thread: any;
|
|
66
|
+
verbose: boolean;
|
|
67
|
+
signal?: AbortSignal;
|
|
68
|
+
postWithFallback: (thread: any, text: string) => Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface StreamResult {
|
|
72
|
+
usedTools: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Handle the agent's event stream, routing events to the chat thread.
|
|
77
|
+
*/
|
|
78
|
+
export async function handleStreaming(
|
|
79
|
+
stream: AsyncIterable<AgentStreamEvent>,
|
|
80
|
+
ctx: StreamContext,
|
|
81
|
+
): Promise<StreamResult> {
|
|
82
|
+
const { thread, verbose, signal, postWithFallback } = ctx;
|
|
83
|
+
let activeTools = new Map<string, string>();
|
|
84
|
+
let usedFileModifyingTools = false;
|
|
85
|
+
|
|
86
|
+
let currentPush: ((text: string) => void) | null = null;
|
|
87
|
+
let currentFinish: (() => void) | null = null;
|
|
88
|
+
let currentPromise: Promise<void> | null = null;
|
|
89
|
+
|
|
90
|
+
const flushCurrentStream = async () => {
|
|
91
|
+
if (!currentPromise) return;
|
|
92
|
+
currentFinish?.();
|
|
93
|
+
try { await currentPromise; } catch (err) {
|
|
94
|
+
console.warn(`[roundhouse] stream flush error:`, (err as Error).message);
|
|
95
|
+
}
|
|
96
|
+
currentPush = null;
|
|
97
|
+
currentFinish = null;
|
|
98
|
+
currentPromise = null;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const useTelegramHtml = isTelegramThread(thread);
|
|
102
|
+
|
|
103
|
+
const ensureStream = () => {
|
|
104
|
+
if (!currentPromise) {
|
|
105
|
+
const ts = createTextStream();
|
|
106
|
+
currentPush = ts.push;
|
|
107
|
+
currentFinish = ts.finish;
|
|
108
|
+
currentPromise = useTelegramHtml
|
|
109
|
+
? handleTelegramHtmlStream(thread, ts.iterable).catch((err: Error) => {
|
|
110
|
+
console.warn(`[roundhouse] telegram html stream error:`, err.message);
|
|
111
|
+
})
|
|
112
|
+
: thread.handleStream(ts.iterable).catch((err: Error) => {
|
|
113
|
+
console.warn(`[roundhouse] handleStream error:`, err.message);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
let hasTextInCurrentTurn = false;
|
|
119
|
+
let hasContentThisTurn = false;
|
|
120
|
+
let modelErrorPosted = false;
|
|
121
|
+
let eventCount = 0;
|
|
122
|
+
let drainingNotified = false;
|
|
123
|
+
|
|
124
|
+
for await (const event of stream) {
|
|
125
|
+
if (signal?.aborted) {
|
|
126
|
+
console.log(`[roundhouse] stream aborted for thread`);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
eventCount++;
|
|
131
|
+
|
|
132
|
+
if (DEBUG_STREAM) {
|
|
133
|
+
const preview = event.type === "text_delta" ? `"${event.text.slice(0, 30)}"`
|
|
134
|
+
: event.type === "custom_message" ? `${event.customType}:${event.content.slice(0, 30)}`
|
|
135
|
+
: event.type === "tool_start" || event.type === "tool_end" ? event.toolName
|
|
136
|
+
: "";
|
|
137
|
+
console.log(`[roundhouse/stream] #${eventCount} ${event.type} ${preview}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
switch (event.type) {
|
|
141
|
+
case "text_delta": {
|
|
142
|
+
ensureStream();
|
|
143
|
+
currentPush!(event.text);
|
|
144
|
+
hasTextInCurrentTurn = true;
|
|
145
|
+
hasContentThisTurn = true;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
case "tool_start": {
|
|
150
|
+
activeTools.set(event.toolCallId, event.toolName);
|
|
151
|
+
if (!READ_ONLY_TOOLS.has(event.toolName)) usedFileModifyingTools = true;
|
|
152
|
+
hasContentThisTurn = true;
|
|
153
|
+
if (verbose) {
|
|
154
|
+
try { await thread.post(`${toolIcon(event.toolName)} Running \`${event.toolName}\`…`); } catch {}
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case "tool_end": {
|
|
160
|
+
activeTools.delete(event.toolCallId);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
case "custom_message": {
|
|
165
|
+
if (currentPromise) {
|
|
166
|
+
await flushCurrentStream();
|
|
167
|
+
hasTextInCurrentTurn = false;
|
|
168
|
+
}
|
|
169
|
+
hasContentThisTurn = true;
|
|
170
|
+
await postWithFallback(thread, event.content);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
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
|
+
|
|
185
|
+
case "turn_end": {
|
|
186
|
+
if (hasTextInCurrentTurn) {
|
|
187
|
+
await flushCurrentStream();
|
|
188
|
+
hasTextInCurrentTurn = false;
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
case "draining": {
|
|
194
|
+
if (hasTextInCurrentTurn) {
|
|
195
|
+
await flushCurrentStream();
|
|
196
|
+
hasTextInCurrentTurn = false;
|
|
197
|
+
}
|
|
198
|
+
try { await thread.post("⏳ Hold on — waiting for follow-up messages..."); drainingNotified = true; } catch {}
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case "drain_complete": {
|
|
203
|
+
if (hasTextInCurrentTurn) {
|
|
204
|
+
await flushCurrentStream();
|
|
205
|
+
hasTextInCurrentTurn = false;
|
|
206
|
+
}
|
|
207
|
+
if (drainingNotified) {
|
|
208
|
+
try { await thread.post("✅ All done — waiting for your input."); } catch {}
|
|
209
|
+
drainingNotified = false;
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
case "agent_end": {
|
|
215
|
+
if (hasTextInCurrentTurn) {
|
|
216
|
+
await flushCurrentStream();
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (currentPromise) {
|
|
224
|
+
await flushCurrentStream();
|
|
225
|
+
}
|
|
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
|
+
|
|
234
|
+
return { usedTools: usedFileModifyingTools };
|
|
235
|
+
}
|