atom-lite 0.1.0 → 0.5.1

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 (169) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +58 -10
  3. package/dist/agent/loop.d.ts +11 -0
  4. package/dist/agent/loop.js +238 -47
  5. package/dist/agent/loop.js.map +1 -1
  6. package/dist/agent/model-turn.d.ts +37 -0
  7. package/dist/agent/model-turn.js +53 -0
  8. package/dist/agent/model-turn.js.map +1 -0
  9. package/dist/agent/turn-recorder.d.ts +33 -0
  10. package/dist/agent/turn-recorder.js +74 -0
  11. package/dist/agent/turn-recorder.js.map +1 -0
  12. package/dist/agent/types.d.ts +4 -0
  13. package/dist/cli/activity.d.ts +45 -0
  14. package/dist/cli/activity.js +141 -0
  15. package/dist/cli/activity.js.map +1 -0
  16. package/dist/cli/app.js +1234 -378
  17. package/dist/cli/app.js.map +1 -1
  18. package/dist/cli/approval-prompt.d.ts +24 -0
  19. package/dist/cli/approval-prompt.js +82 -0
  20. package/dist/cli/approval-prompt.js.map +1 -0
  21. package/dist/cli/banner.d.ts +5 -1
  22. package/dist/cli/banner.js +10 -4
  23. package/dist/cli/banner.js.map +1 -1
  24. package/dist/cli/commands.d.ts +92 -14
  25. package/dist/cli/commands.js +330 -52
  26. package/dist/cli/commands.js.map +1 -1
  27. package/dist/cli/diff.d.ts +1 -0
  28. package/dist/cli/diff.js +28 -0
  29. package/dist/cli/diff.js.map +1 -0
  30. package/dist/cli/input.d.ts +2 -0
  31. package/dist/cli/input.js +10 -0
  32. package/dist/cli/input.js.map +1 -0
  33. package/dist/cli/main.js +2 -2
  34. package/dist/cli/main.js.map +1 -1
  35. package/dist/cli/mouse.d.ts +54 -0
  36. package/dist/cli/mouse.js +86 -0
  37. package/dist/cli/mouse.js.map +1 -0
  38. package/dist/cli/palette.d.ts +41 -0
  39. package/dist/cli/palette.js +212 -0
  40. package/dist/cli/palette.js.map +1 -0
  41. package/dist/cli/panel.d.ts +29 -0
  42. package/dist/cli/panel.js +48 -0
  43. package/dist/cli/panel.js.map +1 -0
  44. package/dist/cli/picker.d.ts +8 -0
  45. package/dist/cli/picker.js +26 -28
  46. package/dist/cli/picker.js.map +1 -1
  47. package/dist/cli/status-line.d.ts +6 -0
  48. package/dist/cli/status-line.js +21 -7
  49. package/dist/cli/status-line.js.map +1 -1
  50. package/dist/cli/stream-text.d.ts +4 -8
  51. package/dist/cli/stream-text.js +26 -12
  52. package/dist/cli/stream-text.js.map +1 -1
  53. package/dist/cli/theme.d.ts +48 -0
  54. package/dist/cli/theme.js +160 -0
  55. package/dist/cli/theme.js.map +1 -0
  56. package/dist/cli/tool-box.d.ts +41 -0
  57. package/dist/cli/tool-box.js +177 -0
  58. package/dist/cli/tool-box.js.map +1 -0
  59. package/dist/cli/transcript.d.ts +24 -1
  60. package/dist/cli/transcript.js +65 -2
  61. package/dist/cli/transcript.js.map +1 -1
  62. package/dist/cli/trust-prompt.d.ts +24 -0
  63. package/dist/cli/trust-prompt.js +89 -0
  64. package/dist/cli/trust-prompt.js.map +1 -0
  65. package/dist/config/defaults.d.ts +12 -0
  66. package/dist/config/defaults.js +12 -0
  67. package/dist/config/defaults.js.map +1 -1
  68. package/dist/config/load.d.ts +2 -0
  69. package/dist/config/load.js +36 -0
  70. package/dist/config/load.js.map +1 -1
  71. package/dist/config/paths.d.ts +2 -0
  72. package/dist/config/paths.js +12 -1
  73. package/dist/config/paths.js.map +1 -1
  74. package/dist/config/schema.d.ts +53 -0
  75. package/dist/config/schema.js +33 -0
  76. package/dist/config/schema.js.map +1 -1
  77. package/dist/config/write.d.ts +2 -0
  78. package/dist/config/write.js +9 -0
  79. package/dist/config/write.js.map +1 -1
  80. package/dist/context/builder.d.ts +7 -0
  81. package/dist/context/builder.js +11 -0
  82. package/dist/context/builder.js.map +1 -0
  83. package/dist/context/manager.d.ts +17 -3
  84. package/dist/context/manager.js +129 -41
  85. package/dist/context/manager.js.map +1 -1
  86. package/dist/extensions/api.d.ts +101 -0
  87. package/dist/extensions/api.js +219 -0
  88. package/dist/extensions/api.js.map +1 -0
  89. package/dist/extensions/discovery.d.ts +42 -0
  90. package/dist/extensions/discovery.js +326 -0
  91. package/dist/extensions/discovery.js.map +1 -0
  92. package/dist/extensions/loader.d.ts +68 -0
  93. package/dist/extensions/loader.js +353 -0
  94. package/dist/extensions/loader.js.map +1 -0
  95. package/dist/extensions/session-manager.d.ts +44 -0
  96. package/dist/extensions/session-manager.js +137 -0
  97. package/dist/extensions/session-manager.js.map +1 -0
  98. package/dist/extensions/tool-adapter.d.ts +12 -0
  99. package/dist/extensions/tool-adapter.js +157 -0
  100. package/dist/extensions/tool-adapter.js.map +1 -0
  101. package/dist/extensions/trust.d.ts +6 -0
  102. package/dist/extensions/trust.js +55 -0
  103. package/dist/extensions/trust.js.map +1 -0
  104. package/dist/extensions/types.d.ts +94 -0
  105. package/dist/extensions/types.js +2 -0
  106. package/dist/extensions/types.js.map +1 -0
  107. package/dist/extensions/ui.d.ts +21 -0
  108. package/dist/extensions/ui.js +75 -0
  109. package/dist/extensions/ui.js.map +1 -0
  110. package/dist/index.d.ts +2 -0
  111. package/dist/index.js +2 -0
  112. package/dist/index.js.map +1 -1
  113. package/dist/mcp/client.d.ts +73 -0
  114. package/dist/mcp/client.js +363 -0
  115. package/dist/mcp/client.js.map +1 -0
  116. package/dist/mcp/format.d.ts +3 -0
  117. package/dist/mcp/format.js +37 -0
  118. package/dist/mcp/format.js.map +1 -0
  119. package/dist/mcp/index.d.ts +6 -0
  120. package/dist/mcp/index.js +7 -0
  121. package/dist/mcp/index.js.map +1 -0
  122. package/dist/mcp/manager.d.ts +53 -0
  123. package/dist/mcp/manager.js +349 -0
  124. package/dist/mcp/manager.js.map +1 -0
  125. package/dist/mcp/stdio-transport.d.ts +26 -0
  126. package/dist/mcp/stdio-transport.js +174 -0
  127. package/dist/mcp/stdio-transport.js.map +1 -0
  128. package/dist/mcp/tools.d.ts +22 -0
  129. package/dist/mcp/tools.js +162 -0
  130. package/dist/mcp/tools.js.map +1 -0
  131. package/dist/mcp/types.d.ts +92 -0
  132. package/dist/mcp/types.js +2 -0
  133. package/dist/mcp/types.js.map +1 -0
  134. package/dist/providers/factory.js +1 -0
  135. package/dist/providers/factory.js.map +1 -1
  136. package/dist/runtime/executor.d.ts +2 -0
  137. package/dist/runtime/executor.js +4 -0
  138. package/dist/runtime/executor.js.map +1 -0
  139. package/dist/runtime/scheduler.d.ts +4 -3
  140. package/dist/runtime/scheduler.js +7 -98
  141. package/dist/runtime/scheduler.js.map +1 -1
  142. package/dist/runtime/tool-executor.d.ts +18 -0
  143. package/dist/runtime/tool-executor.js +126 -0
  144. package/dist/runtime/tool-executor.js.map +1 -0
  145. package/dist/security/process.d.ts +10 -0
  146. package/dist/security/process.js +36 -0
  147. package/dist/security/process.js.map +1 -1
  148. package/dist/sessions/events.d.ts +9 -0
  149. package/dist/sessions/events.js +11 -6
  150. package/dist/sessions/events.js.map +1 -1
  151. package/dist/sessions/history.d.ts +18 -0
  152. package/dist/sessions/history.js +76 -0
  153. package/dist/sessions/history.js.map +1 -0
  154. package/dist/sessions/index.d.ts +2 -0
  155. package/dist/sessions/index.js +2 -0
  156. package/dist/sessions/index.js.map +1 -1
  157. package/dist/sessions/search.d.ts +16 -0
  158. package/dist/sessions/search.js +136 -0
  159. package/dist/sessions/search.js.map +1 -0
  160. package/dist/sessions/store.js +7 -4
  161. package/dist/sessions/store.js.map +1 -1
  162. package/dist/tools/bash.js +6 -3
  163. package/dist/tools/bash.js.map +1 -1
  164. package/dist/tools/edit.js +2 -1
  165. package/dist/tools/edit.js.map +1 -1
  166. package/dist/tools/types.d.ts +6 -0
  167. package/dist/types.d.ts +2 -1
  168. package/dist/types.js.map +1 -1
  169. package/package.json +3 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,63 @@ All notable changes to this project are documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ ## [0.5.0] - 2026-11-05
