localpi 0.5.0 → 0.6.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 (32) hide show
  1. package/README.md +389 -11
  2. package/dist/src/cli/cli.js +115 -6
  3. package/dist/src/cli/main.js +0 -0
  4. package/dist/src/llm/openai.js +65 -4
  5. package/dist/src/localpi/acp.js +118 -0
  6. package/dist/src/localpi/catalog.js +79 -7
  7. package/dist/src/localpi/catppuccin.js +64 -0
  8. package/dist/src/localpi/llama-server.js +72 -39
  9. package/dist/src/localpi/model-profile.js +4 -0
  10. package/dist/src/localpi/options.js +129 -9
  11. package/dist/src/localpi/provider-registry.js +51 -3
  12. package/dist/src/localpi/runtime-connection.js +11 -8
  13. package/dist/src/localpi/runtime.js +12 -7
  14. package/dist/src/localpi/settings-state.js +13 -3
  15. package/dist/src/pi/app.js +13 -6
  16. package/dist/src/pi/extension-sources/continue-on-truncation.js +55 -0
  17. package/dist/src/pi/extension-sources/settings-file.js +31 -0
  18. package/dist/src/pi/extension-sources/status-line.js +424 -0
  19. package/dist/src/pi/extension-sources/thinking-control.js +4 -47
  20. package/dist/src/pi/extension-sources/token-status.js +545 -116
  21. package/dist/src/pi/extension-sources/tool-approval.js +155 -14
  22. package/dist/src/pi/extensions.js +55 -12
  23. package/dist/src/pi/skills.js +24 -0
  24. package/dist/src/pi/theme.js +107 -0
  25. package/docs/2026-06-16-startup-model-and-thinking-control-plan.md +39 -11
  26. package/docs/2026-09-23-acp-mode-plan.md +111 -0
  27. package/docs/2026-09-24-continue-on-truncation-plan.md +115 -0
  28. package/docs/design-principles.md +114 -0
  29. package/docs/implementation-plan.md +33 -0
  30. package/docs/runtime-specification.md +98 -4
  31. package/package.json +11 -7
  32. package/dist/src/pi/extension-sources/demo-mode.js +0 -110
