@rivus/agent 0.3.1 → 0.4.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/README.md +2 -2
- package/dist/acp.d.ts +98 -0
- package/dist/acp.js +436 -0
- package/dist/agent-loop.d.ts +420 -0
- package/dist/agent-loop.js +118 -0
- package/dist/agent-memory.d.ts +98 -0
- package/dist/cli.js +6 -2
- package/dist/index.d.ts +4 -418
- package/dist/index.js +61 -147
- package/dist/rivus-daemon-cli.js +11 -7
- package/dist/rivus-plugin-testkit.d.ts +3 -98
- package/examples/a-share-briefing-analysis.mjs +93 -0
- package/examples/a-share-briefing-renderer.mjs +257 -0
- package/examples/a-share-index-evidence.mjs +99 -0
- package/examples/a-share-market-briefing.mjs +83 -0
- package/examples/a-share-market-date.mjs +10 -0
- package/examples/a-share-overseas-evidence.mjs +86 -0
- package/examples/a-share-policy-evidence.mjs +145 -0
- package/examples/a-share-provider-response.mjs +21 -0
- package/examples/a-share-sector-evidence.mjs +70 -0
- package/examples/acp-stdio-proxy.mjs +55 -0
- package/examples/current-weather.mjs +6 -32
- package/examples/https-response-reader.mjs +36 -0
- package/examples/rivus-agents.plugin.mjs +55 -3
- package/examples/rivus.config.json +30 -1
- package/package.json +11 -2
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
|
|
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
|
|
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 \
|
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 };
|