13
+
14
+ ### Added
15
+
16
+ - Unmodified Pi coding-agent extension support: discovery, project trust,
17
+ in-process loading via `jiti`, extension tools, commands, events, and
18
+ session entries, plus ADR-0011 and the extensions guide.
19
+ - `/extensions doctor` diagnostics, `/steer` mid-turn injection, and
20
+ message queuing during active turns.
21
+ - Continuous dimmed thinking blocks in the TUI.
22
+
23
+ ## [0.4.1] - 2026-11-04
24
+
25
+ ### Fixed
26
+
27
+ - npm publish retry after staged 0.4.0 conflict.
28
+
29
+ ## [0.4.0] - 2026-11-04
30
+
31
+ ### Added
32
+
33
+ - Extension discovery and manifest parsing (`src/extensions/discovery.ts`).
34
+ - Project trust gating and extension loader lifecycle with `/extensions`.
35
+ - `/export` for sessions.
36
+ - Markdown rendering for assistant responses and unified diff summaries in tool boxes.
37
+ - Literal edit replacement handling.
38
+ - Escape to cancel an active turn.
39
+ - TUI mouse capture fixes.
40
+
41
+ ### Fixed
42
+
43
+ - Mouse capture sequence and startup ordering for TUI mouse support.
44
+
45
+ [0.4.0]: https://github.com/beast-ofcourse/atom-lite/releases/tag/v0.4.0
46
+
47
+ ## [0.3.1] - 2026-11-04
48
+
49
+ ### Added
50
+
51
+ - MCP client over stdio: user-config-only `mcpServers`, lazy connections, tool
52
+ discovery and execution as `mcp__<server>__<tool>`, refresh on
53
+ `tools/list_changed`, `/mcp` and `/mcp reconnect`, and `/doctor` health.
54
+ - Session picker and search (`/sessions`, `/search`), resume in place, and
55
+ `/session rename`.
56
+ - Command palette (`ctrl+p`), semantic theme registry, and TUI mouse support.
57
+ - Configuration reference and documentation index.
58
+
59
+ ### Fixed
60
+
61
+ - Ollama provider profiles now forward the `think` option to the adapter.
62
+
63
+ [0.3.1]: https://github.com/beast-ofcourse/atom-lite/releases/tag/v0.3.1
64
+
8
65
  ## [0.1.0] - 2026-11-04
