hoomanjs 1.46.0 → 1.47.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 +7 -5
- package/dist/acp/acp-agent.d.ts +42 -0
- package/dist/acp/acp-agent.js +322 -38
- package/dist/acp/acp-agent.js.map +1 -1
- package/dist/acp/session-config.d.ts +2 -2
- package/dist/acp/session-config.js +24 -20
- package/dist/acp/session-config.js.map +1 -1
- package/dist/acp/utils/tool-kind.js +2 -0
- package/dist/acp/utils/tool-kind.js.map +1 -1
- package/dist/chat/app.js +75 -2
- package/dist/chat/app.js.map +1 -1
- package/dist/chat/components/ApprovalPrompt.js +7 -6
- package/dist/chat/components/ApprovalPrompt.js.map +1 -1
- package/dist/chat/components/BackgroundJobsPanel.d.ts +10 -0
- package/dist/chat/components/BackgroundJobsPanel.js +15 -0
- package/dist/chat/components/BackgroundJobsPanel.js.map +1 -0
- package/dist/chat/components/BottomChrome.d.ts +6 -1
- package/dist/chat/components/BottomChrome.js +3 -2
- package/dist/chat/components/BottomChrome.js.map +1 -1
- package/dist/chat/components/ChromePicker.d.ts +7 -2
- package/dist/chat/components/ChromePicker.js +18 -2
- package/dist/chat/components/ChromePicker.js.map +1 -1
- package/dist/chat/components/Composer.js +3 -2
- package/dist/chat/components/Composer.js.map +1 -1
- package/dist/chat/components/DownloadPanel.js +2 -1
- package/dist/chat/components/DownloadPanel.js.map +1 -1
- package/dist/chat/components/EmptyChatBanner.js +2 -1
- package/dist/chat/components/EmptyChatBanner.js.map +1 -1
- package/dist/chat/components/PromptInput.js +2 -1
- package/dist/chat/components/PromptInput.js.map +1 -1
- package/dist/chat/components/QuestionPrompt.js +5 -4
- package/dist/chat/components/QuestionPrompt.js.map +1 -1
- package/dist/chat/components/QueuedPrompts.js +3 -2
- package/dist/chat/components/QueuedPrompts.js.map +1 -1
- package/dist/chat/components/ReasoningStrip.js +2 -1
- package/dist/chat/components/ReasoningStrip.js.map +1 -1
- package/dist/chat/components/RetryEvent.js +2 -1
- package/dist/chat/components/RetryEvent.js.map +1 -1
- package/dist/chat/components/SelectPicker.js +2 -1
- package/dist/chat/components/SelectPicker.js.map +1 -1
- package/dist/chat/components/ShellToolEvent.d.ts +12 -0
- package/dist/chat/components/ShellToolEvent.js +94 -0
- package/dist/chat/components/ShellToolEvent.js.map +1 -0
- package/dist/chat/components/SlashCommands.js +2 -1
- package/dist/chat/components/SlashCommands.js.map +1 -1
- package/dist/chat/components/Spinner.js +2 -1
- package/dist/chat/components/Spinner.js.map +1 -1
- package/dist/chat/components/StatusBar.js +16 -15
- package/dist/chat/components/StatusBar.js.map +1 -1
- package/dist/chat/components/ThinkingStatus.js +2 -1
- package/dist/chat/components/ThinkingStatus.js.map +1 -1
- package/dist/chat/components/ThoughtEvent.js +2 -1
- package/dist/chat/components/ThoughtEvent.js.map +1 -1
- package/dist/chat/components/TodoPanel.js +2 -1
- package/dist/chat/components/TodoPanel.js.map +1 -1
- package/dist/chat/components/ToolEvent.d.ts +2 -1
- package/dist/chat/components/ToolEvent.js +9 -2
- package/dist/chat/components/ToolEvent.js.map +1 -1
- package/dist/chat/components/ToolEventFileResult.js +8 -7
- package/dist/chat/components/ToolEventFileResult.js.map +1 -1
- package/dist/chat/components/Transcript.d.ts +4 -2
- package/dist/chat/components/Transcript.js +4 -4
- package/dist/chat/components/Transcript.js.map +1 -1
- package/dist/chat/components/markdown/BlockRenderer.js +3 -2
- package/dist/chat/components/markdown/BlockRenderer.js.map +1 -1
- package/dist/chat/components/markdown/CodeBlock.js +3 -2
- package/dist/chat/components/markdown/CodeBlock.js.map +1 -1
- package/dist/chat/components/markdown/InlineRenderer.js +6 -5
- package/dist/chat/components/markdown/InlineRenderer.js.map +1 -1
- package/dist/chat/components/shared.js +7 -6
- package/dist/chat/components/shared.js.map +1 -1
- package/dist/chat/types.d.ts +4 -0
- package/dist/configure/components/BusyScreen.js +2 -1
- package/dist/configure/components/BusyScreen.js.map +1 -1
- package/dist/configure/components/MenuScreen.js +3 -2
- package/dist/configure/components/MenuScreen.js.map +1 -1
- package/dist/configure/components/PromptForm.js +2 -1
- package/dist/configure/components/PromptForm.js.map +1 -1
- package/dist/configure/components/SelectMenuItem.js +4 -3
- package/dist/configure/components/SelectMenuItem.js.map +1 -1
- package/dist/configure/utils.js +4 -3
- package/dist/configure/utils.js.map +1 -1
- package/dist/core/agent/index.js +2 -1
- package/dist/core/agent/index.js.map +1 -1
- package/dist/core/modes/definitions.js +4 -0
- package/dist/core/modes/definitions.js.map +1 -1
- package/dist/core/prompts/static/shell.md +21 -1
- package/dist/core/shell/index.d.ts +7 -0
- package/dist/core/shell/index.js +6 -0
- package/dist/core/shell/index.js.map +1 -0
- package/dist/core/shell/manager.d.ts +23 -0
- package/dist/core/shell/manager.js +604 -0
- package/dist/core/shell/manager.js.map +1 -0
- package/dist/core/shell/output-buffer.d.ts +30 -0
- package/dist/core/shell/output-buffer.js +116 -0
- package/dist/core/shell/output-buffer.js.map +1 -0
- package/dist/core/shell/registry.d.ts +5 -0
- package/dist/core/shell/registry.js +24 -0
- package/dist/core/shell/registry.js.map +1 -0
- package/dist/core/shell/shell.d.ts +41 -0
- package/dist/core/{tools → shell}/shell.js +185 -10
- package/dist/core/shell/shell.js.map +1 -0
- package/dist/core/{tools/shell.d.ts → shell/terminal-backend.d.ts} +20 -18
- package/dist/core/shell/terminal-backend.js +18 -0
- package/dist/core/shell/terminal-backend.js.map +1 -0
- package/dist/core/shell/types.d.ts +95 -0
- package/dist/core/shell/types.js +2 -0
- package/dist/core/shell/types.js.map +1 -0
- package/dist/core/state/shell-jobs.d.ts +9 -0
- package/dist/core/state/shell-jobs.js +20 -0
- package/dist/core/state/shell-jobs.js.map +1 -0
- package/dist/core/state/tool-approvals.js +3 -0
- package/dist/core/state/tool-approvals.js.map +1 -1
- package/dist/core/state/turn-boundaries.d.ts +33 -0
- package/dist/core/state/turn-boundaries.js +49 -0
- package/dist/core/state/turn-boundaries.js.map +1 -0
- package/dist/core/theme.d.ts +14 -0
- package/dist/core/theme.js +14 -0
- package/dist/core/theme.js.map +1 -0
- package/dist/core/tools/index.d.ts +0 -1
- package/dist/core/tools/index.js +0 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/core/tools/shell.js.map +0 -1
package/README.md
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/runtime-Node.js-5FA04E?logo=nodedotjs&logoColor=white" alt="Node.js" /></a>
|
|
9
9
|
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/language-TypeScript-3178C6?logo=typescript&logoColor=white" alt="TypeScript" /></a>
|
|
10
10
|
<a href="https://github.com/vadimdemedes/ink"><img src="https://img.shields.io/badge/ui-Ink-6f42c1" alt="Ink" /></a>
|
|
11
|
-
<a href="https://github.com/vaibhavpandeyvpz/hooman/actions/workflows/
|
|
11
|
+
<a href="https://github.com/vaibhavpandeyvpz/hooman/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/vaibhavpandeyvpz/hooman/ci.yml?branch=main&label=ci" alt="CI" /></a>
|
|
12
|
+
<a href="https://github.com/vaibhavpandeyvpz/hooman/actions/workflows/docs.yml"><img src="https://img.shields.io/github/actions/workflow/status/vaibhavpandeyvpz/hooman/docs.yml?branch=main&label=docs" alt="Docs" /></a>
|
|
12
13
|
<a href="https://github.com/vaibhavpandeyvpz/hooman/stargazers"><img src="https://img.shields.io/github/stars/vaibhavpandeyvpz/hooman?style=flat" alt="GitHub Repo stars" /></a>
|
|
13
14
|
<a href="https://github.com/vaibhavpandeyvpz/hooman/commits/main"><img src="https://img.shields.io/github/last-commit/vaibhavpandeyvpz/hooman" alt="GitHub last commit" /></a>
|
|
14
15
|
</p>
|
|
@@ -40,11 +41,12 @@ Or install globally (`npm i -g hoomanjs`), or via Bun (`bunx hoomanjs`). Then ru
|
|
|
40
41
|
## Features
|
|
41
42
|
|
|
42
43
|
- Multiple LLM providers: `anthropic`, `azure`, `bedrock`, `google`, `groq`, `llama-cpp` (local GGUF, the default), `minimax`, `mlx` (local, Apple Silicon), `moonshot`, `ollama`, `openai`, `openrouter`, `xai` — see [Models](https://vaibhavpandey.com/hooman/guides/configuration/models/)
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
44
|
+
- Session modes (`agent` / `plan` / `ask`) plus a separate Yolo auto-approve toggle — see [CLI](https://vaibhavpandey.com/hooman/guides/cli/#session-mode)
|
|
45
|
+
- MCP server support (`stdio`, `streamable-http`, `sse`) with OAuth (DCR + CIMD), lazy MCP tool discovery via `search_tools` / `activate_tools`, optional browser automation via Playwright MCP (`tools.browser.enabled`), and channel-driven automation via `hooman daemon` — see [MCP](https://vaibhavpandey.com/hooman/guides/mcp/)
|
|
46
|
+
- Runtime [Skills](https://vaibhavpandey.com/hooman/guides/skills/): bundled built-ins, a local `~/.hooman/skills` catalog, and optional project-local `.hooman/skills`
|
|
47
|
+
- Built-in read-only subagents, a ripgrep-backed `grep` tool, filesystem tools that respect `.gitignore`, background shell jobs (`run_in_background` / `shell_output` / `shell_stop`), and an `ask_user` tool for mid-task questions — see [Tools](https://vaibhavpandey.com/hooman/guides/tools/)
|
|
46
48
|
- Context-window utilization and session-cost tracking backed by [models.dev](https://models.dev), shown live in every surface
|
|
47
|
-
- Interactive terminal UI for chat and configuration, plus a native [VS Code chat panel](https://vaibhavpandey.com/hooman/guides/vscode/)
|
|
49
|
+
- Interactive terminal UI for chat and configuration, plus a native [VS Code chat panel](https://vaibhavpandey.com/hooman/guides/vscode/) with Mermaid diagrams, plan checklists, Changes keep/undo, fork/copy, Cursor-style revert, and a background-terminals strip
|
|
48
50
|
|
|
49
51
|
See the [full documentation](https://vaibhavpandey.com/hooman/) for CLI commands, configuration layout, provider setup, and more.
|
|
50
52
|
|
package/dist/acp/acp-agent.d.ts
CHANGED
|
@@ -4,6 +4,30 @@ type AgentIdentity = {
|
|
|
4
4
|
name: string;
|
|
5
5
|
version: string;
|
|
6
6
|
};
|
|
7
|
+
/**
|
|
8
|
+
* Params/response for the custom `_hoomanjs/rewind_session` method (Cursor-
|
|
9
|
+
* style revert). Not part of the ACP spec — registered as a custom request
|
|
10
|
+
* via `AgentApp.onRequest(method: string, parser, handler)`. `messageId` is
|
|
11
|
+
* the agent-generated ACP id (see the MessageId RFD) of the turn's user
|
|
12
|
+
* message, captured by the client from that message's `user_message_chunk`
|
|
13
|
+
* echo — never minted by the client itself.
|
|
14
|
+
*/
|
|
15
|
+
export interface RewindSessionRequest {
|
|
16
|
+
sessionId: string;
|
|
17
|
+
messageId: string;
|
|
18
|
+
}
|
|
19
|
+
export interface RewindSessionResponse {
|
|
20
|
+
/** False when `messageId` has no recorded boundary (e.g. session reloaded since that turn ran). */
|
|
21
|
+
reverted: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Params/response for the custom `_hoomanjs/stop_shell_job` method. */
|
|
24
|
+
export interface StopShellJobRequest {
|
|
25
|
+
sessionId: string;
|
|
26
|
+
jobId: string;
|
|
27
|
+
}
|
|
28
|
+
export interface StopShellJobResponse {
|
|
29
|
+
stopped: boolean;
|
|
30
|
+
}
|
|
7
31
|
/**
|
|
8
32
|
* Hooman's ACP agent, built on the app-style SDK API.
|
|
9
33
|
*
|
|
@@ -21,6 +45,24 @@ export declare class HoomanAcpAgent {
|
|
|
21
45
|
deleteSession(params: DeleteSessionRequest): Promise<void>;
|
|
22
46
|
newSession(params: NewSessionRequest): Promise<NewSessionResponse>;
|
|
23
47
|
forkSession(params: ForkSessionRequest, _client: AgentContext): Promise<ForkSessionResponse>;
|
|
48
|
+
/**
|
|
49
|
+
* Cursor-style revert (custom `_hoomanjs/rewind_session` method, not part
|
|
50
|
+
* of the ACP spec): splice `agent.messages` back to the message index
|
|
51
|
+
* bookmarked for `messageId` (see {@link recordTurnBoundary}), dropping
|
|
52
|
+
* that turn and every turn after it, then persist the trimmed history.
|
|
53
|
+
*
|
|
54
|
+
* Returns `reverted: false` instead of erroring when `messageId` has no
|
|
55
|
+
* recorded boundary — e.g. the session was reloaded since that turn ran,
|
|
56
|
+
* so its in-memory bookmark (and the client's matching file-edit
|
|
57
|
+
* baselines) no longer exist.
|
|
58
|
+
*/
|
|
59
|
+
rewindSession(params: RewindSessionRequest): Promise<RewindSessionResponse>;
|
|
60
|
+
/**
|
|
61
|
+
* Custom `_hoomanjs/stop_shell_job`: stop a background shell job by id.
|
|
62
|
+
* Routes through {@link ShellJobManager.stop}, which kills the host terminal
|
|
63
|
+
* (process group) and emits the completion notification the UI listens for.
|
|
64
|
+
*/
|
|
65
|
+
stopShellJob(params: StopShellJobRequest): Promise<StopShellJobResponse>;
|
|
24
66
|
loadSession(params: LoadSessionRequest, client: AgentContext): Promise<LoadSessionResponse>;
|
|
25
67
|
/**
|
|
26
68
|
* Handle `session/resume`: reactivate a persisted session's in-memory state
|
package/dist/acp/acp-agent.js
CHANGED
|
@@ -9,6 +9,7 @@ import { MODE_DEFINITIONS } from "../core/modes/definitions.js";
|
|
|
9
9
|
import { DEFAULT_SESSION_MODE, isKnownSessionMode, } from "../core/modes/schema.js";
|
|
10
10
|
import { getModeState, setSessionMode } from "../core/state/session-mode.js";
|
|
11
11
|
import { isYoloEnabled, setYoloEnabled } from "../core/state/yolo.js";
|
|
12
|
+
import { dropTurnBoundariesFrom, getTurnBoundary, recordTurnBoundary, } from "../core/state/turn-boundaries.js";
|
|
12
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";
|
|
@@ -17,7 +18,7 @@ import { subscribeModelDownloadProgress, } from "../core/utils/download-progress
|
|
|
17
18
|
import { subscribeModelRetryProgress, } from "../core/agent/retry-progress.js";
|
|
18
19
|
import { toAdditiveUsage } from "../core/utils/usage.js";
|
|
19
20
|
import { computeUsageCostUsd, configuredLlmContext, contextTokensFromUsage, resolveLlmMetadata, } from "../core/utils/metadata.js";
|
|
20
|
-
import { buildSessionConfigOptions, currentModelName, CONFIG_ID_EFFORT, CONFIG_ID_MODE, CONFIG_ID_MODEL,
|
|
21
|
+
import { buildSessionConfigOptions, currentModelName, CONFIG_ID_EFFORT, CONFIG_ID_MODE, CONFIG_ID_MODEL, CONFIG_ID_YOLO, } from "./session-config.js";
|
|
21
22
|
import { activeProviderName, parseReasoningEffortArg, withReasoningEffort, } from "../core/utils/reasoning-effort.js";
|
|
22
23
|
import { ACP_SLASH_COMMANDS, parseAcpSlashCommand, } from "./commands.js";
|
|
23
24
|
import { ENTER_PLAN_MODE_TOOL, EXIT_PLAN_MODE_TOOL, } from "../core/state/tool-approvals.js";
|
|
@@ -33,7 +34,7 @@ import { getTodoViewState } from "../core/state/todos.js";
|
|
|
33
34
|
import { UPDATE_TODOS_TOOL_NAME } from "../core/tools/todo.js";
|
|
34
35
|
import { setAskUserBackend } from "../core/tools/ask-user.js";
|
|
35
36
|
import { setTextFsBackend } from "../core/tools/filesystem.js";
|
|
36
|
-
import { setTerminalBackend, } from "../core/
|
|
37
|
+
import { setTerminalBackend, clearShellJobManager, getShellJobManager, } from "../core/shell/index.js";
|
|
37
38
|
import { createAcpToolApprovalIntervention } from "./approvals.js";
|
|
38
39
|
import { createAcpAskUserBackend } from "./questions.js";
|
|
39
40
|
import { extractAcpClientUserId } from "./meta/user-id.js";
|
|
@@ -338,6 +339,7 @@ export class HoomanAcpAgent {
|
|
|
338
339
|
if (record) {
|
|
339
340
|
this.#sessions.delete(params.sessionId);
|
|
340
341
|
record.turnAbort?.abort();
|
|
342
|
+
await this.#teardownShellJobs(params.sessionId, record);
|
|
341
343
|
await record.mcpDisconnect();
|
|
342
344
|
}
|
|
343
345
|
await deleteSessionEntry(this.#acpRoot, params.sessionId);
|
|
@@ -369,6 +371,7 @@ export class HoomanAcpAgent {
|
|
|
369
371
|
await writeSessionEntry(this.#acpRoot, entry);
|
|
370
372
|
const record = await this.#bootstrapSession(sessionId, params.cwd, clientUserId ?? sessionId, mcpServers, mode, undefined, vscode);
|
|
371
373
|
this.#sessions.set(sessionId, record);
|
|
374
|
+
this.#subscribeShellJobs(record.agent, this.#requireClient(), sessionId);
|
|
372
375
|
await this.#advertiseCommands(this.#requireClient(), sessionId);
|
|
373
376
|
return {
|
|
374
377
|
sessionId,
|
|
@@ -449,6 +452,58 @@ export class HoomanAcpAgent {
|
|
|
449
452
|
configOptions: buildSessionConfigOptions(forkConfig, mode, sourceEntry.yolo === true),
|
|
450
453
|
};
|
|
451
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* Cursor-style revert (custom `_hoomanjs/rewind_session` method, not part
|
|
457
|
+
* of the ACP spec): splice `agent.messages` back to the message index
|
|
458
|
+
* bookmarked for `messageId` (see {@link recordTurnBoundary}), dropping
|
|
459
|
+
* that turn and every turn after it, then persist the trimmed history.
|
|
460
|
+
*
|
|
461
|
+
* Returns `reverted: false` instead of erroring when `messageId` has no
|
|
462
|
+
* recorded boundary — e.g. the session was reloaded since that turn ran,
|
|
463
|
+
* so its in-memory bookmark (and the client's matching file-edit
|
|
464
|
+
* baselines) no longer exist.
|
|
465
|
+
*/
|
|
466
|
+
async rewindSession(params) {
|
|
467
|
+
const rec = this.#sessions.get(params.sessionId);
|
|
468
|
+
if (!rec) {
|
|
469
|
+
throw RequestError.invalidParams({ sessionId: params.sessionId });
|
|
470
|
+
}
|
|
471
|
+
if (this.#isTurnActive(rec)) {
|
|
472
|
+
throw RequestError.invalidParams({
|
|
473
|
+
sessionId: params.sessionId,
|
|
474
|
+
message: "Cannot rewind a session while a turn is still running.",
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
const boundary = getTurnBoundary(rec.agent, params.messageId);
|
|
478
|
+
if (boundary === undefined) {
|
|
479
|
+
return { reverted: false };
|
|
480
|
+
}
|
|
481
|
+
rec.agent.messages.length = boundary;
|
|
482
|
+
dropTurnBoundariesFrom(rec.agent, params.messageId);
|
|
483
|
+
await getAgentSessionManager(rec.agent)?.saveSnapshot({
|
|
484
|
+
target: rec.agent,
|
|
485
|
+
isLatest: true,
|
|
486
|
+
});
|
|
487
|
+
return { reverted: true };
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Custom `_hoomanjs/stop_shell_job`: stop a background shell job by id.
|
|
491
|
+
* Routes through {@link ShellJobManager.stop}, which kills the host terminal
|
|
492
|
+
* (process group) and emits the completion notification the UI listens for.
|
|
493
|
+
*/
|
|
494
|
+
async stopShellJob(params) {
|
|
495
|
+
const rec = this.#sessions.get(params.sessionId);
|
|
496
|
+
if (!rec) {
|
|
497
|
+
throw RequestError.invalidParams({ sessionId: params.sessionId });
|
|
498
|
+
}
|
|
499
|
+
const manager = getShellJobManager(rec.agent);
|
|
500
|
+
const existing = manager.get(params.jobId);
|
|
501
|
+
if (!existing) {
|
|
502
|
+
return { stopped: false };
|
|
503
|
+
}
|
|
504
|
+
await manager.stop(params.jobId);
|
|
505
|
+
return { stopped: true };
|
|
506
|
+
}
|
|
452
507
|
async loadSession(params, client) {
|
|
453
508
|
const { record, mode } = await this.#reactivateSession({
|
|
454
509
|
sessionId: params.sessionId,
|
|
@@ -489,6 +544,7 @@ export class HoomanAcpAgent {
|
|
|
489
544
|
if (record) {
|
|
490
545
|
this.#sessions.delete(params.sessionId);
|
|
491
546
|
record.turnAbort?.abort();
|
|
547
|
+
await this.#teardownShellJobs(params.sessionId, record);
|
|
492
548
|
await record.mcpDisconnect();
|
|
493
549
|
}
|
|
494
550
|
return {};
|
|
@@ -529,6 +585,7 @@ export class HoomanAcpAgent {
|
|
|
529
585
|
await replayConversationHistory(client, params.sessionId, record.agent.messages);
|
|
530
586
|
}
|
|
531
587
|
this.#sessions.set(params.sessionId, record);
|
|
588
|
+
this.#subscribeShellJobs(record.agent, client, params.sessionId);
|
|
532
589
|
await patchSessionEntry(this.#acpRoot, params.sessionId, {
|
|
533
590
|
cwd: params.cwd,
|
|
534
591
|
...(fromRequest !== undefined ? { userId: fromRequest || null } : {}),
|
|
@@ -581,6 +638,27 @@ export class HoomanAcpAgent {
|
|
|
581
638
|
if (!rec) {
|
|
582
639
|
throw RequestError.invalidParams({ sessionId: params.sessionId });
|
|
583
640
|
}
|
|
641
|
+
// Yolo is a boolean toggle (auto-approve all tool calls), so it carries a
|
|
642
|
+
// boolean value rather than a string one; handle it before the string
|
|
643
|
+
// guard below that the select-style options rely on.
|
|
644
|
+
if (params.configId === CONFIG_ID_YOLO) {
|
|
645
|
+
if (typeof params.value !== "boolean") {
|
|
646
|
+
throw RequestError.invalidParams({
|
|
647
|
+
configId: params.configId,
|
|
648
|
+
message: `Config option "${params.configId}" expects a boolean value`,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
const enabled = params.value;
|
|
652
|
+
if (isYoloEnabled(rec.agent) !== enabled) {
|
|
653
|
+
setYoloEnabled(rec.agent, enabled);
|
|
654
|
+
await patchSessionEntry(this.#acpRoot, params.sessionId, {
|
|
655
|
+
yolo: enabled,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
return {
|
|
659
|
+
configOptions: buildSessionConfigOptions(rec.config, resolveSessionMode(getModeState(rec.agent).mode), isYoloEnabled(rec.agent)),
|
|
660
|
+
};
|
|
661
|
+
}
|
|
584
662
|
if (typeof params.value !== "string") {
|
|
585
663
|
throw RequestError.invalidParams({
|
|
586
664
|
configId: params.configId,
|
|
@@ -589,39 +667,16 @@ export class HoomanAcpAgent {
|
|
|
589
667
|
}
|
|
590
668
|
const value = params.value;
|
|
591
669
|
if (params.configId === CONFIG_ID_MODE) {
|
|
592
|
-
if (value
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
await this.#syncCurrentMode(client, params.sessionId, rec);
|
|
598
|
-
}
|
|
599
|
-
if (!isYoloEnabled(rec.agent)) {
|
|
600
|
-
setYoloEnabled(rec.agent, true);
|
|
601
|
-
await patchSessionEntry(this.#acpRoot, params.sessionId, {
|
|
602
|
-
yolo: true,
|
|
603
|
-
});
|
|
604
|
-
}
|
|
670
|
+
if (!isKnownSessionMode(value)) {
|
|
671
|
+
throw RequestError.invalidParams({
|
|
672
|
+
value,
|
|
673
|
+
message: `Unknown mode "${value}"`,
|
|
674
|
+
});
|
|
605
675
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
message: `Unknown mode "${value}"`,
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
if (this.#transitionMode(rec, value)) {
|
|
614
|
-
// Keep the legacy `modes` surface in sync too (see Session Config
|
|
615
|
-
// Options spec: "Agents SHOULD keep both in sync").
|
|
616
|
-
await this.#syncCurrentMode(client, params.sessionId, rec);
|
|
617
|
-
}
|
|
618
|
-
// Selecting a plain mode always turns yolo back off.
|
|
619
|
-
if (isYoloEnabled(rec.agent)) {
|
|
620
|
-
setYoloEnabled(rec.agent, false);
|
|
621
|
-
await patchSessionEntry(this.#acpRoot, params.sessionId, {
|
|
622
|
-
yolo: false,
|
|
623
|
-
});
|
|
624
|
-
}
|
|
676
|
+
if (this.#transitionMode(rec, value)) {
|
|
677
|
+
// Keep the legacy `modes` surface in sync too (see Session Config
|
|
678
|
+
// Options spec: "Agents SHOULD keep both in sync").
|
|
679
|
+
await this.#syncCurrentMode(client, params.sessionId, rec);
|
|
625
680
|
}
|
|
626
681
|
}
|
|
627
682
|
else if (params.configId === CONFIG_ID_MODEL) {
|
|
@@ -937,11 +992,13 @@ export class HoomanAcpAgent {
|
|
|
937
992
|
let stopReason = "end_turn";
|
|
938
993
|
try {
|
|
939
994
|
const echo = acpPromptEchoText(params.prompt);
|
|
995
|
+
let turnMessageId;
|
|
940
996
|
if (echo.length > 0) {
|
|
997
|
+
turnMessageId = crypto.randomUUID();
|
|
941
998
|
await this.#sendUpdate(client, params.sessionId, {
|
|
942
999
|
sessionUpdate: "user_message_chunk",
|
|
943
1000
|
content: { type: "text", text: echo },
|
|
944
|
-
messageId:
|
|
1001
|
+
messageId: turnMessageId,
|
|
945
1002
|
});
|
|
946
1003
|
await this.#maybeDeriveTitle(client, params.sessionId, echo);
|
|
947
1004
|
}
|
|
@@ -969,6 +1026,14 @@ export class HoomanAcpAgent {
|
|
|
969
1026
|
},
|
|
970
1027
|
], rec.metadata)
|
|
971
1028
|
: acpPromptToInvokeArgs(params.prompt, rec.metadata);
|
|
1029
|
+
// The `messageId` we just generated for this turn's user_message_chunk
|
|
1030
|
+
// echo (see the ACP MessageId RFD) doubles as a durable handle for
|
|
1031
|
+
// Cursor-style revert: bookmark where this turn's messages start,
|
|
1032
|
+
// before `agent.stream(...)` appends them, so a later `rewindSession`
|
|
1033
|
+
// can splice history back to exactly this point.
|
|
1034
|
+
if (turnMessageId) {
|
|
1035
|
+
recordTurnBoundary(rec.agent, turnMessageId, rec.agent.messages.length);
|
|
1036
|
+
}
|
|
972
1037
|
const signals = [turnAbort.signal];
|
|
973
1038
|
if (this.#connectionSignal) {
|
|
974
1039
|
signals.push(this.#connectionSignal);
|
|
@@ -1001,7 +1066,18 @@ export class HoomanAcpAgent {
|
|
|
1001
1066
|
// live output with a JSON result blob. The structured result
|
|
1002
1067
|
// still travels to the model via `rawOutput`.
|
|
1003
1068
|
const terminalId = rec.terminalByToolCall.get(ev.toolUse.toolUseId);
|
|
1004
|
-
|
|
1069
|
+
// Keep terminalByToolCall for background jobs so live polling
|
|
1070
|
+
// can continue; only drop the mapping for foreground shells.
|
|
1071
|
+
const bgJob = extractBackgroundShellFromResult(ev.result);
|
|
1072
|
+
if (!bgJob) {
|
|
1073
|
+
rec.terminalByToolCall.delete(ev.toolUse.toolUseId);
|
|
1074
|
+
}
|
|
1075
|
+
else if (terminalId) {
|
|
1076
|
+
rec.shellJobs.set(bgJob.jobId, {
|
|
1077
|
+
terminalId,
|
|
1078
|
+
toolCallId: ev.toolUse.toolUseId,
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1005
1081
|
const content = terminalId !== undefined
|
|
1006
1082
|
? [{ type: "terminal", terminalId }]
|
|
1007
1083
|
: (diff ?? toolResultToAcpContent(ev.result));
|
|
@@ -1012,6 +1088,20 @@ export class HoomanAcpAgent {
|
|
|
1012
1088
|
rawOutput: ev.result.toJSON(),
|
|
1013
1089
|
content,
|
|
1014
1090
|
...(locations ? { locations } : {}),
|
|
1091
|
+
...(bgJob
|
|
1092
|
+
? {
|
|
1093
|
+
_meta: {
|
|
1094
|
+
"hoomanjs/shell_job": {
|
|
1095
|
+
event: "started",
|
|
1096
|
+
job_id: bgJob.jobId,
|
|
1097
|
+
description: bgJob.description,
|
|
1098
|
+
status: bgJob.jobStatus ?? "running",
|
|
1099
|
+
terminal_id: terminalId,
|
|
1100
|
+
tool_call_id: ev.toolUse.toolUseId,
|
|
1101
|
+
},
|
|
1102
|
+
},
|
|
1103
|
+
}
|
|
1104
|
+
: {}),
|
|
1015
1105
|
});
|
|
1016
1106
|
if (ev.toolUse.name === UPDATE_TODOS_TOOL_NAME &&
|
|
1017
1107
|
ev.result.status === "success") {
|
|
@@ -1393,6 +1483,8 @@ export class HoomanAcpAgent {
|
|
|
1393
1483
|
streamingToolInputJson: new Map(),
|
|
1394
1484
|
lastStreamToolUseId: null,
|
|
1395
1485
|
terminalByToolCall: new Map(),
|
|
1486
|
+
shellJobs: new Map(),
|
|
1487
|
+
shellJobUnsub: null,
|
|
1396
1488
|
currentAssistantMessageId: null,
|
|
1397
1489
|
pendingModeReapply: false,
|
|
1398
1490
|
pendingModelRebuild: false,
|
|
@@ -1447,7 +1539,134 @@ export class HoomanAcpAgent {
|
|
|
1447
1539
|
}
|
|
1448
1540
|
setTerminalBackend(agent, {
|
|
1449
1541
|
run: (request) => this.#runClientTerminal(client, sessionId, request),
|
|
1542
|
+
spawn: (request) => this.#spawnClientTerminal(client, sessionId, request),
|
|
1543
|
+
readOutput: (terminalId) => this.#readClientTerminal(client, sessionId, terminalId),
|
|
1544
|
+
kill: (terminalId) => this.#killClientTerminal(client, sessionId, terminalId),
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
/** Call after the SessionRecord is stored in `#sessions`. */
|
|
1548
|
+
#subscribeShellJobs(agent, client, sessionId) {
|
|
1549
|
+
const record = this.#sessions.get(sessionId);
|
|
1550
|
+
if (!record) {
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
record.shellJobUnsub?.();
|
|
1554
|
+
const manager = getShellJobManager(agent);
|
|
1555
|
+
record.shellJobUnsub = manager.on((event) => {
|
|
1556
|
+
void this.#onShellJobEvent(client, sessionId, event);
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
async #onShellJobEvent(client, sessionId, event) {
|
|
1560
|
+
if (event.type !== "completed" &&
|
|
1561
|
+
event.type !== "stopped" &&
|
|
1562
|
+
event.type !== "failed") {
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
const job = event.job;
|
|
1566
|
+
const meta = {
|
|
1567
|
+
"hoomanjs/shell_job": {
|
|
1568
|
+
event: event.type,
|
|
1569
|
+
job_id: job.id,
|
|
1570
|
+
description: job.description,
|
|
1571
|
+
status: job.status,
|
|
1572
|
+
ready: job.ready,
|
|
1573
|
+
exit_code: job.exitCode,
|
|
1574
|
+
signal: job.signal,
|
|
1575
|
+
terminal_id: job.terminalId,
|
|
1576
|
+
tool_call_id: job.toolUseId,
|
|
1577
|
+
},
|
|
1578
|
+
};
|
|
1579
|
+
// Meta-only update for the jobs bar — empty text so nothing lands in the
|
|
1580
|
+
// transcript. Fire-and-forget release: awaiting a nested terminal/* RPC
|
|
1581
|
+
// from inside stop_shell_job deadlocks the ACP connection.
|
|
1582
|
+
void this.#sendUpdate(client, sessionId, {
|
|
1583
|
+
sessionUpdate: "agent_message_chunk",
|
|
1584
|
+
content: {
|
|
1585
|
+
type: "text",
|
|
1586
|
+
text: "",
|
|
1587
|
+
},
|
|
1588
|
+
_meta: meta,
|
|
1589
|
+
}).catch(() => undefined);
|
|
1590
|
+
const record = this.#sessions.get(sessionId);
|
|
1591
|
+
const hosted = record?.shellJobs.get(job.id);
|
|
1592
|
+
if (hosted) {
|
|
1593
|
+
record?.shellJobs.delete(job.id);
|
|
1594
|
+
void client
|
|
1595
|
+
.request(methods.client.terminal.release, {
|
|
1596
|
+
sessionId,
|
|
1597
|
+
terminalId: hosted.terminalId,
|
|
1598
|
+
})
|
|
1599
|
+
.catch(() => undefined);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
async #spawnClientTerminal(client, sessionId, request) {
|
|
1603
|
+
const created = await client.request(methods.client.terminal.create, {
|
|
1604
|
+
sessionId,
|
|
1605
|
+
command: request.command,
|
|
1606
|
+
args: request.args,
|
|
1607
|
+
cwd: request.cwd,
|
|
1608
|
+
...(request.outputByteLimit !== undefined
|
|
1609
|
+
? { outputByteLimit: request.outputByteLimit }
|
|
1610
|
+
: {}),
|
|
1611
|
+
}, { cancellationSignal: request.cancelSignal });
|
|
1612
|
+
const terminalId = created.terminalId;
|
|
1613
|
+
const record = this.#sessions.get(sessionId);
|
|
1614
|
+
if (request.jobId && record) {
|
|
1615
|
+
record.shellJobs.set(request.jobId, {
|
|
1616
|
+
terminalId,
|
|
1617
|
+
toolCallId: request.toolUseId,
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
if (request.toolUseId) {
|
|
1621
|
+
record?.terminalByToolCall.set(request.toolUseId, terminalId);
|
|
1622
|
+
await this.#sendUpdate(client, sessionId, {
|
|
1623
|
+
sessionUpdate: "tool_call_update",
|
|
1624
|
+
toolCallId: request.toolUseId,
|
|
1625
|
+
status: "in_progress",
|
|
1626
|
+
content: [{ type: "terminal", terminalId }],
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
return { terminalId };
|
|
1630
|
+
}
|
|
1631
|
+
async #readClientTerminal(client, sessionId, terminalId) {
|
|
1632
|
+
const output = await client.request(methods.client.terminal.output, {
|
|
1633
|
+
sessionId,
|
|
1634
|
+
terminalId,
|
|
1450
1635
|
});
|
|
1636
|
+
return {
|
|
1637
|
+
output: output.output,
|
|
1638
|
+
truncated: output.truncated,
|
|
1639
|
+
exitCode: output.exitStatus?.exitCode ?? null,
|
|
1640
|
+
signal: output.exitStatus?.signal ?? null,
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
async #killClientTerminal(client, sessionId, terminalId) {
|
|
1644
|
+
await client
|
|
1645
|
+
.request(methods.client.terminal.kill, { sessionId, terminalId })
|
|
1646
|
+
.catch(() => undefined);
|
|
1647
|
+
}
|
|
1648
|
+
async #teardownShellJobs(sessionId, record) {
|
|
1649
|
+
record.shellJobUnsub?.();
|
|
1650
|
+
record.shellJobUnsub = null;
|
|
1651
|
+
const client = this.#client;
|
|
1652
|
+
for (const [jobId, hosted] of record.shellJobs) {
|
|
1653
|
+
if (client) {
|
|
1654
|
+
await client
|
|
1655
|
+
.request(methods.client.terminal.kill, {
|
|
1656
|
+
sessionId,
|
|
1657
|
+
terminalId: hosted.terminalId,
|
|
1658
|
+
})
|
|
1659
|
+
.catch(() => undefined);
|
|
1660
|
+
await client
|
|
1661
|
+
.request(methods.client.terminal.release, {
|
|
1662
|
+
sessionId,
|
|
1663
|
+
terminalId: hosted.terminalId,
|
|
1664
|
+
})
|
|
1665
|
+
.catch(() => undefined);
|
|
1666
|
+
}
|
|
1667
|
+
record.shellJobs.delete(jobId);
|
|
1668
|
+
}
|
|
1669
|
+
await clearShellJobManager(record.agent).catch(() => undefined);
|
|
1451
1670
|
}
|
|
1452
1671
|
async #runClientTerminal(client, sessionId, request) {
|
|
1453
1672
|
const created = await client.request(methods.client.terminal.create, {
|
|
@@ -1545,10 +1764,11 @@ export class HoomanAcpAgent {
|
|
|
1545
1764
|
}
|
|
1546
1765
|
}
|
|
1547
1766
|
async #disposeAll() {
|
|
1548
|
-
const
|
|
1767
|
+
const entries = [...this.#sessions.entries()];
|
|
1549
1768
|
this.#sessions.clear();
|
|
1550
|
-
for (const record of
|
|
1769
|
+
for (const [sessionId, record] of entries) {
|
|
1551
1770
|
record.turnAbort?.abort();
|
|
1771
|
+
await this.#teardownShellJobs(sessionId, record);
|
|
1552
1772
|
await record.mcpDisconnect();
|
|
1553
1773
|
}
|
|
1554
1774
|
}
|
|
@@ -1565,6 +1785,8 @@ export function createAcpApp(agent) {
|
|
|
1565
1785
|
.onRequest(methods.agent.session.list, (ctx) => agent.listSessions(ctx.params))
|
|
1566
1786
|
.onRequest(methods.agent.session.delete, (ctx) => agent.deleteSession(ctx.params))
|
|
1567
1787
|
.onRequest(methods.agent.session.fork, (ctx) => agent.forkSession(ctx.params, ctx.client))
|
|
1788
|
+
.onRequest("_hoomanjs/rewind_session", (params) => params, (ctx) => agent.rewindSession(ctx.params))
|
|
1789
|
+
.onRequest("_hoomanjs/stop_shell_job", (params) => params, (ctx) => agent.stopShellJob(ctx.params))
|
|
1568
1790
|
.onRequest(methods.agent.session.setMode, (ctx) => agent.setSessionMode(ctx.params, ctx.client))
|
|
1569
1791
|
.onRequest(methods.agent.session.setConfigOption, (ctx) => agent.setSessionConfigOption(ctx.params, ctx.client))
|
|
1570
1792
|
.onRequest(methods.agent.session.prompt, (ctx) => agent.prompt(ctx.params))
|
|
@@ -1577,4 +1799,66 @@ export async function runAcpStdio() {
|
|
|
1577
1799
|
const connection = createAcpApp(new HoomanAcpAgent(identity)).connect(stream);
|
|
1578
1800
|
await connection.closed;
|
|
1579
1801
|
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Pull a background shell job payload out of a Strands tool result.
|
|
1804
|
+
* The tool returns `{ status: "background", job_id, ... }` which the SDK wraps
|
|
1805
|
+
* as a ToolResultBlock with a json content block.
|
|
1806
|
+
*/
|
|
1807
|
+
function extractBackgroundShellFromResult(result) {
|
|
1808
|
+
const fromValue = (value) => {
|
|
1809
|
+
if (!value || typeof value !== "object") {
|
|
1810
|
+
return null;
|
|
1811
|
+
}
|
|
1812
|
+
const record = value;
|
|
1813
|
+
if (record.status === "background" && typeof record.job_id === "string") {
|
|
1814
|
+
return {
|
|
1815
|
+
jobId: record.job_id,
|
|
1816
|
+
description: typeof record.description === "string"
|
|
1817
|
+
? record.description
|
|
1818
|
+
: undefined,
|
|
1819
|
+
jobStatus: typeof record.job_status === "string" ? record.job_status : undefined,
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
return null;
|
|
1823
|
+
};
|
|
1824
|
+
if (Array.isArray(result.content)) {
|
|
1825
|
+
for (const block of result.content) {
|
|
1826
|
+
if (!block || typeof block !== "object") {
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
const json = block.json;
|
|
1830
|
+
const nested = fromValue(json ?? block);
|
|
1831
|
+
if (nested) {
|
|
1832
|
+
return nested;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
try {
|
|
1837
|
+
const serialized = result.toJSON?.();
|
|
1838
|
+
if (serialized && typeof serialized === "object") {
|
|
1839
|
+
const wrapped = serialized;
|
|
1840
|
+
const content = wrapped.toolResult?.content ?? wrapped.content;
|
|
1841
|
+
if (Array.isArray(content)) {
|
|
1842
|
+
for (const block of content) {
|
|
1843
|
+
if (!block || typeof block !== "object") {
|
|
1844
|
+
continue;
|
|
1845
|
+
}
|
|
1846
|
+
const json = block.json;
|
|
1847
|
+
const nested = fromValue(json ?? block);
|
|
1848
|
+
if (nested) {
|
|
1849
|
+
return nested;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
const direct = fromValue(serialized);
|
|
1854
|
+
if (direct) {
|
|
1855
|
+
return direct;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
catch {
|
|
1860
|
+
// ignore serialization errors
|
|
1861
|
+
}
|
|
1862
|
+
return null;
|
|
1863
|
+
}
|
|
1580
1864
|
//# sourceMappingURL=acp-agent.js.map
|