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.
Files changed (227) hide show
  1. package/README.md +7 -5
  2. package/dist/acp/acp-agent.d.ts +42 -0
  3. package/dist/acp/acp-agent.js +337 -53
  4. package/dist/acp/acp-agent.js.map +1 -1
  5. package/dist/acp/prompt-invoke.d.ts +2 -1
  6. package/dist/acp/prompt-invoke.js +20 -95
  7. package/dist/acp/prompt-invoke.js.map +1 -1
  8. package/dist/acp/session-config.d.ts +2 -2
  9. package/dist/acp/session-config.js +24 -20
  10. package/dist/acp/session-config.js.map +1 -1
  11. package/dist/acp/utils/tool-kind.js +2 -0
  12. package/dist/acp/utils/tool-kind.js.map +1 -1
  13. package/dist/chat/app.js +93 -20
  14. package/dist/chat/app.js.map +1 -1
  15. package/dist/chat/components/ApprovalPrompt.js +7 -6
  16. package/dist/chat/components/ApprovalPrompt.js.map +1 -1
  17. package/dist/chat/components/BackgroundJobsPanel.d.ts +10 -0
  18. package/dist/chat/components/BackgroundJobsPanel.js +15 -0
  19. package/dist/chat/components/BackgroundJobsPanel.js.map +1 -0
  20. package/dist/chat/components/BottomChrome.d.ts +7 -2
  21. package/dist/chat/components/BottomChrome.js +3 -2
  22. package/dist/chat/components/BottomChrome.js.map +1 -1
  23. package/dist/chat/components/ChromePicker.d.ts +7 -2
  24. package/dist/chat/components/ChromePicker.js +18 -2
  25. package/dist/chat/components/ChromePicker.js.map +1 -1
  26. package/dist/chat/components/Composer.js +3 -2
  27. package/dist/chat/components/Composer.js.map +1 -1
  28. package/dist/chat/components/DownloadPanel.d.ts +1 -1
  29. package/dist/chat/components/DownloadPanel.js +3 -2
  30. package/dist/chat/components/DownloadPanel.js.map +1 -1
  31. package/dist/chat/components/EmptyChatBanner.js +2 -1
  32. package/dist/chat/components/EmptyChatBanner.js.map +1 -1
  33. package/dist/chat/components/PromptInput.js +2 -1
  34. package/dist/chat/components/PromptInput.js.map +1 -1
  35. package/dist/chat/components/QuestionPrompt.js +5 -4
  36. package/dist/chat/components/QuestionPrompt.js.map +1 -1
  37. package/dist/chat/components/QueuedPrompts.js +3 -2
  38. package/dist/chat/components/QueuedPrompts.js.map +1 -1
  39. package/dist/chat/components/ReasoningStrip.js +2 -1
  40. package/dist/chat/components/ReasoningStrip.js.map +1 -1
  41. package/dist/chat/components/RetryEvent.js +2 -1
  42. package/dist/chat/components/RetryEvent.js.map +1 -1
  43. package/dist/chat/components/SelectPicker.js +2 -1
  44. package/dist/chat/components/SelectPicker.js.map +1 -1
  45. package/dist/chat/components/ShellToolEvent.d.ts +12 -0
  46. package/dist/chat/components/ShellToolEvent.js +94 -0
  47. package/dist/chat/components/ShellToolEvent.js.map +1 -0
  48. package/dist/chat/components/SlashCommands.js +2 -1
  49. package/dist/chat/components/SlashCommands.js.map +1 -1
  50. package/dist/chat/components/Spinner.js +2 -1
  51. package/dist/chat/components/Spinner.js.map +1 -1
  52. package/dist/chat/components/StatusBar.js +17 -16
  53. package/dist/chat/components/StatusBar.js.map +1 -1
  54. package/dist/chat/components/ThinkingStatus.js +2 -1
  55. package/dist/chat/components/ThinkingStatus.js.map +1 -1
  56. package/dist/chat/components/ThoughtEvent.js +2 -1
  57. package/dist/chat/components/ThoughtEvent.js.map +1 -1
  58. package/dist/chat/components/TodoPanel.js +2 -1
  59. package/dist/chat/components/TodoPanel.js.map +1 -1
  60. package/dist/chat/components/ToolEvent.d.ts +2 -1
  61. package/dist/chat/components/ToolEvent.js +9 -2
  62. package/dist/chat/components/ToolEvent.js.map +1 -1
  63. package/dist/chat/components/ToolEventFileResult.js +8 -7
  64. package/dist/chat/components/ToolEventFileResult.js.map +1 -1
  65. package/dist/chat/components/Transcript.d.ts +4 -2
  66. package/dist/chat/components/Transcript.js +4 -4
  67. package/dist/chat/components/Transcript.js.map +1 -1
  68. package/dist/chat/components/markdown/BlockRenderer.js +3 -2
  69. package/dist/chat/components/markdown/BlockRenderer.js.map +1 -1
  70. package/dist/chat/components/markdown/CodeBlock.js +3 -2
  71. package/dist/chat/components/markdown/CodeBlock.js.map +1 -1
  72. package/dist/chat/components/markdown/InlineRenderer.js +6 -5
  73. package/dist/chat/components/markdown/InlineRenderer.js.map +1 -1
  74. package/dist/chat/components/shared.js +7 -6
  75. package/dist/chat/components/shared.js.map +1 -1
  76. package/dist/chat/types.d.ts +4 -0
  77. package/dist/cli.js +5 -2
  78. package/dist/cli.js.map +1 -1
  79. package/dist/configure/app.js +292 -0
  80. package/dist/configure/app.js.map +1 -1
  81. package/dist/configure/components/BusyScreen.js +2 -1
  82. package/dist/configure/components/BusyScreen.js.map +1 -1
  83. package/dist/configure/components/MenuScreen.js +3 -2
  84. package/dist/configure/components/MenuScreen.js.map +1 -1
  85. package/dist/configure/components/PromptForm.js +2 -1
  86. package/dist/configure/components/PromptForm.js.map +1 -1
  87. package/dist/configure/components/SelectMenuItem.js +4 -3
  88. package/dist/configure/components/SelectMenuItem.js.map +1 -1
  89. package/dist/configure/utils.js +4 -3
  90. package/dist/configure/utils.js.map +1 -1
  91. package/dist/core/agent/gitignore-guard-plugin.d.ts +2 -0
  92. package/dist/core/agent/gitignore-guard-plugin.js +79 -0
  93. package/dist/core/agent/gitignore-guard-plugin.js.map +1 -0
  94. package/dist/core/agent/index.js +5 -1
  95. package/dist/core/agent/index.js.map +1 -1
  96. package/dist/core/config.d.ts +39 -6
  97. package/dist/core/config.js +5 -2
  98. package/dist/core/config.js.map +1 -1
  99. package/dist/core/index.js +19 -1
  100. package/dist/core/index.js.map +1 -1
  101. package/dist/core/models/anthropic.js +10 -4
  102. package/dist/core/models/anthropic.js.map +1 -1
  103. package/dist/core/models/azure.js +2 -1
  104. package/dist/core/models/azure.js.map +1 -1
  105. package/dist/core/models/bedrock.js +1 -0
  106. package/dist/core/models/bedrock.js.map +1 -1
  107. package/dist/core/models/google.js +1 -0
  108. package/dist/core/models/google.js.map +1 -1
  109. package/dist/core/models/groq.js +2 -1
  110. package/dist/core/models/groq.js.map +1 -1
  111. package/dist/core/models/llama-cpp/index.js +1 -0
  112. package/dist/core/models/llama-cpp/index.js.map +1 -1
  113. package/dist/core/models/llama-cpp/resolve-model.js +1 -1
  114. package/dist/core/models/llama-cpp/resolve-model.js.map +1 -1
  115. package/dist/core/models/minimax.js +2 -1
  116. package/dist/core/models/minimax.js.map +1 -1
  117. package/dist/core/models/mlx/index.js +2 -1
  118. package/dist/core/models/mlx/index.js.map +1 -1
  119. package/dist/core/models/mlx/resolve-model.js +1 -1
  120. package/dist/core/models/mlx/resolve-model.js.map +1 -1
  121. package/dist/core/models/mlx/strands-mlx.js +1 -1
  122. package/dist/core/models/mlx/strands-mlx.js.map +1 -1
  123. package/dist/core/models/moonshot.js +2 -1
  124. package/dist/core/models/moonshot.js.map +1 -1
  125. package/dist/core/models/ollama/index.js +7 -3
  126. package/dist/core/models/ollama/index.js.map +1 -1
  127. package/dist/core/models/openai.js +2 -1
  128. package/dist/core/models/openai.js.map +1 -1
  129. package/dist/core/models/openrouter.js +2 -1
  130. package/dist/core/models/openrouter.js.map +1 -1
  131. package/dist/core/models/types.d.ts +43 -11
  132. package/dist/core/models/types.js +15 -4
  133. package/dist/core/models/types.js.map +1 -1
  134. package/dist/core/models/xai.js +2 -1
  135. package/dist/core/models/xai.js.map +1 -1
  136. package/dist/core/modes/definitions.js +4 -0
  137. package/dist/core/modes/definitions.js.map +1 -1
  138. package/dist/core/prompts/modes/agent.md +1 -1
  139. package/dist/core/prompts/modes/plan.md +11 -7
  140. package/dist/core/prompts/static/browser.md +9 -0
  141. package/dist/core/prompts/static/shell.md +21 -1
  142. package/dist/core/prompts/system.js +3 -0
  143. package/dist/core/prompts/system.js.map +1 -1
  144. package/dist/core/ripgrep/bootstrap.js +5 -19
  145. package/dist/core/ripgrep/bootstrap.js.map +1 -1
  146. package/dist/core/shell/index.d.ts +7 -0
  147. package/dist/core/shell/index.js +6 -0
  148. package/dist/core/shell/index.js.map +1 -0
  149. package/dist/core/shell/manager.d.ts +23 -0
  150. package/dist/core/shell/manager.js +604 -0
  151. package/dist/core/shell/manager.js.map +1 -0
  152. package/dist/core/shell/output-buffer.d.ts +30 -0
  153. package/dist/core/shell/output-buffer.js +116 -0
  154. package/dist/core/shell/output-buffer.js.map +1 -0
  155. package/dist/core/shell/registry.d.ts +5 -0
  156. package/dist/core/shell/registry.js +24 -0
  157. package/dist/core/shell/registry.js.map +1 -0
  158. package/dist/core/shell/shell.d.ts +41 -0
  159. package/dist/core/{tools → shell}/shell.js +188 -24
  160. package/dist/core/shell/shell.js.map +1 -0
  161. package/dist/core/{tools/shell.d.ts → shell/terminal-backend.d.ts} +20 -18
  162. package/dist/core/shell/terminal-backend.js +18 -0
  163. package/dist/core/shell/terminal-backend.js.map +1 -0
  164. package/dist/core/shell/types.d.ts +95 -0
  165. package/dist/core/shell/types.js +2 -0
  166. package/dist/core/shell/types.js.map +1 -0
  167. package/dist/core/skills/built-in/hooman-config/SKILL.md +22 -5
  168. package/dist/core/skills/built-in/hooman-config/providers.md +14 -12
  169. package/dist/core/skills/plugin.js +5 -1
  170. package/dist/core/skills/plugin.js.map +1 -1
  171. package/dist/core/state/shell-jobs.d.ts +9 -0
  172. package/dist/core/state/shell-jobs.js +20 -0
  173. package/dist/core/state/shell-jobs.js.map +1 -0
  174. package/dist/core/state/tool-approvals.js +3 -0
  175. package/dist/core/state/tool-approvals.js.map +1 -1
  176. package/dist/core/state/turn-boundaries.d.ts +33 -0
  177. package/dist/core/state/turn-boundaries.js +49 -0
  178. package/dist/core/state/turn-boundaries.js.map +1 -0
  179. package/dist/core/theme.d.ts +14 -0
  180. package/dist/core/theme.js +14 -0
  181. package/dist/core/theme.js.map +1 -0
  182. package/dist/core/tools/filesystem.js +9 -0
  183. package/dist/core/tools/filesystem.js.map +1 -1
  184. package/dist/core/tools/index.d.ts +0 -1
  185. package/dist/core/tools/index.js +0 -1
  186. package/dist/core/tools/index.js.map +1 -1
  187. package/dist/core/utils/attachments.d.ts +4 -2
  188. package/dist/core/utils/attachments.js +10 -32
  189. package/dist/core/utils/attachments.js.map +1 -1
  190. package/dist/core/utils/command-path.d.ts +1 -0
  191. package/dist/core/utils/command-path.js +17 -0
  192. package/dist/core/utils/command-path.js.map +1 -0
  193. package/dist/core/utils/download-progress.js.map +1 -0
  194. package/dist/core/utils/gitignore.d.ts +11 -0
  195. package/dist/core/utils/gitignore.js +141 -0
  196. package/dist/core/utils/gitignore.js.map +1 -0
  197. package/dist/core/utils/hub-download.js.map +1 -0
  198. package/dist/core/utils/metadata.d.ts +65 -0
  199. package/dist/core/utils/{billing.js → metadata.js} +56 -73
  200. package/dist/core/utils/metadata.js.map +1 -0
  201. package/dist/core/utils/model-metadata.d.ts +39 -0
  202. package/dist/core/utils/model-metadata.js +222 -0
  203. package/dist/core/utils/model-metadata.js.map +1 -0
  204. package/dist/core/utils/paths.d.ts +1 -0
  205. package/dist/core/utils/paths.js +2 -0
  206. package/dist/core/utils/paths.js.map +1 -1
  207. package/dist/core/utils/reasoning-effort.d.ts +9 -1
  208. package/dist/core/{models → utils}/usage.js +1 -1
  209. package/dist/core/utils/usage.js.map +1 -0
  210. package/dist/daemon/index.d.ts +2 -0
  211. package/dist/daemon/index.js +6 -3
  212. package/dist/daemon/index.js.map +1 -1
  213. package/dist/index.d.ts +5 -5
  214. package/dist/index.js +4 -4
  215. package/dist/index.js.map +1 -1
  216. package/package.json +2 -1
  217. package/dist/core/models/download-progress.js.map +0 -1
  218. package/dist/core/models/hub-download.js.map +0 -1
  219. package/dist/core/models/usage.js.map +0 -1
  220. package/dist/core/tools/shell.js.map +0 -1
  221. package/dist/core/utils/billing.d.ts +0 -76
  222. package/dist/core/utils/billing.js.map +0 -1
  223. /package/dist/core/{models → utils}/download-progress.d.ts +0 -0
  224. /package/dist/core/{models → utils}/download-progress.js +0 -0
  225. /package/dist/core/{models → utils}/hub-download.d.ts +0 -0
  226. /package/dist/core/{models → utils}/hub-download.js +0 -0
  227. /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/publish-npm.yml"><img src="https://img.shields.io/github/actions/workflow/status/vaibhavpandeyvpz/hooman/publish-npm.yml?branch=main&label=build" alt="Build" /></a>
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
- - MCP server support (`stdio`, `streamable-http`, `sse`) with OAuth (DCR + CIMD), lazy MCP tool discovery via `search_tools` / `activate_tools`, and channel-driven automation via `hooman daemon` — see [MCP](https://vaibhavpandey.com/hooman/guides/mcp/)
44
- - Runtime [Skills](https://vaibhavpandey.com/hooman/guides/skills/): bundled built-ins plus a local `~/.hooman/skills` catalog
45
- - Built-in read-only subagents, a ripgrep-backed `grep` tool, and an `ask_user` tool for mid-task questions — see [Tools](https://vaibhavpandey.com/hooman/guides/tools/)
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
 
@@ -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
@@ -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/models/download-progress.js";
17
+ import { subscribeModelDownloadProgress, } from "../core/utils/download-progress.js";
17
18
  import { subscribeModelRetryProgress, } from "../core/agent/retry-progress.js";
18
- import { toAdditiveUsage } from "../core/models/usage.js";
19
- import { computeUsageCostUsd, configuredLlmContext, contextTokensFromUsage, resolveLlmBilling, } from "../core/utils/billing.js";
20
- import { buildSessionConfigOptions, currentModelName, CONFIG_ID_EFFORT, CONFIG_ID_MODE, CONFIG_ID_MODEL, MODE_VALUE_YOLO, } from "./session-config.js";
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/tools/shell.js";
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 === MODE_VALUE_YOLO) {
593
- // Yolo is agent mode with auto-approval on, not a distinct SessionMode.
594
- if (this.#transitionMode(rec, "agent")) {
595
- // Keep the legacy `modes` surface in sync too (see Session Config
596
- // Options spec: "Agents SHOULD keep both in sync").
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
- else {
607
- if (!isKnownSessionMode(value)) {
608
- throw RequestError.invalidParams({
609
- value,
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 billing re-resolution)
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.billing = await resolveLlmBilling(resolved.billing, resolved.llmOptions.model, resolved.provider, configuredLlmContext(resolved)).catch(() => null);
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 billing; rescale the client's context gauge
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: crypto.randomUUID(),
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
- rec.terminalByToolCall.delete(ev.toolUse.toolUseId);
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 `billing` config /
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.billing?.costs !== undefined && !rec.costUnpriced;
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.billing?.context ?? 0,
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.billing?.costs) {
1285
- rec.cumulativeCostUsd += computeUsageCostUsd(additive, rec.billing.costs);
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 billing = await resolveLlmBilling(activeLlm.billing, activeLlm.llmOptions.model, activeLlm.provider, configuredLlmContext(activeLlm)).catch(() => null);
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
- billing,
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 records = [...this.#sessions.values()];
1767
+ const entries = [...this.#sessions.entries()];
1549
1768
  this.#sessions.clear();
1550
- for (const record of records) {
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