9
66
 
10
67
  ### Added
package/README.md CHANGED
@@ -6,9 +6,9 @@
6
6
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
8
8
 
9
- **Atom-lite** is a local-first, model-agnostic coding agent with an intentionally small harness: one agent loop, exactly four built-in tools (`read`, `write`, `edit`, `bash`), a deterministic conflict-aware scheduler, durable JSONL sessions, and explicit safety boundaries. It runs against local or hosted model providers through interchangeable adapters — no cloud account, telemetry service, or hosted control plane required.
9
+ **Atom-lite** is a local-first, model-agnostic coding agent with an intentionally small harness: one agent loop, four core built-in tools (`read`, `write`, `edit`, `bash`) plus network tools (`webfetch`, `websearch`), a deterministic conflict-aware scheduler, durable JSONL sessions, and explicit safety boundaries. It runs against local or hosted model providers through interchangeable adapters — no cloud account, telemetry service, or hosted control plane required.
10
10
 
11
- > Working title: *ThinAgent*. See [`project-overview.md`](./project-overview.md) for the full architecture blueprint, [`GLOSSARY.md`](./GLOSSARY.md) for domain vocabulary, and [`docs/adr/`](./docs/adr/) for architectural decisions.
11
+ > See [`docs/README.md`](./docs/README.md) for the documentation index and [`docs/adr/`](./docs/adr/) for architectural decisions.
12
12
 
13
13
  ---
14
14
 
@@ -35,13 +35,13 @@
35
35
 
36
36
  Most coding agents accumulate tools, sub-agents, planning frameworks, retrieval pipelines, and orchestration layers until the harness itself becomes the product. Atom-lite takes the opposite stance:
37
37
 
38
- - **Small core, strong primitives.** The core contains only a provider interface, a session model, a single agent loop, four tools, a scheduler/executor, context-window management, configuration, and a CLI.
38
+ - **Small core, strong primitives.** The core contains only a provider interface, a session model, a single agent loop, four core tools plus two network tools, a scheduler/executor, context-window management, configuration, and a CLI.
39
39
  - **The model decides; the runtime enforces.** The model chooses which tool to call and what to do next. The runtime validates arguments, enforces workspace boundaries, applies approval rules, schedules calls safely, and returns accurate results. There is no hard-coded "plan → inspect → edit → test" workflow.
40
40
  - **Thin harness, not weak engineering.** Provider-specific formats, streaming failures, concurrent file access, shell timeouts, large outputs, context exhaustion, and session recovery are all handled — they are the reliability layer that makes a small harness useful.
41
41
 
42
42
  ## Features
43
43
 
44
- - **Four general-purpose tools** — `read`, `write`, `edit`, `bash`. Everything else (search, tests, git, builds) goes through `bash`.
44
+ - **Six built-in tools** — four core tools (`read`, `write`, `edit`, `bash`) plus `webfetch` and `websearch`. Everything else (search, tests, git, builds) goes through `bash`.
45
45
  - **Single agent loop** — one agent, one conversation, one tool registry, one control loop. Parallelism lives inside a tool-call batch.
46
46
  - **Conflict-aware scheduler** — per-file read/write locks and a global bash barrier. Parallelism is governed by resource conflicts, not applied blindly.
47
47
  - **Durable sessions** — append-only JSONL event logs, resumable across CLI restarts, grouped by workspace.
