hoomanjs 1.45.4 → 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 +337 -53
- package/dist/acp/acp-agent.js.map +1 -1
- package/dist/acp/prompt-invoke.d.ts +2 -1
- package/dist/acp/prompt-invoke.js +20 -95
- package/dist/acp/prompt-invoke.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 +93 -20
- 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 +7 -2
- 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.d.ts +1 -1
- package/dist/chat/components/DownloadPanel.js +3 -2
- 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 +17 -16
- 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/cli.js +5 -2
- package/dist/cli.js.map +1 -1
- package/dist/configure/app.js +292 -0
- package/dist/configure/app.js.map +1 -1
- 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/gitignore-guard-plugin.d.ts +2 -0
- package/dist/core/agent/gitignore-guard-plugin.js +79 -0
- package/dist/core/agent/gitignore-guard-plugin.js.map +1 -0
- package/dist/core/agent/index.js +5 -1
- package/dist/core/agent/index.js.map +1 -1
- package/dist/core/config.d.ts +39 -6
- package/dist/core/config.js +5 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/index.js +19 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/models/anthropic.js +10 -4
- package/dist/core/models/anthropic.js.map +1 -1
- package/dist/core/models/azure.js +2 -1
- package/dist/core/models/azure.js.map +1 -1
- package/dist/core/models/bedrock.js +1 -0
- package/dist/core/models/bedrock.js.map +1 -1
- package/dist/core/models/google.js +1 -0
- package/dist/core/models/google.js.map +1 -1
- package/dist/core/models/groq.js +2 -1
- package/dist/core/models/groq.js.map +1 -1
- package/dist/core/models/llama-cpp/index.js +1 -0
- package/dist/core/models/llama-cpp/index.js.map +1 -1
- package/dist/core/models/llama-cpp/resolve-model.js +1 -1
- package/dist/core/models/llama-cpp/resolve-model.js.map +1 -1
- package/dist/core/models/minimax.js +2 -1
- package/dist/core/models/minimax.js.map +1 -1
- package/dist/core/models/mlx/index.js +2 -1
- package/dist/core/models/mlx/index.js.map +1 -1
- package/dist/core/models/mlx/resolve-model.js +1 -1
- package/dist/core/models/mlx/resolve-model.js.map +1 -1
- package/dist/core/models/mlx/strands-mlx.js +1 -1
- package/dist/core/models/mlx/strands-mlx.js.map +1 -1
- package/dist/core/models/moonshot.js +2 -1
- package/dist/core/models/moonshot.js.map +1 -1
- package/dist/core/models/ollama/index.js +7 -3
- package/dist/core/models/ollama/index.js.map +1 -1
- package/dist/core/models/openai.js +2 -1
- package/dist/core/models/openai.js.map +1 -1
- package/dist/core/models/openrouter.js +2 -1
- package/dist/core/models/openrouter.js.map +1 -1
- package/dist/core/models/types.d.ts +43 -11
- package/dist/core/models/types.js +15 -4
- package/dist/core/models/types.js.map +1 -1
- package/dist/core/models/xai.js +2 -1
- package/dist/core/models/xai.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/modes/agent.md +1 -1
- package/dist/core/prompts/modes/plan.md +11 -7
- package/dist/core/prompts/static/browser.md +9 -0
- package/dist/core/prompts/static/shell.md +21 -1
- package/dist/core/prompts/system.js +3 -0
- package/dist/core/prompts/system.js.map +1 -1
- package/dist/core/ripgrep/bootstrap.js +5 -19
- package/dist/core/ripgrep/bootstrap.js.map +1 -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 +188 -24
- 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/skills/built-in/hooman-config/SKILL.md +22 -5
- package/dist/core/skills/built-in/hooman-config/providers.md +14 -12
- package/dist/core/skills/plugin.js +5 -1
- package/dist/core/skills/plugin.js.map +1 -1
- 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/filesystem.js +9 -0
- package/dist/core/tools/filesystem.js.map +1 -1
- 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/core/utils/attachments.d.ts +4 -2
- package/dist/core/utils/attachments.js +10 -32
- package/dist/core/utils/attachments.js.map +1 -1
- package/dist/core/utils/command-path.d.ts +1 -0
- package/dist/core/utils/command-path.js +17 -0
- package/dist/core/utils/command-path.js.map +1 -0
- package/dist/core/utils/download-progress.js.map +1 -0
- package/dist/core/utils/gitignore.d.ts +11 -0
- package/dist/core/utils/gitignore.js +141 -0
- package/dist/core/utils/gitignore.js.map +1 -0
- package/dist/core/utils/hub-download.js.map +1 -0
- package/dist/core/utils/metadata.d.ts +65 -0
- package/dist/core/utils/{billing.js → metadata.js} +56 -73
- package/dist/core/utils/metadata.js.map +1 -0
- package/dist/core/utils/model-metadata.d.ts +39 -0
- package/dist/core/utils/model-metadata.js +222 -0
- package/dist/core/utils/model-metadata.js.map +1 -0
- package/dist/core/utils/paths.d.ts +1 -0
- package/dist/core/utils/paths.js +2 -0
- package/dist/core/utils/paths.js.map +1 -1
- package/dist/core/utils/reasoning-effort.d.ts +9 -1
- package/dist/core/{models → utils}/usage.js +1 -1
- package/dist/core/utils/usage.js.map +1 -0
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +6 -3
- package/dist/daemon/index.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/core/models/download-progress.js.map +0 -1
- package/dist/core/models/hub-download.js.map +0 -1
- package/dist/core/models/usage.js.map +0 -1
- package/dist/core/tools/shell.js.map +0 -1
- package/dist/core/utils/billing.d.ts +0 -76
- package/dist/core/utils/billing.js.map +0 -1
- /package/dist/core/{models → utils}/download-progress.d.ts +0 -0
- /package/dist/core/{models → utils}/download-progress.js +0 -0
- /package/dist/core/{models → utils}/hub-download.d.ts +0 -0
- /package/dist/core/{models → utils}/hub-download.js +0 -0
- /package/dist/core/{models → utils}/usage.d.ts +0 -0
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,15 +9,16 @@ 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";
|
|
15
16
|
import { modelProviders } from "../core/models/index.js";
|
|
16
|
-
import { subscribeModelDownloadProgress, } from "../core/
|
|
17
|
+
import { subscribeModelDownloadProgress, } from "../core/utils/download-progress.js";
|
|
17
18
|
import { subscribeModelRetryProgress, } from "../core/agent/retry-progress.js";
|
|
18
|
-
import { toAdditiveUsage } from "../core/
|
|
19
|
-
import { computeUsageCostUsd, configuredLlmContext, contextTokensFromUsage,
|
|
20
|
-
import { buildSessionConfigOptions, currentModelName, CONFIG_ID_EFFORT, CONFIG_ID_MODE, CONFIG_ID_MODEL,
|
|
19
|
+
import { toAdditiveUsage } from "../core/utils/usage.js";
|
|
20
|
+
import { computeUsageCostUsd, configuredLlmContext, contextTokensFromUsage, resolveLlmMetadata, } from "../core/utils/metadata.js";
|
|
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) {
|
|
@@ -632,7 +687,7 @@ export class HoomanAcpAgent {
|
|
|
632
687
|
// The conversation (and thus the context tokens in use) carries over to
|
|
633
688
|
// the new model, so rescale the client's context gauge against the new
|
|
634
689
|
// window immediately instead of leaving the old one up until the next
|
|
635
|
-
// turn ends. Skipped mid-turn: the rebuild (and
|
|
690
|
+
// turn ends. Skipped mid-turn: the rebuild (and metadata re-resolution)
|
|
636
691
|
// is deferred to the turn boundary, and the running turn's own
|
|
637
692
|
// `usage_update` is still correctly priced against the old model.
|
|
638
693
|
if (!this.#isTurnActive(rec)) {
|
|
@@ -787,7 +842,7 @@ export class HoomanAcpAgent {
|
|
|
787
842
|
const resolved = rec.config.llm;
|
|
788
843
|
const provider = await modelProviders[resolved.provider]();
|
|
789
844
|
rec.agent.model = provider.create(resolved.providerOptions, resolved.llmOptions);
|
|
790
|
-
rec.
|
|
845
|
+
rec.metadata = await resolveLlmMetadata(resolved.metadata, resolved.llmOptions.model, resolved.provider, configuredLlmContext(resolved)).catch(() => null);
|
|
791
846
|
}
|
|
792
847
|
catch (error) {
|
|
793
848
|
if (previous) {
|
|
@@ -825,7 +880,7 @@ export class HoomanAcpAgent {
|
|
|
825
880
|
if (persistEffort) {
|
|
826
881
|
this.#persistProviderEffort(rec, persistEffort.provider, persistEffort.effort);
|
|
827
882
|
}
|
|
828
|
-
// The rebuild re-resolved
|
|
883
|
+
// The rebuild re-resolved metadata; rescale the client's context gauge
|
|
829
884
|
// against the new model's window (the turn's earlier `usage_update`
|
|
830
885
|
// carried the previous model's size). Best-effort: the turn result
|
|
831
886
|
// must not fail over a lost notification.
|
|
@@ -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
|
}
|
|
@@ -967,8 +1024,16 @@ export class HoomanAcpAgent {
|
|
|
967
1024
|
? `${INIT_AGENTS_PROMPT}\n\n${command.args}`
|
|
968
1025
|
: INIT_AGENTS_PROMPT,
|
|
969
1026
|
},
|
|
970
|
-
])
|
|
971
|
-
: acpPromptToInvokeArgs(params.prompt);
|
|
1027
|
+
], rec.metadata)
|
|
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") {
|
|
@@ -1076,7 +1166,7 @@ export class HoomanAcpAgent {
|
|
|
1076
1166
|
* Emit a `usage_update` at the end of a turn:
|
|
1077
1167
|
* - `used`: context tokens of the latest request (additive prompt total,
|
|
1078
1168
|
* so cache reads/writes count regardless of provider reporting style).
|
|
1079
|
-
* - `size`: the context window resolved from the model's `
|
|
1169
|
+
* - `size`: the context window resolved from the model's `metadata` config /
|
|
1080
1170
|
* the models.dev catalog; `0` ("unknown") when unresolved, in which case
|
|
1081
1171
|
* clients should not render a used/size percentage.
|
|
1082
1172
|
* - `cost`: cumulative session USD, only while every priced request had
|
|
@@ -1091,11 +1181,11 @@ export class HoomanAcpAgent {
|
|
|
1091
1181
|
return Promise.resolve();
|
|
1092
1182
|
}
|
|
1093
1183
|
const tokens = rec.lastTurnUsage;
|
|
1094
|
-
const includeCost = rec.
|
|
1184
|
+
const includeCost = rec.metadata?.costs !== undefined && !rec.costUnpriced;
|
|
1095
1185
|
return this.#sendUpdate(client, sessionId, {
|
|
1096
1186
|
sessionUpdate: "usage_update",
|
|
1097
1187
|
used: contextUsed,
|
|
1098
|
-
size: rec.
|
|
1188
|
+
size: rec.metadata?.context ?? 0,
|
|
1099
1189
|
...(includeCost && {
|
|
1100
1190
|
cost: { amount: rec.cumulativeCostUsd, currency: "USD" },
|
|
1101
1191
|
}),
|
|
@@ -1281,8 +1371,8 @@ export class HoomanAcpAgent {
|
|
|
1281
1371
|
// served it; once a request with usage runs unpriced, the total is
|
|
1282
1372
|
// incomplete and cost reporting stops for the session.
|
|
1283
1373
|
if ((additive.totalTokens ?? 0) > 0) {
|
|
1284
|
-
if (rec.
|
|
1285
|
-
rec.cumulativeCostUsd += computeUsageCostUsd(additive, rec.
|
|
1374
|
+
if (rec.metadata?.costs) {
|
|
1375
|
+
rec.cumulativeCostUsd += computeUsageCostUsd(additive, rec.metadata.costs);
|
|
1286
1376
|
}
|
|
1287
1377
|
else {
|
|
1288
1378
|
rec.costUnpriced = true;
|
|
@@ -1380,7 +1470,7 @@ export class HoomanAcpAgent {
|
|
|
1380
1470
|
this.#registerTerminalBackend(agent, client, sessionId);
|
|
1381
1471
|
setAskUserBackend(agent, createAcpAskUserBackend(client, sessionId));
|
|
1382
1472
|
const activeLlm = config.llm;
|
|
1383
|
-
const
|
|
1473
|
+
const metadata = await resolveLlmMetadata(activeLlm.metadata, activeLlm.llmOptions.model, activeLlm.provider, configuredLlmContext(activeLlm)).catch(() => null);
|
|
1384
1474
|
return {
|
|
1385
1475
|
cwd,
|
|
1386
1476
|
agent,
|
|
@@ -1393,13 +1483,15 @@ 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,
|
|
1399
1491
|
pendingPersistModel: null,
|
|
1400
1492
|
pendingPersistEffort: null,
|
|
1401
1493
|
lastTurnUsage: createEmptyUsage(),
|
|
1402
|
-
|
|
1494
|
+
metadata,
|
|
1403
1495
|
cumulativeCostUsd: 0,
|
|
1404
1496
|
costUnpriced: false,
|
|
1405
1497
|
lastContextTokens: undefined,
|
|
@@ -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
|