@rivus/agent 0.3.1 → 0.4.1

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 CHANGED
@@ -14,7 +14,7 @@ The first milestone is intentionally narrow: make the agent loop and agent harne
14
14
  - Host-issued invocation authority and a call-time Tool Broker that checks exact grants, pinned versions, online revocation, stable operation IDs, durable side-effect fencing, and one-time approval receipts bound to canonical input digests. The Pi Tool Proxy and Feishu Human Interaction gateway are wired in the deployment example.
15
15
  - Multi-agent Host primitives with trusted Endpoint bindings, stable Runtime Generation/Instance identities, lazy per-instance Runtime pooling, isolated active-run gates, and restart restoration from persisted registry records.
16
16
  - Scoped Agent Memory with a single-writer append-only JSONL adapter, explicit Profile/Deployment grants, Host-bound tenant/subject/conversation/audience identity, and a provider-neutral `rivus_memory` Tool wired into the Pi manifest Runtime. Agent-private proposals survive process replacement; group invocations fail closed to their current Conversation scope, and Shared Profile reads expose only confirmed records. Automatic Recall, durable confirmation UX, typed Shared Profile fields, full provenance/retention metadata, and durable Compaction are still separate work.
17
- - Manifest-driven daily Agent Automations with stable Mandate/Tick identity, per-template Tool grant narrowing, isolated Runtime and invocation authority, a durable single-writer JSON Tick adapter that records the actual Agent Run, generated-before-delivery recovery, and idempotent proactive Feishu interactive-card delivery. The first Markdown heading becomes the card title while the remaining Markdown stays scannable in the card body. Revisioned Plugin State and the general Delivery Outbox remain composable domain primitives.
17
+ - Manifest-driven fixed-time Agent Automations with daily or weekday-only schedules, stable Mandate/Tick identity, per-template Tool grant narrowing, isolated Runtime and invocation authority, a durable single-writer JSON Tick adapter that records the actual Agent Run, generated-before-delivery recovery, and idempotent proactive Feishu interactive-card delivery. The first Markdown heading becomes the card title while the remaining Markdown stays scannable in the card body. Revisioned Plugin State and the general Delivery Outbox remain composable domain primitives.
18
18
  - Peer Delegation and Subagent coordination primitives with explicit edges, hop/budget limits, monotonic ToolGrantSet intersection, isolated child authority, denied ambient sender/private-memory/Shell capabilities, and parent cancellation cascading.
19
19
  - `@rivus/agent/testing` Conformance TestKit with a reusable Fake Plugin and checks for registration/reference failures, ToolGrantSet expansion, and lifecycle disposal leaks.
20
20
  - Effect-based `AgentHarness` use case with result-oriented `prompt()`, turn-oriented `promptTurn()`, event-oriented `stream()`, text-oriented `streamText()`, event-handling `promptWithEvents()`, active-run/run/session state and transcript lookup APIs, and a default runtime factory.
@@ -160,7 +160,7 @@ Endpoint factories receive a Host-bound `handle()` function. Inbound payload fie
160
160
 
161
161
  An Endpoint may opt into Feishu's native process message with `"experimental": { "cotMessages": true }`. The deployment then requires a local `RIVUS_FEISHU_COT_BASE_URL`; no environment-specific origin is committed or inferred. Rivus creates the process message before the CardKit target so Feishu displays the process above the authoritative final answer, then renders redacted reasoning, model/Skill steps, and Tool calls into AG-UI COT events. Tool arguments remain valid redacted JSON as required by AG-UI, while the Feishu presenter uses narrow mappings for the deployment's known Tools, a sanitized bounded fallback for unknown Tools, and labeled result lines such as `查询天气 · 北京`. COT failures are reported and degrade to CardKit instead of failing the Agent Run; if CardKit preparation fails after COT creation, Rivus explicitly closes that process as failed.
162
162
 