@@ -51,6 +51,7 @@ Most coding agents accumulate tools, sub-agents, planning frameworks, retrieval
51
51
  - **Hash guards** — `expected_hash` rejects stale `write`/`edit` operations when the file changed since it was read.
52
52
  - **Provider-agnostic** — Ollama, Gemini, and any OpenAI-compatible endpoint (LM Studio, llama.cpp, Kilo AI Gateway, OpenAI, Anthropic, OpenRouter, Groq, Mistral, xAI, …).
53
53
  - **Interactive provider/model pickers** — `/provider` adds a provider and stores its key securely; `/model` fuzzy-searches every configured provider's models.
54
+ - **Command palette and themes** — `ctrl+p` fuzzy-searches commands, skills, providers, models, themes, and recent sessions. `/theme` switches between auto, dark, light, and no-colour modes.
54
55
  - **Local-first** — runs fully against local providers with no cloud account, telemetry, or hosted database.
55
56
  - **Safe path handling** — file tools resolve against the workspace and reject escapes; shell is an explicit, sandbox-aware boundary.
56
57
 
@@ -141,13 +142,14 @@ Example `config.json`:
141
142
  }
142
143
  ```
143
144
 
144
- See [`project-overview.md`](./project-overview.md) for the full configuration shape.
145
+ See [`docs/configuration.md`](./docs/configuration.md) for every field, default, and precedence rule.
145
146
 
146
147
  ## Usage
147
148
 
148
149
  ```
149
150
  atom-lite [task] [--provider NAME] [--model MODEL] [--continue] [--session ID]
150
- [--no-tools] [--json] [--debug] [--version] [--help]
151
+ [--list-sessions] [--search-sessions QUERY] [--no-tools] [--json]
152
+ [--debug] [--theme NAME] [--no-color] [--version] [--help]
151
153
  ```
152
154
 
153
155
  | Flag | Description |
@@ -157,12 +159,24 @@ atom-lite [task] [--provider NAME] [--model MODEL] [--continue] [--session ID]
157
159
  | `--model MODEL` | Override the model for this run |
158
160
  | `--continue` | Resume the most recent session in this workspace |
159
161
  | `--session ID` | Resume a specific session |
162
+ | `--list-sessions` | Print the session index and exit |
163
+ | `--search-sessions QUERY` | Print matching sessions with snippets and exit |
160
164
  | `--no-tools` | Disable all tools (model-only chat) |
161
165
  | `--json` | Non-interactive JSON output |
162
166
  | `--debug` | Debug logging |
167
+ | `--theme NAME` | Use `auto`, `dark`, `light`, or `ansi` for this run |
168
+ | `--no-color` | Force the no-colour theme |
163
169
  | `--version` | Print version |
164
170
  | `--help` | Show help |
165
171
 
172
+ In the TUI, type `/help` to see every command grouped by category, or press
173
+ `ctrl+p` for the fuzzy command palette. `/theme` opens a live-preview picker and
174
+ persists your choice to user config; `NO_COLOR` and `--no-color` always win.
175
+ `/sessions` opens a searchable session picker scoped to the current workspace,
176
+ with a toggle for all workspaces, and `/search <query>` prints matching sessions
177
+ with content snippets. Mouse wheel scrolling and click-to-focus are enabled for
178
+ interactive terminals and can be disabled with `ui.mouse: false`.
179
+
166
180
  ## Tools
167
181
 
168
182
  Four core tools (see [ADR 0001](./docs/adr/0001-four-built-in-tools-only.md)) plus the additive network tools from [ADR 0006](./docs/adr/0006-expanded-toolset.md):
@@ -205,10 +219,37 @@ The selected provider/model is persisted to the user config, so the next session
205
219
  - **No secrets in config** — interactive provider setup stores API keys in `~/.config/atom-lite/credentials.json` with `0600` permissions; environment variables still take precedence.
206
220
  - **Local-first** — no cloud account, telemetry, or remote control plane required (see [ADR 0004](./docs/adr/0004-local-first-no-cloud-dependency.md)).
207
221
 
222
+ ## Commands
223
+
224
+ Type `/help` for the grouped list, or press `ctrl+p` for the fuzzy command
225
+ palette. Common commands:
226
+
227
+ | Command | Description |
228
+ | --- | --- |
229
+ | `/model [model]` | Show or switch the active model |
230
+ | `/provider [profile]` | Show or switch the active provider |
231
+ | `/effort [low\|medium\|high]` | Set reasoning effort |
232
+ | `/session [rename <title>]` | Show or rename the current session |
233
+ | `/sessions [all]` | Search and resume a session |
234
+ | `/search <query>` | Search sessions by title or content |
235
+ | `/export [path]` | Export the session transcript |
236
+ | `/mcp [reconnect [server]]` | Inspect or reconnect MCP servers |
237
+ | `/extensions` | List discovered extensions and load status |
238
+ | `/compact` | Compact older context |
239
+ | `/clear` | Clear the transcript view |
240
+ | `/approval` | Change approval mode |
241
+ | `/doctor` | Run diagnostics |
242
+ | `/thinking` | Toggle thinking display |
243
+ | `/theme [auto\|dark\|light\|ansi]` | Switch theme |
244
+ | `/skills`, `/skill <name>` | List or load a skill |
245
+ | `/exit` | Exit the TUI |
246
+
208
247
  ## Sessions
209
248
 
210
249
  Sessions are append-only JSONL event logs recording everything that happened in one conversation. They are resumable across CLI restarts and grouped by workspace. Stored under the session store (see `src/sessions/`). Compaction summarizes older history to stay within the context budget while retaining the full transcript.
211
250
 
251
+ The session index derives a title from the first user message unless `/session rename <title>` overrides it, and records the provider, update time, and message count. `/sessions` resumes a session in place: retained messages are replayed into the transcript and a `session_resumed` event is appended. `/search <query>` matches titles, message content, and workspace paths, ranking title hits first. `--list-sessions` and `--search-sessions` print the same index as stable tab-separated rows for scripts.
252
+
212
253
  ## Context management
213
254
 
214
255
  The context manager builds model input, estimates tokens, and triggers compaction before the context budget is exceeded. It also loads project instructions (e.g. `AGENTS.md`/`CLAUDE.md`-style files) subject to `maxProjectInstructionBytes`.
@@ -232,7 +273,10 @@ Tests live in `tests/` (agent, config, runtime, tools). The project is TypeScrip
232
273
  1. Update `version` in `package.json` and add a `CHANGELOG.md` entry.
233
274
  2. Run `npm run typecheck && npm test && npm run bench`.
234
275
  3. Commit, then tag and push: `git tag vX.Y.Z && git push origin master --tags`.
235
- 4. The `Release` GitHub workflow publishes to npm with provenance and creates the GitHub release. It requires an `NPM_TOKEN` repository secret with publish access.
276
+ 4. The `Release` workflow stages the package on npm with provenance and opens a draft GitHub release. It requires an `NPM_TOKEN` repository secret with publish access.
277
+ 5. Approve the staged version on npm (Staged Packages tab, or `npm stage approve <stage-id>` with 2FA), then publish the draft release on GitHub.
278
+
279
+ npm requires a maintainer with 2FA to approve staged releases; CI cannot complete that step.
236
280
 
237
281
  ## Architecture
238
282
 
@@ -246,7 +290,7 @@ src/
246
290
  runtime/ # scheduler, approvals
247
291
  security/ # paths, process, redaction
248
292
  sessions/ # store, events, recovery
249
- tools/ # read, write, edit, bash, registry
293
+ tools/ # read, write, edit, bash, webfetch, websearch, registry
250
294
  ```
