@rivus/agent 0.9.0 → 0.10.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/dist/acp.d.ts CHANGED
@@ -77,6 +77,23 @@ interface AcpAgentServerOptions {
77
77
  declare function createAcpAgentServer(options: AcpAgentServerOptions): acp.AgentApp;
78
78
  declare function serveAcpAgentOnStdio(app: acp.AgentApp): Promise<void>;
79
79
  //#endregion
80
+ //#region src/infrastructure/acp/acp-session-store.d.ts
81
+ interface AcpSessionRecord {
82
+ readonly sessionId: string;
83
+ }
84
+ interface AcpSessionStore {
85
+ load(sessionKey: string): Promise<AcpSessionRecord | undefined>;
86
+ save(sessionKey: string, record: AcpSessionRecord): Promise<void>;
87
+ }
88
+ interface JsonAcpSessionStoreOptions {
89
+ readonly filePath: string;
90
+ }
91
+ /**
92
+ * A small deployment-owned store for ACP provider session identities.
93
+ * The file contains no prompts or credentials, only session-key bindings.
94
+ */
95
+ declare function createJsonAcpSessionStore(options: JsonAcpSessionStoreOptions): AcpSessionStore;
96
+ //#endregion
80
97
  //#region src/infrastructure/acp/acp-stdio-agent-loop.d.ts
81
98
  interface AcpMcpServerEnvironmentVariable {
82
99
  readonly name: string;
@@ -97,6 +114,8 @@ interface AcpStdioAgentLoopOptions {
97
114
  readonly mcpServers?: (input: AgentLoopInput) => ReadonlyArray<AcpMcpServer>;
98
115
  readonly onStderr?: (text: string) => void;
99
116
  readonly permissionPolicy?: AcpSessionPermissionPolicy;
117
+ /** Persist provider session IDs so a restarted ACP child can load/resume them. */
118
+ readonly sessionStore?: AcpSessionStore;
100
119
  readonly terminationTimeoutMs?: number;
101
120
  readonly workingDirectory: string;
102
121
  }
@@ -105,5 +124,16 @@ interface AcpStdioAgentLoopHandle {
105
124
  dispose(): Promise<void>;
106
125
  }
107
126
  declare function createAcpStdioAgentLoop(options: AcpStdioAgentLoopOptions): AcpStdioAgentLoopHandle;
127
+ declare class AcpSessionResumeUnavailable extends Error {
128
+ readonly sessionId: string;
129
+ readonly name = "AcpSessionResumeUnavailable";
130
+ constructor(sessionId: string);
131
+ }
132
+ declare class AcpSessionResumeFailed extends Error {
133
+ readonly sessionId: string;
134
+ readonly cause: unknown;
135
+ readonly name = "AcpSessionResumeFailed";
136
+ constructor(sessionId: string, cause: unknown);
137
+ }
108
138
  //#endregion
109
- 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 };
139
+ 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 AcpSessionRecord, AcpSessionResumeFailed, AcpSessionResumeUnavailable, type AcpSessionStore, type AcpSessionUpdate, type AcpStdioAgentLoopHandle, type AcpStdioAgentLoopOptions, type AcpToolCallStatus, type JsonAcpSessionStoreOptions, createAcpAgentLoop, createAcpAgentServer, createAcpPermissionBridge, createAcpStdioAgentLoop, createJsonAcpSessionStore, decideAcpPermission, serveAcpAgentOnStdio };
package/dist/acp.js CHANGED
@@ -4,6 +4,8 @@ import { randomUUID } from "node:crypto";
4
4
  import { Readable, Writable } from "node:stream";
5
5
  import * as acp from "@agentclientprotocol/sdk";
6
6
  import { spawn } from "node:child_process";
7
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
8
+ import { dirname } from "node:path";
7
9
  //#region src/infrastructure/acp/acp-agent-loop.ts
8
10
  function createAcpAgentLoop(options) {
9
11
  return createAsyncIterableAgentLoop({ run: (input) => runAcpSession(input, options) });
@@ -109,10 +111,10 @@ function mapAcpSessionUpdate(update, tools) {
109
111
  })];
110
112
  }
111
113
  function readTextContent(content) {
112
- if (!isRecord(content) || content.type !== "text" || typeof content.text !== "string") return void 0;
114
+ if (!isRecord$1(content) || content.type !== "text" || typeof content.text !== "string") return void 0;
113
115
  return content.text;
114
116
  }
