@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 InceptionStack
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # roundhouse
2
+
3
+ A multi-platform chat gateway that routes messages through a single configured AI agent.
4
+
5
+ One gateway instance = one agent target (pi, Kiro, etc.), configured at install time.
6
+ Multiple chat inputs (Telegram, Slack, Discord via [Vercel Chat SDK](https://chat-sdk.dev)) all feed into that same agent.
7
+
8
+ ## Architecture
9
+
10
+ ```
11
+ ┌─────────────────────────────────────────────────┐
12
+ │ Vercel Chat SDK │
13
+ │ ┌───────────┐ ┌───────────┐ ┌──────────────┐ │
14
+ │ │ Telegram │ │ Slack │ │ Discord │ │
15
+ │ └─────┬─────┘ └─────┬─────┘ └──────┬───────┘ │
16
+ │ └──────────────┼──────────────┘ │
17
+ └───────────────────────┬──────────────────────────┘
18
+
19
+ ┌─────────┴──────────┐
20
+ │ Gateway │
21
+ │ │
22
+ │ • user allowlist │
23
+ │ • message split │
24
+ │ • typing indicator│
25
+ └─────────┬──────────┘
26
+
27
+ ┌─────────┴──────────┐
28
+ │ AgentRouter │
29
+ │ │
30
+ │ today: single │
31
+ │ agent pass-through│
32
+ │ │
33
+ │ future: per-thread│
34
+ │ multi-agent, etc. │
35
+ └─────────┬──────────┘
36
+
37
+ ┌─────────┴──────────┐
38
+ │ AgentAdapter │ ← ONE, configured at install time
39
+ │ │
40
+ │ e.g. Pi agent on │
41
+ │ THIS machine with │
42
+ │ persistent │
43
+ │ sessions on disk │
44
+ └────────────────────┘
45
+ ```
46
+
47
+ ### Design decisions
48
+
49
+ - **One gateway = one agent target.** The `agent` block in config picks the type and its settings. All chat inputs route to this single agent instance.
50
+ - **Multiple chat inputs into the same agent.** Telegram and Slack messages go to the same agent, each on their own session thread (`telegram:<id>`, `slack:<id>`).
51
+ - **AgentRouter is a seam.** Today it's `SingleAgentRouter` (hardcoded pass-through). The interface exists so we can later swap in per-thread routing, multi-agent, or load-balanced strategies without changing the gateway or adapters.
52
+ - **AgentAdapter is the only abstraction we own.** The chat side is Vercel Chat SDK — we don't wrap it. The agent side is our `AgentAdapter` interface: `prompt(threadId, text) → AgentResponse`.
53
+ - **Config-driven.** `gateway.config.json` (or `--config` flag, or env vars) determines everything at startup. No runtime reconfiguration.
54
+ - **Persistent sessions.** Each thread gets its own session file on disk. Gateway restarts resume from the same file. Pi CLI can join the same session.
55
+
56
+ ## Quick start
57
+
58
+ ```bash
59
+ npm install
60
+ export TELEGRAM_BOT_TOKEN="your-token"
61
+ export ALLOWED_USERS="your_telegram_username"
62
+ npm start
63
+ ```
64
+
65
+ ## Config
66
+
67
+ Place `gateway.config.json` in the project root, or use `--config path`:
68
+
69
+ ```json
70
+ {
71
+ "agent": {
72
+ "type": "pi",
73
+ "cwd": "/home/you/project"
74
+ },
75
+ "chat": {
76
+ "botUsername": "my_bot",
77
+ "allowedUsers": ["your_username"],
78
+ "adapters": {
79
+ "telegram": { "mode": "polling" }
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ Without a config file, defaults are used with env vars (`TELEGRAM_BOT_TOKEN`, `BOT_USERNAME`, `ALLOWED_USERS`).
86
+
87
+ ### Config reference
88
+
89
+ | Field | Description |
90
+ |-------|-------------|
91
+ | `agent.type` | Agent backend: `"pi"` (more coming) |
92
+ | `agent.cwd` | Working directory for the agent |
93
+ | `agent.sessionDir` | Override session storage path |
94
+ | `chat.botUsername` | Bot display name for Chat SDK |
95
+ | `chat.allowedUsers` | Telegram usernames / user IDs allowed (empty = allow all) |
96
+ | `chat.adapters.telegram` | `{ "mode": "polling" \| "webhook" \| "auto" }` |
97
+
98
+ Secrets stay in env vars: `TELEGRAM_BOT_TOKEN`, `ANTHROPIC_API_KEY`, etc.
99
+
100
+ ## Joining a session from pi CLI
101
+
102
+ Sessions are stored at `~/.pi/agent/gateway-sessions/<thread>/`. Resume from CLI:
103
+
104
+ ```bash
105
+ pi --resume ~/.pi/agent/gateway-sessions/<thread_dir>/<session>.jsonl
106
+ ```
107
+
108
+ Messages from Telegram/Slack and from the CLI share the same context.
109
+
110
+ ## Adding a new agent backend
111
+
112
+ 1. Create `src/agents/kiro.ts` implementing `AgentAdapter`
113
+ 2. Register in `src/agents/registry.ts`: `registry.set("kiro", createKiroAgentAdapter)`
114
+ 3. Set `"agent": { "type": "kiro" }` in config
115
+
116
+ ```typescript
117
+ // src/agents/kiro.ts
118
+ import type { AgentAdapter, AgentAdapterFactory } from "../types";
119
+
120
+ export const createKiroAgentAdapter: AgentAdapterFactory = (config) => {
121
+ return {
122
+ name: "kiro",
123
+ async prompt(threadId, text) {
124
+ // your implementation
125
+ return { text: "response" };
126
+ },
127
+ async dispose() {},
128
+ };
129
+ };
130
+ ```
131
+
132
+ ## Adding a new chat platform
133
+
134
+ Add the Chat SDK adapter package and wire it in `gateway.ts`:
135
+
136
+ ```typescript
137
+ // In buildChatAdapters():
138
+ if (config.slack) {
139
+ const { createSlackAdapter } = await import("@chat-adapter/slack");
140
+ adapters.slack = createSlackAdapter();
141
+ }
142
+ ```
143
+
144
+ No other changes needed — the gateway's unified handler covers all platforms.
145
+
146
+ ## Files
147
+
148
+ | File | Purpose |
149
+ |------|---------|
150
+ | `src/index.ts` | Entry point, config loading, startup |
151
+ | `src/gateway.ts` | Owns Chat SDK, wires events → router → agent |
152
+ | `src/router.ts` | `AgentRouter` interface + `SingleAgentRouter` |
153
+ | `src/types.ts` | Core interfaces: `AgentAdapter`, `AgentRouter`, `GatewayConfig` |
154
+ | `src/util.ts` | Pure utilities: `splitMessage`, `isAllowed`, `threadIdToDir` |
155
+ | `src/agents/pi.ts` | Pi agent adapter (persistent sessions via pi SDK) |
156
+ | `src/agents/registry.ts` | Agent type → factory registry |
157
+ | `test/` | Unit tests (vitest, 32 passing) |
158
+
159
+ ## Testing
160
+
161
+ ```bash
162
+ npm test # run once
163
+ npm run test:watch # watch mode
164
+ ```
@@ -0,0 +1,214 @@
1
+ # Architecture
2
+
3
+ ## Overview
4
+
5
+ Roundhouse is a gateway that sits between **chat platforms** (Telegram, Slack, Discord) and a **single AI agent backend** (pi, Kiro, etc.).
6
+
7
+ One gateway instance is configured for exactly one agent target at install time. Multiple chat platforms can feed into that same agent simultaneously.
8
+
9
+ ## System diagram
10
+
11
+ ```
12
+ ┌─────────────────────────────────────────────────────┐
13
+ │ Vercel Chat SDK │
14
+ │ │
15
+ │ ┌────────────┐ ┌────────────┐ ┌─────────────┐ │
16
+ Telegram users ──▶│ │ Telegram │ │ Slack │ │ Discord │ │◀── Discord users
17
+ │ │ Adapter │ │ Adapter │ │ Adapter │ │
18
+ Slack users ─────▶│ └─────┬──────┘ └─────┬──────┘ └──────┬──────┘ │
19
+ │ │ │ │ │
20
+ │ └───────────────┼────────────────┘ │
21
+ │ │ │
22
+ │ onDirectMessage / onNewMention / │
23
+ │ onSubscribedMessage │
24
+ └─────────────────────────┬──────────────────────────┘
25
+
26
+
27
+ ┌───────────────────────┐
28
+ │ Gateway │
29
+ │ │
30
+ │ • User allowlist │
31
+ │ • Message splitting │
32
+ │ • Typing indicators │
33
+ │ • Error handling │
34
+ └───────────┬────────────┘
35
+
36
+
37
+ ┌───────────────────────┐
38
+ │ AgentRouter │
39
+ │ │
40
+ │ SingleAgentRouter │
41
+ │ (pass-through today) │
42
+ │ │
43
+ │ Future: │
44
+ │ • MultiAgentRouter │
45
+ │ • UserChoiceRouter │
46
+ │ • FallbackRouter │
47
+ └───────────┬────────────┘
48
+
49
+
50
+ ┌───────────────────────┐
51
+ │ AgentAdapter │
52
+ │ │
53
+ │ Configured at install │
54
+ │ time via config file │
55
+ │ │
56
+ │ ┌─────────────────┐ │
57
+ │ │ Pi Agent │ │
58
+ │ │ │ │
59
+ │ │ • pi SDK │ │
60
+ │ │ • persistent │ │
61
+ │ │ .jsonl │ │
62
+ │ │ sessions │ │
63
+ │ │ • per-thread │ │
64
+ │ │ isolation │ │
65
+ │ └─────────────────┘ │
66
+ │ │
67
+ │ (or Kiro, Raw LLM, │
68
+ │ custom agent, etc.) │
69
+ └────────────────────────┘
70
+
71
+
72
+ ┌────────────────────────┐
73
+ │ Session storage │
74
+ │ │
75
+ │ ~/.pi/agent/ │
76
+ │ gateway-sessions/ │
77
+ │ telegram_c<id>/ │
78
+ │ <session>.jsonl │
79
+ │ slack_c<id>/ │
80
+ │ <session>.jsonl │
81
+ └────────────────────────┘
82
+ ```
83
+
84
+ ## Data flow
85
+
86
+ ```
87
+ User sends "list files" on Telegram
88
+
89
+
90
+ ┌─ Vercel Chat SDK ────────────────────────────────────────────┐
91
+ │ Telegram adapter receives update via polling │
92
+ │ Normalizes to: { thread.id, message.text, message.author } │
93
+ │ Fires onDirectMessage(thread, message) │
94
+ └──────────────────────────────┬───────────────────────────────┘
95
+
96
+
97
+ ┌─ Gateway ────────────────────────────────────────────────────┐
98
+ │ 1. Check isAllowed(message.author, allowedUsers) │
99
+ │ 2. Resolve agent via router.resolve(thread.id) │
100
+ │ 3. thread.startTyping() │
101
+ │ 4. agent.prompt(thread.id, "list files") │
102
+ │ └─▶ Pi SDK creates/resumes session │
103
+ │ └─▶ LLM processes, tools execute │
104
+ │ └─▶ Returns AgentResponse { text: "..." } │
105
+ │ 5. splitMessage(response.text, 4000) │
106
+ │ 6. thread.post(chunk) for each chunk │
107
+ └──────────────────────────────────────────────────────────────┘
108
+
109
+
110
+ User receives reply on Telegram
111
+ ```
112
+
113
+ ## Key interfaces
114
+
115
+ ```typescript
116
+ interface AgentAdapter {
117
+ name: string;
118
+ prompt(threadId: string, text: string): Promise<AgentResponse>;
119
+ dispose(): Promise<void>;
120
+ }
121
+
122
+ interface AgentResponse {
123
+ text: string;
124
+ metadata?: Record<string, unknown>;
125
+ }
126
+
127
+ interface AgentRouter {
128
+ resolve(threadId: string): AgentAdapter;
129
+ dispose(): Promise<void>;
130
+ }
131
+ ```
132
+
133
+ ## Config model
134
+
135
+ ```
136
+ gateway.config.json
137
+ ├── agent # Exactly ONE agent target
138
+ │ ├── type: "pi" # Selects factory from registry
139
+ │ ├── cwd: "/home/user" # Agent working directory
140
+ │ └── sessionDir: "..." # Override session storage
141
+
142
+ └── chat # Multiple chat inputs
143
+ ├── botUsername: "my_bot"
144
+ ├── allowedUsers: [...] # Auth filter (userName or userId)
145
+ └── adapters
146
+ ├── telegram: { mode: "polling" }
147
+ ├── slack: { ... } # (future)
148
+ └── discord: { ... } # (future)
149
+ ```
150
+
151
+ Secrets (`TELEGRAM_BOT_TOKEN`, `ANTHROPIC_API_KEY`) are always env vars, never in config.
152
+
153
+ ## Startup sequence
154
+
155
+ ```
156
+ 1. Load config (--config flag → gateway.config.json → env var defaults)
157
+ 2. Look up agent.type in registry → get factory function
158
+ 3. factory(agentConfig) → AgentAdapter instance
159
+ 4. Wrap in SingleAgentRouter
160
+ 5. Create Gateway(router, config)
161
+ 6. gateway.start():
162
+ a. Build Chat SDK adapters from config (lazy import)
163
+ b. Create Chat instance with all adapters
164
+ c. Wire onDirectMessage / onNewMention / onSubscribedMessage → handle()
165
+ d. chat.initialize() — starts polling / webhooks
166
+ 7. Running. Ctrl+C → gateway.stop() → router.dispose() → agent.dispose()
167
+ ```
168
+
169
+ ## Session threading
170
+
171
+ Each chat platform thread gets its own agent session:
172
+
173
+ ```
174
+ Telegram DM with Alice → threadId = "telegram:123456789" → session A
175
+ Slack DM with Alice → threadId = "slack:U12345" → session B
176
+ Telegram group mention → threadId = "telegram:-100123456" → session C
177
+ ```
178
+
179
+ These are **separate sessions** by design. Cross-platform session unification (mapping multiple platform identities to one session) is a future capability.
180
+
181
+ Sessions persist as `.jsonl` files. The gateway resumes them on restart. Pi CLI can join any session with:
182
+
183
+ ```bash
184
+ pi --resume ~/.pi/agent/gateway-sessions/<thread_dir>/<session>.jsonl
185
+ ```
186
+
187
+ ## Router extensibility
188
+
189
+ The `AgentRouter` interface is a seam for future multi-agent routing:
190
+
191
+ | Router | Behavior |
192
+ |--------|----------|
193
+ | `SingleAgentRouter` | All threads → one agent (current) |
194
+ | `MultiAgentRouter` | Map thread prefixes to different agents |
195
+ | `UserChoiceRouter` | User sends `/agent kiro` to switch |
196
+ | `FallbackRouter` | Try primary agent, fall back to secondary |
197
+ | `RoundRobinRouter` | Load balance across agent instances |
198
+
199
+ The gateway and agent adapters don't change — only the router.
200
+
201
+ ## Module dependency graph
202
+
203
+ ```
204
+ index.ts
205
+ ├── agents/registry.ts
206
+ │ └── agents/pi.ts
207
+ │ └── util.ts (threadIdToDir)
208
+ ├── router.ts
209
+ ├── gateway.ts
210
+ │ └── util.ts (splitMessage, isAllowed)
211
+ └── types.ts (shared interfaces)
212
+ ```
213
+
214
+ No circular dependencies. `util.ts` and `types.ts` are leaf modules.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import("tsx/esm/api").then(({ register }) => {
3
+ register();
4
+ import("../src/cli/cli.ts");
5
+ });
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@inceptionstack/roundhouse",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Multi-platform chat gateway that routes messages through a configured AI agent",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/inceptionstack/roundhouse.git"
10
+ },
11
+ "keywords": [
12
+ "chat",
13
+ "gateway",
14
+ "telegram",
15
+ "slack",
16
+ "discord",
17
+ "ai",
18
+ "agent",
19
+ "pi",
20
+ "bot"
21
+ ],
22
+ "bin": {
23
+ "roundhouse": "bin/roundhouse.mjs"
24
+ },
25
+ "scripts": {
26
+ "start": "tsx src/index.ts",
27
+ "dev": "tsx watch src/index.ts",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest"
30
+ },
31
+ "files": [
32
+ "src/",
33
+ "bin/",
34
+ "LICENSE",
35
+ "README.md",
36
+ "architecture.md"
37
+ ],
38
+ "dependencies": {
39
+ "@chat-adapter/state-memory": "latest",
40
+ "@chat-adapter/telegram": "latest",
41
+ "@mariozechner/pi-coding-agent": "latest",
42
+ "chat": "latest",
43
+ "tsx": "^4.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "vitest": "^4.1.5"
47
+ }
48
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * agents/pi.ts — Pi agent adapter
3
+ *
4
+ * Wraps pi's SDK (createAgentSession) as an AgentAdapter.
5
+ * One persistent session per thread, stored at:
6
+ * ~/.pi/agent/gateway-sessions/<thread_id>/<session>.jsonl
7
+ */
8
+
9
+ import { mkdir } from "node:fs/promises";
10
+ import { join } from "node:path";
11
+ import { homedir } from "node:os";
12
+
13
+ import {
14
+ AuthStorage,
15
+ createAgentSession,
16
+ ModelRegistry,
17
+ SessionManager,
18
+ type AgentSession,
19
+ type AgentSessionEvent,
20
+ } from "@mariozechner/pi-coding-agent";
21
+
22
+ import type { AgentAdapter, AgentAdapterFactory, AgentResponse } from "../types";
23
+ import { threadIdToDir } from "../util";
24
+
25
+ interface SessionEntry {
26
+ session: AgentSession;
27
+ lastUsed: number;
28
+ }
29
+
30
+ const DEFAULT_SESSIONS_DIR = join(
31
+ homedir(),
32
+ ".pi",
33
+ "agent",
34
+ "gateway-sessions"
35
+ );
36
+ const DEFAULT_MAX_IDLE_MS = 30 * 60 * 1000;
37
+
38
+ export const createPiAgentAdapter: AgentAdapterFactory = (config) => {
39
+ const cwd = (config.cwd as string) ?? process.cwd();
40
+ const sessionsDir =
41
+ (config.sessionDir as string) ?? DEFAULT_SESSIONS_DIR;
42
+ const maxIdleMs =
43
+ (config.maxIdleMs as number) ?? DEFAULT_MAX_IDLE_MS;
44
+
45
+ const authStorage = AuthStorage.create();
46
+ const modelRegistry = ModelRegistry.create(authStorage);
47
+ const sessions = new Map<string, SessionEntry>();
48
+ // Track in-flight session creation to prevent races
49
+ const creating = new Map<string, Promise<SessionEntry>>();
50
+ let reapInterval: ReturnType<typeof setInterval> | undefined;
51
+
52
+ async function createSession(threadId: string): Promise<SessionEntry> {
53
+ const threadDir = join(sessionsDir, threadIdToDir(threadId));
54
+ await mkdir(threadDir, { recursive: true });
55
+
56
+ let sessionManager: InstanceType<typeof SessionManager>;
57
+
58
+ try {
59
+ sessionManager = SessionManager.continueRecent(cwd, threadDir);
60
+ console.log(
61
+ `[pi-agent] resuming session for ${threadId}: ${sessionManager.getSessionFile()}`
62
+ );
63
+ } catch {
64
+ sessionManager = SessionManager.create(cwd, threadDir);
65
+ console.log(
66
+ `[pi-agent] new session for ${threadId}: ${sessionManager.getSessionFile()}`
67
+ );
68
+ }
69
+
70
+ const result = await createAgentSession({
71
+ cwd,
72
+ sessionManager,
73
+ authStorage,
74
+ modelRegistry,
75
+ });
76
+
77
+ if (result.modelFallbackMessage) {
78
+ console.log(`[pi-agent] model fallback: ${result.modelFallbackMessage}`);
79
+ }
80
+
81
+ const entry: SessionEntry = { session: result.session, lastUsed: Date.now() };
82
+ sessions.set(threadId, entry);
83
+ return entry;
84
+ }
85
+
86
+ async function getOrCreate(threadId: string): Promise<SessionEntry> {
87
+ // Fast path: already created
88
+ const existing = sessions.get(threadId);
89
+ if (existing) return existing;
90
+
91
+ // Prevent concurrent creation for the same threadId
92
+ let pending = creating.get(threadId);
93
+ if (pending) return pending;
94
+
95
+ pending = createSession(threadId).finally(() => {
96
+ creating.delete(threadId);
97
+ });
98
+ creating.set(threadId, pending);
99
+ return pending;
100
+ }
101
+
102
+ function reap() {
103
+ const now = Date.now();
104
+ for (const [id, entry] of sessions) {
105
+ if (now - entry.lastUsed > maxIdleMs) {
106
+ entry.session.dispose();
107
+ sessions.delete(id);
108
+ console.log(`[pi-agent] reaped idle handle for ${id}`);
109
+ }
110
+ }
111
+ }
112
+
113
+ // Start reaper (unref so it doesn't prevent Node from exiting)
114
+ reapInterval = setInterval(reap, 60_000);
115
+ reapInterval.unref();
116
+
117
+ const adapter: AgentAdapter = {
118
+ name: "pi",
119
+
120
+ async prompt(threadId: string, text: string): Promise<AgentResponse> {
121
+ const entry = await getOrCreate(threadId);
122
+ entry.lastUsed = Date.now();
123
+
124
+ let fullText = "";
125
+ const unsub = entry.session.subscribe((event: AgentSessionEvent) => {
126
+ if (
127
+ event.type === "message_update" &&
128
+ event.assistantMessageEvent.type === "text_delta"
129
+ ) {
130
+ fullText += event.assistantMessageEvent.delta;
131
+ }
132
+ });
133
+
134
+ try {
135
+ await entry.session.prompt(text);
136
+ } finally {
137
+ unsub();
138
+ }
139
+
140
+ return { text: fullText };
141
+ },
142
+
143
+ async dispose(): Promise<void> {
144
+ if (reapInterval) clearInterval(reapInterval);
145
+ for (const [, entry] of sessions) {
146
+ entry.session.dispose();
147
+ }
148
+ sessions.clear();
149
+ creating.clear();
150
+ },
151
+ };
152
+
153
+ return adapter;
154
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * agents/registry.ts — Agent adapter registry
3
+ *
4
+ * Maps agent type names to their factory functions.
5
+ * Add new agents here.
6
+ */
7
+
8
+ import type { AgentAdapterFactory } from "../types";
9
+ import { createPiAgentAdapter } from "./pi";
10
+
11
+ const registry = new Map<string, AgentAdapterFactory>();
12
+
13
+ registry.set("pi", createPiAgentAdapter);
14
+ // registry.set("kiro", createKiroAgentAdapter);
15
+
16
+ export function getAgentFactory(type: string): AgentAdapterFactory {
17
+ const factory = registry.get(type);
18
+ if (!factory) {
19
+ const available = [...registry.keys()].join(", ");
20
+ throw new Error(
21
+ `Unknown agent type "${type}". Available: ${available}`
22
+ );
23
+ }
24
+ return factory;
25
+ }