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.
@@ -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"]
@@ -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
+ )