251
295
 
252
296
  Key design decisions are recorded as ADRs in [`docs/adr/`](./docs/adr/):
@@ -255,11 +299,15 @@ Key design decisions are recorded as ADRs in [`docs/adr/`](./docs/adr/):
255
299
  - [0002 — Single agent loop, no sub-agents in v1](./docs/adr/0002-single-agent-loop.md)
256
300
  - [0003 — `bash` is a global exclusive barrier](./docs/adr/0003-bash-as-global-exclusive-barrier.md)
257
301
  - [0004 — Local-first, no cloud dependency](./docs/adr/0004-local-first-no-cloud-dependency.md)
258
- - [0005 — OpenTUI for the terminal UI](./docs/adr/0005-opentui-for-terminal-ui.md)
302
+ - [0005 — OpenTUI for the terminal UI](./docs/adr/0005-opentui-for-terminal-ui.md) (superseded by 0007)
303
+ - [0006 — Expanded built-in toolset (webfetch, websearch)](./docs/adr/0006-expanded-toolset.md)
304
+ - [0007 — pi-tui replaces OpenTUI](./docs/adr/0007-pi-tui-replaces-opentui.md)
259
305
 
260
306
  ## Contributing
261
307
 
262
- Issues and specs live as markdown under `.scratch/` (see [`docs/agents/issue-tracker.md`](./docs/agents/issue-tracker.md)). Domain vocabulary is in [`GLOSSARY.md`](./GLOSSARY.md). Please keep changes consistent with the "small core" principle: new tools require strong justification against ADR 0001.
308
+ Read [`CONTRIBUTING.md`](./CONTRIBUTING.md) before opening a pull request. Issues and specs live as markdown under `.scratch/` (see [`docs/agents/issue-tracker.md`](./docs/agents/issue-tracker.md)). Please keep changes consistent with the "small core" principle: new tools require strong justification against ADR 0001.
309
+
310
+ Report vulnerabilities privately as described in [`SECURITY.md`](./SECURITY.md).
263
311
 
264
312
  ## License
265
313
 
