@inceptionstack/roundhouse 0.4.2 → 0.4.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/README.md +27 -18
- package/architecture.md +10 -4
- package/package.json +1 -6
- package/pi/extensions/web-search.ts +1 -9
- package/src/agents/base-adapter.ts +92 -0
- package/src/agents/index.ts +7 -0
- package/src/agents/kiro/acp/client.ts +117 -0
- package/src/agents/kiro/acp/index.ts +8 -0
- package/src/agents/kiro/acp/process.ts +111 -0
- package/src/agents/kiro/acp/types.ts +83 -0
- package/src/agents/kiro/kiro-adapter.ts +448 -0
- package/src/agents/kiro/session.ts +124 -0
- package/src/agents/kiro/tool-names.ts +37 -0
- package/src/agents/{pi.ts → pi/pi-adapter.ts} +8 -5
- package/src/agents/registry.ts +26 -4
- package/src/bundle.ts +83 -2
- package/src/cli/cli.ts +38 -2
- package/src/cli/doctor/checks/credentials.ts +2 -2
- package/src/cli/env-file.ts +25 -0
- package/src/cli/setup.ts +16 -5
- package/src/gateway.ts +12 -0
- package/src/types.ts +66 -20
package/README.md
CHANGED
|
@@ -58,7 +58,8 @@ When you run `roundhouse setup`, the following are installed automatically:
|
|
|
58
58
|
|
|
59
59
|
- **30+ Skills** (agent knowledge): Synced from [loki-skills](https://github.com/inceptionstack/loki-skills) (AWS, infrastructure, DevOps patterns)
|
|
60
60
|
- **CLI Tools**: `mcporter` (MCP server bridge), `@playwright/cli` (browser automation), `uv`/`uvx` (Python package runner)
|
|
61
|
-
- **Extensions** (
|
|
61
|
+
- **Extensions** (copied to `~/.pi/agent/extensions/` if not present; never overwrites user copies): `web-search` (Tavily)
|
|
62
|
+
- **Extension packages** (registered in `settings.json`): `pi-hard-no` (code review), `pi-branch-enforcer` (branch protection)
|
|
62
63
|
- **Config**: MCP server definitions copied to `~/.mcporter/mcporter.json`
|
|
63
64
|
|
|
64
65
|
This gives the agent access to:
|
|
@@ -429,24 +430,30 @@ pi install git:github.com/inceptionstack/pi-autoreview
|
|
|
429
430
|
|
|
430
431
|
## Adding a new agent backend
|
|
431
432
|
|
|
432
|
-
1. Create `src/agents/
|
|
433
|
-
2. Register in `src/agents/registry.ts
|
|
434
|
-
3. Set `"agent": { "type": "
|
|
433
|
+
1. Create `src/agents/myagent/myagent-adapter.ts` extending `BaseAdapter`
|
|
434
|
+
2. Register in `src/agents/registry.ts`
|
|
435
|
+
3. Set `"agent": { "type": "myagent" }` in config
|
|
435
436
|
|
|
436
437
|
```typescript
|
|
437
|
-
import type {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
};
|
|
438
|
+
import type { AgentAdapterFactory, AgentMessage, AgentResponse, AgentStreamEvent } from "../../types.js";
|
|
439
|
+
import { BaseAdapter } from "../base-adapter.js";
|
|
440
|
+
|
|
441
|
+
class MyAgentAdapter extends BaseAdapter {
|
|
442
|
+
readonly name = "myagent";
|
|
443
|
+
|
|
444
|
+
async prompt(threadId: string, message: AgentMessage): Promise<AgentResponse> {
|
|
445
|
+
return { text: "response" };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async *promptStream(threadId: string, message: AgentMessage): AsyncIterable<AgentStreamEvent> {
|
|
449
|
+
yield { type: "text_delta", text: "response" };
|
|
450
|
+
yield { type: "agent_end" };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async dispose(): Promise<void> {}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export const createMyAgentAdapter: AgentAdapterFactory = (config) => new MyAgentAdapter();
|
|
450
457
|
```
|
|
451
458
|
|
|
452
459
|
## Adding a new chat platform
|
|
@@ -480,7 +487,9 @@ No other changes needed — the gateway's unified handler covers all platforms.
|
|
|
480
487
|
| `src/cron/` | Cron scheduler, runner, store, schedule, template, format |
|
|
481
488
|
| `src/cron/helpers.ts` | Shared cron constants and utilities |
|
|
482
489
|
| `src/notify/telegram.ts` | Shared Telegram Bot API sender |
|
|
483
|
-
| `src/agents/pi.ts` | Pi agent adapter (persistent sessions via pi SDK) |
|
|
490
|
+
| `src/agents/pi/pi-adapter.ts` | Pi agent adapter (persistent sessions via pi SDK) |
|
|
491
|
+
| `src/agents/kiro/kiro-adapter.ts` | Kiro CLI agent adapter (ACP over stdio) |
|
|
492
|
+
| `src/agents/base-adapter.ts` | Abstract base class — adapter interface contract |
|
|
484
493
|
| `src/agents/registry.ts` | Agent type → factory registry |
|
|
485
494
|
| `src/config.ts` | Shared config loading, defaults, env overrides |
|
|
486
495
|
| `test/` | Unit tests (vitest, 75 passing) |
|
package/architecture.md
CHANGED
|
@@ -143,16 +143,22 @@ interface AttachmentTranscript {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
interface AgentAdapter {
|
|
146
|
-
name: string;
|
|
147
|
-
prompt(threadId: string, message: AgentMessage): Promise<AgentResponse>;
|
|
148
|
-
promptStream
|
|
146
|
+
readonly name: string;
|
|
147
|
+
prompt(threadId: string, message: AgentMessage): Promise<AgentResponse>; // required
|
|
148
|
+
promptStream(threadId: string, message: AgentMessage): AsyncIterable<AgentStreamEvent>; // required
|
|
149
|
+
dispose(): Promise<void>; // required
|
|
150
|
+
promptWithModel?(threadId: string, message: AgentMessage, modelId: string): Promise<AgentResponse>;
|
|
149
151
|
restart?(threadId: string): Promise<void>;
|
|
150
152
|
compact?(threadId: string): Promise<{ tokensBefore: number; tokensAfter: number | null } | null>;
|
|
153
|
+
compactWithModel?(threadId: string, modelId: string): Promise<{ tokensBefore: number; tokensAfter: number | null } | null>;
|
|
151
154
|
abort?(threadId: string): Promise<void>;
|
|
152
155
|
getInfo?(threadId?: string): Record<string, unknown>;
|
|
153
|
-
dispose(): Promise<void>;
|
|
154
156
|
}
|
|
155
157
|
|
|
158
|
+
// New adapters extend BaseAdapter (src/agents/base-adapter.ts) which
|
|
159
|
+
// provides default implementations for optional methods.
|
|
160
|
+
// Each adapter lives in its own directory: pi/pi-adapter.ts, kiro/kiro-adapter.ts
|
|
161
|
+
|
|
156
162
|
interface AgentResponse {
|
|
157
163
|
text: string;
|
|
158
164
|
metadata?: Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inceptionstack/roundhouse",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Multi-platform chat gateway that routes messages through a configured AI agent",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,10 +48,5 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"vitest": "^4.1.5"
|
|
51
|
-
},
|
|
52
|
-
"pi": {
|
|
53
|
-
"extensions": [
|
|
54
|
-
"./pi/extensions"
|
|
55
|
-
]
|
|
56
51
|
}
|
|
57
52
|
}
|
|
@@ -31,8 +31,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
31
31
|
const startTime = Date.now();
|
|
32
32
|
const controller = new AbortController();
|
|
33
33
|
const timeout = setTimeout(() => controller.abort(), 30_000);
|
|
34
|
-
if (signal
|
|
35
|
-
else if (signal) signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
34
|
+
if (signal) signal.addEventListener("abort", () => controller.abort());
|
|
36
35
|
|
|
37
36
|
let response: Response;
|
|
38
37
|
try {
|
|
@@ -47,13 +46,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
47
46
|
}),
|
|
48
47
|
signal: controller.signal,
|
|
49
48
|
});
|
|
50
|
-
} catch (err: any) {
|
|
51
|
-
clearTimeout(timeout);
|
|
52
|
-
const msg = err.name === "AbortError" ? "Request timed out or was cancelled" : err.message;
|
|
53
|
-
return {
|
|
54
|
-
content: [{ type: "text", text: `Web search failed: ${msg}` }],
|
|
55
|
-
details: { query: params.query, error: msg },
|
|
56
|
-
};
|
|
57
49
|
} finally {
|
|
58
50
|
clearTimeout(timeout);
|
|
59
51
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agents/base-adapter.ts — Abstract base class for agent adapters.
|
|
3
|
+
*
|
|
4
|
+
* Defines the fixed interface contract that every adapter must fulfill.
|
|
5
|
+
* Required methods are abstract; optional capabilities have default
|
|
6
|
+
* implementations that callers can rely on.
|
|
7
|
+
*
|
|
8
|
+
* Each concrete adapter (PiAdapter, KiroAdapter) extends this class
|
|
9
|
+
* and lives in its own directory with no cross-adapter imports.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { AgentAdapter, AgentMessage, AgentResponse, AgentStreamEvent, AdapterInfo } from "../types.js";
|
|
13
|
+
export type { AdapterInfo } from "../types.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Abstract base class for all agent adapters.
|
|
17
|
+
*
|
|
18
|
+
* Subclasses MUST implement:
|
|
19
|
+
* - name (property)
|
|
20
|
+
* - prompt()
|
|
21
|
+
* - promptStream()
|
|
22
|
+
* - dispose()
|
|
23
|
+
*
|
|
24
|
+
* Subclasses MAY override:
|
|
25
|
+
* - promptWithModel() — defaults to prompt() ignoring model
|
|
26
|
+
* - restart() — defaults to no-op
|
|
27
|
+
* - compact() — defaults to null (not supported)
|
|
28
|
+
* - compactWithModel() — defaults to compact() ignoring model
|
|
29
|
+
* - abort() — defaults to no-op
|
|
30
|
+
* - getInfo() — defaults to empty object
|
|
31
|
+
*/
|
|
32
|
+
export abstract class BaseAdapter implements AgentAdapter {
|
|
33
|
+
/** Unique agent type identifier, e.g. "pi", "kiro" */
|
|
34
|
+
abstract readonly name: string;
|
|
35
|
+
|
|
36
|
+
// ── Required: every adapter must implement these ─────
|
|
37
|
+
|
|
38
|
+
/** Send a user message and return the full assistant response. */
|
|
39
|
+
abstract prompt(threadId: string, message: AgentMessage): Promise<AgentResponse>;
|
|
40
|
+
|
|
41
|
+
/** Send a user message and stream back events in real time. */
|
|
42
|
+
abstract promptStream(threadId: string, message: AgentMessage): AsyncIterable<AgentStreamEvent>;
|
|
43
|
+
|
|
44
|
+
/** Tear down all sessions and release resources. */
|
|
45
|
+
abstract dispose(): Promise<void>;
|
|
46
|
+
|
|
47
|
+
// ── Optional: override for adapter-specific behavior ─
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Send a prompt using a specific model (e.g. Haiku for memory flush).
|
|
51
|
+
* Default: ignores modelId, delegates to prompt().
|
|
52
|
+
*/
|
|
53
|
+
async promptWithModel(threadId: string, message: AgentMessage, _modelId: string): Promise<AgentResponse> {
|
|
54
|
+
return this.prompt(threadId, message);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Dispose the session for a thread and start fresh on next prompt.
|
|
59
|
+
* Default: no-op.
|
|
60
|
+
*/
|
|
61
|
+
async restart(_threadId: string): Promise<void> {}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Compact the session context for a thread.
|
|
65
|
+
* Default: returns null (not supported).
|
|
66
|
+
*/
|
|
67
|
+
async compact(_threadId: string): Promise<{ tokensBefore: number; tokensAfter: number | null } | null> {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Compact with a specific model.
|
|
73
|
+
* Default: ignores modelId, delegates to compact().
|
|
74
|
+
*/
|
|
75
|
+
async compactWithModel(threadId: string, _modelId: string): Promise<{ tokensBefore: number; tokensAfter: number | null } | null> {
|
|
76
|
+
return this.compact(threadId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Abort the current agent run for a thread.
|
|
81
|
+
* Default: no-op.
|
|
82
|
+
*/
|
|
83
|
+
async abort(_threadId: string): Promise<void> {}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Return runtime info about the agent (model, version, context usage, etc.).
|
|
87
|
+
* Default: returns empty object.
|
|
88
|
+
*/
|
|
89
|
+
getInfo(_threadId?: string): AdapterInfo {
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agents/index.ts — Public API for the agents subsystem.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { BaseAdapter } from "./base-adapter.js";
|
|
6
|
+
export { getAgentDefinition, getAgentFactory, listAvailableAgentTypes, isKnownAgentType, getAgentSdkPackage } from "./registry.js";
|
|
7
|
+
export type { AgentDefinition, AgentPackageRequirement, AgentSetupContext } from "./registry.js";
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* acp/client.ts — JSON-RPC stdio transport for kiro-cli ACP
|
|
3
|
+
*
|
|
4
|
+
* Handles request/response correlation and notification dispatch.
|
|
5
|
+
* Does NOT manage process lifecycle — that's acp/process.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EventEmitter } from "node:events";
|
|
9
|
+
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
export interface PendingRequest {
|
|
12
|
+
resolve: (value: unknown) => void;
|
|
13
|
+
reject: (error: unknown) => void;
|
|
14
|
+
timer: ReturnType<typeof setTimeout>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class AcpClient extends EventEmitter {
|
|
18
|
+
private buf = "";
|
|
19
|
+
private pending = new Map<number, PendingRequest>();
|
|
20
|
+
private nextId = 1;
|
|
21
|
+
private closed = false;
|
|
22
|
+
|
|
23
|
+
constructor(private proc: ChildProcessWithoutNullStreams, private requestTimeoutMs = 60_000) {
|
|
24
|
+
super();
|
|
25
|
+
this.proc.stdout.on("data", (chunk: Buffer) => this.onData(chunk));
|
|
26
|
+
this.proc.on("exit", (code) => {
|
|
27
|
+
this.closed = true;
|
|
28
|
+
this.rejectAll(new Error(`kiro-cli exited with code ${code}`));
|
|
29
|
+
this.emit("exit", code);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Send a JSON-RPC request and await its response. */
|
|
34
|
+
async call<T = unknown>(method: string, params?: unknown): Promise<T> {
|
|
35
|
+
if (this.closed) throw new Error("ACP client is closed");
|
|
36
|
+
|
|
37
|
+
const id = this.nextId++;
|
|
38
|
+
const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} });
|
|
39
|
+
this.proc.stdin.write(payload + "\n");
|
|
40
|
+
|
|
41
|
+
return new Promise<T>((resolve, reject) => {
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
this.pending.delete(id);
|
|
44
|
+
reject(new Error(`ACP call "${method}" timed out after ${this.requestTimeoutMs}ms`));
|
|
45
|
+
}, this.requestTimeoutMs);
|
|
46
|
+
|
|
47
|
+
this.pending.set(id, { resolve: resolve as (v: unknown) => void, reject, timer });
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Send a JSON-RPC notification (no response expected). */
|
|
52
|
+
notify(method: string, params?: unknown): void {
|
|
53
|
+
if (this.closed) return;
|
|
54
|
+
const payload = JSON.stringify({ jsonrpc: "2.0", method, params: params ?? {} });
|
|
55
|
+
this.proc.stdin.write(payload + "\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Gracefully close — reject pending requests. */
|
|
59
|
+
close(): void {
|
|
60
|
+
this.closed = true;
|
|
61
|
+
this.rejectAll(new Error("ACP client closed"));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get isClosed(): boolean {
|
|
65
|
+
return this.closed;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Private ──────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
private onData(chunk: Buffer): void {
|
|
71
|
+
this.buf += chunk.toString("utf8");
|
|
72
|
+
let idx: number;
|
|
73
|
+
while ((idx = this.buf.indexOf("\n")) >= 0) {
|
|
74
|
+
const line = this.buf.slice(0, idx).trim();
|
|
75
|
+
this.buf = this.buf.slice(idx + 1);
|
|
76
|
+
if (!line) continue;
|
|
77
|
+
this.parseLine(line);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private parseLine(line: string): void {
|
|
82
|
+
let msg: Record<string, unknown>;
|
|
83
|
+
try {
|
|
84
|
+
msg = JSON.parse(line);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
this.emit("parse_error", e, line);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Response to a pending request
|
|
91
|
+
if ("id" in msg && typeof msg.id === "number" && this.pending.has(msg.id)) {
|
|
92
|
+
const { resolve, reject, timer } = this.pending.get(msg.id)!;
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
this.pending.delete(msg.id);
|
|
95
|
+
if (msg.error) reject(msg.error);
|
|
96
|
+
else resolve(msg.result);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Notification from kiro-cli
|
|
101
|
+
if ("method" in msg && typeof msg.method === "string") {
|
|
102
|
+
this.emit(msg.method, msg.params);
|
|
103
|
+
this.emit("notification", msg.method, msg.params);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.emit("unknown_message", msg);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private rejectAll(error: Error): void {
|
|
111
|
+
for (const [id, { reject, timer }] of this.pending) {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
reject(error);
|
|
114
|
+
}
|
|
115
|
+
this.pending.clear();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kiro/acp/index.ts — Barrel export for ACP module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { AcpClient } from "./client.js";
|
|
6
|
+
export { spawnKiroCli, shutdownProcess, getKiroCliVersion } from "./process.js";
|
|
7
|
+
export type { AcpProcess, SpawnOptions } from "./process.js";
|
|
8
|
+
export type * from "./types.js";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* acp/process.ts — kiro-cli process lifecycle management
|
|
3
|
+
*
|
|
4
|
+
* Handles spawning, stderr capture, orphan guards, and graceful shutdown.
|
|
5
|
+
* One process per agent config (main or flush).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn, execFileSync, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
9
|
+
import { AcpClient } from "./client.js";
|
|
10
|
+
|
|
11
|
+
export interface SpawnOptions {
|
|
12
|
+
agentName: string;
|
|
13
|
+
cwd: string;
|
|
14
|
+
env?: Record<string, string>;
|
|
15
|
+
/** Max stderr buffer in bytes (default 1MB) */
|
|
16
|
+
maxStderrBytes?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AcpProcess {
|
|
20
|
+
client: AcpClient;
|
|
21
|
+
proc: ChildProcessWithoutNullStreams;
|
|
22
|
+
stderr: string[];
|
|
23
|
+
kill(signal?: NodeJS.Signals): void;
|
|
24
|
+
killGroup(): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Spawn kiro-cli in ACP mode and return the wrapped process + client.
|
|
29
|
+
* Throws if the binary is not found.
|
|
30
|
+
*/
|
|
31
|
+
export function spawnKiroCli(opts: SpawnOptions): AcpProcess {
|
|
32
|
+
const { agentName, cwd, env, maxStderrBytes = 1_048_576 } = opts;
|
|
33
|
+
|
|
34
|
+
const proc = spawn("kiro-cli", ["chat", "--agent", agentName, "--acp"], {
|
|
35
|
+
cwd,
|
|
36
|
+
env: { ...process.env, ...env },
|
|
37
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
38
|
+
detached: true, // own process group for clean kill
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Handle spawn failures (e.g. ENOENT if kiro-cli not on PATH)
|
|
42
|
+
proc.on("error", (err) => {
|
|
43
|
+
console.error(`[kiro] failed to spawn kiro-cli: ${err.message}`);
|
|
44
|
+
// Emit exit so AcpClient rejects pending requests immediately
|
|
45
|
+
proc.emit("exit", 1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Buffer stderr for diagnostics (capped)
|
|
49
|
+
const stderr: string[] = [];
|
|
50
|
+
let stderrBytes = 0;
|
|
51
|
+
proc.stderr.on("data", (chunk: Buffer) => {
|
|
52
|
+
const str = chunk.toString("utf8");
|
|
53
|
+
stderrBytes += chunk.length;
|
|
54
|
+
if (stderrBytes <= maxStderrBytes) {
|
|
55
|
+
stderr.push(str);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const client = new AcpClient(proc);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
client,
|
|
63
|
+
proc,
|
|
64
|
+
stderr,
|
|
65
|
+
kill(signal: NodeJS.Signals = "SIGTERM") {
|
|
66
|
+
try { proc.kill(signal); } catch {}
|
|
67
|
+
},
|
|
68
|
+
killGroup() {
|
|
69
|
+
if (proc.pid) {
|
|
70
|
+
try { process.kill(-proc.pid, "SIGKILL"); } catch {}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Gracefully shutdown a kiro-cli process:
|
|
78
|
+
* SIGTERM → wait up to `gracePeriodMs` → SIGKILL the process group.
|
|
79
|
+
*/
|
|
80
|
+
export async function shutdownProcess(acpProc: AcpProcess, gracePeriodMs = 5_000): Promise<void> {
|
|
81
|
+
acpProc.client.close();
|
|
82
|
+
acpProc.kill("SIGTERM");
|
|
83
|
+
|
|
84
|
+
await new Promise<void>((resolve) => {
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
acpProc.killGroup();
|
|
87
|
+
resolve();
|
|
88
|
+
}, gracePeriodMs);
|
|
89
|
+
|
|
90
|
+
acpProc.proc.on("exit", () => {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
resolve();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if kiro-cli is available on PATH.
|
|
99
|
+
* Returns the version string or null if not found.
|
|
100
|
+
*/
|
|
101
|
+
export function getKiroCliVersion(): string | null {
|
|
102
|
+
try {
|
|
103
|
+
const output = execFileSync("kiro-cli", ["--version"], {
|
|
104
|
+
encoding: "utf8",
|
|
105
|
+
timeout: 5_000,
|
|
106
|
+
}).trim();
|
|
107
|
+
return output || null;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* acp/types.ts — ACP (Agent Control Protocol) type definitions
|
|
3
|
+
*
|
|
4
|
+
* Discriminated union of events received from kiro-cli over JSON-RPC stdio.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ── Stop reasons ─────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export type StopReason = "end_turn" | "cancelled" | "max_turns" | "error";
|
|
10
|
+
|
|
11
|
+
// ── ACP Events (notifications from kiro-cli) ─────────
|
|
12
|
+
|
|
13
|
+
export interface AcpTextChunk {
|
|
14
|
+
type: "text_chunk";
|
|
15
|
+
text: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AcpThinkingChunk {
|
|
19
|
+
type: "thinking_chunk";
|
|
20
|
+
text: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AcpToolCall {
|
|
24
|
+
type: "tool_call";
|
|
25
|
+
tool_call_id: string;
|
|
26
|
+
title: string;
|
|
27
|
+
tool_name: string;
|
|
28
|
+
tool_input: Record<string, unknown>;
|
|
29
|
+
tool_kind?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AcpToolResult {
|
|
33
|
+
type: "tool_result";
|
|
34
|
+
tool_call_id: string;
|
|
35
|
+
output: string;
|
|
36
|
+
exit_code: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AcpPermissionRequest {
|
|
40
|
+
type: "permission_request";
|
|
41
|
+
tool_call_id: string;
|
|
42
|
+
tool_name: string;
|
|
43
|
+
tool_input: Record<string, unknown>;
|
|
44
|
+
title?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AcpComplete {
|
|
48
|
+
type: "complete";
|
|
49
|
+
stop_reason: StopReason;
|
|
50
|
+
error?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface AcpSessionUpdate {
|
|
54
|
+
type: "session/update";
|
|
55
|
+
context_tokens?: number;
|
|
56
|
+
context_window?: number;
|
|
57
|
+
model?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type AcpEvent =
|
|
61
|
+
| AcpTextChunk
|
|
62
|
+
| AcpThinkingChunk
|
|
63
|
+
| AcpToolCall
|
|
64
|
+
| AcpToolResult
|
|
65
|
+
| AcpPermissionRequest
|
|
66
|
+
| AcpComplete
|
|
67
|
+
| AcpSessionUpdate;
|
|
68
|
+
|
|
69
|
+
// ── ACP method results ───────────────────────────────
|
|
70
|
+
|
|
71
|
+
export interface InitializeResult {
|
|
72
|
+
protocolVersion: string;
|
|
73
|
+
capabilities: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SessionNewResult {
|
|
77
|
+
sessionId: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface SessionLoadResult {
|
|
81
|
+
sessionId: string;
|
|
82
|
+
restored: boolean;
|
|
83
|
+
}
|