@@ -0,0 +1,115 @@
1
+ ---
2
+ date: 2026-09-24
3
+ author: Onur Solmaz
4
+ title: Continue on truncation for localpi
5
+ tags:
6
+ - localpi
7
+ - pi
8
+ - plan
9
+ ---
10
+
11
+ # Continue on truncation for localpi
12
+
13
+ ## Status
14
+
15
+ Planned. This document is the selected plan for the feature. It is the only source to follow.
16
+
17
+ ## Goal
18
+
19
+ Give localpi an opt-in way to continue a Pi turn that stopped because it hit the output token limit.
20
+
21
+ A truncated final reply is worthless in a batch run: the model stops mid-answer, the harness ends the
22
+ run, and a verifier scores the half-written result as zero. Measured on the ShellBench Structured
23
+ suite, this happened in 16 of 89 tasks with one model and 6 of 89 with another.
24
+
25
+ The feature is off by default. A normal launch must behave exactly as it does today.
26
+
27
+ ## Design
28
+
29
+ Today localpi declares the served limits itself: `--context-window` and `--max-tokens` (default
30
+ 8192). Pi stops a turn when the reply reaches the declared output cap and reports the turn as
31
+ complete. localpi does nothing about it, so the reply stays truncated.
32
+
33
+ Design rules:
34
+
35
+ - Opt-in only. No flag and no environment variable means the current behavior, byte for byte.
36
+ - Flag, then environment, then default, like every other localpi option.
37
+ - Every default needs a flag or an environment variable, per `docs/design-principles.md`.
38
+ - The guard is a Pi extension that uses only the documented public Pi extension API.
39
+ - One implementation. The harness repository must not carry a second copy afterward.
40
+
41
+ ### Interface
42
+
43
+ | Interface | Meaning |
44
+ | ------------------------------------ | ---------------------------------------------------------------------------- |
45
+ | `--continue-on-truncation <n>` | Continue at most `n` extra times when a turn ends on the output token limit. |
46
+ | `LOCALPI_CONTINUE_ON_TRUNCATION=<n>` | Same meaning from the environment. |
47
+ | absent | Feature off. |
48
+
49
+ `n` must be a positive integer. `0` means off and is allowed only from the environment, so an
50
+ operator can disable an inherited value without dropping the variable. An invalid `n` fails with one
51
+ clear message and exit code 2, like the other option errors.
52
+
53
+ ### Behavior
54
+
55
+ - Detect the truncation stop reason on the turn-end hook. Pi reports a turn that reached the output
56
+ cap as a length stop, so no guessing is needed.
57
+ - Send exactly one follow-up user message that tells the model to continue where it stopped and not
58
+ to repeat earlier text.
59
+ - Count continuations per session. Stop after `n` and let the run end normally, so the feature cannot
60
+ loop forever.
61
+ - Never continue a turn that ended for any other reason, including a normal stop, a tool-only turn,
62
+ an error stop, or a user cancellation.
63
+ - Write warnings and notes to stderr only. stdout stays free for protocol bytes and batch output.
64
+ - The generated extension carries the limit inside its own source, like the other generated extension
65
+ sources, so no extra environment variable reaches the child.
66
+
67
+ ### Where the code goes
68
+
69
+ - `src/pi/extension-sources/continue-on-truncation.ts`: the generated Pi extension source, plus the
70
+ factory that bakes the limit into it.
71
+ - `src/pi/extensions.ts`: add the extension to the bundle only when the feature is enabled.
72
+ - `src/localpi/options.ts`: the `continueOnTruncation` option, its flag, its environment variable, the
73
+ usage line, and its validation.
74
+ - `src/cli/cli.ts`: pass the resolved value into the extension bundle.
75
+
76
+ The extension source follows the existing pattern of the other `extension-sources` modules. The
77
+ optional feature stays out of the runtime resolution path, so discovery, Pi config generation, and
78
+ process launching keep their current separation.
79
+
80
+ ## Deliverables
81
+
82
+ 1. The new extension source module and its wiring.
83
+ 2. The option, the flag, the environment variable, the usage line, and validation.
84
+ 3. Tests: option parsing and validation, the extension bundle containing the guard only when it is
85
+ enabled, a truncated turn producing exactly one continuation message, the continuation count
86
+ stopping at `n`, and a non-truncated turn producing no continuation.
87
+ 4. A README section for the feature.
88
+ 5. An entry in `docs/runtime-specification.md` and a section in `docs/implementation-plan.md`.
89
+
90
+ ## Acceptance
91
+
92
+ - `npm run check` passes, including `prettier --check`, `eslint`, `tsc --noEmit`, the full test
93
+ suite, and the build.
94
+ - The tests prove the off-by-default behavior is unchanged, the enabled behavior continues at most
95
+ `n` times, and a non-truncated turn is untouched.
96
+ - The coverage thresholds stay satisfied.
97
+ - No generated output, model response, session file, or secret is committed.
98
+
99
+ ## Out of scope
100
+
101
+ - Publishing to npm and creating a release.
102
+ - Any change to the harness repository, including its existing guard and its published artifacts.
103
+ - Any change to the thinking budget, the model profile limits, or the ACP mode contract.
104
+ - Any change to a normal launch when the feature is off.
105
+
106
+ ## Constraints
107
+
108
+ - Follow `AGENTS.md`: strict TypeScript, no `any`, validate unknown JSON at the boundary.
109
+ - Keep local model discovery, Pi config generation, and process launching in separate modules.
110
+ - Add or update tests for every behavior change.
111
+ - Keep mutation testing out of the default gate.
112
+ - Keep `@earendil-works/pi-coding-agent` a devDependency on the newest Pi release and keep the
113
+ generated-extension typecheck test.
114
+ - Keep `@osolmaz/pi-factory` on the newest published version.
115
+ - Use Conventional Commits, and add no coding agent branding.
@@ -0,0 +1,114 @@
1
+ # Localpi Design Principles
2
+
3
+ This document records the principles that shape localpi. Use it to decide how a new feature
4
+ should look, and to judge whether a proposed change fits the project.
5
+
6
+ ## Simple
7
+
8
+ Localpi does one job: connect Pi to local inference engines, then get out of the way.
9
+
10
+ - Prefer the smallest change that solves the real problem.
11
+ - Prefer one obvious way to do a thing over three configurable ways.
12
+ - Do not add a second mechanism when the first one only needs a small extension.
13
+ - Keep output short. A status line that reads well at a glance beats a dashboard.
14
+ - Do not build infrastructure for a problem you can observe directly.
15
+
16
+ ## Unopinionated
17
+
18
+ Localpi has a default for every choice, and none of the defaults are permanent.
19
+
20
+ - Pick a sensible default, then make it changeable.
21
+ - Never hard-code one vendor's behavior into another vendor's path.
22
+ - Detect the engine instead of assuming it. When detection is impossible, say so and continue.
23
+ - Report facts, not verdicts. If a model is unloaded, tell the user; do not silently load it.
24
+ - When localpi cannot know something (for example, a token rate before the first token), show
25
+ nothing rather than a guess.
26
+ - Keep measurements honest. Label estimated values as estimates.
27
+
28
+ ## Customizable
29
+
30
+ Every default must have an escape hatch, and the escape hatch must be easy to find.
31
+
32
+ - Follow a fixed precedence order: command-line flag, environment variable, saved setting, default.
33
+ - Save what the user changes during a session so the next launch keeps it.
34
+ - Expose the same setting through the flag, the environment, and the in-session command when that
35
+ costs little.
36
+ - Keep user settings in `<state-dir>/settings.json`. Never scatter settings across files.
37
+ - Never edit files that Pi owns. Write generated parts into the localpi state directory.
38
+
39
+ ## Pi-native
40
+
41
+ Pi is the user interface. Localpi configures Pi and launches it.
42
+
43
+ - Do not replace Pi's TUI, and do not add a parallel UI.
44
+ - Use Pi extension APIs: `setWorkingMessage`, `setStatus`, `appendEntry`, `registerCommand`.
45
+ - Prefer the documented Pi API over a local workaround. If the API is missing, propose it upstream
46
+ instead of building a private channel.
47
+ - Generated extensions must depend only on the public Pi extension API and Node built-ins.
48
+ - Keep generated code readable, because users can open it in `<state-dir>/pi-extensions/`.
49
+
50
+ ## Explicit and safe
51
+
52
+ - Announce what localpi starts, stops, or reuses before doing it.
53
+ - Never stop, unload, or reconfigure a server that localpi did not start.
54
+ - Fail loudly with an actionable message, then stop. Do not guess a fix.
55
+ - Keep tool approval explicit, and report denied calls back to the model.
56
+ - Treat unknown input as unknown. Validate JSON at the boundary, then trust the typed value inside.
57
+
58
+ ## Worked example: the status display
59
+
60
+ The status display shows the same principles in a small feature.
61
+
62
+ 1. **Simple.** One line answers "is this machine keeping up?" The line holds elapsed time, output
63
+ tokens, token rate, and context use. Nothing else.
64
+ 2. **Unopinionated.** Localpi does not impose colors, layout, or thresholds on users. The display
65
+ uses the active theme. Context coloring starts at 80 percent and 95 percent, and those numbers
66
+ are visible in one file.
67
+ 3. **Customizable.** `--stats off|line|full` sets the mode. `LOCALPI_STATS` sets it for a shell.
68
+ `/stats` changes it during a session and saves the result to `<state-dir>/settings.json`.
69
+ `--no-token-status` stays as a short alias for `--stats off`. The tool approval gate works the
70
+ same way, with `/approval ask|allow` and `on`/`off` as aliases: on by default, off on request,
71
+ and saved as the `permission` setting that new sessions start from. The dialog choice to allow
72
+ every tool call for one session is deliberately not saved, because that choice is about the task
73
+ at hand, not about the user's default.
74
+ 4. **Pi-native.** The display is a generated Pi extension. It uses `setFooter` for one status line,
75
+ `setWorkingMessage` for the live line, `appendEntry` for the transcript summary, and
76
+ `registerCommand` for `/stats`. It writes complete lines, so Pi keeps ownership of layout and
77
+ wrapping. It renders the same facts Pi showed, plus the engine next to the model, and it adds no
78
+ row of its own. The status line and the stats line are separate generated files, so the last
79
+ finished turn travels between them through one typed global. Pi loads every extension into one
80
+ process, and a second status item would add the row this design exists to avoid. When the bridge
81
+ is absent, only the rate disappears.
82
+ 5. **Explicit.** Prefill progress comes from the llama.cpp `/slots` endpoint. When that endpoint is
83
+ missing or slow, localpi stops polling and shows elapsed time instead of inventing a percentage.
84
+
85
+ The three modes exist because users want different amounts of information:
86
+
87
+ | Mode | Status line | Live line | Transcript entry |
88
+ | ------ | ----------- | --------- | ---------------- |
89
+ | `off` | Pi's own | no | no |
90
+ | `line` | localpi | yes | no |
91
+ | `full` | localpi | yes | yes |
92
+
93
+ ## Worked example: the default look
94
+
95
+ localpi picks the look and lets the user replace it.
96
+
97
+ 1. **Simple.** One palette file holds the colors. Both the Pi theme file and localpi's own terminal
98
+ colors come from it, so the launcher and the session match.
99
+ 2. **Unopinionated.** Catppuccin Mocha is the default, not a rule. `--no-themes` removes the theme,
100
+ `--use-theme <name>` selects another one, and `NO_COLOR` removes color from localpi's output.
101
+ 3. **Customizable.** The theme file lives in the localpi state directory, so a user can read it,
102
+ copy it, or edit it. Localpi never edits global Pi themes or settings.
103
+ 4. **Explicit.** localpi says which theme it loads only when it changes the session. Colors stay out
104
+ of piped output, so scripts keep the plain text.
105
+
106
+ ## Using the principles
107
+
108
+ Before you add a feature, answer these questions in the pull request:
109
+
110
+ - Which single problem does this solve?
111
+ - Which default do I choose, and how does a user change it?
112
+ - Which existing flag, environment variable, or command already covers part of this?
113
+ - What does the feature show when the information is unavailable?
114
+ - Which generated file or state file does this touch, and is that the right home for it?
@@ -73,3 +73,36 @@ Older workspace wrappers outside this repository still mention `localagent --fin
73
73
  - approval denial in an interactive tool call
74
74
  - token status display in an interactive session
75
75
  - Run `npm run check` before merging implementation changes.
76
+
77
+ ## 8. ACP Mode
78
+
79
+ - [x] Add `--acp` and `LOCALPI_ACP=1`, and keep a normal launch as the default.
80
+ - [x] Pin `pi-acp` in `package.json` and the lockfile. Do not vendor its source.
81
+ - [x] Keep the ACP start in its own module next to the existing launcher.
82
+ - [x] Resolve the model and write the Pi configuration a normal launch writes, then start the adapter with inherited stdio.
83
+ - [x] Point `PI_ACP_PI_COMMAND` at a generated launcher script that execs Pi with the complete launch line of a normal launch, and pass the environment a normal launch uses.
84
+ - [x] Require an explicit model from the flag, environment, or model profile, and fail with one clear message instead of printing a picker.
85
+ - [x] Keep stdout for protocol bytes only, and send diagnostics, warnings, and startup notes to stderr.
86
+ - [x] Refuse a Pi command that is localpi itself, and set `LOCALPI_ACP=0` for the child, so a spawned child cannot re-enter ACP mode.
87
+ - [x] Unit-test the command and environment, stdout purity, the missing-model failure, exit-code propagation, and the absence of ACP re-entry, with a fake adapter child.
88
+ - [x] Document ACP mode in the README and in `docs/runtime-specification.md`.
89
+ - [x] Keep `npm run check` green, and leave the thinking budget and model profile semantics unchanged.
90
+
91
+ ## 9. Continue On Truncation
92
+
93
+ - [x] Add `--continue-on-truncation <n>` and `LOCALPI_CONTINUE_ON_TRUNCATION=<n>`, where `n` is a positive integer and is the maximum number of extra continuations.
94
+ - [x] Keep the feature off by default, so a normal launch, an ACP launch, and demo mode behave exactly as they do today with no flag and no environment variable.
95
+ - [x] Treat `0` from the environment as off, so an inherited value can be disabled without dropping the variable.
96
+ - [x] Keep the usual precedence, command-line flag, environment variable, then default, and add the usage line.
97
+ - [x] Fail an invalid value with one clear message and exit code 2.
98
+ - [x] Add the generated extension source `src/pi/extension-sources/continue-on-truncation.ts`, and bake the continuation limit into the generated source.
99
+ - [x] Include the extension in the bundle only when the feature is enabled, and change nothing in the bundle otherwise.
100
+ - [x] Detect the length stop on the turn-end hook, and continue only for that reason.
101
+ - [x] Leave a truncated turn that already asked for a tool alone, because Pi runs the tool and keeps going on its own.
102
+ - [x] Send exactly one follow-up user message that tells the model to continue where it stopped and not to repeat earlier text.
103
+ - [x] Count continuations per session, and stop after the limit, so the feature cannot loop forever.
104
+ - [x] Never continue a turn that ended for another reason, including a normal stop, a tool-only turn, an error stop, and a user cancellation.
105
+ - [x] Write diagnostics to stderr only, and keep stdout free for protocol bytes and batch output.
106
+ - [x] Unit-test option parsing and validation, the bundle containing the guard only when it is enabled, one continuation message on a truncated turn, the count stopping at the limit, and no continuation on a normal turn.
107
+ - [x] Document the feature in the README and in `docs/runtime-specification.md`.
108
+ - [x] Keep `npm run check` green, and leave the thinking budget, the model profile limits, and the ACP contract unchanged.
@@ -7,8 +7,9 @@ It should make the common local-model path one command while keeping the selecte
7
7
  ## Goals
8
8
 
9
9
  - Run Pi against local open-weight models without hand-editing Pi config.
10
+ - Treat llama.cpp as the default engine and the preferred discovery target.
10
11
  - Discover local providers by default and select from the loaded model catalog.
11
- - Support LM Studio and vLLM as built-in OpenAI-compatible providers.
12
+ - Support llama.cpp, LM Studio, and vLLM as built-in OpenAI-compatible providers.
12
13
  - Keep managed `llama-server` as an optional fallback when no external model is loaded.
13
14
  - Keep the tool generic: no classifier prompts, topic schemas, dataset generation, or final-schema output.
14
15
  - Keep large model memory usage predictable by managing only one localpi-owned `llama-server` process at a time.
@@ -21,16 +22,34 @@ Default runtime.
21
22
 
22
23
  Localpi:
23
24
 
24
- - probes built-in LM Studio and vLLM endpoints
25
+ - probes a running llama.cpp server first, then built-in LM Studio and vLLM endpoints
25
26
  - loads configured OpenAI-compatible providers from `--providers-file`, `LOCALPI_PROVIDERS_FILE`, or `LOCALPI_MODELS_FILE`
26
27
  - includes the localpi-owned `llama-server` catalog as startable fallback entries when available
27
- - selects the only loaded model automatically
28
+ - selects the only loaded model automatically, preferring llama.cpp when several engines have loaded models
28
29
  - opens Pi's native model selector when multiple loaded models are available in an interactive TTY
29
30
  - never prompts in non-interactive runs; automation can pin a model with concrete `--provider` and `--model` values
30
31
  - treats `--provider` without `--model` as catalog scoping, not as a concrete model choice
31
32
  - skips automatic managed `llama-server` fallback when the configured `llama-server` command is unavailable
32
33
  - writes Pi config for all launch-time loaded catalog entries so Pi `/model` can switch among them
33
34
 
35
+ ### `llama-cpp`
36
+
37
+ Default external engine.
38
+
39
+ Localpi:
40
+
41
+ - probes `http://127.0.0.1:8080/v1` by default and is listed first in `auto` discovery
42
+ - reads llama.cpp `/v1/models` entries: a model with `status.value` `loaded`, or with no status, is usable
43
+ - offers a model with `status.value` `unloaded` as startable only when `/props` reports `models_autoload`, because the llama.cpp router loads it on request
44
+ - reports unloaded models that the server will not autoload as a catalog warning instead of claiming they are usable
45
+ - reads llama.cpp `meta.n_ctx` as the model context window when the server reports it
46
+ - never starts, stops, or unloads an external llama.cpp server
47
+
48
+ ```bash
49
+ localpi --runtime llama-cpp
50
+ localpi --runtime llama-cpp --base-url http://127.0.0.1:9931/v1 --model ternary-bonsai-2-27b-pq2_0
51
+ ```
52
+
34
53
  ### `llama-server`
