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.
- package/README.md +389 -11
- package/dist/src/cli/cli.js +115 -6
- package/dist/src/cli/main.js +0 -0
- package/dist/src/llm/openai.js +65 -4
- package/dist/src/localpi/acp.js +118 -0
- package/dist/src/localpi/catalog.js +79 -7
- package/dist/src/localpi/catppuccin.js +64 -0
- package/dist/src/localpi/llama-server.js +72 -39
- package/dist/src/localpi/model-profile.js +4 -0
- package/dist/src/localpi/options.js +129 -9
- package/dist/src/localpi/provider-registry.js +51 -3
- package/dist/src/localpi/runtime-connection.js +11 -8
- package/dist/src/localpi/runtime.js +12 -7
- package/dist/src/localpi/settings-state.js +13 -3
- package/dist/src/pi/app.js +13 -6
- package/dist/src/pi/extension-sources/continue-on-truncation.js +55 -0
- package/dist/src/pi/extension-sources/settings-file.js +31 -0
- package/dist/src/pi/extension-sources/status-line.js +424 -0
- package/dist/src/pi/extension-sources/thinking-control.js +4 -47
- package/dist/src/pi/extension-sources/token-status.js +545 -116
- package/dist/src/pi/extension-sources/tool-approval.js +155 -14
- package/dist/src/pi/extensions.js +55 -12
- package/dist/src/pi/skills.js +24 -0
- package/dist/src/pi/theme.js +107 -0
- package/docs/2026-06-16-startup-model-and-thinking-control-plan.md +39 -11
- package/docs/2026-09-23-acp-mode-plan.md +111 -0
- package/docs/2026-09-24-continue-on-truncation-plan.md +115 -0
- package/docs/design-principles.md +114 -0
- package/docs/implementation-plan.md +33 -0
- package/docs/runtime-specification.md +98 -4
- package/package.json +11 -7
- 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.
|
|
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/
|
|
9
|
+
"url": "git+https://github.com/osolmaz/localpi.git"
|
|
10
10
|
},
|
|
11
11
|
"bugs": {
|
|
12
|
-
"url": "https://github.com/
|
|
12
|
+
"url": "https://github.com/osolmaz/localpi/issues"
|
|
13
13
|
},
|
|
14
|
-
"homepage": "https://github.com/
|
|
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.
|
|
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
|
-
"@
|
|
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
|
-
}
|