ph-core 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.
- ph_core-0.1.0/.gitignore +28 -0
- ph_core-0.1.0/LICENSE +21 -0
- ph_core-0.1.0/PKG-INFO +239 -0
- ph_core-0.1.0/README.md +205 -0
- ph_core-0.1.0/pyproject.toml +146 -0
- ph_core-0.1.0/src/ph/__init__.py +9 -0
- ph_core-0.1.0/src/ph/agent/__init__.py +33 -0
- ph_core-0.1.0/src/ph/agent/inbox.py +215 -0
- ph_core-0.1.0/src/ph/agent/registry.py +186 -0
- ph_core-0.1.0/src/ph/agent/types.py +214 -0
- ph_core-0.1.0/src/ph/agent_loop/__init__.py +35 -0
- ph_core-0.1.0/src/ph/agent_loop/driver.py +624 -0
- ph_core-0.1.0/src/ph/agent_loop/invariant.py +83 -0
- ph_core-0.1.0/src/ph/bundles/__init__.py +63 -0
- ph_core-0.1.0/src/ph/bundles/base.yaml +335 -0
- ph_core-0.1.0/src/ph/bundles/headless.yaml +10 -0
- ph_core-0.1.0/src/ph/cancel.py +96 -0
- ph_core-0.1.0/src/ph/commands/__init__.py +15 -0
- ph_core-0.1.0/src/ph/commands/autonomous.py +231 -0
- ph_core-0.1.0/src/ph/commands/revert.py +192 -0
- ph_core-0.1.0/src/ph/commands/sandbox.py +359 -0
- ph_core-0.1.0/src/ph/commands/workspaces.py +371 -0
- ph_core-0.1.0/src/ph/cordis/__init__.py +114 -0
- ph_core-0.1.0/src/ph/cordis/catalog.py +170 -0
- ph_core-0.1.0/src/ph/cordis/context.py +1716 -0
- ph_core-0.1.0/src/ph/cordis/errors.py +69 -0
- ph_core-0.1.0/src/ph/cordis/events.py +122 -0
- ph_core-0.1.0/src/ph/cordis/key.py +57 -0
- ph_core-0.1.0/src/ph/cordis/loader.py +775 -0
- ph_core-0.1.0/src/ph/cordis/plugin.py +144 -0
- ph_core-0.1.0/src/ph/json.py +379 -0
- ph_core-0.1.0/src/ph/keys.py +163 -0
- ph_core-0.1.0/src/ph/lingering.py +404 -0
- ph_core-0.1.0/src/ph/llm/__init__.py +105 -0
- ph_core-0.1.0/src/ph/llm/adapter.py +314 -0
- ph_core-0.1.0/src/ph/llm/assembler.py +206 -0
- ph_core-0.1.0/src/ph/llm/dimensions.py +163 -0
- ph_core-0.1.0/src/ph/llm/fake.py +124 -0
- ph_core-0.1.0/src/ph/llm/media.py +317 -0
- ph_core-0.1.0/src/ph/llm/replay.py +188 -0
- ph_core-0.1.0/src/ph/llm/retry.py +124 -0
- ph_core-0.1.0/src/ph/llm/structured.py +249 -0
- ph_core-0.1.0/src/ph/llm/types.py +689 -0
- ph_core-0.1.0/src/ph/orphans.py +309 -0
- ph_core-0.1.0/src/ph/paths.py +350 -0
- ph_core-0.1.0/src/ph/persistence/__init__.py +37 -0
- ph_core-0.1.0/src/ph/persistence/checkpoint_policy.py +83 -0
- ph_core-0.1.0/src/ph/persistence/families.py +86 -0
- ph_core-0.1.0/src/ph/persistence/jsonl.py +455 -0
- ph_core-0.1.0/src/ph/persistence/lease.py +78 -0
- ph_core-0.1.0/src/ph/persistence/lineage.py +285 -0
- ph_core-0.1.0/src/ph/persistence/protocol.py +299 -0
- ph_core-0.1.0/src/ph/persistence/repair.py +260 -0
- ph_core-0.1.0/src/ph/persistence/turso.py +414 -0
- ph_core-0.1.0/src/ph/py.typed +0 -0
- ph_core-0.1.0/src/ph/resources.py +184 -0
- ph_core-0.1.0/src/ph/seams/__init__.py +5 -0
- ph_core-0.1.0/src/ph/seams/_names.py +51 -0
- ph_core-0.1.0/src/ph/seams/_registry.py +222 -0
- ph_core-0.1.0/src/ph/seams/_restriction.py +42 -0
- ph_core-0.1.0/src/ph/seams/approval.py +475 -0
- ph_core-0.1.0/src/ph/seams/attachments.py +596 -0
- ph_core-0.1.0/src/ph/seams/changes.py +418 -0
- ph_core-0.1.0/src/ph/seams/code_runtime.py +324 -0
- ph_core-0.1.0/src/ph/seams/code_runtime_stub.py +89 -0
- ph_core-0.1.0/src/ph/seams/commands.py +282 -0
- ph_core-0.1.0/src/ph/seams/compaction.py +292 -0
- ph_core-0.1.0/src/ph/seams/containment.py +316 -0
- ph_core-0.1.0/src/ph/seams/credentials.py +112 -0
- ph_core-0.1.0/src/ph/seams/diagnostics.py +160 -0
- ph_core-0.1.0/src/ph/seams/fs.py +1093 -0
- ph_core-0.1.0/src/ph/seams/goals.py +365 -0
- ph_core-0.1.0/src/ph/seams/invariants.py +284 -0
- ph_core-0.1.0/src/ph/seams/jobs.py +405 -0
- ph_core-0.1.0/src/ph/seams/permission_presets.py +184 -0
- ph_core-0.1.0/src/ph/seams/sandbox.py +870 -0
- ph_core-0.1.0/src/ph/seams/sandbox_allow.py +82 -0
- ph_core-0.1.0/src/ph/seams/sandbox_egress.py +467 -0
- ph_core-0.1.0/src/ph/seams/sandbox_local.py +886 -0
- ph_core-0.1.0/src/ph/seams/schedule.py +470 -0
- ph_core-0.1.0/src/ph/seams/schedule_index.py +174 -0
- ph_core-0.1.0/src/ph/seams/scope_invariant.py +157 -0
- ph_core-0.1.0/src/ph/seams/settings.py +95 -0
- ph_core-0.1.0/src/ph/seams/shell.py +161 -0
- ph_core-0.1.0/src/ph/seams/skills.py +978 -0
- ph_core-0.1.0/src/ph/seams/skills_invariant.py +59 -0
- ph_core-0.1.0/src/ph/seams/spill.py +263 -0
- ph_core-0.1.0/src/ph/seams/subagents.py +1492 -0
- ph_core-0.1.0/src/ph/seams/subprocess.py +592 -0
- ph_core-0.1.0/src/ph/seams/telemetry.py +249 -0
- ph_core-0.1.0/src/ph/seams/telemetry_otel.py +173 -0
- ph_core-0.1.0/src/ph/seams/token_meter.py +347 -0
- ph_core-0.1.0/src/ph/seams/topology.py +44 -0
- ph_core-0.1.0/src/ph/seams/tui_screens.py +242 -0
- ph_core-0.1.0/src/ph/seams/tui_status.py +204 -0
- ph_core-0.1.0/src/ph/seams/uploads.py +345 -0
- ph_core-0.1.0/src/ph/seams/user_questions.py +266 -0
- ph_core-0.1.0/src/ph/seams/workspace.py +2161 -0
- ph_core-0.1.0/src/ph/seams/workspace_agentfs.py +721 -0
- ph_core-0.1.0/src/ph/seams/workspace_git.py +917 -0
- ph_core-0.1.0/src/ph/seams/workspace_jj.py +1045 -0
- ph_core-0.1.0/src/ph/seams/workspace_provision.py +345 -0
- ph_core-0.1.0/src/ph/seams/workspace_scratch.py +131 -0
- ph_core-0.1.0/src/ph/selectors.py +168 -0
- ph_core-0.1.0/src/ph/session/__init__.py +92 -0
- ph_core-0.1.0/src/ph/session/derive.py +67 -0
- ph_core-0.1.0/src/ph/session/events.py +224 -0
- ph_core-0.1.0/src/ph/session/folds.py +147 -0
- ph_core-0.1.0/src/ph/session/invariant.py +46 -0
- ph_core-0.1.0/src/ph/session/json.py +198 -0
- ph_core-0.1.0/src/ph/session/known_event_types.py +370 -0
- ph_core-0.1.0/src/ph/session/request_header.py +141 -0
- ph_core-0.1.0/src/ph/session/session.py +672 -0
- ph_core-0.1.0/src/ph/session/store.py +434 -0
- ph_core-0.1.0/src/ph/session/surface.py +349 -0
- ph_core-0.1.0/src/ph/system_prompt/__init__.py +25 -0
- ph_core-0.1.0/src/ph/system_prompt/assembly.py +383 -0
- ph_core-0.1.0/src/ph/system_prompt/memory.py +201 -0
- ph_core-0.1.0/src/ph/testing/__init__.py +159 -0
- ph_core-0.1.0/src/ph/testing/anthropic_wire.py +66 -0
- ph_core-0.1.0/src/ph/testing/builders.py +596 -0
- ph_core-0.1.0/src/ph/testing/diagnostics.py +23 -0
- ph_core-0.1.0/src/ph/testing/folds.py +346 -0
- ph_core-0.1.0/src/ph/testing/git.py +91 -0
- ph_core-0.1.0/src/ph/testing/jj.py +82 -0
- ph_core-0.1.0/src/ph/testing/skills.py +58 -0
- ph_core-0.1.0/src/ph/testing/stub_sandbox.py +38 -0
- ph_core-0.1.0/src/ph/testing/stub_subagent.py +96 -0
- ph_core-0.1.0/src/ph/testing/stub_workspace.py +156 -0
- ph_core-0.1.0/src/ph/text.py +123 -0
- ph_core-0.1.0/src/ph/tools/__init__.py +90 -0
- ph_core-0.1.0/src/ph/tools/batch.py +294 -0
- ph_core-0.1.0/src/ph/tools/builtin/__init__.py +5 -0
- ph_core-0.1.0/src/ph/tools/builtin/ask_user.py +128 -0
- ph_core-0.1.0/src/ph/tools/builtin/attach_tool.py +197 -0
- ph_core-0.1.0/src/ph/tools/builtin/bash_tool.py +127 -0
- ph_core-0.1.0/src/ph/tools/builtin/fs_tools.py +279 -0
- ph_core-0.1.0/src/ph/tools/builtin/subagent_task.py +224 -0
- ph_core-0.1.0/src/ph/tools/code_mode.py +554 -0
- ph_core-0.1.0/src/ph/tools/definition.py +739 -0
- ph_core-0.1.0/src/ph/tools/errors.py +125 -0
- ph_core-0.1.0/src/ph/tools/invariant.py +58 -0
- ph_core-0.1.0/src/ph/tools/json_schema.py +296 -0
- ph_core-0.1.0/src/ph/tools/presentation.py +199 -0
- ph_core-0.1.0/src/ph/tools/prompt.py +29 -0
- ph_core-0.1.0/src/ph/tools/registry.py +1167 -0
- ph_core-0.1.0/src/ph/tools/sdk.py +133 -0
- ph_core-0.1.0/src/ph/tools/timeout.py +57 -0
- ph_core-0.1.0/src/ph/wire.py +236 -0
- ph_core-0.1.0/tests/test_agent_loop.py +425 -0
- ph_core-0.1.0/tests/test_ask_user.py +281 -0
- ph_core-0.1.0/tests/test_attach_tool.py +251 -0
- ph_core-0.1.0/tests/test_attachments.py +243 -0
- ph_core-0.1.0/tests/test_attachments_gc.py +292 -0
- ph_core-0.1.0/tests/test_catalog.py +130 -0
- ph_core-0.1.0/tests/test_changes.py +444 -0
- ph_core-0.1.0/tests/test_code_mode.py +676 -0
- ph_core-0.1.0/tests/test_commands_sandbox.py +185 -0
- ph_core-0.1.0/tests/test_commands_workspaces.py +333 -0
- ph_core-0.1.0/tests/test_containment.py +195 -0
- ph_core-0.1.0/tests/test_containment_ladder.py +188 -0
- ph_core-0.1.0/tests/test_cordis_context.py +555 -0
- ph_core-0.1.0/tests/test_cordis_dispatch.py +270 -0
- ph_core-0.1.0/tests/test_cordis_events.py +98 -0
- ph_core-0.1.0/tests/test_cordis_loader.py +610 -0
- ph_core-0.1.0/tests/test_cordis_plugin.py +219 -0
- ph_core-0.1.0/tests/test_derive.py +141 -0
- ph_core-0.1.0/tests/test_diagnostics.py +277 -0
- ph_core-0.1.0/tests/test_dimensions.py +215 -0
- ph_core-0.1.0/tests/test_docs_seams.py +197 -0
- ph_core-0.1.0/tests/test_exhaustive_dispatch.py +147 -0
- ph_core-0.1.0/tests/test_fold_laws.py +318 -0
- ph_core-0.1.0/tests/test_fork.py +323 -0
- ph_core-0.1.0/tests/test_fs.py +736 -0
- ph_core-0.1.0/tests/test_goals.py +297 -0
- ph_core-0.1.0/tests/test_invariants.py +631 -0
- ph_core-0.1.0/tests/test_json.py +285 -0
- ph_core-0.1.0/tests/test_json_schema.py +352 -0
- ph_core-0.1.0/tests/test_keys.py +144 -0
- ph_core-0.1.0/tests/test_layering.py +70 -0
- ph_core-0.1.0/tests/test_lingering.py +253 -0
- ph_core-0.1.0/tests/test_llm.py +198 -0
- ph_core-0.1.0/tests/test_loop_policies.py +289 -0
- ph_core-0.1.0/tests/test_media_degrade.py +283 -0
- ph_core-0.1.0/tests/test_memory.py +231 -0
- ph_core-0.1.0/tests/test_orphans.py +248 -0
- ph_core-0.1.0/tests/test_paths.py +378 -0
- ph_core-0.1.0/tests/test_persistence.py +344 -0
- ph_core-0.1.0/tests/test_persistence_backends.py +1062 -0
- ph_core-0.1.0/tests/test_prefix_stability.py +243 -0
- ph_core-0.1.0/tests/test_registration_ownership.py +1679 -0
- ph_core-0.1.0/tests/test_repair.py +485 -0
- ph_core-0.1.0/tests/test_resources.py +378 -0
- ph_core-0.1.0/tests/test_sandbox_allow.py +426 -0
- ph_core-0.1.0/tests/test_sandbox_egress.py +400 -0
- ph_core-0.1.0/tests/test_sandbox_local.py +659 -0
- ph_core-0.1.0/tests/test_schedule.py +398 -0
- ph_core-0.1.0/tests/test_sdk.py +247 -0
- ph_core-0.1.0/tests/test_seams.py +1336 -0
- ph_core-0.1.0/tests/test_selectors.py +295 -0
- ph_core-0.1.0/tests/test_session_admit.py +126 -0
- ph_core-0.1.0/tests/test_session_append.py +355 -0
- ph_core-0.1.0/tests/test_session_event.py +125 -0
- ph_core-0.1.0/tests/test_skills_progressive.py +603 -0
- ph_core-0.1.0/tests/test_spill_sweep.py +150 -0
- ph_core-0.1.0/tests/test_structured.py +264 -0
- ph_core-0.1.0/tests/test_subagent_grant.py +665 -0
- ph_core-0.1.0/tests/test_subagent_task.py +164 -0
- ph_core-0.1.0/tests/test_surface.py +257 -0
- ph_core-0.1.0/tests/test_telemetry.py +107 -0
- ph_core-0.1.0/tests/test_telemetry_otel.py +133 -0
- ph_core-0.1.0/tests/test_text.py +74 -0
- ph_core-0.1.0/tests/test_tools_batch.py +277 -0
- ph_core-0.1.0/tests/test_tools_pipeline.py +466 -0
- ph_core-0.1.0/tests/test_tools_registry.py +195 -0
- ph_core-0.1.0/tests/test_wire_forms.py +178 -0
- ph_core-0.1.0/tests/test_workspace.py +625 -0
- ph_core-0.1.0/tests/test_workspace_agentfs.py +631 -0
- ph_core-0.1.0/tests/test_workspace_checkpoint.py +477 -0
- ph_core-0.1.0/tests/test_workspace_git.py +931 -0
- ph_core-0.1.0/tests/test_workspace_jj.py +1180 -0
- ph_core-0.1.0/tests/test_workspace_lifecycle.py +404 -0
- ph_core-0.1.0/tests/test_workspace_provision.py +351 -0
- ph_core-0.1.0/tests/test_workspace_reconcile.py +256 -0
- ph_core-0.1.0/tests/test_workspace_retention.py +510 -0
- ph_core-0.1.0/tests/test_workspace_scratch.py +242 -0
ph_core-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
htmlcov/
|
|
17
|
+
# Dropped at the repo root by pytest-textual-snapshot when a snapshot test
|
|
18
|
+
# fails. The reference snapshots under `__snapshots__/` are the committed
|
|
19
|
+
# expectation; this is the diff viewer for a run that did not match one.
|
|
20
|
+
snapshot_report.html
|
|
21
|
+
|
|
22
|
+
# Reference checkouts of the upstream projects this port reads from. Vendored
|
|
23
|
+
# locally so the plans' citations are verifiable; never part of this repo.
|
|
24
|
+
sources/
|
|
25
|
+
|
|
26
|
+
# Local scratch
|
|
27
|
+
.ph/
|
|
28
|
+
*.local.yaml
|
ph_core-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.
|
ph_core-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ph-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: pH core: the cordis plugin subset, the session log, the LLM vocabulary, and the agent loop.
|
|
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,harness,llm,plugin,react,session-log
|
|
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: croniter>=3.0
|
|
26
|
+
Requires-Dist: filelock>=3.15
|
|
27
|
+
Requires-Dist: pydantic>=2.9
|
|
28
|
+
Requires-Dist: pyturso>=0.7
|
|
29
|
+
Requires-Dist: pyyaml>=6.0
|
|
30
|
+
Provides-Extra: otel
|
|
31
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27; extra == 'otel'
|
|
32
|
+
Requires-Dist: opentelemetry-sdk>=1.27; extra == 'otel'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# ph-core
|
|
36
|
+
|
|
37
|
+
*Everything a pH profile is made of: the plugin framework, the append-only log,
|
|
38
|
+
the ReAct loop, the capability seams — and the seventy-four rows that provide
|
|
39
|
+
them.*
|
|
40
|
+
|
|
41
|
+
There is no privileged core to patch (invariant I1). What this distribution
|
|
42
|
+
ships is the *vocabulary* a profile is written in — a context that holds
|
|
43
|
+
services, an event bus, scopes that unwind what they created — plus one row per
|
|
44
|
+
thing pH can do, each addressable by id from a YAML document. The agent loop is
|
|
45
|
+
a row. The session store is a row. The filesystem is a row. So is the tool
|
|
46
|
+
registry that decides whether a tool call is even allowed to happen.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
phern --profile headless -p "hello" # ph-base + the fake adapter
|
|
50
|
+
phern config --profile base # every knob every row accepts
|
|
51
|
+
phern doctor --profile base # what actually activated
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`ph-core` is a dependency of every other package in this workspace and depends
|
|
55
|
+
on none of them.
|
|
56
|
+
|
|
57
|
+
## What is in here
|
|
58
|
+
|
|
59
|
+
| module | what it owns |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `ph.cordis` | the plugin meta-framework subset (D1): `Context`, service keys, the four event dispatch modes, scopes whose disposal unwinds every registration (I2), and the YAML profile loader |
|
|
62
|
+
| `ph.session` | the append-only event log, the derived model surface (`derive_messages`), the human `transcript()`, and the folds over both |
|
|
63
|
+
| `ph.llm` | the provider-neutral vocabulary, the stream assembler, the `ctx.llm` seam, plus the `fake` and `replay` adapters |
|
|
64
|
+
| `ph.agent` / `ph.agent_loop` | the agent handle and its inbox; the ReAct driver, mounted as a row like anything else |
|
|
65
|
+
| `ph.tools` | the registry, the governed pipeline (`tools/pre-execute`, `tools/post-execute`, guards), the batch scheduler, and Code Mode's transport and generated SDK |
|
|
66
|
+
| `ph.seams` | one module per capability seam: the Protocol, the local provider, and what it refuses |
|
|
67
|
+
| `ph.system_prompt` | prompt assembly — cached `section`s, post-cache `context()` snapshots, and `AGENTS.md` discovery |
|
|
68
|
+
| `ph.persistence` | JSONL and Turso backends, checkpoints, leases, lineage and crash repair |
|
|
69
|
+
| `ph.commands` | the slash commands ph-core itself owns |
|
|
70
|
+
| `ph.bundles` | `base.yaml`, `headless.yaml`, and the `ph.bundles` entry-point group other distributions register into |
|
|
71
|
+
| `ph.paths` | the three roots (`$PH_HOME`, `$PH_CACHE`, `$PH_RUNTIME`) and their resolution rules |
|
|
72
|
+
| `ph.testing` | builders, stubs and fixtures a test stands a profile up with. Nothing shipped imports it |
|
|
73
|
+
|
|
74
|
+
## The two bundles it ships
|
|
75
|
+
|
|
76
|
+
**`ph-base`** (`src/ph/bundles/base.yaml`) is the shared core of every pH
|
|
77
|
+
profile: the log, the loop, the tool registry, every capability seam with its
|
|
78
|
+
local provider, the built-in tools, durability, resilience and the runtime
|
|
79
|
+
invariant rows. **`ph-headless`** adds one row — the scripted `llm-fake`
|
|
80
|
+
adapter — so a one-shot or a scenario test can script a conversation without
|
|
81
|
+
touching code.
|
|
82
|
+
|
|
83
|
+
Both are *paths*, not entry points, because `ph-app` can import them directly.
|
|
84
|
+
Every other bundle in this workspace is **discovered** through the `ph.bundles`
|
|
85
|
+
entry-point group, which is what lets `ph-app` compose the `rlm` profile without
|
|
86
|
+
depending on `ph-rlm`:
|
|
87
|
+
|
|
88
|
+
```toml
|
|
89
|
+
[project.entry-points."ph.bundles"]
|
|
90
|
+
rlm = "ph_rlm:BUNDLE"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## The tools the model gets
|
|
94
|
+
|
|
95
|
+
Registered by rows, so a profile decides which of them exist at all.
|
|
96
|
+
|
|
97
|
+
| row | tools | note |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `tool-fs` | `read`, `write`, `edit`, `glob`, `grep` | thin shells over `ctx.fs`, so the policy gates and the workspace root apply to any second editing tool too |
|
|
100
|
+
| `tool-bash` | `bash` | goes through `ctx.shell`, which is what `sandbox-local` confines |
|
|
101
|
+
| `tool-attach` | `attach` | puts an image, audio file, video or PDF from the workspace in front of the model's own eyes. **Registers nothing without an attachment store** |
|
|
102
|
+
| `tool-ask-user` | `ask_user` | **ships disabled**: a question with nobody to answer it spends a turn on nothing. `tui.yaml` arms it |
|
|
103
|
+
| `subagent-task` | `task` | blocking delegation. **Registers nothing until a `ctx.subagents` provider is mounted** — a tool named in every prompt and refused on every call teaches the model a capability the deployment does not have |
|
|
104
|
+
| `skills-progressive` | `skill` | the catalog goes in the prompt; a body only when the model asks for it by name (G9) |
|
|
105
|
+
|
|
106
|
+
## The commands a person can type
|
|
107
|
+
|
|
108
|
+
These are **commands, not tools**: a person asks the harness directly, it costs
|
|
109
|
+
no model turn, and the log records `command/run`/`command/done` rather than the
|
|
110
|
+
model having decided something the user decided.
|
|
111
|
+
|
|
112
|
+
| command | row | what it does |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `/sandbox` | `sandbox-commands` | show what confined commands may reach, and change it without a restart |
|
|
115
|
+
| `/workspaces` | `workspace-commands` | list, export, merge or remove the branches agents left behind in this session |
|
|
116
|
+
| `/revert` | `workspace-revert` | restore this agent's workspace to a per-run checkpoint |
|
|
117
|
+
| `/autonomous` | `autonomous` | work toward a goal until its gates pass or a budget stops it |
|
|
118
|
+
|
|
119
|
+
## Adjusting it from a profile
|
|
120
|
+
|
|
121
|
+
Three layers, applied in this order — and a patch replaces the targeted row's
|
|
122
|
+
**whole** config rather than merging into it, so a row's effective value is
|
|
123
|
+
always one layer's and readable in one place:
|
|
124
|
+
|
|
125
|
+
1. the shipped documents — `ph-base`, then whatever the profile layers;
|
|
126
|
+
2. your overlay, `$PH_HOME/profiles/<name>.yaml`;
|
|
127
|
+
3. drop-ins pH wrote on your behalf, `$PH_HOME/profiles/<name>.d/*.yaml`, in
|
|
128
|
+
name order (this is where `/sandbox allow …` keeps its decisions);
|
|
129
|
+
4. `--patch`, this run only, same grammar as a profile document.
|
|
130
|
+
|
|
131
|
+
A patch entry is `{id: …, config: {…}}` to reconfigure, `{id: …, disabled:
|
|
132
|
+
false}` to arm a row a bundle ships off, `{id: …, remove: true}` to drop one, or
|
|
133
|
+
`{insert: [...]}` to add one. **A list in row config is replaced, not merged** —
|
|
134
|
+
an overlay restating one field of a route must restate the whole route entry.
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
phern --patch '{id: fs, config: {root: /tmp/scratch}}' --profile tui --mode tui
|
|
138
|
+
phern --patch '{id: tool-ask-user, disabled: false}' --profile headless -p "..."
|
|
139
|
+
phern --dump-config --profile llama # the composition, before anything runs
|
|
140
|
+
phern config --row containment --profile tui # one row's knobs, defaults and what the profile set
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### The knobs most deployments touch
|
|
144
|
+
|
|
145
|
+
| row | config | default |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `agent-loop` | `maxParallelToolCalls` | `10` |
|
|
148
|
+
| `tools` | `mode: native \| code` | `native` |
|
|
149
|
+
| `tools-code-mode` | `maxDispatchesPerRun`, `maxSubagentSpawnsPerRun`, `maxParallelSubCalls` | `256`, `32`, `10` |
|
|
150
|
+
| `fs-local` | `root`, `ignore` | the session cwd |
|
|
151
|
+
| `sandbox-policy` | `defaultMode: read-only \| workspace-write` | `read-only` (`tui.yaml` sets `workspace-write`) |
|
|
152
|
+
| `sandbox-allow` | `paths`, `network.{mode,hosts}` | an allowlist over the package indexes and documentation hosts in `DEFAULT_HOSTS` |
|
|
153
|
+
| `containment` | `tier`, `childTier`, `strict` | **unset** — which is not `advisory`: unset means nobody chose, so a tier provider a profile layered is used |
|
|
154
|
+
| `workspace-lifecycle` | `access`, `provision` | `write` |
|
|
155
|
+
| `jobs-local` | `concurrency` (per job kind) | unset |
|
|
156
|
+
| `llm-retry` | `maxAttempts`, `baseDelayMs`, `maxDelayMs` | `3`, `500`, `20000` |
|
|
157
|
+
| `session-telemetry` | `enabled`, `path` | `enabled: false` in `ph-base` |
|
|
158
|
+
| `subagent-presets` | `presets` | empty — a menu, never a grant: selecting a preset never widens what the parent itself holds |
|
|
159
|
+
| `skills-progressive` | `paths` | **empty on purpose** — scanning a well-known directory would make "install a skill" mean "drop a file somewhere" (I7) |
|
|
160
|
+
| `autonomous` | `maxContinuations`, `maxTurns`, `maxTokens`, `timeoutMs` | `3`, `12`, `80000`, `1800000` |
|
|
161
|
+
| `subprocess-local` | `scrub`, `keep`, `maxOutputBytes` | `8388608` |
|
|
162
|
+
| `tool-attach` | `maxBytes` | `33554432` |
|
|
163
|
+
|
|
164
|
+
**An entry carrying both `id:` and `name:` is a row, not a patch** — that is the
|
|
165
|
+
rule that decides which of the two a document line is. So swapping a *provider*
|
|
166
|
+
is a removal and an insertion, not a rename; consumers never learn which one
|
|
167
|
+
answered either way (I5):
|
|
168
|
+
|
|
169
|
+
```yaml
|
|
170
|
+
# $PH_HOME/profiles/tui.yaml — keep the log in Turso instead of JSONL
|
|
171
|
+
- id: session-persistence
|
|
172
|
+
remove: true
|
|
173
|
+
- insert:
|
|
174
|
+
- id: session-persistence
|
|
175
|
+
name: session-persistence-turso
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Giving the existing id a new `name:` in place looks like it should work and does
|
|
179
|
+
not: it appends a *second* row, and the mount then refuses with
|
|
180
|
+
`service "session_persistence" is already provided`.
|
|
181
|
+
|
|
182
|
+
## Seams that ship with no provider, deliberately
|
|
183
|
+
|
|
184
|
+
`ph-base` mounts the *definition* of several seams and no backend, because these
|
|
185
|
+
have genuinely different answers per deployment and a harness that shipped one
|
|
186
|
+
would have made the choice for you:
|
|
187
|
+
|
|
188
|
+
- **`code_runtime`** — nothing runs model-written code until `ph-rlm` mounts
|
|
189
|
+
`code-runtime-python` (or a profile mounts `code-runtime-stub`).
|
|
190
|
+
- **`subagents`** — no child-agent provider, which is why `subagent-task`
|
|
191
|
+
registers no tool in `ph-base`.
|
|
192
|
+
- **`compaction`** — the seam records and replaces; *when* and *what to say* are
|
|
193
|
+
`ph-stabilize`'s `compaction-summarize`. Every profile `ph-app` offers layers
|
|
194
|
+
that bundle when it is installed, so a *profile* compacts; `ph-base` composed
|
|
195
|
+
on its own still does not.
|
|
196
|
+
- **`uploads`** — mounted with no uploader; each adapter row registers its own,
|
|
197
|
+
so a profile with no file API sends every byte inline.
|
|
198
|
+
|
|
199
|
+
A profile that layers nothing here simply never compacts, never delegates and
|
|
200
|
+
never runs code. That is the plain harness, and it is a supported posture.
|
|
201
|
+
|
|
202
|
+
## Limitations, and things that are deliberate
|
|
203
|
+
|
|
204
|
+
- **No Textual, Rich, Typer, `ph_app`, aiohttp, textual-serve or jinja2
|
|
205
|
+
imports** — `tests/test_layering.py` fails the build on any of them. A front
|
|
206
|
+
end is a consumer of this package, never the other way round.
|
|
207
|
+
- **No real model wire ships here.** `llm-fake` and `llm-replay` are for wiring
|
|
208
|
+
work and tests; Anthropic, Google and the OpenAI-compatible route are rows in
|
|
209
|
+
`ph-app`, so a headless deployment that wants a real provider layers one of
|
|
210
|
+
its profiles.
|
|
211
|
+
- **`sandbox-local` is Linux (bwrap) and macOS (Seatbelt).** It probes both
|
|
212
|
+
claims at mount — a write outside the workspace must fail, and a `CONNECT`
|
|
213
|
+
through the egress shim must reach the proxy and be refused — and on a host
|
|
214
|
+
that cannot confine it says so in `phern doctor` and declines rather than
|
|
215
|
+
pretending.
|
|
216
|
+
- **`permissions-fs`-style path rules are not here** — they are `ph-stabilize`,
|
|
217
|
+
and they bound *seam-mediated* access only. A model-authored `open(path, "w")`
|
|
218
|
+
inside a code cell never fires an intent; what bounds that is the sandbox rung
|
|
219
|
+
below the rules (E9, N1).
|
|
220
|
+
- **Nothing watches the filesystem.** `AGENTS.md` is re-read as a post-cache
|
|
221
|
+
snapshot each turn, which is what makes an edit take effect in the turn after
|
|
222
|
+
it; nothing else polls.
|
|
223
|
+
- **Row order carries no load semantics.** Activation is service-availability
|
|
224
|
+
driven, so a row waits for whatever it injects regardless of where it sits.
|
|
225
|
+
The grouping in `base.yaml` is for readers.
|
|
226
|
+
|
|
227
|
+
## Tests
|
|
228
|
+
|
|
229
|
+
`tests/` — 75 modules, the largest suite in the workspace. Three worth knowing
|
|
230
|
+
about, because they enforce rules rather than behaviour:
|
|
231
|
+
|
|
232
|
+
- `test_layering.py` — the forbidden-import rule above;
|
|
233
|
+
- `test_keys.py` — every `ctx.provide(...)` in this package has a typed key in
|
|
234
|
+
`ph.keys`, and every key has a provider, held against each other in both
|
|
235
|
+
directions;
|
|
236
|
+
- the invariant rows (`agent-loop-invariant`, `session-invariant`,
|
|
237
|
+
`tools-invariant`, `skills-invariant`, `scope-invariant`) are checked *at
|
|
238
|
+
runtime*, not only in CI — a harness that can only prove these in a test
|
|
239
|
+
cannot prove them about your session, and `phern doctor` reports which hold.
|
ph_core-0.1.0/README.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# ph-core
|
|
2
|
+
|
|
3
|
+
*Everything a pH profile is made of: the plugin framework, the append-only log,
|
|
4
|
+
the ReAct loop, the capability seams — and the seventy-four rows that provide
|
|
5
|
+
them.*
|
|
6
|
+
|
|
7
|
+
There is no privileged core to patch (invariant I1). What this distribution
|
|
8
|
+
ships is the *vocabulary* a profile is written in — a context that holds
|
|
9
|
+
services, an event bus, scopes that unwind what they created — plus one row per
|
|
10
|
+
thing pH can do, each addressable by id from a YAML document. The agent loop is
|
|
11
|
+
a row. The session store is a row. The filesystem is a row. So is the tool
|
|
12
|
+
registry that decides whether a tool call is even allowed to happen.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
phern --profile headless -p "hello" # ph-base + the fake adapter
|
|
16
|
+
phern config --profile base # every knob every row accepts
|
|
17
|
+
phern doctor --profile base # what actually activated
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`ph-core` is a dependency of every other package in this workspace and depends
|
|
21
|
+
on none of them.
|
|
22
|
+
|
|
23
|
+
## What is in here
|
|
24
|
+
|
|
25
|
+
| module | what it owns |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `ph.cordis` | the plugin meta-framework subset (D1): `Context`, service keys, the four event dispatch modes, scopes whose disposal unwinds every registration (I2), and the YAML profile loader |
|
|
28
|
+
| `ph.session` | the append-only event log, the derived model surface (`derive_messages`), the human `transcript()`, and the folds over both |
|
|
29
|
+
| `ph.llm` | the provider-neutral vocabulary, the stream assembler, the `ctx.llm` seam, plus the `fake` and `replay` adapters |
|
|
30
|
+
| `ph.agent` / `ph.agent_loop` | the agent handle and its inbox; the ReAct driver, mounted as a row like anything else |
|
|
31
|
+
| `ph.tools` | the registry, the governed pipeline (`tools/pre-execute`, `tools/post-execute`, guards), the batch scheduler, and Code Mode's transport and generated SDK |
|
|
32
|
+
| `ph.seams` | one module per capability seam: the Protocol, the local provider, and what it refuses |
|
|
33
|
+
| `ph.system_prompt` | prompt assembly — cached `section`s, post-cache `context()` snapshots, and `AGENTS.md` discovery |
|
|
34
|
+
| `ph.persistence` | JSONL and Turso backends, checkpoints, leases, lineage and crash repair |
|
|
35
|
+
| `ph.commands` | the slash commands ph-core itself owns |
|
|
36
|
+
| `ph.bundles` | `base.yaml`, `headless.yaml`, and the `ph.bundles` entry-point group other distributions register into |
|
|
37
|
+
| `ph.paths` | the three roots (`$PH_HOME`, `$PH_CACHE`, `$PH_RUNTIME`) and their resolution rules |
|
|
38
|
+
| `ph.testing` | builders, stubs and fixtures a test stands a profile up with. Nothing shipped imports it |
|
|
39
|
+
|
|
40
|
+
## The two bundles it ships
|
|
41
|
+
|
|
42
|
+
**`ph-base`** (`src/ph/bundles/base.yaml`) is the shared core of every pH
|
|
43
|
+
profile: the log, the loop, the tool registry, every capability seam with its
|
|
44
|
+
local provider, the built-in tools, durability, resilience and the runtime
|
|
45
|
+
invariant rows. **`ph-headless`** adds one row — the scripted `llm-fake`
|
|
46
|
+
adapter — so a one-shot or a scenario test can script a conversation without
|
|
47
|
+
touching code.
|
|
48
|
+
|
|
49
|
+
Both are *paths*, not entry points, because `ph-app` can import them directly.
|
|
50
|
+
Every other bundle in this workspace is **discovered** through the `ph.bundles`
|
|
51
|
+
entry-point group, which is what lets `ph-app` compose the `rlm` profile without
|
|
52
|
+
depending on `ph-rlm`:
|
|
53
|
+
|
|
54
|
+
```toml
|
|
55
|
+
[project.entry-points."ph.bundles"]
|
|
56
|
+
rlm = "ph_rlm:BUNDLE"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## The tools the model gets
|
|
60
|
+
|
|
61
|
+
Registered by rows, so a profile decides which of them exist at all.
|
|
62
|
+
|
|
63
|
+
| row | tools | note |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `tool-fs` | `read`, `write`, `edit`, `glob`, `grep` | thin shells over `ctx.fs`, so the policy gates and the workspace root apply to any second editing tool too |
|
|
66
|
+
| `tool-bash` | `bash` | goes through `ctx.shell`, which is what `sandbox-local` confines |
|
|
67
|
+
| `tool-attach` | `attach` | puts an image, audio file, video or PDF from the workspace in front of the model's own eyes. **Registers nothing without an attachment store** |
|
|
68
|
+
| `tool-ask-user` | `ask_user` | **ships disabled**: a question with nobody to answer it spends a turn on nothing. `tui.yaml` arms it |
|
|
69
|
+
| `subagent-task` | `task` | blocking delegation. **Registers nothing until a `ctx.subagents` provider is mounted** — a tool named in every prompt and refused on every call teaches the model a capability the deployment does not have |
|
|
70
|
+
| `skills-progressive` | `skill` | the catalog goes in the prompt; a body only when the model asks for it by name (G9) |
|
|
71
|
+
|
|
72
|
+
## The commands a person can type
|
|
73
|
+
|
|
74
|
+
These are **commands, not tools**: a person asks the harness directly, it costs
|
|
75
|
+
no model turn, and the log records `command/run`/`command/done` rather than the
|
|
76
|
+
model having decided something the user decided.
|
|
77
|
+
|
|
78
|
+
| command | row | what it does |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `/sandbox` | `sandbox-commands` | show what confined commands may reach, and change it without a restart |
|
|
81
|
+
| `/workspaces` | `workspace-commands` | list, export, merge or remove the branches agents left behind in this session |
|
|
82
|
+
| `/revert` | `workspace-revert` | restore this agent's workspace to a per-run checkpoint |
|
|
83
|
+
| `/autonomous` | `autonomous` | work toward a goal until its gates pass or a budget stops it |
|
|
84
|
+
|
|
85
|
+
## Adjusting it from a profile
|
|
86
|
+
|
|
87
|
+
Three layers, applied in this order — and a patch replaces the targeted row's
|
|
88
|
+
**whole** config rather than merging into it, so a row's effective value is
|
|
89
|
+
always one layer's and readable in one place:
|
|
90
|
+
|
|
91
|
+
1. the shipped documents — `ph-base`, then whatever the profile layers;
|
|
92
|
+
2. your overlay, `$PH_HOME/profiles/<name>.yaml`;
|
|
93
|
+
3. drop-ins pH wrote on your behalf, `$PH_HOME/profiles/<name>.d/*.yaml`, in
|
|
94
|
+
name order (this is where `/sandbox allow …` keeps its decisions);
|
|
95
|
+
4. `--patch`, this run only, same grammar as a profile document.
|
|
96
|
+
|
|
97
|
+
A patch entry is `{id: …, config: {…}}` to reconfigure, `{id: …, disabled:
|
|
98
|
+
false}` to arm a row a bundle ships off, `{id: …, remove: true}` to drop one, or
|
|
99
|
+
`{insert: [...]}` to add one. **A list in row config is replaced, not merged** —
|
|
100
|
+
an overlay restating one field of a route must restate the whole route entry.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
phern --patch '{id: fs, config: {root: /tmp/scratch}}' --profile tui --mode tui
|
|
104
|
+
phern --patch '{id: tool-ask-user, disabled: false}' --profile headless -p "..."
|
|
105
|
+
phern --dump-config --profile llama # the composition, before anything runs
|
|
106
|
+
phern config --row containment --profile tui # one row's knobs, defaults and what the profile set
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### The knobs most deployments touch
|
|
110
|
+
|
|
111
|
+
| row | config | default |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `agent-loop` | `maxParallelToolCalls` | `10` |
|
|
114
|
+
| `tools` | `mode: native \| code` | `native` |
|
|
115
|
+
| `tools-code-mode` | `maxDispatchesPerRun`, `maxSubagentSpawnsPerRun`, `maxParallelSubCalls` | `256`, `32`, `10` |
|
|
116
|
+
| `fs-local` | `root`, `ignore` | the session cwd |
|
|
117
|
+
| `sandbox-policy` | `defaultMode: read-only \| workspace-write` | `read-only` (`tui.yaml` sets `workspace-write`) |
|
|
118
|
+
| `sandbox-allow` | `paths`, `network.{mode,hosts}` | an allowlist over the package indexes and documentation hosts in `DEFAULT_HOSTS` |
|
|
119
|
+
| `containment` | `tier`, `childTier`, `strict` | **unset** — which is not `advisory`: unset means nobody chose, so a tier provider a profile layered is used |
|
|
120
|
+
| `workspace-lifecycle` | `access`, `provision` | `write` |
|
|
121
|
+
| `jobs-local` | `concurrency` (per job kind) | unset |
|
|
122
|
+
| `llm-retry` | `maxAttempts`, `baseDelayMs`, `maxDelayMs` | `3`, `500`, `20000` |
|
|
123
|
+
| `session-telemetry` | `enabled`, `path` | `enabled: false` in `ph-base` |
|
|
124
|
+
| `subagent-presets` | `presets` | empty — a menu, never a grant: selecting a preset never widens what the parent itself holds |
|
|
125
|
+
| `skills-progressive` | `paths` | **empty on purpose** — scanning a well-known directory would make "install a skill" mean "drop a file somewhere" (I7) |
|
|
126
|
+
| `autonomous` | `maxContinuations`, `maxTurns`, `maxTokens`, `timeoutMs` | `3`, `12`, `80000`, `1800000` |
|
|
127
|
+
| `subprocess-local` | `scrub`, `keep`, `maxOutputBytes` | `8388608` |
|
|
128
|
+
| `tool-attach` | `maxBytes` | `33554432` |
|
|
129
|
+
|
|
130
|
+
**An entry carrying both `id:` and `name:` is a row, not a patch** — that is the
|
|
131
|
+
rule that decides which of the two a document line is. So swapping a *provider*
|
|
132
|
+
is a removal and an insertion, not a rename; consumers never learn which one
|
|
133
|
+
answered either way (I5):
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
# $PH_HOME/profiles/tui.yaml — keep the log in Turso instead of JSONL
|
|
137
|
+
- id: session-persistence
|
|
138
|
+
remove: true
|
|
139
|
+
- insert:
|
|
140
|
+
- id: session-persistence
|
|
141
|
+
name: session-persistence-turso
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Giving the existing id a new `name:` in place looks like it should work and does
|
|
145
|
+
not: it appends a *second* row, and the mount then refuses with
|
|
146
|
+
`service "session_persistence" is already provided`.
|
|
147
|
+
|
|
148
|
+
## Seams that ship with no provider, deliberately
|
|
149
|
+
|
|
150
|
+
`ph-base` mounts the *definition* of several seams and no backend, because these
|
|
151
|
+
have genuinely different answers per deployment and a harness that shipped one
|
|
152
|
+
would have made the choice for you:
|
|
153
|
+
|
|
154
|
+
- **`code_runtime`** — nothing runs model-written code until `ph-rlm` mounts
|
|
155
|
+
`code-runtime-python` (or a profile mounts `code-runtime-stub`).
|
|
156
|
+
- **`subagents`** — no child-agent provider, which is why `subagent-task`
|
|
157
|
+
registers no tool in `ph-base`.
|
|
158
|
+
- **`compaction`** — the seam records and replaces; *when* and *what to say* are
|
|
159
|
+
`ph-stabilize`'s `compaction-summarize`. Every profile `ph-app` offers layers
|
|
160
|
+
that bundle when it is installed, so a *profile* compacts; `ph-base` composed
|
|
161
|
+
on its own still does not.
|
|
162
|
+
- **`uploads`** — mounted with no uploader; each adapter row registers its own,
|
|
163
|
+
so a profile with no file API sends every byte inline.
|
|
164
|
+
|
|
165
|
+
A profile that layers nothing here simply never compacts, never delegates and
|
|
166
|
+
never runs code. That is the plain harness, and it is a supported posture.
|
|
167
|
+
|
|
168
|
+
## Limitations, and things that are deliberate
|
|
169
|
+
|
|
170
|
+
- **No Textual, Rich, Typer, `ph_app`, aiohttp, textual-serve or jinja2
|
|
171
|
+
imports** — `tests/test_layering.py` fails the build on any of them. A front
|
|
172
|
+
end is a consumer of this package, never the other way round.
|
|
173
|
+
- **No real model wire ships here.** `llm-fake` and `llm-replay` are for wiring
|
|
174
|
+
work and tests; Anthropic, Google and the OpenAI-compatible route are rows in
|
|
175
|
+
`ph-app`, so a headless deployment that wants a real provider layers one of
|
|
176
|
+
its profiles.
|
|
177
|
+
- **`sandbox-local` is Linux (bwrap) and macOS (Seatbelt).** It probes both
|
|
178
|
+
claims at mount — a write outside the workspace must fail, and a `CONNECT`
|
|
179
|
+
through the egress shim must reach the proxy and be refused — and on a host
|
|
180
|
+
that cannot confine it says so in `phern doctor` and declines rather than
|
|
181
|
+
pretending.
|
|
182
|
+
- **`permissions-fs`-style path rules are not here** — they are `ph-stabilize`,
|
|
183
|
+
and they bound *seam-mediated* access only. A model-authored `open(path, "w")`
|
|
184
|
+
inside a code cell never fires an intent; what bounds that is the sandbox rung
|
|
185
|
+
below the rules (E9, N1).
|
|
186
|
+
- **Nothing watches the filesystem.** `AGENTS.md` is re-read as a post-cache
|
|
187
|
+
snapshot each turn, which is what makes an edit take effect in the turn after
|
|
188
|
+
it; nothing else polls.
|
|
189
|
+
- **Row order carries no load semantics.** Activation is service-availability
|
|
190
|
+
driven, so a row waits for whatever it injects regardless of where it sits.
|
|
191
|
+
The grouping in `base.yaml` is for readers.
|
|
192
|
+
|
|
193
|
+
## Tests
|
|
194
|
+
|
|
195
|
+
`tests/` — 75 modules, the largest suite in the workspace. Three worth knowing
|
|
196
|
+
about, because they enforce rules rather than behaviour:
|
|
197
|
+
|
|
198
|
+
- `test_layering.py` — the forbidden-import rule above;
|
|
199
|
+
- `test_keys.py` — every `ctx.provide(...)` in this package has a typed key in
|
|
200
|
+
`ph.keys`, and every key has a provider, held against each other in both
|
|
201
|
+
directions;
|
|
202
|
+
- the invariant rows (`agent-loop-invariant`, `session-invariant`,
|
|
203
|
+
`tools-invariant`, `skills-invariant`, `scope-invariant`) are checked *at
|
|
204
|
+
runtime*, not only in CI — a harness that can only prove these in a test
|
|
205
|
+
cannot prove them about your session, and `phern doctor` reports which hold.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ph-core"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "pH core: the cordis plugin subset, the session log, the LLM vocabulary, and the agent loop."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Charles Tabor" }]
|
|
14
|
+
keywords = ["agent", "llm", "harness", "plugin", "session-log", "react"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Operating System :: POSIX :: Linux",
|
|
22
|
+
"Operating System :: MacOS",
|
|
23
|
+
"Topic :: Software Development",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
# Ceilinged, and the ceiling is load-bearing. `ph_app.daemon.cancelsafe`
|
|
29
|
+
# is keyed to how anyio registers a socket readiness wait — a bound
|
|
30
|
+
# `Future.set_result` handed to `loop.add_reader` — and `ph_rlm.kernel`
|
|
31
|
+
# depends on the *hardened* free `anyio.wait_readable` staying hardened.
|
|
32
|
+
# Neither is public API, so a major release that changes either should be
|
|
33
|
+
# a decision someone makes, not one a resolver makes for them.
|
|
34
|
+
"anyio>=4.4,<5",
|
|
35
|
+
"croniter>=3.0",
|
|
36
|
+
"filelock>=3.15",
|
|
37
|
+
"pydantic>=2.9",
|
|
38
|
+
"pyturso>=0.7",
|
|
39
|
+
"pyyaml>=6.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# Exporting to a collector is opt-in; `session-telemetry-otel` refuses with a
|
|
43
|
+
# message naming this extra rather than failing to import. Both distributions,
|
|
44
|
+
# because the row needs the provider *and* a transport and refuses without
|
|
45
|
+
# either — half of the extra is a sink that ships nowhere.
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/chastabor/pH"
|
|
48
|
+
Repository = "https://github.com/chastabor/pH"
|
|
49
|
+
Documentation = "https://github.com/chastabor/pH/blob/main/docs/README.md"
|
|
50
|
+
Issues = "https://github.com/chastabor/pH/issues"
|
|
51
|
+
|
|
52
|
+
[project.optional-dependencies]
|
|
53
|
+
otel = ["opentelemetry-sdk>=1.27", "opentelemetry-exporter-otlp-proto-http>=1.27"]
|
|
54
|
+
|
|
55
|
+
# The plugin discovery group. Reserved in Phase 0 (P0-01) so a third-party
|
|
56
|
+
# wheel can mount a row by name from its first release onward.
|
|
57
|
+
[project.entry-points."ph.plugins"]
|
|
58
|
+
# core
|
|
59
|
+
llm = "ph.llm.adapter:apply"
|
|
60
|
+
session = "ph.session.store:apply"
|
|
61
|
+
agent = "ph.agent.registry:apply"
|
|
62
|
+
agent-loop = "ph.agent_loop:apply"
|
|
63
|
+
system-prompt = "ph.system_prompt.assembly:apply"
|
|
64
|
+
memory-agents-md = "ph.system_prompt.memory:apply"
|
|
65
|
+
tools = "ph.tools.registry:apply"
|
|
66
|
+
tools-prompt = "ph.tools.prompt:apply"
|
|
67
|
+
tools-timeout = "ph.tools.timeout:apply"
|
|
68
|
+
# capability seams
|
|
69
|
+
approval = "ph.seams.approval:apply"
|
|
70
|
+
attachments-local = "ph.seams.attachments:apply"
|
|
71
|
+
uploads-local = "ph.seams.uploads:apply"
|
|
72
|
+
code-runtime = "ph.seams.code_runtime:apply"
|
|
73
|
+
compaction = "ph.seams.compaction:apply"
|
|
74
|
+
commands = "ph.seams.commands:apply"
|
|
75
|
+
credentials-env = "ph.seams.credentials:apply"
|
|
76
|
+
fs-local = "ph.seams.fs:apply"
|
|
77
|
+
fs-read-before-edit = "ph.seams.fs:read_before_edit"
|
|
78
|
+
jobs-local = "ph.seams.jobs:apply"
|
|
79
|
+
permission-presets = "ph.seams.permission_presets:apply"
|
|
80
|
+
goals = "ph.seams.goals:apply"
|
|
81
|
+
autonomous = "ph.commands.autonomous:apply"
|
|
82
|
+
schedule = "ph.seams.schedule:apply"
|
|
83
|
+
sandbox-local = "ph.seams.sandbox_local:apply"
|
|
84
|
+
sandbox-policy = "ph.seams.sandbox:apply"
|
|
85
|
+
sandbox-allow = "ph.seams.sandbox_allow:apply"
|
|
86
|
+
sandbox-commands = "ph.commands.sandbox:apply"
|
|
87
|
+
settings-local = "ph.seams.settings:apply"
|
|
88
|
+
shell-local = "ph.seams.shell:apply"
|
|
89
|
+
skills = "ph.seams.skills:apply"
|
|
90
|
+
skills-progressive = "ph.seams.skills:progressive"
|
|
91
|
+
subagents = "ph.seams.subagents:apply"
|
|
92
|
+
subagent-presets = "ph.seams.subagents:presets"
|
|
93
|
+
spill-local = "ph.seams.spill:apply"
|
|
94
|
+
subprocess-local = "ph.seams.subprocess:apply"
|
|
95
|
+
tui-screens = "ph.seams.tui_screens:apply"
|
|
96
|
+
tui-status = "ph.seams.tui_status:apply"
|
|
97
|
+
token-meter = "ph.seams.token_meter:apply"
|
|
98
|
+
user-questions = "ph.seams.user_questions:apply"
|
|
99
|
+
containment = "ph.seams.containment:apply"
|
|
100
|
+
diagnostics = "ph.seams.diagnostics:apply"
|
|
101
|
+
topology = "ph.seams.topology:apply"
|
|
102
|
+
invariants = "ph.seams.invariants:apply"
|
|
103
|
+
workspace-shared = "ph.seams.workspace:apply"
|
|
104
|
+
workspace-agentfs = "ph.seams.workspace_agentfs:apply"
|
|
105
|
+
workspace-git-worktree = "ph.seams.workspace_git:apply"
|
|
106
|
+
workspace-jj = "ph.seams.workspace_jj:apply"
|
|
107
|
+
workspace-readonly-scratch = "ph.seams.workspace_scratch:apply"
|
|
108
|
+
workspace-commands = "ph.commands.workspaces:apply"
|
|
109
|
+
workspace-checkpoint = "ph.seams.workspace:checkpoint_policy"
|
|
110
|
+
workspace-revert = "ph.commands.revert:apply"
|
|
111
|
+
workspace-lifecycle = "ph.seams.workspace:lifecycle"
|
|
112
|
+
workspace-reconcile = "ph.seams.workspace:reconcile"
|
|
113
|
+
session-telemetry = "ph.seams.telemetry:apply"
|
|
114
|
+
# model-facing tools
|
|
115
|
+
tool-fs = "ph.tools.builtin.fs_tools:apply"
|
|
116
|
+
tool-bash = "ph.tools.builtin.bash_tool:apply"
|
|
117
|
+
tool-ask-user = "ph.tools.builtin.ask_user:apply"
|
|
118
|
+
tool-attach = "ph.tools.builtin.attach_tool:apply"
|
|
119
|
+
subagent-task = "ph.tools.builtin.subagent_task:apply"
|
|
120
|
+
# durability and resilience
|
|
121
|
+
session-telemetry-otel = "ph.seams.telemetry_otel:apply"
|
|
122
|
+
session-persistence-jsonl = "ph.persistence.jsonl:apply"
|
|
123
|
+
session-persistence-turso = "ph.persistence.turso:apply"
|
|
124
|
+
session-checkpoint-policy = "ph.persistence.checkpoint_policy:apply"
|
|
125
|
+
llm-retry = "ph.llm.retry:apply"
|
|
126
|
+
media-degrade = "ph.llm.media:apply"
|
|
127
|
+
# testing and invariants
|
|
128
|
+
#
|
|
129
|
+
# The first three are test-facing *rows* whose modules are not, and the paths
|
|
130
|
+
# say so: a row is mountable, so it lives with the seam it implements, while
|
|
131
|
+
# `ph.testing` is scaffolding nothing shipped may import. `llm-fake` is the one
|
|
132
|
+
# that made the rule — `headless`, the default profile, mounts it, so an install
|
|
133
|
+
# with no pytest could not compose its own default while this pointed into the
|
|
134
|
+
# test package.
|
|
135
|
+
llm-fake = "ph.llm.fake:apply"
|
|
136
|
+
code-runtime-stub = "ph.seams.code_runtime_stub:apply"
|
|
137
|
+
llm-replay = "ph.llm.replay:apply"
|
|
138
|
+
tools-code-mode = "ph.tools.code_mode:apply"
|
|
139
|
+
agent-loop-invariant = "ph.agent_loop.invariant:apply"
|
|
140
|
+
session-invariant = "ph.session.invariant:apply"
|
|
141
|
+
tools-invariant = "ph.tools.invariant:apply"
|
|
142
|
+
skills-invariant = "ph.seams.skills_invariant:apply"
|
|
143
|
+
scope-invariant = "ph.seams.scope_invariant:apply"
|
|
144
|
+
|
|
145
|
+
[tool.hatch.build.targets.wheel]
|
|
146
|
+
packages = ["src/ph"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""pH — a Python agent harness ported from DeepSeek Harness.
|
|
2
|
+
|
|
3
|
+
The core distribution. See `docs/dev-notes/phase-0.md` for what Phase 0 landed
|
|
4
|
+
and `plans/Implementation_Plan.md` for the work breakdown.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|