35
54
 
36
55
  Managed runtime.
@@ -44,6 +63,19 @@ Localpi:
44
63
  - writes Pi config that points at that endpoint
45
64
  - stops the old localpi-owned server before starting a different managed model
46
65
  - reports any detected LM Studio loaded models before starting a large managed model
66
+ - passes the reasoning flags of the selected thinking level: `--reasoning off` and no budget when
67
+ thinking is off, and `--reasoning on` with a token budget otherwise
68
+ - caps thinking with `--thinking-budget <n>` or `LOCALPI_THINKING_BUDGET` when set, where `-1` means
69
+ unrestricted and a positive value replaces the budget of the thinking level
70
+ - injects a default message before the end-of-thinking tag when the budget is finite, so a model
71
+ that loops in its thinking still answers
72
+ - rewords that message with `--thinking-budget-message <text>` or `LOCALPI_THINKING_BUDGET_MESSAGE`,
73
+ and passes no message flag when the value is empty
74
+ - records the reasoning mode, the budget, and the message in the managed server metadata, and
75
+ restarts the owned server when a recorded value changes
76
+
77
+ The engine reads the thinking tags from the model template, so localpi passes no tag of its own. A
78
+ budget of `-1` leaves thinking unrestricted.
47
79
 
48
80
  ### LM Studio
