@moxxy/plugin-provider-acp 0.0.0-stage → 0.45.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/LICENSE +21 -0
- package/dist/acp-session.d.ts +201 -0
- package/dist/acp-session.d.ts.map +1 -0
- package/dist/acp-session.js +812 -0
- package/dist/acp-session.js.map +1 -0
- package/dist/agent-provider.d.ts +71 -0
- package/dist/agent-provider.d.ts.map +1 -0
- package/dist/agent-provider.js +203 -0
- package/dist/agent-provider.js.map +1 -0
- package/dist/agents.d.ts +23 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +21 -0
- package/dist/agents.js.map +1 -0
- package/dist/constants.d.ts +31 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +31 -0
- package/dist/constants.js.map +1 -0
- package/dist/executable.d.ts +47 -0
- package/dist/executable.d.ts.map +1 -0
- package/dist/executable.js +106 -0
- package/dist/executable.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/login.d.ts +48 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +180 -0
- package/dist/login.js.map +1 -0
- package/dist/opencode-api.d.ts +32 -0
- package/dist/opencode-api.d.ts.map +1 -0
- package/dist/opencode-api.js +86 -0
- package/dist/opencode-api.js.map +1 -0
- package/dist/opencode-helpers.d.ts +49 -0
- package/dist/opencode-helpers.d.ts.map +1 -0
- package/dist/opencode-helpers.js +175 -0
- package/dist/opencode-helpers.js.map +1 -0
- package/dist/protocol.d.ts +143 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +7 -0
- package/dist/protocol.js.map +1 -0
- package/dist/rules.d.ts +47 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +84 -0
- package/dist/rules.js.map +1 -0
- package/dist/translate.d.ts +50 -0
- package/dist/translate.d.ts.map +1 -0
- package/dist/translate.js +290 -0
- package/dist/translate.js.map +1 -0
- package/package.json +81 -3
- package/src/acp-session.ts +892 -0
- package/src/agent-cli.test.ts +76 -0
- package/src/agent-provider.test.ts +1137 -0
- package/src/agent-provider.ts +234 -0
- package/src/agents.ts +37 -0
- package/src/constants.ts +36 -0
- package/src/event-coverage.test.ts +65 -0
- package/src/executable.test.ts +89 -0
- package/src/executable.ts +143 -0
- package/src/fake-opencode.testkit.ts +755 -0
- package/src/fixtures/acp-session-updates.json +19 -0
- package/src/index.test.ts +69 -0
- package/src/index.ts +83 -0
- package/src/login.test.ts +155 -0
- package/src/login.ts +222 -0
- package/src/opencode-api.ts +105 -0
- package/src/opencode-helpers.test.ts +76 -0
- package/src/opencode-helpers.ts +195 -0
- package/src/protocol.ts +104 -0
- package/src/rules.test.ts +107 -0
- package/src/rules.ts +102 -0
- package/src/translate.test.ts +253 -0
- package/src/translate.ts +318 -0
- package/README.md +0 -3
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { isContextWindowSize, waitFor } from '@moxxy/sdk';
|
|
3
|
+
import { Inbox, JsonRpcError, JsonRpcLineClient, agentModeFor, earlierConversationNote, idleToolsHandler, moxxyToolsHandler, resumeAgentSession, sameToolNames, skillsForAgent, startMoxxyMcpServer, untilStopped, withoutOwnMcpServers, } from '@moxxy/agent-connection-kit';
|
|
4
|
+
import { ACP_PROTOCOL_VERSION, } from './protocol.js';
|
|
5
|
+
import { AcpTurnTranslator, toAcpPrompt, toTokenUsage } from './translate.js';
|
|
6
|
+
import { abortOpenCodeSession, watchOpenCodeEvents } from './opencode-api.js';
|
|
7
|
+
import { OpenCodeHelpers } from './opencode-helpers.js';
|
|
8
|
+
import { askingConfig, asksOutsideProject, runsWithoutAsking } from './rules.js';
|
|
9
|
+
const MAX_STDERR = 2048;
|
|
10
|
+
const MAX_DECLINE_FOLLOW_UPS = 3;
|
|
11
|
+
const STOPPED = { allow: false, reason: 'The person stopped the turn.' };
|
|
12
|
+
const CANCELLED = { outcome: { outcome: 'cancelled' } };
|
|
13
|
+
const PLAN_MODE = 'plan';
|
|
14
|
+
const BUILD_MODE = 'build';
|
|
15
|
+
const SIGN_IN_EXPIRED = /token refresh failed|\b401\b|unauthori[sz]ed/i;
|
|
16
|
+
const CONFIG_CONTENT_ENV = 'OPENCODE_CONFIG_CONTENT';
|
|
17
|
+
/** ACP's error for a resource, here a session, that does not exist. */
|
|
18
|
+
const RESOURCE_NOT_FOUND = -32002;
|
|
19
|
+
/** How many pages of the agent's sessions Moxxy reads before it stops looking. */
|
|
20
|
+
const MAX_SESSION_PAGES = 50;
|
|
21
|
+
/** How long the first turn waits for the agent's list of commands; OpenCode sends it as the session opens. */
|
|
22
|
+
const COMMANDS_WAIT_MS = 2_000;
|
|
23
|
+
/** How long the agent stays quiet on ACP after OpenCode says its session is idle before a turn it started by itself counts as over (ACP lags its HTTP API). */
|
|
24
|
+
const SETTLE_MS = 300;
|
|
25
|
+
/** Steps kept per subagent whose start the conversation has not seen yet. */
|
|
26
|
+
const MAX_HELD_STEPS = 64;
|
|
27
|
+
/** ACP updates that show the agent at work: one outside any turn means it started one by itself. */
|
|
28
|
+
const WORK_UPDATES = new Set(['agent_message_chunk', 'agent_thought_chunk', 'tool_call', 'tool_call_update', 'plan']);
|
|
29
|
+
const CLIENT_VERSION = createRequire(import.meta.url)('../package.json').version;
|
|
30
|
+
export const CLIENT_CAPABILITIES = { fs: { readTextFile: false, writeTextFile: false }, terminal: false };
|
|
31
|
+
/** Moxxy tools the agent lacks, lent over MCP for this turn. */
|
|
32
|
+
export function toolsLentTo(agent, tools) {
|
|
33
|
+
return tools.filter((tool) => !agent.ownTools.has(tool.name) && !tool.name.startsWith('memory_'));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* One running ACP agent with one session for one Moxxy conversation. Every
|
|
37
|
+
* message the person sends is the next prompt of the same session, so the
|
|
38
|
+
* agent keeps its context between turns. The agent's notifications, its
|
|
39
|
+
* permission asks and Moxxy tool calls over MCP are queued together and
|
|
40
|
+
* handled in order, so the log shows a call before the decision about it.
|
|
41
|
+
*/
|
|
42
|
+
export class AcpAgentSession {
|
|
43
|
+
settings;
|
|
44
|
+
mcp;
|
|
45
|
+
inbox = new Inbox();
|
|
46
|
+
client;
|
|
47
|
+
connection;
|
|
48
|
+
/** A replacement agent is loading the session: what it replays is history. */
|
|
49
|
+
loading = false;
|
|
50
|
+
alive = true;
|
|
51
|
+
closed = false;
|
|
52
|
+
inTurn = false;
|
|
53
|
+
sessionId = '';
|
|
54
|
+
options = [];
|
|
55
|
+
/** The agent mode the person's setup starts in, restored after plan mode. */
|
|
56
|
+
ownMode = BUILD_MODE;
|
|
57
|
+
/** Why the person declined, for the next message when the turn ended first. */
|
|
58
|
+
pendingNotes = [];
|
|
59
|
+
/** The Moxxy tools the agent is shown: every one of the conversation, the same in every mode. */
|
|
60
|
+
listed;
|
|
61
|
+
/** The Moxxy skills the agent is offered. */
|
|
62
|
+
offered;
|
|
63
|
+
/** Names of the agent's own commands and skills, as it last announced them. */
|
|
64
|
+
ownCommands = [];
|
|
65
|
+
commandsHeard = false;
|
|
66
|
+
commandsAwaited = false;
|
|
67
|
+
commandWakers = new Set();
|
|
68
|
+
/** The session is new while the conversation may already have run: its first prompt carries the conversation so far. */
|
|
69
|
+
newSession;
|
|
70
|
+
stderr = '';
|
|
71
|
+
/** No turn has shown the conversation yet how the agent runs. */
|
|
72
|
+
fresh = true;
|
|
73
|
+
/** The agent's own rules, once read; `null` when it cannot say. */
|
|
74
|
+
ownRules;
|
|
75
|
+
ownMcpServers;
|
|
76
|
+
/** This agent was started to ask before what its own rules allow. */
|
|
77
|
+
asking = false;
|
|
78
|
+
/** Whether Moxxy answers the agent's asks for the person in this turn. */
|
|
79
|
+
autoApprove = false;
|
|
80
|
+
/** Auto-approve is on because Moxxy turned it on, not because the person chose it. */
|
|
81
|
+
adopted = false;
|
|
82
|
+
/** The agent can list its sessions, which tells a gone session from a failed load. */
|
|
83
|
+
canList = false;
|
|
84
|
+
/** A replacement agent could not load the session: the next turn starts the agent again. */
|
|
85
|
+
broken = false;
|
|
86
|
+
/** OpenCode's events, read while its HTTP API is up. */
|
|
87
|
+
watch;
|
|
88
|
+
/** The subagents OpenCode runs, read from its events. */
|
|
89
|
+
helpers = new OpenCodeHelpers(() => this.sessionId);
|
|
90
|
+
/** Subagents whose start the conversation has seen; the steps of others wait for it. */
|
|
91
|
+
shownHelpers = new Set();
|
|
92
|
+
heldSteps = new Map();
|
|
93
|
+
/** The background work last reported, to report it again only when it changes. */
|
|
94
|
+
shownWork = [];
|
|
95
|
+
/** OpenCode started a turn by itself and the conversation was asked for one to show it. */
|
|
96
|
+
agentTurnPending = false;
|
|
97
|
+
/** A turn reads the one OpenCode started by itself. */
|
|
98
|
+
readingOwnTurn = false;
|
|
99
|
+
/** OpenCode's session works now, as its events last said. */
|
|
100
|
+
agentBusy = false;
|
|
101
|
+
settleTimer;
|
|
102
|
+
/** This turn showed the account limit reached. */
|
|
103
|
+
limitShown = false;
|
|
104
|
+
static async start(settings) {
|
|
105
|
+
const mcp = await startMoxxyMcpServer({ name: settings.agent.mcpServerName });
|
|
106
|
+
let session;
|
|
107
|
+
try {
|
|
108
|
+
session = new AcpAgentSession(settings, mcp, await settings.start({ cwd: settings.cwd }));
|
|
109
|
+
await session.open();
|
|
110
|
+
return session;
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
if (session)
|
|
114
|
+
await session.close();
|
|
115
|
+
else
|
|
116
|
+
await mcp.close();
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
constructor(settings, mcp, connection) {
|
|
121
|
+
this.settings = settings;
|
|
122
|
+
this.mcp = mcp;
|
|
123
|
+
this.connection = connection;
|
|
124
|
+
this.listed = toolsLentTo(settings.agent, settings.tools ?? []);
|
|
125
|
+
this.offered = this.skillsFor(settings.skills);
|
|
126
|
+
this.mcp.setHandler(idleToolsHandler(this.listed, this.offered));
|
|
127
|
+
this.client = this.listen(connection);
|
|
128
|
+
}
|
|
129
|
+
/** The client for a running agent, its notifications and asks queued for the turn. */
|
|
130
|
+
listen(connection) {
|
|
131
|
+
const client = new JsonRpcLineClient({ readable: connection.readable, writable: connection.writable, jsonrpc: true });
|
|
132
|
+
connection.stderr?.on('data', (data) => {
|
|
133
|
+
this.stderr = (this.stderr + String(data)).slice(-MAX_STDERR);
|
|
134
|
+
});
|
|
135
|
+
// Only the agent in use ending ends the conversation's agent; a replaced one is expected to.
|
|
136
|
+
const ended = () => {
|
|
137
|
+
if (connection === this.connection)
|
|
138
|
+
this.ended();
|
|
139
|
+
};
|
|
140
|
+
connection.readable.once('end', ended);
|
|
141
|
+
connection.readable.once('close', ended);
|
|
142
|
+
// Outside a turn nothing is shown: that drops the history `session/load` replays.
|
|
143
|
+
// The agent at work outside one started a turn by itself, which the conversation is asked to show.
|
|
144
|
+
client.onNotification((method, params) => {
|
|
145
|
+
if (method !== 'session/update')
|
|
146
|
+
return;
|
|
147
|
+
const commands = announcedCommands(params);
|
|
148
|
+
if (commands) {
|
|
149
|
+
this.ownCommands = commands;
|
|
150
|
+
this.commandsHeard = true;
|
|
151
|
+
this.commandWakers.forEach((wake) => wake());
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (this.loading)
|
|
155
|
+
return;
|
|
156
|
+
if (!this.inTurn && !this.agentTurnPending && this.startsOwnTurn(params))
|
|
157
|
+
this.askForTurn('A background task finished');
|
|
158
|
+
if (!this.inTurn && !this.agentTurnPending)
|
|
159
|
+
return;
|
|
160
|
+
this.inbox.push({ kind: 'note', params });
|
|
161
|
+
if (this.settleTimer)
|
|
162
|
+
this.settleSoon();
|
|
163
|
+
});
|
|
164
|
+
client.onRequest((method, params) => this.inTurn || this.agentTurnPending ? new Promise((resolve, reject) => this.inbox.push({ kind: 'request', method, params, resolve, reject })) : refusal(method));
|
|
165
|
+
return client;
|
|
166
|
+
}
|
|
167
|
+
get isAlive() {
|
|
168
|
+
return this.alive && !this.broken;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The agent lists its commands just after its session opens, maybe after the
|
|
172
|
+
* turn began: the first turn waits for that list, an agent that never sends
|
|
173
|
+
* one only briefly.
|
|
174
|
+
*/
|
|
175
|
+
async hearCommands(signal) {
|
|
176
|
+
if (this.commandsHeard || this.commandsAwaited)
|
|
177
|
+
return;
|
|
178
|
+
this.commandsAwaited = true;
|
|
179
|
+
await waitFor(() => this.commandsHeard || undefined, {
|
|
180
|
+
wakeOn: [
|
|
181
|
+
(wake) => {
|
|
182
|
+
this.commandWakers.add(wake);
|
|
183
|
+
return () => this.commandWakers.delete(wake);
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
timeoutMs: COMMANDS_WAIT_MS,
|
|
187
|
+
signal,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/** A Moxxy skill named like one of the agent's own commands or skills is left to the agent's. */
|
|
191
|
+
skillsFor(skills) {
|
|
192
|
+
return skillsForAgent(skills ?? [], this.listed.map((tool) => tool.name), this.ownCommands);
|
|
193
|
+
}
|
|
194
|
+
/** Whether this agent was started where the turn runs (a running session cannot move). */
|
|
195
|
+
fits(settings) {
|
|
196
|
+
return settings.cwd === this.settings.cwd;
|
|
197
|
+
}
|
|
198
|
+
async *turn(req) {
|
|
199
|
+
// A turn opened to show the one OpenCode started by itself: OpenCode is sent nothing.
|
|
200
|
+
if (req.continuesAgentTurn) {
|
|
201
|
+
if (this.agentTurnPending)
|
|
202
|
+
yield* this.ownTurn(req);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const mode = agentModeFor(req.mode);
|
|
206
|
+
this.inTurn = true;
|
|
207
|
+
// A turn OpenCode started by itself and not shown yet is read by this one: OpenCode answers this prompt after it.
|
|
208
|
+
this.agentTurnPending = false;
|
|
209
|
+
this.clearSettle();
|
|
210
|
+
this.limitShown = false;
|
|
211
|
+
const onAbort = () => {
|
|
212
|
+
this.inbox.push({ kind: 'stop' });
|
|
213
|
+
this.client.notify('session/cancel', { sessionId: this.sessionId });
|
|
214
|
+
};
|
|
215
|
+
req.signal.addEventListener('abort', onAbort, { once: true });
|
|
216
|
+
const all = withoutOwnMcpServers(toolsLentTo(this.settings.agent, req.allTools ?? req.tools), await this.mcpServers());
|
|
217
|
+
if (!sameToolNames(all, this.listed))
|
|
218
|
+
this.listed = all;
|
|
219
|
+
try {
|
|
220
|
+
yield { type: 'session', sessionId: this.sessionId };
|
|
221
|
+
this.autoApprove = req.autoApprove;
|
|
222
|
+
// Auto-approve OpenCode's own rules turned on, not the person: the
|
|
223
|
+
// person still answers what those rules ask (work outside the project).
|
|
224
|
+
// Once the person turned it off, auto-approve on again is their choice.
|
|
225
|
+
if (req.autoApproveFromAgent !== undefined)
|
|
226
|
+
this.adopted = req.autoApprove && req.autoApproveFromAgent;
|
|
227
|
+
else if (!req.autoApprove)
|
|
228
|
+
this.adopted = false;
|
|
229
|
+
// A new conversation shows how the person's agent really runs (one
|
|
230
|
+
// state); a conversation going on, or one that recorded a switch, keeps
|
|
231
|
+
// the auto-approve the person chose. In goal mode Moxxy answers for the person because of the goal,
|
|
232
|
+
// not because they chose auto-approve, so that waits.
|
|
233
|
+
if (this.fresh && mode !== 'goal') {
|
|
234
|
+
this.fresh = false;
|
|
235
|
+
const rules = await this.rules();
|
|
236
|
+
const ownAuto = rules !== undefined && runsWithoutAsking(rules);
|
|
237
|
+
if (this.settings.resume) {
|
|
238
|
+
// Told nothing of who turned it on, while the agent's own rules would have, it asks as if the person did not.
|
|
239
|
+
if (ownAuto && this.autoApprove && req.autoApproveFromAgent === undefined)
|
|
240
|
+
this.adopted = true;
|
|
241
|
+
}
|
|
242
|
+
else if (ownAuto && !this.autoApprove && !req.autoApproveRecorded) {
|
|
243
|
+
this.autoApprove = true;
|
|
244
|
+
this.adopted = true;
|
|
245
|
+
yield { type: 'auto_approve', enabled: true };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (!this.autoApprove && mode !== 'goal')
|
|
249
|
+
await this.askFromNowOn();
|
|
250
|
+
// OpenCode reads the Moxxy tools when the session opens and keeps that
|
|
251
|
+
// list: when the conversation gained or lost a tool or a skill, it reads them again first.
|
|
252
|
+
await this.hearCommands(req.signal);
|
|
253
|
+
this.offered = this.skillsFor(req.skills);
|
|
254
|
+
this.mcp.setHandler(moxxyToolsHandler(this.listed, { runTool: (call) => new Promise((resolve) => this.inbox.push({ kind: 'moxxy-tool', call, resolve })) }, this.offered));
|
|
255
|
+
await this.mcp.announceTools({ signal: req.signal });
|
|
256
|
+
await this.configure(req, mode);
|
|
257
|
+
// A new session of the agent's own gets the whole conversation; one it goes on in, the turns it was away for.
|
|
258
|
+
const note = this.newSession ? earlierConversationNote(this.newSession, req.earlierConversation?.()) : req.missedTurns?.();
|
|
259
|
+
this.newSession = undefined;
|
|
260
|
+
if (note)
|
|
261
|
+
this.pendingNotes.unshift(note);
|
|
262
|
+
const notes = this.pendingNotes.map((text) => ({ type: 'text', text }));
|
|
263
|
+
this.pendingNotes = [];
|
|
264
|
+
let input = [...notes, ...toAcpPrompt(req.prompt)];
|
|
265
|
+
// The agent's answer to an ask carries no reason, and it ends its turn on
|
|
266
|
+
// a refusal: the person's reason is its next message, in the same turn.
|
|
267
|
+
// An agent that keeps asking for what was refused gets the reason with
|
|
268
|
+
// the person's next message instead.
|
|
269
|
+
for (let followUps = 0;; followUps += 1) {
|
|
270
|
+
if (req.signal.aborted)
|
|
271
|
+
return;
|
|
272
|
+
const declines = [];
|
|
273
|
+
const finished = yield* this.prompt(input, req, declines);
|
|
274
|
+
if (!finished || declines.length === 0 || followUps === MAX_DECLINE_FOLLOW_UPS) {
|
|
275
|
+
this.pendingNotes.push(...declines);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
input = declines.map((text) => ({ type: 'text', text }));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
if (!req.signal.aborted)
|
|
283
|
+
yield { type: 'error', message: this.alive ? this.failureMessage(err) : this.endedMessage(), retryable: false };
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
req.signal.removeEventListener('abort', onAbort);
|
|
287
|
+
this.mcp.setHandler(idleToolsHandler(this.listed, this.offered));
|
|
288
|
+
this.inTurn = false;
|
|
289
|
+
this.dismissQueued();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/** Stop one subagent OpenCode runs in the background (`AgentBackgroundWork.id`); `false` when it runs no such work. */
|
|
293
|
+
async stopWork(workId) {
|
|
294
|
+
const api = this.connection.api;
|
|
295
|
+
const session = api ? this.helpers.stop(workId) : undefined;
|
|
296
|
+
if (!api || !session)
|
|
297
|
+
return false;
|
|
298
|
+
const stopped = await abortOpenCodeSession(api, this.settings.cwd, session);
|
|
299
|
+
if (!stopped)
|
|
300
|
+
this.helpers.unstop(workId);
|
|
301
|
+
return stopped;
|
|
302
|
+
}
|
|
303
|
+
async close() {
|
|
304
|
+
if (this.closed)
|
|
305
|
+
return;
|
|
306
|
+
this.closed = true;
|
|
307
|
+
this.alive = false;
|
|
308
|
+
this.unwatch();
|
|
309
|
+
this.client.close();
|
|
310
|
+
this.connection.close();
|
|
311
|
+
this.dismissQueued();
|
|
312
|
+
await this.mcp.close();
|
|
313
|
+
}
|
|
314
|
+
/** The agent's own MCP servers that are on, read once. */
|
|
315
|
+
async mcpServers() {
|
|
316
|
+
const { readMcpServers, cwd } = this.settings;
|
|
317
|
+
if (this.ownMcpServers === undefined) {
|
|
318
|
+
const servers = readMcpServers ? ((await readMcpServers({ cwd }).catch(() => undefined)) ?? []) : [];
|
|
319
|
+
this.ownMcpServers = servers.filter((name) => name !== this.settings.agent.mcpServerName);
|
|
320
|
+
}
|
|
321
|
+
return this.ownMcpServers;
|
|
322
|
+
}
|
|
323
|
+
/** The rules of the agent the person runs, read once. */
|
|
324
|
+
async rules() {
|
|
325
|
+
const { readRules, cwd } = this.settings;
|
|
326
|
+
if (this.ownRules === undefined)
|
|
327
|
+
this.ownRules = readRules ? ((await readRules({ cwd, agent: this.ownMode })) ?? null) : null;
|
|
328
|
+
return this.ownRules ?? undefined;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* With auto-approve off the agent asks before what its own rules allow:
|
|
332
|
+
* it is started again with those rules turned into asks (for this process
|
|
333
|
+
* only, nothing written to its config) and loads the same session. Once
|
|
334
|
+
* started so it stays, and Moxxy answers its asks while auto-approve is on.
|
|
335
|
+
*/
|
|
336
|
+
async askFromNowOn() {
|
|
337
|
+
if (this.asking)
|
|
338
|
+
return;
|
|
339
|
+
const rules = await this.rules();
|
|
340
|
+
if (!rules || !runsWithoutAsking(rules))
|
|
341
|
+
return;
|
|
342
|
+
const env = { [CONFIG_CONTENT_ENV]: askingConfig(this.ownMode, rules, process.env[CONFIG_CONTENT_ENV]) };
|
|
343
|
+
const replaced = { connection: this.connection, client: this.client };
|
|
344
|
+
const next = await this.settings.start({ cwd: this.settings.cwd, env });
|
|
345
|
+
// The subagents of the OpenCode replaced end with it.
|
|
346
|
+
this.unwatch();
|
|
347
|
+
this.connection = next;
|
|
348
|
+
this.client = this.listen(next);
|
|
349
|
+
replaced.client.close();
|
|
350
|
+
replaced.connection.close();
|
|
351
|
+
this.asking = true;
|
|
352
|
+
this.loading = true;
|
|
353
|
+
try {
|
|
354
|
+
await this.open(this.sessionId);
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
this.broken = true;
|
|
358
|
+
throw err;
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
this.loading = false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async open(resume = this.settings.resume) {
|
|
365
|
+
const init = await this.client.request('initialize', {
|
|
366
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
367
|
+
clientCapabilities: CLIENT_CAPABILITIES,
|
|
368
|
+
clientInfo: { name: 'moxxy', title: 'Moxxy', version: CLIENT_VERSION },
|
|
369
|
+
});
|
|
370
|
+
this.canList = init.agentCapabilities?.sessionCapabilities?.list !== undefined;
|
|
371
|
+
// OpenCode serves its HTTP API once it answered `initialize`.
|
|
372
|
+
this.watchEvents();
|
|
373
|
+
const { cwd } = this.settings;
|
|
374
|
+
const mcpServers = [
|
|
375
|
+
{
|
|
376
|
+
type: 'http',
|
|
377
|
+
name: this.settings.agent.mcpServerName,
|
|
378
|
+
url: this.mcp.url,
|
|
379
|
+
headers: [{ name: 'Authorization', value: this.mcp.authorization }],
|
|
380
|
+
},
|
|
381
|
+
];
|
|
382
|
+
let session;
|
|
383
|
+
if (resume) {
|
|
384
|
+
const resumed = await resumeAgentSession({
|
|
385
|
+
label: this.settings.agent.label,
|
|
386
|
+
load: () => this.client.request('session/load', { sessionId: resume, cwd, mcpServers }),
|
|
387
|
+
gone: (error) => this.sessionGone(error, resume),
|
|
388
|
+
});
|
|
389
|
+
if (resumed.kind === 'loaded') {
|
|
390
|
+
session = resumed.session;
|
|
391
|
+
this.sessionId = resume;
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
this.newSession = 'lost';
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
this.newSession = 'new';
|
|
399
|
+
}
|
|
400
|
+
if (!session) {
|
|
401
|
+
session = await this.client.request('session/new', { cwd, mcpServers });
|
|
402
|
+
if (!session.sessionId)
|
|
403
|
+
throw new Error(`${this.settings.agent.label} started no session.`);
|
|
404
|
+
this.sessionId = session.sessionId;
|
|
405
|
+
}
|
|
406
|
+
this.options = session.configOptions ?? [];
|
|
407
|
+
const mode = this.option('mode');
|
|
408
|
+
if (mode && mode.currentValue !== PLAN_MODE)
|
|
409
|
+
this.ownMode = mode.currentValue;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Whether a session the agent failed to load no longer exists. OpenCode 1.18
|
|
413
|
+
* answers a missing session as it answers a failure ("OpenCode service
|
|
414
|
+
* failure"), so its session list decides; an agent that can say neither
|
|
415
|
+
* keeps the session as existing, and nothing replaces it.
|
|
416
|
+
*/
|
|
417
|
+
async sessionGone(error, sessionId) {
|
|
418
|
+
if (error instanceof JsonRpcError && error.code === RESOURCE_NOT_FOUND)
|
|
419
|
+
return true;
|
|
420
|
+
if (!this.canList)
|
|
421
|
+
return false;
|
|
422
|
+
try {
|
|
423
|
+
let cursor;
|
|
424
|
+
for (let page = 0; page < MAX_SESSION_PAGES; page += 1) {
|
|
425
|
+
const listed = await this.client.request('session/list', { cwd: this.settings.cwd, ...(cursor ? { cursor } : {}) });
|
|
426
|
+
if (listed.sessions.some((session) => session.sessionId === sessionId))
|
|
427
|
+
return false;
|
|
428
|
+
if (!listed.nextCursor)
|
|
429
|
+
return true;
|
|
430
|
+
cursor = listed.nextCursor;
|
|
431
|
+
}
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/** How full OpenCode's context is (`used`) and the window it runs the turn's model with (`size`), from its `usage_update`. */
|
|
439
|
+
contextOf(update, requested) {
|
|
440
|
+
if (update.sessionUpdate !== 'usage_update')
|
|
441
|
+
return [];
|
|
442
|
+
const { used, size } = update;
|
|
443
|
+
const model = requested || this.option('model')?.currentValue;
|
|
444
|
+
return [
|
|
445
|
+
...(typeof used === 'number' && Number.isSafeInteger(used) && used >= 0 ? [{ type: 'context_used', tokens: used }] : []),
|
|
446
|
+
...(typeof model === 'string' && model !== '' && isContextWindowSize(size) ? [{ type: 'context_window', model, tokens: size }] : []),
|
|
447
|
+
];
|
|
448
|
+
}
|
|
449
|
+
/** Model, effort and mode for this turn, changed on the session only where they differ. */
|
|
450
|
+
async configure(req, mode) {
|
|
451
|
+
const model = this.option('model');
|
|
452
|
+
if (req.model && model && model.currentValue !== req.model) {
|
|
453
|
+
await this.set(model.id, req.model).catch((err) => {
|
|
454
|
+
throw new Error(`${this.settings.agent.label} could not switch to the model ${req.model}: ${messageOf(err)}`);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
const effort = typeof req.reasoning === 'object' ? req.reasoning.effort : undefined;
|
|
458
|
+
const level = this.option('thought_level');
|
|
459
|
+
if (effort && level && level.currentValue !== effort && offers(level, effort))
|
|
460
|
+
await this.set(level.id, effort);
|
|
461
|
+
const agentMode = this.option('mode');
|
|
462
|
+
const wanted = mode === 'plan' ? PLAN_MODE : this.ownMode;
|
|
463
|
+
if (agentMode && agentMode.currentValue !== wanted && offers(agentMode, wanted))
|
|
464
|
+
await this.set(agentMode.id, wanted);
|
|
465
|
+
}
|
|
466
|
+
option(category) {
|
|
467
|
+
return this.options.find((option) => option.category === category) ?? this.options.find((option) => option.id === category);
|
|
468
|
+
}
|
|
469
|
+
async set(configId, value) {
|
|
470
|
+
const response = await this.client.request('session/set_config_option', { sessionId: this.sessionId, configId, value });
|
|
471
|
+
this.options = response.configOptions ?? this.options.map((option) => (option.id === configId ? { ...option, currentValue: value } : option));
|
|
472
|
+
}
|
|
473
|
+
/** One prompt, read until the agent answers it. False when the turn is over without it (stopped, failed). */
|
|
474
|
+
async *prompt(input, req, declines) {
|
|
475
|
+
const translator = new AcpTurnTranslator(this.settings.agent.moxxyToolPrefix, this.settings.cwd);
|
|
476
|
+
this.client.request('session/prompt', { sessionId: this.sessionId, prompt: input }).then((result) => this.inbox.push({ kind: 'done', result }), (error) => this.inbox.push({ kind: 'failed', error }));
|
|
477
|
+
for (;;) {
|
|
478
|
+
const item = await this.inbox.next();
|
|
479
|
+
switch (item.kind) {
|
|
480
|
+
case 'ended':
|
|
481
|
+
yield* translator.finish();
|
|
482
|
+
yield { type: 'error', message: this.endedMessage(), retryable: false };
|
|
483
|
+
return false;
|
|
484
|
+
case 'stop':
|
|
485
|
+
// The agent answers the prompt as cancelled.
|
|
486
|
+
continue;
|
|
487
|
+
case 'idle':
|
|
488
|
+
continue;
|
|
489
|
+
case 'moxxy-tool':
|
|
490
|
+
item.resolve(await req.host.runTool(item.call));
|
|
491
|
+
continue;
|
|
492
|
+
case 'request':
|
|
493
|
+
yield* this.answer(item, req, translator, declines);
|
|
494
|
+
continue;
|
|
495
|
+
case 'api':
|
|
496
|
+
yield* item.events;
|
|
497
|
+
continue;
|
|
498
|
+
case 'note':
|
|
499
|
+
yield* this.shown(item.params, translator, req);
|
|
500
|
+
continue;
|
|
501
|
+
case 'done':
|
|
502
|
+
yield* translator.finish();
|
|
503
|
+
if (item.result.usage)
|
|
504
|
+
yield { type: 'usage', usage: toTokenUsage(item.result.usage) };
|
|
505
|
+
return item.result.stopReason !== 'cancelled' && !req.signal.aborted;
|
|
506
|
+
case 'failed':
|
|
507
|
+
yield* translator.finish();
|
|
508
|
+
if (!this.alive) {
|
|
509
|
+
yield { type: 'error', message: this.endedMessage(), retryable: false };
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
throw item.error;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/** What the agent asked of Moxxy, answered with the turn's host. */
|
|
517
|
+
async *answer(item, req, translator, declines) {
|
|
518
|
+
if (item.method !== 'session/request_permission') {
|
|
519
|
+
item.reject(new JsonRpcError(-32601, `Moxxy does not handle ${item.method}.`));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
const ask = (item.params ?? {});
|
|
524
|
+
const callId = ask.toolCall.toolCallId;
|
|
525
|
+
const { call, events } = translator.callFor(ask.toolCall);
|
|
526
|
+
yield* this.withHeldSteps(events);
|
|
527
|
+
// With auto-approve on Moxxy answers for the person, as OpenCode's own auto mode does; standing rules still apply.
|
|
528
|
+
// Work outside the project OpenCode asks about even on its defaults: while
|
|
529
|
+
// Moxxy turned auto-approve on, not the person, the person still answers.
|
|
530
|
+
const askPerson = this.adopted && asksOutsideProject(ask.toolCall.rawInput);
|
|
531
|
+
const answering = askPerson ? { askPerson: true } : this.autoApprove ? { policyOnly: true } : {};
|
|
532
|
+
const decision = await untilStopped(req.host.approve({ callId, name: call.name, input: call.input, ...answering }), req.signal, STOPPED);
|
|
533
|
+
if (req.signal.aborted) {
|
|
534
|
+
item.resolve(CANCELLED);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
item.resolve(permissionOutcome(decision, ask.options));
|
|
538
|
+
if (!decision.allow)
|
|
539
|
+
declines.push(declineNote(call, decision.reason));
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
item.reject(err instanceof Error ? err : new Error(String(err)));
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* A turn OpenCode started by itself: a background subagent finished and
|
|
547
|
+
* OpenCode prompted its session with the result. It is read until OpenCode's
|
|
548
|
+
* session is idle again; its asks and Moxxy tool calls are answered as in any turn.
|
|
549
|
+
*/
|
|
550
|
+
async *ownTurn(req) {
|
|
551
|
+
this.inTurn = true;
|
|
552
|
+
this.readingOwnTurn = true;
|
|
553
|
+
this.limitShown = false;
|
|
554
|
+
const onAbort = () => {
|
|
555
|
+
this.inbox.push({ kind: 'stop' });
|
|
556
|
+
this.client.notify('session/cancel', { sessionId: this.sessionId });
|
|
557
|
+
};
|
|
558
|
+
req.signal.addEventListener('abort', onAbort, { once: true });
|
|
559
|
+
this.mcp.setHandler(moxxyToolsHandler(this.listed, { runTool: (call) => new Promise((resolve) => this.inbox.push({ kind: 'moxxy-tool', call, resolve })) }, this.offered));
|
|
560
|
+
const translator = new AcpTurnTranslator(this.settings.agent.moxxyToolPrefix, this.settings.cwd);
|
|
561
|
+
// Already over (OpenCode's ACP lags its events), it is read to the end of what came.
|
|
562
|
+
if (!this.agentBusy)
|
|
563
|
+
this.settleSoon();
|
|
564
|
+
try {
|
|
565
|
+
yield { type: 'session', sessionId: this.sessionId };
|
|
566
|
+
for (;;) {
|
|
567
|
+
const item = await this.inbox.next();
|
|
568
|
+
switch (item.kind) {
|
|
569
|
+
case 'ended':
|
|
570
|
+
yield* translator.finish();
|
|
571
|
+
yield { type: 'error', message: this.endedMessage(), retryable: false };
|
|
572
|
+
return;
|
|
573
|
+
case 'idle':
|
|
574
|
+
yield* translator.finish();
|
|
575
|
+
return;
|
|
576
|
+
case 'moxxy-tool':
|
|
577
|
+
item.resolve(await req.host.runTool(item.call));
|
|
578
|
+
continue;
|
|
579
|
+
case 'request': {
|
|
580
|
+
const declines = [];
|
|
581
|
+
yield* this.answer(item, req, translator, declines);
|
|
582
|
+
this.pendingNotes.push(...declines);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
case 'api':
|
|
586
|
+
yield* item.events;
|
|
587
|
+
continue;
|
|
588
|
+
case 'note':
|
|
589
|
+
yield* this.shown(item.params, translator, req);
|
|
590
|
+
continue;
|
|
591
|
+
default:
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
finally {
|
|
597
|
+
req.signal.removeEventListener('abort', onAbort);
|
|
598
|
+
this.mcp.setHandler(idleToolsHandler(this.listed, this.offered));
|
|
599
|
+
this.agentTurnPending = false;
|
|
600
|
+
this.readingOwnTurn = false;
|
|
601
|
+
this.inTurn = false;
|
|
602
|
+
this.clearSettle();
|
|
603
|
+
this.dismissQueued();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/** One ACP update of the conversation's session as agent events. */
|
|
607
|
+
*shown(params, translator, req) {
|
|
608
|
+
const { sessionId, update } = (params ?? {});
|
|
609
|
+
if (sessionId !== this.sessionId || !update)
|
|
610
|
+
return;
|
|
611
|
+
yield* this.contextOf(update, req.model);
|
|
612
|
+
yield* this.withHeldSteps(translator.update(update));
|
|
613
|
+
}
|
|
614
|
+
/** The events, each subagent's start followed by the steps it took before the conversation saw it start. */
|
|
615
|
+
*withHeldSteps(events) {
|
|
616
|
+
for (const event of events) {
|
|
617
|
+
yield event;
|
|
618
|
+
if (event.type !== 'subagent_started')
|
|
619
|
+
continue;
|
|
620
|
+
this.shownHelpers.add(event.id);
|
|
621
|
+
const held = this.heldSteps.get(event.id) ?? [];
|
|
622
|
+
this.heldSteps.delete(event.id);
|
|
623
|
+
if (this.helpers.inBackground(event.id))
|
|
624
|
+
this.report(held);
|
|
625
|
+
else
|
|
626
|
+
yield* held;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/** Start reading the events of the OpenCode in use, when it serves its HTTP API. */
|
|
630
|
+
watchEvents() {
|
|
631
|
+
this.watch?.close();
|
|
632
|
+
const api = this.connection.api;
|
|
633
|
+
if (!api)
|
|
634
|
+
return;
|
|
635
|
+
this.watch = watchOpenCodeEvents(api, this.settings.cwd, (event) => this.onEvent(event), {
|
|
636
|
+
// Without events the end of a turn OpenCode started by itself would never come.
|
|
637
|
+
onDisconnected: () => {
|
|
638
|
+
if (this.agentTurnPending || this.readingOwnTurn)
|
|
639
|
+
this.settleSoon();
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
/** Stop reading OpenCode's events: its subagents end with it. */
|
|
644
|
+
unwatch() {
|
|
645
|
+
this.watch?.close();
|
|
646
|
+
this.watch = undefined;
|
|
647
|
+
this.clearSettle();
|
|
648
|
+
this.helpers.clear();
|
|
649
|
+
this.shownHelpers.clear();
|
|
650
|
+
this.heldSteps.clear();
|
|
651
|
+
this.agentBusy = false;
|
|
652
|
+
this.reportWork([]);
|
|
653
|
+
}
|
|
654
|
+
onEvent(event) {
|
|
655
|
+
if (this.closed)
|
|
656
|
+
return;
|
|
657
|
+
const news = this.helpers.take(event);
|
|
658
|
+
for (const step of news.steps)
|
|
659
|
+
this.deliver(step);
|
|
660
|
+
if (news.workChanged)
|
|
661
|
+
this.reportWork();
|
|
662
|
+
if (news.status) {
|
|
663
|
+
this.agentBusy = news.status === 'busy';
|
|
664
|
+
if (this.agentBusy)
|
|
665
|
+
this.clearSettle();
|
|
666
|
+
else if (this.agentTurnPending || this.readingOwnTurn)
|
|
667
|
+
this.settleSoon();
|
|
668
|
+
}
|
|
669
|
+
// OpenCode would wait for the reset, hours maybe, retrying: the turn ends on the limit, as Claude's does.
|
|
670
|
+
if (news.limitReached && this.inTurn && !this.limitShown) {
|
|
671
|
+
this.limitShown = true;
|
|
672
|
+
this.inbox.push({ kind: 'api', events: [{ type: 'account_limit', limit: { state: 'reached' } }] });
|
|
673
|
+
this.client.notify('session/cancel', { sessionId: this.sessionId });
|
|
674
|
+
}
|
|
675
|
+
if (news.finished && !this.inTurn) {
|
|
676
|
+
const { label, failed, workId } = news.finished;
|
|
677
|
+
this.askForTurn(`${label} ${failed ? 'failed' : 'finished'} in the background`, workId);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* A subagent's step: in the turn that waits for it, or recorded at once for
|
|
682
|
+
* one working in the background. A step of a subagent whose start the
|
|
683
|
+
* conversation has not seen waits for it.
|
|
684
|
+
*/
|
|
685
|
+
deliver(step) {
|
|
686
|
+
const id = 'id' in step && typeof step.id === 'string' ? step.id : undefined;
|
|
687
|
+
if (id === undefined)
|
|
688
|
+
return;
|
|
689
|
+
if (!this.shownHelpers.has(id)) {
|
|
690
|
+
const held = this.heldSteps.get(id) ?? [];
|
|
691
|
+
if (held.length < MAX_HELD_STEPS)
|
|
692
|
+
held.push(step);
|
|
693
|
+
this.heldSteps.set(id, held);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (this.inTurn && !this.helpers.inBackground(id))
|
|
697
|
+
this.inbox.push({ kind: 'api', events: [step] });
|
|
698
|
+
else
|
|
699
|
+
this.report([step]);
|
|
700
|
+
}
|
|
701
|
+
/** Whether an ACP update outside any turn shows the agent at work in the conversation's session. */
|
|
702
|
+
startsOwnTurn(params) {
|
|
703
|
+
const { sessionId, update } = (params ?? {});
|
|
704
|
+
return this.watch !== undefined && sessionId === this.sessionId && typeof update?.sessionUpdate === 'string' && WORK_UPDATES.has(update.sessionUpdate);
|
|
705
|
+
}
|
|
706
|
+
askForTurn(name, workId) {
|
|
707
|
+
if (this.closed || this.agentTurnPending)
|
|
708
|
+
return;
|
|
709
|
+
this.agentTurnPending = true;
|
|
710
|
+
// Already idle by the time ACP shows its work, the turn is over once the agent is quiet.
|
|
711
|
+
if (!this.agentBusy)
|
|
712
|
+
this.settleSoon();
|
|
713
|
+
this.settings.onAgentTurn?.(name, workId);
|
|
714
|
+
}
|
|
715
|
+
/** The turn OpenCode started by itself is over once its session is idle and ACP has been quiet a moment. */
|
|
716
|
+
settleSoon() {
|
|
717
|
+
this.clearSettle();
|
|
718
|
+
this.settleTimer = setTimeout(() => {
|
|
719
|
+
this.settleTimer = undefined;
|
|
720
|
+
this.inbox.push({ kind: 'idle' });
|
|
721
|
+
}, SETTLE_MS);
|
|
722
|
+
}
|
|
723
|
+
clearSettle() {
|
|
724
|
+
if (this.settleTimer)
|
|
725
|
+
clearTimeout(this.settleTimer);
|
|
726
|
+
this.settleTimer = undefined;
|
|
727
|
+
}
|
|
728
|
+
/** What OpenCode runs in the background now; reported when it changes. */
|
|
729
|
+
reportWork(work = this.helpers.running()) {
|
|
730
|
+
if (sameWork(work, this.shownWork))
|
|
731
|
+
return;
|
|
732
|
+
this.shownWork = work;
|
|
733
|
+
this.report([{ type: 'background_work', work }]);
|
|
734
|
+
}
|
|
735
|
+
report(events) {
|
|
736
|
+
if (events.length > 0)
|
|
737
|
+
this.settings.onActivity?.(events);
|
|
738
|
+
}
|
|
739
|
+
failureMessage(err) {
|
|
740
|
+
const message = messageOf(err);
|
|
741
|
+
const label = this.settings.agent.label;
|
|
742
|
+
if (SIGN_IN_EXPIRED.test(message)) {
|
|
743
|
+
return `${label} is signed out of the model's provider (${message}). Run \`${this.settings.agent.loginCommand}\` and try again.`;
|
|
744
|
+
}
|
|
745
|
+
return err instanceof JsonRpcError ? `${label}: ${message}` : message;
|
|
746
|
+
}
|
|
747
|
+
ended() {
|
|
748
|
+
if (!this.alive)
|
|
749
|
+
return;
|
|
750
|
+
this.alive = false;
|
|
751
|
+
this.inbox.push({ kind: 'ended' });
|
|
752
|
+
}
|
|
753
|
+
endedMessage() {
|
|
754
|
+
const stderr = this.stderr.trim();
|
|
755
|
+
const stopped = `${this.settings.agent.label} stopped.`;
|
|
756
|
+
return stderr ? `${stopped}\n${stderr}` : stopped;
|
|
757
|
+
}
|
|
758
|
+
/** Answer whatever is still queued once no turn can: the agent must never wait on Moxxy forever. */
|
|
759
|
+
dismissQueued() {
|
|
760
|
+
for (const item of this.inbox.drain()) {
|
|
761
|
+
if (item.kind === 'ended')
|
|
762
|
+
this.inbox.push(item);
|
|
763
|
+
else if (item.kind === 'api')
|
|
764
|
+
this.report(item.events.filter((event) => event.type !== 'account_limit'));
|
|
765
|
+
else if (item.kind === 'moxxy-tool')
|
|
766
|
+
item.resolve({ ok: false, message: 'No Moxxy turn is running, so the tool cannot run now.' });
|
|
767
|
+
else if (item.kind === 'request') {
|
|
768
|
+
try {
|
|
769
|
+
item.resolve(refusal(item.method));
|
|
770
|
+
}
|
|
771
|
+
catch (err) {
|
|
772
|
+
item.reject(err instanceof Error ? err : new Error(String(err)));
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
/** What Moxxy tells the agent when nobody is in the conversation to decide. */
|
|
779
|
+
function refusal(method) {
|
|
780
|
+
if (method === 'session/request_permission')
|
|
781
|
+
return CANCELLED;
|
|
782
|
+
throw new JsonRpcError(-32601, `Moxxy does not handle ${method}.`);
|
|
783
|
+
}
|
|
784
|
+
function sameWork(a, b) {
|
|
785
|
+
return a.length === b.length && a.every((work, i) => work.id === b[i]?.id && work.label === b[i]?.label);
|
|
786
|
+
}
|
|
787
|
+
function permissionOutcome(decision, options) {
|
|
788
|
+
const find = (...kinds) => kinds.flatMap((kind) => options.filter((option) => option.kind === kind))[0];
|
|
789
|
+
const chosen = !decision.allow ? find('reject_once', 'reject_always') : decision.always ? find('allow_always', 'allow_once') : find('allow_once', 'allow_always');
|
|
790
|
+
return chosen ? { outcome: { outcome: 'selected', optionId: chosen.optionId } } : CANCELLED;
|
|
791
|
+
}
|
|
792
|
+
function offers(option, value) {
|
|
793
|
+
return option.options === undefined || option.options.some((candidate) => candidate.value === value);
|
|
794
|
+
}
|
|
795
|
+
function declineNote(call, reason) {
|
|
796
|
+
const input = (call.input ?? {});
|
|
797
|
+
const what = typeof input.command === 'string' ? `${call.name} \`${input.command}\``
|
|
798
|
+
: typeof input.file_path === 'string' ? `${call.name} (${input.file_path})`
|
|
799
|
+
: call.name;
|
|
800
|
+
return `The person declined ${what}. Their reason: ${reason}`;
|
|
801
|
+
}
|
|
802
|
+
function messageOf(err) {
|
|
803
|
+
return err instanceof Error ? err.message : String(err);
|
|
804
|
+
}
|
|
805
|
+
/** The names in an `available_commands_update`, the agent's commands and skills; `undefined` for any other update. */
|
|
806
|
+
function announcedCommands(params) {
|
|
807
|
+
const update = params?.update;
|
|
808
|
+
if (!update || update.sessionUpdate !== 'available_commands_update' || !Array.isArray(update.availableCommands))
|
|
809
|
+
return undefined;
|
|
810
|
+
return update.availableCommands.flatMap((command) => (typeof command.name === 'string' ? [command.name] : []));
|
|
811
|
+
}
|
|
812
|
+
//# sourceMappingURL=acp-session.js.map
|