sliceagent 0.1.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.
- sliceagent/__init__.py +3 -0
- sliceagent/__main__.py +6 -0
- sliceagent/access.py +93 -0
- sliceagent/agents.py +173 -0
- sliceagent/background_review.py +146 -0
- sliceagent/binsniff.py +89 -0
- sliceagent/cli.py +890 -0
- sliceagent/clock.py +32 -0
- sliceagent/code_grep.py +329 -0
- sliceagent/code_index.py +417 -0
- sliceagent/config.py +240 -0
- sliceagent/context_overflow.py +227 -0
- sliceagent/envspec.py +129 -0
- sliceagent/errors.py +167 -0
- sliceagent/events.py +96 -0
- sliceagent/finding_types.py +70 -0
- sliceagent/flags.py +63 -0
- sliceagent/fuzzy.py +135 -0
- sliceagent/guardrails.py +438 -0
- sliceagent/guidance.py +69 -0
- sliceagent/hippocampus.py +581 -0
- sliceagent/hooks.py +334 -0
- sliceagent/interfaces.py +144 -0
- sliceagent/llm.py +695 -0
- sliceagent/loop.py +548 -0
- sliceagent/mcp_client.py +255 -0
- sliceagent/mcp_security.py +77 -0
- sliceagent/memory.py +428 -0
- sliceagent/metrics.py +103 -0
- sliceagent/model_catalog.py +124 -0
- sliceagent/monitor.py +615 -0
- sliceagent/neocortex.py +436 -0
- sliceagent/onboarding.py +323 -0
- sliceagent/oracle.py +36 -0
- sliceagent/pagetable.py +255 -0
- sliceagent/pfc.py +449 -0
- sliceagent/plugins.py +127 -0
- sliceagent/policy.py +234 -0
- sliceagent/procman.py +187 -0
- sliceagent/prompt.py +239 -0
- sliceagent/records.py +108 -0
- sliceagent/recovery.py +119 -0
- sliceagent/regions.py +678 -0
- sliceagent/registry.py +128 -0
- sliceagent/retriever.py +19 -0
- sliceagent/safety.py +332 -0
- sliceagent/sandbox.py +143 -0
- sliceagent/scheduler.py +92 -0
- sliceagent/search_index.py +289 -0
- sliceagent/seed.py +465 -0
- sliceagent/sensory_cortex.py +500 -0
- sliceagent/session.py +222 -0
- sliceagent/skill_provenance.py +71 -0
- sliceagent/skill_usage.py +123 -0
- sliceagent/skills.py +209 -0
- sliceagent/subagent.py +332 -0
- sliceagent/subdir_hints.py +222 -0
- sliceagent/swap.py +182 -0
- sliceagent/taskstate.py +57 -0
- sliceagent/telemetry.py +59 -0
- sliceagent/terminal.py +240 -0
- sliceagent/text_utils.py +56 -0
- sliceagent/tool_summary.py +93 -0
- sliceagent/tools.py +1194 -0
- sliceagent/tui.py +1377 -0
- sliceagent/web.py +354 -0
- sliceagent-0.1.0.dist-info/METADATA +262 -0
- sliceagent-0.1.0.dist-info/RECORD +71 -0
- sliceagent-0.1.0.dist-info/WHEEL +4 -0
- sliceagent-0.1.0.dist-info/entry_points.txt +2 -0
- sliceagent-0.1.0.dist-info/licenses/LICENSE +21 -0
sliceagent/regions.py
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
"""Typed-region renderers — per-kind views over the EXISTING Slice dataclass fields.
|
|
2
|
+
|
|
3
|
+
The slice is an address space of TYPED REGIONS (open files, ghosts, conversation, skills,
|
|
4
|
+
threads, …); each region knows how to render itself and to SUPPRESS itself when empty.
|
|
5
|
+
seed.py's render_slice is the layout pass that orders these region renderers into the one
|
|
6
|
+
user string (the moat); the renderers themselves live here.
|
|
7
|
+
|
|
8
|
+
This module is a pure rendering/metadata layer: it reads Slice fields (pfc.py) and low-level
|
|
9
|
+
helpers (safety.wrap_untrusted, the working-set bounds OWNED by swap.py) but imports NOTHING
|
|
10
|
+
from pfc.py/seed.py — they import FROM here (one direction), so there is no import cycle.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
from .safety import wrap_untrusted
|
|
18
|
+
from .swap import MAX_REVIEWED
|
|
19
|
+
from .text_utils import normalize_ws, one_line
|
|
20
|
+
|
|
21
|
+
MANIFEST_TURNS = 8 # PAGED-OUT HISTORY manifest window — bounded locator count (the moat: constant
|
|
22
|
+
# size regardless of session length; content is paged in on demand, never accumulated into the slice).
|
|
23
|
+
MAX_OPEN_THREADS = 6 # OTHER OPEN THREADS tier cap — bounded presentation of parked topics
|
|
24
|
+
MAX_FINDINGS = 8 # bounded ring of distilled conclusions (anti-re-derivation; not a transcript)
|
|
25
|
+
MAX_FINDING_CHARS = 300 # each finding is ONE compact line — distilled, never narration (causal tail matters)
|
|
26
|
+
MAX_REQUIREMENTS = 20 # bounded STANDING REQUIREMENTS contract (count) — the moat's no-unbounded-growth
|
|
27
|
+
MAX_REQ_CHARS = 300 # each requirement is ONE compact line (contracts — a long signature must survive)
|
|
28
|
+
MAX_PLAN_ITEMS = 20 # bounded PLAN (TodoWrite) — same no-unbounded-growth rule as requirements
|
|
29
|
+
MAX_PLAN_CHARS = 300 # each plan step is ONE compact line (multi-file scope must survive)
|
|
30
|
+
_PLAN_MARK = {"done": "x", "in_progress": "~", "pending": " "}
|
|
31
|
+
MAX_MISSION_CHARS = 500 # the MISSION (session north-star) is ONE compact objective line (don't clip scope)
|
|
32
|
+
|
|
33
|
+
MAX_REPORT_CHARS = 280 # OPEN USER REPORT — one compact verbatim line (bounded; never a transcript)
|
|
34
|
+
MAX_ACTION_LOG = 24 # bounded anti-loop tally (no-transcript: the action_log can't grow per-topic forever)
|
|
35
|
+
MAX_ACTION_SHOWN = 12 # cap on REPEATED/FAILING entries rendered (highest-signal first)
|
|
36
|
+
|
|
37
|
+
# Working-set view caps (the OPEN FILES region). A working-set file is shown IN FULL up to
|
|
38
|
+
# FULL_FILE_LINES; only a PATHOLOGICALLY huge file collapses to its RELEVANT REGION (REGION_LINES).
|
|
39
|
+
# Co-located here because they parameterize the OPEN FILES region renderer (build_artifacts in
|
|
40
|
+
# seed.py imports them from here — one direction). DISCOVERY_K is the RELATED CODE region's k.
|
|
41
|
+
FULL_FILE_LINES = 1200
|
|
42
|
+
REGION_LINES = 400
|
|
43
|
+
DISCOVERY_K = 6
|
|
44
|
+
MAX_CONVERSATION = 4 # RECENT CONVERSATION ring — last N user<->assistant exchanges (short-range continuity)
|
|
45
|
+
CONVO_MSG_CHARS = 800 # per-message GIST cap in the conversation tier (count-bounded by MAX_CONVERSATION;
|
|
46
|
+
# the cache holds the full text and recall pages it back, so this is a display-gist size, not the only copy)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ── PER-REGION RENDER: UNCAPPED-BY-RELEVANCE ──────────────────────────────────
|
|
50
|
+
# _NO_CAP — the "no render cap" sentinel. OPEN FILES / YOUR NOTES are bounded by RELEVANCE
|
|
51
|
+
# (record_note dedup/retire), never by an arbitrary size cap — bound ≠ size, the slice shows all that's
|
|
52
|
+
# relevant. The only hard limit is the physical context window, handled by the loop's overflow path
|
|
53
|
+
# (drop the oldest accumulated exchange), not by truncating a tier.
|
|
54
|
+
_NO_CAP = 1_000_000
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# `one_line` is re-exported from text_utils (single definition — pfc.py/seed.py/neocortex.py import the
|
|
58
|
+
# real definition directly). Kept importable from regions too for the existing call sites here.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def render_reviewed(s) -> str:
|
|
62
|
+
"""The recall_history RATCHET tier — lookbacks already done this task, so the model sees the
|
|
63
|
+
lookback advanced the state (and doesn't re-fetch the SAME turn). Only rendered when something's
|
|
64
|
+
been reviewed. Non-suppressive wording: it guards against re-fetching what's already paged in, but
|
|
65
|
+
still invites fetching a DIFFERENT turn (recall is a normal read, not a smell)."""
|
|
66
|
+
if not s.reviewed:
|
|
67
|
+
return ""
|
|
68
|
+
return ("# HISTORY REVIEWED (already paged in this task — their content is in YOUR NOTES / RECENT "
|
|
69
|
+
"above; fetch a DIFFERENT turn from PAGED-OUT HISTORY if you need more)\n"
|
|
70
|
+
f"{', '.join(s.reviewed[-MAX_REVIEWED:])}\n\n")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def render_cache_manifest(refs) -> str:
|
|
74
|
+
"""PAGED-OUT HISTORY body: one locator line per earlier turn of THIS session (NOT in the slice),
|
|
75
|
+
each ending with the EXACT call to page it back — so reaching back is copy-paste, not a blind
|
|
76
|
+
guess. This is the TRIGGER the dead recall channel was missing: a cache the model can't see is a
|
|
77
|
+
cache it never calls (the read-side analogue of REPO MAP advertising file paths). ``refs`` are
|
|
78
|
+
locator-only PageRefs from PageTable._episodes_thissession (ONE read seam); this is pure
|
|
79
|
+
formatting. MOAT: locators only — turn/title/breadcrumb, never content; the turn's body pages in
|
|
80
|
+
on demand and is bounded by recall_history's own caps."""
|
|
81
|
+
if not refs:
|
|
82
|
+
return ""
|
|
83
|
+
lines = []
|
|
84
|
+
for r in refs:
|
|
85
|
+
if r.handle == "…older":
|
|
86
|
+
lines.append(f"- {r.preview}") # the "+N earlier" tail (no single-turn call)
|
|
87
|
+
else:
|
|
88
|
+
lines.append(f"- {r.preview} → recall_history(turns=[{r.handle}])")
|
|
89
|
+
return "\n".join(lines)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def render_focus(focus, extra_roots, *, home: str = "", workspace: str = "") -> str:
|
|
93
|
+
"""CURRENT PROJECT body: the dir the agent is actively working in, when it has moved beyond the boundary
|
|
94
|
+
root. Surfaces the auto-granted file-tool reach + the moved relative-path base (otherwise INVISIBLE →
|
|
95
|
+
the model stays in the start-dir frame and can't resolve 'the project' / a bare filename to where the
|
|
96
|
+
work actually is, then re-asks or cold-searches — the hunter 'index.ts' miss). The boundary (the floor)
|
|
97
|
+
never moves; this is the frame on top of it. Self-suppresses for the common single-project case."""
|
|
98
|
+
def short(p: str) -> str:
|
|
99
|
+
return ("~" + p[len(home):]) if home and p.startswith(home) else p
|
|
100
|
+
roots = [r for r in (extra_roots or []) if r and r != workspace]
|
|
101
|
+
if not roots and not (focus and focus != workspace):
|
|
102
|
+
return ""
|
|
103
|
+
lines = []
|
|
104
|
+
if focus and focus != workspace:
|
|
105
|
+
lines.append(
|
|
106
|
+
f"You are now working in `{short(focus)}`. Bare relative paths resolve HERE, and your file "
|
|
107
|
+
f"tools — read_file, list_files, grep, edit_file — act here. Resolve a bare filename or "
|
|
108
|
+
f"\"the project\"/\"it\" against THIS and the RECENT CONVERSATION first; do NOT fall back to a "
|
|
109
|
+
f"boundary-wide search or re-ask when the referent is already clear from recent work.")
|
|
110
|
+
others = [short(r) for r in roots if r != focus]
|
|
111
|
+
if others:
|
|
112
|
+
lines.append("Also within your boundary (reachable by file tools): " + ", ".join(f"`{o}`" for o in others) + ".")
|
|
113
|
+
return "\n".join(lines)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def render_skills(active_skills: list[dict]) -> str:
|
|
117
|
+
if not active_skills:
|
|
118
|
+
return wrap_untrusted("", kind="skill")
|
|
119
|
+
joined = "\n\n".join(f"## SKILL: {sk['name']}\n{sk['body']}" for sk in active_skills)
|
|
120
|
+
return wrap_untrusted(joined, kind="skill")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def render_threads(refs) -> str:
|
|
124
|
+
"""Render the bounded OTHER OPEN THREADS index (parked topics the model can resume)."""
|
|
125
|
+
if not refs:
|
|
126
|
+
return ""
|
|
127
|
+
lines = [f"- [{r.task_id}] {r.title} ({r.status})" for r in refs[:MAX_OPEN_THREADS]]
|
|
128
|
+
extra = len(refs) - min(len(refs), MAX_OPEN_THREADS)
|
|
129
|
+
if extra > 0:
|
|
130
|
+
lines.append(f"- …and {extra} more")
|
|
131
|
+
return "\n".join(lines)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def render_conversation(s) -> str:
|
|
135
|
+
"""The RECENT CONVERSATION tier: the last few COMPLETED user<->assistant exchanges (the in-progress
|
|
136
|
+
one is excluded — its user message is the current task). Ends with a pointer to recall the rest."""
|
|
137
|
+
prior = [e for e in s.conversation[:-1] if e.get("user")]
|
|
138
|
+
if not prior:
|
|
139
|
+
return ""
|
|
140
|
+
lines = []
|
|
141
|
+
n = len(prior)
|
|
142
|
+
for idx, e in enumerate(prior):
|
|
143
|
+
lines.append(f"- user: {e['user']}")
|
|
144
|
+
if e.get("assistant"):
|
|
145
|
+
lines.append(f" you: {e['assistant']}")
|
|
146
|
+
if e.get("truncated"):
|
|
147
|
+
# the gist above is a CUT of a longer reply — advertise the exact recall so the model pages
|
|
148
|
+
# the FULL text back instead of confabulating detail past the cut (last=1 == this reply).
|
|
149
|
+
lines.append(f" ⋯ (shortened to a gist — recall_history(last={n - idx}) for the FULL "
|
|
150
|
+
f"reply before answering about its specifics; do NOT guess past the cut)")
|
|
151
|
+
older = s.turns - len(prior) - 1 # turns beyond the ring (minus the current in-progress turn)
|
|
152
|
+
tail = (f"\n(+{older} earlier turn(s) this session not shown — they're listed in PAGED-OUT HISTORY "
|
|
153
|
+
"below; recall_history(turns=[N]) to view any)") if older > 0 else ""
|
|
154
|
+
return "\n".join(lines) + tail
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# I1 PROVENANCE — narration filter. A FINDING must be a durable FACT, never the model's running
|
|
158
|
+
# narration. Notes that merely announce intent ("Let me run it", "I'll check the file", "Now I'll
|
|
159
|
+
# edit X", "Next, …") carry no established fact: folding them made FINDINGS read like a transcript
|
|
160
|
+
# and let "**Done — built it**" ratchet as an ESTABLISHED truth (F1/C3/G5). Task-agnostic + cheap:
|
|
161
|
+
# pure lexical, no LLM. Matched at the START of the note (the leading clause sets its kind).
|
|
162
|
+
_NARRATION_RE = re.compile(
|
|
163
|
+
r"^\s*(?:ok(?:ay)?[,. ]+)?(?:"
|
|
164
|
+
r"(?:let'?s|let me|let us|i['’]?ll|i will|i['’]?m going to|i am going to|now i|now let|"
|
|
165
|
+
r"then i|i need to|i should|going to|gonna|i plan to)\b"
|
|
166
|
+
r"|(?:next|first|then)\b[,. ]" # leading sequencing adverbs ("Next, …", "First …")
|
|
167
|
+
r")",
|
|
168
|
+
re.I,
|
|
169
|
+
)
|
|
170
|
+
# A note that ASSERTS completion ("done", "all set", "task complete", "finished") is a CLAIM, not an
|
|
171
|
+
# observation — durable ONLY if a real tool RESULT backed it (see slice_sink). Detected so the source
|
|
172
|
+
# can be DOWNGRADED to "claim" (rendered "unverified — confirm against OPEN FILES"), never silently
|
|
173
|
+
# promoted to an established truth. Task-agnostic lexical signal; no LLM.
|
|
174
|
+
_DONE_CLAIM_RE = re.compile(
|
|
175
|
+
r"\b(?:done|all set|all done|complete(?:d|ly)?|finished|it works|works now|ready to use|"
|
|
176
|
+
r"task (?:is )?(?:done|complete)|already (?:done|complete|built|implemented)|"
|
|
177
|
+
r"successfully (?:built|created|implemented|added|completed))\b",
|
|
178
|
+
re.I,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def is_done_claim(text: str) -> bool:
|
|
183
|
+
"""True when `text` asserts the work is finished — a claim that needs an observation to be durable."""
|
|
184
|
+
return bool(_DONE_CLAIM_RE.search(text or ""))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# RECALL-ON-CUT marker (see memory: recall-ring-truncation-gap). A silent one_line() cut with no signal
|
|
188
|
+
# reads as "this is the whole thing" — the model then RE-DERIVES the missing part from scratch instead of
|
|
189
|
+
# recalling it, and a re-derived answer usually does NOT match the original (confabulation, not correction).
|
|
190
|
+
# Found live TWICE via two independent cut sites (a bug-hunt reply cut in the RECENT CONVERSATION ring, then
|
|
191
|
+
# again cut here in FINDINGS/OPEN USER REPORT) — any NEW site that bounds model- or user-authored text with
|
|
192
|
+
# one_line() should go through this helper rather than a bare one_line() call.
|
|
193
|
+
_RECALL_ON_CUT_MARK = " [cut — PARTIAL; see PAGED-OUT HISTORY or recall_history(search=...) for the rest, don't guess]"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _cut_with_recall_marker(text: str, cap: int) -> str:
|
|
197
|
+
"""one_line(text, cap), but if the cut actually removed content, replace the tail with a marker
|
|
198
|
+
naming the cut + the two general recall paths (no turn number is available at these call sites,
|
|
199
|
+
unlike the RECENT CONVERSATION ring which knows an exact recall_history(last=K)) + an explicit
|
|
200
|
+
don't-guess instruction."""
|
|
201
|
+
was_cut = len(one_line(text, cap + 1)) > cap
|
|
202
|
+
if not was_cut:
|
|
203
|
+
return one_line(text, cap)
|
|
204
|
+
return one_line(text, max(0, cap - len(_RECALL_ON_CUT_MARK))) + _RECALL_ON_CUT_MARK
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def record_note(s, text: str, source: str = "tool-note") -> bool:
|
|
208
|
+
"""Fold the model's per-turn note (a distilled FACT it established) into the FINDINGS tier.
|
|
209
|
+
Returns True iff a GENUINELY NEW finding was added (not narration, not a dedup refresh) — the
|
|
210
|
+
convergence check uses this so 'actively learning' doesn't count as 'spinning' (review #5).
|
|
211
|
+
|
|
212
|
+
The slice carries no transcript, so a reasoning model would otherwise re-derive the
|
|
213
|
+
situation each turn (costly reasoning bursts). This lets it carry its OWN conclusions
|
|
214
|
+
forward — bounded (ring of MAX_FINDINGS) and deduped so it stays distilled, not a log.
|
|
215
|
+
|
|
216
|
+
I1 PROVENANCE: a finding is a FACT FROM THE WORLD, never raw narration. Notes that announce
|
|
217
|
+
intent ("Let me…", "I'll…") are dropped — they're transcript, not established state. `source`
|
|
218
|
+
tags where the fact came from ("observed" > "tool-note" > "claim"); a completion ("done") note
|
|
219
|
+
is downgraded to "claim" unless the caller passed an observed source, so it can't ratchet into
|
|
220
|
+
an ESTABLISHED truth. No extra LLM call — pure lexical, captured from the note arg on a real call.
|
|
221
|
+
|
|
222
|
+
RECALL BRIDGE: a long AssistantText reply (e.g. a multi-item bug-hunt report) folds in here as a
|
|
223
|
+
"claim" — a hard per-item cut to MAX_FINDING_CHARS, since findings must stay compact. See
|
|
224
|
+
_cut_with_recall_marker: without a signal, a later "what were those bugs" sees ONLY the surviving
|
|
225
|
+
fragment and re-derives the rest from the code instead of recalling it — a confirmed fabrication."""
|
|
226
|
+
note = _cut_with_recall_marker(text, MAX_FINDING_CHARS)
|
|
227
|
+
if not note:
|
|
228
|
+
return False
|
|
229
|
+
if _NARRATION_RE.match(note): # pure intent/narration — carries no durable fact
|
|
230
|
+
return False
|
|
231
|
+
# a "done" claim is durable only if an observation backed it; otherwise it's a hypothesis
|
|
232
|
+
if source != "observed" and is_done_claim(note):
|
|
233
|
+
source = "claim"
|
|
234
|
+
is_new = note not in s.findings # genuinely new knowledge vs a refresh of an existing finding
|
|
235
|
+
if not is_new: # already established — refresh its recency, don't duplicate
|
|
236
|
+
s.findings.remove(note)
|
|
237
|
+
s.findings.append(note)
|
|
238
|
+
# BOUNDED = SEAL THE LOOP, not cut within it: findings are NOT truncated or retired inside a loop —
|
|
239
|
+
# every distinct conclusion the loop established stays whole (any within-loop cut harms the LLM). The
|
|
240
|
+
# only reduction is exact-duplicate dedup above (same fact refreshed, no information lost). The bound
|
|
241
|
+
# is the loop-boundary SEAL (TurnEnd archive + a fresh next loop), never a within-section filter.
|
|
242
|
+
s.finding_source[note] = source
|
|
243
|
+
# keep the source map bounded to the LIVE finding set (no unbounded growth across turns)
|
|
244
|
+
live = set(s.findings)
|
|
245
|
+
for k in [k for k in s.finding_source if k not in live]:
|
|
246
|
+
del s.finding_source[k]
|
|
247
|
+
return is_new
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# I1 PROVENANCE — per-source trust framing. The slice's #1 ground truth is OPEN FILES (disk);
|
|
251
|
+
# FINDINGS are the model's own prior notes, which must be VERIFIED, never blindly reused. We never
|
|
252
|
+
# render model-sourced text as "do not re-derive" (that authored the "already done" ratchet).
|
|
253
|
+
_SOURCE_TAG = {
|
|
254
|
+
"observed": "", # backed by a tool result — trust, but OPEN FILES still wins
|
|
255
|
+
"tool-note": " (your note — verify against OPEN FILES)",
|
|
256
|
+
"claim": " (UNVERIFIED claim — confirm against OPEN FILES/a tool result before relying on it)",
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def render_findings(findings: list[str], sources: dict | None = None) -> str:
|
|
261
|
+
if not findings:
|
|
262
|
+
return ""
|
|
263
|
+
sources = sources or {}
|
|
264
|
+
return "\n".join(f"- {f}{_SOURCE_TAG.get(sources.get(f, 'tool-note'), '')}" for f in findings)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def render_world(world: dict) -> str:
|
|
268
|
+
"""The agent's durable WORLD MODEL — a maintained key→value scratchpad (maze map, inventory,
|
|
269
|
+
system state, plan). Long/multiline values render as their own block; short ones as bullets.
|
|
270
|
+
No cap (bound = the seal, not a cut): the whole maintained state renders into each turn's seed."""
|
|
271
|
+
if not world:
|
|
272
|
+
return ""
|
|
273
|
+
parts = []
|
|
274
|
+
for k, v in world.items():
|
|
275
|
+
v = str(v)
|
|
276
|
+
if "\n" in v or len(v) > 80:
|
|
277
|
+
parts.append(f"## {k}\n{v}")
|
|
278
|
+
else:
|
|
279
|
+
parts.append(f"- {k}: {v}")
|
|
280
|
+
return "\n".join(parts)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def render_requirements(requirements: list[dict]) -> str:
|
|
284
|
+
"""The STANDING REQUIREMENTS contract body: the constraints that must hold when the task is DONE.
|
|
285
|
+
Self-suppresses when empty (a greeting/question has no contract → no bytes, no binding spec — the
|
|
286
|
+
structural kill for the 'first message becomes the spec' bug). Append-order + status-flip-in-place
|
|
287
|
+
(open '- [ ]', satisfied '- [x] … (done)') so a change touches only its own line and unrelated
|
|
288
|
+
turns stay byte-identical (warm STABLE prefix). Bounded by MAX_REQUIREMENTS (folded in slice_sink)."""
|
|
289
|
+
if not requirements:
|
|
290
|
+
return ""
|
|
291
|
+
return "\n".join(f"- [{'x' if r.get('done') else ' '}] {r.get('text', '')}" + (" (done)" if r.get("done") else "")
|
|
292
|
+
for r in requirements)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def render_plan(plan: list[dict]) -> str:
|
|
296
|
+
"""The PLAN tier body: the model's ordered execution steps with live status (todo list).
|
|
297
|
+
Numbered + status-marked ('[~]' in-progress, '[x]' done, '[ ]' pending). Self-suppresses when empty.
|
|
298
|
+
Bounded by MAX_PLAN_ITEMS (folded in slice_sink). Volatile WORKING state — distinct from STANDING
|
|
299
|
+
REQUIREMENTS (acceptance criteria): this is the step sequence and the agent's live progress through it."""
|
|
300
|
+
if not plan:
|
|
301
|
+
return ""
|
|
302
|
+
return "\n".join(f"{i}. [{_PLAN_MARK.get(it.get('status'), ' ')}] {it.get('step', '')}"
|
|
303
|
+
for i, it in enumerate(plan, 1))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ── ANTI-LOOP / RECENT / CURRENT ERROR ────────────────────────────────────────
|
|
307
|
+
# the underlying operations inside an execute_code body — so the anti-loop tally can see
|
|
308
|
+
# THROUGH code-as-action (otherwise every script is a unique signature and loops hide)
|
|
309
|
+
_CODE_OP_RE = re.compile(
|
|
310
|
+
r"\b(read_file|write_file|append_file|str_replace|list_files|run)\(\s*['\"]?([^'\",)]*)"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def code_ops(code: str) -> list[str]:
|
|
315
|
+
"""Normalized operation list inside an execute_code body (op + the tail of its literal arg)."""
|
|
316
|
+
out, seen = [], set()
|
|
317
|
+
for op, arg in _CODE_OP_RE.findall(code or ""):
|
|
318
|
+
arg = arg.strip().split("/")[-1][:24]
|
|
319
|
+
sig = f"{op} {arg}".strip()
|
|
320
|
+
if sig not in seen:
|
|
321
|
+
seen.add(sig)
|
|
322
|
+
out.append(sig)
|
|
323
|
+
return out
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def observe(out, n: int = 260) -> str:
|
|
327
|
+
"""A one-line observation that PRESERVES THE TAIL. For most command output the decisive part —
|
|
328
|
+
the verdict, the final status, the exception — is at the END, so head-only truncation hides it
|
|
329
|
+
and the agent re-runs to 'see the result'. Task-agnostic: we don't interpret the outcome, we
|
|
330
|
+
just guarantee the end is visible. Keep a little head for context plus the whole tail."""
|
|
331
|
+
o = normalize_ws(out)
|
|
332
|
+
if len(o) <= n:
|
|
333
|
+
return o
|
|
334
|
+
if n < 8: # too small to split head+sep+tail; a plain head-cut is the bound
|
|
335
|
+
return o[:n] # (else tail = n-head-3 <= 0 and o[-0:] returns the WHOLE string)
|
|
336
|
+
head = n // 4
|
|
337
|
+
tail = n - head - 3 # 3 = len(" … "); head + sep + tail == n
|
|
338
|
+
return o[:head] + " … " + o[-tail:]
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def action_sig(name: str, args: dict) -> str:
|
|
342
|
+
if name == "run_command":
|
|
343
|
+
return f"run_command `{one_line(args.get('command', ''), 50)}`"
|
|
344
|
+
if name == "execute_code":
|
|
345
|
+
ops = code_ops(args.get("code", ""))
|
|
346
|
+
return "execute_code[" + ", ".join(ops[:4]) + "]" if ops else "execute_code(script)"
|
|
347
|
+
if name in ("edit_file", "append_to_file", "str_replace", "read_file"):
|
|
348
|
+
return f"{name} {args.get('path', '')}"
|
|
349
|
+
if name == "list_files":
|
|
350
|
+
return f"list_files {args.get('path', '.')}"
|
|
351
|
+
return name
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def record_action(s, name: str, args: dict, out: str, failing: bool | None = None) -> None:
|
|
355
|
+
"""Fold one tool result into the action tally + error/exploration state (deterministic — no LLM).
|
|
356
|
+
|
|
357
|
+
`failing` is the AUTHORITATIVE flag from the tool layer (ToolText.ok / event.failing); the loop
|
|
358
|
+
passes it. The prose heuristic is a back-compat fallback only — relying on it misclassified a grep/
|
|
359
|
+
log line that legitimately starts with "Error" as a failure (corrupting last_error/anti-loop)."""
|
|
360
|
+
s.turn_actions = getattr(s, "turn_actions", 0) + 1 # per-turn exploration counter (finding-independent)
|
|
361
|
+
if failing is None:
|
|
362
|
+
failing = out.startswith("Error") or out.startswith("Exit code")
|
|
363
|
+
if failing:
|
|
364
|
+
s.last_error = out if len(out) <= 3000 else out[:2000] + "\n…[trace truncated]…\n" + out[-900:]
|
|
365
|
+
elif name in ("run_command", "execute_code"):
|
|
366
|
+
s.last_error = "" # a successful run/script clears the error (both are execution — general)
|
|
367
|
+
sig = action_sig(name, args)
|
|
368
|
+
prev = s.action_log.get(sig, {"count": 0})
|
|
369
|
+
s.action_log[sig] = {"count": prev["count"] + 1, "failing": failing, "last": observe(out, 100)}
|
|
370
|
+
if len(s.action_log) > MAX_ACTION_LOG:
|
|
371
|
+
# bounded like every tier (no-transcript): evict lowest-signal first — oldest one-shot,
|
|
372
|
+
# non-failing entries — so failing/repeated ones (the anti-loop signal) survive longest.
|
|
373
|
+
for k in [k for k, a in s.action_log.items() if a["count"] < 2 and not a["failing"]]:
|
|
374
|
+
if len(s.action_log) <= MAX_ACTION_LOG:
|
|
375
|
+
break
|
|
376
|
+
del s.action_log[k]
|
|
377
|
+
while len(s.action_log) > MAX_ACTION_LOG:
|
|
378
|
+
del s.action_log[next(iter(s.action_log))]
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# POSIX-general signal that a command is UNAVAILABLE (not that the agent's code is wrong): the
|
|
382
|
+
# shell couldn't find/execute it (exit 127 = not found, 126 = not executable). Task-agnostic — no
|
|
383
|
+
# tool/language/runner name. Re-running an unavailable command can never succeed.
|
|
384
|
+
# Deliberately NOT "no such file" (a path mistake is usually fixable, not an unavailable command).
|
|
385
|
+
_CMD_UNAVAILABLE = ("command not found", "[exit 127]", "exit code 127",
|
|
386
|
+
"[exit 126]", "exit code 126", "not executable", "executable not found")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def render_action_history(action_log: dict) -> str:
|
|
390
|
+
entries = [(sig, a) for sig, a in action_log.items() if a["count"] >= 2 or a["failing"]]
|
|
391
|
+
if not entries:
|
|
392
|
+
return "- (nothing repeated or failing)"
|
|
393
|
+
entries.sort(key=lambda e: (e[1]["failing"], e[1]["count"]), reverse=True) # highest-signal first
|
|
394
|
+
extra = max(0, len(entries) - MAX_ACTION_SHOWN)
|
|
395
|
+
entries = entries[:MAX_ACTION_SHOWN]
|
|
396
|
+
lines = []
|
|
397
|
+
for sig, a in entries:
|
|
398
|
+
last_low = (a.get("last") or "").lower()
|
|
399
|
+
unavailable = sig.startswith(("run_command", "execute_code")) and any(m in last_low for m in _CMD_UNAVAILABLE)
|
|
400
|
+
if a["failing"] and a["count"] >= 2 and unavailable:
|
|
401
|
+
# the command itself is unavailable here — re-running can't fix it (general: env/tooling
|
|
402
|
+
# gap, not your code). Don't repeat it; finish if the work is done, else change command.
|
|
403
|
+
warn = (" ⚠ this command is UNAVAILABLE here — re-running can't fix it; if your work is "
|
|
404
|
+
"complete write the final summary, otherwise use a different command")
|
|
405
|
+
elif a["failing"] and a["count"] >= 3:
|
|
406
|
+
# same command, same failure, repeatedly — re-running won't change the outcome
|
|
407
|
+
warn = (" ⚠ REPEATEDLY FAILING the same way — re-running won't change it; fix the root cause, "
|
|
408
|
+
"or if your work is already complete, finish")
|
|
409
|
+
elif a["count"] >= 3:
|
|
410
|
+
# a non-failing action repeated this much is a soft loop (e.g. a str_replace whose
|
|
411
|
+
# old_string never matches, run via execute_code so it never trips the failing flag)
|
|
412
|
+
warn = " ⚠ REPEATED with no progress — STOP; change approach (read the full file, make ONE precise edit)"
|
|
413
|
+
elif a["failing"]:
|
|
414
|
+
warn = " (failing)"
|
|
415
|
+
else:
|
|
416
|
+
warn = ""
|
|
417
|
+
lines.append(f"- {sig} ×{a['count']}{warn} → {a['last']}")
|
|
418
|
+
if extra:
|
|
419
|
+
lines.append(f"- …and {extra} more repeated/failing (omitted)")
|
|
420
|
+
return "\n".join(lines)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
# ── CONVERGENCE ───────────────────────────────────────────────────────────────
|
|
424
|
+
STOP_NUDGE_AFTER = 2 # non-edit tool calls since the last edit (with no error) before nudging to converge
|
|
425
|
+
READONLY_NUDGE_AFTER = 4 # read-only tool calls with NO edit at all before nudging to answer/act
|
|
426
|
+
EXPLORE_NUDGE_AFTER = 5 # tool calls in ONE turn with no edit before nudging to ANSWER or ask_user — keyed on
|
|
427
|
+
# turn_actions (finding-INDEPENDENT), so a read-heavy Q&A that records a note each step still converges
|
|
428
|
+
CLOSURE_MAX_SHOWN = 3 # max dangling-dependent locators in one CLOSURE block (bounds tokens; symbol-aware
|
|
429
|
+
# staleness keeps the set tiny + self-extinguishing, so no window cap is needed to prevent a cascade)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def render_closure(s) -> str:
|
|
433
|
+
"""CHANGE-SET CLOSURE — the PRECISE half of 'verify before done'. After an edit settles, name the
|
|
434
|
+
dependents whose code STILL references a symbol your edit removed or moved: a dangling call-site a
|
|
435
|
+
coordinated change must fix (re-observation-reach = action-reach). SYMBOL-AWARE (SwapManager.prefetch
|
|
436
|
+
computes stale_deps from the code graph — a SENSORY CORTEX derived view, re-derived on file change,
|
|
437
|
+
not a persisted store: pre-edit defs - current defs, intersected with each
|
|
438
|
+
dependent's current ref tokens), so it is SILENT on feature-adds (nothing removed → never inflates a
|
|
439
|
+
non-refactor task) and on already-fixed sites (their tokens no longer name the symbol). Locator-only,
|
|
440
|
+
advisory, self-extinguishing (kept to UNOPENED stale deps), bounded; empty on a no-graph host. It does
|
|
441
|
+
NOT police a WRONG edit at an already-reached site (e.g. s3's depth bug) — that needs behavioral verify."""
|
|
442
|
+
if os.environ.get("SLICEAGENT_NO_CLOSURE"): # safety kill-switch for the gated rollout
|
|
443
|
+
return ""
|
|
444
|
+
stale = getattr(s, "stale_deps", None) or set()
|
|
445
|
+
# SYMBOL-AWARE: stale_deps (computed in SwapManager.prefetch) are the dependents whose CURRENT tokens
|
|
446
|
+
# still reference a symbol the edit removed/moved — silent on feature-adds (nothing removed). Keep only
|
|
447
|
+
# the UNOPENED ones so the nudge self-extinguishes the instant the model opens the site to fix/confirm
|
|
448
|
+
# (precise + terminating: no cascade on tasks whose callers don't need changing). Capped small.
|
|
449
|
+
if not stale or s.last_error or s.since_edit < STOP_NUDGE_AFTER:
|
|
450
|
+
return ""
|
|
451
|
+
active = set(s.active_files)
|
|
452
|
+
unclosed = sorted(p for p in stale if p not in s.edited_files and p not in active)[:CLOSURE_MAX_SHOWN]
|
|
453
|
+
if not unclosed:
|
|
454
|
+
return ""
|
|
455
|
+
edited = ", ".join(sorted(s.edited_files)[:4])
|
|
456
|
+
body = "\n".join(f"- {p} — still references a symbol you changed/moved in {edited}; open it and update "
|
|
457
|
+
f"it (or confirm it's correct)" for p in unclosed)
|
|
458
|
+
return ("# CHANGE-SET CLOSURE\nyour edit removed or moved a symbol these files still reference — a "
|
|
459
|
+
"coordinated change must fix every call-site, not just the definition:\n" + body + "\ndo NOT "
|
|
460
|
+
"declare done until each is updated or confirmed.\n\n")
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def render_convergence(s) -> str:
|
|
464
|
+
"""Convergence pressure against over-verification. Once a change exists and the agent has spent
|
|
465
|
+
several tool calls since its last edit with NO current error, it is re-checking something already
|
|
466
|
+
settled — tell it to finish. General + Markov: purely a function of state (edited? error?
|
|
467
|
+
calls-since-edit), no task/tool/language assumptions. Fires ONLY post-edit and ONLY when nothing
|
|
468
|
+
is broken, so it never cuts off active fixing (a failing check keeps last_error set → no nudge).
|
|
469
|
+
This SHRINKS wasted steps/tokens/time; the model still decides (it may continue for a real edit)."""
|
|
470
|
+
if not s.edited_files:
|
|
471
|
+
# EXPLORER children are SUPPOSED to do many read-only calls (their deliverable is the
|
|
472
|
+
# investigation); the read-only nudge below is for the TOP-LEVEL agent over-exploring instead
|
|
473
|
+
# of answering the user, and it was cutting delegated reviews short BEFORE the key (large) files
|
|
474
|
+
# were read. max_steps bounds an explorer, not this nudge.
|
|
475
|
+
if getattr(s, "explore_mode", False):
|
|
476
|
+
return ""
|
|
477
|
+
# READ-ONLY spin: many tool calls, nothing changed. Edit-gated convergence never fires here,
|
|
478
|
+
# so a trivial/answer-only task (greeting, "show the path", "summarize") over-explores. Nudge
|
|
479
|
+
# it to answer/act. General + Markov (edits vs non-edits, no task-type); dormant once anything
|
|
480
|
+
# is edited (→ the post-edit path below), so real edit-tasks are unaffected.
|
|
481
|
+
ta = getattr(s, "turn_actions", 0)
|
|
482
|
+
if not s.last_error and ta >= EXPLORE_NUDGE_AFTER:
|
|
483
|
+
strong = "STOP exploring NOW — " if ta >= EXPLORE_NUDGE_AFTER + 3 else ""
|
|
484
|
+
return (
|
|
485
|
+
f"# CONVERGENCE CHECK\n{strong}you've made {ta} tool calls this turn and edited nothing. Decide "
|
|
486
|
+
f"NOW — stop exploring (do NOT re-read what you've seen). If the task needs a CODE CHANGE, make "
|
|
487
|
+
f"your best-effort minimal edit immediately: never finish, and never run out of steps, having "
|
|
488
|
+
f"edited nothing — an empty result is a failure, a best-effort patch is not. If the task only "
|
|
489
|
+
f"needs an ANSWER, answer the user now (cite OPEN FILES) and make NO tool call; if it is "
|
|
490
|
+
f"genuinely ambiguous, call ask_user with ONE concise question.\n\n")
|
|
491
|
+
return ""
|
|
492
|
+
if s.last_error or s.since_edit < STOP_NUDGE_AFTER:
|
|
493
|
+
return ""
|
|
494
|
+
if render_closure(s): # an unreached dependent outranks the done-nudge (targeted > frequency):
|
|
495
|
+
return "" # show CLOSURE instead of STOP so the model finishes the refactor first
|
|
496
|
+
strong = "STOP NOW — " if s.since_edit >= STOP_NUDGE_AFTER + 2 else ""
|
|
497
|
+
return (
|
|
498
|
+
f"# CONVERGENCE CHECK\n{strong}you have edited {len(s.edited_files)} file(s) and made "
|
|
499
|
+
f"{s.since_edit} tool calls since your last edit with no error — the change appears complete and "
|
|
500
|
+
f"verified as well as the environment allows. Write your final summary and make NO tool "
|
|
501
|
+
f"call. Continue ONLY to make a SPECIFIC new edit you have identified — do NOT re-read or re-run a "
|
|
502
|
+
f"check you have already passed.\n\n"
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
# ── OPEN USER REPORT ──────────────────────────────────────────────────────────
|
|
507
|
+
# I3 — OPEN USER REPORT capture heuristic. A user follow-up that looks like a FAILURE REPORT ("it
|
|
508
|
+
# can't play", "it doesn't work", "still broken", "cd: no such file") is the user pushing back on a
|
|
509
|
+
# (possibly false) "done" — the dialectic a Markov snapshot loses. We carry it as a blocker the model
|
|
510
|
+
# must verify against the REAL artifact before re-claiming done (it drove the "already done" ratchet:
|
|
511
|
+
# F1's user-pushback half). Task-agnostic + LLM-agnostic: pure lexical, no command/tool parsing, no
|
|
512
|
+
# model call. Two signals: (a) negation/failure phrasing about the work, (b) a literal error/diagnostic
|
|
513
|
+
# pasted from a terminal (a shell/runtime error string the user is reporting back).
|
|
514
|
+
_USER_REPORT_RE = re.compile(
|
|
515
|
+
r"(?:"
|
|
516
|
+
# explicit failure/negation about the artifact
|
|
517
|
+
r"\b(?:doesn'?t|does not|don'?t|do not|won'?t|will not|can'?t|cannot|can ?not)\b\s*"
|
|
518
|
+
r"(?:\w+\s+){0,3}?(?:work|works|run|runs|play|plays|load|loads|open|opens|start|starts|build|builds|compile|compiles)\b"
|
|
519
|
+
r"|\b(?:not|isn'?t|aren'?t|wasn'?t)\s+(?:\w+\s+){0,2}?(?:work|working|run|running|play|playing|load|loading|right|correct)\b"
|
|
520
|
+
r"|\b(?:still\s+)?(?:broken|failing|fails|failed|crash(?:es|ed|ing)?|errored|buggy|not working)\b" # bare 'error'/'bug' dropped (dev vocabulary, not a report); re-admitted with context below
|
|
521
|
+
r"|\b(?:it|this|that)\s+(?:still\s+)?(?:doesn'?t|does not|won'?t|can'?t|cannot)\b"
|
|
522
|
+
# a pasted terminal/runtime diagnostic the user is reporting
|
|
523
|
+
r"|\b(?:no such file|command not found|traceback|exception|permission denied|"
|
|
524
|
+
r"syntaxerror|nameerror|typeerror|modulenotfound|exit code|segmentation fault)\b"
|
|
525
|
+
r"|:\s*no such file or directory\b"
|
|
526
|
+
# phrasings the first pass missed: hangs / no-output, red|failing tests/build,
|
|
527
|
+
# "didn't fix it", "same error still", HTTP 4xx/5xx in a failure context, ModuleNotFoundError.
|
|
528
|
+
r"|\b(?:hang(?:s|ing|ed)?|frozen|freeze(?:s|ing)?|stuck)\b"
|
|
529
|
+
r"|\bnothing (?:happen(?:s|ed)?|shows?|showed|loads?|loaded|renders?|rendered)\b"
|
|
530
|
+
r"|\b(?:tests?|the build|build|ci|pipeline)\b(?:\s+\w+){0,2}?\s+(?:are|is|still|now)?\s*(?:red|failing|fail|broken)\b"
|
|
531
|
+
r"|\b(?:failing|red|broken)\s+(?:tests?|build|ci)\b"
|
|
532
|
+
r"|\bdid(?:n'?t|\s+not)\b(?:\s+\w+){0,2}?\s*fix\b"
|
|
533
|
+
r"|\b(?:still|same)\b(?:\s+\w+){0,3}?\s+(?:error|issue|problem|bug|failure|failing|broken)\b"
|
|
534
|
+
r"|\bhttp\s*[45]\d\d\b|\b[45]\d\d\s+(?:error|not found|internal server)\b" # dropped bare 'status'/'response' (feature-spec phrasing, e.g. 'return a 404 status')
|
|
535
|
+
r"|\b(?:return(?:s|ed|ing)?|get(?:s|ting)?|got|throw(?:s|n|ing)?|give[sn]?)\s+(?:an?\s+)?(?:http\s*)?[45]\d\d\b"
|
|
536
|
+
r"|\bmodulenotfounderror\b"
|
|
537
|
+
r")",
|
|
538
|
+
re.I,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def is_user_report(text: str) -> bool:
|
|
543
|
+
"""True when a user message looks like a FAILURE REPORT about prior work — captured as an OPEN
|
|
544
|
+
USER REPORT blocker. Conservative + task-agnostic (pure lexical); a normal directive that merely
|
|
545
|
+
contains 'add'/'fix' is NOT a report unless it carries an explicit failure/negation signal."""
|
|
546
|
+
return bool(_USER_REPORT_RE.search(text or ""))
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def capture_user_report(s, message: str) -> bool:
|
|
550
|
+
"""If `message` looks like a failure report, store it (verbatim, bounded) as the OPEN USER REPORT
|
|
551
|
+
blocker on the slice and return True. A NEWER report replaces an older one (most-recent wins,
|
|
552
|
+
inherently bounded). Returns False (and leaves any prior report intact) for a non-report message —
|
|
553
|
+
so a benign follow-up does NOT clear a still-open report.
|
|
554
|
+
|
|
555
|
+
The CAPTURING turn also shows the message in full via CURRENT REQUEST (no cap there); the risk is a
|
|
556
|
+
LATER turn, where this bounded field is the only surviving copy — see _cut_with_recall_marker."""
|
|
557
|
+
if not is_user_report(message):
|
|
558
|
+
return False
|
|
559
|
+
s.open_report = _cut_with_recall_marker(message, MAX_REPORT_CHARS)
|
|
560
|
+
return True
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
# ── REGION_ORDER — the slice layout, region-by-region ─────────────────────────
|
|
564
|
+
# The slice is an address space of TYPED REGIONS. REGION_ORDER encodes their EXACT render order and
|
|
565
|
+
# the stable/volatile split that governs prompt-cache locality. A prefix cache matches only up to the
|
|
566
|
+
# first byte that differs from the previous request, so the STABLE BULK (OPEN FILES, RELATED CODE,
|
|
567
|
+
# skills, memory, conversation — byte-identical across the common read-only / reasoning steps) LEADS,
|
|
568
|
+
# and the VOLATILE tier (findings, action tally, RECENT, error, convergence — changes most steps) is
|
|
569
|
+
# the recency-salient TAIL: the immediate state and the high-authority blocker/error sit right above
|
|
570
|
+
# NOW. Each region renders its OWN framed fragment (header + body + spacing) and SUPPRESSES itself
|
|
571
|
+
# when empty (returns ''); render_regions joins the fragments. This replaces render_slice's
|
|
572
|
+
# hand-ordered parts[] list — the iteration MUST equal the old concatenation byte-for-byte.
|
|
573
|
+
#
|
|
574
|
+
# `slot` groups fragments into the original parts[] elements (fragments in the same slot are
|
|
575
|
+
# concatenated, in REGION_ORDER order, into one "\n".join part); the slot sequence + blank-line
|
|
576
|
+
# glue is fixed in render_regions. `tier` documents the stable/volatile split.
|
|
577
|
+
STABLE, VOLATILE = "stable", "volatile"
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
# Each region is (name, tier, render(ctx)->framed-fragment, slot). The renderer OWNS its header
|
|
581
|
+
# literal + spacing and SUPPRESSES itself (returns '') when empty. `tier` documents the
|
|
582
|
+
# stable-bulk/volatile-tail split (prompt-cache locality). `slot` maps the fragment onto the former
|
|
583
|
+
# CURRENT REQUEST (the live user ask) and the NOW footer render OUTSIDE the <context> envelope in
|
|
584
|
+
# slice.build() — NOT as REGION_ORDER entries. The envelope marks "reference STATE"; the live INSTRUCTION must
|
|
585
|
+
# frame it from OUTSIDE, at both ends (primacy + recency), with NOW as the outermost tail. ONE `goal` source
|
|
586
|
+
# feeds both request copies (no primacy/recency divergence).
|
|
587
|
+
_CURRENT_REQUEST_HDR = ("# CURRENT REQUEST (what the user is asking for RIGHT NOW — your PRIMARY instruction; "
|
|
588
|
+
"address THIS)\n")
|
|
589
|
+
_NOW_FOOTER = ("# NOW: address the CURRENT REQUEST above. If it asks a QUESTION or for an explanation, answer "
|
|
590
|
+
"it directly (read/grep to ground the answer if useful — you need NOT edit); if it asks for a "
|
|
591
|
+
"CHANGE, make it with tools based on OPEN FILES; once the request is fully handled and verified "
|
|
592
|
+
"as well as the environment allows, write your final summary and make NO tool call.")
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def render_current_request(goal: str) -> str:
|
|
596
|
+
"""The live user ask, rendered OUTSIDE the context fence (used at BOTH primacy and recency from
|
|
597
|
+
one source). Empty goal → '' (no header)."""
|
|
598
|
+
g = (goal or "").strip()
|
|
599
|
+
return f"{_CURRENT_REQUEST_HDR}{g}\n\n" if g else ""
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def render_now(hints: str = "") -> str:
|
|
603
|
+
"""The intent-aware NOW footer — the OUTERMOST tail (after the fence closes), so the final instruction
|
|
604
|
+
reads as an instruction, not as 'context'. `hints` = pre-framed SUBDIRECTORY CONTEXT prefix (may be '')."""
|
|
605
|
+
return (hints or "") + _NOW_FOOTER
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
# parts[] grouping: fragments sharing a slot are concatenated, in order, into one "\n".join part —
|
|
609
|
+
# so the iteration equals the old hand-ordered concatenation BYTE-FOR-BYTE. (Provenance framing for
|
|
610
|
+
# # YOUR NOTES / the # OPEN USER REPORT blocker / the # REPEATED-FAILING header all live in the
|
|
611
|
+
# literals below — relocated verbatim from render_slice, not duplicated.)
|
|
612
|
+
REGION_ORDER = (
|
|
613
|
+
# STANDING REQUIREMENTS — the live contract that must hold when the task is DONE: a model-curated set
|
|
614
|
+
# of constraints (exact signature, output format, stated rule, an added requirement), maintained in-band
|
|
615
|
+
# via require/requirement_done/drop_requirement. NOT the frozen first message — EMPTY by default, so a
|
|
616
|
+
# greeting/question renders nothing (the structural kill for the 'first message = binding spec' bug).
|
|
617
|
+
# STABLE/slot-0 but write-RARELY (changes only on a require/drop/done event) → the prefix stays cache-warm.
|
|
618
|
+
# MISSION — the session-spanning NORTH STAR (goal mode): the overarching objective that persists
|
|
619
|
+
# ACROSS topic switches, above any single topic's goal. Self-suppresses when unset → zero bytes by
|
|
620
|
+
# default (no bloat), real opt-in-by-use feature. STABLE/slot-0, changes rarely → prefix stays cache-warm.
|
|
621
|
+
("mission", STABLE, lambda c: (f"# MISSION (your overarching objective for this whole session — keep steering toward it across tasks until you call mission_done)\n{c['s'].mission}\n\n" if getattr(c['s'], 'mission', '') else ""), 0),
|
|
622
|
+
("requirements", STABLE, lambda c: (f"# STANDING REQUIREMENTS (the contract that must HOLD when the task is done — honor each EXACTLY; '[x]' = already satisfied)\n{render_requirements(c['s'].requirements)}\n\n" if getattr(c['s'], 'requirements', None) else ""), 0),
|
|
623
|
+
("open_files", STABLE, lambda c: "# OPEN FILES (live — your ground truth; edit based on this. Lines are numbered for citation/reference; the leading number is NOT part of the file — never include it in a str_replace old_string)\n" + c["artifacts"], 0),
|
|
624
|
+
("related_code", STABLE, lambda c: (f"\n# RELATED CODE (repo map — relevant files & their definitions; read/grep for the actual code)\n{c['discovery']}\n" if c["discovery"] else ""), 1),
|
|
625
|
+
# REPO MAP moved to the BYTE-STABLE system prefix (make_build_slice) so it's a prompt-cache PREFIX
|
|
626
|
+
# shared across every turn + subagent, instead of full-price in the volatile user slice. (Region removed.)
|
|
627
|
+
("skills", STABLE, lambda c: (f"# ACTIVE SKILL(S) (loaded instructions — FOLLOW these for the task)\n{render_skills(c['s'].active_skills)}\n\n" if render_skills(c["s"].active_skills) else ""), 2),
|
|
628
|
+
("memory", STABLE, lambda c: (f"# RELEVANT MEMORY (lessons from past sessions — apply if useful)\n{c['memory']}\n\n" if c["memory"] else ""), 2),
|
|
629
|
+
("conversation", STABLE, lambda c: (f"# RECENT CONVERSATION (the last few exchanges this session — for continuity; older turns are paged out — see PAGED-OUT HISTORY below for the recall_history call to fetch each)\n{render_conversation(c['s'])}\n\n" if render_conversation(c["s"]) else ""), 2),
|
|
630
|
+
("findings", VOLATILE, lambda c: (f"# YOUR NOTES FROM PRIOR TOOL CALLS (established facts to REUSE — don't re-derive these; OPEN FILES stays the ground truth for current file contents. Per-note tags mark trust: no tag = observed, '(your note)' = your summary, '(UNVERIFIED claim)' = not yet confirmed)\n{render_findings(c['s'].findings[-c['max_findings']:], c['s'].finding_source)}\n\n" if render_findings(c["s"].findings[-c["max_findings"]:], c["s"].finding_source) else ""), 3),
|
|
631
|
+
("plan", VOLATILE, lambda c: (f"# PLAN (your ordered steps & live progress — keep exactly ONE step in_progress; '[~]'=in progress, '[x]'=done, '[ ]'=pending; update with update_plan)\n{render_plan(c['s'].plan)}\n\n" if getattr(c['s'], 'plan', None) else ""), 3),
|
|
632
|
+
("world", VOLATILE, lambda c: (f"# WORLD MODEL (durable task state YOU maintain — your map / inventory / progress; update with world_set, it persists across turns until the task changes)\n{render_world(c['s'].world)}\n\n" if c['s'].world else ""), 3),
|
|
633
|
+
("reviewed", VOLATILE, lambda c: render_reviewed(c["s"]), 3),
|
|
634
|
+
("threads", VOLATILE, lambda c: (f"# OTHER OPEN THREADS (parked topics — resume one with switch_topic; do NOT mix them into the current task)\n{c['threads']}\n\n" if c["threads"] else ""), 3),
|
|
635
|
+
# PAGED-OUT HISTORY — the cache MANIFEST: earlier turns of THIS session that are NOT in the slice,
|
|
636
|
+
# each with the exact recall_history call to page it back. Sits beside GHOST INDEX (same "it's paged
|
|
637
|
+
# out, here's the one call to get it" idiom — files there, turns here) so the model has a SEEN target
|
|
638
|
+
# to call; an unseen cache is the dead channel. Locators only (moat); suppresses itself when empty.
|
|
639
|
+
("cache_manifest", VOLATILE, lambda c: (f"\n# PAGED-OUT HISTORY (earlier turns of THIS session — NOT in the slice; page any back with the call shown)\n{c['cache_manifest']}\n" if c.get("cache_manifest") else ""), 3),
|
|
640
|
+
# # REPEATED/FAILING ACTIONS header (always present; body says "(nothing…)" when empty) closes slot 3.
|
|
641
|
+
("action_header", VOLATILE, lambda c: "# REPEATED/FAILING ACTIONS", 3),
|
|
642
|
+
("action_history", VOLATILE, lambda c: render_action_history(c["s"].action_log), 4), # body — own part
|
|
643
|
+
# (CURRENT REQUEST renders OUTSIDE the fence in build() — see render_current_request above — not here.)
|
|
644
|
+
# REPO STATE — the LIVE world-state region (SENSORY CORTEX — a derived view, tier A): current branch
|
|
645
|
+
# + changed-file set, re-probed every build (not the session-start snapshot, and never persisted).
|
|
646
|
+
# High-authority current-state ground truth, so it rides in the salient tail just above the blocker/
|
|
647
|
+
# error. Suppresses itself when not a repo.
|
|
648
|
+
# CURRENT PROJECT — where the agent is working RIGHT NOW (the frame on top of the immutable boundary):
|
|
649
|
+
# the moved relative-path base + auto-granted file-tool reach, otherwise invisible. Rides the salient
|
|
650
|
+
# tail so a follow-up's referent resolves HERE. Self-suppresses for the single-project case.
|
|
651
|
+
("focus", VOLATILE, lambda c: (f"# CURRENT PROJECT (where you are working RIGHT NOW — bare relative paths resolve here and your file tools reach here)\n{c['focus']}\n\n" if c.get("focus") else ""), 6),
|
|
652
|
+
("worktree", VOLATILE, lambda c: (f"# REPO STATE (LIVE — current branch & changed files, re-read THIS turn; this is the up-to-date git state — trust it over any session-start project facts)\n{c['worktree']}\n\n" if c.get("worktree") else ""), 6),
|
|
653
|
+
# OPEN USER REPORT rides ABOVE the error (a stale "done" note can't outrank a user's BROKEN report);
|
|
654
|
+
# both are the highest-authority, freshest tail right above NOW.
|
|
655
|
+
("user_report", VOLATILE, lambda c: (f"# OPEN USER REPORT (the user reports this is BROKEN — treat it as an UNRESOLVED blocker; do NOT claim it is done or already working until you have VERIFIED the fix against the real artifact, e.g. run/open it and observe success)\n{c['s'].open_report}\n\n" if c["s"].open_report else ""), 6),
|
|
656
|
+
("error", VOLATILE, lambda c: (f"# CURRENT ERROR (unresolved — fix this, verbatim)\n{c['s'].last_error}\n\n" if c["s"].last_error else ""), 6),
|
|
657
|
+
("closure", VOLATILE, lambda c: render_closure(c["s"]), 6),
|
|
658
|
+
("convergence", VOLATILE, lambda c: render_convergence(c["s"]), 6),
|
|
659
|
+
# (NOW footer renders OUTSIDE the fence as the outermost tail in build() — see render_now above — not here.)
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def render_regions(ctx: dict) -> str:
|
|
664
|
+
"""Iterate REGION_ORDER, render each typed region into its framed fragment, and assemble the ONE
|
|
665
|
+
user string (the moat). Each region suppresses itself when empty; the slot grouping + the blank-line
|
|
666
|
+
separator between the action tally (slot 4) and the high-authority tail (slot 6) keeps the stable bulk
|
|
667
|
+
leading for prompt-cache locality and the volatile salient tail trailing. `ctx` carries the Slice + the
|
|
668
|
+
pre-rendered passthroughs (artifacts / discovery / memory / threads) + the max_findings cap."""
|
|
669
|
+
slots: dict[int, str] = {}
|
|
670
|
+
for _name, _tier, render, slot in REGION_ORDER:
|
|
671
|
+
slots[slot] = slots.get(slot, "") + render(ctx)
|
|
672
|
+
if not slots:
|
|
673
|
+
return ""
|
|
674
|
+
# #17: assemble by iterating ALL slot positions rather than a hand-synced literal index list — that
|
|
675
|
+
# list KeyError'd if a leading slot was empty and SILENTLY DROPPED any region added at a gap slot
|
|
676
|
+
# (e.g. 5). Slot 5 stays the reserved blank separator between the stable bulk (≤4, cache-leading) and
|
|
677
|
+
# the volatile high-authority tail (≥6); an empty slot renders as "" (a blank line), as before.
|
|
678
|
+
return "\n".join(slots.get(i, "") for i in range(max(slots) + 1))
|