@@ -12,6 +12,8 @@ export declare class AgentRuntime {
12
12
  private bashConcurrency;
13
13
  private toolMap;
14
14
  private limits;
15
+ private extensionDispatch;
16
+ private steeringQueue;
15
17
  private systemPrompt;
16
18
  private contextWindow;
17
19
  private contextUsed;
@@ -22,9 +24,18 @@ export declare class AgentRuntime {
22
24
  getContextUsage(): ContextUsage;
23
25
  private emitContextUsage;
24
26
  setEffort(effort: string): void;
27
+ setSteeringQueue(queue: string[]): void;
28
+ compactSession(session: {
29
+ messages: Message[];
30
+ appendMessage(m: Message): void;
31
+ appendEvent?(partial: any): any;
32
+ }, focus?: string): Promise<string | null>;
33
+ /** Replace the toolset, e.g. after MCP discovery or a list_changed refresh. */
34
+ setTools(tools: AgentRuntimeOptions["tools"]): void;
25
35
  getEffort(): string;
26
36
  runAgentTurn(input: string, session: {
27
37
  messages: Message[];
28
38
  appendMessage(m: Message): void;
39
+ appendEvent?(partial: any): any;
29
40
  }, signal: AbortSignal): Promise<string>;
30
41
  }
@@ -1,6 +1,8 @@
1
1
  import { DEFAULT_LIMITS } from "../types.js";
2
2
  import { executeToolBatch } from "../runtime/scheduler.js";
3
3
  import { ContextManager } from "../context/manager.js";
4
+ import { runModelTurn } from "./model-turn.js";
5
+ import { TurnRecorder } from "./turn-recorder.js";
4
6
  export class AgentRuntime {
5
7
  provider;
6
8
  model;
@@ -13,6 +15,8 @@ export class AgentRuntime {
13
15
  bashConcurrency;
14
16
  toolMap;
15
17
  limits;
18
+ extensionDispatch;
19
+ steeringQueue;
16
20
  systemPrompt;
17
21
  contextWindow;
18
22
  contextUsed;
@@ -38,6 +42,8 @@ export class AgentRuntime {
38
42
  });
39
43
  this.toolMap = new Map(options.tools.map((t) => [t.definition.name, t]));
40
44
  this.limits = { ...DEFAULT_LIMITS, ...(options.limits ?? {}) };
45
+ this.extensionDispatch = options.extensionDispatch;
46
+ this.steeringQueue = options.steeringQueue;
41
47
  this.systemPrompt = options.systemPrompt ?? "You are a coding assistant with tools: read, write, edit, bash, webfetch, websearch.";
42
48
  this.contextWindow = options.contextWindow;
43
49
  this.contextManager.setModelContextWindow(options.contextWindow);
@@ -76,14 +82,55 @@ export class AgentRuntime {
76
82
  setEffort(effort) {
77
83
  this.effort = effort;
78
84
  }
85
+ setSteeringQueue(queue) {
86
+ this.steeringQueue = queue;
87
+ }
88
+ async compactSession(session, focus) {
89
+ const summary = await this.contextManager.compactHistory(this.workspace, session.messages, focus);
90
+ if (summary) {
91
+ this.contextManager.setSessionMemory(summary, this.contextManager.getLastCompactionCoveredCount());
92
+ if (typeof session.appendEvent === "function") {
93
+ session.appendEvent({
94
+ type: "context_compacted",
95
+ summary,
96
+ coveredCount: this.contextManager.getLastCompactionCoveredCount(),
97
+ });
98
+ }
99
+ }
100
+ return summary;
101
+ }
102
+ /** Replace the toolset, e.g. after MCP discovery or a list_changed refresh. */
103
+ setTools(tools) {
104
+ this.tools = tools;
105
+ this.toolMap = new Map(tools.map((t) => [t.definition.name, t]));
106
+ }
79
107
  getEffort() {
80
108
  return this.effort;
81
109
  }
82
110
  async runAgentTurn(input, session, signal) {
111
+ // Restore any persisted session memory for resumed sessions.
112
+ const events = session.events;
113
+ if (Array.isArray(events)) {
114
+ const latest = [...events].reverse().find((e) => e?.type === "context_compacted");
115
+ this.contextManager.setSessionMemory(typeof latest?.summary === "string" ? latest.summary : null, typeof latest?.coveredCount === "number" ? latest.coveredCount : undefined);
116
+ }
83
117
  session.appendMessage({ role: "user", content: [{ type: "text", text: input }] });
118
+ await this.extensionDispatch?.("turn_start", { type: "turn_start" });
84
119
  let totalToolCalls = 0;
120
+ let forceContinue = false;
85
121
  const start = Date.now();
86
122
  for (let iteration = 0; iteration < this.limits.maxIterations; iteration++) {
123
+ forceContinue = false;
124
+ // Drain any steering messages into the session before the next model turn.
125
+ if (this.steeringQueue && this.steeringQueue.length > 0) {
126
+ while (this.steeringQueue.length > 0) {
127
+ const msg = this.steeringQueue.shift();
128
+ session.appendMessage({
129
+ role: "user",
130
+ content: [{ type: "text", text: msg }],
131
+ });
132
+ }
133
+ }
87
134
  if (signal.aborted) {
88
135
  throw new Error("USER_CANCELLED");
89
136
  }
@@ -96,49 +143,58 @@ export class AgentRuntime {
96
143
  // assistant response reports fresh numbers.
97
144
  this.contextUsed = undefined;
98
145
  this.emitContextUsage();
146
+ const summary = this.contextManager.consumeCompactionSummary();
147
+ const coveredCount = this.contextManager.consumeCompactionCoveredCount();
148
+ if (summary && typeof session.appendEvent === "function") {
149
+ session.appendEvent({
150
+ type: "context_compacted",
151
+ summary,
152
+ coveredCount,
153
+ });
154
+ }
99
155
  }
100
- const request = {
156
+ let response;
157
+ let streamedText = "";
158
+ await this.extensionDispatch?.("before_provider_request", {
159
+ type: "before_provider_request",
160
+ messages,
161
+ tools: this.tools.map((t) => t.definition),
162
+ });
163
+ for await (const ev of runModelTurn({
164
+ provider: this.provider,
101
165
  model: this.model,
102
166
  messages,
103
167
  tools: this.tools.map((t) => t.definition),
168
+ effort: this.effort,
104
169
  signal,
105
- options: { effort: this.effort },
106
- };
107
- let response;
108
- let streamedText = "";
109
- if (typeof this.provider.stream === "function") {
110
- // Stream tokens as they arrive.
111
- for await (const ev of this.provider.stream(request)) {
112
- if (ev.type === "thinking") {
113
- this.onEvent?.({ type: "thinking_delta", text: ev.text });
114
- }
115
- else if (ev.type === "text") {
116
- streamedText += ev.text;
117
- this.onEvent?.({ type: "assistant_delta", text: ev.text });
118
- }
119
- else if (ev.type === "done") {
120
- response = ev.response;
121
- }
170
+ })) {
171
+ if (ev.type === "thinking_delta") {
172
+ this.onEvent?.({ type: "thinking_delta", text: ev.text });
122
173
  }
123
- if (!response) {
124
- // Fallback if stream ended without done
125
- response = {
126
- message: { role: "assistant", content: streamedText ? [{ type: "text", text: streamedText }] : undefined, toolCalls: undefined },
127
- toolCalls: [],
128
- finishReason: "stop",
129
- };
174
+ else if (ev.type === "assistant_delta") {
175
+ streamedText += ev.text;
176
+ this.onEvent?.({ type: "assistant_delta", text: ev.text });
177
+ }
178
+ else if (ev.type === "assistant_text") {
179
+ streamedText = ev.text;
180
+ this.onEvent?.({ type: "assistant_text", text: ev.text });
181
+ }
182
+ else if (ev.type === "final") {
183
+ response = ev.response;
184
+ streamedText = ev.streamedText;
130
185
  }
131
186
  }
132
- else {
133
- response = await this.provider.generate(request);
134
- const t = response.message.content?.map((p) => (p.type === "text" ? p.text : "")).join("") ?? "";
135
- if (t)
136
- this.onEvent?.({ type: "assistant_text", text: t });
137
- streamedText = t;
138
- }
139
- session.appendMessage(response.message);
187
+ await this.extensionDispatch?.("after_provider_response", {
188
+ type: "after_provider_response",
189
+ response,
190
+ });
191
+ const recorder = new TurnRecorder({
192
+ session,
193
+ onEvent: this.onEvent,
194
+ extensionDispatch: this.extensionDispatch,
195
+ });
196
+ const text = await recorder.recordAssistant(response, streamedText);
140
197
  if (response.usage) {
141
- this.onEvent?.({ type: "usage", input: response.usage.input, output: response.usage.output });
142
198
  const used = (response.usage.input ?? 0) + (response.usage.output ?? 0);
143
199
  if (used > 0) {
144
200
  // The latest assistant step's prompt + generated tokens is the
@@ -147,14 +203,61 @@ export class AgentRuntime {
147
203
  this.emitContextUsage();
148
204
  }
149
205
  }
150
- const text = streamedText || (response.message.content?.map((p) => (p.type === "text" ? p.text : "")).join("") ?? "");
151
206
  const toolCalls = response.toolCalls ?? [];
152
207
  if (toolCalls.length === 0) {
153
- this.onEvent?.({ type: "final", text });
208
+ // turn_end / agent_before_settle continuation handling.
209
+ let continued = false;
210
+ const turnResults = await this.extensionDispatch?.("turn_end", {
211
+ type: "turn_end",
212
+ });
213
+ for (const r of turnResults ?? []) {
214
+ if (r && r.continue)
215
+ continued = true;
216
+ }
217
+ if (continued) {
218
+ forceContinue = true;
219
+ continue;
220
+ }
221
+ const settleResults = await this.extensionDispatch?.("agent_before_settle", { type: "agent_before_settle" });
222
+ for (const r of settleResults ?? []) {
223
+ if (r && r.continue)
224
+ continued = true;
225
+ }
226
+ if (continued) {
227
+ forceContinue = true;
228
+ continue;
229
+ }
230
+ await this.extensionDispatch?.("agent_settled", {
231
+ type: "agent_settled",
232
+ });
154
233
  return text;
155
234
  }
156
- for (const c of toolCalls)
157
- this.onEvent?.({ type: "tool_call", id: c.id, name: c.name, args: c.arguments });
235
+ // Emit tool_call events; allow mutation and block decisions.
236
+ const blocked = new Set();
237
+ let terminate = false;
238
+ for (const call of toolCalls) {
239
+ const payload = {
240
+ type: "tool_call",
241
+ toolCallId: call.id,
242
+ name: call.name,
243
+ input: call.arguments,
244
+ };
245
+ const results = await this.extensionDispatch?.("tool_call", payload);
246
+ // Apply in-place mutation.
247
+ if (payload.input && payload.input !== call.arguments) {
248
+ call.arguments = payload.input;
249
+ }
250
+ for (const r of results ?? []) {
251
+ if (r && typeof r === "object") {
252
+ if (r.block)
253
+ blocked.add(call.id);
254
+ if (r.terminate)
255
+ terminate = true;
256
+ if (r.input)
257
+ call.arguments = r.input;
258
+ }
259
+ }
260
+ }
158
261
  totalToolCalls += toolCalls.length;
159
262
  if (totalToolCalls > this.limits.maxToolCalls) {
160
263
  return "TASK_BUDGET_REACHED";
@@ -173,24 +276,112 @@ export class AgentRuntime {
173
276
  stream,
174
277
  text,
175
278
  });
279
+ void this.extensionDispatch?.("tool_execution_update", {
280
+ type: "tool_execution_update",
281
+ toolCallId: call.id,
282
+ name: call.name,
283
+ stream,
284
+ text,
285
+ });
176
286
  },
177
287
  };
178
- const results = await executeToolBatch(toolCalls, this.toolMap, ctx, this.limits.maxParallelToolCalls, signal, undefined, this.bashConcurrency);
179
- for (const r of results) {
180
- session.appendMessage({
181
- role: "tool",
182
- toolCallId: r.toolCallId,
183
- name: r.name,
184
- content: r.content,
288
+ // Wrapped tool map emits tool_execution_start/end.
289
+ const wrappedToolMap = new Map(this.toolMap);
290
+ for (const [name, tool] of this.toolMap) {
291
+ wrappedToolMap.set(name, {
292
+ ...tool,
293
+ execute: async (call, ctx2) => {
294
+ await this.extensionDispatch?.("tool_execution_start", {
295
+ type: "tool_execution_start",
296
+ toolCallId: call.id,
297
+ name: call.name,
298
+ args: call.arguments,
299
+ });
300
+ let result;
301
+ try {
302
+ result = await tool.execute(call, ctx2);
303
+ }
304
+ catch (err) {
305
+ result = {
306
+ toolCallId: call.id,
307
+ name: call.name,
308
+ content: `COMMAND_FAILED: ${err instanceof Error ? err.message : String(err)}`,
309
+ isError: true,
310
+ };
311
+ }
312
+ await this.extensionDispatch?.("tool_execution_end", {
313
+ type: "tool_execution_end",
314
+ toolCallId: call.id,
315
+ name: call.name,
316
+ result,
317
+ isError: result?.isError,
318
+ });
319
+ return result;
320
+ },
321
+ });
322
+ }
323
+ const blockedResults = new Map();
324
+ for (const call of toolCalls) {
325
+ if (!blocked.has(call.id))
326
+ continue;
327
+ const reason = "blocked by extension";
328
+ const r = {
329
+ toolCallId: call.id,
330
+ name: call.name,
331
+ content: reason,
332
+ isError: true,
333
+ };
334
+ blockedResults.set(call.id, r);
335
+ await this.extensionDispatch?.("tool_execution_start", {
336
+ type: "tool_execution_start",
337
+ toolCallId: call.id,
338
+ name: call.name,
339
+ args: call.arguments,
340
+ });
341
+ await this.extensionDispatch?.("tool_execution_end", {
342
+ type: "tool_execution_end",
343
+ toolCallId: call.id,
344
+ name: call.name,
345
+ result: r,
346
+ isError: true,
185
347
  });
186
- this.onEvent?.({
348
+ }
349
+ const execCalls = toolCalls.filter((c) => !blocked.has(c.id));
350
+ const execResults = await executeToolBatch(execCalls, wrappedToolMap, ctx, this.limits.maxParallelToolCalls, signal, undefined, this.bashConcurrency);
351
+ const resultMap = new Map();
352
+ for (const r of blockedResults.values())
353
+ resultMap.set(r.toolCallId, r);
354
+ for (const r of execResults) {
355
+ // Emit tool_result event and allow patches.
356
+ const payload = {
187
357
  type: "tool_result",
188
358
  toolCallId: r.toolCallId,
189
359
  name: r.name,
190
360
  content: r.content,
361
+ details: r.details,
362
+ structuredContent: r.structuredContent,
191
363
  isError: r.isError,
192
- diff: r.diff,
364
+ usage: r.usage,
365
+ };
366
+ await this.extensionDispatch?.("tool_result", payload);
367
+ r.content = payload.content ?? r.content;
368
+ r.details = payload.details ?? r.details;
369
+ r.structuredContent = payload.structuredContent ?? r.structuredContent;
370
+ r.isError = payload.isError ?? r.isError;
371
+ r.usage = payload.usage ?? r.usage;
372
+ resultMap.set(r.toolCallId, r);
373
+ }
374
+ const orderedResults = toolCalls
375
+ .map((c) => resultMap.get(c.id))
376
+ .filter(Boolean);
377
+ for (const r of orderedResults) {
378
+ recorder.recordToolResult(r);
379
+ }
380
+ if (terminate) {
381
+ await this.extensionDispatch?.("agent_settled", {
382
+ type: "agent_settled",
193
383
  });
384
+ return text;
194
385
  }
195
386
  }
196
387
  return "TASK_BUDGET_REACHED";