49
81
 
@@ -81,7 +113,7 @@ Localpi:
81
113
 
82
114
  ### Configured Providers
83
115
 
84
- Provider registry JSON can define additional OpenAI-compatible providers:
116
+ Provider registry JSON can define additional OpenAI-compatible providers, and can override the built-in `llama-cpp` provider with `type: "llama-cpp"`:
85
117
 
86
118
  ```json
87
119
  {
@@ -91,11 +123,19 @@ Provider registry JSON can define additional OpenAI-compatible providers:
91
123
  "name": "vLLM Qwen",
92
124
  "baseUrl": "http://127.0.0.1:8000/v1",
93
125
  "discover": true
126
+ },
127
+ "llama-cpp": {
128
+ "type": "llama-cpp",
129
+ "name": "llama.cpp",
130
+ "baseUrl": "http://127.0.0.1:9931/v1",
131
+ "discover": true
94
132
  }
95
133
  }
96
134
  }
97
135
  ```
98
136
 
137
+ A `llama-cpp` provider uses the llama.cpp status and autoload rules described in the `llama-cpp` runtime section. When `baseUrl` is omitted, it defaults to `http://127.0.0.1:8080/v1`.
138
+
99
139
  Set `discover: false` when the endpoint should not be probed during startup. Explicit `--provider <id> --model <id>` can still select that provider and generate Pi config.
