hoomanjs 1.40.1 → 1.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -3
- package/dist/acp/acp-agent.d.ts +16 -3
- package/dist/acp/acp-agent.js +371 -177
- package/dist/acp/acp-agent.js.map +1 -1
- package/dist/acp/commands.d.ts +6 -3
- package/dist/acp/commands.js +6 -24
- package/dist/acp/commands.js.map +1 -1
- package/dist/acp/meta/vscode.d.ts +10 -0
- package/dist/acp/meta/vscode.js +16 -0
- package/dist/acp/meta/vscode.js.map +1 -0
- package/dist/acp/questions.d.ts +18 -0
- package/dist/acp/questions.js +80 -0
- package/dist/acp/questions.js.map +1 -0
- package/dist/acp/session-config.d.ts +3 -1
- package/dist/acp/session-config.js +20 -8
- package/dist/acp/session-config.js.map +1 -1
- package/dist/acp/sessions/replay.js +117 -1
- package/dist/acp/sessions/replay.js.map +1 -1
- package/dist/acp/sessions/store.d.ts +55 -16
- package/dist/acp/sessions/store.js +174 -71
- package/dist/acp/sessions/store.js.map +1 -1
- package/dist/acp/utils/tool-kind.js +1 -1
- package/dist/acp/utils/tool-kind.js.map +1 -1
- package/dist/acp/utils/tool-result-content.d.ts +1 -0
- package/dist/acp/utils/tool-result-content.js +28 -1
- package/dist/acp/utils/tool-result-content.js.map +1 -1
- package/dist/chat/app.d.ts +3 -1
- package/dist/chat/app.js +65 -24
- package/dist/chat/app.js.map +1 -1
- package/dist/chat/components/BottomChrome.d.ts +12 -3
- package/dist/chat/components/BottomChrome.js +2 -2
- package/dist/chat/components/BottomChrome.js.map +1 -1
- package/dist/chat/components/ChromePicker.d.ts +5 -1
- package/dist/chat/components/ChromePicker.js +6 -2
- package/dist/chat/components/ChromePicker.js.map +1 -1
- package/dist/chat/components/QuestionPrompt.d.ts +8 -0
- package/dist/chat/components/QuestionPrompt.js +41 -0
- package/dist/chat/components/QuestionPrompt.js.map +1 -0
- package/dist/chat/components/StatusBar.d.ts +8 -3
- package/dist/chat/components/StatusBar.js +55 -26
- package/dist/chat/components/StatusBar.js.map +1 -1
- package/dist/chat/index.d.ts +2 -1
- package/dist/chat/index.js +1 -3
- package/dist/chat/index.js.map +1 -1
- package/dist/chat/questions.d.ts +24 -0
- package/dist/chat/questions.js +71 -0
- package/dist/chat/questions.js.map +1 -0
- package/dist/cli.js +13 -10
- package/dist/cli.js.map +1 -1
- package/dist/core/agent/index.d.ts +3 -0
- package/dist/core/agent/index.js +11 -6
- package/dist/core/agent/index.js.map +1 -1
- package/dist/core/agent/session-title-plugin.d.ts +27 -0
- package/dist/core/agent/session-title-plugin.js +77 -0
- package/dist/core/agent/session-title-plugin.js.map +1 -0
- package/dist/core/config.d.ts +30 -1
- package/dist/core/config.js +2 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/index.d.ts +12 -0
- package/dist/core/index.js +7 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/models/azure.js +5 -1
- package/dist/core/models/azure.js.map +1 -1
- package/dist/core/models/groq.js +5 -1
- package/dist/core/models/groq.js.map +1 -1
- package/dist/core/models/moonshot.js +5 -1
- package/dist/core/models/moonshot.js.map +1 -1
- package/dist/core/models/openai.js +6 -1
- package/dist/core/models/openai.js.map +1 -1
- package/dist/core/models/openrouter.js +5 -1
- package/dist/core/models/openrouter.js.map +1 -1
- package/dist/core/models/types.d.ts +44 -0
- package/dist/core/models/types.js +15 -0
- package/dist/core/models/types.js.map +1 -1
- package/dist/core/models/usage.d.ts +10 -0
- package/dist/core/models/usage.js +45 -0
- package/dist/core/models/usage.js.map +1 -0
- package/dist/core/models/xai.js +5 -1
- package/dist/core/models/xai.js.map +1 -1
- package/dist/core/modes/definitions.js +2 -2
- package/dist/core/modes/definitions.js.map +1 -1
- package/dist/core/prompts/harness/behaviour.md +3 -15
- package/dist/core/prompts/harness/communication.md +3 -16
- package/dist/core/prompts/harness/execution.md +6 -13
- package/dist/core/prompts/harness/guardrails.md +6 -12
- package/dist/core/prompts/static/ask-user.md +7 -0
- package/dist/core/prompts/static/daemon.md +4 -18
- package/dist/core/prompts/static/environment.md +2 -7
- package/dist/core/prompts/static/fetch.md +4 -30
- package/dist/core/prompts/static/filesystem.md +1 -1
- package/dist/core/prompts/static/identity.md +4 -19
- package/dist/core/prompts/static/planning.md +1 -9
- package/dist/core/prompts/static/subagents.md +4 -29
- package/dist/core/prompts/static/thinking.md +4 -24
- package/dist/core/prompts/static/todo.md +6 -32
- package/dist/core/prompts/system.js +5 -4
- package/dist/core/prompts/system.js.map +1 -1
- package/dist/core/sessions/generate-title.d.ts +10 -0
- package/dist/core/sessions/generate-title.js +113 -0
- package/dist/core/sessions/generate-title.js.map +1 -0
- package/dist/core/sessions/lazy-session-manager.js +0 -2
- package/dist/core/sessions/lazy-session-manager.js.map +1 -1
- package/dist/core/sessions/list-cli-sessions.js +13 -1
- package/dist/core/sessions/list-cli-sessions.js.map +1 -1
- package/dist/core/skills/built-in/hooman-channels/SKILL.md +11 -148
- package/dist/core/skills/built-in/hooman-channels/channels.md +117 -0
- package/dist/core/skills/built-in/hooman-coding/SKILL.md +49 -44
- package/dist/core/skills/built-in/hooman-coding/debugging.md +26 -0
- package/dist/core/skills/built-in/hooman-coding/new-project.md +23 -0
- package/dist/core/skills/built-in/hooman-config/SKILL.md +19 -257
- package/dist/core/skills/built-in/hooman-config/providers.md +171 -0
- package/dist/core/skills/built-in/hooman-config/search.md +39 -0
- package/dist/core/skills/built-in/hooman-mcp/SKILL.md +22 -153
- package/dist/core/skills/built-in/hooman-skills/SKILL.md +11 -147
- package/dist/core/skills/built-in/hooman-skills/authoring.md +80 -0
- package/dist/core/state/session-title.d.ts +17 -0
- package/dist/core/state/session-title.js +15 -0
- package/dist/core/state/session-title.js.map +1 -0
- package/dist/core/state/tool-approvals.js +3 -2
- package/dist/core/state/tool-approvals.js.map +1 -1
- package/dist/core/tools/ask-user.d.ts +38 -0
- package/dist/core/tools/ask-user.js +62 -0
- package/dist/core/tools/ask-user.js.map +1 -0
- package/dist/core/tools/filesystem.d.ts +26 -1
- package/dist/core/tools/filesystem.js +44 -23
- package/dist/core/tools/filesystem.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -1
- package/dist/core/tools/index.js +1 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/shell.d.ts +41 -0
- package/dist/core/tools/shell.js +8 -1
- package/dist/core/tools/shell.js.map +1 -1
- package/dist/core/tools/sleep.js +1 -1
- package/dist/core/tools/sleep.js.map +1 -1
- package/dist/core/utils/billing.d.ts +49 -0
- package/dist/core/utils/billing.js +295 -0
- package/dist/core/utils/billing.js.map +1 -0
- package/dist/core/utils/edit-replace.d.ts +6 -0
- package/dist/core/utils/edit-replace.js +88 -6
- package/dist/core/utils/edit-replace.js.map +1 -1
- package/dist/core/utils/paths.d.ts +1 -0
- package/dist/core/utils/paths.js +1 -0
- package/dist/core/utils/paths.js.map +1 -1
- package/dist/core/{models → utils}/reasoning-effort.d.ts +9 -0
- package/dist/core/utils/reasoning-effort.js.map +1 -0
- package/dist/daemon/index.d.ts +1 -1
- package/dist/daemon/index.js +1 -11
- package/dist/daemon/index.js.map +1 -1
- package/dist/exec/questions.d.ts +9 -0
- package/dist/exec/questions.js +50 -0
- package/dist/exec/questions.js.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/core/models/reasoning-effort.js.map +0 -1
- package/dist/core/prompts/static/bye.md +0 -15
- package/dist/core/prompts/static/sleep.md +0 -20
- package/dist/core/state/exit-request.d.ts +0 -14
- package/dist/core/state/exit-request.js +0 -16
- package/dist/core/state/exit-request.js.map +0 -1
- package/dist/core/tools/bye.d.ts +0 -4
- package/dist/core/tools/bye.js +0 -23
- package/dist/core/tools/bye.js.map +0 -1
- package/dist/core/tools/terminal-backend.d.ts +0 -41
- package/dist/core/tools/terminal-backend.js +0 -9
- package/dist/core/tools/terminal-backend.js.map +0 -1
- package/dist/core/tools/text-fs-backend.d.ts +0 -25
- package/dist/core/tools/text-fs-backend.js +0 -9
- package/dist/core/tools/text-fs-backend.js.map +0 -1
- /package/dist/core/{models → utils}/reasoning-effort.js +0 -0
package/dist/acp/acp-agent.js
CHANGED
|
@@ -3,24 +3,25 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { Readable, Writable } from "node:stream";
|
|
4
4
|
import { stdin, stdout } from "node:process";
|
|
5
5
|
import { agent as acpAgent, methods, ndJsonStream, PROTOCOL_VERSION, RequestError, } from "@agentclientprotocol/sdk";
|
|
6
|
-
import { isModelStreamEvent,
|
|
6
|
+
import { isModelStreamEvent, } from "@strands-agents/sdk";
|
|
7
7
|
import { bootstrap } from "../core/index.js";
|
|
8
8
|
import { applySessionMode } from "../core/agent/sync-tool-registry-mode.js";
|
|
9
9
|
import { MODE_DEFINITIONS } from "../core/modes/definitions.js";
|
|
10
10
|
import { DEFAULT_SESSION_MODE, isKnownSessionMode, } from "../core/modes/schema.js";
|
|
11
11
|
import { getModeState, setSessionMode } from "../core/state/session-mode.js";
|
|
12
12
|
import { isYoloEnabled, setYoloEnabled } from "../core/state/yolo.js";
|
|
13
|
+
import { ChatTurnSteeringController, createChatTurnSteeringIntervention, } from "../core/agent/turn-steering.js";
|
|
13
14
|
import { getAgentConversationManager, getAgentSessionManager, } from "../core/agent/index.js";
|
|
14
15
|
import { readBundledPrompt } from "../core/prompts/bundled.js";
|
|
15
|
-
import { formatModeNames } from "../core/modes/definitions.js";
|
|
16
16
|
import { modelProviders } from "../core/models/index.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
17
|
+
import { toAdditiveUsage } from "../core/models/usage.js";
|
|
18
|
+
import { computeUsageCostUsd, contextTokensFromUsage, resolveLlmBilling, } from "../core/utils/billing.js";
|
|
19
|
+
import { buildSessionConfigOptions, currentModelName, CONFIG_ID_EFFORT, CONFIG_ID_MODE, CONFIG_ID_MODEL, MODE_VALUE_YOLO, } from "./session-config.js";
|
|
20
|
+
import { activeProviderName, parseReasoningEffortArg, withReasoningEffort, } from "../core/utils/reasoning-effort.js";
|
|
21
|
+
import { ACP_SLASH_COMMANDS, parseAcpSlashCommand, } from "./commands.js";
|
|
20
22
|
import { ENTER_PLAN_MODE_TOOL, EXIT_PLAN_MODE_TOOL, } from "../core/state/tool-approvals.js";
|
|
21
23
|
import { runWithCwd } from "../core/utils/cwd-context.js";
|
|
22
24
|
import { runWithAgentMemoryScope } from "../core/memory/index.js";
|
|
23
|
-
import { consumeExitRequest, EXIT_REQUESTED_CODE, } from "../core/state/exit-request.js";
|
|
24
25
|
import { createSessionConfig, } from "../core/session-config.js";
|
|
25
26
|
import { acpSessionsRootPath } from "./utils/paths.js";
|
|
26
27
|
import { inferToolKind } from "./utils/tool-kind.js";
|
|
@@ -29,15 +30,20 @@ import { toolCallLocationsFromInput } from "./utils/tool-locations.js";
|
|
|
29
30
|
import { takeFileToolDisplay } from "../core/state/file-tool-display.js";
|
|
30
31
|
import { getTodoViewState } from "../core/state/todos.js";
|
|
31
32
|
import { UPDATE_TODOS_TOOL_NAME } from "../core/tools/todo.js";
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
33
|
+
import { setAskUserBackend } from "../core/tools/ask-user.js";
|
|
34
|
+
import { setTextFsBackend } from "../core/tools/filesystem.js";
|
|
35
|
+
import { setTerminalBackend, } from "../core/tools/shell.js";
|
|
34
36
|
import { createAcpToolApprovalIntervention } from "./approvals.js";
|
|
37
|
+
import { createAcpAskUserBackend } from "./questions.js";
|
|
35
38
|
import { extractAcpClientUserId } from "./meta/user-id.js";
|
|
39
|
+
import { extractAcpVscodeFlag } from "./meta/vscode.js";
|
|
36
40
|
import { deriveSessionTitleFromEcho } from "./sessions/title.js";
|
|
37
41
|
import { acpPromptEchoText, acpPromptToInvokeArgs } from "./prompt-invoke.js";
|
|
38
42
|
import { normalizeAcpSessionMcpServers } from "./mcp-servers.js";
|
|
39
43
|
import { replayConversationHistory } from "./sessions/replay.js";
|
|
40
|
-
import {
|
|
44
|
+
import { compactSessionIndex, deleteSessionEntry, migrateLegacySessionStore, patchSessionEntry, readSessionEntry, readSessionIndex, touchSessionEntry, writeSessionEntry, } from "./sessions/store.js";
|
|
45
|
+
import { FlatFileStorage } from "../core/sessions/flat-file-storage.js";
|
|
46
|
+
import { sessionsPath } from "../core/utils/paths.js";
|
|
41
47
|
/** Max sessions returned per `session/list` page. */
|
|
42
48
|
const LIST_PAGE_SIZE = 40;
|
|
43
49
|
/** Fallback when a session record is missing (never mutate). */
|
|
@@ -84,6 +90,36 @@ function decodeCursor(cursor) {
|
|
|
84
90
|
message: "Invalid pagination cursor",
|
|
85
91
|
});
|
|
86
92
|
}
|
|
93
|
+
/** Zeroed `Usage` accumulator for a fresh session's cumulative billing meter. */
|
|
94
|
+
function createEmptyUsage() {
|
|
95
|
+
return { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Accumulate `source` into `target` in place, mirroring the CLI TUI's
|
|
99
|
+
* cumulative billing meter (`src/chat/app.tsx`): sum every request's token
|
|
100
|
+
* usage over the session, including cache reads/writes which recur (and are
|
|
101
|
+
* billed) on every request. `source` must already be in the additive shape
|
|
102
|
+
* (see `toAdditiveUsage`), where cache reads are not part of `inputTokens`.
|
|
103
|
+
*/
|
|
104
|
+
function accumulateUsage(target, source) {
|
|
105
|
+
target.inputTokens += source.inputTokens ?? 0;
|
|
106
|
+
target.outputTokens += source.outputTokens ?? 0;
|
|
107
|
+
if (source.cacheReadInputTokens !== undefined ||
|
|
108
|
+
target.cacheReadInputTokens !== undefined) {
|
|
109
|
+
target.cacheReadInputTokens =
|
|
110
|
+
(target.cacheReadInputTokens ?? 0) + (source.cacheReadInputTokens ?? 0);
|
|
111
|
+
}
|
|
112
|
+
if (source.cacheWriteInputTokens !== undefined ||
|
|
113
|
+
target.cacheWriteInputTokens !== undefined) {
|
|
114
|
+
target.cacheWriteInputTokens =
|
|
115
|
+
(target.cacheWriteInputTokens ?? 0) + (source.cacheWriteInputTokens ?? 0);
|
|
116
|
+
}
|
|
117
|
+
target.totalTokens =
|
|
118
|
+
target.inputTokens +
|
|
119
|
+
target.outputTokens +
|
|
120
|
+
(target.cacheReadInputTokens ?? 0) +
|
|
121
|
+
(target.cacheWriteInputTokens ?? 0);
|
|
122
|
+
}
|
|
87
123
|
function assertAbsolutePath(value, field) {
|
|
88
124
|
const trimmed = value?.trim() ?? "";
|
|
89
125
|
if (!trimmed) {
|
|
@@ -123,23 +159,26 @@ function isCancellationError(err, cancelSignals) {
|
|
|
123
159
|
function toAcpStopReason(reason) {
|
|
124
160
|
switch (reason) {
|
|
125
161
|
case "cancelled":
|
|
162
|
+
case "interrupt":
|
|
126
163
|
return "cancelled";
|
|
127
164
|
case "maxTokens":
|
|
128
165
|
case "modelContextWindowExceeded":
|
|
166
|
+
case "limitOutputTokens":
|
|
167
|
+
case "limitTotalTokens":
|
|
129
168
|
return "max_tokens";
|
|
169
|
+
case "limitTurns":
|
|
170
|
+
return "max_turn_requests";
|
|
130
171
|
case "contentFiltered":
|
|
131
172
|
case "guardrailIntervened":
|
|
132
173
|
return "refusal";
|
|
133
174
|
case "endTurn":
|
|
134
175
|
case "toolUse":
|
|
135
176
|
case "stopSequence":
|
|
177
|
+
case "pauseTurn":
|
|
136
178
|
default:
|
|
137
179
|
return "end_turn";
|
|
138
180
|
}
|
|
139
181
|
}
|
|
140
|
-
function serializeAgentMessages(agent) {
|
|
141
|
-
return agent.messages.map((m) => m.toJSON());
|
|
142
|
-
}
|
|
143
182
|
/** Normalize a persisted/requested mode to a known id, else the default. */
|
|
144
183
|
function resolveSessionMode(mode) {
|
|
145
184
|
return mode && isKnownSessionMode(mode) ? mode : DEFAULT_SESSION_MODE;
|
|
@@ -165,6 +204,10 @@ function buildSessionModeState(currentModeId) {
|
|
|
165
204
|
export class HoomanAcpAgent {
|
|
166
205
|
#identity;
|
|
167
206
|
#acpRoot = acpSessionsRootPath();
|
|
207
|
+
/** Strands snapshot storage root (`<project>/sessions`); message source of truth. */
|
|
208
|
+
#snapshotsRoot = sessionsPath();
|
|
209
|
+
/** Resolves once the legacy store migration + index compaction have run. */
|
|
210
|
+
#storeReady;
|
|
168
211
|
#sessions = new Map();
|
|
169
212
|
/** Connection-scoped outbound context, captured in {@link onConnect}. */
|
|
170
213
|
#client = null;
|
|
@@ -175,6 +218,9 @@ export class HoomanAcpAgent {
|
|
|
175
218
|
#clientTerminalCap = false;
|
|
176
219
|
constructor(identity) {
|
|
177
220
|
this.#identity = identity;
|
|
221
|
+
this.#storeReady = migrateLegacySessionStore(this.#acpRoot, this.#snapshotsRoot)
|
|
222
|
+
.then(() => compactSessionIndex(this.#acpRoot))
|
|
223
|
+
.catch(() => undefined);
|
|
178
224
|
}
|
|
179
225
|
/** Bind connection-scoped lifecycle: capture the client + dispose on close. */
|
|
180
226
|
onConnect(connection) {
|
|
@@ -203,7 +249,6 @@ export class HoomanAcpAgent {
|
|
|
203
249
|
loadSession: true,
|
|
204
250
|
promptCapabilities: {
|
|
205
251
|
image: true,
|
|
206
|
-
audio: true,
|
|
207
252
|
embeddedContext: true,
|
|
208
253
|
},
|
|
209
254
|
mcpCapabilities: {
|
|
@@ -213,17 +258,24 @@ export class HoomanAcpAgent {
|
|
|
213
258
|
sessionCapabilities: {
|
|
214
259
|
list: {},
|
|
215
260
|
delete: {},
|
|
261
|
+
resume: {},
|
|
262
|
+
close: {},
|
|
216
263
|
},
|
|
217
264
|
},
|
|
218
265
|
};
|
|
219
266
|
}
|
|
220
267
|
async listSessions(params) {
|
|
221
|
-
|
|
268
|
+
await this.#storeReady;
|
|
269
|
+
const entries = await readSessionIndex(this.#acpRoot);
|
|
222
270
|
const sessions = [];
|
|
223
|
-
for (const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
271
|
+
for (const entry of entries.values()) {
|
|
272
|
+
if (!params.cwd || entry.cwd === params.cwd) {
|
|
273
|
+
sessions.push({
|
|
274
|
+
sessionId: entry.sessionId,
|
|
275
|
+
cwd: entry.cwd,
|
|
276
|
+
title: entry.title ?? null,
|
|
277
|
+
updatedAt: entry.updatedAt,
|
|
278
|
+
});
|
|
227
279
|
}
|
|
228
280
|
}
|
|
229
281
|
sessions.sort((a, b) => String(b.updatedAt ?? "").localeCompare(String(a.updatedAt ?? "")));
|
|
@@ -234,70 +286,135 @@ export class HoomanAcpAgent {
|
|
|
234
286
|
return { sessions: page, nextCursor };
|
|
235
287
|
}
|
|
236
288
|
async deleteSession(params) {
|
|
289
|
+
await this.#storeReady;
|
|
237
290
|
const record = this.#sessions.get(params.sessionId);
|
|
238
291
|
if (record) {
|
|
239
292
|
this.#sessions.delete(params.sessionId);
|
|
240
293
|
record.turnAbort?.abort();
|
|
241
294
|
await record.mcpDisconnect();
|
|
242
295
|
}
|
|
243
|
-
await
|
|
296
|
+
await deleteSessionEntry(this.#acpRoot, params.sessionId);
|
|
297
|
+
// Also drop the Strands snapshot (the conversation history itself).
|
|
298
|
+
await new FlatFileStorage(this.#snapshotsRoot).deleteSession({
|
|
299
|
+
sessionId: params.sessionId,
|
|
300
|
+
});
|
|
244
301
|
}
|
|
245
302
|
async newSession(params) {
|
|
303
|
+
await this.#storeReady;
|
|
246
304
|
assertAbsolutePath(params.cwd, "cwd");
|
|
247
305
|
const sessionId = crypto.randomUUID();
|
|
248
306
|
const clientUserId = extractAcpClientUserId(params._meta) ?? null;
|
|
249
307
|
const mcpServers = normalizeAcpSessionMcpServers(params.mcpServers);
|
|
308
|
+
const vscode = extractAcpVscodeFlag(params._meta);
|
|
250
309
|
const mode = DEFAULT_SESSION_MODE;
|
|
251
310
|
const now = new Date().toISOString();
|
|
252
|
-
const
|
|
311
|
+
const entry = {
|
|
312
|
+
sessionId,
|
|
253
313
|
cwd: params.cwd,
|
|
254
314
|
createdAt: now,
|
|
255
315
|
updatedAt: now,
|
|
256
316
|
title: null,
|
|
257
317
|
userId: clientUserId,
|
|
258
318
|
mcpServers,
|
|
319
|
+
...(vscode ? { vscode } : {}),
|
|
259
320
|
sessionMode: mode,
|
|
260
321
|
};
|
|
261
|
-
await
|
|
262
|
-
const record = await this.#bootstrapSession(sessionId, params.cwd, clientUserId ?? sessionId, mcpServers, mode);
|
|
322
|
+
await writeSessionEntry(this.#acpRoot, entry);
|
|
323
|
+
const record = await this.#bootstrapSession(sessionId, params.cwd, clientUserId ?? sessionId, mcpServers, mode, undefined, vscode);
|
|
263
324
|
this.#sessions.set(sessionId, record);
|
|
264
325
|
await this.#advertiseCommands(this.#requireClient(), sessionId);
|
|
265
326
|
return {
|
|
266
327
|
sessionId,
|
|
267
328
|
modes: buildSessionModeState(mode),
|
|
268
|
-
configOptions: buildSessionConfigOptions(record.config, mode),
|
|
329
|
+
configOptions: buildSessionConfigOptions(record.config, mode, isYoloEnabled(record.agent)),
|
|
269
330
|
};
|
|
270
331
|
}
|
|
271
332
|
async loadSession(params, client) {
|
|
333
|
+
const { record, mode } = await this.#reactivateSession({
|
|
334
|
+
sessionId: params.sessionId,
|
|
335
|
+
cwd: params.cwd,
|
|
336
|
+
mcpServers: params.mcpServers,
|
|
337
|
+
meta: params._meta,
|
|
338
|
+
}, client, { replayHistory: true });
|
|
339
|
+
return {
|
|
340
|
+
modes: buildSessionModeState(mode),
|
|
341
|
+
configOptions: buildSessionConfigOptions(record.config, mode, isYoloEnabled(record.agent)),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Handle `session/resume`: reactivate a persisted session's in-memory state
|
|
346
|
+
* (including its conversation history, so the turn continues correctly)
|
|
347
|
+
* without replaying that history to the client — the spec reserves the full
|
|
348
|
+
* transcript replay for `session/load`.
|
|
349
|
+
*/
|
|
350
|
+
async resumeSession(params, client) {
|
|
351
|
+
const { record, mode } = await this.#reactivateSession({
|
|
352
|
+
sessionId: params.sessionId,
|
|
353
|
+
cwd: params.cwd,
|
|
354
|
+
mcpServers: params.mcpServers ?? [],
|
|
355
|
+
meta: params._meta,
|
|
356
|
+
}, client, { replayHistory: false });
|
|
357
|
+
return {
|
|
358
|
+
modes: buildSessionModeState(mode),
|
|
359
|
+
configOptions: buildSessionConfigOptions(record.config, mode, isYoloEnabled(record.agent)),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Handle `session/close`: cancel any in-flight turn and free the in-memory
|
|
364
|
+
* session state, without deleting the persisted session from disk (unlike
|
|
365
|
+
* `session/delete`).
|
|
366
|
+
*/
|
|
367
|
+
async closeSession(params) {
|
|
368
|
+
const record = this.#sessions.get(params.sessionId);
|
|
369
|
+
if (record) {
|
|
370
|
+
this.#sessions.delete(params.sessionId);
|
|
371
|
+
record.turnAbort?.abort();
|
|
372
|
+
await record.mcpDisconnect();
|
|
373
|
+
}
|
|
374
|
+
return {};
|
|
375
|
+
}
|
|
376
|
+
/** Shared setup for `session/load` and `session/resume`. */
|
|
377
|
+
async #reactivateSession(params, client, options) {
|
|
272
378
|
if (this.#sessions.has(params.sessionId)) {
|
|
273
379
|
throw RequestError.invalidParams({
|
|
274
380
|
sessionId: params.sessionId,
|
|
275
381
|
message: "Session is already active in this agent process.",
|
|
276
382
|
});
|
|
277
383
|
}
|
|
278
|
-
|
|
384
|
+
await this.#storeReady;
|
|
385
|
+
const existing = await readSessionEntry(this.#acpRoot, params.sessionId);
|
|
279
386
|
if (!existing) {
|
|
280
387
|
throw RequestError.resourceNotFound(`session:${params.sessionId}`);
|
|
281
388
|
}
|
|
282
389
|
assertAbsolutePath(params.cwd, "cwd");
|
|
283
|
-
const fromRequest = extractAcpClientUserId(params.
|
|
390
|
+
const fromRequest = extractAcpClientUserId(params.meta);
|
|
284
391
|
const clientUserId = fromRequest !== undefined ? fromRequest : (existing.userId ?? null);
|
|
285
392
|
const mcpServers = params.mcpServers.length > 0
|
|
286
393
|
? normalizeAcpSessionMcpServers(params.mcpServers)
|
|
287
394
|
: (existing.mcpServers ?? []);
|
|
395
|
+
const vscode = extractAcpVscodeFlag(params.meta) || existing.vscode === true;
|
|
288
396
|
const mode = resolveSessionMode(existing.sessionMode);
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
record.
|
|
292
|
-
|
|
293
|
-
|
|
397
|
+
// Bootstrapping restores the Strands snapshot (messages + appState) during
|
|
398
|
+
// `agent.initialize()` — the snapshot is the conversation source of truth.
|
|
399
|
+
const record = await this.#bootstrapSession(params.sessionId, params.cwd, clientUserId ?? params.sessionId, mcpServers, mode, existing.model, vscode);
|
|
400
|
+
// The snapshot restore replaces appState wholesale, so re-apply the
|
|
401
|
+
// connection-scoped values that must win over persisted state: the
|
|
402
|
+
// client's user id, and the index-persisted yolo/mode settings.
|
|
403
|
+
record.agent.appState.set("userId", clientUserId ?? params.sessionId);
|
|
404
|
+
setYoloEnabled(record.agent, existing.yolo === true);
|
|
405
|
+
if (resolveSessionMode(getModeState(record.agent).mode) !== mode) {
|
|
406
|
+
setSessionMode(record.agent, mode);
|
|
407
|
+
applySessionMode(record.agent);
|
|
408
|
+
}
|
|
409
|
+
if (options.replayHistory) {
|
|
410
|
+
await replayConversationHistory(client, params.sessionId, record.agent.messages);
|
|
294
411
|
}
|
|
295
|
-
await replayConversationHistory(client, params.sessionId, record.agent.messages);
|
|
296
412
|
this.#sessions.set(params.sessionId, record);
|
|
297
|
-
await
|
|
413
|
+
await patchSessionEntry(this.#acpRoot, params.sessionId, {
|
|
298
414
|
cwd: params.cwd,
|
|
299
415
|
...(fromRequest !== undefined ? { userId: fromRequest || null } : {}),
|
|
300
416
|
mcpServers,
|
|
417
|
+
...(vscode ? { vscode } : {}),
|
|
301
418
|
sessionMode: mode,
|
|
302
419
|
});
|
|
303
420
|
await this.#advertiseCommands(client, params.sessionId);
|
|
@@ -305,10 +422,7 @@ export class HoomanAcpAgent {
|
|
|
305
422
|
if (getTodoViewState(record.agent).total > 0) {
|
|
306
423
|
await this.#sendPlanUpdate(client, params.sessionId, record);
|
|
307
424
|
}
|
|
308
|
-
return {
|
|
309
|
-
modes: buildSessionModeState(mode),
|
|
310
|
-
configOptions: buildSessionConfigOptions(record.config, mode),
|
|
311
|
-
};
|
|
425
|
+
return { record, mode };
|
|
312
426
|
}
|
|
313
427
|
/** Advertise the available slash commands for a freshly set-up session. */
|
|
314
428
|
#advertiseCommands(client, sessionId) {
|
|
@@ -321,7 +435,7 @@ export class HoomanAcpAgent {
|
|
|
321
435
|
* Handle `session/set_mode`: switch the active mode for a running session,
|
|
322
436
|
* re-applying the tool surface and persisting the choice.
|
|
323
437
|
*/
|
|
324
|
-
async setSessionMode(params) {
|
|
438
|
+
async setSessionMode(params, client) {
|
|
325
439
|
const rec = this.#sessions.get(params.sessionId);
|
|
326
440
|
if (!rec) {
|
|
327
441
|
throw RequestError.invalidParams({ sessionId: params.sessionId });
|
|
@@ -333,9 +447,9 @@ export class HoomanAcpAgent {
|
|
|
333
447
|
});
|
|
334
448
|
}
|
|
335
449
|
if (this.#transitionMode(rec, params.modeId)) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
450
|
+
// Keep the (superseding) config-options `mode` value in sync too, since
|
|
451
|
+
// Clients may read either surface (see Session Config Options spec).
|
|
452
|
+
await this.#syncCurrentMode(client, params.sessionId, rec);
|
|
339
453
|
}
|
|
340
454
|
return {};
|
|
341
455
|
}
|
|
@@ -343,7 +457,7 @@ export class HoomanAcpAgent {
|
|
|
343
457
|
* Handle `session/set_config_option`: apply a model or mode selection and
|
|
344
458
|
* return the complete, up-to-date configuration state (as the spec requires).
|
|
345
459
|
*/
|
|
346
|
-
async setSessionConfigOption(params) {
|
|
460
|
+
async setSessionConfigOption(params, client) {
|
|
347
461
|
const rec = this.#sessions.get(params.sessionId);
|
|
348
462
|
if (!rec) {
|
|
349
463
|
throw RequestError.invalidParams({ sessionId: params.sessionId });
|
|
@@ -356,21 +470,55 @@ export class HoomanAcpAgent {
|
|
|
356
470
|
}
|
|
357
471
|
const value = params.value;
|
|
358
472
|
if (params.configId === CONFIG_ID_MODE) {
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
473
|
+
if (value === MODE_VALUE_YOLO) {
|
|
474
|
+
// Yolo is agent mode with auto-approval on, not a distinct SessionMode.
|
|
475
|
+
if (this.#transitionMode(rec, "agent")) {
|
|
476
|
+
// Keep the legacy `modes` surface in sync too (see Session Config
|
|
477
|
+
// Options spec: "Agents SHOULD keep both in sync").
|
|
478
|
+
await this.#syncCurrentMode(client, params.sessionId, rec);
|
|
479
|
+
}
|
|
480
|
+
if (!isYoloEnabled(rec.agent)) {
|
|
481
|
+
setYoloEnabled(rec.agent, true);
|
|
482
|
+
await patchSessionEntry(this.#acpRoot, params.sessionId, {
|
|
483
|
+
yolo: true,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
364
486
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
487
|
+
else {
|
|
488
|
+
if (!isKnownSessionMode(value)) {
|
|
489
|
+
throw RequestError.invalidParams({
|
|
490
|
+
value,
|
|
491
|
+
message: `Unknown mode "${value}"`,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
if (this.#transitionMode(rec, value)) {
|
|
495
|
+
// Keep the legacy `modes` surface in sync too (see Session Config
|
|
496
|
+
// Options spec: "Agents SHOULD keep both in sync").
|
|
497
|
+
await this.#syncCurrentMode(client, params.sessionId, rec);
|
|
498
|
+
}
|
|
499
|
+
// Selecting a plain mode always turns yolo back off.
|
|
500
|
+
if (isYoloEnabled(rec.agent)) {
|
|
501
|
+
setYoloEnabled(rec.agent, false);
|
|
502
|
+
await patchSessionEntry(this.#acpRoot, params.sessionId, {
|
|
503
|
+
yolo: false,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
369
506
|
}
|
|
370
507
|
}
|
|
371
508
|
else if (params.configId === CONFIG_ID_MODEL) {
|
|
372
509
|
await this.#applyModelChange(rec, value);
|
|
373
|
-
await
|
|
510
|
+
await patchSessionEntry(this.#acpRoot, params.sessionId, {
|
|
511
|
+
model: value,
|
|
512
|
+
});
|
|
513
|
+
// The conversation (and thus the context tokens in use) carries over to
|
|
514
|
+
// the new model, so rescale the client's context gauge against the new
|
|
515
|
+
// window immediately instead of leaving the old one up until the next
|
|
516
|
+
// turn ends. Skipped mid-turn: the rebuild (and billing re-resolution)
|
|
517
|
+
// is deferred to the turn boundary, and the running turn's own
|
|
518
|
+
// `usage_update` is still correctly priced against the old model.
|
|
519
|
+
if (!this.#isTurnActive(rec)) {
|
|
520
|
+
await this.#sendUsageUpdate(this.#requireClient(), params.sessionId, rec, undefined);
|
|
521
|
+
}
|
|
374
522
|
}
|
|
375
523
|
else if (params.configId === CONFIG_ID_EFFORT) {
|
|
376
524
|
const parsed = parseReasoningEffortArg(value);
|
|
@@ -389,7 +537,7 @@ export class HoomanAcpAgent {
|
|
|
389
537
|
});
|
|
390
538
|
}
|
|
391
539
|
return {
|
|
392
|
-
configOptions: buildSessionConfigOptions(rec.config, resolveSessionMode(getModeState(rec.agent).mode)),
|
|
540
|
+
configOptions: buildSessionConfigOptions(rec.config, resolveSessionMode(getModeState(rec.agent).mode), isYoloEnabled(rec.agent)),
|
|
393
541
|
};
|
|
394
542
|
}
|
|
395
543
|
/** Whether a prompt turn is currently streaming for this session. */
|
|
@@ -523,6 +671,7 @@ export class HoomanAcpAgent {
|
|
|
523
671
|
const resolved = rec.config.llm;
|
|
524
672
|
const provider = await modelProviders[resolved.provider]();
|
|
525
673
|
rec.agent.model = provider.create(resolved.providerOptions, resolved.llmOptions);
|
|
674
|
+
rec.billing = await resolveLlmBilling(resolved.billing, resolved.llmOptions.model).catch(() => null);
|
|
526
675
|
}
|
|
527
676
|
catch (error) {
|
|
528
677
|
if (previous) {
|
|
@@ -542,7 +691,7 @@ export class HoomanAcpAgent {
|
|
|
542
691
|
* Apply settings changes deferred during a turn (mode tool-surface + model
|
|
543
692
|
* rebuild). Runs at the turn boundary before the next turn may start.
|
|
544
693
|
*/
|
|
545
|
-
async #flushPendingSettings(rec) {
|
|
694
|
+
async #flushPendingSettings(rec, client, sessionId) {
|
|
546
695
|
if (rec.pendingModeReapply) {
|
|
547
696
|
rec.pendingModeReapply = false;
|
|
548
697
|
applySessionMode(rec.agent);
|
|
@@ -561,6 +710,11 @@ export class HoomanAcpAgent {
|
|
|
561
710
|
if (persistEffort) {
|
|
562
711
|
this.#persistProviderEffort(rec, persistEffort.provider, persistEffort.effort);
|
|
563
712
|
}
|
|
713
|
+
// The rebuild re-resolved billing; rescale the client's context gauge
|
|
714
|
+
// against the new model's window (the turn's earlier `usage_update`
|
|
715
|
+
// carried the previous model's size). Best-effort: the turn result
|
|
716
|
+
// must not fail over a lost notification.
|
|
717
|
+
await this.#sendUsageUpdate(client, sessionId, rec, undefined).catch(() => undefined);
|
|
564
718
|
}
|
|
565
719
|
}
|
|
566
720
|
}
|
|
@@ -568,117 +722,14 @@ export class HoomanAcpAgent {
|
|
|
568
722
|
* Execute an inline (non-model) slash command and return the text reply to
|
|
569
723
|
* stream back as an `agent_message_chunk`. `/init` is handled by the caller.
|
|
570
724
|
*/
|
|
571
|
-
#runControlCommand(
|
|
725
|
+
#runControlCommand(rec, command) {
|
|
572
726
|
switch (command.name) {
|
|
573
|
-
case "mode":
|
|
574
|
-
return this.#commandSetMode(client, sessionId, rec, command.args);
|
|
575
|
-
case "model":
|
|
576
|
-
return this.#commandSetModel(client, sessionId, rec, command.args);
|
|
577
|
-
case "effort":
|
|
578
|
-
return this.#commandSetEffort(client, sessionId, rec, command.args);
|
|
579
|
-
case "yolo":
|
|
580
|
-
return this.#commandSetYolo(sessionId, rec, command.args);
|
|
581
727
|
case "compact":
|
|
582
728
|
return this.#commandCompact(rec);
|
|
583
729
|
default:
|
|
584
730
|
return Promise.resolve(`Unknown command "/${command.name}".`);
|
|
585
731
|
}
|
|
586
732
|
}
|
|
587
|
-
async #commandSetMode(client, sessionId, rec, args) {
|
|
588
|
-
const current = resolveSessionMode(getModeState(rec.agent).mode);
|
|
589
|
-
const arg = args.trim().toLowerCase();
|
|
590
|
-
if (!arg) {
|
|
591
|
-
return `Usage: /mode <${formatModeNames()}>. Current mode: "${current}".`;
|
|
592
|
-
}
|
|
593
|
-
if (!isKnownSessionMode(arg)) {
|
|
594
|
-
return `Unknown mode "${arg}". Use ${formatModeNames()}.`;
|
|
595
|
-
}
|
|
596
|
-
if (!this.#transitionMode(rec, arg)) {
|
|
597
|
-
return `Already in "${arg}" mode.`;
|
|
598
|
-
}
|
|
599
|
-
await this.#syncCurrentMode(client, sessionId, rec);
|
|
600
|
-
return `Switched session mode to "${arg}".`;
|
|
601
|
-
}
|
|
602
|
-
async #commandSetModel(client, sessionId, rec, args) {
|
|
603
|
-
const arg = args.trim();
|
|
604
|
-
if (!arg) {
|
|
605
|
-
return this.#listModelsText(rec);
|
|
606
|
-
}
|
|
607
|
-
if (!rec.config.llms.some((entry) => entry.name === arg)) {
|
|
608
|
-
return `Unknown model "${arg}".\n\n${this.#listModelsText(rec)}`;
|
|
609
|
-
}
|
|
610
|
-
if (arg === currentModelName(rec.config)) {
|
|
611
|
-
return `Already using model "${arg}".`;
|
|
612
|
-
}
|
|
613
|
-
try {
|
|
614
|
-
await this.#applyModelChange(rec, arg);
|
|
615
|
-
}
|
|
616
|
-
catch (error) {
|
|
617
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
618
|
-
return `Could not switch to model "${arg}": ${message}`;
|
|
619
|
-
}
|
|
620
|
-
await patchSessionMeta(this.#acpRoot, sessionId, { model: arg });
|
|
621
|
-
await this.#sendUpdate(client, sessionId, {
|
|
622
|
-
sessionUpdate: "config_option_update",
|
|
623
|
-
configOptions: buildSessionConfigOptions(rec.config, resolveSessionMode(getModeState(rec.agent).mode)),
|
|
624
|
-
});
|
|
625
|
-
return `Switched model to "${arg}".`;
|
|
626
|
-
}
|
|
627
|
-
#listModelsText(rec) {
|
|
628
|
-
const current = currentModelName(rec.config);
|
|
629
|
-
const lines = rec.config.llms.map((entry) => {
|
|
630
|
-
const marker = entry.name === current ? "*" : "-";
|
|
631
|
-
return `${marker} ${entry.name} (${entry.provider}/${entry.options.model})`;
|
|
632
|
-
});
|
|
633
|
-
return [
|
|
634
|
-
`Current model: ${current ?? "(none)"}`,
|
|
635
|
-
"Available models:",
|
|
636
|
-
...lines,
|
|
637
|
-
'Use "/model <name>" to switch.',
|
|
638
|
-
].join("\n");
|
|
639
|
-
}
|
|
640
|
-
async #commandSetEffort(client, sessionId, rec, args) {
|
|
641
|
-
const current = currentReasoningEffort(rec.config);
|
|
642
|
-
const arg = args.trim();
|
|
643
|
-
if (!arg) {
|
|
644
|
-
return `Current reasoning effort: "${current ?? REASONING_EFFORT_OFF}". Use ${REASONING_EFFORT_LEVELS.join(", ")} or ${REASONING_EFFORT_OFF}.`;
|
|
645
|
-
}
|
|
646
|
-
const parsed = parseReasoningEffortArg(arg);
|
|
647
|
-
if (!parsed) {
|
|
648
|
-
return `Unknown effort "${arg}". Use ${REASONING_EFFORT_LEVELS.join(", ")} or ${REASONING_EFFORT_OFF}.`;
|
|
649
|
-
}
|
|
650
|
-
if (parsed.value === current) {
|
|
651
|
-
return `Reasoning effort is already "${current ?? REASONING_EFFORT_OFF}".`;
|
|
652
|
-
}
|
|
653
|
-
try {
|
|
654
|
-
await this.#applyEffortChange(rec, parsed.value);
|
|
655
|
-
}
|
|
656
|
-
catch (error) {
|
|
657
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
658
|
-
return `Could not set reasoning effort: ${message}`;
|
|
659
|
-
}
|
|
660
|
-
await this.#sendUpdate(client, sessionId, {
|
|
661
|
-
sessionUpdate: "config_option_update",
|
|
662
|
-
configOptions: buildSessionConfigOptions(rec.config, resolveSessionMode(getModeState(rec.agent).mode)),
|
|
663
|
-
});
|
|
664
|
-
return `Set reasoning effort to "${parsed.value ?? REASONING_EFFORT_OFF}".`;
|
|
665
|
-
}
|
|
666
|
-
async #commandSetYolo(sessionId, rec, args) {
|
|
667
|
-
const arg = args.trim();
|
|
668
|
-
if (!arg) {
|
|
669
|
-
return `Usage: /yolo <on|off>. Auto-approve is currently ${isYoloEnabled(rec.agent) ? "on" : "off"}.`;
|
|
670
|
-
}
|
|
671
|
-
const enabled = parseYoloToggle(arg);
|
|
672
|
-
if (enabled === undefined) {
|
|
673
|
-
return `Unknown value "${arg}". Use on or off.`;
|
|
674
|
-
}
|
|
675
|
-
if (isYoloEnabled(rec.agent) === enabled) {
|
|
676
|
-
return `Auto-approve is already ${enabled ? "on" : "off"}.`;
|
|
677
|
-
}
|
|
678
|
-
setYoloEnabled(rec.agent, enabled);
|
|
679
|
-
await patchSessionMeta(this.#acpRoot, sessionId, { yolo: enabled });
|
|
680
|
-
return `Auto-approve tools ${enabled ? "enabled" : "disabled"}.`;
|
|
681
|
-
}
|
|
682
733
|
async #commandCompact(rec) {
|
|
683
734
|
const conversationManager = getAgentConversationManager(rec.agent);
|
|
684
735
|
if (!conversationManager) {
|
|
@@ -718,6 +769,28 @@ export class HoomanAcpAgent {
|
|
|
718
769
|
/* ignore */
|
|
719
770
|
}
|
|
720
771
|
}
|
|
772
|
+
/**
|
|
773
|
+
* Queue guidance for the currently active turn via {@link ChatTurnSteeringController}
|
|
774
|
+
* rather than starting a new turn. Echoes the steered text back to the
|
|
775
|
+
* client tagged with `_meta["hoomanjs/steered"]` so it can be shown inline
|
|
776
|
+
* in the transcript. Errors (no active turn) surface as a normal RPC error.
|
|
777
|
+
*/
|
|
778
|
+
async #steerActiveTurn(client, rec, params) {
|
|
779
|
+
if (!this.#isTurnActive(rec)) {
|
|
780
|
+
throw RequestError.invalidParams({
|
|
781
|
+
message: "No active turn to steer.",
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
const text = acpPromptEchoText(params.prompt);
|
|
785
|
+
rec.steering.queue([{ text, attachments: [] }]);
|
|
786
|
+
await this.#sendUpdate(client, params.sessionId, {
|
|
787
|
+
sessionUpdate: "user_message_chunk",
|
|
788
|
+
content: { type: "text", text },
|
|
789
|
+
messageId: crypto.randomUUID(),
|
|
790
|
+
_meta: { "hoomanjs/steered": true },
|
|
791
|
+
});
|
|
792
|
+
return { stopReason: "end_turn" };
|
|
793
|
+
}
|
|
721
794
|
/** Run one prompt turn: stream model output, tool calls, and a stop reason. */
|
|
722
795
|
async prompt(params) {
|
|
723
796
|
const rec = this.#sessions.get(params.sessionId);
|
|
@@ -725,6 +798,12 @@ export class HoomanAcpAgent {
|
|
|
725
798
|
throw RequestError.invalidParams({ sessionId: params.sessionId });
|
|
726
799
|
}
|
|
727
800
|
const client = this.#requireClient();
|
|
801
|
+
// A client-side "steer" request injects guidance into the *currently
|
|
802
|
+
// running* turn (via the steering intervention) instead of queuing a
|
|
803
|
+
// brand new one. See `_meta["hoomanjs/steer"]` extensibility contract.
|
|
804
|
+
if (params._meta?.["hoomanjs/steer"] === true) {
|
|
805
|
+
return this.#steerActiveTurn(client, rec, params);
|
|
806
|
+
}
|
|
728
807
|
// Serialize concurrent prompt turns for the same session.
|
|
729
808
|
const prevExclusive = rec.promptExclusive;
|
|
730
809
|
let releaseExclusive;
|
|
@@ -738,6 +817,7 @@ export class HoomanAcpAgent {
|
|
|
738
817
|
rec.streamedToolCallIds.clear();
|
|
739
818
|
rec.streamingToolInputJson.clear();
|
|
740
819
|
rec.lastStreamToolUseId = null;
|
|
820
|
+
rec.currentAssistantMessageId = null;
|
|
741
821
|
let stopReason = "end_turn";
|
|
742
822
|
try {
|
|
743
823
|
const echo = acpPromptEchoText(params.prompt);
|
|
@@ -745,6 +825,7 @@ export class HoomanAcpAgent {
|
|
|
745
825
|
await this.#sendUpdate(client, params.sessionId, {
|
|
746
826
|
sessionUpdate: "user_message_chunk",
|
|
747
827
|
content: { type: "text", text: echo },
|
|
828
|
+
messageId: crypto.randomUUID(),
|
|
748
829
|
});
|
|
749
830
|
await this.#maybeDeriveTitle(client, params.sessionId, echo);
|
|
750
831
|
}
|
|
@@ -752,11 +833,12 @@ export class HoomanAcpAgent {
|
|
|
752
833
|
// (no model turn); `/init` rewrites the prompt and runs normally.
|
|
753
834
|
const command = parseAcpSlashCommand(echo);
|
|
754
835
|
if (command && command.name !== "init") {
|
|
755
|
-
const reply = await this.#runControlCommand(
|
|
836
|
+
const reply = await this.#runControlCommand(rec, command);
|
|
756
837
|
if (reply) {
|
|
757
838
|
await this.#sendUpdate(client, params.sessionId, {
|
|
758
839
|
sessionUpdate: "agent_message_chunk",
|
|
759
840
|
content: { type: "text", text: reply },
|
|
841
|
+
messageId: crypto.randomUUID(),
|
|
760
842
|
});
|
|
761
843
|
}
|
|
762
844
|
return { stopReason: "end_turn" };
|
|
@@ -827,6 +909,7 @@ export class HoomanAcpAgent {
|
|
|
827
909
|
}
|
|
828
910
|
else if (ev.type === "agentResultEvent") {
|
|
829
911
|
stopReason = toAcpStopReason(ev.result.stopReason);
|
|
912
|
+
await this.#sendUsageUpdate(client, params.sessionId, rec, ev.result.contextSize);
|
|
830
913
|
}
|
|
831
914
|
iter = await stream.next();
|
|
832
915
|
}
|
|
@@ -843,6 +926,7 @@ export class HoomanAcpAgent {
|
|
|
843
926
|
await this.#sendUpdate(client, params.sessionId, {
|
|
844
927
|
sessionUpdate: "agent_message_chunk",
|
|
845
928
|
content: { type: "text", text: `\n[error] ${message}\n` },
|
|
929
|
+
messageId: this.#assistantMessageId(rec),
|
|
846
930
|
});
|
|
847
931
|
stopReason = "refusal";
|
|
848
932
|
}
|
|
@@ -858,21 +942,60 @@ export class HoomanAcpAgent {
|
|
|
858
942
|
rec.turnAbort = null;
|
|
859
943
|
// Apply mode/model changes requested mid-turn before releasing the gate
|
|
860
944
|
// so the next turn sees the settled state.
|
|
861
|
-
await this.#flushPendingSettings(rec);
|
|
945
|
+
await this.#flushPendingSettings(rec, client, params.sessionId);
|
|
862
946
|
releaseExclusive();
|
|
947
|
+
// Conversation history is persisted by the Strands session manager
|
|
948
|
+
// (snapshot save on AfterInvocation, which fires even on error/cancel);
|
|
949
|
+
// only the index's `updatedAt` needs bumping for `session/list` order.
|
|
863
950
|
try {
|
|
864
|
-
await
|
|
951
|
+
await touchSessionEntry(this.#acpRoot, params.sessionId);
|
|
865
952
|
}
|
|
866
953
|
catch {
|
|
867
954
|
/* ignore */
|
|
868
955
|
}
|
|
869
956
|
}
|
|
870
|
-
if (consumeExitRequest(rec.agent)) {
|
|
871
|
-
await this.#disposeAll();
|
|
872
|
-
setTimeout(() => process.exit(EXIT_REQUESTED_CODE), 25);
|
|
873
|
-
}
|
|
874
957
|
return { stopReason };
|
|
875
958
|
}
|
|
959
|
+
/**
|
|
960
|
+
* Emit a `usage_update` at the end of a turn:
|
|
961
|
+
* - `used`: context tokens of the latest request (additive prompt total,
|
|
962
|
+
* so cache reads/writes count regardless of provider reporting style).
|
|
963
|
+
* - `size`: the context window resolved from the model's `billing` config /
|
|
964
|
+
* the models.dev catalog; `0` ("unknown") when unresolved, in which case
|
|
965
|
+
* clients should not render a used/size percentage.
|
|
966
|
+
* - `cost`: cumulative session USD, only while every priced request had
|
|
967
|
+
* resolved rates — otherwise omitted rather than reporting a lowball.
|
|
968
|
+
* - `_meta["hoomanjs/tokens"]`: cumulative token totals for the session
|
|
969
|
+
* (mirroring the CLI TUI's `in`/`cin`/`out` billing meter).
|
|
970
|
+
*/
|
|
971
|
+
#sendUsageUpdate(client, sessionId, rec, used) {
|
|
972
|
+
const contextUsed = rec.lastContextTokens ?? used;
|
|
973
|
+
if (contextUsed === undefined) {
|
|
974
|
+
return Promise.resolve();
|
|
975
|
+
}
|
|
976
|
+
const tokens = rec.cumulativeUsage;
|
|
977
|
+
const includeCost = rec.billing?.costs !== undefined && !rec.costUnpriced;
|
|
978
|
+
return this.#sendUpdate(client, sessionId, {
|
|
979
|
+
sessionUpdate: "usage_update",
|
|
980
|
+
used: contextUsed,
|
|
981
|
+
size: rec.billing?.context ?? 0,
|
|
982
|
+
...(includeCost && {
|
|
983
|
+
cost: { amount: rec.cumulativeCostUsd, currency: "USD" },
|
|
984
|
+
}),
|
|
985
|
+
_meta: {
|
|
986
|
+
"hoomanjs/tokens": {
|
|
987
|
+
input: tokens.inputTokens,
|
|
988
|
+
output: tokens.outputTokens,
|
|
989
|
+
...(tokens.cacheReadInputTokens !== undefined && {
|
|
990
|
+
cacheRead: tokens.cacheReadInputTokens,
|
|
991
|
+
}),
|
|
992
|
+
...(tokens.cacheWriteInputTokens !== undefined && {
|
|
993
|
+
cacheWrite: tokens.cacheWriteInputTokens,
|
|
994
|
+
}),
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
});
|
|
998
|
+
}
|
|
876
999
|
/**
|
|
877
1000
|
* Surface the agent's todo list as an ACP `plan` update. Sends the complete
|
|
878
1001
|
* list, as the client replaces the plan wholesale on each update. The
|
|
@@ -897,7 +1020,7 @@ export class HoomanAcpAgent {
|
|
|
897
1020
|
*/
|
|
898
1021
|
async #syncCurrentMode(client, sessionId, rec) {
|
|
899
1022
|
const modeId = resolveSessionMode(getModeState(rec.agent).mode);
|
|
900
|
-
await
|
|
1023
|
+
await patchSessionEntry(this.#acpRoot, sessionId, { sessionMode: modeId });
|
|
901
1024
|
await this.#sendUpdate(client, sessionId, {
|
|
902
1025
|
sessionUpdate: "current_mode_update",
|
|
903
1026
|
currentModeId: modeId,
|
|
@@ -905,16 +1028,20 @@ export class HoomanAcpAgent {
|
|
|
905
1028
|
// Config Options supersede modes; mirror the change for config-aware clients.
|
|
906
1029
|
await this.#sendUpdate(client, sessionId, {
|
|
907
1030
|
sessionUpdate: "config_option_update",
|
|
908
|
-
configOptions: buildSessionConfigOptions(rec.config, modeId),
|
|
1031
|
+
configOptions: buildSessionConfigOptions(rec.config, modeId, isYoloEnabled(rec.agent)),
|
|
909
1032
|
});
|
|
910
1033
|
}
|
|
911
|
-
/**
|
|
1034
|
+
/**
|
|
1035
|
+
* Derive + persist an instant placeholder title from the first meaningful
|
|
1036
|
+
* prompt echo. The session-title plugin upgrades it with an AI-generated
|
|
1037
|
+
* summary once the turn runs (see `onSessionTitle` in the bootstrap meta).
|
|
1038
|
+
*/
|
|
912
1039
|
async #maybeDeriveTitle(client, sessionId, echo) {
|
|
913
|
-
const
|
|
914
|
-
const needsTitle =
|
|
915
|
-
(
|
|
916
|
-
|
|
917
|
-
String(
|
|
1040
|
+
const entry = await readSessionEntry(this.#acpRoot, sessionId);
|
|
1041
|
+
const needsTitle = entry &&
|
|
1042
|
+
(entry.title === undefined ||
|
|
1043
|
+
entry.title === null ||
|
|
1044
|
+
String(entry.title).trim() === "");
|
|
918
1045
|
if (!needsTitle) {
|
|
919
1046
|
return;
|
|
920
1047
|
}
|
|
@@ -922,7 +1049,7 @@ export class HoomanAcpAgent {
|
|
|
922
1049
|
if (!title) {
|
|
923
1050
|
return;
|
|
924
1051
|
}
|
|
925
|
-
await
|
|
1052
|
+
await patchSessionEntry(this.#acpRoot, sessionId, { title });
|
|
926
1053
|
await this.#sendUpdate(client, sessionId, {
|
|
927
1054
|
sessionUpdate: "session_info_update",
|
|
928
1055
|
title,
|
|
@@ -936,6 +1063,12 @@ export class HoomanAcpAgent {
|
|
|
936
1063
|
return;
|
|
937
1064
|
}
|
|
938
1065
|
switch (inner.type) {
|
|
1066
|
+
case "modelMessageStartEvent": {
|
|
1067
|
+
// A new assistant message starts a fresh `messageId` shared by all of
|
|
1068
|
+
// its `agent_message_chunk`/`agent_thought_chunk` updates.
|
|
1069
|
+
rec.currentAssistantMessageId = crypto.randomUUID();
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
939
1072
|
case "modelContentBlockStartEvent": {
|
|
940
1073
|
const start = inner.start;
|
|
941
1074
|
if (start?.type === "toolUseStart") {
|
|
@@ -959,6 +1092,7 @@ export class HoomanAcpAgent {
|
|
|
959
1092
|
await this.#sendUpdate(client, sessionId, {
|
|
960
1093
|
sessionUpdate: "agent_message_chunk",
|
|
961
1094
|
content: { type: "text", text: delta.text },
|
|
1095
|
+
messageId: this.#assistantMessageId(rec),
|
|
962
1096
|
});
|
|
963
1097
|
return;
|
|
964
1098
|
}
|
|
@@ -967,6 +1101,7 @@ export class HoomanAcpAgent {
|
|
|
967
1101
|
await this.#sendUpdate(client, sessionId, {
|
|
968
1102
|
sessionUpdate: "agent_thought_chunk",
|
|
969
1103
|
content: { type: "text", text: delta.text },
|
|
1104
|
+
messageId: this.#assistantMessageId(rec),
|
|
970
1105
|
});
|
|
971
1106
|
}
|
|
972
1107
|
return;
|
|
@@ -995,11 +1130,34 @@ export class HoomanAcpAgent {
|
|
|
995
1130
|
type: "text",
|
|
996
1131
|
text: `[citations: ${n} reference(s)]\n`,
|
|
997
1132
|
},
|
|
1133
|
+
messageId: this.#assistantMessageId(rec),
|
|
998
1134
|
});
|
|
999
1135
|
}
|
|
1000
1136
|
}
|
|
1001
1137
|
return;
|
|
1002
1138
|
}
|
|
1139
|
+
case "modelMetadataEvent": {
|
|
1140
|
+
if (inner.usage) {
|
|
1141
|
+
// Providers like OpenAI/Moonshot report input inclusive of cache
|
|
1142
|
+
// reads; normalize to the additive shape so `in`/`cin` don't
|
|
1143
|
+
// double-count in the client's meter.
|
|
1144
|
+
const additive = toAdditiveUsage(inner.usage, rec.agent.model);
|
|
1145
|
+
accumulateUsage(rec.cumulativeUsage, additive);
|
|
1146
|
+
rec.lastContextTokens = contextTokensFromUsage(additive);
|
|
1147
|
+
// Session cost accrues per request at the rates of the model that
|
|
1148
|
+
// served it; once a request with usage runs unpriced, the total is
|
|
1149
|
+
// incomplete and cost reporting stops for the session.
|
|
1150
|
+
if ((additive.totalTokens ?? 0) > 0) {
|
|
1151
|
+
if (rec.billing?.costs) {
|
|
1152
|
+
rec.cumulativeCostUsd += computeUsageCostUsd(additive, rec.billing.costs);
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
rec.costUnpriced = true;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1003
1161
|
case "modelRedactionEvent": {
|
|
1004
1162
|
const replace = inner.outputRedaction?.replaceContent ??
|
|
1005
1163
|
inner.inputRedaction?.replaceContent;
|
|
@@ -1007,6 +1165,7 @@ export class HoomanAcpAgent {
|
|
|
1007
1165
|
await this.#sendUpdate(client, sessionId, {
|
|
1008
1166
|
sessionUpdate: "agent_message_chunk",
|
|
1009
1167
|
content: { type: "text", text: replace },
|
|
1168
|
+
messageId: this.#assistantMessageId(rec),
|
|
1010
1169
|
});
|
|
1011
1170
|
}
|
|
1012
1171
|
return;
|
|
@@ -1015,6 +1174,13 @@ export class HoomanAcpAgent {
|
|
|
1015
1174
|
return;
|
|
1016
1175
|
}
|
|
1017
1176
|
}
|
|
1177
|
+
/** Lazily assign a `messageId` for the in-flight assistant message. */
|
|
1178
|
+
#assistantMessageId(rec) {
|
|
1179
|
+
if (!rec.currentAssistantMessageId) {
|
|
1180
|
+
rec.currentAssistantMessageId = crypto.randomUUID();
|
|
1181
|
+
}
|
|
1182
|
+
return rec.currentAssistantMessageId;
|
|
1183
|
+
}
|
|
1018
1184
|
#streamToolUseIdForInputDelta(rec) {
|
|
1019
1185
|
if (rec.lastStreamToolUseId &&
|
|
1020
1186
|
rec.streamedToolCallIds.has(rec.lastStreamToolUseId)) {
|
|
@@ -1034,7 +1200,7 @@ export class HoomanAcpAgent {
|
|
|
1034
1200
|
}
|
|
1035
1201
|
return this.#client;
|
|
1036
1202
|
}
|
|
1037
|
-
async #bootstrapSession(sessionId, cwd, userId, mcpServers, mode, preferredModel) {
|
|
1203
|
+
async #bootstrapSession(sessionId, cwd, userId, mcpServers, mode, preferredModel, vscode = false) {
|
|
1038
1204
|
const client = this.#requireClient();
|
|
1039
1205
|
const sessionConfig = createSessionConfig();
|
|
1040
1206
|
if (preferredModel &&
|
|
@@ -1047,18 +1213,37 @@ export class HoomanAcpAgent {
|
|
|
1047
1213
|
})),
|
|
1048
1214
|
});
|
|
1049
1215
|
}
|
|
1216
|
+
const steering = new ChatTurnSteeringController();
|
|
1050
1217
|
const { config, agent, mcp: { manager }, } = await bootstrap("acp", {
|
|
1051
1218
|
userId,
|
|
1052
1219
|
sessionId,
|
|
1053
1220
|
mode,
|
|
1221
|
+
interventions: [createChatTurnSteeringIntervention(steering)],
|
|
1054
1222
|
createInterventions: () => [
|
|
1055
1223
|
createAcpToolApprovalIntervention(client, sessionId, () => this.#sessions.get(sessionId)?.streamedToolCallIds ??
|
|
1056
1224
|
EMPTY_STREAMED_TOOL_CALL_IDS),
|
|
1057
1225
|
],
|
|
1058
|
-
|
|
1226
|
+
// The session-title plugin generated an AI title (upgrading the
|
|
1227
|
+
// echo-derived placeholder): persist it and notify the client.
|
|
1228
|
+
onSessionTitle: async (title) => {
|
|
1229
|
+
await patchSessionEntry(this.#acpRoot, sessionId, { title });
|
|
1230
|
+
await this.#sendUpdate(client, sessionId, {
|
|
1231
|
+
sessionUpdate: "session_info_update",
|
|
1232
|
+
title,
|
|
1233
|
+
updatedAt: new Date().toISOString(),
|
|
1234
|
+
});
|
|
1235
|
+
},
|
|
1236
|
+
acp: {
|
|
1237
|
+
mcpServers: mcpServers ?? [],
|
|
1238
|
+
vscode,
|
|
1239
|
+
cwd,
|
|
1240
|
+
},
|
|
1059
1241
|
}, false, sessionConfig);
|
|
1060
1242
|
this.#registerTextFsBackend(agent, client, sessionId);
|
|
1061
1243
|
this.#registerTerminalBackend(agent, client, sessionId);
|
|
1244
|
+
setAskUserBackend(agent, createAcpAskUserBackend(client, sessionId));
|
|
1245
|
+
const activeLlm = config.llm;
|
|
1246
|
+
const billing = await resolveLlmBilling(activeLlm.billing, activeLlm.llmOptions.model).catch(() => null);
|
|
1062
1247
|
return {
|
|
1063
1248
|
cwd,
|
|
1064
1249
|
agent,
|
|
@@ -1066,14 +1251,21 @@ export class HoomanAcpAgent {
|
|
|
1066
1251
|
mcpDisconnect: () => manager.disconnect().catch(() => undefined),
|
|
1067
1252
|
turnAbort: null,
|
|
1068
1253
|
promptExclusive: Promise.resolve(),
|
|
1254
|
+
steering,
|
|
1069
1255
|
streamedToolCallIds: new Set(),
|
|
1070
1256
|
streamingToolInputJson: new Map(),
|
|
1071
1257
|
lastStreamToolUseId: null,
|
|
1072
1258
|
terminalByToolCall: new Map(),
|
|
1259
|
+
currentAssistantMessageId: null,
|
|
1073
1260
|
pendingModeReapply: false,
|
|
1074
1261
|
pendingModelRebuild: false,
|
|
1075
1262
|
pendingPersistModel: null,
|
|
1076
1263
|
pendingPersistEffort: null,
|
|
1264
|
+
cumulativeUsage: createEmptyUsage(),
|
|
1265
|
+
billing,
|
|
1266
|
+
cumulativeCostUsd: 0,
|
|
1267
|
+
costUnpriced: false,
|
|
1268
|
+
lastContextTokens: undefined,
|
|
1077
1269
|
};
|
|
1078
1270
|
}
|
|
1079
1271
|
/**
|
|
@@ -1229,10 +1421,12 @@ export function createAcpApp(agent) {
|
|
|
1229
1421
|
.onRequest(methods.agent.initialize, (ctx) => agent.initialize(ctx.params))
|
|
1230
1422
|
.onRequest(methods.agent.session.new, (ctx) => agent.newSession(ctx.params))
|
|
1231
1423
|
.onRequest(methods.agent.session.load, (ctx) => agent.loadSession(ctx.params, ctx.client))
|
|
1424
|
+
.onRequest(methods.agent.session.resume, (ctx) => agent.resumeSession(ctx.params, ctx.client))
|
|
1425
|
+
.onRequest(methods.agent.session.close, (ctx) => agent.closeSession(ctx.params))
|
|
1232
1426
|
.onRequest(methods.agent.session.list, (ctx) => agent.listSessions(ctx.params))
|
|
1233
1427
|
.onRequest(methods.agent.session.delete, (ctx) => agent.deleteSession(ctx.params))
|
|
1234
|
-
.onRequest(methods.agent.session.setMode, (ctx) => agent.setSessionMode(ctx.params))
|
|
1235
|
-
.onRequest(methods.agent.session.setConfigOption, (ctx) => agent.setSessionConfigOption(ctx.params))
|
|
1428
|
+
.onRequest(methods.agent.session.setMode, (ctx) => agent.setSessionMode(ctx.params, ctx.client))
|
|
1429
|
+
.onRequest(methods.agent.session.setConfigOption, (ctx) => agent.setSessionConfigOption(ctx.params, ctx.client))
|
|
1236
1430
|
.onRequest(methods.agent.session.prompt, (ctx) => agent.prompt(ctx.params))
|
|
1237
1431
|
.onNotification(methods.agent.session.cancel, (ctx) => agent.cancel(ctx.params));
|
|
1238
1432
|
}
|