163
- The example also declares a disabled `daily-ielts-word` Automation. Enable it only in a gitignored local manifest and set its `targetRef` variable to a Feishu receive ID. The current scheduler accepts the fixed-daily cron subset `M H * * *` plus an IANA time zone. Each occurrence uses an Automation-only Agent Instance and Session, persists generated text before delivery, and sends with a stable Feishu UUID so restart or delivery retry does not create another model answer. It is a local single-writer scheduler, not a distributed cron service.
163
+ The example declares disabled `daily-ielts-word`, `a-share-pre-market`, and `a-share-post-market` Automations. Enable them only in a gitignored local manifest and set each `targetRef` variable to a Feishu receive ID. The scheduler accepts `M H * * *` and the weekday-only subset `M H * * 1-5` plus an IANA time zone. The A-share templates run at 09:00 and 16:00 on weekdays, request only the bounded evidence-backed briefing Tool, and relay its source-attributed facts and explicitly labeled inferences without letting the model invent market numbers or causal claims; see the [A-share briefing runbook](docs/en/operations/a-share-market-briefing.md). Each occurrence uses an Automation-only Agent Instance and Session, persists generated text before delivery, and sends with a stable Feishu UUID so restart or delivery retry does not create another model answer. An endpoint-less worker Agent may deliver through another enabled Agent's Host-owned Endpoint, allowing one visible Feishu bot to present multiple isolated task Profiles without inheriting their Tool grants or state. It is a local single-writer scheduler, not a distributed cron service.
164
164
 
165
165
  ```bash
166
166
  rivus --env-file .env.local \
@@ -1059,7 +1059,7 @@ const bootstrap = await Effect.runPromise(
1059
1059
  );
1060
1060
  ```
1061
1061
 
1062
- The configured preparation helper creates a blue streaming CardKit entity, replies to the inbound Feishu message, and binds the run to the card target before any stream action is published. The running card includes a “Stop generating” button carrying `{ rivus_action: "cancel_run", run_id, session_key }`; completed, failed, and cancelled cards remove it. The configured publisher coalesces consecutive `update_text` actions by run, flushes the latest text before a terminal action, and exposes `flush(runId?)` for explicit worker-driven flushing. `createConfiguredRivusDaemonBootstrap` adds the event log, history restore helper, status reporter, optional status transport, WebSocket transport, worker loop, and managed process around those same pieces; `restoreConfiguredRivusDaemonBootstrap` first replays that event log as restored harness events so read models and same-session loop input are hydrated after restart. Local tests can use `createInMemoryFeishuCardTargetRegistry`; daemon processes should prefer `createJsonFileFeishuCardTargetRegistry` so run-to-card bindings survive restarts and malformed registry files fail loudly instead of being treated as empty state.
1062
+ The configured preparation helper creates a blue streaming CardKit entity, replies to the inbound Feishu message, and binds the run to the card target before any stream action is published. The running card includes a “Stop generating” button carrying `{ rivus_action: "cancel_run", run_id, session_key }`; completed, failed, and cancelled cards remove it. The configured publisher coalesces consecutive `update_text` actions by run, flushes the latest text before a terminal action, and exposes `flush(runId?)` for explicit worker-driven flushing. `createConfiguredRivusDaemonBootstrap` flushes pending text at `config.feishu.streamMinIntervalMs` by default; pass `flushIntervalMs` only to override that cadence. It also adds the event log, history restore helper, status reporter, optional status transport, WebSocket transport, worker loop, and managed process around those same pieces; `restoreConfiguredRivusDaemonBootstrap` first replays that event log as restored harness events so read models and same-session loop input are hydrated after restart. Local tests can use `createInMemoryFeishuCardTargetRegistry`; daemon processes should prefer `createJsonFileFeishuCardTargetRegistry` so run-to-card bindings survive restarts and malformed registry files fail loudly instead of being treated as empty state.
1063
1063
 
1064
1064
  ## Design Notes
1065
1065
 