100
140
 
101
141
  ## Capability Profiles
@@ -124,6 +164,14 @@ When the served model id matches `model` or `id`, localpi uses the profile to ge
124
164
 
125
165
  Name-based capability detection remains fallback behavior. Built-in vLLM Gemma 4 model ids are treated as reasoning-capable with `qwen-chat-template`, matching vLLM Gemma servers launched with `--reasoning-parser gemma4`.
126
166
 
167
+ ### Image input
168
+
169
+ A llama.cpp server reports model architecture in `/v1/models`. Localpi reads `architecture.input_modalities` and writes `input: ["text", "image"]` into the Pi model config when the entry lists `image`, so Pi passes image attachments to that model. Every other model is written as `input: ["text"]`.
170
+
171
+ A model profile states the same fact for a server that reports nothing: add `"image": true` to `capabilities`. The profile wins over the server, so `"image": false` also disables image input for a model that reports it.
172
+
173
+ The server side must hold up its end: the model needs a multimodal projector, and the server must run with `--mmproj <file>.gguf`. A llama.cpp router passes the projector of a model directory to the child server on its own. Test a model with one real image request before you trust it, because a text-only server answers an image request with text about the prompt, not the picture.
174
+
127
175
  ## Model Selection
128
176
 
129
177
  `--model` should accept:
@@ -164,6 +212,49 @@ Localpi appends a short system prompt that tells the model:
164
212
 
165
213
  The prompt should be generic and should not mention localpager, OpenClaw, datasets, or classifier labels.
166
214
 
215
+ ## ACP Mode
216
+
217
+ `localpi --acp` and `LOCALPI_ACP=1` start localpi as an ACP agent, so an ACP client such as an editor can drive the same Pi that a normal launch runs.
218
+
219
+ Localpi:
220
+
221
+ - follows the usual precedence, command-line flag, environment variable, then default, and keeps a normal launch as the default
222
+ - resolves the model and writes the same Pi configuration a normal launch writes before it starts the adapter
223
+ - starts the pinned `pi-acp` adapter from `node_modules` on stdio as a child process with inherited stdio, and does not vendor its source
224
+ - writes a launcher script into `<state-dir>/acp/`, and sets `PI_ACP_PI_COMMAND` to that script, because the adapter starts Pi itself and passes only its own arguments. The script quotes the Pi program and every argument, so a program path with a space survives, and it execs Pi with the complete launch line of a normal launch, so the models file, the settings file, the extensions, the system prompt, the theme, and the tool flags stay the same
225
+ - passes the environment a normal launch uses: the Pi config directory, the provider base URL, the API key name, the thinking level, and the session directory
226
+ - requires an explicit model from the flag, the environment, or a model profile, because there is no TTY, and fails with one clear message instead of printing a picker
227
+ - keeps stdout for protocol bytes only, and writes diagnostics, warnings, and startup notes to stderr
228
+ - refuses `--demo`, a forwarded Pi `--mode`, forwarded Pi session flags, and forwarded prompts, because the adapter owns the session
229
+ - refuses the immediate commands together with a command-line `--acp`, and lets a command-line immediate command win over an environment-set `LOCALPI_ACP=1`, the way `LOCALPI_DEMO` behaves
230
+ - refuses a Pi command that is localpi itself, and sets `LOCALPI_ACP=0` for the child, so a spawned child cannot re-enter ACP mode
231
+ - runs a different adapter build only when `LOCALPI_ACP_ADAPTER` names its entrypoint
232
+ - leaves the thinking budget and the model profile limits unchanged, and never invents a smaller reply cap than the declared one
233
+
234
+ The adapter spawns Pi as `pi --mode rpc --no-themes`, adds `--session <path>` when a session path exists, and does not pass `--no-extensions`, so Pi extension discovery stays enabled. Approval dialogs reach the ACP client, because the adapter forwards Pi extension UI requests as ACP permission requests.
235
+
236
+ Pi has no ACP mode of its own. ACP support always comes from the adapter, and localpi only configures it and launches it.
237
+
238
+ ## Continue On Truncation
239
+
240
+ `localpi --continue-on-truncation <n>` and `LOCALPI_CONTINUE_ON_TRUNCATION=<n>` continue a Pi turn that stopped because it reached the output token limit, so a run finishes its answer instead of ending with a half-written reply.
241
+
242
+ - `n` is a positive integer and is the maximum number of extra continuations
243
+ - the feature is off by default. With no flag and no environment variable, a normal launch, an ACP launch, and demo mode behave exactly as they do today
244
+ - follows the usual precedence, command-line flag, environment variable, then default
245
+ - treats `0` from the environment as off, so an inherited value can be disabled without dropping the variable
246
+ - fails an invalid value with one clear message and exit code 2
247
+ - detects the length stop on the turn-end hook, and continues only for that reason
248
+ - leaves a truncated turn that already asked for a tool alone, because Pi runs the tool and keeps going on its own
249
+ - sends exactly one follow-up user message that tells the model to continue where it stopped and not to repeat earlier text
250
+ - counts continuations per session, and stops after the limit, so the feature cannot loop forever
251
+ - never continues a turn that ended for another reason, including a normal stop, a tool-only turn, an error stop, and a user cancellation
252
+ - writes diagnostics to stderr only, and keeps stdout free for protocol bytes and batch output
253
+ - is not a default extension. Localpi installs the guard extension only when the feature is enabled. When it is enabled, the guard is part of the extensions localpi writes, so an ACP launch uses it too
254
+ - leaves the thinking budget, the model profile limits, and the ACP contract unchanged
255
+
256
+ The guard changes no served limit. It reacts to the stop reason Pi reports, so a run that reached the declared output cap continues inside the declared limits.
257
+
167
258
  ## Out Of Scope