115
- function isRecord(value) {
117
+ function isRecord$1(value) {
116
118
  return typeof value === "object" && value !== null;
117
119
  }
118
120
  //#endregion
@@ -252,17 +254,48 @@ async function decideAcpPermission(request, policy) {
252
254
  //#endregion
253
255
  //#region src/infrastructure/acp/acp-stdio-agent-loop.ts
254
256
  async function buildAcpSession(processConnection, options, input) {
255
- const builder = processConnection.connection.agent.buildSession(options.workingDirectory);
256
- for (const server of options.mcpServers?.(input) ?? []) builder.withMcpServer({
257
+ const mcpServers = createMcpServers(options, input);
258
+ const persisted = await options.sessionStore?.load(input.sessionKey);
259
+ if (persisted) return restoreAcpSession(processConnection, options, persisted.sessionId, mcpServers);
260
+ const session = await processConnection.connection.agent.buildSession({
261
+ cwd: options.workingDirectory,
262
+ mcpServers
263
+ }).start();
264
+ await options.sessionStore?.save(input.sessionKey, { sessionId: session.sessionId });
265
+ return session;
266
+ }
267
+ function createMcpServers(options, input) {
268
+ return (options.mcpServers?.(input) ?? []).map((server) => ({
257
269
  args: [...server.args],
258
270
  command: server.command,
259
271
  env: server.env.map(({ name, value }) => ({
260
272
  name,
261
273
  value
262
274
  })),
263
- name: server.name
264
- });
265
- return builder.start();
275
+ name: server.name,
276
+ type: "stdio"
277
+ }));
278
+ }
279
+ async function restoreAcpSession(processConnection, options, sessionId, mcpServers) {
280
+ const session = new ResumedAcpSession(processConnection, sessionId);
281
+ try {
282
+ if (processConnection.agentCapabilities?.loadSession) await processConnection.connection.agent.request(acp.methods.agent.session.load, {
283
+ cwd: options.workingDirectory,
284
+ mcpServers,
285
+ sessionId
286
+ });
287
+ else if (processConnection.agentCapabilities?.sessionCapabilities?.resume) await processConnection.connection.agent.request(acp.methods.agent.session.resume, {
288
+ cwd: options.workingDirectory,
289
+ mcpServers,
290
+ sessionId
291
+ });
292
+ else throw new AcpSessionResumeUnavailable(sessionId);
293
+ session.clearReplay();
294
+ return session;
295
+ } catch (error) {
296
+ session.dispose();
297
+ throw new AcpSessionResumeFailed(sessionId, error);
298
+ }
266
299
  }
267
300
  function createAcpStdioAgentLoop(options) {
268
301
  let pendingConnection;
@@ -291,6 +324,7 @@ function createAcpStdioAgentLoop(options) {
291
324
  sessionKeys.clear();
292
325
  for (const session of processConnection.sessions.values()) session.dispose();
293
326
  processConnection.sessions.clear();
327
+ processConnection.sessionUpdateHandlers.clear();
294
328
  terminateChild(processConnection.child, options.terminationTimeoutMs);
295
329
  });
296
330
  }, () => {
@@ -312,6 +346,7 @@ function createAcpStdioAgentLoop(options) {
312
346
  if (processConnection) {
313
347
  for (const session of processConnection.sessions.values()) session.dispose();
314
348
  processConnection.sessions.clear();
349
+ processConnection.sessionUpdateHandlers.clear();
315
350
  processConnection.connection.close();
316
351
  }
317
352
  sessionKeys.clear();
@@ -330,7 +365,7 @@ function createAcpStdioAgentLoop(options) {
330
365
  current.dispose();
331
366
  processConnection.sessions.delete(input.sessionKey);
332
367
  }
333
- const session = new SdkAcpAgentSession(await buildAcpSession(processConnection, options, input), processConnection.connection.agent);
368
+ const session = new SdkAcpAgentSession(await buildAcpSession(processConnection, options, input), processConnection.connection.agent, options.sessionStore !== void 0);
334
369
  sessionKeys.set(session.sessionId, input.sessionKey);
335
370
  processConnection.sessions.set(input.sessionKey, session);
336
371
  return session;
@@ -341,10 +376,12 @@ function createAcpStdioAgentLoop(options) {
341
376
  var SdkAcpAgentSession = class {
342
377
  session;
343
378
  agent;
379
+ preserveOnCancel;
344
380
  reusable = true;
345
- constructor(session, agent) {
381
+ constructor(session, agent, preserveOnCancel) {
346
382
  this.session = session;
347
383
  this.agent = agent;
384
+ this.preserveOnCancel = preserveOnCancel;
348
385
  }
349
386
  get sessionId() {
350
387
  return this.session.sessionId;
@@ -353,7 +390,7 @@ var SdkAcpAgentSession = class {
353
390
  return this.reusable;
354
391
  }
355
392
  cancel() {
356
- this.reusable = false;
393
+ if (!this.preserveOnCancel) this.reusable = false;
357
394
  return this.agent.notify(acp.methods.agent.session.cancel, { sessionId: this.session.sessionId });
358
395
  }
359
396
  dispose() {
@@ -369,6 +406,88 @@ var SdkAcpAgentSession = class {
369
406
  }
370
407
  }
371
408
  };
409
+ var ResumedAcpSession = class {
410
+ processConnection;
411
+ sessionId;
412
+ updates = [];
413
+ waiters = [];
414
+ disposed = false;
415
+ failure;
416
+ constructor(processConnection, sessionId) {
417
+ this.processConnection = processConnection;
418
+ this.sessionId = sessionId;
419
+ processConnection.sessionUpdateHandlers.set(sessionId, (notification) => {
420
+ this.enqueue({
421
+ kind: "session_update",
422
+ update: notification.update
423
+ });
424
+ });
425
+ }
426
+ clearReplay() {
427
+ this.updates.splice(0);
428
+ }
429
+ dispose() {
430
+ if (this.disposed) return;
431
+ this.disposed = true;
432
+ this.processConnection.sessionUpdateHandlers.delete(this.sessionId);
433
+ const error = /* @__PURE__ */ new Error(`ACP session ${this.sessionId} observer disposed`);
434
+ for (const waiter of this.waiters.splice(0)) waiter.reject(error);
435
+ this.updates.splice(0);
436
+ }
437
+ nextUpdate() {
438
+ if (this.updates.length > 0) return Promise.resolve(this.updates.shift());
439
+ if (this.failure !== void 0) return Promise.reject(this.failure);
440
+ if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error(`ACP session ${this.sessionId} observer disposed`));
441
+ return new Promise((resolve, reject) => this.waiters.push({
442
+ reject,
443
+ resolve
444
+ }));
445
+ }
446
+ prompt(text) {
447
+ if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error(`ACP session ${this.sessionId} observer disposed`));
448
+ const response = this.processConnection.connection.agent.request(acp.methods.agent.session.prompt, {
449
+ prompt: [{
450
+ text,
451
+ type: "text"
452
+ }],
453
+ sessionId: this.sessionId
454
+ });
455
+ response.then((result) => this.enqueue({
456
+ kind: "stop",
457
+ stopReason: result.stopReason
458
+ }), (error) => this.fail(error));
459
+ return response;
460
+ }
461
+ enqueue(message) {
462
+ if (this.disposed) return;
463
+ const waiter = this.waiters.shift();
464
+ if (waiter) waiter.resolve(message);
465
+ else this.updates.push(message);
466
+ }
467
+ fail(error) {
468
+ if (this.failure !== void 0 || this.disposed) return;
469
+ this.failure = error;
470
+ for (const waiter of this.waiters.splice(0)) waiter.reject(error);
471
+ }
472
+ };
473
+ var AcpSessionResumeUnavailable = class extends Error {
474
+ sessionId;
475
+ name = "AcpSessionResumeUnavailable";
476
+ constructor(sessionId) {
477
+ super(`ACP Agent cannot load or resume persisted session ${sessionId}`);
478
+ this.sessionId = sessionId;
479
+ }
480
+ };
481
+ var AcpSessionResumeFailed = class extends Error {
482
+ sessionId;
483
+ cause;
484
+ name = "AcpSessionResumeFailed";
485
+ constructor(sessionId, cause) {
486
+ super(`ACP Agent failed to restore persisted session ${sessionId}`);
487
+ this.sessionId = sessionId;
488
+ this.cause = cause;
489
+ }
490
+ };
372
491
  async function openConnection(options, sessionKeys, onSpawn) {
373
492
  const child = spawn(options.command, [...options.arguments ?? []], {
374
493
  cwd: options.workingDirectory,
@@ -383,9 +502,12 @@ async function openConnection(options, sessionKeys, onSpawn) {
383
502
  child.stderr.setEncoding("utf8");
384
503
  if (options.onStderr) child.stderr.on("data", options.onStderr);
385
504
  else child.stderr.resume();
505
+ const sessionUpdateHandlers = /* @__PURE__ */ new Map();
386
506
  const app = acp.client({ name: options.clientName ?? "rivus" }).onRequest(acp.methods.client.session.requestPermission, async ({ params }) => {
387
507
  const sessionKey = sessionKeys.get(params.sessionId);
388
508
  return { outcome: await decideAcpPermission(toPermissionRequest(params), sessionKey && options.permissionPolicy ? (request) => options.permissionPolicy?.(request, { sessionKey }) : void 0) };
509
+ }).onNotification(acp.methods.client.session.update, ({ params }) => {
510
+ sessionUpdateHandlers.get(params.sessionId)?.(params);
389
511
  });
390
512
  const stream = acp.ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
391
513
  const connection = app.connect(stream);
@@ -400,8 +522,10 @@ async function openConnection(options, sessionKeys, onSpawn) {
400
522
  }), options.initializationTimeoutMs ?? 1e4, "ACP initialization timed out");
401
523
  if (initialized.protocolVersion !== acp.PROTOCOL_VERSION) throw new Error(`ACP protocol version ${initialized.protocolVersion} is not supported`);
402
524
  return {
525
+ ...initialized.agentCapabilities ? { agentCapabilities: initialized.agentCapabilities } : {},
403
526
  child,
404
527
  connection,
528
+ sessionUpdateHandlers,
405
529
  sessions: /* @__PURE__ */ new Map()
406
530
  };
407
531
  } catch (error) {
@@ -456,4 +580,57 @@ function withTimeout(promise, timeoutMs, message) {
456
580
  });
457
581
  }
458
582
  //#endregion
459
- export { createAcpAgentLoop, createAcpAgentServer, createAcpPermissionBridge, createAcpStdioAgentLoop, decideAcpPermission, serveAcpAgentOnStdio };
583
+ //#region src/infrastructure/acp/acp-session-store.ts
584
+ /**
585
+ * A small deployment-owned store for ACP provider session identities.
586
+ * The file contains no prompts or credentials, only session-key bindings.
587
+ */
588
+ function createJsonAcpSessionStore(options) {
589
+ let recordsPromise;
590
+ let writeChain = Promise.resolve();
591
+ const readRecords = async () => {
592
+ try {
593
+ const raw = await readFile(options.filePath, "utf8");
594
+ const parsed = JSON.parse(raw);
595
+ if (!isRecord(parsed)) throw new Error("ACP session store must contain an object");
596
+ const records = /* @__PURE__ */ new Map();
597
+ for (const [sessionKey, value] of Object.entries(parsed)) {
598
+ if (!isRecord(value) || typeof value.sessionId !== "string" || value.sessionId.trim() === "") throw new Error(`invalid ACP session record for ${sessionKey}`);
599
+ records.set(sessionKey, { sessionId: value.sessionId });
600
+ }
601
+ return records;
602
+ } catch (error) {
603
+ if (isMissingFile(error)) return /* @__PURE__ */ new Map();
604
+ throw error;
605
+ }
606
+ };
607
+ const records = async () => {
608
+ recordsPromise ??= readRecords();
609
+ return recordsPromise;
610
+ };
611
+ const persist = async (value) => {
612
+ const temporaryPath = `${options.filePath}.tmp-${process.pid}-${Date.now()}`;
613
+ await mkdir(dirname(options.filePath), { recursive: true });
614
+ await writeFile(temporaryPath, `${JSON.stringify(Object.fromEntries(value), null, 2)}\n`, "utf8");
615
+ await rename(temporaryPath, options.filePath);
616
+ };
617
+ return {
618
+ load: async (sessionKey) => (await records()).get(sessionKey),
619
+ save: async (sessionKey, record) => {
620
+ writeChain = writeChain.then(async () => {
621
+ const current = await records();
622
+ current.set(sessionKey, { sessionId: record.sessionId });
623
+ await persist(current);
624
+ });
625
+ await writeChain;
626
+ }
627
+ };
628
+ }
629
+ function isMissingFile(error) {
630
+ return isRecord(error) && error.code === "ENOENT";
631
+ }
632
+ function isRecord(value) {
633
+ return value !== null && typeof value === "object" && !Array.isArray(value);
634
+ }
635
+ //#endregion
636
+ export { AcpSessionResumeFailed, AcpSessionResumeUnavailable, createAcpAgentLoop, createAcpAgentServer, createAcpPermissionBridge, createAcpStdioAgentLoop, createJsonAcpSessionStore, decideAcpPermission, serveAcpAgentOnStdio };
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { D as loadRivusDeploymentManifest, T as resolveNodeRivusPluginModulePath, X as resolveFeishuEndpointCredentials, Z as loadMergedLocalEnvFile, et as validateRivusDeploymentManifest, t as runRivusDaemonCli } from "./rivus-daemon-cli.js";
3
3
  import { Effect } from "effect";
4
- import { fileURLToPath, pathToFileURL } from "node:url";
5
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
4
  import { lstat, mkdir, readFile, rmdir, stat, unlink, writeFile } from "node:fs/promises";
5
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
7
  //#region src/infrastructure/project/rivus-project-doctor.ts
8
8
  const REQUIRED_FILES = Object.freeze([
9
9
  "package.json",
package/dist/index.js CHANGED
@@ -8,8 +8,8 @@ import { a as readBackgroundSessionWaitInput, i as readBackgroundSessionString,
8
8
  import { n as assertRivusPluginConforms, r as createFakeRivusPlugin, t as RivusPluginConformanceError } from "./rivus-plugin-testkit.js";
9
9
  import { Cause, Deferred, Effect, Either, Exit, Fiber, Option, Stream } from "effect";
10
10
  import { createHash, randomUUID } from "node:crypto";
11
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
12
11
  import { appendFile, lstat, mkdir, readFile, readdir, realpath, rename, stat, truncate, unlink, writeFile } from "node:fs/promises";
12
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
13
13
  import { isDeepStrictEqual } from "node:util";
14
14
  import { createServer } from "node:http";
15
15
  import { Buffer as Buffer$1 } from "node:buffer";
package/dist/pi.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { d as requiresToolApproval } from "./agent-memory.js";
2
2
  import { l as createPiSkillRuntime, o as createInvocationAuthority, r as createToolInputDigest } from "./tool-input-digest.js";
3
3
  import { createHash } from "node:crypto";
4
- import { isAbsolute, relative } from "node:path";
5
4
  import { readFile, realpath, stat } from "node:fs/promises";
5
+ import { isAbsolute, relative } from "node:path";
6
6
  import { Unsafe } from "typebox";
7
7
  import { createReadToolDefinition } from "@earendil-works/pi-coding-agent";
8
8
  //#region src/infrastructure/pi/pi-project-skill-read-tool.ts
@@ -4,9 +4,9 @@ import { n as resolveRivusAgentDefinition, r as deepFreeze, t as createRivusPlug
4
4
  import { createRequire } from "node:module";
5
5
  import { Effect } from "effect";
6
6
  import { createHash, randomUUID } from "node:crypto";
7
- import { pathToFileURL } from "node:url";
8
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
9
7
  import { lstat, open, readFile, readdir, realpath, stat } from "node:fs/promises";
8
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
9
+ import { pathToFileURL } from "node:url";
10
10
  import { constants } from "node:fs";
11
11
  //#region src/application/plugin/rivus-automation-runtime-definition.ts
12
12
  function resolveRivusAutomationRuntimeDefinition(definition, requestedToolIds, requestedSkillIds) {
@@ -4,6 +4,7 @@ import {
4
4
  createAcpAgentServer,
5
5
  createAcpPermissionBridge,
6
6
  createAcpStdioAgentLoop,
7
+ createJsonAcpSessionStore,
7
8
  serveAcpAgentOnStdio
8
9
  } from "@rivus/agent/acp";
9
10
 
@@ -11,6 +12,7 @@ const command = process.env.RIVUS_ACP_SERVER_COMMAND?.trim();
11
12
  if (!command) throw new Error("RIVUS_ACP_SERVER_COMMAND is required and must resolve to an explicit executable");
12
13
 
13
14
  const workingDirectory = process.env.RIVUS_ACP_WORKING_DIRECTORY?.trim() || process.cwd();
15
+ const sessionStorePath = process.env.RIVUS_ACP_SESSION_STORE?.trim();
14
16
  const permissionBridge = createAcpPermissionBridge();
15
17
  const downstream = createAcpStdioAgentLoop({
16
18
  arguments: parseArguments(process.env.RIVUS_ACP_SERVER_ARGUMENTS),
@@ -18,6 +20,7 @@ const downstream = createAcpStdioAgentLoop({
18
20
  environment: selectEnvironment(process.env.RIVUS_ACP_SERVER_ENV_KEYS),
19
21
  onStderr: (text) => process.stderr.write(text),
20
22
  permissionPolicy: permissionBridge.policy,
23
+ ...(sessionStorePath ? { sessionStore: createJsonAcpSessionStore({ filePath: sessionStorePath }) } : {}),
21
24
  workingDirectory
22
25
  });
23
26
  const server = createAcpAgentServer({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivus/agent",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "A local agent daemon core built around a usable agent harness and domain events.",
5
5
  "type": "module",
6
6
  "license": "MIT",