package/dist/acp.d.ts ADDED
@@ -0,0 +1,98 @@
1
+ import { s as AgentLoopInput, t as AgentLoop } from "./agent-loop.js";
2
+ import * as acp from "@agentclientprotocol/sdk";
3
+
4
+ //#region src/infrastructure/acp/acp-agent-loop.d.ts
5
+ type AcpToolCallStatus = "pending" | "in_progress" | "completed" | "failed";
6
+ interface AcpSessionUpdate {
7
+ readonly content?: unknown;
8
+ readonly name?: string | null;
9
+ readonly rawInput?: unknown;
10
+ readonly rawOutput?: unknown;
11
+ readonly sessionUpdate: string;
12
+ readonly status?: AcpToolCallStatus | null;
13
+ readonly title?: string | null;
14
+ readonly toolCallId?: string;
15
+ readonly [key: string]: unknown;
16
+ }
17
+ interface AcpPromptResult {
18
+ readonly stopReason: string;
19
+ }
20
+ interface AcpAgentSession {
21
+ cancel(): Promise<void> | void;
22
+ prompt(text: string, onUpdate: (update: AcpSessionUpdate) => void): Promise<AcpPromptResult>;
23
+ dispose?(): Promise<void> | void;
24
+ }
25
+ interface AcpAgentLoopOptions {
26
+ readonly disposeSessionAfterRun?: boolean;
27
+ readonly resolveSession: (input: AgentLoopInput) => Promise<AcpAgentSession> | AcpAgentSession;
28
+ }
29
+ declare function createAcpAgentLoop(options: AcpAgentLoopOptions): AgentLoop;
30
+ //#endregion
31
+ //#region src/infrastructure/acp/acp-permission-policy.d.ts
32
+ interface AcpPermissionOption {
33
+ readonly kind: "allow_once" | "allow_always" | "reject_once" | "reject_always";
34
+ readonly name: string;
35
+ readonly optionId: string;
36
+ }
37
+ interface AcpPermissionToolCall {
38
+ readonly toolCallId: string;
39
+ readonly title?: string;
40
+ readonly rawInput?: unknown;
41
+ }
42
+ interface AcpPermissionRequest {
43
+ readonly options: ReadonlyArray<AcpPermissionOption>;
44
+ readonly sessionId: string;
45
+ readonly toolCall: AcpPermissionToolCall;
46
+ }
47
+ interface AcpPermissionSelection {
48
+ readonly optionId: string;
49
+ }
50
+ type AcpPermissionDecision = {
51
+ readonly outcome: "cancelled";
52
+ } | {
53
+ readonly optionId: string;
54
+ readonly outcome: "selected";
55
+ };
56
+ type AcpPermissionPolicy = (request: AcpPermissionRequest) => Promise<AcpPermissionSelection | undefined> | AcpPermissionSelection | undefined;
57
+ interface AcpSessionPermissionContext {
58
+ readonly sessionKey: string;
59
+ }
60
+ type AcpSessionPermissionPolicy = (request: AcpPermissionRequest, context: AcpSessionPermissionContext) => Promise<AcpPermissionSelection | undefined> | AcpPermissionSelection | undefined;
61
+ declare function decideAcpPermission(request: AcpPermissionRequest, policy?: AcpPermissionPolicy): Promise<AcpPermissionDecision>;
62
+ //#endregion
63
+ //#region src/infrastructure/acp/acp-permission-bridge.d.ts
64
+ interface AcpPermissionBridge {
65
+ readonly policy: AcpSessionPermissionPolicy;
66
+ bind(sessionKey: string, policy: AcpPermissionPolicy): () => void;
67
+ }
68
+ declare function createAcpPermissionBridge(): AcpPermissionBridge;
69
+ //#endregion
70
+ //#region src/infrastructure/acp/acp-agent-server.d.ts
71
+ interface AcpAgentServerOptions {
72
+ readonly agentName?: string;
73
+ readonly loop: AgentLoop;
74
+ readonly permissionBridge?: AcpPermissionBridge;
75
+ readonly workingDirectory: string;
76
+ }
77
+ declare function createAcpAgentServer(options: AcpAgentServerOptions): acp.AgentApp;
78
+ declare function serveAcpAgentOnStdio(app: acp.AgentApp): Promise<void>;
79
+ //#endregion
80
+ //#region src/infrastructure/acp/acp-stdio-agent-loop.d.ts
81
+ interface AcpStdioAgentLoopOptions {
82
+ readonly arguments?: ReadonlyArray<string>;
83
+ readonly clientName?: string;
84
+ readonly command: string;
85
+ readonly environment?: Readonly<Record<string, string>>;
86
+ readonly initializationTimeoutMs?: number;
87
+ readonly onStderr?: (text: string) => void;
88
+ readonly permissionPolicy?: AcpSessionPermissionPolicy;
89
+ readonly terminationTimeoutMs?: number;
90
+ readonly workingDirectory: string;
91
+ }
92
+ interface AcpStdioAgentLoopHandle {
93
+ readonly loop: AgentLoop;
94
+ dispose(): Promise<void>;
95
+ }
96
+ declare function createAcpStdioAgentLoop(options: AcpStdioAgentLoopOptions): AcpStdioAgentLoopHandle;
97
+ //#endregion
98
+ export { type AcpAgentLoopOptions, type AcpAgentServerOptions, type AcpAgentSession, type AcpPermissionBridge, type AcpPermissionDecision, type AcpPermissionOption, type AcpPermissionPolicy, type AcpPermissionRequest, type AcpPermissionSelection, type AcpPermissionToolCall, type AcpPromptResult, type AcpSessionPermissionContext, type AcpSessionPermissionPolicy, type AcpSessionUpdate, type AcpStdioAgentLoopHandle, type AcpStdioAgentLoopOptions, type AcpToolCallStatus, createAcpAgentLoop, createAcpAgentServer, createAcpPermissionBridge, createAcpStdioAgentLoop, decideAcpPermission, serveAcpAgentOnStdio };
package/dist/acp.js ADDED
@@ -0,0 +1,436 @@
1
+ import { c as createAgentLoopToolExecutionEnd, d as createAsyncIterableAgentLoop, l as createAgentLoopToolExecutionStart, o as createAgentLoopTextDelta, s as createAgentLoopThinkingDelta, u as createAgentLoopToolExecutionUpdate } from "./agent-loop.js";
2
+ import { Effect, Stream } from "effect";
3
+ import { randomUUID } from "node:crypto";
4
+ import { Readable, Writable } from "node:stream";
5
+ import * as acp from "@agentclientprotocol/sdk";
6
+ import { spawn } from "node:child_process";
7
+ //#region src/infrastructure/acp/acp-agent-loop.ts
8
+ function createAcpAgentLoop(options) {
9
+ return createAsyncIterableAgentLoop({ run: (input) => runAcpSession(input, options) });
10
+ }
11
+ async function* runAcpSession(input, options) {
12
+ const session = await options.resolveSession(input);
13
+ const updates = [];
14
+ const tools = /* @__PURE__ */ new Map();
15
+ let wake;
16
+ let completed = false;
17
+ let failure;
18
+ const onAbort = () => {
19
+ Promise.resolve(session.cancel()).catch((error) => {
20
+ failure = error;
21
+ completed = true;
22
+ wake?.();
23
+ wake = void 0;
24
+ });
25
+ };
26
+ input.abortSignal.addEventListener("abort", onAbort, { once: true });
27
+ const prompt = session.prompt(input.text, (update) => {
28
+ updates.push(...mapAcpSessionUpdate(update, tools));
29
+ wake?.();
30
+ wake = void 0;
31
+ }).catch((error) => {
32
+ if (!input.abortSignal.aborted) failure = error;
33
+ }).finally(() => {
34
+ completed = true;
35
+ wake?.();
36
+ wake = void 0;
37
+ });
38
+ try {
39
+ if (input.abortSignal.aborted) onAbort();
40
+ while (!completed || updates.length > 0) {
41
+ const event = updates.shift();
42
+ if (event) yield event;
43
+ else await new Promise((resolve) => {
44
+ wake = resolve;
45
+ });
46
+ }
47
+ if (failure !== void 0) throw failure;
48
+ await prompt;
49
+ } finally {
50
+ input.abortSignal.removeEventListener("abort", onAbort);
51
+ if (options.disposeSessionAfterRun !== false) await session.dispose?.();
52
+ }
53
+ }
54
+ function mapAcpSessionUpdate(update, tools) {
55
+ if (update.sessionUpdate === "agent_message_chunk") {
56
+ const text = readTextContent(update.content);
57
+ return text === void 0 ? [] : [createAgentLoopTextDelta(text)];
58
+ }
59
+ if (update.sessionUpdate === "agent_thought_chunk") {
60
+ const text = readTextContent(update.content);
61
+ return text === void 0 ? [] : [createAgentLoopThinkingDelta(text)];
62
+ }
63
+ if (update.sessionUpdate === "tool_call") {
64
+ if (!update.toolCallId || !update.title) return [];
65
+ const name = update.name?.trim() || update.title;
66
+ const state = {
67
+ input: update.rawInput,
68
+ name
69
+ };
70
+ tools.set(update.toolCallId, state);
71
+ const start = createAgentLoopToolExecutionStart({
72
+ input: state.input,
73
+ toolCallId: update.toolCallId,
74
+ toolName: state.name
75
+ });
76
+ if (update.status === "completed" || update.status === "failed") {
77
+ tools.delete(update.toolCallId);
78
+ return [start, createAgentLoopToolExecutionEnd({
79
+ isError: update.status === "failed",
80
+ result: update.rawOutput,
81
+ toolCallId: update.toolCallId,
82
+ toolName: state.name
83
+ })];
84
+ }
85
+ return [start];
86
+ }
87
+ if (update.sessionUpdate !== "tool_call_update") return [];
88
+ if (!update.toolCallId) return [];
89
+ const prior = tools.get(update.toolCallId);
90
+ const state = {
91
+ input: update.rawInput ?? prior?.input,
92
+ name: update.name?.trim() || update.title?.trim() || prior?.name || update.toolCallId
93
+ };
94
+ tools.set(update.toolCallId, state);
95
+ if (update.status === "completed" || update.status === "failed") {
96
+ tools.delete(update.toolCallId);
97
+ return [createAgentLoopToolExecutionEnd({
98
+ isError: update.status === "failed",
99
+ result: update.rawOutput,
100
+ toolCallId: update.toolCallId,
101
+ toolName: state.name
102
+ })];
103
+ }
104
+ return [createAgentLoopToolExecutionUpdate({
105
+ input: state.input,
106
+ partialResult: update.rawOutput,
107
+ toolCallId: update.toolCallId,
108
+ toolName: state.name
109
+ })];
110
+ }
111
+ function readTextContent(content) {
112
+ if (!isRecord(content) || content.type !== "text" || typeof content.text !== "string") return void 0;
113
+ return content.text;
114
+ }
115
+ function isRecord(value) {
116
+ return typeof value === "object" && value !== null;
117
+ }
118
+ //#endregion
119
+ //#region src/infrastructure/acp/acp-agent-server.ts
120
+ function createAcpAgentServer(options) {
121
+ const sessions = /* @__PURE__ */ new Set();
122
+ const active = /* @__PURE__ */ new Map();
123
+ return acp.agent({ name: options.agentName ?? "rivus" }).onRequest(acp.methods.agent.initialize, ({ params }) => ({
124
+ agentCapabilities: {},
125
+ agentInfo: {
126
+ name: options.agentName ?? "rivus",
127
+ version: "1"
128
+ },
129
+ protocolVersion: params.protocolVersion === acp.PROTOCOL_VERSION ? params.protocolVersion : acp.PROTOCOL_VERSION
130
+ })).onRequest(acp.methods.agent.session.new, () => {
131
+ const sessionId = randomUUID();
132
+ sessions.add(sessionId);
133
+ return { sessionId };
134
+ }).onRequest(acp.methods.agent.session.prompt, async ({ client, params }) => {
135
+ if (!sessions.has(params.sessionId)) throw new Error(`Unknown ACP session: ${params.sessionId}`);
136
+ if (active.has(params.sessionId)) throw new Error(`ACP session is already active: ${params.sessionId}`);
137
+ const controller = new AbortController();
138
+ active.set(params.sessionId, controller);
139
+ const sessionKey = `acp:${params.sessionId}`;
140
+ const releasePermissionBridge = options.permissionBridge?.bind(sessionKey, async (request) => {
141
+ const response = await client.request(acp.methods.client.session.requestPermission, {
142
+ options: request.options.map((option) => ({ ...option })),
143
+ sessionId: params.sessionId,
144
+ toolCall: {
145
+ ...request.toolCall.rawInput === void 0 ? {} : { rawInput: request.toolCall.rawInput },
146
+ ...request.toolCall.title === void 0 ? {} : { title: request.toolCall.title },
147
+ toolCallId: request.toolCall.toolCallId
148
+ }
149
+ });
150
+ return response.outcome.outcome === "selected" ? { optionId: response.outcome.optionId } : void 0;
151
+ });
152
+ try {
153
+ await Effect.runPromise(options.loop.run({
154
+ abortSignal: controller.signal,
155
+ runId: randomUUID(),
156
+ sessionKey,
157
+ text: readPromptText(params.prompt)
158
+ }).pipe(Stream.runForEach((event) => Effect.tryPromise({
159
+ catch: (cause) => cause,
160
+ try: async () => {
161
+ const update = mapAgentLoopEvent(event);
162
+ if (update) await client.notify(acp.methods.client.session.update, {
163
+ sessionId: params.sessionId,
164
+ update
165
+ });
166
+ }
167
+ }))));
168
+ return { stopReason: controller.signal.aborted ? "cancelled" : "end_turn" };
169
+ } finally {
170
+ releasePermissionBridge?.();
171
+ active.delete(params.sessionId);
172
+ }
173
+ }).onNotification(acp.methods.agent.session.cancel, ({ params }) => {
174
+ active.get(params.sessionId)?.abort();
175
+ });
176
+ }
177
+ async function serveAcpAgentOnStdio(app) {
178
+ const stream = acp.ndJsonStream(Writable.toWeb(process.stdout), Readable.toWeb(process.stdin));
179
+ await app.connect(stream).closed;
180
+ }
181
+ function readPromptText(prompt) {
182
+ const parts = prompt.flatMap((content) => content.type === "text" ? [content.text] : []);
183
+ if (parts.length !== prompt.length) throw new Error("Rivus ACP facade currently accepts text prompt content only");
184
+ return parts.join("\n");
185
+ }
186
+ function mapAgentLoopEvent(event) {
187
+ if (event.type === "assistant_text_delta") return {
188
+ content: {
189
+ text: event.delta,
190
+ type: "text"
191
+ },
192
+ sessionUpdate: "agent_message_chunk"
193
+ };
194
+ if (event.type === "assistant_thinking_delta") return {
195
+ content: {
196
+ text: event.delta,
197
+ type: "text"
198
+ },
199
+ sessionUpdate: "agent_thought_chunk"
200
+ };
201
+ if (event.type === "tool_execution_start") return {
202
+ name: event.toolName,
203
+ rawInput: event.input,
204
+ sessionUpdate: "tool_call",
205
+ status: "in_progress",
206
+ title: event.toolName,
207
+ toolCallId: event.toolCallId
208
+ };
209
+ if (event.type === "tool_execution_update") return {
210
+ name: event.toolName,
211
+ rawInput: event.input,
212
+ rawOutput: event.partialResult,
213
+ sessionUpdate: "tool_call_update",
214
+ status: "in_progress",
215
+ title: event.toolName,
216
+ toolCallId: event.toolCallId
217
+ };
218
+ if (event.type === "tool_execution_end") return {
219
+ name: event.toolName,
220
+ rawOutput: event.result,
221
+ sessionUpdate: "tool_call_update",
222
+ status: event.isError ? "failed" : "completed",
223
+ title: event.toolName,
224
+ toolCallId: event.toolCallId
225
+ };
226
+ }
227
+ //#endregion
228
+ //#region src/infrastructure/acp/acp-permission-bridge.ts
229
+ function createAcpPermissionBridge() {
230
+ const policies = /* @__PURE__ */ new Map();
231
+ return {
232
+ bind: (sessionKey, policy) => {
233
+ if (policies.has(sessionKey)) throw new Error(`ACP permission bridge is already bound: ${sessionKey}`);
234
+ policies.set(sessionKey, policy);
235
+ return () => {
236
+ if (policies.get(sessionKey) === policy) policies.delete(sessionKey);
237
+ };
238
+ },
239
+ policy: async (request, context) => policies.get(context.sessionKey)?.(request)
240
+ };
241
+ }
242
+ //#endregion
243
+ //#region src/infrastructure/acp/acp-permission-policy.ts
244
+ async function decideAcpPermission(request, policy) {
245
+ const selection = await policy?.(request);
246
+ if (!selection || !request.options.some((option) => option.optionId === selection.optionId)) return { outcome: "cancelled" };
247
+ return {
248
+ optionId: selection.optionId,
249
+ outcome: "selected"
250
+ };
251
+ }
252
+ //#endregion
253
+ //#region src/infrastructure/acp/acp-stdio-agent-loop.ts
254
+ function createAcpStdioAgentLoop(options) {
255
+ let pendingConnection;
256
+ let activeConnection;
257
+ let ownedChild;
258
+ const sessionKeys = /* @__PURE__ */ new Map();
259
+ const resolveConnection = () => {
260
+ if (pendingConnection) return pendingConnection;
261
+ let current;
262
+ current = openConnection(options, sessionKeys, (child) => {
263
+ ownedChild = child;
264
+ });
265
+ pendingConnection = current;
266
+ current.then((processConnection) => {
267
+ if (pendingConnection !== current) {
268
+ processConnection.connection.close();
269
+ terminateChild(processConnection.child, options.terminationTimeoutMs);
270
+ return;
271
+ }
272
+ activeConnection = processConnection;
273
+ processConnection.connection.closed.finally(() => {
274
+ if (pendingConnection !== current) return;
275
+ pendingConnection = void 0;
276
+ activeConnection = void 0;
277
+ if (ownedChild === processConnection.child) ownedChild = void 0;
278
+ sessionKeys.clear();
279
+ for (const session of processConnection.sessions.values()) session.dispose();
280
+ processConnection.sessions.clear();
281
+ terminateChild(processConnection.child, options.terminationTimeoutMs);
282
+ });
283
+ }, () => {
284
+ if (pendingConnection === current) {
285
+ pendingConnection = void 0;
286
+ activeConnection = void 0;
287
+ ownedChild = void 0;
288
+ sessionKeys.clear();
289
+ }
290
+ });
291
+ return current;
292
+ };
293
+ return {
294
+ dispose: async () => {
295
+ const current = pendingConnection;
296
+ pendingConnection = void 0;
297
+ const processConnection = activeConnection;
298
+ activeConnection = void 0;
299
+ if (processConnection) {
300
+ for (const session of processConnection.sessions.values()) session.dispose();
301
+ processConnection.sessions.clear();
302
+ processConnection.connection.close();
303
+ }
304
+ sessionKeys.clear();
305
+ const child = ownedChild;
306
+ ownedChild = void 0;
307
+ if (child) await terminateChild(child, options.terminationTimeoutMs);
308
+ await current?.catch(() => void 0);
309
+ },
310
+ loop: createAcpAgentLoop({
311
+ disposeSessionAfterRun: false,
312
+ resolveSession: async (input) => {
313
+ const processConnection = await resolveConnection();
314
+ const current = processConnection.sessions.get(input.sessionKey);
315
+ if (current) return current;
316
+ const session = new SdkAcpAgentSession(await processConnection.connection.agent.buildSession(options.workingDirectory).start(), processConnection.connection.agent);
317
+ sessionKeys.set(session.sessionId, input.sessionKey);
318
+ processConnection.sessions.set(input.sessionKey, session);
319
+ return session;
320
+ }
321
+ })
322
+ };
323
+ }
324
+ var SdkAcpAgentSession = class {
325
+ session;
326
+ agent;
327
+ constructor(session, agent) {
328
+ this.session = session;
329
+ this.agent = agent;
330
+ }
331
+ get sessionId() {
332
+ return this.session.sessionId;
333
+ }
334
+ cancel() {
335
+ return this.agent.notify(acp.methods.agent.session.cancel, { sessionId: this.session.sessionId });
336
+ }
337
+ dispose() {
338
+ this.session.dispose();
339
+ }
340
+ async prompt(text, onUpdate) {
341
+ const failure = this.session.prompt(text).then(() => new Promise(() => void 0), (error) => Promise.reject(error));
342
+ for (;;) {
343
+ const message = await Promise.race([this.session.nextUpdate(), failure]);
344
+ if (message.kind === "stop") return { stopReason: message.stopReason };
345
+ onUpdate(message.update);
346
+ }
347
+ }
348
+ };
349
+ async function openConnection(options, sessionKeys, onSpawn) {
350
+ const child = spawn(options.command, [...options.arguments ?? []], {
351
+ cwd: options.workingDirectory,
352
+ env: { ...options.environment },
353
+ stdio: [
354
+ "pipe",
355
+ "pipe",
356
+ "pipe"
357
+ ]
358
+ });
359
+ onSpawn(child);
360
+ child.stderr.setEncoding("utf8");
361
+ if (options.onStderr) child.stderr.on("data", options.onStderr);
362
+ else child.stderr.resume();
363
+ const app = acp.client({ name: options.clientName ?? "rivus" }).onRequest(acp.methods.client.session.requestPermission, async ({ params }) => {
364
+ const sessionKey = sessionKeys.get(params.sessionId);
365
+ return { outcome: await decideAcpPermission(toPermissionRequest(params), sessionKey && options.permissionPolicy ? (request) => options.permissionPolicy?.(request, { sessionKey }) : void 0) };
366
+ });
367
+ const stream = acp.ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
368
+ const connection = app.connect(stream);
369
+ try {
370
+ const initialized = await withTimeout(connection.agent.request(acp.methods.agent.initialize, {
371
+ clientCapabilities: {},
372
+ clientInfo: {
373
+ name: options.clientName ?? "rivus",
374
+ version: "1"
375
+ },
376
+ protocolVersion: acp.PROTOCOL_VERSION
377
+ }), options.initializationTimeoutMs ?? 1e4, "ACP initialization timed out");
378
+ if (initialized.protocolVersion !== acp.PROTOCOL_VERSION) throw new Error(`ACP protocol version ${initialized.protocolVersion} is not supported`);
379
+ return {
380
+ child,
381
+ connection,
382
+ sessions: /* @__PURE__ */ new Map()
383
+ };
384
+ } catch (error) {
385
+ connection.close(error);
386
+ await terminateChild(child, options.terminationTimeoutMs);
387
+ throw error;
388
+ }
389
+ }
390
+ function toPermissionRequest(request) {
391
+ return {
392
+ options: request.options,
393
+ sessionId: request.sessionId,
394
+ toolCall: {
395
+ ...request.toolCall.rawInput === void 0 ? {} : { rawInput: request.toolCall.rawInput },
396
+ ...request.toolCall.title == null ? {} : { title: request.toolCall.title },
397
+ toolCallId: request.toolCall.toolCallId
398
+ }
399
+ };
400
+ }
401
+ async function terminateChild(child, timeoutMs = 2e3) {
402
+ if (child.exitCode !== null || child.signalCode !== null) return;
403
+ child.kill("SIGTERM");
404
+ if (await waitForChildExit(child, timeoutMs)) return;
405
+ child.kill("SIGKILL");
406
+ if (!await waitForChildExit(child, timeoutMs)) throw new Error(`ACP child did not exit after SIGKILL within ${timeoutMs}ms`);
407
+ }
408
+ function waitForChildExit(child, timeoutMs) {
409
+ if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve(true);
410
+ return new Promise((resolve) => {
411
+ const onExit = () => {
412
+ clearTimeout(timeout);
413
+ resolve(true);
414
+ };
415
+ const timeout = setTimeout(() => {
416
+ child.removeListener("exit", onExit);
417
+ resolve(false);
418
+ }, timeoutMs);
419
+ timeout.unref();
420
+ child.once("exit", onExit);
421
+ });
422
+ }
423
+ function withTimeout(promise, timeoutMs, message) {
424
+ return new Promise((resolve, reject) => {
425
+ const timeout = setTimeout(() => reject(/* @__PURE__ */ new Error(`${message} after ${timeoutMs}ms`)), timeoutMs);
426
+ promise.then((value) => {
427
+ clearTimeout(timeout);
428
+ resolve(value);
429
+ }, (error) => {
430
+ clearTimeout(timeout);
431
+ reject(error);
432
+ });
433
+ });
434
+ }
435
+ //#endregion
436
+ export { createAcpAgentLoop, createAcpAgentServer, createAcpPermissionBridge, createAcpStdioAgentLoop, decideAcpPermission, serveAcpAgentOnStdio };