phern 0.1.0__tar.gz
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.
- phern-0.1.0/.gitignore +31 -0
- phern-0.1.0/LICENSE +21 -0
- phern-0.1.0/PKG-INFO +297 -0
- phern-0.1.0/README.md +254 -0
- phern-0.1.0/pyproject.toml +139 -0
- phern-0.1.0/src/ph_app/__init__.py +5 -0
- phern-0.1.0/src/ph_app/__main__.py +15 -0
- phern-0.1.0/src/ph_app/adapters/__init__.py +5 -0
- phern-0.1.0/src/ph_app/adapters/_http.py +231 -0
- phern-0.1.0/src/ph_app/adapters/_media.py +181 -0
- phern-0.1.0/src/ph_app/adapters/anthropic.py +635 -0
- phern-0.1.0/src/ph_app/adapters/google.py +785 -0
- phern-0.1.0/src/ph_app/adapters/openai_compatible.py +884 -0
- phern-0.1.0/src/ph_app/adapters/sse.py +58 -0
- phern-0.1.0/src/ph_app/agents.py +713 -0
- phern-0.1.0/src/ph_app/attach.py +160 -0
- phern-0.1.0/src/ph_app/attachments.py +179 -0
- phern-0.1.0/src/ph_app/cli.py +875 -0
- phern-0.1.0/src/ph_app/console.py +181 -0
- phern-0.1.0/src/ph_app/daemon/__init__.py +32 -0
- phern-0.1.0/src/ph_app/daemon/cancelsafe.py +163 -0
- phern-0.1.0/src/ph_app/daemon/cards.py +97 -0
- phern-0.1.0/src/ph_app/daemon/client.py +295 -0
- phern-0.1.0/src/ph_app/daemon/duplex.py +389 -0
- phern-0.1.0/src/ph_app/daemon/follow.py +234 -0
- phern-0.1.0/src/ph_app/daemon/framing.py +134 -0
- phern-0.1.0/src/ph_app/daemon/frontend.py +284 -0
- phern-0.1.0/src/ph_app/daemon/launch.py +184 -0
- phern-0.1.0/src/ph_app/daemon/projections.py +283 -0
- phern-0.1.0/src/ph_app/daemon/recovery.py +270 -0
- phern-0.1.0/src/ph_app/daemon/server.py +1505 -0
- phern-0.1.0/src/ph_app/daemon/supervisor.py +1541 -0
- phern-0.1.0/src/ph_app/modes/__init__.py +20 -0
- phern-0.1.0/src/ph_app/modes/json_mode.py +68 -0
- phern-0.1.0/src/ph_app/modes/print_mode.py +67 -0
- phern-0.1.0/src/ph_app/modes/rpc_mode.py +208 -0
- phern-0.1.0/src/ph_app/modes/transcript_mode.py +94 -0
- phern-0.1.0/src/ph_app/params.py +205 -0
- phern-0.1.0/src/ph_app/payloads.py +717 -0
- phern-0.1.0/src/ph_app/profiles/anthropic.yaml +9 -0
- phern-0.1.0/src/ph_app/profiles/deepseek.yaml +16 -0
- phern-0.1.0/src/ph_app/profiles/google.yaml +18 -0
- phern-0.1.0/src/ph_app/profiles/llama.yaml +118 -0
- phern-0.1.0/src/ph_app/profiles/rlm-stable.yaml +67 -0
- phern-0.1.0/src/ph_app/profiles/tui.yaml +36 -0
- phern-0.1.0/src/ph_app/profiles.py +414 -0
- phern-0.1.0/src/ph_app/protocol.py +639 -0
- phern-0.1.0/src/ph_app/py.typed +0 -0
- phern-0.1.0/src/ph_app/runtime.py +154 -0
- phern-0.1.0/src/ph_app/sessions.py +251 -0
- phern-0.1.0/src/ph_app/shell.py +221 -0
- phern-0.1.0/src/ph_app/trust.py +80 -0
- phern-0.1.0/src/ph_app/tui/__init__.py +21 -0
- phern-0.1.0/src/ph_app/tui/adapter.py +1203 -0
- phern-0.1.0/src/ph_app/tui/app.py +882 -0
- phern-0.1.0/src/ph_app/tui/autocomplete.py +163 -0
- phern-0.1.0/src/ph_app/tui/commands.py +221 -0
- phern-0.1.0/src/ph_app/tui/config.py +169 -0
- phern-0.1.0/src/ph_app/tui/frontend.py +224 -0
- phern-0.1.0/src/ph_app/tui/modals/__init__.py +1 -0
- phern-0.1.0/src/ph_app/tui/modals/approval.py +188 -0
- phern-0.1.0/src/ph_app/tui/modals/ask_user.py +95 -0
- phern-0.1.0/src/ph_app/tui/modals/base.py +230 -0
- phern-0.1.0/src/ph_app/tui/modals/login.py +81 -0
- phern-0.1.0/src/ph_app/tui/modals/pickers.py +226 -0
- phern-0.1.0/src/ph_app/tui/modals/trust.py +55 -0
- phern-0.1.0/src/ph_app/tui/remote.py +771 -0
- phern-0.1.0/src/ph_app/tui/screens.py +141 -0
- phern-0.1.0/src/ph_app/tui/state.py +311 -0
- phern-0.1.0/src/ph_app/tui/terminal.py +49 -0
- phern-0.1.0/src/ph_app/tui/themes/__init__.py +230 -0
- phern-0.1.0/src/ph_app/tui/themes/high-contrast.json +20 -0
- phern-0.1.0/src/ph_app/tui/themes/ph-dark.json +20 -0
- phern-0.1.0/src/ph_app/tui/themes/ph-light.json +20 -0
- phern-0.1.0/src/ph_app/tui/trajectory.py +533 -0
- phern-0.1.0/src/ph_app/tui/trajectory_app.py +164 -0
- phern-0.1.0/src/ph_app/tui/trajectory_screen.py +271 -0
- phern-0.1.0/src/ph_app/tui/widgets/__init__.py +1 -0
- phern-0.1.0/src/ph_app/tui/widgets/prompt.py +232 -0
- phern-0.1.0/src/ph_app/tui/widgets/selection.py +197 -0
- phern-0.1.0/src/ph_app/tui/widgets/status.py +370 -0
- phern-0.1.0/src/ph_app/tui/widgets/trajectory.py +301 -0
- phern-0.1.0/src/ph_app/tui/widgets/transcript.py +563 -0
- phern-0.1.0/src/ph_app/verbs.py +253 -0
- phern-0.1.0/src/ph_app/web/__init__.py +26 -0
- phern-0.1.0/src/ph_app/web/serve.py +450 -0
- phern-0.1.0/src/ph_app/wire.py +246 -0
- phern-0.1.0/src/ph_app/workspaces.py +186 -0
- phern-0.1.0/tests/__snapshots__/test_tui_snapshot/test_a_code_cell_shows_its_program_and_what_it_cost.raw +131 -0
- phern-0.1.0/tests/__snapshots__/test_tui_snapshot/test_a_code_mode_card_lists_its_dispatches.raw +114 -0
- phern-0.1.0/tests/__snapshots__/test_tui_snapshot/test_a_compaction_marker_keeps_what_it_replaced.raw +129 -0
- phern-0.1.0/tests/__snapshots__/test_tui_snapshot/test_a_failed_tool_card_and_an_error_row.raw +114 -0
- phern-0.1.0/tests/__snapshots__/test_tui_snapshot/test_a_settled_tool_card.raw +114 -0
- phern-0.1.0/tests/__snapshots__/test_tui_snapshot/test_every_theme_renders[high-contrast].raw +95 -0
- phern-0.1.0/tests/__snapshots__/test_tui_snapshot/test_every_theme_renders[ph-light].raw +95 -0
- phern-0.1.0/tests/__snapshots__/test_tui_snapshot/test_markup_in_user_text_renders_literally.raw +103 -0
- phern-0.1.0/tests/__snapshots__/test_tui_snapshot/test_streaming_assistant_text.raw +112 -0
- phern-0.1.0/tests/__snapshots__/test_tui_snapshot/test_the_full_app_shell.raw +155 -0
- phern-0.1.0/tests/app_fixtures.py +87 -0
- phern-0.1.0/tests/daemon_helpers.py +352 -0
- phern-0.1.0/tests/test_adapters.py +1094 -0
- phern-0.1.0/tests/test_agents_cli.py +892 -0
- phern-0.1.0/tests/test_app_layering.py +121 -0
- phern-0.1.0/tests/test_attach.py +205 -0
- phern-0.1.0/tests/test_attachments_cli.py +195 -0
- phern-0.1.0/tests/test_cancelsafe.py +287 -0
- phern-0.1.0/tests/test_cli.py +1097 -0
- phern-0.1.0/tests/test_daemon.py +1820 -0
- phern-0.1.0/tests/test_daemon_asks.py +402 -0
- phern-0.1.0/tests/test_daemon_attachments.py +329 -0
- phern-0.1.0/tests/test_daemon_framing.py +97 -0
- phern-0.1.0/tests/test_daemon_invariants.py +244 -0
- phern-0.1.0/tests/test_daemon_launch.py +199 -0
- phern-0.1.0/tests/test_daemon_lifetime.py +283 -0
- phern-0.1.0/tests/test_daemon_methods.py +348 -0
- phern-0.1.0/tests/test_daemon_mutations.py +190 -0
- phern-0.1.0/tests/test_daemon_projections.py +752 -0
- phern-0.1.0/tests/test_daemon_shell.py +308 -0
- phern-0.1.0/tests/test_modes.py +304 -0
- phern-0.1.0/tests/test_non_guarantees.py +174 -0
- phern-0.1.0/tests/test_payloads.py +254 -0
- phern-0.1.0/tests/test_protocol.py +138 -0
- phern-0.1.0/tests/test_trajectory.py +470 -0
- phern-0.1.0/tests/test_trajectory_view.py +470 -0
- phern-0.1.0/tests/test_tui_adapter.py +667 -0
- phern-0.1.0/tests/test_tui_code_cell.py +308 -0
- phern-0.1.0/tests/test_tui_copy.py +185 -0
- phern-0.1.0/tests/test_tui_frames.py +217 -0
- phern-0.1.0/tests/test_tui_pilot.py +982 -0
- phern-0.1.0/tests/test_tui_remote.py +595 -0
- phern-0.1.0/tests/test_tui_screens.py +360 -0
- phern-0.1.0/tests/test_tui_snapshot.py +255 -0
- phern-0.1.0/tests/test_tui_state.py +355 -0
- phern-0.1.0/tests/test_uploads.py +451 -0
- phern-0.1.0/tests/test_uploads_google.py +606 -0
- phern-0.1.0/tests/test_uploads_openai.py +334 -0
- phern-0.1.0/tests/test_web.py +446 -0
- phern-0.1.0/tests/test_workspaces_cli.py +225 -0
- phern-0.1.0/tests/tui_helpers.py +199 -0
phern-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Build and environment
|
|
2
|
+
.venv/
|
|
3
|
+
dist/
|
|
4
|
+
build/
|
|
5
|
+
*.egg-info/
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
jjt/
|
|
9
|
+
w2/
|
|
10
|
+
|
|
11
|
+
# Tooling caches
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.coverage
|
|
16
|
+
# Written by the guest's subprocess collectors and combined at the end of the
|
|
17
|
+
# run; see `conftest.GUEST_COVERAGE_RC`.
|
|
18
|
+
.coverage-guest*
|
|
19
|
+
htmlcov/
|
|
20
|
+
# Dropped at the repo root by pytest-textual-snapshot when a snapshot test
|
|
21
|
+
# fails. The reference snapshots under `__snapshots__/` are the committed
|
|
22
|
+
# expectation; this is the diff viewer for a run that did not match one.
|
|
23
|
+
snapshot_report.html
|
|
24
|
+
|
|
25
|
+
# Reference checkouts of the upstream projects this port reads from. Vendored
|
|
26
|
+
# locally so the plans' citations are verifiable; never part of this repo.
|
|
27
|
+
sources/
|
|
28
|
+
|
|
29
|
+
# Local scratch
|
|
30
|
+
.ph/
|
|
31
|
+
*.local.yaml
|
phern-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Charles Tabor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
phern-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: phern
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: pH — a Python agent harness with no privileged core: every part is a row in a profile.
|
|
5
|
+
Project-URL: Homepage, https://github.com/chastabor/pH
|
|
6
|
+
Project-URL: Repository, https://github.com/chastabor/pH
|
|
7
|
+
Project-URL: Documentation, https://github.com/chastabor/pH/blob/main/docs/README.md
|
|
8
|
+
Project-URL: Issues, https://github.com/chastabor/pH/issues
|
|
9
|
+
Author: Charles Tabor
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent,cli,code-mode,harness,llm,plugin,react,tui
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Software Development
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Requires-Dist: anyio<5,>=4.4
|
|
25
|
+
Requires-Dist: filelock>=3.15
|
|
26
|
+
Requires-Dist: httpx>=0.27
|
|
27
|
+
Requires-Dist: ph-code-graph==0.1.0
|
|
28
|
+
Requires-Dist: ph-core==0.1.0
|
|
29
|
+
Requires-Dist: ph-rlm==0.1.0
|
|
30
|
+
Requires-Dist: ph-runtime-guest==0.1.0
|
|
31
|
+
Requires-Dist: ph-stabilize==0.1.0
|
|
32
|
+
Requires-Dist: ph-text-index==0.1.0
|
|
33
|
+
Requires-Dist: rich>=13.0
|
|
34
|
+
Requires-Dist: textual<9,>=8.2.5
|
|
35
|
+
Requires-Dist: typer>=0.12
|
|
36
|
+
Provides-Extra: local
|
|
37
|
+
Requires-Dist: ph-text-index[local]==0.1.0; extra == 'local'
|
|
38
|
+
Provides-Extra: otel
|
|
39
|
+
Requires-Dist: ph-core[otel]==0.1.0; extra == 'otel'
|
|
40
|
+
Provides-Extra: web
|
|
41
|
+
Requires-Dist: textual-serve>=1.1.3; extra == 'web'
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# phern
|
|
45
|
+
|
|
46
|
+
*The `phern` command: four output modes, the Textual TUI, the browser tab, the
|
|
47
|
+
daemon and its client — and the three real model wires.*
|
|
48
|
+
|
|
49
|
+
`ph-core` is the harness; this is the thing a person runs. It owns no seam and
|
|
50
|
+
publishes no service that a profile could not do without: what it contributes is
|
|
51
|
+
**front ends** (print, json, transcript, rpc, tui, web, trajectory), the
|
|
52
|
+
**supervisor** that lets a run outlive the terminal that started it, the
|
|
53
|
+
**adapters** that speak Anthropic, Gemini and the OpenAI-compatible wire, and
|
|
54
|
+
the **profile table** that composes everything installed into a name you can
|
|
55
|
+
type.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv tool install ./packages/phern # the CLI and core alone, about 60 MB
|
|
59
|
+
phern --profile llama --provider llama --model <model> -p "what is in this repo?"
|
|
60
|
+
phern --profile tui --provider llama --model <model> --mode tui
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Installed on its own it composes seven profiles — `anthropic`, `base`,
|
|
64
|
+
`deepseek`, `google`, `headless`, `llama` and `tui`. The `rlm*` profiles need
|
|
65
|
+
bundles it deliberately does not depend on; see *Profiles* below.
|
|
66
|
+
|
|
67
|
+
## The commands
|
|
68
|
+
|
|
69
|
+
| command | what it does |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `phern -p "…"` | one prompt, one answer. `-a/--attach FILE` sends a file with it, repeatable |
|
|
72
|
+
| `phern --mode json \| transcript \| rpc \| tui \| web \| trajectory` | what reaches stdout, or which front end runs |
|
|
73
|
+
| `phern doctor [--profile …]` | the three path roots, then mount a profile and let every row report what activated |
|
|
74
|
+
| `phern events [--json]` | the event producer/consumer matrix, generated from the declaration registry |
|
|
75
|
+
| `phern config [--row id] [--all]` | what every row accepts, its default, and what this profile sets. Composed, not mounted: it starts no agent and opens no session |
|
|
76
|
+
| `phern --dump-config` | the composed rows in order, before anything runs |
|
|
77
|
+
| `phern daemon` | run the supervisor |
|
|
78
|
+
| `phern agents …` | the client that talks to it: bare (list roots), `status`, `attach`, `send`, `schedule`, `doctor`, `shutdown` |
|
|
79
|
+
| `phern workspaces gc` | the git trees agents left behind, across every stored session. Reports by default; collects with `--remove` |
|
|
80
|
+
| `phern attachments gc` | media no stored session references. Same rule |
|
|
81
|
+
|
|
82
|
+
`phern doctor` reports what *this invocation's* flags and environment would
|
|
83
|
+
produce; `phern agents doctor` reports what is **in force** in the running daemon.
|
|
84
|
+
They are different questions and it is worth knowing which one you asked.
|
|
85
|
+
|
|
86
|
+
### The modes, and why the choice matters
|
|
87
|
+
|
|
88
|
+
`json` and `rpc` emit the session log's **own** envelopes rather than a per-mode
|
|
89
|
+
rendering (I-7), so a wrapper streaming from a pipe and a tool reading the
|
|
90
|
+
stored JSONL parse one format. `transcript` reads `session.transcript()` — what
|
|
91
|
+
the person saw — so a compacted conversation still shows the turns they actually
|
|
92
|
+
had, where the model surface deliberately shadows replaced ranges. `text` is the
|
|
93
|
+
default. `trajectory` audits a stored log and mounts nothing.
|
|
94
|
+
|
|
95
|
+
## The TUI
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
phern --mode tui --provider llama --model <model> # new session
|
|
99
|
+
phern --mode tui --resume 20260908T041607-1c5576 # reopen one
|
|
100
|
+
phern --mode tui --no-spawn # refuse rather than start a daemon
|
|
101
|
+
phern --mode tui --keep-daemon # the daemon it starts is a service
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The front end talks to the daemon over `$PH_RUNTIME/daemon.sock` and starts an
|
|
105
|
+
ephemeral one if nothing is listening, so closing the TUI does not end the turn.
|
|
106
|
+
|
|
107
|
+
Every front-end action is a `TuiVerb` reachable three ways — a slash command
|
|
108
|
+
registered into `ctx.commands` (so the palette lists it, the prompt completes
|
|
109
|
+
it, and `command/run` records it), a Textual action, and a key:
|
|
110
|
+
|
|
111
|
+
| slash | key | |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `/commands` | `ctrl+k` | browse every command, the daemon's and the client's |
|
|
114
|
+
| `/model` | `ctrl+p` | choose the provider and model |
|
|
115
|
+
| `/theme` | `ctrl+y` | `ph-dark`, `ph-light`, `high-contrast` |
|
|
116
|
+
| `/sessions` | `ctrl+r` | reopen a stored session |
|
|
117
|
+
| `/permissions` | `ctrl+g` | change what pH may do without asking |
|
|
118
|
+
| `/thinking` | `ctrl+t` | show or hide the model's reasoning |
|
|
119
|
+
| `/tools` | `ctrl+o` | show or hide tool results |
|
|
120
|
+
| `/sidebar` | `ctrl+b` | show or hide the sidebar |
|
|
121
|
+
| `/login` | | provide a provider credential for this process |
|
|
122
|
+
| `/attach <path> …` | | attach files to the next prompt |
|
|
123
|
+
| `/quit` | `ctrl+d` | |
|
|
124
|
+
|
|
125
|
+
Rows contribute their own screens and commands through `ctx.tui_screens` and
|
|
126
|
+
`ctx.commands`, and they arrive with the same three routes — `/trajectory` is
|
|
127
|
+
one such screen, contributed by `tui.yaml` rather than built in, and it takes
|
|
128
|
+
its key and palette entry away with it if the row is removed.
|
|
129
|
+
|
|
130
|
+
### `$PH_HOME/tui.json`
|
|
131
|
+
|
|
132
|
+
Keybindings, theme and preferences. **Never hard-code a key check**: every
|
|
133
|
+
binding is a named field whose name doubles as the Textual binding id, so one
|
|
134
|
+
`set_keymap` rebinds the whole app, screens and modals included — a contributed
|
|
135
|
+
screen's key is remappable exactly like a built-in.
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"theme": "ph-dark",
|
|
140
|
+
"sidebar": "right",
|
|
141
|
+
"turn_notification": "bell",
|
|
142
|
+
"show_thinking": true,
|
|
143
|
+
"show_tool_results": true,
|
|
144
|
+
"keybindings": { "command_palette": "ctrl+k", "quit": "ctrl+d" }
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
A file that fails to parse does not stop the TUI starting: it launches on
|
|
149
|
+
defaults and says so. Unrecognized keys are kept rather than dropped, because
|
|
150
|
+
one of them is a plugin screen's binding id.
|
|
151
|
+
|
|
152
|
+
## The browser tab
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
phern --mode web --provider llama --model <model> # 127.0.0.1:8000
|
|
156
|
+
phern --mode web --port 8080 --open
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`textual-serve` runs a real `PHTuiApp` as a subprocess and streams its frames,
|
|
160
|
+
so the browser shows the terminal — one layout, not two. Three things it prints
|
|
161
|
+
before it binds, each of which is load-bearing:
|
|
162
|
+
|
|
163
|
+
- **the token in the URL is the whole authentication story** — no TLS, no users;
|
|
164
|
+
treat the URL like the terminal it came from;
|
|
165
|
+
- **every tab of one launch is on one session** (a second tab joins the
|
|
166
|
+
conversation; a second `phern --mode web` is a new one);
|
|
167
|
+
- `--host` anything but loopback reaches anyone who can route to the port.
|
|
168
|
+
|
|
169
|
+
Needs the `web` extra: `uv tool install "phern[web]"`, or
|
|
170
|
+
`uv tool install "./packages/phern[web]"`. Without it, `--mode web` fails with
|
|
171
|
+
the install line rather than an `ImportError`.
|
|
172
|
+
|
|
173
|
+
## The daemon
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
phern daemon --profile tui --provider llama --model <model>
|
|
177
|
+
phern daemon --max-concurrent-children 6 # across every root; the rest queue
|
|
178
|
+
phern daemon --passivate-after 30 # minutes of quiet before a root is released, or `off`
|
|
179
|
+
phern daemon --ephemeral # exit once no client, root or appointment needs it
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
One `anyio` task per root, and the client is not it: a root owns a mounted
|
|
183
|
+
profile, a session, an agent and a queue, and its task drains that queue whether
|
|
184
|
+
or not anybody is attached. Attaching subscribes a connection to the root's
|
|
185
|
+
events; detaching unsubscribes it. **Neither starts nor stops the work**, which
|
|
186
|
+
is why leaving is free.
|
|
187
|
+
|
|
188
|
+
The socket is per boot and per user. A stale socket from a crashed daemon is
|
|
189
|
+
cleared; a live one is refused rather than stolen. On Linux, note that logind
|
|
190
|
+
reaps `$XDG_RUNTIME_DIR` at logout for a user who is not lingering — a daemon
|
|
191
|
+
can keep running and *lose its socket*, after which every client is told to
|
|
192
|
+
start one and the leases the first still holds will refuse it. `phern doctor` and
|
|
193
|
+
`phern daemon` say so in advance; `loginctl enable-linger` is the fix.
|
|
194
|
+
|
|
195
|
+
## Profiles
|
|
196
|
+
|
|
197
|
+
The table lives in `src/ph_app/profiles.py`, the documents in
|
|
198
|
+
`src/ph_app/profiles/`.
|
|
199
|
+
|
|
200
|
+
| `--profile` | layers | credential |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `base` | `ph-base` | — |
|
|
203
|
+
| `headless` | `base` + the fake adapter | — |
|
|
204
|
+
| `tui` | `headless` + `tui.yaml` (writable workspace, `/trajectory`, `ask_user` armed) | — |
|
|
205
|
+
|
|
206
|
+
Every row of that table also layers the **`stabilize` bundle**, so every profile
|
|
207
|
+
compacts at 85% of its window and offers `/compact`. It is layered *optionally*:
|
|
208
|
+
an install without `ph-stabilize` composes each of these profiles unchanged and
|
|
209
|
+
simply never compacts, which is what keeps the lean `uv tool install
|
|
210
|
+
./packages/phern` target whole. `phern doctor` reports which rows activated.
|
|
211
|
+
| `llama` | `base` + a local llama.cpp route | `LLAMA_API_KEY` (a formality llama.cpp ignores, but it must be set) |
|
|
212
|
+
| `deepseek` | `base` + DeepSeek over the OpenAI-compatible wire | `DEEPSEEK_API_KEY` |
|
|
213
|
+
| `anthropic` | `base` + the messages API | `ANTHROPIC_API_KEY` |
|
|
214
|
+
| `google` | `base` + Gemini (the one route declaring video, so `uploads` has a provider) | `GEMINI_API_KEY` |
|
|
215
|
+
| `rlm` | `tui` + the `rlm` bundle | needs `ph-rlm` |
|
|
216
|
+
| `rlm-stable` | `rlm` + `stabilize`, gates on | needs `ph-rlm`, `ph-stabilize` |
|
|
217
|
+
| `rlm-indexed` | `rlm-stable` + `code-graph` + `text-index` | needs both plugin distributions too |
|
|
218
|
+
|
|
219
|
+
**A profile is offered only if every layer it names resolves.**
|
|
220
|
+
`available_profiles()` asks exactly the question `resolve_profile` will answer,
|
|
221
|
+
so a `--help` line and a command line cannot disagree; an install missing a
|
|
222
|
+
bundle sees no `rlm-indexed` rather than one that fails at mount, and the
|
|
223
|
+
refusal names the package to install. A `--profile` value that is a path to a
|
|
224
|
+
`.yaml` is used directly, which is what makes a scenario test or a one-off
|
|
225
|
+
deployment one file rather than an install step.
|
|
226
|
+
|
|
227
|
+
## Adjusting it
|
|
228
|
+
|
|
229
|
+
The same three layers every pH row uses — the shipped documents, your overlay at
|
|
230
|
+
`$PH_HOME/profiles/<name>.yaml`, then `--patch` for one run. What is specific to
|
|
231
|
+
this package is the rows it registers:
|
|
232
|
+
|
|
233
|
+
| row | config | default |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| `llm-anthropic` | `provider`, `baseUrl`, `apiKeyEnv`, `contextWindow`, `defaultMaxTokens`, `accepts`, `maxAttachmentBytes`, `uploads`, `filesBeta`, `maxImageEdge`, `usableImageEdge`, `cacheControl` | `anthropic`, `ANTHROPIC_API_KEY`, `200000`, `8192`, images + PDF, 5 MiB, prompt caching on |
|
|
236
|
+
| `llm-google` | the same shape plus `uploadReadyMs` | `1048576` window, images/audio/video/PDF, 20 MiB, video routed through the Files API |
|
|
237
|
+
| `llm-openai-compatible` | `profiles: [ProviderProfile, …]` | one entry per route; this is the row `llama` and `deepseek` insert |
|
|
238
|
+
| `tui-screen-trajectory` | — | contributed by `tui.yaml` |
|
|
239
|
+
|
|
240
|
+
```yaml
|
|
241
|
+
# $PH_HOME/profiles/anthropic.yaml — a different model ceiling, caching off
|
|
242
|
+
- id: llm-anthropic
|
|
243
|
+
config:
|
|
244
|
+
provider: anthropic
|
|
245
|
+
apiKeyEnv: ANTHROPIC_API_KEY
|
|
246
|
+
contextWindow: 200000
|
|
247
|
+
defaultMaxTokens: 16384
|
|
248
|
+
cacheControl: false
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**A list is replaced, not merged**, and on these rows that is the trap worth
|
|
252
|
+
naming: an overlay that restates one field of an OpenAI-compatible route must
|
|
253
|
+
restate the whole route entry, or it inherits `api.openai.com` from the row
|
|
254
|
+
default — a local deployment quietly calling a hosted provider. `llama.yaml`'s
|
|
255
|
+
own comments carry the worked example.
|
|
256
|
+
|
|
257
|
+
An `apiKeyEnv` is a **name**, never an interpolation. The adapter resolves it at
|
|
258
|
+
the request edge, so the value never enters a row, an event, or a child process
|
|
259
|
+
(I-3). `${env:…}` interpolation is available for everything that is not a
|
|
260
|
+
secret, with `${env:VAR:-default}` for a fallback.
|
|
261
|
+
|
|
262
|
+
## Limitations, and things that are deliberate
|
|
263
|
+
|
|
264
|
+
- **`ph_app` must not import `ph_rlm` or `ph_stabilize`**, although the `phern`
|
|
265
|
+
distribution depends on both wheels. It composes their profiles through the
|
|
266
|
+
`ph.bundles` entry-point group and reads their events (`subagent/*`) without
|
|
267
|
+
importing the rows that emit them — `tests/test_app_layering.py` walks the AST
|
|
268
|
+
and enforces it. Depending on a wheel decides what is installed; it does not
|
|
269
|
+
grant a name this module may write down, and keeping the two separate is what
|
|
270
|
+
lets a bundle nobody here ships compose a profile on the same footing.
|
|
271
|
+
- **Textual is pinned at both ends (`>=8.2.5,<9`), and both ends are
|
|
272
|
+
load-bearing.** The floor is where the suite actually passes: `MarkdownStream`
|
|
273
|
+
(the transcript's streaming append) does not exist before Textual 5, and the
|
|
274
|
+
committed SVG snapshots then narrow it further — 8.2.4 fails one of them. The
|
|
275
|
+
ceiling guards `add_binding`, which writes through a private `BindingsMap`
|
|
276
|
+
because the public `bind()` drops the id that `set_keymap` matches on — and
|
|
277
|
+
that id is what makes a contributed screen's key rebindable like every other.
|
|
278
|
+
- **The web UI is the terminal in a canvas**, not an HTML renderer on the same
|
|
279
|
+
view model. That is the trade that buys layout parity by construction.
|
|
280
|
+
- **A client reads no session file at all.** After P5-14 the daemon holds them
|
|
281
|
+
and answers `sessions/browse`, which is what makes a front end on another
|
|
282
|
+
machine possible and stops a client and a daemon disagreeing about which
|
|
283
|
+
`$PH_HOME` they meant.
|
|
284
|
+
- **The daemon's method vocabulary is typed and closed** (`ph_app.verbs`,
|
|
285
|
+
`ph_app.params`, `ph_app.payloads`): a field a method does not take is a
|
|
286
|
+
refusal that names the field, not a silent drop. A client that believed it had
|
|
287
|
+
said something is the failure a typed edge exists to end.
|
|
288
|
+
|
|
289
|
+
## Tests
|
|
290
|
+
|
|
291
|
+
`tests/` — 36 modules covering the CLI, the four non-interactive modes, the
|
|
292
|
+
daemon (framing, methods, mutations, lifetime, recovery, asks), the TUI (pilot
|
|
293
|
+
runs, remote verbs, screens, state) and the three adapters, plus committed
|
|
294
|
+
Textual SVG snapshots. `test_non_guarantees.py` is worth reading first: it pins
|
|
295
|
+
the *claims* rather than a mechanism — the sentences a person reads before
|
|
296
|
+
deciding whether to run six agents under one daemon, and that `phern doctor` and
|
|
297
|
+
`phern agents doctor` still print them.
|
phern-0.1.0/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# phern
|
|
2
|
+
|
|
3
|
+
*The `phern` command: four output modes, the Textual TUI, the browser tab, the
|
|
4
|
+
daemon and its client — and the three real model wires.*
|
|
5
|
+
|
|
6
|
+
`ph-core` is the harness; this is the thing a person runs. It owns no seam and
|
|
7
|
+
publishes no service that a profile could not do without: what it contributes is
|
|
8
|
+
**front ends** (print, json, transcript, rpc, tui, web, trajectory), the
|
|
9
|
+
**supervisor** that lets a run outlive the terminal that started it, the
|
|
10
|
+
**adapters** that speak Anthropic, Gemini and the OpenAI-compatible wire, and
|
|
11
|
+
the **profile table** that composes everything installed into a name you can
|
|
12
|
+
type.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv tool install ./packages/phern # the CLI and core alone, about 60 MB
|
|
16
|
+
phern --profile llama --provider llama --model <model> -p "what is in this repo?"
|
|
17
|
+
phern --profile tui --provider llama --model <model> --mode tui
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Installed on its own it composes seven profiles — `anthropic`, `base`,
|
|
21
|
+
`deepseek`, `google`, `headless`, `llama` and `tui`. The `rlm*` profiles need
|
|
22
|
+
bundles it deliberately does not depend on; see *Profiles* below.
|
|
23
|
+
|
|
24
|
+
## The commands
|
|
25
|
+
|
|
26
|
+
| command | what it does |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `phern -p "…"` | one prompt, one answer. `-a/--attach FILE` sends a file with it, repeatable |
|
|
29
|
+
| `phern --mode json \| transcript \| rpc \| tui \| web \| trajectory` | what reaches stdout, or which front end runs |
|
|
30
|
+
| `phern doctor [--profile …]` | the three path roots, then mount a profile and let every row report what activated |
|
|
31
|
+
| `phern events [--json]` | the event producer/consumer matrix, generated from the declaration registry |
|
|
32
|
+
| `phern config [--row id] [--all]` | what every row accepts, its default, and what this profile sets. Composed, not mounted: it starts no agent and opens no session |
|
|
33
|
+
| `phern --dump-config` | the composed rows in order, before anything runs |
|
|
34
|
+
| `phern daemon` | run the supervisor |
|
|
35
|
+
| `phern agents …` | the client that talks to it: bare (list roots), `status`, `attach`, `send`, `schedule`, `doctor`, `shutdown` |
|
|
36
|
+
| `phern workspaces gc` | the git trees agents left behind, across every stored session. Reports by default; collects with `--remove` |
|
|
37
|
+
| `phern attachments gc` | media no stored session references. Same rule |
|
|
38
|
+
|
|
39
|
+
`phern doctor` reports what *this invocation's* flags and environment would
|
|
40
|
+
produce; `phern agents doctor` reports what is **in force** in the running daemon.
|
|
41
|
+
They are different questions and it is worth knowing which one you asked.
|
|
42
|
+
|
|
43
|
+
### The modes, and why the choice matters
|
|
44
|
+
|
|
45
|
+
`json` and `rpc` emit the session log's **own** envelopes rather than a per-mode
|
|
46
|
+
rendering (I-7), so a wrapper streaming from a pipe and a tool reading the
|
|
47
|
+
stored JSONL parse one format. `transcript` reads `session.transcript()` — what
|
|
48
|
+
the person saw — so a compacted conversation still shows the turns they actually
|
|
49
|
+
had, where the model surface deliberately shadows replaced ranges. `text` is the
|
|
50
|
+
default. `trajectory` audits a stored log and mounts nothing.
|
|
51
|
+
|
|
52
|
+
## The TUI
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
phern --mode tui --provider llama --model <model> # new session
|
|
56
|
+
phern --mode tui --resume 20260908T041607-1c5576 # reopen one
|
|
57
|
+
phern --mode tui --no-spawn # refuse rather than start a daemon
|
|
58
|
+
phern --mode tui --keep-daemon # the daemon it starts is a service
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The front end talks to the daemon over `$PH_RUNTIME/daemon.sock` and starts an
|
|
62
|
+
ephemeral one if nothing is listening, so closing the TUI does not end the turn.
|
|
63
|
+
|
|
64
|
+
Every front-end action is a `TuiVerb` reachable three ways — a slash command
|
|
65
|
+
registered into `ctx.commands` (so the palette lists it, the prompt completes
|
|
66
|
+
it, and `command/run` records it), a Textual action, and a key:
|
|
67
|
+
|
|
68
|
+
| slash | key | |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `/commands` | `ctrl+k` | browse every command, the daemon's and the client's |
|
|
71
|
+
| `/model` | `ctrl+p` | choose the provider and model |
|
|
72
|
+
| `/theme` | `ctrl+y` | `ph-dark`, `ph-light`, `high-contrast` |
|
|
73
|
+
| `/sessions` | `ctrl+r` | reopen a stored session |
|
|
74
|
+
| `/permissions` | `ctrl+g` | change what pH may do without asking |
|
|
75
|
+
| `/thinking` | `ctrl+t` | show or hide the model's reasoning |
|
|
76
|
+
| `/tools` | `ctrl+o` | show or hide tool results |
|
|
77
|
+
| `/sidebar` | `ctrl+b` | show or hide the sidebar |
|
|
78
|
+
| `/login` | | provide a provider credential for this process |
|
|
79
|
+
| `/attach <path> …` | | attach files to the next prompt |
|
|
80
|
+
| `/quit` | `ctrl+d` | |
|
|
81
|
+
|
|
82
|
+
Rows contribute their own screens and commands through `ctx.tui_screens` and
|
|
83
|
+
`ctx.commands`, and they arrive with the same three routes — `/trajectory` is
|
|
84
|
+
one such screen, contributed by `tui.yaml` rather than built in, and it takes
|
|
85
|
+
its key and palette entry away with it if the row is removed.
|
|
86
|
+
|
|
87
|
+
### `$PH_HOME/tui.json`
|
|
88
|
+
|
|
89
|
+
Keybindings, theme and preferences. **Never hard-code a key check**: every
|
|
90
|
+
binding is a named field whose name doubles as the Textual binding id, so one
|
|
91
|
+
`set_keymap` rebinds the whole app, screens and modals included — a contributed
|
|
92
|
+
screen's key is remappable exactly like a built-in.
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"theme": "ph-dark",
|
|
97
|
+
"sidebar": "right",
|
|
98
|
+
"turn_notification": "bell",
|
|
99
|
+
"show_thinking": true,
|
|
100
|
+
"show_tool_results": true,
|
|
101
|
+
"keybindings": { "command_palette": "ctrl+k", "quit": "ctrl+d" }
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
A file that fails to parse does not stop the TUI starting: it launches on
|
|
106
|
+
defaults and says so. Unrecognized keys are kept rather than dropped, because
|
|
107
|
+
one of them is a plugin screen's binding id.
|
|
108
|
+
|
|
109
|
+
## The browser tab
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
phern --mode web --provider llama --model <model> # 127.0.0.1:8000
|
|
113
|
+
phern --mode web --port 8080 --open
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`textual-serve` runs a real `PHTuiApp` as a subprocess and streams its frames,
|
|
117
|
+
so the browser shows the terminal — one layout, not two. Three things it prints
|
|
118
|
+
before it binds, each of which is load-bearing:
|
|
119
|
+
|
|
120
|
+
- **the token in the URL is the whole authentication story** — no TLS, no users;
|
|
121
|
+
treat the URL like the terminal it came from;
|
|
122
|
+
- **every tab of one launch is on one session** (a second tab joins the
|
|
123
|
+
conversation; a second `phern --mode web` is a new one);
|
|
124
|
+
- `--host` anything but loopback reaches anyone who can route to the port.
|
|
125
|
+
|
|
126
|
+
Needs the `web` extra: `uv tool install "phern[web]"`, or
|
|
127
|
+
`uv tool install "./packages/phern[web]"`. Without it, `--mode web` fails with
|
|
128
|
+
the install line rather than an `ImportError`.
|
|
129
|
+
|
|
130
|
+
## The daemon
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
phern daemon --profile tui --provider llama --model <model>
|
|
134
|
+
phern daemon --max-concurrent-children 6 # across every root; the rest queue
|
|
135
|
+
phern daemon --passivate-after 30 # minutes of quiet before a root is released, or `off`
|
|
136
|
+
phern daemon --ephemeral # exit once no client, root or appointment needs it
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
One `anyio` task per root, and the client is not it: a root owns a mounted
|
|
140
|
+
profile, a session, an agent and a queue, and its task drains that queue whether
|
|
141
|
+
or not anybody is attached. Attaching subscribes a connection to the root's
|
|
142
|
+
events; detaching unsubscribes it. **Neither starts nor stops the work**, which
|
|
143
|
+
is why leaving is free.
|
|
144
|
+
|
|
145
|
+
The socket is per boot and per user. A stale socket from a crashed daemon is
|
|
146
|
+
cleared; a live one is refused rather than stolen. On Linux, note that logind
|
|
147
|
+
reaps `$XDG_RUNTIME_DIR` at logout for a user who is not lingering — a daemon
|
|
148
|
+
can keep running and *lose its socket*, after which every client is told to
|
|
149
|
+
start one and the leases the first still holds will refuse it. `phern doctor` and
|
|
150
|
+
`phern daemon` say so in advance; `loginctl enable-linger` is the fix.
|
|
151
|
+
|
|
152
|
+
## Profiles
|
|
153
|
+
|
|
154
|
+
The table lives in `src/ph_app/profiles.py`, the documents in
|
|
155
|
+
`src/ph_app/profiles/`.
|
|
156
|
+
|
|
157
|
+
| `--profile` | layers | credential |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `base` | `ph-base` | — |
|
|
160
|
+
| `headless` | `base` + the fake adapter | — |
|
|
161
|
+
| `tui` | `headless` + `tui.yaml` (writable workspace, `/trajectory`, `ask_user` armed) | — |
|
|
162
|
+
|
|
163
|
+
Every row of that table also layers the **`stabilize` bundle**, so every profile
|
|
164
|
+
compacts at 85% of its window and offers `/compact`. It is layered *optionally*:
|
|
165
|
+
an install without `ph-stabilize` composes each of these profiles unchanged and
|
|
166
|
+
simply never compacts, which is what keeps the lean `uv tool install
|
|
167
|
+
./packages/phern` target whole. `phern doctor` reports which rows activated.
|
|
168
|
+
| `llama` | `base` + a local llama.cpp route | `LLAMA_API_KEY` (a formality llama.cpp ignores, but it must be set) |
|
|
169
|
+
| `deepseek` | `base` + DeepSeek over the OpenAI-compatible wire | `DEEPSEEK_API_KEY` |
|
|
170
|
+
| `anthropic` | `base` + the messages API | `ANTHROPIC_API_KEY` |
|
|
171
|
+
| `google` | `base` + Gemini (the one route declaring video, so `uploads` has a provider) | `GEMINI_API_KEY` |
|
|
172
|
+
| `rlm` | `tui` + the `rlm` bundle | needs `ph-rlm` |
|
|
173
|
+
| `rlm-stable` | `rlm` + `stabilize`, gates on | needs `ph-rlm`, `ph-stabilize` |
|
|
174
|
+
| `rlm-indexed` | `rlm-stable` + `code-graph` + `text-index` | needs both plugin distributions too |
|
|
175
|
+
|
|
176
|
+
**A profile is offered only if every layer it names resolves.**
|
|
177
|
+
`available_profiles()` asks exactly the question `resolve_profile` will answer,
|
|
178
|
+
so a `--help` line and a command line cannot disagree; an install missing a
|
|
179
|
+
bundle sees no `rlm-indexed` rather than one that fails at mount, and the
|
|
180
|
+
refusal names the package to install. A `--profile` value that is a path to a
|
|
181
|
+
`.yaml` is used directly, which is what makes a scenario test or a one-off
|
|
182
|
+
deployment one file rather than an install step.
|
|
183
|
+
|
|
184
|
+
## Adjusting it
|
|
185
|
+
|
|
186
|
+
The same three layers every pH row uses — the shipped documents, your overlay at
|
|
187
|
+
`$PH_HOME/profiles/<name>.yaml`, then `--patch` for one run. What is specific to
|
|
188
|
+
this package is the rows it registers:
|
|
189
|
+
|
|
190
|
+
| row | config | default |
|
|
191
|
+
|---|---|---|
|
|
192
|
+
| `llm-anthropic` | `provider`, `baseUrl`, `apiKeyEnv`, `contextWindow`, `defaultMaxTokens`, `accepts`, `maxAttachmentBytes`, `uploads`, `filesBeta`, `maxImageEdge`, `usableImageEdge`, `cacheControl` | `anthropic`, `ANTHROPIC_API_KEY`, `200000`, `8192`, images + PDF, 5 MiB, prompt caching on |
|
|
193
|
+
| `llm-google` | the same shape plus `uploadReadyMs` | `1048576` window, images/audio/video/PDF, 20 MiB, video routed through the Files API |
|
|
194
|
+
| `llm-openai-compatible` | `profiles: [ProviderProfile, …]` | one entry per route; this is the row `llama` and `deepseek` insert |
|
|
195
|
+
| `tui-screen-trajectory` | — | contributed by `tui.yaml` |
|
|
196
|
+
|
|
197
|
+
```yaml
|
|
198
|
+
# $PH_HOME/profiles/anthropic.yaml — a different model ceiling, caching off
|
|
199
|
+
- id: llm-anthropic
|
|
200
|
+
config:
|
|
201
|
+
provider: anthropic
|
|
202
|
+
apiKeyEnv: ANTHROPIC_API_KEY
|
|
203
|
+
contextWindow: 200000
|
|
204
|
+
defaultMaxTokens: 16384
|
|
205
|
+
cacheControl: false
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**A list is replaced, not merged**, and on these rows that is the trap worth
|
|
209
|
+
naming: an overlay that restates one field of an OpenAI-compatible route must
|
|
210
|
+
restate the whole route entry, or it inherits `api.openai.com` from the row
|
|
211
|
+
default — a local deployment quietly calling a hosted provider. `llama.yaml`'s
|
|
212
|
+
own comments carry the worked example.
|
|
213
|
+
|
|
214
|
+
An `apiKeyEnv` is a **name**, never an interpolation. The adapter resolves it at
|
|
215
|
+
the request edge, so the value never enters a row, an event, or a child process
|
|
216
|
+
(I-3). `${env:…}` interpolation is available for everything that is not a
|
|
217
|
+
secret, with `${env:VAR:-default}` for a fallback.
|
|
218
|
+
|
|
219
|
+
## Limitations, and things that are deliberate
|
|
220
|
+
|
|
221
|
+
- **`ph_app` must not import `ph_rlm` or `ph_stabilize`**, although the `phern`
|
|
222
|
+
distribution depends on both wheels. It composes their profiles through the
|
|
223
|
+
`ph.bundles` entry-point group and reads their events (`subagent/*`) without
|
|
224
|
+
importing the rows that emit them — `tests/test_app_layering.py` walks the AST
|
|
225
|
+
and enforces it. Depending on a wheel decides what is installed; it does not
|
|
226
|
+
grant a name this module may write down, and keeping the two separate is what
|
|
227
|
+
lets a bundle nobody here ships compose a profile on the same footing.
|
|
228
|
+
- **Textual is pinned at both ends (`>=8.2.5,<9`), and both ends are
|
|
229
|
+
load-bearing.** The floor is where the suite actually passes: `MarkdownStream`
|
|
230
|
+
(the transcript's streaming append) does not exist before Textual 5, and the
|
|
231
|
+
committed SVG snapshots then narrow it further — 8.2.4 fails one of them. The
|
|
232
|
+
ceiling guards `add_binding`, which writes through a private `BindingsMap`
|
|
233
|
+
because the public `bind()` drops the id that `set_keymap` matches on — and
|
|
234
|
+
that id is what makes a contributed screen's key rebindable like every other.
|
|
235
|
+
- **The web UI is the terminal in a canvas**, not an HTML renderer on the same
|
|
236
|
+
view model. That is the trade that buys layout parity by construction.
|
|
237
|
+
- **A client reads no session file at all.** After P5-14 the daemon holds them
|
|
238
|
+
and answers `sessions/browse`, which is what makes a front end on another
|
|
239
|
+
machine possible and stops a client and a daemon disagreeing about which
|
|
240
|
+
`$PH_HOME` they meant.
|
|
241
|
+
- **The daemon's method vocabulary is typed and closed** (`ph_app.verbs`,
|
|
242
|
+
`ph_app.params`, `ph_app.payloads`): a field a method does not take is a
|
|
243
|
+
refusal that names the field, not a silent drop. A client that believed it had
|
|
244
|
+
said something is the failure a typed edge exists to end.
|
|
245
|
+
|
|
246
|
+
## Tests
|
|
247
|
+
|
|
248
|
+
`tests/` — 36 modules covering the CLI, the four non-interactive modes, the
|
|
249
|
+
daemon (framing, methods, mutations, lifetime, recovery, asks), the TUI (pilot
|
|
250
|
+
runs, remote verbs, screens, state) and the three adapters, plus committed
|
|
251
|
+
Textual SVG snapshots. `test_non_guarantees.py` is worth reading first: it pins
|
|
252
|
+
the *claims* rather than a mechanism — the sentences a person reads before
|
|
253
|
+
deciding whether to run six agents under one daemon, and that `phern doctor` and
|
|
254
|
+
`phern agents doctor` still print them.
|