168
259
 
169
260
  - `--final-schema`
@@ -173,3 +264,6 @@ The prompt should be generic and should not mention localpager, OpenClaw, datase
173
264
  - GitHub issue or pull request fetching
174
265
  - reposhell-specific behavior
175
266
  - dataset generation
267
+ - an ACP server implemented inside localpi, because the adapter is a pinned dependency
268
+ - ACP file system or terminal delegation, because Pi reads, writes, and runs commands in its own process
269
+ - interactive model selection in ACP mode, because ACP has no terminal
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "localpi",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Swiss army knife for running Pi with local inference engines.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/dutifuldev/localpi.git"
9
+ "url": "git+https://github.com/osolmaz/localpi.git"
10
10
  },
11
11
  "bugs": {
12
- "url": "https://github.com/dutifuldev/localpi/issues"
12
+ "url": "https://github.com/osolmaz/localpi/issues"
13
13
  },
14
- "homepage": "https://github.com/dutifuldev/localpi#readme",
14
+ "homepage": "https://github.com/osolmaz/localpi#readme",
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
@@ -26,7 +26,8 @@
26
26
  "LICENSE"
27
27
  ],
28
28
  "scripts": {
29
- "build": "tsc -p tsconfig.json",
29
+ "build": "tsc -p tsconfig.json && chmod +x dist/src/cli/main.js",
30
+ "prepare": "npm run build",
30
31
  "format": "prettier --check .",
31
32
  "lint": "eslint .",
32
33
  "typecheck": "tsc --noEmit",
@@ -40,6 +41,7 @@
40
41
  "check": "npm run format && npm run lint && npm run typecheck && npm test && npm run build"
41
42
  },
42
43
  "devDependencies": {
44
+ "@earendil-works/pi-coding-agent": "^0.87.0",
43
45
  "@eslint/js": "^9.0.0",
44
46
  "@stryker-mutator/core": "^9.6.1",
45
47
  "@stryker-mutator/typescript-checker": "^9.6.1",
@@ -48,13 +50,15 @@
48
50
  "@vitest/coverage-v8": "^3.0.0",
49
51
  "eslint": "^9.0.0",
50
52
  "prettier": "^3.0.0",
51
- "slophammer-ts": "0.4.0",
53
+ "slophammer-ts": "0.4.1",
52
54
  "tsx": "^4.19.4",
53
55
  "typescript": "^5.0.0",
54
56
  "typescript-eslint": "^8.0.0",
55
57
  "vitest": "^3.0.0"
56
58
  },
