@inceptionstack/roundhouse 0.1.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.
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/architecture.md +214 -0
- package/bin/roundhouse.mjs +5 -0
- package/package.json +48 -0
- package/src/agents/pi.ts +154 -0
- package/src/agents/registry.ts +25 -0
- package/src/cli/cli.ts +425 -0
- package/src/gateway.ts +129 -0
- package/src/index.ts +121 -0
- package/src/router.ts +24 -0
- package/src/types.ts +56 -0
- package/src/util.ts +87 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.ts — Core abstractions for roundhouse
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ── Agent adapter ────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export interface AgentAdapter {
|
|
8
|
+
/** Unique agent name, e.g. "pi", "kiro" */
|
|
9
|
+
name: string;
|
|
10
|
+
|
|
11
|
+
/** Send a user message and return the full assistant response */
|
|
12
|
+
prompt(threadId: string, text: string): Promise<AgentResponse>;
|
|
13
|
+
|
|
14
|
+
/** Tear down all sessions */
|
|
15
|
+
dispose(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AgentResponse {
|
|
19
|
+
text: string;
|
|
20
|
+
/** Agent-specific metadata (tokens, cost, model, etc.) */
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Factory that creates an AgentAdapter from its config block */
|
|
25
|
+
export type AgentAdapterFactory = (
|
|
26
|
+
config: Record<string, unknown>
|
|
27
|
+
) => AgentAdapter;
|
|
28
|
+
|
|
29
|
+
// ── Agent router ─────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export interface AgentRouter {
|
|
32
|
+
/** Resolve which agent handles a given thread */
|
|
33
|
+
resolve(threadId: string): AgentAdapter;
|
|
34
|
+
|
|
35
|
+
/** Dispose all agents owned by the router */
|
|
36
|
+
dispose(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Gateway config ───────────────────────────────────
|
|
40
|
+
|
|
41
|
+
export interface GatewayConfig {
|
|
42
|
+
agent: {
|
|
43
|
+
type: string;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
};
|
|
46
|
+
chat: {
|
|
47
|
+
botUsername: string;
|
|
48
|
+
allowedUsers?: string[];
|
|
49
|
+
adapters: {
|
|
50
|
+
telegram?: Record<string, unknown>;
|
|
51
|
+
slack?: Record<string, unknown>;
|
|
52
|
+
discord?: Record<string, unknown>;
|
|
53
|
+
[key: string]: Record<string, unknown> | undefined;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
}
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* util.ts — Pure utility functions for roundhouse
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Split a long message into chunks that fit within maxLen.
|
|
7
|
+
* Prefers splitting at newline boundaries.
|
|
8
|
+
*/
|
|
9
|
+
export function splitMessage(text: string, maxLen: number): string[] {
|
|
10
|
+
if (maxLen <= 0) throw new Error(`splitMessage: maxLen must be > 0, got ${maxLen}`);
|
|
11
|
+
if (text.length <= maxLen) return [text];
|
|
12
|
+
const chunks: string[] = [];
|
|
13
|
+
let remaining = text;
|
|
14
|
+
while (remaining.length > 0) {
|
|
15
|
+
if (remaining.length <= maxLen) {
|
|
16
|
+
chunks.push(remaining);
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
let splitAt = remaining.lastIndexOf("\n", maxLen);
|
|
20
|
+
if (splitAt < maxLen / 2) splitAt = maxLen;
|
|
21
|
+
chunks.push(remaining.slice(0, splitAt));
|
|
22
|
+
// If we split at a newline, consume it so next chunk doesn't start with \n
|
|
23
|
+
if (splitAt < remaining.length && remaining[splitAt] === "\n") {
|
|
24
|
+
remaining = remaining.slice(splitAt + 1);
|
|
25
|
+
} else {
|
|
26
|
+
remaining = remaining.slice(splitAt);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return chunks;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if a Chat SDK message author is in the allowlist.
|
|
34
|
+
* Only matches on userName (unique handle) and userId (numeric ID).
|
|
35
|
+
* Does NOT match on fullName (user-controlled display name).
|
|
36
|
+
*/
|
|
37
|
+
export function isAllowed(
|
|
38
|
+
message: { author?: { userName?: string; userId?: string; fullName?: string } },
|
|
39
|
+
allowedUsers: string[]
|
|
40
|
+
): boolean {
|
|
41
|
+
if (allowedUsers.length === 0) return true;
|
|
42
|
+
const author = message.author ?? {};
|
|
43
|
+
const candidates = [author.userName, author.userId]
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.map((s) => String(s).toLowerCase());
|
|
46
|
+
return candidates.some((c) => allowedUsers.includes(c));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Start a periodic typing indicator loop.
|
|
51
|
+
* Calls thread.startTyping() immediately and then every intervalMs.
|
|
52
|
+
* Returns a stop function.
|
|
53
|
+
*/
|
|
54
|
+
export function startTypingLoop(
|
|
55
|
+
thread: { startTyping: (status?: string) => Promise<void> },
|
|
56
|
+
intervalMs: number = 4000
|
|
57
|
+
): () => void {
|
|
58
|
+
let stopped = false;
|
|
59
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
60
|
+
|
|
61
|
+
const send = () => {
|
|
62
|
+
if (stopped) return;
|
|
63
|
+
thread.startTyping().catch(() => {});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
send(); // fire immediately
|
|
67
|
+
timer = setInterval(send, intervalMs);
|
|
68
|
+
if (timer.unref) timer.unref(); // don't hold Node alive
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
stopped = true;
|
|
72
|
+
if (timer) clearInterval(timer);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Convert a threadId to a safe directory name.
|
|
78
|
+
* Uses a scheme that avoids collisions between different separators.
|
|
79
|
+
*/
|
|
80
|
+
export function threadIdToDir(threadId: string): string {
|
|
81
|
+
// Escape underscores first, then encode special chars:
|
|
82
|
+
// ":" → "_c", "_" → "_u", everything else → "_x"
|
|
83
|
+
return threadId
|
|
84
|
+
.replace(/_/g, "_u") // escape existing underscores first
|
|
85
|
+
.replace(/:/g, "_c") // encode colons
|
|
86
|
+
.replace(/[^a-zA-Z0-9_-]/g, "_x"); // encode everything else
|
|
87
|
+
}
|