ph-stabilize 0.2.0__py3-none-any.whl
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_stabilize/__init__.py +17 -0
- ph_stabilize/bundle.yaml +110 -0
- ph_stabilize/compact_command.py +86 -0
- ph_stabilize/compaction.py +1344 -0
- ph_stabilize/destructive.py +517 -0
- ph_stabilize/hitl.py +266 -0
- ph_stabilize/input_offload.py +165 -0
- ph_stabilize/limits.py +619 -0
- ph_stabilize/offload.py +268 -0
- ph_stabilize/permissions_fs.py +701 -0
- ph_stabilize/py.typed +0 -0
- ph_stabilize/skill_steps.py +262 -0
- ph_stabilize/todo.py +778 -0
- ph_stabilize-0.2.0.dist-info/METADATA +248 -0
- ph_stabilize-0.2.0.dist-info/RECORD +18 -0
- ph_stabilize-0.2.0.dist-info/WHEEL +4 -0
- ph_stabilize-0.2.0.dist-info/entry_points.txt +13 -0
- ph_stabilize-0.2.0.dist-info/licenses/LICENSE +21 -0
ph_stabilize/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""pH stabilization bundle: todo, offload, compaction, limits, HITL and permissions.
|
|
2
|
+
|
|
3
|
+
Deep Agents' features are *algorithms plus prompts, not runtime* (§1.3 of the
|
|
4
|
+
port plan), so each one lands here as a row on a seam `ph-core` already
|
|
5
|
+
publishes. Reserved in Phase 0 (P0-01); built from Phase 4.
|
|
6
|
+
|
|
7
|
+
@module ph_stabilize
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
BUNDLE = Path(__file__).parent / "bundle.yaml"
|
|
15
|
+
"""The rows the `stabilize` layer adds over `ph-base`."""
|
|
16
|
+
|
|
17
|
+
__all__ = ["BUNDLE"]
|
ph_stabilize/bundle.yaml
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# ph-stabilize: Deep Agents' stabilization features as plugins, over `ph-base`.
|
|
2
|
+
#
|
|
3
|
+
# Every row here attaches to a waterfall that already exists — D12, the whole
|
|
4
|
+
# integration thesis: todo planning is a tool plus a prompt section, offloading
|
|
5
|
+
# is `tools/post-execute`, summarization is `agent/pre-step`. None of them is a
|
|
6
|
+
# parameter on the loop, and none of them needs the loop to know it is there.
|
|
7
|
+
#
|
|
8
|
+
# Layered by a profile rather than by `ph-base`, because a deployment that wants
|
|
9
|
+
# the plain harness should get the plain harness — and `ph-base` composed on its
|
|
10
|
+
# own still is one.
|
|
11
|
+
#
|
|
12
|
+
# **Every profile `ph-app` offers layers this bundle**, `base` included, and as
|
|
13
|
+
# an *optional* bundle: an install without this distribution composes the same
|
|
14
|
+
# profiles and simply never compacts. The reason is one row rather than all
|
|
15
|
+
# nine — a session that grows until the provider refuses it is a defect in any
|
|
16
|
+
# posture, not a feature of an unattended one — and the rest come along because
|
|
17
|
+
# a bundle is the unit a profile can name. They are inert by design, with one
|
|
18
|
+
# exception worth knowing: `permissions-fs` below ships a live rule.
|
|
19
|
+
|
|
20
|
+
# --- planning ---------------------------------------------------------------
|
|
21
|
+
# `write_todos` + the `todo/write` event + the prompt section (G1). `disabled`
|
|
22
|
+
# in its own bundle — the `rlm-context-loader` idiom — because layering this
|
|
23
|
+
# bundle for offload or compaction must not, by itself, hand the model a tool:
|
|
24
|
+
# upstream dropped `TodoListMiddleware` from its default stack and re-adds it
|
|
25
|
+
# per model, so a profile flips this row on purpose (`rlm-stable` does, P4-15).
|
|
26
|
+
- id: tool-todo
|
|
27
|
+
name: tool-todo
|
|
28
|
+
disabled: true
|
|
29
|
+
|
|
30
|
+
# A skill that declares `steps:` becomes work the model must finish, and a
|
|
31
|
+
# listener on `agent/turn-stopping` objects while any of it is startable (P7-18).
|
|
32
|
+
# `disabled` for `tool-todo`'s reason and one more: this row can keep a turn
|
|
33
|
+
# going, which is a posture a deployment chooses rather than inherits. Useless
|
|
34
|
+
# without `tool-todo` — the list it seeds is that row's — so a profile that
|
|
35
|
+
# enables this enables both.
|
|
36
|
+
- id: skill-steps
|
|
37
|
+
name: skill-steps
|
|
38
|
+
disabled: true
|
|
39
|
+
|
|
40
|
+
# --- context offloading -----------------------------------------------------
|
|
41
|
+
# One oversized tool result can end a session's usefulness on its own, and the
|
|
42
|
+
# model rarely needs all of it (G2/C5). Enabled: unlike planning, this changes
|
|
43
|
+
# nothing until a result actually exceeds 80 000 characters, so a profile that
|
|
44
|
+
# layered this bundle for any reason wants it — there is no posture in which
|
|
45
|
+
# losing the context window to one `find /` is the preferred outcome.
|
|
46
|
+
- id: tool-result-offload
|
|
47
|
+
name: tool-result-offload
|
|
48
|
+
|
|
49
|
+
# The other direction: a pasted build log or dumped table (G3). Same reasoning
|
|
50
|
+
# as above for being on — and the same shape, except that the substitution is a
|
|
51
|
+
# surface `replace`, so the log keeps what the person actually sent while the
|
|
52
|
+
# model reads a preview.
|
|
53
|
+
- id: input-offload
|
|
54
|
+
name: input-offload
|
|
55
|
+
|
|
56
|
+
# --- compaction -------------------------------------------------------------
|
|
57
|
+
# When the conversation itself is the thing that no longer fits (G4). Enabled,
|
|
58
|
+
# for the same reason the offload rows are: this changes nothing until a session
|
|
59
|
+
# actually reaches 85% of its window, and there is no posture in which losing
|
|
60
|
+
# the turn to an overflow is preferable to summarizing what has already been
|
|
61
|
+
# answered. The engine is the `ctx.compaction` provider; the seam ships in
|
|
62
|
+
# `ph-base` with no backend, so a profile that layers nothing here simply never
|
|
63
|
+
# compacts.
|
|
64
|
+
- id: compaction-summarize
|
|
65
|
+
name: compaction-summarize
|
|
66
|
+
|
|
67
|
+
# The human verb, split from the policy exactly as dsh splits them: a deployment
|
|
68
|
+
# may want automatic compaction without handing anyone a command, or the command
|
|
69
|
+
# with a different engine behind it.
|
|
70
|
+
- id: command-compact
|
|
71
|
+
name: command-compact
|
|
72
|
+
|
|
73
|
+
# --- limits -----------------------------------------------------------------
|
|
74
|
+
# Hard boundaries on a loop that has stopped making progress (G5). Mounted with
|
|
75
|
+
# every ceiling unset, so layering the bundle changes nothing until a profile
|
|
76
|
+
# names a number — a limit nobody chose is a limit that fires on someone's
|
|
77
|
+
# longest legitimate turn. The breaker is the exception and is on by default:
|
|
78
|
+
# five identical failures in a row is not a long task, it is a stuck one.
|
|
79
|
+
- id: limits
|
|
80
|
+
name: limits
|
|
81
|
+
|
|
82
|
+
# --- human in the loop ------------------------------------------------------
|
|
83
|
+
# A person between the model and what it cannot take back (G6). `interrupt_on`
|
|
84
|
+
# is empty by default, so layering the bundle prompts about nothing: a harness
|
|
85
|
+
# that asks on first run teaches its user to approve without reading, which is
|
|
86
|
+
# worse than not asking. A profile names the tools and the patterns it cares
|
|
87
|
+
# about — `rlm-stable` does, at P4-15.
|
|
88
|
+
- id: hitl
|
|
89
|
+
name: hitl
|
|
90
|
+
|
|
91
|
+
# Path rules over filesystem access (G7). Attached to `ctx.fs`'s intent
|
|
92
|
+
# waterfalls rather than to the fs tools, so a second editing tool or a Code
|
|
93
|
+
# Mode binding is covered by the same rules; `rules` is empty by default, for
|
|
94
|
+
# `hitl`'s reason — layering a bundle must not start refusing file access. The
|
|
95
|
+
# row says out loud what it does not reach when no sandbox is mounted (E9).
|
|
96
|
+
- id: permissions-fs
|
|
97
|
+
name: permissions-fs
|
|
98
|
+
config:
|
|
99
|
+
rules:
|
|
100
|
+
# The default write scope (E6, §12 Q9b), and the *last* rule on purpose:
|
|
101
|
+
# first-match-wins means every explicit rule an operator writes above it
|
|
102
|
+
# decides first, so this is a default rather than an override. A write
|
|
103
|
+
# inside the agent's own workspace or its scratch matches nothing and is
|
|
104
|
+
# allowed; one leaving them asks, once. Inert until a containment tier is
|
|
105
|
+
# in force — with no acquired workspace there is nothing to be outside of.
|
|
106
|
+
- operations: [write]
|
|
107
|
+
paths: ["**"]
|
|
108
|
+
scope: outside-workspace
|
|
109
|
+
mode: interrupt
|
|
110
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""`command-compact` — `/compact`, over the backend-independent seam (P4-03).
|
|
2
|
+
|
|
3
|
+
A row of its own, exactly as dsh splits `command-compact` from
|
|
4
|
+
`compaction-basic`: the human verb and the summarization policy are different
|
|
5
|
+
decisions, and a deployment that wants automatic compaction without a slash
|
|
6
|
+
command — or a slash command with a different engine behind it — should not have
|
|
7
|
+
to fork either. This row injects `commands` and `compaction` and knows nothing
|
|
8
|
+
about summaries.
|
|
9
|
+
|
|
10
|
+
**A command is not a turn.** `/compact` is something the *person* asked the
|
|
11
|
+
harness to do, so it dispatches directly, records `command/run` and
|
|
12
|
+
`command/done`, and never opens a `turn/*`. The one thing it does open is a
|
|
13
|
+
surface replacement, which is why it insists the agent is idle first — a
|
|
14
|
+
compaction that landed mid-turn would move the surface underneath a request the
|
|
15
|
+
loop had already derived.
|
|
16
|
+
|
|
17
|
+
**Every failure is a sentence, not a traceback.** `CompactionError.code` is a
|
|
18
|
+
closed set precisely so a front end can phrase each one; the mapping lives here
|
|
19
|
+
because this is the only place that has a person to say it to.
|
|
20
|
+
|
|
21
|
+
@module ph_stabilize.compact_command
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from ph.cordis import Context, plugin
|
|
27
|
+
from ph.keys import COMMANDS, COMPACTION
|
|
28
|
+
from ph.seams.commands import CommandContext, CommandDefinition
|
|
29
|
+
from ph.seams.compaction import CompactionError
|
|
30
|
+
from ph.text import count_of
|
|
31
|
+
|
|
32
|
+
__all__ = ["FAILURE_TEXT", "apply"]
|
|
33
|
+
|
|
34
|
+
FAILURE_TEXT: dict[str, str] = {
|
|
35
|
+
"busy": (
|
|
36
|
+
"Compaction needs an idle session: this one is working, or a compaction "
|
|
37
|
+
"is already running. The conversation is unchanged."
|
|
38
|
+
),
|
|
39
|
+
"summary": (
|
|
40
|
+
"Compaction could not produce a usable summary. The conversation is "
|
|
41
|
+
"unchanged; the attempt is recorded in the session log."
|
|
42
|
+
),
|
|
43
|
+
"unavailable": "This profile has no compaction engine, so there is nothing to compact with.",
|
|
44
|
+
}
|
|
45
|
+
"""One line per `CompactionError.code`. dsh's `expectedFailure`, narrowed to the
|
|
46
|
+
codes pH's seam actually defines — a phrase for a code nothing raises is a
|
|
47
|
+
promise nobody can check."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@plugin("command-compact", inject=[COMMANDS, COMPACTION])
|
|
51
|
+
async def apply(ctx: Context, config: None) -> None:
|
|
52
|
+
"""Register `/compact`."""
|
|
53
|
+
|
|
54
|
+
async def compact(argument: str, invocation: CommandContext) -> str:
|
|
55
|
+
agent = invocation.agent
|
|
56
|
+
if agent is None:
|
|
57
|
+
return "refusing: /compact needs an agent whose session to compact"
|
|
58
|
+
try:
|
|
59
|
+
# Anything typed after the verb is the person saying what they are
|
|
60
|
+
# about to work on. dsh refuses arguments here and deepagents'
|
|
61
|
+
# compact tool takes none — but the moment someone compacts on
|
|
62
|
+
# purpose is usually the moment they are changing subject, and they
|
|
63
|
+
# know something about what comes next that the summarizer cannot
|
|
64
|
+
# read off the conversation. The engine decides what to do with it;
|
|
65
|
+
# this row only passes it on and lets the log record it.
|
|
66
|
+
result = await ctx.require(COMPACTION).compact_now(agent, instructions=argument.strip())
|
|
67
|
+
except CompactionError as error:
|
|
68
|
+
# `.get` rather than a lookup: a future code without a phrase should
|
|
69
|
+
# still say something true, and the exception's own message is the
|
|
70
|
+
# truest thing available.
|
|
71
|
+
return FAILURE_TEXT.get(error.code, str(error))
|
|
72
|
+
if result is None:
|
|
73
|
+
return "no compactable history yet"
|
|
74
|
+
return (
|
|
75
|
+
f"compacted {count_of(len(result.shadowed_seqs), 'message')} "
|
|
76
|
+
f"(~{result.shadowed_tokens} tokens)"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
ctx.require(COMMANDS).register(
|
|
80
|
+
CommandDefinition(
|
|
81
|
+
name="compact",
|
|
82
|
+
summary="Replace older conversation history with a summary.",
|
|
83
|
+
argument_hint="[what you are about to work on]",
|
|
84
|
+
run=compact,
|
|
85
|
+
)
|
|
86
|
+
)
|