57
59
  "dependencies": {
58
- "@dutifuldev/pi-factory": "^0.1.1"
60
+ "@osolmaz/pi-factory": "^0.7.0",
61
+ "pi-acp": "0.0.33",
62
+ "pi-demo-mode": "github:osolmaz/pi-demo-mode#v0.1.0"
59
63
  }
60
64
  }
@@ -1,110 +0,0 @@
1
- export function demoModeExtensionSource(prompts) {
2
- const initialPromptSource = JSON.stringify(prompts.initial);
3
- const followupPromptSource = JSON.stringify(prompts.followup);
4
- return `import type { ExtensionAPI, ExtensionContext, TurnEndEvent } from "@earendil-works/pi-coding-agent";
5
-
6
- const initialPrompt = ${initialPromptSource};
7
- const followupPrompt = ${followupPromptSource};
8
- const compactAtContextPercent = 70;
9
- const demoCompactionInstructions = [
10
- "Preserve the demo narrative state, named entities, current setting,",
11
- "unresolved plot threads, and latest user direction.",
12
- "Keep the summary concise so the story can continue after compaction."
13
- ].join(" ");
14
-
15
- export default function localpiDemoMode(pi: ExtensionAPI): void {
16
- let started = false;
17
- let stopped = false;
18
- let compacting = false;
19
-
20
- function queueInitialPrompt(): void {
21
- queueMicrotask(() => {
22
- if (!stopped) {
23
- pi.sendUserMessage(initialPrompt);
24
- }
25
- });
26
- }
27
-
28
- function queueFollowup(): void {
29
- queueMicrotask(() => {
30
- if (!stopped && !compacting) {
31
- pi.sendUserMessage(followupPrompt, { deliverAs: "followUp" });
32
- }
33
- });
34
- }
35
-
36
- function compactThenFollowup(ctx: ExtensionContext): void {
37
- if (compacting) {
38
- return;
39
- }
40
- compacting = true;
41
- ctx.compact({
42
- customInstructions: demoCompactionInstructions,
43
- onComplete: () => {
44
- compacting = false;
45
- queueFollowup();
46
- },
47
- onError: (error) => {
48
- compacting = false;
49
- stopped = true;
50
- ctx.ui.notify("Demo compaction failed: " + error.message, "error");
51
- }
52
- });
53
- }
54
-
55
- pi.on("session_start", (event, ctx) => {
56
- if (started || stopped || event.reason !== "startup" || ctx.mode !== "tui") {
57
- return;
58
- }
59
- started = true;
60
- queueInitialPrompt();
61
- });
62
-
63
- pi.on("turn_end", (event, ctx) => {
64
- if (!started || stopped || compacting || ctx.mode !== "tui") {
65
- return;
66
- }
67
- if (event.message.role !== "assistant") {
68
- return;
69
- }
70
- switch (event.message.stopReason) {
71
- case "aborted":
72
- case "error":
73
- stopped = true;
74
- return;
75
- case "toolUse":
76
- return;
77
- }
78
- if (shouldCompactBeforeFollowup(event, ctx)) {
79
- compactThenFollowup(ctx);
80
- return;
81
- }
82
- queueFollowup();
83
- });
84
-
85
- pi.on("session_shutdown", () => {
86
- stopped = true;
87
- });
88
- }
89
-
90
- function shouldCompactBeforeFollowup(event: TurnEndEvent, ctx: ExtensionContext): boolean {
91
- const contextPercent = currentContextPercent(event, ctx);
92
- return contextPercent !== undefined && contextPercent >= compactAtContextPercent;
93
- }
94
-
95
- function currentContextPercent(event: TurnEndEvent, ctx: ExtensionContext): number | undefined {
96
- const usage = ctx.getContextUsage();
97
- if (usage?.percent !== undefined && usage.percent !== null) {
98
- return usage.percent;
99
- }
100
- if (event.message.role !== "assistant") {
101
- return undefined;
102
- }
103
- const contextWindow = ctx.model?.contextWindow;
104
- if (contextWindow === undefined || contextWindow <= 0) {
105
- return undefined;
106
- }
107
- return (event.message.usage.totalTokens / contextWindow) * 100;
108
- }
109
- `;
110
- }