@ssheleg/agent-stack 0.3.0 → 0.4.0

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,39 @@ All notable changes to this project are documented here.
4
4
  Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
  Versioning: [SemVer](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.4.0] — 2026-08-12
8
+
9
+ ### Added
10
+
11
+ - **`references/context-engineering.md`** — what the loop gives up when the window
12
+ runs out, which the skill named a threshold for and never answered. The five-rung
13
+ compaction ladder, cheapest first, with a re-measure between rungs so a small
14
+ overage never buys a model call; the rung that exists only for the case where the
15
+ compaction request itself does not fit; the **tool-pair invariant** — one boundary
16
+ finder used by every rung, because a truncation that orphans a `tool_use` fails the
17
+ *next* request with a 400 in the middle of a task; **typed carryover blocks** copied
18
+ across the boundary rather than summarized, since a summarizer keeps the discussion
19
+ and drops the state, including the flag that said not to write; **tool-output
20
+ offload** to a file with the path left in context, because trimming history cannot
21
+ save a window one result already filled; token estimation and the direction it errs;
22
+ the compaction circuit breaker; sub-agent context isolation; the filesystem as
23
+ context; and how to pick constants for your own window.
24
+
25
+ Ladder structure and the attachment taxonomy are adapted from
26
+ [HKUDS/OpenHarness](https://github.com/HKUDS/OpenHarness) (MIT),
27
+ `services/compact/__init__.py` and `services/tool_outputs.py`. The constants are
28
+ deliberately ours — and deliberately absent, with anchors for choosing them, because
29
+ a threshold copied without its window is a number nobody can defend.
30
+
31
+ ### Changed
32
+
33
+ - **§12 Context Engineering** in the body: five traps an agent cannot know to look up,
34
+ because it does not know they exist. **§7 gains Layer 0 — carryover state**, the one
35
+ memory layer whose survival is deterministic rather than a model's choice. §2 gains
36
+ the iteration refund. Body lands at 486 lines / ~4985 tokens against the 500 / 5000
37
+ budget — every insertion was cut twice to fit, and the depth is in the reference
38
+ where it belongs.
39
+
7
40
  ## [0.3.0] — 2026-08-12
8
41
 
9
42
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssheleg/agent-stack",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Production patterns for AI agent orchestrators — tool-calling loops, multi-stage pipelines with checkpoints, LLM provider routing with fallback, four-layer memory with confidence decay — plus the wallet side of reselling LLM access. This package is the installer CLI.",
5
5
  "bin": {
6
6
  "agent-stack": "bin/agent-stack.js"
@@ -2,7 +2,7 @@
2
2
  "name": "agent-stack",
3
3
  "displayName": "Agent Stack",
4
4
  "description": "Production patterns for AI agent orchestrators: tool-calling loops, multi-stage pipelines with checkpoints, LLM provider routing with fallback, four-layer memory with confidence decay — plus the wallet side of reselling LLM access.",
5
- "version": "0.3.0",
5
+ "version": "0.4.0",
6
6
  "author": {
7
7
  "name": "ssheleg",
8
8
  "url": "https://x.com/sshlg93"
@@ -137,6 +137,7 @@ else:
137
137
  - **In-loop trimming**: At ~80% capacity, collapse older assistant+tool pairs into one-liner summaries
138
138
  - **Token limit recovery**: On `LLMTokenLimitError`, compress to 60% and retry once. If still fails, return partial answer
139
139
  - **Max iterations guard**: Always have a hard limit. On exhaustion, compose best-effort answer from data gathered so far
140
+ - **Iteration refund**: a recoverable provider error is not charged to that guard
140
141
 
141
142
  ---
142
143
 
@@ -323,6 +324,9 @@ context window, so allocation has to be decided per call rather than per layer:
323
324
  a session that trims chat history to fit a large set of learnings has quietly
324
325
  chosen old generalities over what the user said sixty seconds ago. Give layer 1
325
326
  a floor.
327
+
328
+ **Layer 0 — carryover state.** Goal, artifacts, verified work and restrictive
329
+ mode cross a compaction boundary as copied typed blocks, not prose (§12).
326
330
  ## 8. Self-Learning Feedback Loops
327
331
 
328
332
  ### Cycle 1: Automatic (Validation Loop)
@@ -436,6 +440,27 @@ async def handle_ask_user(tc, context, wf_id):
436
440
 
437
441
  ---
438
442
 
443
+ ## 12. Context Engineering
444
+
445
+ **Compaction is a ladder, not a call.** Clear old tool results, collapse
446
+ oversized blocks, condense messages, and only then pay a summarizer —
447
+ re-measuring between rungs. Most pressure resolves before the first call.
448
+
449
+ **Never orphan a `tool_use`.** Every truncation point lands *between* an
450
+ assistant+tool pair, or the next request is a 400 in the middle of a task.
451
+
452
+ **Carry state across the boundary as typed blocks, not prose** — goal,
453
+ artifacts, verified work, restrictive mode. A summarizer keeps the discussion
454
+ and drops the state, including the flag that said not to write anything.
455
+
456
+ **Offload a large tool result to a file, keep the path.** Trimming history
457
+ cannot save a window one tool output already filled.
458
+
459
+ **A sub-agent's value is its own window**: it returns a typed summary, not a
460
+ transcript.
461
+
462
+ ---
463
+
439
464
  ## Checklist — Building a New Orchestrator
440
465
 
441
466
  - [ ] Shared `AgentContext` dataclass with all sub-agents
@@ -459,6 +484,7 @@ async def handle_ask_user(tc, context, wf_id):
459
484
  - [ ] Multi-stage pipeline with checkpoints and resume
460
485
  - [ ] `ask_user` clarification mechanism
461
486
  - [ ] Graceful degradation (partial answers on context overflow or max iterations)
487
+ - [ ] Compaction ladder, tool-pair-safe boundaries, typed carryover, output offload
462
488
 
463
489
  ---
464
490
 
@@ -470,4 +496,5 @@ are the territory.
470
496
  | File | Read it when |
471
497
  |---|---|
472
498
  | [`references/patterns.md`](references/patterns.md) | you need the **data models and algorithms**: message and result protocols, pipeline models, the SQL validation loop, context-window sizes and token estimation, learning-extraction heuristics, confidence lifecycle, fuzzy dedup, conflict resolution, cross-resource transfer, the no-LLM suggestion engine |
499
+ | [`references/context-engineering.md`](references/context-engineering.md) | the loop is **running out of window**: the five-rung compaction ladder, the tool-pair boundary invariant, typed carryover attachments, tool-output offload, token estimation, the compaction circuit breaker, sub-agent isolation, and how to pick your own constants |
473
500
  | [`references/llm-proxy-billing.md`](references/llm-proxy-billing.md) | the product **resells LLM access**: tiered wallets and where markup applies, two-phase commit against a provider API with compensating transactions, advisory locking, optimistic concurrency for reclaims, spend-delta polling and its three cases, budget/loop/auto-pause guardrails, per-tenant key lifecycle and healing, the refund waterfall, model routing |
@@ -0,0 +1,201 @@
1
+ # Context engineering — what survives the window, and how
2
+
3
+ **Load this when** the loop is running out of window: choosing a compaction strategy,
4
+ deciding what crosses the boundary, bounding a tool result that will not fit, or
5
+ splitting message history without breaking the next request.
6
+
7
+ `SKILL.md` §2 gives the loop and its two thresholds — trim at ~80% of the window,
8
+ inject the wrap-up at ~70%. This file is what happens *at* those thresholds. The
9
+ difference matters: §2 decides **when** to act, this decides **what to give up**.
10
+
11
+ Ladder structure and the attachment taxonomy are adapted from
12
+ [HKUDS/OpenHarness](https://github.com/HKUDS/OpenHarness) (MIT),
13
+ `src/openharness/services/compact/__init__.py` and `services/tool_outputs.py`. The
14
+ constants below are ours; theirs are tuned for a different window and a different tool
15
+ mix, and a threshold copied without its context is a number nobody can defend.
16
+
17
+ ## Contents
18
+
19
+ - The ladder — five rungs, cheapest first
20
+ - Re-measure between rungs
21
+ - The tool-pair invariant
22
+ - Carryover attachments — what crosses the boundary
23
+ - Tool-output offload
24
+ - Estimating what you have left
25
+ - The circuit breaker
26
+ - Sub-agent context isolation
27
+ - The filesystem as context
28
+ - Choosing your own constants
29
+
30
+ ## The ladder — five rungs, cheapest first
31
+
32
+ Compaction is not one call. It is an ordered set of strategies, and the expensive one
33
+ is last. A loop that summarizes at the first sign of pressure pays a model call and a
34
+ round-trip for what a string operation would have solved.
35
+
36
+ | Rung | What it does | Cost | Loses |
37
+ |---|---|---|---|
38
+ | 1. Microcompact | Replace old tool results with a tombstone: `[tool result cleared]`. Keep the N most recent. | free | old observations, kept recent ones |
39
+ | 2. Head/tail collapse | For an oversized text block, keep a head and a tail, elide the middle with a marker | free | the middle of long outputs |
40
+ | 3. Session condensation | Collapse each older message to a one-line summary, capped in total | free | phrasing, keeps the thread of events |
41
+ | 4. LLM compaction | One model call summarizes the transcript into a structured brief | a call + latency | anything the summarizer does not think to keep |
42
+ | 5. Prompt-round truncation | Drop the oldest whole prompt rounds, boundary-aligned | free | the earliest history entirely |
43
+
44
+ Rung 5 exists for one case: **the compaction request itself does not fit.** When rung 4
45
+ fails because the transcript it must summarize is over the limit, summarizing harder is
46
+ not available — you drop oldest rounds and retry, bounded by a small retry count.
47
+
48
+ **Eligibility, not just age.** A tool result becomes a rung-1 candidate by size as well
49
+ as position: anything past a few thousand characters, and every result from an external
50
+ tool server, whose outputs are the usual window hog and the least likely to be re-read.
51
+
52
+ ## Re-measure between rungs
53
+
54
+ After every rung, measure again and stop if you are under the threshold. Two failure
55
+ modes hide here:
56
+
57
+ - **Running the whole ladder every time** turns a 200-character overage into a model
58
+ call. Rung 1 usually settles it.
59
+ - **Trusting a rung that did nothing.** Head/tail collapse on a transcript with no
60
+ oversized blocks changes nothing. If the estimate did not drop, reject the result and
61
+ escalate rather than recording a compaction that never happened.
62
+
63
+ ## The tool-pair invariant
64
+
65
+ **A tool call and its result are one unit. Never split them.**
66
+
67
+ Every provider rejects a request where an assistant message announces a tool call whose
68
+ result is missing, or a tool result with no matching call. It is not a soft error — the
69
+ next request fails, mid-task, with a 400 that names nothing useful.
70
+
71
+ This makes every boundary computation in this file conditional: a truncation point, a
72
+ collapse range, a dropped round must all land **between** pairs. Write the boundary
73
+ finder once, use it from every rung, and give it a test that plants an orphan and
74
+ requires rejection.
75
+
76
+ The same invariant governs parallel dispatch: if one tool in a batch raises, its result
77
+ block must still be emitted — as an error result. A sibling's crash is not a reason to
78
+ send a malformed request.
79
+
80
+ ## Carryover attachments — what crosses the boundary
81
+
82
+ A summarizer keeps prose and drops state. Ask one to compress a transcript and it will
83
+ faithfully preserve the discussion while losing the fact that you are in a read-only
84
+ mode, the path of the file you verified, and what the user actually asked for.
85
+
86
+ So state does not go through the summarizer. It is accumulated during normal execution
87
+ and re-attached after compaction as **typed blocks**:
88
+
89
+ | Block | Carries | Why it is lost otherwise |
90
+ |---|---|---|
91
+ | `task_focus` | the goal, recent sub-goals, active artifacts, verified state, next step | the summarizer rewrites the goal into its own words and drifts |
92
+ | `recent_files` | paths read or written, most recent first | the agent re-reads what it already has |
93
+ | `verified_work` | what was checked and how it was checked | re-verification, or worse, a claim of verification that was never re-run |
94
+ | `plan` | the active plan and any mode that restricts what may be done | **a safety mode forgotten across a boundary is how a read-only run starts writing** |
95
+ | `invoked_skills` | which doctrine is already loaded | re-loading a large file that is already in effect |
96
+ | `work_log` | one line per significant action | the agent repeats a step it already completed |
97
+
98
+ Accumulate them in bounded buckets during the loop — a small cap per bucket, oldest
99
+ evicted — so the attachment cost is known in advance and does not itself grow into a
100
+ context problem. Cap the number of attached blocks too.
101
+
102
+ The property that matters: this is **deterministic**. Structured state survives because
103
+ it is copied, not because a model chose to keep it.
104
+
105
+ ## Tool-output offload
106
+
107
+ One large tool result poisons a window before any history trimming is relevant. Trimming
108
+ the conversation does not help when the problem is a single 200 KB response sitting in
109
+ the current turn.
110
+
111
+ The rule: **above an inline limit, write the full output to a file and put a preview
112
+ plus the path into the context.** The model reads the preview, and when it needs the
113
+ rest it reads the file with the tools it already has.
114
+
115
+ - **Inline limit** — the size above which offload happens.
116
+ - **Preview size** — enough to decide whether the rest is needed; head, or head and tail
117
+ when the tail carries the verdict (test runs, build logs).
118
+ - **Path** — stable and unique per call, so two calls to the same tool do not collide.
119
+ - **Location** — a scratch directory scoped to the run, not the project tree; artifacts
120
+ of a loop are not deliverables and must never arrive in a diff.
121
+
122
+ Both limits belong in configuration. The right value depends on the window and the tool
123
+ mix, and the only way to find it is to measure a real run.
124
+
125
+ ## Estimating what you have left
126
+
127
+ Every threshold in this file depends on an estimate, and estimates are optimistic in the
128
+ direction that hurts:
129
+
130
+ - **Character-based estimation runs low.** A divisor calibrated on prose under-counts
131
+ code, JSON and non-Latin text. Apply a padding factor, and prefer being early over
132
+ being right.
133
+ - **Non-text content costs more than its representation suggests.** An image is worth
134
+ hundreds to thousands of tokens depending on its dimensions; a reference to a file is
135
+ worth nothing until it is read.
136
+ - **Reserve the output.** The threshold is not "the window" — it is the window minus the
137
+ space the answer needs, minus a buffer for the next tool result. Express the trigger
138
+ as a floor in absolute tokens, not only as a percentage: at a small window a
139
+ percentage silently reserves too little.
140
+
141
+ ## The circuit breaker
142
+
143
+ Compaction can fail — the summarizer errors, the provider is down, the result does not
144
+ shrink. **After a small number of consecutive failures, disable compaction for the run
145
+ and degrade deliberately**: stop adding to the context, compose the best answer
146
+ available, and say that compaction is unavailable.
147
+
148
+ Without this, a failing compaction is retried at the top of every iteration, and the
149
+ loop spends its remaining budget on the one operation that is not working.
150
+
151
+ **Related: refund the iteration.** When an iteration ends in a recoverable provider
152
+ error — a token limit that can be clamped and retried, a rate limit — do not charge it
153
+ to the max-iteration guard. A misconfiguration should not consume the budget that exists
154
+ to stop a runaway agent.
155
+
156
+ ## Sub-agent context isolation
157
+
158
+ A sub-agent's value is not that it is a different prompt. **It is that it has its own
159
+ window.**
160
+
161
+ The contract: the sub-agent receives a task and the context it needs, works in a window
162
+ the parent never sees, and returns a typed result — a summary, not a transcript. The
163
+ parent's window grows by the size of the result, not by the size of the work.
164
+
165
+ This makes delegation the strongest context tool available: a search across forty files
166
+ costs the parent one paragraph. It also sets the design rule — if a sub-agent's return
167
+ value is proportional to its input rather than its conclusion, it is a function call
168
+ wearing a costume, and it will fill the parent's window anyway.
169
+
170
+ ## The filesystem as context
171
+
172
+ Anything durable belongs in a file, not in the transcript:
173
+
174
+ - **It survives the boundary.** Compaction cannot delete what was never in the window.
175
+ - **It survives the process.** A crash loses the conversation and keeps the work.
176
+ - **It is shared.** Two agents on one task coordinate through files; a transcript is
177
+ private to one loop.
178
+ - **It is addressable.** A path is a few tokens; the content it names can be any size.
179
+
180
+ The pattern is the same in each case: do the work, write the artifact, keep the path.
181
+ Where the runtime offers no real filesystem, the same contract works over any keyed
182
+ store — what matters is that the address is cheap and the content is retrievable, not
183
+ that it is POSIX.
184
+
185
+ ## Choosing your own constants
186
+
187
+ Every number in this file is deliberately absent, because the useful value depends on
188
+ the window, the tool mix and the cost of a summarizer call in your stack. Pick them
189
+ against these anchors:
190
+
191
+ 1. **Reserve output before you reserve anything else.** Start from window minus expected
192
+ answer minus one worst-case tool result.
193
+ 2. **Rung 1's keep-count** is the smallest number of recent tool results the loop
194
+ actually re-reads. Measure it; the intuition is always too high.
195
+ 3. **The inline limit for offload** should be crossed only by outputs that are genuinely
196
+ large — if half of all tool calls offload, the limit is too low and the model is
197
+ reading files instead of working.
198
+ 4. **The circuit-breaker count** is small. Three consecutive failures is a pattern, not
199
+ a coincidence.
200
+ 5. **Write them down where the loop reads them**, not inline at the call site. A
201
+ threshold nobody can find is a threshold nobody will tune.