@rho-workspace/rho 0.5.5 → 0.5.7
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/agent-connect.d.ts +42 -0
- package/dist/agent-connect.js +68 -0
- package/dist/answer-board.js +3 -3
- package/dist/bridge.js +6 -1
- package/dist/browser-access.d.ts +1 -0
- package/dist/browser-access.js +5 -3
- package/dist/capabilities.d.ts +21 -5
- package/dist/capabilities.js +6 -4
- package/dist/cli.js +113 -18
- package/dist/composition.d.ts +1 -1
- package/dist/composition.js +13 -3
- package/dist/connect-cli.d.ts +50 -0
- package/dist/connect-cli.js +128 -0
- package/dist/contract.generated.d.ts +1437 -381
- package/dist/contract.generated.js +1 -1
- package/dist/contracts/owners.generated.js +1160 -267
- package/dist/conversation-presentation.d.ts +9 -2
- package/dist/conversation-presentation.js +3 -1
- package/dist/conversations.d.ts +306 -7
- package/dist/conversations.js +208 -22
- package/dist/discovery.js +3 -3
- package/dist/extension-contract.d.ts +19 -0
- package/dist/extension-contract.js +15 -6
- package/dist/extension-markets.d.ts +74 -0
- package/dist/extension-markets.js +166 -0
- package/dist/extensions.d.ts +29 -1
- package/dist/extensions.js +13 -4
- package/dist/external-work.d.ts +22 -0
- package/dist/external-work.js +91 -0
- package/dist/plugin-cli.js +75 -3
- package/dist/plugin-inventory.d.ts +56 -0
- package/dist/plugin-inventory.js +41 -0
- package/dist/plugin-inventory.json +1581 -102
- package/dist/plugin-presentation.d.ts +105 -2
- package/dist/plugin-presentation.js +59 -5
- package/dist/plugins.d.ts +14 -3
- package/dist/plugins.js +12 -6
- package/dist/project-notes.js +1 -1
- package/dist/protocol.d.ts +7 -0
- package/dist/protocol.js +40 -15
- package/dist/replay.js +4 -0
- package/dist/response-contracts.js +26 -11
- package/dist/running.d.ts +10 -0
- package/dist/running.js +45 -0
- package/dist/server.d.ts +4 -10
- package/dist/server.js +140 -14
- package/dist/state.d.ts +8 -0
- package/dist/state.js +8 -0
- package/dist/studio-layout.js +3 -3
- package/dist/task-frame.d.ts +1 -1
- package/dist/timeline-events.d.ts +1 -0
- package/dist/timeline-events.js +8 -1
- package/dist/ui-context.d.ts +73 -0
- package/dist/ui-context.js +98 -0
- package/dist/ui-contract.d.ts +23 -0
- package/dist/web/.vite/manifest.json +135 -76
- package/dist/web/assets/answer-board-CeYS8k8y.js +1 -0
- package/dist/web/assets/choices-D8vGwsl2.js +1 -0
- package/dist/web/assets/{console-BSjLGe0T.js → console-Dyuo1q4N.js} +4 -4
- package/dist/web/assets/{dist-Tzoe_zm-.js → dist-CSiCyBml.js} +1 -1
- package/dist/web/assets/{dist-Cw8hRIlT.js → dist-D9FLb9Hm.js} +1 -1
- package/dist/web/assets/{dist-foH9IKc6.js → dist-FXbTBwML.js} +1 -1
- package/dist/web/assets/{dist-Buk0lwzI.js → dist-l3DUhbw5.js} +1 -1
- package/dist/web/assets/{explore-in-BVUy3ZW_.js → explore-in-BfSFGRh2.js} +1 -1
- package/dist/web/assets/graph-BU2_o5uA.js +2 -0
- package/dist/web/assets/index-C16mXVvk.css +1 -0
- package/dist/web/assets/index-CWg9XYXx.js +952 -0
- package/dist/web/assets/markdown.reader-Ct2-e2Zt.png +0 -0
- package/dist/web/assets/{notes-meaning-pVbbQUGP.js → notes-meaning-T2m5zQfv.js} +1 -1
- package/dist/web/assets/overview-PzfLJCo-.js +6 -0
- package/dist/web/assets/preview-BguEdsza.js +1 -0
- package/dist/web/assets/previews-pWNik-2g.js +3 -0
- package/dist/web/assets/{review-CMxQlQ-C.js → review-Dy_85lcH.js} +1 -1
- package/dist/web/assets/rho.editor-CCJeyHma.png +0 -0
- package/dist/web/assets/rho.flow-WkMj6fRS.png +0 -0
- package/dist/web/assets/rho.media-Cc6amodO.png +0 -0
- package/dist/web/assets/rho.notebook-6vsZKGDq.png +0 -0
- package/dist/web/assets/rho.object-BGoeXjte.png +0 -0
- package/dist/web/assets/rho.plots-yIiISCkb.png +0 -0
- package/dist/web/assets/rho.scatter-DwHxesCs.png +0 -0
- package/dist/web/assets/rho.servers-DKf_mnNq.png +0 -0
- package/dist/web/assets/rho.table-DWOGaWDa.png +0 -0
- package/dist/web/assets/rho.variables-CKcWczhQ.png +0 -0
- package/dist/web/assets/{skills-BFNSyrnD.js → skills-Dyz7mBlc.js} +1 -1
- package/dist/web/assets/{skills-work-BGPSnF_I.js → skills-work-BEGqlCkx.js} +1 -1
- package/dist/web/assets/studio-process-D-ykbC4D.js +2 -0
- package/dist/web/assets/timeline-events-DKTho9fU.js +1 -0
- package/dist/web/assets/work-turns-DSabCogR.js +1 -0
- package/dist/web/assets/{workspace-CWlkjLNx.js → workspace-DfjBL--r.js} +2 -2
- package/dist/web/composition.json +135 -80
- package/dist/web/index.html +3 -3
- package/dist/web/renderer.json +1 -1
- package/dist/web-assets.js +2 -2
- package/dist/work-graph.d.ts +11 -1
- package/dist/work-graph.js +16 -5
- package/dist/workspace.d.ts +2 -2
- package/dist/workspace.js +8 -7
- package/docs/FRONTEND.md +71 -21
- package/docs/HEADLESS.md +127 -24
- package/docs/THIRD-PARTY-PLUGINS.md +75 -3
- package/node_modules/@rho-workspace/plugin-agent/README.md +34 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +32 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +54 -11
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +29 -3
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +11 -5
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +12 -1
- package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +8 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/index.js +3 -2
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.d.ts +13 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.js +13 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp.js +16 -8
- package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +70 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.js +538 -0
- package/node_modules/@rho-workspace/plugin-agent/package.json +2 -1
- package/node_modules/@rho-workspace/plugin-figures/LICENSE +661 -0
- package/node_modules/@rho-workspace/plugin-figures/README.md +191 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/index.d.ts +236 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/index.js +165 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/operations.d.ts +44 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/operations.js +39 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/presentation.d.ts +19 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/presentation.js +26 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/record.d.ts +188 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/record.js +143 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.js +68 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui.d.ts +56 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui.js +363 -0
- package/node_modules/@rho-workspace/plugin-figures/package.json +41 -0
- package/node_modules/@rho-workspace/plugin-figures/py/figures.py +856 -0
- package/node_modules/@rho-workspace/plugin-figures/r/figures.R +775 -0
- package/node_modules/@rho-workspace/plugin-kernel/README.md +27 -3
- package/node_modules/@rho-workspace/plugin-kernel/dist/contract.d.ts +12 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/contract.js +8 -1
- package/node_modules/@rho-workspace/plugin-kernel/dist/index.d.ts +61 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/index.js +125 -9
- package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.d.ts +14 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.js +9 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-base.d.ts +23 -5
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook-work.js +1 -1
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.d.ts +6 -4
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.js +44 -17
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-styles.js +6 -0
- package/node_modules/@rho-workspace/plugin-kernel/package.json +1 -1
- package/node_modules/@rho-workspace/plugin-kernel/runners/runner.R +144 -4
- package/node_modules/@rho-workspace/plugin-kernel/runners/runner.py +106 -1
- package/node_modules/@rho-workspace/plugin-tables/README.md +5 -0
- package/node_modules/@rho-workspace/plugin-tables/dist/index.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-tables/dist/index.js +19 -3
- package/node_modules/@rho-workspace/plugin-tables/dist/ui.js +1 -1
- package/node_modules/@rho-workspace/plugin-tables/package.json +1 -1
- package/node_modules/hono/dist/adapter/aws-lambda/handler.js +1 -1
- package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +1 -1
- package/node_modules/hono/dist/cjs/adapter/bun/serve-static.js +1 -1
- package/node_modules/hono/dist/cjs/adapter/bun/websocket.js +1 -1
- package/node_modules/hono/dist/cjs/client/fetch-result-please.js +1 -1
- package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +2 -1
- package/node_modules/hono/dist/cjs/helper/ssg/ssg.js +5 -3
- package/node_modules/hono/dist/cjs/jsx/base.js +49 -30
- package/node_modules/hono/dist/cjs/jsx/components.js +2 -2
- package/node_modules/hono/dist/cjs/jsx/constants.js +2 -0
- package/node_modules/hono/dist/cjs/jsx/jsx-runtime.js +13 -17
- package/node_modules/hono/dist/cjs/middleware/etag/index.js +3 -1
- package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +1 -0
- package/node_modules/hono/dist/cjs/middleware/timing/timing.js +4 -3
- package/node_modules/hono/dist/cjs/utils/cookie.js +2 -2
- package/node_modules/hono/dist/cjs/utils/mime.js +1 -1
- package/node_modules/hono/dist/cjs/utils/url.js +1 -1
- package/node_modules/hono/dist/client/fetch-result-please.js +1 -1
- package/node_modules/hono/dist/helper/accepts/accepts.js +2 -1
- package/node_modules/hono/dist/helper/ssg/ssg.js +5 -3
- package/node_modules/hono/dist/jsx/base.js +49 -32
- package/node_modules/hono/dist/jsx/components.js +2 -2
- package/node_modules/hono/dist/jsx/constants.js +2 -1
- package/node_modules/hono/dist/jsx/jsx-runtime.js +15 -19
- package/node_modules/hono/dist/middleware/etag/index.js +3 -1
- package/node_modules/hono/dist/middleware/language/language.js +1 -1
- package/node_modules/hono/dist/middleware/pretty-json/index.js +1 -0
- package/node_modules/hono/dist/middleware/timing/timing.js +4 -3
- package/node_modules/hono/dist/utils/cookie.js +2 -2
- package/node_modules/hono/dist/utils/mime.js +1 -1
- package/node_modules/hono/dist/utils/url.js +1 -1
- package/node_modules/hono/package.json +1 -1
- package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +3 -2
- package/node_modules/jose/dist/webapi/jwks/remote.js +1 -1
- package/node_modules/jose/dist/webapi/jws/general/verify.js +2 -8
- package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +11 -8
- package/node_modules/jose/dist/webapi/lib/key_management.js +0 -1
- package/node_modules/jose/package.json +1 -1
- package/package.json +6 -4
- package/protocol/index.json +106 -64
- package/protocol/operations/agent.sessions.list.json +57 -3
- package/protocol/operations/agent.sessions.read.json +61 -4
- package/protocol/operations/capability.list.json +382 -6
- package/protocol/operations/composition.apply.json +10 -2
- package/protocol/operations/composition.delete.json +845 -0
- package/protocol/operations/composition.read.json +10 -2
- package/protocol/operations/composition.undo.json +10 -2
- package/protocol/operations/conversation.agents.add.json +346 -0
- package/protocol/operations/conversation.agents.remove.json +340 -0
- package/protocol/operations/conversation.configure.json +29 -0
- package/protocol/operations/conversation.decide.json +225 -0
- package/protocol/operations/conversation.list.json +29 -0
- package/protocol/operations/conversation.permission.json +225 -0
- package/protocol/operations/conversation.read.json +225 -0
- package/protocol/operations/conversation.restore.json +29 -0
- package/protocol/operations/conversation.send.json +294 -0
- package/protocol/operations/conversation.settings.check.json +226 -416
- package/protocol/operations/conversation.settings.read.json +224 -411
- package/protocol/operations/conversation.settings.update.json +248 -475
- package/protocol/operations/conversation.start.json +313 -6
- package/protocol/operations/conversation.stop.json +225 -0
- package/protocol/operations/conversation.trash.json +29 -0
- package/protocol/operations/extension.describe.json +39 -0
- package/protocol/operations/extension.install.json +62 -0
- package/protocol/operations/extension.list.json +164 -1
- package/protocol/operations/extension.market.change.json +210 -0
- package/protocol/operations/figure.read.json +265 -0
- package/protocol/operations/figure.select.json +157 -0
- package/protocol/operations/kernel.interrupt.json +119 -0
- package/protocol/operations/kernel.read.json +119 -0
- package/protocol/operations/kernel.run.json +119 -0
- package/protocol/operations/observation.snapshot.json +445 -8
- package/protocol/operations/project.openFile.json +10 -2
- package/protocol/operations/session.grant.json +4 -0
- package/protocol/operations/session.instructions.json +43 -0
- package/protocol/operations/shared.json +1 -0
- package/protocol/operations/view.close.json +10 -2
- package/protocol/operations/view.protection.set.json +10 -2
- package/vendor/composition.json +174 -109
- package/vendor/rho-workspace-plugin-agent-0.4.33.tgz +0 -0
- package/vendor/rho-workspace-plugin-figures-0.5.1.tgz +0 -0
- package/vendor/rho-workspace-plugin-kernel-0.5.13.tgz +0 -0
- package/vendor/rho-workspace-plugin-tables-0.5.2.tgz +0 -0
- package/web/theme.css +17 -0
- package/dist/web/assets/answer-board-CBkGbSLD.js +0 -1
- package/dist/web/assets/canvas-D8rDdfDz.js +0 -1
- package/dist/web/assets/choices-b0XhrGQV.js +0 -1
- package/dist/web/assets/graph-1OdIJ9l7.js +0 -2
- package/dist/web/assets/index-CsC3sTRq.css +0 -1
- package/dist/web/assets/index-CxmItXqa.js +0 -891
- package/dist/web/assets/overview-B9UtVJ32.js +0 -6
- package/dist/web/assets/preview-CksD_5KS.js +0 -1
- package/dist/web/assets/studio-process-CsRoTHOh.js +0 -2
- package/dist/web/assets/timeline-events-BbuuFWHV.js +0 -1
- package/dist/web/assets/work-turns-BvJlIMmA.js +0 -1
- package/vendor/rho-workspace-plugin-agent-0.4.29.tgz +0 -0
- package/vendor/rho-workspace-plugin-kernel-0.5.5.tgz +0 -0
- package/vendor/rho-workspace-plugin-tables-0.5.0.tgz +0 -0
package/dist/conversations.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { projectPath, commandFiles, buildWorkGraph } from './work-graph.js';
|
|
2
2
|
import { notesInstructions, projectNotesPath } from './project-notes.js';
|
|
3
|
+
import { uiContextPrompt } from './ui-context.js';
|
|
3
4
|
import { addSpend, describeLimits, describable, fileFacts, figureContext, raster } from './file-describer.js';
|
|
4
5
|
import { agentTurnLimit, displayCommand, toolActivity } from './conversation-presentation.js';
|
|
5
6
|
import { privateEvidencePath } from './evidence-capture.js';
|
|
@@ -8,6 +9,7 @@ import path from 'node:path';
|
|
|
8
9
|
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
9
10
|
import { fileURLToPath } from 'node:url';
|
|
10
11
|
import { atomicJson, canonical, digest, Fault, now, readJson, requireThat } from '@rho-workspace/plugin-contracts';
|
|
12
|
+
import { acpEnvironment, installedAcpAgents, knownAcpAgent, locate, missingClient } from '@rho-workspace/plugin-agent/acp-command';
|
|
11
13
|
// Rho holds user messages and their original identities. The selected Agent plugin
|
|
12
14
|
// owns the connection, native session and protocol. Reads never start a driver.
|
|
13
15
|
// A new Claude Code session opens in Manual mode unless the person's Claude settings name another.
|
|
@@ -15,6 +17,9 @@ import { atomicJson, canonical, digest, Fault, now, readJson, requireThat } from
|
|
|
15
17
|
// settings stays, and a model without Auto falls back to Accept edits with the adapter's warning.
|
|
16
18
|
const claudePreferred = { mode: { value: 'auto', replaces: ['default'] } };
|
|
17
19
|
export const conversationLimits = {
|
|
20
|
+
// Per-message interface observations: omit and count extra views; refuse an
|
|
21
|
+
// observation still larger than the byte budget before sending the message.
|
|
22
|
+
maxUiContextBytes: 16384, maxUiContextViews: 32,
|
|
18
23
|
// Conversations kept per project, not counting the trash; starting or restoring another is
|
|
19
24
|
// refused. The trash keeps as many again; past that, moving one there is refused.
|
|
20
25
|
maxConversations: 64,
|
|
@@ -55,6 +60,9 @@ export const conversationLimits = {
|
|
|
55
60
|
// Settings chosen for one Agent or one conversation, and the length of one chosen value;
|
|
56
61
|
// more are refused.
|
|
57
62
|
maxChoices: 12, maxChoiceChars: 512,
|
|
63
|
+
// ACP Agents the person adds in Rho, kept in this service's Agent settings and offered in every
|
|
64
|
+
// project; adding another is refused until one is removed.
|
|
65
|
+
maxAddedAgents: 16,
|
|
58
66
|
// Files compared on disk before and after a message, to record which changed during it; past
|
|
59
67
|
// this many the comparison stops and the record says it is partial. Changes kept per message;
|
|
60
68
|
// more are counted.
|
|
@@ -127,7 +135,13 @@ function compare(before, after, limit) {
|
|
|
127
135
|
changed.sort((a, b) => (a.modifiedAt ?? '\uffff').localeCompare(b.modifiedAt ?? '\uffff') || a.path.localeCompare(b.path));
|
|
128
136
|
return { changed: changed.slice(0, limit), omitted: Math.max(0, changed.length - limit), partial };
|
|
129
137
|
}
|
|
130
|
-
const
|
|
138
|
+
const builtIn = ['claude-acp', 'codex-acp', 'acp'];
|
|
139
|
+
const addedName = /^acp:[a-z0-9][a-z0-9-]{0,39}$/;
|
|
140
|
+
const backendName = (value) => typeof value === 'string' && (builtIn.includes(value) || addedName.test(value));
|
|
141
|
+
const generic = (backend) => backend === 'acp' || !!backend?.startsWith('acp:');
|
|
142
|
+
const sameCommand = (a, b) => a.file === b.file && a.args.length === b.args.length && a.args.every((arg, i) => arg === b.args[i]);
|
|
143
|
+
// DeepSeek Harness's own ACP command, which continues the sessions Harness keeps in this folder.
|
|
144
|
+
const harnessCommand = (command) => !!command && (knownAcpAgent(command)?.id === 'dsh' || [command.file, ...command.args].some(part => /(^|[\\/])dsh(\.[a-z]+)?$/i.test(part)));
|
|
131
145
|
const blankSettings = () => ({ format: 1, version: 0, updatedAt: null, backend: null, choices: {}, offered: {} });
|
|
132
146
|
const acpDriver = (driver) => !!driver && 'configure' in driver;
|
|
133
147
|
const identity = /^c-[a-f0-9]{16}$/;
|
|
@@ -137,6 +151,9 @@ function cut(text, maxBytes) {
|
|
|
137
151
|
// Cut on a character boundary; the cut is reported, never silent.
|
|
138
152
|
return { text: Buffer.from(text).subarray(0, maxBytes).toString('utf8').replace(/�+$/, ''), cut: true };
|
|
139
153
|
}
|
|
154
|
+
// Some Agents (Kimi Code) title a session with the start of its first prompt, which carries the
|
|
155
|
+
// interface context Rho appends to the person's words; the title keeps the person's words.
|
|
156
|
+
const agentTitleOf = (title) => title ? title.split(/\s*Rho interface context for this message:/)[0].trim() || null : null;
|
|
140
157
|
function titleOf(text) { const line = text.trim().split('\n')[0].trim(); return line.length > 80 ? line.slice(0, 79) + '…' : line; }
|
|
141
158
|
// An Agent reports a session it no longer has, in its own words: the ACP load answers "Resource
|
|
142
159
|
// not found", Claude Code says the session does not exist, and a load that cannot run at all says
|
|
@@ -186,6 +203,8 @@ export class Conversations {
|
|
|
186
203
|
drivers = new Map();
|
|
187
204
|
permissions = new Map();
|
|
188
205
|
checks = new Map();
|
|
206
|
+
// Agents being added, by command: a second request for the same command waits for the first.
|
|
207
|
+
adding = new Map();
|
|
189
208
|
// Descriptions being written by a model now, by project, conversation, message and file.
|
|
190
209
|
writing = new Set();
|
|
191
210
|
describing = { ...describeLimits };
|
|
@@ -232,9 +251,10 @@ export class Conversations {
|
|
|
232
251
|
const last = c.turns.at(-1), usage = c.observation?.usage;
|
|
233
252
|
const costs = c.turns.map(t => t.result?.costUsd).filter((v) => typeof v === 'number');
|
|
234
253
|
return { conversationId: c.id, changeId: c.changeId, requestDigest: c.requestDigest, title: c.title, client: c.client, principal: c.principal, createdAt: c.createdAt, updatedAt: c.updatedAt, version: c.version, turns: c.turns.length, state: last?.state ?? 'idle', lastAt: last?.endedAt ?? last?.sentAt ?? c.createdAt,
|
|
235
|
-
listed: 2, turnLimit: !!last && agentTurnLimit(last), waitingPermission: last?.state === 'running' && !!last.permissions?.some(p => p.state === 'pending'), backend: c.backend ?? 'claude-cli', agentTitle: c.observation?.info?.title
|
|
254
|
+
listed: 2, turnLimit: !!last && agentTurnLimit(last), waitingPermission: last?.state === 'running' && !!last.permissions?.some(p => p.state === 'pending'), backend: c.backend ?? 'claude-cli', agentTitle: agentTitleOf(c.observation?.info?.title), adapter: c.adapter ?? null, model: [...c.turns].reverse().find(t => t.model)?.model ?? null, config: c.config ?? {},
|
|
236
255
|
usage: usage ? { used: usage.used, size: usage.size, cost: usage.cost ?? null } : null, costUsd: costs.length ? costs.reduce((a, b) => a + b, 0) : null,
|
|
237
|
-
sessionId: c.acpSessionId ?? c.nativeSessionId ?? null, trashedAt: c.trashedAt ?? null
|
|
256
|
+
sessionId: c.acpSessionId ?? c.nativeSessionId ?? null, trashedAt: c.trashedAt ?? null,
|
|
257
|
+
origin: c.origin ? { producer: c.origin.producer, where: c.origin.where, ...(c.origin.copy ? { copyOf: c.origin.sessionId } : {}) } : null };
|
|
238
258
|
}
|
|
239
259
|
async write(project, c) {
|
|
240
260
|
const directory = await this.folder(project);
|
|
@@ -391,10 +411,12 @@ export class Conversations {
|
|
|
391
411
|
// Without an Agent or settings named in the request, the person's saved ones are used and
|
|
392
412
|
// recorded with the conversation.
|
|
393
413
|
async start(project, by, input) {
|
|
394
|
-
requireThat(!input.backend ||
|
|
414
|
+
requireThat(!input.backend || backendName(input.backend), 'invalid', 'Choose an Agent Rho starts.');
|
|
395
415
|
if (input.config)
|
|
396
416
|
this.choices(input.config);
|
|
397
|
-
const
|
|
417
|
+
const from = input.continue;
|
|
418
|
+
const text = this.message(input.text), requestDigest = digest(canonical({ text, ...(input.contexts?.length ? { contexts: input.contexts } : {}), ...(input.backend ? { backend: input.backend } : {}), ...(input.config ? { config: input.config } : {}),
|
|
419
|
+
...(input.uiContext ? { uiContext: input.uiContext } : {}), ...(from ? { continue: { producer: from.producer, sessionId: from.sessionId, copy: from.copy } } : {}) }));
|
|
398
420
|
return this.serial(project.id, async () => {
|
|
399
421
|
const index = await this.index(project), existing = index.conversations.find(x => x.changeId === input.changeId);
|
|
400
422
|
requireThat(!index.deleted?.some(x => x.changeId === input.changeId), 'conflict', 'That request started a conversation that was later deleted. It is not started again.');
|
|
@@ -404,16 +426,35 @@ export class Conversations {
|
|
|
404
426
|
return this.reply(project, c, c.turns[0]);
|
|
405
427
|
}
|
|
406
428
|
requireThat(index.conversations.filter(x => !x.trashedAt).length < this.limits.maxConversations, 'limit', `This project keeps at most ${this.limits.maxConversations} conversations started in Rho. Move one to the trash to start another.`, { limit: 'maxConversations', maximum: this.limits.maxConversations });
|
|
429
|
+
if (from && !from.copy) {
|
|
430
|
+
const already = index.conversations.find(x => x.sessionId === from.sessionId);
|
|
431
|
+
requireThat(!already, 'conflict', already?.trashedAt ? 'This session is a conversation in the trash. Restore it to continue.' : 'This session already continues as a conversation here. Open it to send your message.');
|
|
432
|
+
await this.notHeld(project, from.producer, from.sessionId, false);
|
|
433
|
+
}
|
|
407
434
|
this.capacity();
|
|
408
435
|
const saved = await this.readSettings();
|
|
409
|
-
|
|
436
|
+
// A saved Agent the person has since removed is not used; the service's own is.
|
|
437
|
+
const savedBackend = saved.backend && (!saved.backend.startsWith('acp:') || this.commandOf(saved.backend, saved)) ? saved.backend : null;
|
|
438
|
+
const backend = from?.backend ?? input.backend ?? savedBackend ?? this.config.backend ?? 'claude-acp';
|
|
439
|
+
const command = generic(backend) ? this.commandOf(backend, saved) : undefined;
|
|
440
|
+
requireThat(!generic(backend) || command, 'unavailable', backend === 'acp' ? 'No ACP command was named when Rho started. Add the Agent under Agents in Rho. Nothing was sent.' : 'That Agent is not added in Rho. Add it under Agents first. Nothing was sent.');
|
|
410
441
|
const config = backend === 'claude-cli' ? undefined : input.config ?? saved.choices[backend];
|
|
411
|
-
const at = now(), c = { format: 2, backend, id: 'c-' + randomBytes(8).toString('hex'), changeId: input.changeId, requestDigest, createdAt: at, createdBy: by, updatedAt: at, version: 0, client: backend === 'codex-acp' ? 'codex' : backend
|
|
442
|
+
const at = now(), c = { format: 2, backend, id: 'c-' + randomBytes(8).toString('hex'), changeId: input.changeId, requestDigest, createdAt: at, createdBy: by, updatedAt: at, version: 0, client: backend === 'codex-acp' ? 'codex' : generic(backend) ? 'acp' : 'claude-code', nativeSessionId: backend === 'claude-cli' ? randomUUID() : null, confirmed: false, principal: null, title: titleOf(text), turns: [], ...(config && Object.keys(config).length ? { config } : {}), ...(command ? { acpCommand: command } : {}) };
|
|
412
443
|
for (const id of input.contexts ?? [])
|
|
413
444
|
await this.ports.context?.(project, id);
|
|
445
|
+
if (from) {
|
|
446
|
+
c.origin = { producer: from.producer, sessionId: from.sessionId, where: from.where, copy: from.copy };
|
|
447
|
+
if (from.copy)
|
|
448
|
+
c.forkFrom = from.sessionId;
|
|
449
|
+
else
|
|
450
|
+
c.acpSessionId = from.sessionId;
|
|
451
|
+
c.title = from.title;
|
|
452
|
+
}
|
|
414
453
|
const turn = this.turn(input.changeId, requestDigest, by, text, 1);
|
|
415
454
|
if (input.contexts?.length)
|
|
416
455
|
turn.contexts = [...input.contexts];
|
|
456
|
+
if (input.uiContext)
|
|
457
|
+
turn.uiContext = await this.ports.uiContext?.(project, input.uiContext, this.limits);
|
|
417
458
|
c.turns.push(turn);
|
|
418
459
|
// Recorded before the process starts, so a crash in between leaves an unknown message, never a resend.
|
|
419
460
|
await this.write(project, c);
|
|
@@ -421,8 +462,31 @@ export class Conversations {
|
|
|
421
462
|
return this.reply(project, c, turn);
|
|
422
463
|
});
|
|
423
464
|
}
|
|
465
|
+
/** Whether a process holding a session open is Rho's own connection to this conversation: the
|
|
466
|
+
* Agent its adapter started. */
|
|
467
|
+
async owns(project, conversationId, pid) {
|
|
468
|
+
const adapter = this.drivers.get(project.id + '/' + conversationId)?.driver?.pid;
|
|
469
|
+
if (!adapter || !pid || process.platform === 'win32')
|
|
470
|
+
return false;
|
|
471
|
+
const { execFile } = await import('node:child_process');
|
|
472
|
+
let current = pid;
|
|
473
|
+
for (let depth = 0; depth < 4 && current > 1; depth++) {
|
|
474
|
+
const parent = await new Promise(resolve => execFile('ps', ['-o', 'ppid=', '-p', String(current)], { timeout: 2000 }, (error, out) => resolve(error ? 0 : Number(String(out).trim()))));
|
|
475
|
+
if (parent === adapter)
|
|
476
|
+
return true;
|
|
477
|
+
current = parent;
|
|
478
|
+
}
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
async notHeld(project, producer, sessionId, existing, conversationId) {
|
|
482
|
+
if (producer !== 'claude-code' && producer !== 'codex')
|
|
483
|
+
return;
|
|
484
|
+
const found = await this.ports.nativeSession?.(project, producer, sessionId) ?? null;
|
|
485
|
+
const holder = found?.open && !(conversationId && await this.owns(project, conversationId, found.open.pid)) ? found.open : null;
|
|
486
|
+
requireThat(!holder, 'condition', `This session is open in ${holder?.where ?? 'another window'}. Close it there to continue here${existing ? '' : ', or continue a copy'}. Nothing was sent.`, { heldBy: holder?.where ?? null });
|
|
487
|
+
}
|
|
424
488
|
async send(project, by, input) {
|
|
425
|
-
const text = this.message(input.text), requestDigest = digest(canonical({ text, ...(input.contexts?.length ? { contexts: input.contexts } : {}), ...(input.continuationOf ? { continuationOf: input.continuationOf } : {}) }));
|
|
489
|
+
const text = this.message(input.text), requestDigest = digest(canonical({ text, ...(input.contexts?.length ? { contexts: input.contexts } : {}), ...(input.uiContext ? { uiContext: input.uiContext } : {}), ...(input.continuationOf ? { continuationOf: input.continuationOf } : {}) }));
|
|
426
490
|
return this.serial(project.id, async () => {
|
|
427
491
|
const c = await this.load(project, input.conversationId), existing = c.turns.find(t => t.changeId === input.changeId);
|
|
428
492
|
if (existing) {
|
|
@@ -444,6 +508,9 @@ export class Conversations {
|
|
|
444
508
|
requireThat(!alive, 'conflict', `The Agent from before Rho restarted may still be answering (process ${last.pid}). End that process before sending another message.`);
|
|
445
509
|
}
|
|
446
510
|
requireThat(c.turns.length < this.limits.maxTurns, 'limit', `A conversation keeps at most ${this.limits.maxTurns} messages. Start a new conversation.`, { limit: 'maxTurns', maximum: this.limits.maxTurns });
|
|
511
|
+
// A session continued in a terminal or another app may be open there now.
|
|
512
|
+
if (c.acpSessionId && (c.client === 'claude-code' || c.client === 'codex'))
|
|
513
|
+
await this.notHeld(project, c.client, c.acpSessionId, true, c.id);
|
|
447
514
|
requireThat(Buffer.byteLength(JSON.stringify({ ...c, observation: undefined })) + this.limits.maxObservationBytes + this.limits.maxTurnBytes + this.limits.maxMessageBytes <= this.limits.maxRecordBytes, 'limit', 'This conversation has reached the size Rho keeps. Start a new conversation.', { limit: 'maxRecordBytes', maximum: this.limits.maxRecordBytes });
|
|
448
515
|
this.capacity();
|
|
449
516
|
for (const old of c.turns)
|
|
@@ -456,6 +523,8 @@ export class Conversations {
|
|
|
456
523
|
turn.continuationOf = input.continuationOf;
|
|
457
524
|
if (input.contexts?.length)
|
|
458
525
|
turn.contexts = [...input.contexts];
|
|
526
|
+
if (input.uiContext)
|
|
527
|
+
turn.uiContext = await this.ports.uiContext?.(project, input.uiContext, this.limits);
|
|
459
528
|
c.turns.push(turn);
|
|
460
529
|
await this.write(project, c);
|
|
461
530
|
this.launch(project, c, turn);
|
|
@@ -468,7 +537,7 @@ export class Conversations {
|
|
|
468
537
|
this.choices({ [input.configId]: input.value });
|
|
469
538
|
return this.serial(project.id, async () => {
|
|
470
539
|
const c = await this.load(project, input.conversationId), last = c.turns.at(-1);
|
|
471
|
-
requireThat(c.backend === 'claude-acp' || c.backend === 'codex-acp' || c.backend
|
|
540
|
+
requireThat(c.backend === 'claude-acp' || c.backend === 'codex-acp' || generic(c.backend), 'unavailable', 'This conversation\'s Agent has no settings Rho can change.');
|
|
472
541
|
requireThat(!c.trashedAt, 'conflict', 'This conversation is in the trash. Restore it to change its settings.');
|
|
473
542
|
requireThat(!last || last.state !== 'running' || !this.liveTurn(project, c, last), 'conflict', 'The Agent is answering. Change its settings after this message.');
|
|
474
543
|
const driver = this.drivers.get(project.id + '/' + c.id)?.driver;
|
|
@@ -509,20 +578,53 @@ export class Conversations {
|
|
|
509
578
|
return value;
|
|
510
579
|
}
|
|
511
580
|
settingsView(s) {
|
|
512
|
-
const service = this.config.backend ?? 'claude-acp';
|
|
513
|
-
return { version: s.version, updatedAt: s.updatedAt, backend: s.backend, serviceBackend: service, choices: s.choices, offered: s.offered, checking: [...this.checks.keys()], describe: { enabled: s.describe?.enabled !== false }
|
|
581
|
+
const service = this.config.backend ?? 'claude-acp', added = s.added ?? [];
|
|
582
|
+
return { version: s.version, updatedAt: s.updatedAt, backend: s.backend, serviceBackend: service, choices: s.choices, offered: s.offered, checking: [...this.checks.keys()], describe: { enabled: s.describe?.enabled !== false }, agents: this.agents(s),
|
|
583
|
+
added: added.map(a => ({ backend: a.backend, name: a.name, command: [a.command.file, ...a.command.args], addedAt: a.addedAt })),
|
|
584
|
+
// The ACP Agents Rho knows that are installed here and not yet added. Finding them reads PATH.
|
|
585
|
+
installed: installedAcpAgents(process.env).filter(i => !(this.config.acpCommand && sameCommand(i.command, this.config.acpCommand)) && !added.some(a => sameCommand(a.command, i.command)))
|
|
586
|
+
.map(i => ({ id: i.id, name: i.name, command: [i.command.file, ...i.command.args] })) };
|
|
587
|
+
}
|
|
588
|
+
// The command that starts an ACP Agent other than Claude Code or Codex: one the person added, or
|
|
589
|
+
// the one named when this service started.
|
|
590
|
+
commandOf(backend, s) {
|
|
591
|
+
return backend === 'acp' ? this.config.acpCommand : s.added?.find(a => a.backend === backend)?.command;
|
|
592
|
+
}
|
|
593
|
+
// Which Agents this service can start: the person's installed clients, found as the adapters
|
|
594
|
+
// find them, the ACP command named when it started and the ACP Agents the person added. Looking
|
|
595
|
+
// starts nothing. The named command's Agent is named as Rho knows it, else as it named itself the
|
|
596
|
+
// last time it answered, else by its program.
|
|
597
|
+
agents(s) {
|
|
598
|
+
const found = (problem) => problem ? { found: false, problem } : { found: true };
|
|
599
|
+
const command = this.config.acpCommand, reported = s.offered.acp?.adapter?.name;
|
|
600
|
+
return {
|
|
601
|
+
// A command the embedding names is its own choice and is not looked for, as the adapters do.
|
|
602
|
+
'claude-acp': found(this.config.claudeAcp?.command ? null : missingClient('claude', acpEnvironment('claude', { ...process.env }, this.binary))),
|
|
603
|
+
'codex-acp': found(this.config.codexAcp?.command ? null : missingClient('codex', acpEnvironment('codex', { ...process.env }, this.config.codex))),
|
|
604
|
+
acp: command ? { found: true, name: knownAcpAgent(command)?.name ?? (harnessCommand(command) ? 'DeepSeek Harness' : reported || path.basename(command.file).replace(/\.(exe|cmd)$/i, '')) } : { found: false, problem: 'No ACP command was named when Rho started.' },
|
|
605
|
+
...Object.fromEntries((s.added ?? []).map(a => [a.backend, locate(a.command.file, process.env) ? { found: true, name: a.name }
|
|
606
|
+
: { found: false, name: a.name, problem: `${a.name} is no longer at ${a.command.file}. Install it again, or remove it and add it again.` }]))
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
// The Agent that continues DeepSeek Harness's sessions: Harness's own ACP command, named when
|
|
610
|
+
// Rho started or added by the person.
|
|
611
|
+
async harness() {
|
|
612
|
+
if (harnessCommand(this.config.acpCommand))
|
|
613
|
+
return 'acp';
|
|
614
|
+
return (await this.readSettings()).added?.find(a => harnessCommand(a.command))?.backend ?? null;
|
|
514
615
|
}
|
|
515
616
|
async settings() { return this.settingsView(await this.readSettings()); }
|
|
516
617
|
// The person's Agent and settings for new conversations. Each Agent's choices are replaced as a
|
|
517
618
|
// whole and must be among the values it last offered; a stale version is refused.
|
|
518
619
|
async updateSettings(input) {
|
|
519
620
|
requireThat(this.config.settingsFile, 'unavailable', 'This service keeps no Agent settings.');
|
|
520
|
-
requireThat(input.backend == null || backends.includes(input.backend), 'invalid', 'Choose Claude Code or Codex.');
|
|
521
621
|
return this.serial('settings', async () => {
|
|
522
622
|
const s = await this.readSettings();
|
|
523
623
|
requireThat(s.version === input.expectedVersion, 'conflict', 'The Agent settings changed in another tab. Read them again.', { version: s.version });
|
|
624
|
+
const startable = (backend) => backendName(backend) && (builtIn.includes(backend) || !!this.commandOf(backend, s));
|
|
625
|
+
requireThat(input.backend == null || startable(input.backend), 'invalid', 'Choose an Agent Rho starts.');
|
|
524
626
|
for (const [backend, values] of Object.entries(input.choices ?? {})) {
|
|
525
|
-
requireThat(
|
|
627
|
+
requireThat(startable(backend), 'invalid', 'Choose an Agent Rho starts.');
|
|
526
628
|
this.choices(values);
|
|
527
629
|
const offered = s.offered[backend]?.options;
|
|
528
630
|
requireThat(offered || !Object.keys(values).length, 'unavailable', 'Check what this Agent offers first.');
|
|
@@ -546,7 +648,7 @@ export class Conversations {
|
|
|
546
648
|
// ends the process; no message is sent. The protected resource is local Agent processes: one
|
|
547
649
|
// check per Agent at a time (a second request waits for the same one), each bounded by connectTimeoutMs.
|
|
548
650
|
async check(project, input) {
|
|
549
|
-
requireThat(
|
|
651
|
+
requireThat(backendName(input.backend), 'invalid', 'Choose an Agent Rho starts.');
|
|
550
652
|
requireThat(this.config.settingsFile, 'unavailable', 'This service keeps no Agent settings.');
|
|
551
653
|
requireThat(!this.closing, 'unavailable', 'Rho is closing. Nothing was started.');
|
|
552
654
|
let pending = this.checks.get(input.backend);
|
|
@@ -560,7 +662,9 @@ export class Conversations {
|
|
|
560
662
|
async inspect(project, backend) {
|
|
561
663
|
const { ClaudeAcpConversation, CodexAcpConversation, GenericAcpConversation } = await import('@rho-workspace/plugin-agent/conversation');
|
|
562
664
|
const common = { cwd: project.root, mcpServers: [], limits: this.limits };
|
|
563
|
-
const
|
|
665
|
+
const command = generic(backend) ? this.commandOf(backend, await this.readSettings()) : undefined;
|
|
666
|
+
requireThat(!generic(backend) || command, 'unavailable', backend === 'acp' ? 'No ACP command was named when Rho started.' : 'That Agent is not added in Rho.');
|
|
667
|
+
const driver = backend === 'codex-acp' ? new CodexAcpConversation({ ...this.config.codexAcp, ...common, codex: this.config.codex }) : command ? new GenericAcpConversation({ ...common, command }) : new ClaudeAcpConversation({ preferred: claudePreferred, ...this.config.claudeAcp, ...common, claude: this.binary });
|
|
564
668
|
let found;
|
|
565
669
|
try {
|
|
566
670
|
found = await driver.inspect();
|
|
@@ -581,6 +685,77 @@ export class Conversations {
|
|
|
581
685
|
await atomicJson(this.config.settingsFile, s);
|
|
582
686
|
});
|
|
583
687
|
}
|
|
688
|
+
// An ACP Agent the person adds for new conversations in every project. Rho starts its command once
|
|
689
|
+
// in this project, as a check does (it opens a session, reads what the Agent offers and removes
|
|
690
|
+
// the session; no message is sent), and keeps it only when the Agent answered. The command is
|
|
691
|
+
// the person's own: a program on PATH or a path to one, and its arguments. Adding the same
|
|
692
|
+
// command again checks it again; one start per command runs at a time.
|
|
693
|
+
async addAgent(project, input) {
|
|
694
|
+
requireThat(this.config.settingsFile, 'unavailable', 'This service keeps no Agent settings.');
|
|
695
|
+
requireThat(!this.closing, 'unavailable', 'Rho is closing. Nothing was started.');
|
|
696
|
+
const [program, ...args] = input.command;
|
|
697
|
+
const file = locate(program, process.env);
|
|
698
|
+
requireThat(file, 'unavailable', `${program} was not found ${/[\\/]/.test(program) ? 'there' : 'on PATH'}. Install it, or give the path of its program.`);
|
|
699
|
+
const command = { file: path.resolve(file), args }, key = JSON.stringify(command);
|
|
700
|
+
let pending = this.adding.get(key);
|
|
701
|
+
if (!pending) {
|
|
702
|
+
pending = this.addOnce(project, command).finally(() => this.adding.delete(key));
|
|
703
|
+
this.adding.set(key, pending);
|
|
704
|
+
}
|
|
705
|
+
return pending;
|
|
706
|
+
}
|
|
707
|
+
async addOnce(project, command) {
|
|
708
|
+
const known = knownAcpAgent(command), label = known?.name ?? path.basename(command.file);
|
|
709
|
+
const room = (s) => requireThat(s.added?.some(a => sameCommand(a.command, command)) || (s.added?.length ?? 0) < this.limits.maxAddedAgents, 'limit', `Rho keeps at most ${this.limits.maxAddedAgents} added Agents. Remove one first. Nothing was started.`, { limit: 'maxAddedAgents', maximum: this.limits.maxAddedAgents });
|
|
710
|
+
room(await this.readSettings());
|
|
711
|
+
const { GenericAcpConversation } = await import('@rho-workspace/plugin-agent/conversation');
|
|
712
|
+
let found;
|
|
713
|
+
try {
|
|
714
|
+
found = await new GenericAcpConversation({ cwd: project.root, mcpServers: [], limits: this.limits, command }).inspect();
|
|
715
|
+
}
|
|
716
|
+
catch (error) {
|
|
717
|
+
throw new Fault('unavailable', `${label} did not start as an ACP Agent (${(error instanceof Error ? error.message : String(error)).replace(/\.$/, '')}). Nothing was added.`);
|
|
718
|
+
}
|
|
719
|
+
return this.serial('settings', async () => {
|
|
720
|
+
const s = await this.readSettings(), added = s.added ?? [], existing = added.find(a => sameCommand(a.command, command));
|
|
721
|
+
room(s);
|
|
722
|
+
const name = existing?.name ?? known?.name ?? found.adapter?.name ?? label;
|
|
723
|
+
const backend = existing?.backend ?? this.newName(known?.id ?? found.adapter?.name ?? label, added);
|
|
724
|
+
if (!existing)
|
|
725
|
+
s.added = [...added, { backend, name, command, addedAt: now() }];
|
|
726
|
+
s.offered[backend] = { at: now(), source: 'check', adapter: found.adapter, options: found.options, omitted: found.omitted };
|
|
727
|
+
s.version++;
|
|
728
|
+
s.updatedAt = now();
|
|
729
|
+
await atomicJson(this.config.settingsFile, s);
|
|
730
|
+
return this.settingsView(s);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
newName(seed, added) {
|
|
734
|
+
const base = seed.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 32) || 'agent';
|
|
735
|
+
for (let n = 1;; n++) {
|
|
736
|
+
const name = `acp:${base}${n > 1 ? '-' + n : ''}`;
|
|
737
|
+
if (!added.some(a => a.backend === name))
|
|
738
|
+
return name;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// Removing an added Agent takes it out of every Agent menu. Its conversations stay, each with
|
|
742
|
+
// the command it started with, so they can still be read and continued.
|
|
743
|
+
async removeAgent(input) {
|
|
744
|
+
requireThat(this.config.settingsFile, 'unavailable', 'This service keeps no Agent settings.');
|
|
745
|
+
return this.serial('settings', async () => {
|
|
746
|
+
const s = await this.readSettings(), backend = input.backend;
|
|
747
|
+
requireThat(s.added?.some(a => a.backend === backend), 'not_found', 'No Agent added in Rho has that name.');
|
|
748
|
+
s.added = s.added.filter(a => a.backend !== backend);
|
|
749
|
+
delete s.choices[backend];
|
|
750
|
+
delete s.offered[backend];
|
|
751
|
+
if (s.backend === backend)
|
|
752
|
+
s.backend = null;
|
|
753
|
+
s.version++;
|
|
754
|
+
s.updatedAt = now();
|
|
755
|
+
await atomicJson(this.config.settingsFile, s);
|
|
756
|
+
return this.settingsView(s);
|
|
757
|
+
});
|
|
758
|
+
}
|
|
584
759
|
// A stop is a request until the driver confirms it; what was already done stays done.
|
|
585
760
|
async stop(project, by, input) {
|
|
586
761
|
return this.serial(project.id, async () => {
|
|
@@ -733,12 +908,20 @@ export class Conversations {
|
|
|
733
908
|
}
|
|
734
909
|
});
|
|
735
910
|
}
|
|
911
|
+
async changedElsewhere(project, c) {
|
|
912
|
+
if (!c.acpSessionId || (c.client !== 'claude-code' && c.client !== 'codex'))
|
|
913
|
+
return false;
|
|
914
|
+
const previous = c.turns.at(-2), found = await this.ports.nativeSession?.(project, c.client, c.acpSessionId).catch(() => null);
|
|
915
|
+
return !!found && !!previous?.endedAt && Date.parse(found.lastAt) > Date.parse(previous.endedAt) + 5000;
|
|
916
|
+
}
|
|
736
917
|
async driver(project, c) {
|
|
737
918
|
return this.serial('driver-pool', async () => {
|
|
738
919
|
const key = project.id + '/' + c.id, old = this.drivers.get(key);
|
|
739
920
|
if (old?.timer)
|
|
740
921
|
clearTimeout(old.timer);
|
|
741
|
-
|
|
922
|
+
// An open connection keeps the session as it last saw it. When the session went on in
|
|
923
|
+
// another window since, it is opened again from the Agent's own record.
|
|
924
|
+
if (old?.driver.connected && !await this.changedElsewhere(project, c))
|
|
742
925
|
return old.driver;
|
|
743
926
|
if (old)
|
|
744
927
|
await this.retire(key);
|
|
@@ -754,7 +937,7 @@ export class Conversations {
|
|
|
754
937
|
const observed = { observation: c.observation, observe: (observation) => {
|
|
755
938
|
requireThat(observation.sessionId === c.acpSessionId, 'conflict', 'The observed Agent session differs from the retained identity.');
|
|
756
939
|
requireThat(Buffer.byteLength(JSON.stringify(observation)) <= this.limits.maxObservationBytes, 'limit', 'The session projection exceeded maxObservationBytes; prior observations were kept.');
|
|
757
|
-
const config = observation.config, learned = config && config.at !== c.observation?.config?.at && config.origin === 'initial' && !c.config && (c.backend === 'claude-acp' || c.backend === 'codex-acp' || c.backend
|
|
940
|
+
const config = observation.config, learned = config && config.at !== c.observation?.config?.at && config.origin === 'initial' && !c.config && (c.backend === 'claude-acp' || c.backend === 'codex-acp' || generic(c.backend));
|
|
758
941
|
c.observation = observation;
|
|
759
942
|
this.schedule(project, c);
|
|
760
943
|
for (const live of this.live.values())
|
|
@@ -770,13 +953,16 @@ export class Conversations {
|
|
|
770
953
|
} };
|
|
771
954
|
// Every session started here is told about the project's shared notes and its section.
|
|
772
955
|
const instructions = notesInstructions(c.id, { notebook: !!this.ports.notebook?.() });
|
|
773
|
-
const common = { cwd: project.root, mcpServers, limits: this.limits, sessionId: c.acpSessionId, config: c.config, instructions, ...observed };
|
|
956
|
+
const common = { cwd: project.root, mcpServers, limits: this.limits, sessionId: c.acpSessionId, ...(!c.acpSessionId && c.forkFrom ? { forkFrom: c.forkFrom } : {}), config: c.config, instructions, ...observed };
|
|
957
|
+
// A conversation started before Rho kept its ACP command uses the one named at this start.
|
|
958
|
+
const command = generic(c.backend) ? c.acpCommand ?? (c.backend === 'acp' ? this.config.acpCommand : undefined) : undefined;
|
|
959
|
+
requireThat(!generic(c.backend) || command, 'unavailable', 'This conversation\'s ACP Agent was named when Rho started, and this start names none. Start Rho with the same --acp-command to continue it.');
|
|
774
960
|
const driver = c.backend === 'codex-acp'
|
|
775
961
|
? new CodexAcpConversation({ ...this.config.codexAcp, ...common, codex: this.config.codex })
|
|
776
962
|
: c.backend === 'claude-acp'
|
|
777
963
|
? new ClaudeAcpConversation({ preferred: claudePreferred, ...this.config.claudeAcp, ...common, claude: this.binary, plugins: [...this.config.claudeAcp?.plugins ?? [], ...await this.ports.claudePlugins?.(project) ?? []] })
|
|
778
|
-
:
|
|
779
|
-
? new GenericAcpConversation({ ...common, command
|
|
964
|
+
: command
|
|
965
|
+
? new GenericAcpConversation({ ...common, command })
|
|
780
966
|
: new ClaudeCliConversation({ cwd: project.root, binary: this.binary, sessionId: c.nativeSessionId, confirmed: c.confirmed, projectName: project.name ?? path.basename(project.root), mcpServers, limits: this.limits, instructions });
|
|
781
967
|
this.drivers.set(key, { driver, project, conversation: c });
|
|
782
968
|
return driver;
|
|
@@ -965,7 +1151,7 @@ export class Conversations {
|
|
|
965
1151
|
}
|
|
966
1152
|
}
|
|
967
1153
|
});
|
|
968
|
-
const utterance = turn.text + (turn.continuationOf ? '\n\nOriginal task:\n' + c.turns[0].text : '') + (turn.contexts?.length ? '\n\nRead these exact frozen review contexts with context.read before responding: ' + turn.contexts.join(', ') + '. Reply with annotation.reply and explicit result resource versions.' : '');
|
|
1154
|
+
const utterance = turn.text + uiContextPrompt(turn.uiContext) + (turn.continuationOf ? '\n\nOriginal task:\n' + c.turns[0].text : '') + (turn.contexts?.length ? '\n\nRead these exact frozen review contexts with context.read before responding: ' + turn.contexts.join(', ') + '. Reply with annotation.reply and explicit result resource versions.' : '');
|
|
969
1155
|
// The Agent's session ended with its process: after Rho restarts, and on a machine that
|
|
970
1156
|
// never ran the session a project was copied from. The messages Rho kept are the record, so
|
|
971
1157
|
// the message runs in a new session told what was said before. Nothing is resent as a new
|
|
@@ -1071,7 +1257,7 @@ export class Conversations {
|
|
|
1071
1257
|
const given = shown.get(file);
|
|
1072
1258
|
const reason = !enabled ? 'Descriptions by a model are switched off; the facts are kept.'
|
|
1073
1259
|
: c.backend === 'codex-acp' ? 'Descriptions are written with Claude Code; this session\'s Agent is Codex.'
|
|
1074
|
-
: c.backend
|
|
1260
|
+
: generic(c.backend) ? 'Descriptions are written with Claude Code; this session\'s Agent is another ACP Agent.'
|
|
1075
1261
|
: !this.ports.describe ? 'No model is set up to describe files.' : null;
|
|
1076
1262
|
const keep = (why) => { if (record.reason !== why) {
|
|
1077
1263
|
record.state = record.sentence ? 'described' : 'facts';
|
package/dist/discovery.js
CHANGED
|
@@ -4,10 +4,10 @@ import { protocol } from './protocol.js';
|
|
|
4
4
|
import { schemaClosure } from './protocol-discovery.js';
|
|
5
5
|
const size = (v) => Buffer.byteLength(JSON.stringify(v)) + 64;
|
|
6
6
|
export const applicationEffects = Object.fromEntries([
|
|
7
|
-
...['extension.list', 'extension.describe', 'extension.content', 'extension.read', 'protocol.describe', 'protocol.validate', 'project.entries', 'capability.list', 'skill.list', 'skill.read', 'action.lookup', 'action.observe', 'action.output', 'action.list', 'composition.read', 'observation.snapshot', 'interaction.selection.read', 'workspace.observe', 'task.map.read'].map(m => [m, 'read']),
|
|
8
|
-
...['composition.put', 'view.close', 'capability.configure', 'composition.apply', 'composition.undo', 'view.protection.set', 'interaction.selection.set', 'selection.set', 'workspace.enhance', 'task.map.declare', 'task.map.present', 'task.map.respond'].map(m => [m, 'metadata']),
|
|
7
|
+
...['session.instructions', 'extension.list', 'extension.describe', 'extension.content', 'extension.read', 'protocol.describe', 'protocol.validate', 'project.entries', 'capability.list', 'skill.list', 'skill.read', 'action.lookup', 'action.observe', 'action.output', 'action.list', 'composition.read', 'observation.snapshot', 'interaction.selection.read', 'workspace.observe', 'task.map.read'].map(m => [m, 'read']),
|
|
8
|
+
...['composition.put', 'view.close', 'capability.configure', 'composition.apply', 'composition.undo', 'composition.delete', 'view.protection.set', 'interaction.selection.set', 'selection.set', 'workspace.enhance', 'task.map.declare', 'task.map.present', 'task.map.respond'].map(m => [m, 'metadata']),
|
|
9
9
|
...['workspace.save', 'workspace.export', 'workspace.restore', 'action.export'].map(m => [m, 'file']),
|
|
10
|
-
...['extension.call', 'extension.install', 'extension.manage', 'action.retry', 'action.cancel'].map(m => [m, 'native']),
|
|
10
|
+
...['extension.call', 'extension.install', 'extension.manage', 'extension.market.change', 'action.retry', 'action.cancel'].map(m => [m, 'native']),
|
|
11
11
|
]);
|
|
12
12
|
const rhoSummary = 'The project in Rho: its folder\'s entries, managed runs, the views on the person\'s screen and what is selected in them, the task map, and which plugins are on.';
|
|
13
13
|
const metadata = new Set(['capability.index', 'capability.resolve', 'binding.activity.append', 'task.routing.put', 'feedback.submit', 'view.observe', 'view.observations.read']);
|
|
@@ -35,6 +35,18 @@ export declare const extensionOperations: {
|
|
|
35
35
|
required: string[];
|
|
36
36
|
additionalProperties: boolean;
|
|
37
37
|
};
|
|
38
|
+
'extension.market.change': {
|
|
39
|
+
oneOf: {
|
|
40
|
+
type: string;
|
|
41
|
+
properties: Record<string, unknown>;
|
|
42
|
+
required: string[];
|
|
43
|
+
additionalProperties: boolean;
|
|
44
|
+
}[];
|
|
45
|
+
type: string;
|
|
46
|
+
properties: Record<string, unknown>;
|
|
47
|
+
required: string[];
|
|
48
|
+
additionalProperties: boolean;
|
|
49
|
+
};
|
|
38
50
|
'extension.manage': {
|
|
39
51
|
type: string;
|
|
40
52
|
properties: Record<string, unknown>;
|
|
@@ -49,6 +61,12 @@ export declare const extensionResponses: {
|
|
|
49
61
|
required: string[];
|
|
50
62
|
additionalProperties: boolean;
|
|
51
63
|
};
|
|
64
|
+
'extension.market.change': {
|
|
65
|
+
type: string;
|
|
66
|
+
properties: Record<string, unknown>;
|
|
67
|
+
required: string[];
|
|
68
|
+
additionalProperties: boolean;
|
|
69
|
+
};
|
|
52
70
|
'extension.describe': {
|
|
53
71
|
type: string;
|
|
54
72
|
properties: Record<string, unknown>;
|
|
@@ -88,6 +106,7 @@ export declare const extensionResponses: {
|
|
|
88
106
|
};
|
|
89
107
|
export declare const extensionDescriptions: {
|
|
90
108
|
'extension.list': string;
|
|
109
|
+
'extension.market.change': string;
|
|
91
110
|
'extension.describe': string;
|
|
92
111
|
'extension.content': string;
|
|
93
112
|
'extension.call': string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Installed third-party packages are application assembly, independent of the
|
|
2
2
|
// functional owners compiled into Rho. This protocol never imports their code.
|
|
3
|
+
import { marketEntrySchema } from './extension-markets.js';
|
|
3
4
|
const str = { type: 'string', minLength: 1, maxLength: 1024 };
|
|
4
5
|
const id = { type: 'string', pattern: '^[a-z0-9][a-z0-9._-]{0,95}$' };
|
|
5
6
|
const sha = { type: 'string', pattern: '^[a-f0-9]{64}$' };
|
|
@@ -7,13 +8,19 @@ const object = (properties, required = Object.keys(properties)) => ({ type: 'obj
|
|
|
7
8
|
const json = { type: 'object', additionalProperties: true };
|
|
8
9
|
const project = { projectId: str };
|
|
9
10
|
const selected = { ...project, pluginId: id, expectedDigest: sha };
|
|
11
|
+
const marketOrigin = object({ id, title: str, url: str, digest: sha, keywords: { type: 'array', maxItems: 16, items: { type: 'string', minLength: 1, maxLength: 64 } } }, ['id', 'title', 'url', 'digest']);
|
|
12
|
+
const marketSelection = object({ id, expectedDigest: sha, pluginId: id });
|
|
13
|
+
const revision = { type: 'integer', minimum: 1 };
|
|
14
|
+
const marketChange = { ...object({ action: { enum: ['add', 'refresh', 'remove'] }, url: str, id, expectedRevision: revision }, ['action']), oneOf: [object({ action: { const: 'add' }, url: str }), object({ action: { enum: ['refresh', 'remove'] }, id, expectedRevision: revision })] };
|
|
15
|
+
const market = object({ id, url: str, title: str, revision, digest: sha, fetchedAt: str, checkedAt: str, error: { type: ['string', 'null'] }, plugins: { type: 'array', items: marketEntrySchema } });
|
|
10
16
|
export const extensionOperations = {
|
|
11
17
|
'extension.list': object(project, []),
|
|
12
18
|
'extension.describe': object({ ...project, pluginId: id }),
|
|
13
19
|
'extension.content': object({ ...selected, kind: { enum: ['skill', 'view'] }, entryId: id }),
|
|
14
20
|
'extension.call': object({ ...selected, operation: id, input: json, changeId: { type: 'string', pattern: '^[A-Za-z0-9._:@-]{1,128}$' } }),
|
|
15
21
|
'extension.read': object({ ...project, invocationId: sha }),
|
|
16
|
-
'extension.install': object({ source: str, replace: id, expectedDigest: sha }, ['source']),
|
|
22
|
+
'extension.install': object({ source: str, replace: id, expectedDigest: sha, market: marketSelection }, ['source']),
|
|
23
|
+
'extension.market.change': marketChange,
|
|
17
24
|
'extension.manage': object({ pluginId: id, expectedDigest: sha, action: { enum: ['enable', 'disable', 'remove'] } }),
|
|
18
25
|
};
|
|
19
26
|
const array = (items) => ({ type: 'array', items });
|
|
@@ -23,15 +30,16 @@ const manifest = { ...object({ apiVersion: { const: 1 }, id, title: text, descri
|
|
|
23
30
|
operations: array({ ...object({ id, title: text, description: text, effect: { enum: ['read', 'native'] }, methods: array(text), inputSchema: json }), additionalProperties: true }),
|
|
24
31
|
skills: array(entry), views: array(entry),
|
|
25
32
|
}, ['apiVersion', 'id', 'title', 'description', 'operations', 'skills', 'views']), additionalProperties: true };
|
|
26
|
-
const installed = { id, name: text, version: text, title: text, description: text, digest: sha, source: text, sourceCommit: text, enabled: { type: 'boolean' }, installedAt: text };
|
|
27
|
-
const installedRequired = Object.keys(installed).filter(key =>
|
|
33
|
+
const installed = { id, name: text, version: text, title: text, description: text, digest: sha, source: text, sourceCommit: text, market: marketOrigin, enabled: { type: 'boolean' }, installedAt: text };
|
|
34
|
+
const installedRequired = Object.keys(installed).filter(key => !['sourceCommit', 'market'].includes(key));
|
|
28
35
|
const descriptor = object({ ...installed, manifest }, [...installedRequired, 'manifest']);
|
|
29
36
|
const receipt = object({ invocationId: sha, caller: text, projectId: text, pluginId: id, packageDigest: sha, operation: id, requestDigest: sha,
|
|
30
37
|
state: { enum: ['pending', 'completed', 'unknown'] }, startedAt: text, endedAt: text, value: {}, error: text,
|
|
31
38
|
delegations: array(object({ method: text, request: {}, state: { enum: ['pending', 'completed', 'unknown'] }, value: {}, error: text }, ['method', 'request', 'state'])),
|
|
32
39
|
}, ['invocationId', 'caller', 'projectId', 'pluginId', 'packageDigest', 'operation', 'requestDigest', 'state', 'startedAt']);
|
|
33
40
|
export const extensionResponses = {
|
|
34
|
-
'extension.list': object({ plugins: array(object(installed, installedRequired)), directory: array(object({ id, title: text, version: text, description: text, source: text, repository: text, needs: text })), limits: json }),
|
|
41
|
+
'extension.list': object({ plugins: array(object(installed, installedRequired)), directory: array(object({ id, title: text, version: text, description: text, source: text, repository: text, needs: text })), markets: array(market), limits: json }),
|
|
42
|
+
'extension.market.change': object({ markets: array(market) }),
|
|
35
43
|
'extension.describe': descriptor,
|
|
36
44
|
'extension.install': descriptor,
|
|
37
45
|
'extension.content': object({ pluginId: id, packageDigest: sha, entryId: id, kind: { enum: ['skill', 'view'] }, title: text, text }),
|
|
@@ -40,7 +48,8 @@ export const extensionResponses = {
|
|
|
40
48
|
'extension.manage': object({ pluginId: id, action: { enum: ['enable', 'disable', 'remove'] }, retained: text }),
|
|
41
49
|
};
|
|
42
50
|
export const extensionDescriptions = {
|
|
43
|
-
'extension.list': 'List
|
|
51
|
+
'extension.list': 'List installed third-party packages, the bundled directory and retained remote market indexes. Listing makes no network request and starts no plugin or runtime. Use extension.describe for installed operation schemas and Skills.',
|
|
52
|
+
'extension.market.change': 'Local person/operator: add an HTTPS plugin market index, refresh it explicitly, or remove its source. Failed refresh retains the last valid index; removal preserves installed plugins. Listing never refreshes a source.',
|
|
44
53
|
'extension.describe': 'Read an installed third-party package manifest, operation input schemas, required Rho methods, Skills and views. Read its Skill with extension.content before use. Disabled packages cannot run.',
|
|
45
54
|
'extension.content': 'Read one installed plugin Skill or standalone HTML view at its selected package digest. Reading starts no plugin or runtime.',
|
|
46
55
|
'extension.call': 'Call an enabled third-party operation at the described package digest, under the caller and project. The package runs trusted native code. Non-read calls retain the original changeId: repeat observes that invocation, including unknown outcomes, without re-execution. Completion is not scientific validity.',
|
|
@@ -48,5 +57,5 @@ export const extensionDescriptions = {
|
|
|
48
57
|
'extension.install': 'Local person/operator: install a trusted prebuilt npm package, local folder, tarball, or Git source. Lifecycle scripts are disabled. Replacement stages and checks the new package before switching; use replace and expectedDigest for updates.',
|
|
49
58
|
'extension.manage': 'Local person/operator: enable, disable or remove an installed third-party package at the expected digest. Existing native tasks, R sessions, project files and retained receipts are preserved.',
|
|
50
59
|
};
|
|
51
|
-
export const extensionManagement = ['extension.install', 'extension.manage'];
|
|
60
|
+
export const extensionManagement = ['extension.install', 'extension.manage', 'extension.market.change'];
|
|
52
61
|
export const extensionMethods = Object.keys(extensionOperations);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export declare const marketLimits: {
|
|
2
|
+
maxMarkets: number;
|
|
3
|
+
maxMarketBytes: number;
|
|
4
|
+
maxMarketEntries: number;
|
|
5
|
+
marketTimeoutMs: number;
|
|
6
|
+
};
|
|
7
|
+
export declare const marketEntrySchema: {
|
|
8
|
+
type: string;
|
|
9
|
+
properties: Record<string, unknown>;
|
|
10
|
+
required: string[];
|
|
11
|
+
additionalProperties: boolean;
|
|
12
|
+
};
|
|
13
|
+
export declare const marketIndexSchema: {
|
|
14
|
+
type: string;
|
|
15
|
+
properties: Record<string, unknown>;
|
|
16
|
+
required: string[];
|
|
17
|
+
additionalProperties: boolean;
|
|
18
|
+
};
|
|
19
|
+
export interface MarketEntry {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
version: string;
|
|
23
|
+
description: string;
|
|
24
|
+
source: string;
|
|
25
|
+
repository: string;
|
|
26
|
+
needs: string;
|
|
27
|
+
keywords?: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface ExtensionMarket {
|
|
30
|
+
id: string;
|
|
31
|
+
url: string;
|
|
32
|
+
title: string;
|
|
33
|
+
revision: number;
|
|
34
|
+
digest: string;
|
|
35
|
+
fetchedAt: string;
|
|
36
|
+
checkedAt: string;
|
|
37
|
+
error: string | null;
|
|
38
|
+
plugins: MarketEntry[];
|
|
39
|
+
}
|
|
40
|
+
export interface MarketSelection {
|
|
41
|
+
id: string;
|
|
42
|
+
expectedDigest: string;
|
|
43
|
+
pluginId: string;
|
|
44
|
+
}
|
|
45
|
+
export type MarketChange = {
|
|
46
|
+
action: 'add';
|
|
47
|
+
url: string;
|
|
48
|
+
} | {
|
|
49
|
+
action: 'refresh' | 'remove';
|
|
50
|
+
id: string;
|
|
51
|
+
expectedRevision: number;
|
|
52
|
+
};
|
|
53
|
+
export declare class ExtensionMarkets {
|
|
54
|
+
readonly directory: string;
|
|
55
|
+
readonly limits: typeof marketLimits;
|
|
56
|
+
private values;
|
|
57
|
+
constructor(directory: string, limits: typeof marketLimits);
|
|
58
|
+
init(): Promise<void>;
|
|
59
|
+
list(): ExtensionMarket[];
|
|
60
|
+
private save;
|
|
61
|
+
private fetch;
|
|
62
|
+
private observe;
|
|
63
|
+
change(input: MarketChange): Promise<ExtensionMarket[]>;
|
|
64
|
+
select(selection: MarketSelection, source: string): {
|
|
65
|
+
entry: MarketEntry;
|
|
66
|
+
origin: {
|
|
67
|
+
id: string;
|
|
68
|
+
title: string;
|
|
69
|
+
url: string;
|
|
70
|
+
digest: string;
|
|
71
|
+
keywords: string[];
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
}
|