@qilin-agent/compaction 0.0.0-stage → 3.1.3
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/LICENSE +21 -0
- package/README.i18n.yaml +85 -0
- package/README.md +174 -2
- package/README.zh.md +175 -0
- package/lib/index.js +175 -0
- package/lib/types/brand.d.ts +10 -0
- package/lib/types/brand.js +9 -0
- package/lib/types/checkpoint.d.ts +39 -0
- package/lib/types/checkpoint.js +36 -0
- package/lib/types/index.d.ts +160 -0
- package/lib/types/index.js +50 -0
- package/lib/types/tool-pairing.d.ts +26 -0
- package/lib/types/tool-pairing.js +97 -0
- package/lib/types/types.d.ts +132 -0
- package/lib/types/types.js +10 -0
- package/package.json +52 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kkutysllb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/compaction/compaction/README.md
|
|
5
|
+
/:
|
|
6
|
+
en: 5c47c9629036f119
|
|
7
|
+
zh: 7730d7468a38b38d
|
|
8
|
+
/qilin-compaction:
|
|
9
|
+
en: c840d0ca2fbd48b5
|
|
10
|
+
zh: 7f56057c5118fe46
|
|
11
|
+
/qilin-compaction/summary:
|
|
12
|
+
en: 45df7ffaa893ddba
|
|
13
|
+
zh: bc09330e487a0822
|
|
14
|
+
/qilin-compaction/table-of-contents:
|
|
15
|
+
en: d152484eb41ac6b4
|
|
16
|
+
zh: 09388d293f9be9cb
|
|
17
|
+
/qilin-compaction/use-this-package:
|
|
18
|
+
en: 7d1dad97626dbd2e
|
|
19
|
+
zh: 075fef4e1844ad12
|
|
20
|
+
/qilin-compaction/use-this-package/when-to-choose-it:
|
|
21
|
+
en: 5a17090293b8cfb0
|
|
22
|
+
zh: 6707423be7039a60
|
|
23
|
+
/qilin-compaction/use-this-package/what-condensation-looks-like:
|
|
24
|
+
en: 0abbee3a3b929a90
|
|
25
|
+
zh: 334736dffe93488d
|
|
26
|
+
/qilin-compaction/use-this-package/turning-condensation-on:
|
|
27
|
+
en: e551048815373de2
|
|
28
|
+
zh: bc08e4a0946a8d80
|
|
29
|
+
/qilin-compaction/use-this-package/implementing-a-backend:
|
|
30
|
+
en: 8f4465774226630c
|
|
31
|
+
zh: cb5351816428e083
|
|
32
|
+
/qilin-compaction/use-this-package/recognizing-condensed-history:
|
|
33
|
+
en: 6b698c775b41d3e5
|
|
34
|
+
zh: 333d2b86cc1d6eb0
|
|
35
|
+
/qilin-compaction/understand-the-implementation:
|
|
36
|
+
en: 321c3dc665ea03ff
|
|
37
|
+
zh: f0fa408cde7b0454
|
|
38
|
+
/qilin-compaction/understand-the-implementation/design-philosophy:
|
|
39
|
+
en: fc76a3dfe6dfb68a
|
|
40
|
+
zh: cee531777137932b
|
|
41
|
+
/qilin-compaction/understand-the-implementation/service-api:
|
|
42
|
+
en: 0b9edbbf144e6973
|
|
43
|
+
zh: 6de6e6d920e70e96
|
|
44
|
+
/qilin-compaction/understand-the-implementation/manual-failure-taxonomy:
|
|
45
|
+
en: f139df949cf288ea
|
|
46
|
+
zh: 51ab9f127578cf75
|
|
47
|
+
/qilin-compaction/understand-the-implementation/tool-pairing-boundaries:
|
|
48
|
+
en: 15dad68a04d6c86b
|
|
49
|
+
zh: 3b95acf1b7e503bf
|
|
50
|
+
/qilin-compaction/understand-the-implementation/the-surface-contract:
|
|
51
|
+
en: 8606610465cf1d5e
|
|
52
|
+
zh: 5dad56458947e548
|
|
53
|
+
/qilin-compaction/understand-the-implementation/source-map:
|
|
54
|
+
en: ca0a8b4b73e2556f
|
|
55
|
+
zh: 1b671f96fb217d5e
|
|
56
|
+
/qilin-compaction/understand-the-implementation/locking-and-serialization:
|
|
57
|
+
en: 68caa51bed28bada
|
|
58
|
+
zh: 3f8261f9dcba95ed
|
|
59
|
+
/qilin-compaction/understand-the-implementation/events:
|
|
60
|
+
en: c972fe7e246ac3e0
|
|
61
|
+
zh: 69c7b92eaf8d0e44
|
|
62
|
+
/qilin-compaction/further-exploration:
|
|
63
|
+
en: 30ce8988a71dceec
|
|
64
|
+
zh: 03b615969099762d
|
|
65
|
+
/qilin-compaction/model-experience:
|
|
66
|
+
en: 215e7ba838619b7b
|
|
67
|
+
zh: a311da3843709f90
|
|
68
|
+
/qilin-compaction/model-experience/conversation-history-when-a-backend-is-invoked:
|
|
69
|
+
en: 94de97ed1e85db85
|
|
70
|
+
zh: c8e37d8050609e63
|
|
71
|
+
/qilin-compaction/model-experience/conversation-history-when-a-backend-is-invoked/what-the-model-sees:
|
|
72
|
+
en: 3c4875a13da17814
|
|
73
|
+
zh: 9b257950a9e2c7c1
|
|
74
|
+
/qilin-compaction/model-experience/conversation-history-when-a-backend-is-invoked/token-effect:
|
|
75
|
+
en: be91f4a4bbb9338b
|
|
76
|
+
zh: 2b5de31d4d7caff1
|
|
77
|
+
/qilin-compaction/model-experience/conversation-history-when-a-backend-is-invoked/kv-cache-effect:
|
|
78
|
+
en: 40ba838a2309c44a
|
|
79
|
+
zh: c609fc1587a0d666
|
|
80
|
+
/qilin-compaction/known-limitations-and-deferred-work:
|
|
81
|
+
en: fee9ba06d3e5f35c
|
|
82
|
+
zh: 2bb5241e2447d15d
|
|
83
|
+
/qilin-compaction/known-limitations-and-deferred-work/dev-note:
|
|
84
|
+
en: 270e23ae6414aff0
|
|
85
|
+
zh: c6d219b8a547dbca
|
package/README.md
CHANGED
|
@@ -1,3 +1,175 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "Shared compaction contract for backend implementers and deployers: what conversation condensation does, when to use it, and how to build a backend."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# @qilin-agent/compaction
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`qilin-compaction` lets a long session condense its older history into a single summary message, keep the recent conversation intact, and continue as if the summary had always been there — with a backend such as `qilin-compaction-basic` and the optional `/compact` command. The shadowed content stays in the session log, so replaying the session deterministically reproduces the same condensed conversation. Reach for this package when you implement a condensation backend, build something that triggers condensation, or need to recognize condensed messages — it performs no condensation itself. Choose the shipped backend when you want the feature working out of the box.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
Decide first what you need. The shipped backend plus the `/compact` command give you automatic and on-demand condensation with no code; this package only matters when you extend or reimplement the feature. The sections below describe what condensation does, how to turn it on, and how to write a backend.
|
|
29
|
+
|
|
30
|
+
### When to choose it
|
|
31
|
+
|
|
32
|
+
Choose `qilin-compaction-basic` when a model-written summary fits your needs: you get condensation automatically as the conversation grows, and on demand through `qilin-command-compact`. Choose this package when you write a backend with a different summarizer — a fixed template or a remote service — or when you build something that triggers condensation programmatically. Do not mount it alone: without a backend nothing condenses.
|
|
33
|
+
|
|
34
|
+
### What condensation looks like
|
|
35
|
+
|
|
36
|
+
When condensation runs, the selected older span of the conversation is replaced by one summary message; recent history is untouched and the conversation continues from the summary. Condensation can be triggered automatically as token pressure builds, on demand, or over an explicit span; the result reports which history was condensed and the estimated tokens freed.
|
|
37
|
+
|
|
38
|
+
### Turning condensation on
|
|
39
|
+
|
|
40
|
+
Mount the shipped backend to register the condensation service, and add `qilin-command-compact` for the on-demand command:
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
- name: '@qilin-agent/compaction-basic'
|
|
44
|
+
- name: '@qilin-agent/command-compact'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
With these two rows the feature is on: the conversation condenses automatically as it grows, and `/compact` condenses immediately on request and reports how many history items were replaced. If no backend is mounted, nothing condenses and `/compact` fails; the full dependency chain for the shipped backend is in its own README.
|
|
48
|
+
|
|
49
|
+
### Implementing a backend
|
|
50
|
+
|
|
51
|
+
Extend the provided base class and implement three operations: one that decides and performs condensation for an automatic trigger, one that condenses on demand, and one that condenses an explicit range of the conversation. Load your class as a plugin and it becomes the condensation service for the composition. The exact signatures, the failure rules, and the checkpoint marker every backend must produce are in the implementation section below and in the [compaction subsystem reference](../../../docs/subsystems/compaction.md).
|
|
52
|
+
|
|
53
|
+
### Recognizing condensed history
|
|
54
|
+
|
|
55
|
+
Messages a backend wrote as summaries carry a stable marker, so any consumer can recognize condensed history after persistence or cloning without knowing which backend produced it. The marker is exported from the package root and from a Kylin-free subpath that client and wire programs can import.
|
|
56
|
+
|
|
57
|
+
-----
|
|
58
|
+
|
|
59
|
+
<a id="understand-the-implementation"></a>
|
|
60
|
+
## Understand the implementation
|
|
61
|
+
|
|
62
|
+
<details>
|
|
63
|
+
<summary>Implementation internals — click to expand</summary>
|
|
64
|
+
|
|
65
|
+
This section explains the contract in API terms and the design decisions behind it; the feature-level behavior is covered in [Use this package](#use-this-package).
|
|
66
|
+
|
|
67
|
+
### Design philosophy
|
|
68
|
+
|
|
69
|
+
The seam is built on one split and three commitments:
|
|
70
|
+
|
|
71
|
+
- **Abstract contract, concrete backends.** The interface states what condensation does; providers own policy, retention, and summarization so each role evolves and swaps independently.
|
|
72
|
+
- **Session and LLM vocabulary are part of the contract.** The operations act on a `Session` and the summary uses `ContentBlock`, so the Service Definition depends on `qilin-session` and `qilin-llm` despite the general Kylin-only guidance — a deliberate deviation recorded in the [compaction capability-seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md).
|
|
73
|
+
- **The log-recorded bracket is the lock.** `compaction/start` is appended before summarization yields and `compaction/end` releases; every failure makes exactly one close attempt, and a failed close leaves the unmatched start as the intentional busy signal.
|
|
74
|
+
- **The surface is mutated exactly once.** The summary rides on a `user/message` replacement inside the bracket; all `compaction/*` events stay log-only.
|
|
75
|
+
|
|
76
|
+
### Service API
|
|
77
|
+
|
|
78
|
+
The contract is three abstract operations a backend implements: `compactIfNeeded` for automatic `pressure` or `context-overflow` triggers, `compactNow` for one explicit on-demand reduction, and `compactRegion` for a caller-selected surface range. Reusable request measurement is a separate service, `ctx.tokenMeter`. The exhaustive per-operation semantics live in the [compaction subsystem reference](../../../docs/subsystems/compaction.md); the exact signatures are in [`src/index.ts`](src/index.ts).
|
|
79
|
+
|
|
80
|
+
A backend that summarizes through `ctx.llm.stream()` must forward the abort signal into the call's `GenerateOptions.signal`, so an abort or fiber dispose tears down the in-flight summarization. Automatic and explicit-region brackets recover their numeric owner from the open turn; manual brackets require no open turn and stamp `turn: null`.
|
|
81
|
+
|
|
82
|
+
### Manual failure taxonomy
|
|
83
|
+
|
|
84
|
+
The synchronous `compaction/summary-error` waterfall lets a backend request durable input recovery for a failed summary and an explicit selection. A listener returns true only after recording progress, or calls `next()` to delegate. The backend checks cancellation and selection stability before dispatch, then re-derives and re-prices input before retrying. The event declaration documents its payload; image policy belongs to `qilin-compaction-image-offload`.
|
|
85
|
+
|
|
86
|
+
Expected manual failures throw `ManualCompactionError` with a stable `code` from a small closed set; only failures after the `compaction/start` marker are recorded — as a `compaction/end` carrying the error — while a `busy` rejection or pre-start cancellation leaves no record. The per-code semantics live in the [compaction subsystem reference](../../../docs/subsystems/compaction.md).
|
|
87
|
+
|
|
88
|
+
<a id="tool-pairing-boundaries"></a>
|
|
89
|
+
### Tool-pairing boundaries
|
|
90
|
+
|
|
91
|
+
The Service Definition exports `toolPairingBalancedBefore(session, seq)` and `toolPairingBalancedAfter(session, seq)` for snapping and validating compaction edges. A safe edge has no unanswered assistant tool call crossing it. Each helper validates that the event sequence is in the current surface and answers from balances cached per cut in surface order, so repeated checks read no events; a replace generation rebuilds the cache, and missing seqs or an orphan `tool/result` reject as corrupt surface state.
|
|
92
|
+
|
|
93
|
+
### The surface contract
|
|
94
|
+
|
|
95
|
+
`SurfaceEventType` is a closed union — `user/message`, `assistant/message`, and `tool/result` require `surfaceOp`, and other events forbid it, so a `compaction/*` event cannot appear on the surface. A successful backend run instead brackets the operation in the log: it appends `compaction/start` (log-only) to acquire the lock, summarizes the range, appends the log-only `compaction/summary` record, replaces the selected span with one `user/message` carrying the summary — the only surface mutation — and appends `compaction/end` (log-only) to release the lock.
|
|
96
|
+
|
|
97
|
+
The replacement sits inside the lock bracket, so a crash between `compaction/start` and `compaction/end` leaves a detectable orphaned lock rather than a `compaction/end` that falsely claims success. `deriveMessages()` renders the summary as a user-role message followed by the retained nodes; the shadowed events stay in the raw log, so replay is deterministic. The per-event payloads are enumerated in the [compaction subsystem reference](../../../docs/subsystems/compaction.md).
|
|
98
|
+
|
|
99
|
+
### Source map
|
|
100
|
+
|
|
101
|
+
| File | Role |
|
|
102
|
+
|---|---|
|
|
103
|
+
| [`src/index.ts`](src/index.ts) | Plugin entry: abstract `CompactionEngine`, `CompactionTrigger`, `ManualCompactionError`, `ctx.compaction` merge |
|
|
104
|
+
| [`src/types.ts`](src/types.ts) | `CompactionResult` and the declaration-merged `compaction/*` session events |
|
|
105
|
+
| [`src/tool-pairing.ts`](src/tool-pairing.ts) | Per-session cut-balance cache behind the two boundary helpers |
|
|
106
|
+
| [`src/checkpoint.ts`](src/checkpoint.ts) | Kylin-free checkpoint source constructor and predicate (`./checkpoint` leaf) |
|
|
107
|
+
| [`src/brand.ts`](src/brand.ts) | `CompactionId` branded identity |
|
|
108
|
+
|
|
109
|
+
### Locking and serialization
|
|
110
|
+
|
|
111
|
+
One log-recorded lock is shared by all entry points. Tail inspection finds the latest unmatched `compaction/start` and the newest `session/end-seed`; an unmatched start after that boundary is live and reports `busy`, while an older one is stale evidence from a prior process lifecycle. A live bracket cannot cross a `turn/start` or `turn/end`. The markers are lock time points, not an exclusive container: an idle `inject()` may append unrelated context between a manual start and end, so the manual path revalidates its selected span rather than demanding whole-surface equality.
|
|
112
|
+
|
|
113
|
+
### Events
|
|
114
|
+
|
|
115
|
+
The `compaction/*` events extend `SessionEventMap` (merge-extensible) via declaration merging — session events, not Kylin `Events`, and all log-only. The generated [persistence log event catalog](../../../docs/persistence-catalog.md) owns the per-event payloads; `compaction/prune` documents the shadow-price protocol shared with the tool-result pruner.
|
|
116
|
+
|
|
117
|
+
</details>
|
|
118
|
+
|
|
119
|
+
-----
|
|
120
|
+
|
|
121
|
+
<a id="further-exploration"></a>
|
|
122
|
+
## Further Exploration
|
|
123
|
+
|
|
124
|
+
Read these pages when the package-level contract is not enough; they move from the shared vocabulary to the shipped backend and the decision evidence.
|
|
125
|
+
|
|
126
|
+
- [Compaction subsystem reference](../../../docs/subsystems/compaction.md) — the condensation vocabulary, results, and generated service API.
|
|
127
|
+
- [Compaction basic backend](../compaction-basic/README.md) — the shipped backend that condenses automatically and on demand.
|
|
128
|
+
- [Tool-result pruner](../compaction-tool-result-pruner/README.md) — the optional companion that trims oversized tool outputs first.
|
|
129
|
+
- [Human /compact command](../command-compact/README.md) — the on-demand trigger for condensation.
|
|
130
|
+
- [Token meter](../../llm/token-meter/README.md) — the measurement service that decides when to condense.
|
|
131
|
+
- [Compaction capability-seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md) — the split and the session/llm dependency rationale.
|
|
132
|
+
|
|
133
|
+
-----
|
|
134
|
+
|
|
135
|
+
<a id="model-experience"></a>
|
|
136
|
+
## Model Experience
|
|
137
|
+
|
|
138
|
+
### Conversation history, when a backend is invoked
|
|
139
|
+
|
|
140
|
+
#### What the model sees
|
|
141
|
+
|
|
142
|
+
A successful backend replaces an older surface range with one user-role summary checkpoint — a `user/message` carrying `surfaceOp: { op: 'replace', startSeq, endSeq }`. The raw events stay logged but stop appearing in derived model messages; the seam itself performs no rewrite.
|
|
143
|
+
|
|
144
|
+
#### Token effect
|
|
145
|
+
|
|
146
|
+
Zero direct tokens from this Service Definition. A backend trades many retained history tokens for one summary and leaves the recent tail unchanged.
|
|
147
|
+
|
|
148
|
+
#### KV Cache effect
|
|
149
|
+
|
|
150
|
+
A successful backend replacement invalidates reuse from the first shadowed history token; the seam itself does not alter a request.
|
|
151
|
+
|
|
152
|
+
## Known Limitations and Deferred Work
|
|
153
|
+
|
|
154
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
These limits describe what condensation cannot do, regardless of which backend is loaded; they are the current package constraints.
|
|
158
|
+
|
|
159
|
+
- **Human command, not a model tool** — condensation is triggered by the `/compact` command and by automatic pressure; no model-facing compaction tool is registered.
|
|
160
|
+
- **Some single-unit overflow is out of contract** — balanced summary compaction cannot split one indivisible unit. The optional pruning companion can still repair a closed tool pair when text-bearing tool-result bulk is removable; a large non-tool node or a tool unit whose non-prunable remainder is oversized cannot be compacted.
|
|
161
|
+
- **An envelope that alone approaches the window is not surface-compaction work** — compaction shrinks derived history, never the system prompt, tools, or session prefix.
|
|
162
|
+
|
|
163
|
+
<a id="dev-note"></a>
|
|
164
|
+
### Dev Note
|
|
165
|
+
|
|
166
|
+
<details>
|
|
167
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
168
|
+
|
|
169
|
+
This Dev Note is working context for maintainers and is explicitly non-authoritative; shipped behavior lives in the sections above, the package code, and the linked Agent Notes.
|
|
170
|
+
|
|
171
|
+
- **Model-facing tool, undecided** — compaction is human-command only. A model-facing compaction tool remains an open question; it would need its own schema and interaction with the existing command path.
|
|
172
|
+
- **Template and remote backends, undecided** — the `SummaryResult` contract already carries an unmarked `rawOutput` variant for summarizers that do not identify a call through `ctx.llm.stream()`, but no such backend ships.
|
|
173
|
+
- **Range arguments for `/compact`, undecided** — the argument-free form keeps behavior stable across command adapters; explicit ranges stay the programmatic `compactRegion()` path.
|
|
174
|
+
|
|
175
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "面向后端实现者与部署方的共享压缩约定:会话压缩做什么、何时使用,以及如何实现后端。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @qilin-agent/compaction
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`qilin-compaction` 让长时会话把较早历史压缩(compaction)成一条摘要消息、保持近期对话不变,并像摘要一直存在那样继续下去——配合 `qilin-compaction-basic` 之类的后端与可选的 `/compact` 命令即可实现。被遮蔽的内容仍保留在会话日志中,因此回放会话时能确定性地重现同一份压缩后的对话。当你实现压缩后端、构建触发压缩的组件,或需要识别压缩后的消息时,才需要本包——它本身不执行任何压缩。想开箱即用地获得该功能时,请选择随附后端。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
先确定你需要什么。随附后端加 `/compact` 命令即可零代码获得自动与按需压缩;只有在你扩展或重实现该功能时才需要本包。以下各节说明压缩做什么、如何启用,以及如何编写后端。
|
|
29
|
+
|
|
30
|
+
### 何时选择
|
|
31
|
+
|
|
32
|
+
当模型撰写的摘要符合需求时,选择 `qilin-compaction-basic`:会话增长时自动压缩,并可通过 `qilin-command-compact` 按需压缩。当你需要不同的摘要方式——固定模板或远程服务——或构建以编程方式触发压缩的组件时,选择本包。不要单独挂载它:没有后端就什么都不会压缩。
|
|
33
|
+
|
|
34
|
+
### 压缩后会发生什么
|
|
35
|
+
|
|
36
|
+
压缩运行时,所选较早范围的对话会被替换为一条摘要消息;近期历史不受影响,对话从摘要继续。压缩可以由 token 压力自动触发、按需触发,或针对显式范围触发;结果会报告压缩了哪些历史以及估算释放的 token 数。
|
|
37
|
+
|
|
38
|
+
### 启用压缩
|
|
39
|
+
|
|
40
|
+
挂载随附后端以注册压缩服务,并添加 `qilin-command-compact` 获得按需命令:
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
- name: '@qilin-agent/compaction-basic'
|
|
44
|
+
- name: '@qilin-agent/command-compact'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
有了这两行配置,功能即已开启:会话增长时自动压缩,`/compact` 收到请求后立即压缩并报告替换了多少历史项。如果未挂载后端,什么都不会压缩,`/compact` 也会失败;随附后端的完整依赖链见其自身 README。
|
|
48
|
+
|
|
49
|
+
### 实现后端
|
|
50
|
+
|
|
51
|
+
继承提供的基类并实现三个操作:一个针对自动触发决定并执行压缩,一个按需压缩,一个压缩对话的显式范围。把你的类作为插件加载,它就会成为该组合的压缩服务。精确签名、失败规则以及每个后端必须生成的检查点标记,见下方实现章节与[压缩子系统参考](../../../docs/subsystems/compaction.zh.md)。
|
|
52
|
+
|
|
53
|
+
### 识别压缩后的历史
|
|
54
|
+
|
|
55
|
+
后端写入的摘要消息带有稳定标记,因此任何消费方都能在持久化或克隆后识别压缩历史,而无需知道是哪个后端生成的。该标记从包根导出,也从一个无 Cordis 依赖的子路径导出,客户端与 wire 程序均可导入。
|
|
56
|
+
|
|
57
|
+
-----
|
|
58
|
+
|
|
59
|
+
<a id="understand-the-implementation"></a>
|
|
60
|
+
## 理解实现
|
|
61
|
+
|
|
62
|
+
<details>
|
|
63
|
+
<summary>实现细节——点击展开</summary>
|
|
64
|
+
|
|
65
|
+
本节以 API 术语解释约定及其背后的设计决策;功能层面行为见[使用本包](#use-this-package)。
|
|
66
|
+
|
|
67
|
+
### 设计理念
|
|
68
|
+
|
|
69
|
+
该 seam 建立在一个拆分与三项承诺之上:
|
|
70
|
+
|
|
71
|
+
- **抽象约定,具体后端。** 接口规定压缩做什么;提供方拥有策略、保留与摘要,因此各角色可独立演进、独立替换。
|
|
72
|
+
- **会话与 LLM(大语言模型)词汇是约定的一部分。** 操作作用于 `Session`,摘要使用 `ContentBlock`,因此尽管有通用的 Cordis-only 指引,Service Definition 仍依赖 `qilin-session` 与 `qilin-llm`——这是一项有意的偏离,记录在[压缩能力 seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.zh.md) 中。
|
|
73
|
+
- **日志记录的标记对就是锁。** `compaction/start` 在摘要让出控制权之前追加,`compaction/end` 释放;每次失败都恰好进行一次闭合尝试,闭合失败会留下未匹配 start 作为有意的 busy 信号。
|
|
74
|
+
- **表层只变更一次。** 摘要承载在标记对内的一条 `user/message` 替换上;所有 `compaction/*` 事件仅写入日志。
|
|
75
|
+
|
|
76
|
+
### 服务 API
|
|
77
|
+
|
|
78
|
+
该约定是后端实现的三个抽象操作:`compactIfNeeded` 针对自动 `pressure` 或 `context-overflow` 触发,`compactNow` 进行一次显式按需缩减,`compactRegion` 针对调用方选择的表层范围。可复用的请求测量是独立服务 `ctx.tokenMeter`。穷尽式逐操作语义见[压缩子系统参考](../../../docs/subsystems/compaction.zh.md);精确签名见 [`src/index.ts`](src/index.ts)。
|
|
79
|
+
|
|
80
|
+
通过 `ctx.llm.stream()` 摘要的后端必须将 signal 转发到调用的 `GenerateOptions.signal`,因此 abort 或 fiber dispose(资源释放)会停止进行中的摘要。自动和显式范围标记对会从打开的轮次恢复其数字形式归属;手动标记对不要求存在打开的轮次,并标记 `turn: null`。
|
|
81
|
+
|
|
82
|
+
### 手动失败分类
|
|
83
|
+
|
|
84
|
+
同步的 `compaction/summary-error` waterfall 让后端为失败的摘要及明确选区请求持久输入恢复。监听器只有在记录实际进展后才返回 true,否则调用 `next()` 委派后续处理。后端在触发事件前检查取消和选区稳定性,重试前重新派生输入并计价。事件声明说明其载荷;图片策略由 `qilin-compaction-image-offload` 负责。
|
|
85
|
+
|
|
86
|
+
预期手动失败会抛出 `ManualCompactionError`,携带来自小型封闭集合的稳定 `code`;只有 `compaction/start` 标记之后发生的失败才会被记录——以携带错误的 `compaction/end` 形式——而 `busy` 拒绝或 start 之前的取消不会留下记录。每个错误码的语义见[压缩子系统参考](../../../docs/subsystems/compaction.zh.md)。
|
|
87
|
+
|
|
88
|
+
<a id="tool-pairing-boundaries"></a>
|
|
89
|
+
### 工具配对边界
|
|
90
|
+
|
|
91
|
+
该 Service Definition 导出 `toolPairingBalancedBefore(session, seq)` 与 `toolPairingBalancedAfter(session, seq)`,用于对齐和验证压缩边界。安全边界不会被尚未回答的 assistant 工具调用跨越。每个 helper 都会验证给定事件 seq 位于当前表层,并根据按表层顺序缓存的各切分点配对状态返回结果,因此重复检查不读取事件;replace generation 会重建缓存,缺失 seq 或孤立的 `tool/result` 会被视为表层状态损坏并遭拒绝。
|
|
92
|
+
|
|
93
|
+
### 表层约定
|
|
94
|
+
|
|
95
|
+
`SurfaceEventType` 是封闭联合——`user/message`、`assistant/message` 与 `tool/result` 必须携带 `surfaceOp`,其他事件禁止携带该字段,因此 `compaction/*` 事件不能出现在表层上。成功的后端运行改为在日志中包围整个操作:先追加 `compaction/start`(仅日志)获取锁,摘要该范围,追加仅日志的 `compaction/summary` 记录,用一条承载摘要的 `user/message` 替换所选范围——这是唯一的表层变更——最后追加 `compaction/end`(仅日志)释放锁。
|
|
96
|
+
|
|
97
|
+
替换位于锁的起止范围**内**,因此 `compaction/start` 与 `compaction/end` 之间崩溃会留下可检测的遗留锁,而不是虚假声称成功的 `compaction/end`。`deriveMessages()` 将摘要渲染为 user 角色消息,后面跟随已保留节点;已遮蔽事件仍保留在原始日志中,因此回放具有确定性。每个事件的具体 payload 见[压缩子系统参考](../../../docs/subsystems/compaction.zh.md)。
|
|
98
|
+
|
|
99
|
+
### 源码地图
|
|
100
|
+
|
|
101
|
+
| 文件 | 职责 |
|
|
102
|
+
|---|---|
|
|
103
|
+
| [`src/index.ts`](src/index.ts) | 插件入口:抽象 `CompactionEngine`、`CompactionTrigger`、`ManualCompactionError`、`ctx.compaction` 合并 |
|
|
104
|
+
| [`src/types.ts`](src/types.ts) | `CompactionResult` 与声明合并的 `compaction/*` 会话事件 |
|
|
105
|
+
| [`src/tool-pairing.ts`](src/tool-pairing.ts) | 两个边界 helper 背后的每会话切分点平衡缓存 |
|
|
106
|
+
| [`src/checkpoint.ts`](src/checkpoint.ts) | 无 Cordis 依赖的检查点来源构造函数与谓词(`./checkpoint` 叶子) |
|
|
107
|
+
| [`src/brand.ts`](src/brand.ts) | `CompactionId` 品牌化标识 |
|
|
108
|
+
|
|
109
|
+
### 锁与串行化
|
|
110
|
+
|
|
111
|
+
所有入口点共享一个日志记录的锁。尾部检查会分别查找最新的未匹配 `compaction/start` 与最新的 `session/end-seed`;位于该边界之后的未匹配 start 是活动锁并报告 `busy`,更早的则是先前进程生命周期留下的陈旧证据。活动标记对不能跨越 `turn/start` 或 `turn/end`。标记是锁的时间点,而非排他容器:空闲的 `inject()` 可以在手动 start 与 end 之间追加不相关的上下文,因此手动路径重新验证其选中范围,而不要求整个表层相等。
|
|
112
|
+
|
|
113
|
+
### 事件
|
|
114
|
+
|
|
115
|
+
`compaction/*` 事件通过 declaration merging 扩展 `SessionEventMap`(可合并扩展)——它们是会话事件,不是 Cordis `Events`,且都仅写入日志。生成的[持久化日志事件目录](../../../docs/persistence-catalog.zh.md)拥有每个事件的 payload;`compaction/prune` 记录了与工具结果修剪器共享的影子价格协议。
|
|
116
|
+
|
|
117
|
+
</details>
|
|
118
|
+
|
|
119
|
+
-----
|
|
120
|
+
|
|
121
|
+
<a id="further-exploration"></a>
|
|
122
|
+
## 进一步探索
|
|
123
|
+
|
|
124
|
+
当包级约定不够用时阅读以下页面;它们从共享词汇逐步进入随附后端与决策证据。
|
|
125
|
+
|
|
126
|
+
- [压缩子系统参考](../../../docs/subsystems/compaction.zh.md)——压缩词汇、结果与生成的 API。
|
|
127
|
+
- [压缩基础后端](../compaction-basic/README.zh.md)——自动与按需压缩的随附后端。
|
|
128
|
+
- [工具结果修剪器](../compaction-tool-result-pruner/README.zh.md)——先修剪超大工具输出的可选配套工具。
|
|
129
|
+
- [面向用户的 /compact 命令](../command-compact/README.zh.md)——按需触发压缩的入口。
|
|
130
|
+
- [Token meter](../../llm/token-meter/README.zh.md)——决定何时压缩的测量服务。
|
|
131
|
+
- [压缩能力 seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.zh.md)——拆分及 session/llm 依赖的依据。
|
|
132
|
+
|
|
133
|
+
-----
|
|
134
|
+
|
|
135
|
+
<a id="model-experience"></a>
|
|
136
|
+
## 模型体验
|
|
137
|
+
|
|
138
|
+
### 调用后端时的会话历史
|
|
139
|
+
|
|
140
|
+
#### 模型看到的内容
|
|
141
|
+
|
|
142
|
+
成功的后端会用一条 user 角色摘要检查点替换较早表层范围——一条携带 `surfaceOp: { op: 'replace', startSeq, endSeq }` 的 `user/message`。原始事件仍会记录,但不再出现在派生模型消息中;seam 本身不执行改写。
|
|
143
|
+
|
|
144
|
+
#### Token 影响
|
|
145
|
+
|
|
146
|
+
该 Service Definition 不会直接产生 token。后端用一份摘要换取多个原本保留的历史 token,并保持近期尾部不变。
|
|
147
|
+
|
|
148
|
+
#### KV Cache 影响
|
|
149
|
+
|
|
150
|
+
成功的后端替换会使从第一个被遮蔽的历史 token 起的复用失效;seam 本身不会改变请求。
|
|
151
|
+
|
|
152
|
+
## 已知限制与延期工作
|
|
153
|
+
|
|
154
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
这些限制说明无论加载哪个后端,压缩都无法做到的事;它们是当前包约束。
|
|
158
|
+
|
|
159
|
+
- **面向用户的命令,而非模型工具**——压缩由 `/compact` 命令与自动压力触发;不会注册面向模型的压缩工具。
|
|
160
|
+
- **部分单元溢出不在约定内**——平衡摘要压缩无法拆分一个不可分单元。当闭合工具对中可移除的主要部分是承载文本的工具结果时,可选剪枝配套服务仍可修复该工具对;无法压缩大型非工具节点,或不可剪枝剩余部分过大的工具单元。
|
|
161
|
+
- **单独接近窗口大小的 envelope 不属于表层压缩工作**——压缩缩减派生历史,绝不缩减系统提示词、工具或会话前缀。
|
|
162
|
+
|
|
163
|
+
<a id="dev-note"></a>
|
|
164
|
+
### 开发备注
|
|
165
|
+
|
|
166
|
+
<details>
|
|
167
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
168
|
+
|
|
169
|
+
本开发备注是维护者的工作上下文,明确不具权威性;已交付行为以上文、包代码与所链接的 Agent Note 为准。
|
|
170
|
+
|
|
171
|
+
- **面向模型的工具,尚未决定**——压缩目前仅限用户命令。面向模型的压缩工具仍是开放问题;它需要自己的 schema,并与现有命令路径协调。
|
|
172
|
+
- **模板与远程后端,尚未决定**——`SummaryResult` 约定已带有未标记的 `rawOutput` 变体,供不通过 `ctx.llm.stream()` 识别调用的摘要器使用,但此类后端尚未随附。
|
|
173
|
+
- **`/compact` 的范围参数,尚未决定**——无参数形式使各命令适配器的行为保持稳定;显式范围仍由编程接口 `compactRegion()` 处理。
|
|
174
|
+
|
|
175
|
+
</details>
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Service } from "@qilin-agent/kylin";
|
|
2
|
+
//#region lib/types/brand.js
|
|
3
|
+
/**
|
|
4
|
+
* Brand an implementation-minted compaction identity.
|
|
5
|
+
* @param id - opaque transaction identity.
|
|
6
|
+
* @returns the same string, branded; no validation is performed.
|
|
7
|
+
*/
|
|
8
|
+
function CompactionId(id) {
|
|
9
|
+
return id;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region lib/types/tool-pairing.js
|
|
13
|
+
/**
|
|
14
|
+
* Tool-pairing balance over a session surface. Compaction changes surface
|
|
15
|
+
* positions, so safe cuts are derived from tool-call/result content in current
|
|
16
|
+
* surface order rather than step markers.
|
|
17
|
+
* @module @qilin-agent/compaction/tool-pairing
|
|
18
|
+
*/
|
|
19
|
+
const balanceCacheBySession = /* @__PURE__ */ new WeakMap();
|
|
20
|
+
/** Return how one surface event changes the in-progress tool-call count. */
|
|
21
|
+
function eventDelta(event) {
|
|
22
|
+
switch (event.type) {
|
|
23
|
+
case "assistant/message": return event.data.message.content.filter((block) => block.type === "tool-call").length;
|
|
24
|
+
case "tool/result": return -1;
|
|
25
|
+
default: return 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Fold surface sequences not yet in the cache into its balance state. */
|
|
29
|
+
function extendCache(session, cache, seqs) {
|
|
30
|
+
const processed = cache.cutBalanced.length - 1;
|
|
31
|
+
const tail = seqs.slice(processed);
|
|
32
|
+
const pendingCuts = [];
|
|
33
|
+
let inProgressToolCalls = cache.inProgressToolCalls;
|
|
34
|
+
for (const seq of tail) {
|
|
35
|
+
const event = session.eventAt(seq);
|
|
36
|
+
if (event === void 0 || event.seq !== seq) throw new Error(`tool-pairing balance: surface seq ${seq} has no matching session event (corrupt surface)`);
|
|
37
|
+
inProgressToolCalls += eventDelta(event);
|
|
38
|
+
if (inProgressToolCalls < 0) throw new Error(`tool-pairing balance: tool/result at surface seq ${seq} has no matching tool-call (corrupt surface)`);
|
|
39
|
+
pendingCuts.push(inProgressToolCalls === 0);
|
|
40
|
+
}
|
|
41
|
+
tail.forEach((seq, offset) => cache.indexBySeq.set(seq, processed + offset));
|
|
42
|
+
cache.cutBalanced = cache.cutBalanced.concat(pendingCuts);
|
|
43
|
+
cache.inProgressToolCalls = inProgressToolCalls;
|
|
44
|
+
return cache;
|
|
45
|
+
}
|
|
46
|
+
/** Return balance state synchronized with the current session surface. */
|
|
47
|
+
function balanceCache(session) {
|
|
48
|
+
const surface = session.surface;
|
|
49
|
+
const seqs = surface.nodes;
|
|
50
|
+
const generation = surface.replaceGeneration;
|
|
51
|
+
const cached = balanceCacheBySession.get(session);
|
|
52
|
+
if (cached === void 0 || cached.generation !== generation || cached.cutBalanced.length - 1 > seqs.length) {
|
|
53
|
+
const rebuilt = extendCache(session, {
|
|
54
|
+
generation,
|
|
55
|
+
cutBalanced: [true],
|
|
56
|
+
indexBySeq: /* @__PURE__ */ new Map(),
|
|
57
|
+
inProgressToolCalls: 0
|
|
58
|
+
}, seqs);
|
|
59
|
+
balanceCacheBySession.set(session, rebuilt);
|
|
60
|
+
return rebuilt;
|
|
61
|
+
}
|
|
62
|
+
if (cached.cutBalanced.length - 1 < seqs.length) return extendCache(session, cached, seqs);
|
|
63
|
+
return cached;
|
|
64
|
+
}
|
|
65
|
+
/** Balance of the cut at a sequence's position plus offset, rejecting seqs outside current membership. */
|
|
66
|
+
function cutBalance(cache, seq, offset) {
|
|
67
|
+
const index = cache.indexBySeq.get(seq);
|
|
68
|
+
const balanced = index === void 0 ? void 0 : cache.cutBalanced[index + offset];
|
|
69
|
+
if (balanced === void 0) throw new Error(`tool-pairing balance: surface seq ${seq} not found`);
|
|
70
|
+
return balanced;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether the cut immediately before a current surface sequence is tool-pairing balanced.
|
|
74
|
+
* @param session - session whose surface is checked.
|
|
75
|
+
* @param seq - event sequence whose leading cut is checked.
|
|
76
|
+
* @returns true when no unanswered tool call crosses the cut.
|
|
77
|
+
* @throws when the seq is absent from the current surface, a surface sequence has no
|
|
78
|
+
* matching log event, or a tool result has no preceding open call.
|
|
79
|
+
*/
|
|
80
|
+
function toolPairingBalancedBefore(session, seq) {
|
|
81
|
+
return cutBalance(balanceCache(session), seq, 0);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Whether the cut immediately after a current surface sequence is tool-pairing balanced.
|
|
85
|
+
* @param session - session whose surface is checked.
|
|
86
|
+
* @param seq - event sequence whose trailing cut is checked.
|
|
87
|
+
* @returns true when no unanswered tool call crosses the cut.
|
|
88
|
+
* @throws when the seq is absent from the current surface, a surface sequence has no
|
|
89
|
+
* matching log event, or a tool result has no preceding open call.
|
|
90
|
+
*/
|
|
91
|
+
function toolPairingBalancedAfter(session, seq) {
|
|
92
|
+
return cutBalance(balanceCache(session), seq, 1);
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region lib/types/checkpoint.js
|
|
96
|
+
/**
|
|
97
|
+
* Compaction checkpoint source: the correlated constructor and type
|
|
98
|
+
* every backend uses for its replacement user message, plus the predicate that
|
|
99
|
+
* recognizes persisted checkpoints.
|
|
100
|
+
*
|
|
101
|
+
* The seam itself lives in `@qilin-agent/compaction`, which re-exports these
|
|
102
|
+
* contracts; this module is a pure type/value/predicate outlet (no cordis
|
|
103
|
+
* imports, no module augmentation) so client and wire programs can name the
|
|
104
|
+
* checkpoint source without loading the host plugin's Context merges — the
|
|
105
|
+
* `qilin-commands/brand` shape.
|
|
106
|
+
*
|
|
107
|
+
* @module @qilin-agent/compaction/checkpoint
|
|
108
|
+
*/
|
|
109
|
+
const COMPACT_CHECKPOINT_MARKER = Object.freeze({ kind: "compact-checkpoint" });
|
|
110
|
+
/**
|
|
111
|
+
* Create a checkpoint source correlated with one compaction transaction.
|
|
112
|
+
* @param compactionId - owning compaction identity.
|
|
113
|
+
* @param sourceCommandId - initiating manual command, when present.
|
|
114
|
+
* @returns immutable checkpoint source.
|
|
115
|
+
*/
|
|
116
|
+
function compactCheckpointSource(compactionId, sourceCommandId) {
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
...COMPACT_CHECKPOINT_MARKER,
|
|
119
|
+
compactionId,
|
|
120
|
+
...sourceCommandId === void 0 ? {} : { sourceCommandId }
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Test whether a persisted message source identifies a compaction checkpoint.
|
|
125
|
+
* @param source - source restored from a surface user message.
|
|
126
|
+
* @returns whether the source carries the backend-independent checkpoint marker.
|
|
127
|
+
*/
|
|
128
|
+
function isCompactCheckpointSource(source) {
|
|
129
|
+
return source.kind === "compact-checkpoint";
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region lib/types/index.js
|
|
133
|
+
/**
|
|
134
|
+
* Compaction Service Definition (`ctx.compaction`): providers decide when to
|
|
135
|
+
* compact and replace a history range with one summary node by subclassing
|
|
136
|
+
* {@link CompactionEngine}. This interface necessarily depends on session and LLM
|
|
137
|
+
* vocabulary; the rationale is in the
|
|
138
|
+
* [compaction Agent Note](../../../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md).
|
|
139
|
+
* @module @qilin-agent/compaction
|
|
140
|
+
*/
|
|
141
|
+
/**
|
|
142
|
+
* Expected manual-compaction failure suitable for a direct human-command result.
|
|
143
|
+
* Shared durable-lock entry assertions may also throw the `busy` subtype from
|
|
144
|
+
* automatic compaction paths.
|
|
145
|
+
*/
|
|
146
|
+
var ManualCompactionError = class extends Error {
|
|
147
|
+
code;
|
|
148
|
+
name = "ManualCompactionError";
|
|
149
|
+
/**
|
|
150
|
+
* Create one classified compaction failure.
|
|
151
|
+
* @param code - stable failure class; `busy` may originate from any compaction entry path.
|
|
152
|
+
* @param message - backend diagnostic retained as the Error message.
|
|
153
|
+
* @param options - optional original failure.
|
|
154
|
+
*/
|
|
155
|
+
constructor(code, message, options) {
|
|
156
|
+
super(message, options);
|
|
157
|
+
this.code = code;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Abstract compaction service. Implementations own trigger policy, retention,
|
|
162
|
+
* and summarization, and may consume a separate measurement service. A
|
|
163
|
+
* successful run replaces the selected surface span with one summary node and
|
|
164
|
+
* prevents concurrent compaction of the same session. The replacement user
|
|
165
|
+
* message uses {@link compactCheckpointSource} with the transaction identity
|
|
166
|
+
* so consumers recognize and correlate it independently of the backend. Load
|
|
167
|
+
* one implementation per context as `ctx.compaction`.
|
|
168
|
+
*/
|
|
169
|
+
var CompactionEngine = class extends Service {
|
|
170
|
+
constructor(ctx) {
|
|
171
|
+
super(ctx, "compaction");
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
//#endregion
|
|
175
|
+
export { CompactionEngine, CompactionEngine as default, CompactionId, ManualCompactionError, compactCheckpointSource, isCompactCheckpointSource, toolPairingBalancedAfter, toolPairingBalancedBefore };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Branded } from '@qilin-agent/brand';
|
|
2
|
+
/** Stable identity shared by one compact start/summary/checkpoint/end transaction. */
|
|
3
|
+
export type CompactionId = Branded<'CompactionId'>;
|
|
4
|
+
/**
|
|
5
|
+
* Brand an implementation-minted compaction identity.
|
|
6
|
+
* @param id - opaque transaction identity.
|
|
7
|
+
* @returns the same string, branded; no validation is performed.
|
|
8
|
+
*/
|
|
9
|
+
export declare function CompactionId(id: string): CompactionId;
|
|
10
|
+
//# sourceMappingURL=brand.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand an implementation-minted compaction identity.
|
|
3
|
+
* @param id - opaque transaction identity.
|
|
4
|
+
* @returns the same string, branded; no validation is performed.
|
|
5
|
+
*/
|
|
6
|
+
export function CompactionId(id) {
|
|
7
|
+
return id;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=brand.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction checkpoint source: the correlated constructor and type
|
|
3
|
+
* every backend uses for its replacement user message, plus the predicate that
|
|
4
|
+
* recognizes persisted checkpoints.
|
|
5
|
+
*
|
|
6
|
+
* The seam itself lives in `@qilin-agent/compaction`, which re-exports these
|
|
7
|
+
* contracts; this module is a pure type/value/predicate outlet (no cordis
|
|
8
|
+
* imports, no module augmentation) so client and wire programs can name the
|
|
9
|
+
* checkpoint source without loading the host plugin's Context merges — the
|
|
10
|
+
* `qilin-commands/brand` shape.
|
|
11
|
+
*
|
|
12
|
+
* @module @qilin-agent/compaction/checkpoint
|
|
13
|
+
*/
|
|
14
|
+
import type { MessageSource } from '@qilin-agent/llm/message';
|
|
15
|
+
import type { CommandId } from '@qilin-agent/commands/brand';
|
|
16
|
+
import type { CompactionId } from './brand.ts';
|
|
17
|
+
declare const COMPACT_CHECKPOINT_MARKER: Readonly<{
|
|
18
|
+
readonly kind: "compact-checkpoint";
|
|
19
|
+
}>;
|
|
20
|
+
/** Message source carried by a concrete compaction checkpoint. */
|
|
21
|
+
export type CompactionCheckpointSource = typeof COMPACT_CHECKPOINT_MARKER & {
|
|
22
|
+
readonly compactionId: CompactionId;
|
|
23
|
+
readonly sourceCommandId?: CommandId;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Create a checkpoint source correlated with one compaction transaction.
|
|
27
|
+
* @param compactionId - owning compaction identity.
|
|
28
|
+
* @param sourceCommandId - initiating manual command, when present.
|
|
29
|
+
* @returns immutable checkpoint source.
|
|
30
|
+
*/
|
|
31
|
+
export declare function compactCheckpointSource(compactionId: CompactionId, sourceCommandId?: CommandId): CompactionCheckpointSource;
|
|
32
|
+
/**
|
|
33
|
+
* Test whether a persisted message source identifies a compaction checkpoint.
|
|
34
|
+
* @param source - source restored from a surface user message.
|
|
35
|
+
* @returns whether the source carries the backend-independent checkpoint marker.
|
|
36
|
+
*/
|
|
37
|
+
export declare function isCompactCheckpointSource(source: MessageSource): source is CompactionCheckpointSource;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=checkpoint.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction checkpoint source: the correlated constructor and type
|
|
3
|
+
* every backend uses for its replacement user message, plus the predicate that
|
|
4
|
+
* recognizes persisted checkpoints.
|
|
5
|
+
*
|
|
6
|
+
* The seam itself lives in `@qilin-agent/compaction`, which re-exports these
|
|
7
|
+
* contracts; this module is a pure type/value/predicate outlet (no cordis
|
|
8
|
+
* imports, no module augmentation) so client and wire programs can name the
|
|
9
|
+
* checkpoint source without loading the host plugin's Context merges — the
|
|
10
|
+
* `qilin-commands/brand` shape.
|
|
11
|
+
*
|
|
12
|
+
* @module @qilin-agent/compaction/checkpoint
|
|
13
|
+
*/
|
|
14
|
+
const COMPACT_CHECKPOINT_MARKER = Object.freeze({ kind: 'compact-checkpoint' });
|
|
15
|
+
/**
|
|
16
|
+
* Create a checkpoint source correlated with one compaction transaction.
|
|
17
|
+
* @param compactionId - owning compaction identity.
|
|
18
|
+
* @param sourceCommandId - initiating manual command, when present.
|
|
19
|
+
* @returns immutable checkpoint source.
|
|
20
|
+
*/
|
|
21
|
+
export function compactCheckpointSource(compactionId, sourceCommandId) {
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
...COMPACT_CHECKPOINT_MARKER,
|
|
24
|
+
compactionId,
|
|
25
|
+
...sourceCommandId === undefined ? {} : { sourceCommandId },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Test whether a persisted message source identifies a compaction checkpoint.
|
|
30
|
+
* @param source - source restored from a surface user message.
|
|
31
|
+
* @returns whether the source carries the backend-independent checkpoint marker.
|
|
32
|
+
*/
|
|
33
|
+
export function isCompactCheckpointSource(source) {
|
|
34
|
+
return source.kind === 'compact-checkpoint';
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=checkpoint.js.map
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction Service Definition (`ctx.compaction`): providers decide when to
|
|
3
|
+
* compact and replace a history range with one summary node by subclassing
|
|
4
|
+
* {@link CompactionEngine}. This interface necessarily depends on session and LLM
|
|
5
|
+
* vocabulary; the rationale is in the
|
|
6
|
+
* [compaction Agent Note](../../../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md).
|
|
7
|
+
* @module @qilin-agent/compaction
|
|
8
|
+
*/
|
|
9
|
+
import { Context, Service } from '@qilin-agent/kylin';
|
|
10
|
+
import type { Session, SessionSeq } from '@qilin-agent/session';
|
|
11
|
+
import type { CommandId } from '@qilin-agent/commands/brand';
|
|
12
|
+
import type { CompactionResult } from './types.ts';
|
|
13
|
+
import type { CompactionCheckpointSource } from './checkpoint.ts';
|
|
14
|
+
export type { CompactionResult } from './types.ts';
|
|
15
|
+
export { CompactionId } from './brand.ts';
|
|
16
|
+
export { toolPairingBalancedAfter, toolPairingBalancedBefore } from './tool-pairing.ts';
|
|
17
|
+
export { compactCheckpointSource, isCompactCheckpointSource } from './checkpoint.ts';
|
|
18
|
+
export type { CompactionCheckpointSource } from './checkpoint.ts';
|
|
19
|
+
declare module '@qilin-agent/llm' {
|
|
20
|
+
interface MessageSourceMap {
|
|
21
|
+
'compact-checkpoint': CompactionCheckpointSource;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Why automatic policy is asking a backend to consider compaction. */
|
|
25
|
+
export type CompactionTrigger = 'pressure' | 'context-overflow';
|
|
26
|
+
/** Expected failure classes for an explicit idle-session compaction request. */
|
|
27
|
+
export type ManualCompactionErrorCode = 'busy' | 'cancelled' | 'changed' | 'summary' | 'commit' | 'persistence';
|
|
28
|
+
/**
|
|
29
|
+
* Expected manual-compaction failure suitable for a direct human-command result.
|
|
30
|
+
* Shared durable-lock entry assertions may also throw the `busy` subtype from
|
|
31
|
+
* automatic compaction paths.
|
|
32
|
+
*/
|
|
33
|
+
export declare class ManualCompactionError extends Error {
|
|
34
|
+
readonly code: ManualCompactionErrorCode;
|
|
35
|
+
readonly name = "ManualCompactionError";
|
|
36
|
+
/**
|
|
37
|
+
* Create one classified compaction failure.
|
|
38
|
+
* @param code - stable failure class; `busy` may originate from any compaction entry path.
|
|
39
|
+
* @param message - backend diagnostic retained as the Error message.
|
|
40
|
+
* @param options - optional original failure.
|
|
41
|
+
*/
|
|
42
|
+
constructor(code: ManualCompactionErrorCode, message: string, options?: ErrorOptions);
|
|
43
|
+
}
|
|
44
|
+
/** Minimal agent context compaction needs without depending on the agent package. */
|
|
45
|
+
export interface CompactionAgentContext {
|
|
46
|
+
session: Session;
|
|
47
|
+
options: {
|
|
48
|
+
provider?: string;
|
|
49
|
+
model?: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Agent capability required to serialize an explicit idle-session compaction
|
|
54
|
+
* against driver turns. The durable `compaction/start` marker separately excludes
|
|
55
|
+
* other compaction transactions.
|
|
56
|
+
*/
|
|
57
|
+
export interface ManualCompactAgentContext extends CompactionAgentContext {
|
|
58
|
+
/**
|
|
59
|
+
* Run a non-turn maintenance operation only while the agent is idle, withholding later
|
|
60
|
+
* waking input until it settles.
|
|
61
|
+
* @param task - operation whose fulfillment or rejection is preserved, with an agent-owned cancellation signal.
|
|
62
|
+
* @throws synchronously when the agent is already active.
|
|
63
|
+
* @returns the task promise.
|
|
64
|
+
*/
|
|
65
|
+
runMaintenance<T>(task: (signal: AbortSignal) => Promise<T>): Promise<T>;
|
|
66
|
+
}
|
|
67
|
+
declare module '@qilin-agent/kylin' {
|
|
68
|
+
interface Context {
|
|
69
|
+
compaction: CompactionEngine;
|
|
70
|
+
}
|
|
71
|
+
interface Events {
|
|
72
|
+
/**
|
|
73
|
+
* Recover a failed summary request by synchronously recording a durable
|
|
74
|
+
* change to its selected input. Return true only after making progress;
|
|
75
|
+
* the provider re-derives and re-prices the selection before retrying.
|
|
76
|
+
* Call next() when the failure cannot be recovered. Decisions survive a
|
|
77
|
+
* later summary failure or cancellation.
|
|
78
|
+
* @param payload.session - session containing the selected input.
|
|
79
|
+
* @param payload.sourceEventSeqs - selected message events in request order.
|
|
80
|
+
* @param payload.error - failure thrown by the summarizer.
|
|
81
|
+
* @param payload.signal - optional compaction cancellation signal.
|
|
82
|
+
* @param next - delegate to the next recovery listener.
|
|
83
|
+
* @mode waterfall
|
|
84
|
+
*/
|
|
85
|
+
'compaction/summary-error'(payload: {
|
|
86
|
+
session: Session;
|
|
87
|
+
sourceEventSeqs: readonly SessionSeq[];
|
|
88
|
+
error: unknown;
|
|
89
|
+
signal?: AbortSignal;
|
|
90
|
+
}, next: () => boolean): boolean;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Abstract compaction service. Implementations own trigger policy, retention,
|
|
95
|
+
* and summarization, and may consume a separate measurement service. A
|
|
96
|
+
* successful run replaces the selected surface span with one summary node and
|
|
97
|
+
* prevents concurrent compaction of the same session. The replacement user
|
|
98
|
+
* message uses {@link compactCheckpointSource} with the transaction identity
|
|
99
|
+
* so consumers recognize and correlate it independently of the backend. Load
|
|
100
|
+
* one implementation per context as `ctx.compaction`.
|
|
101
|
+
*/
|
|
102
|
+
export declare abstract class CompactionEngine extends Service {
|
|
103
|
+
constructor(ctx: Context);
|
|
104
|
+
/**
|
|
105
|
+
* Consider automatic compaction for one explicit trigger. Pressure policy
|
|
106
|
+
* uses the latest durable routed request, while context-overflow policy may
|
|
107
|
+
* force a useful balanced reduction even below the normal threshold. Return
|
|
108
|
+
* `null` when no safe range can be compacted. A single oversized retained
|
|
109
|
+
* unit or request envelope cannot be repaired through surface compaction.
|
|
110
|
+
*
|
|
111
|
+
* @param agent - agent context owning the session surface and routing options.
|
|
112
|
+
* @param trigger - normal pressure or provider-confirmed context overflow.
|
|
113
|
+
* @param signal - cancellation signal; model-backed implementations must forward it.
|
|
114
|
+
* @returns the compaction result, or `null` if no compaction was needed.
|
|
115
|
+
*/
|
|
116
|
+
abstract compactIfNeeded(agent: CompactionAgentContext, trigger: CompactionTrigger, signal: AbortSignal): Promise<CompactionResult | null>;
|
|
117
|
+
/**
|
|
118
|
+
* Explicitly compact useful history even below automatic pressure thresholds.
|
|
119
|
+
* Implementations synchronously start an idle task before any asynchronous
|
|
120
|
+
* work, select a useful range without writing on a no-op, then
|
|
121
|
+
* append a standalone `compaction/start` before summarization. That durable
|
|
122
|
+
* marker is the compaction lock until one `compaction/end` attempt. Later waking
|
|
123
|
+
* prompts remain accepted in FIFO order and start only after the optional
|
|
124
|
+
* durability checkpoint and idle-task settlement. Context injected while the
|
|
125
|
+
* summary runs may sit between the marker pair; only the selected span must
|
|
126
|
+
* remain stable.
|
|
127
|
+
*
|
|
128
|
+
* @param agent - idle agent whose durable history should be compacted.
|
|
129
|
+
* @param signal - cancellation scoped to this compaction request.
|
|
130
|
+
* @param sourceCommandId - initiating command identity for a manual compaction.
|
|
131
|
+
* @returns the compaction result, or `null` when no safe useful range exists.
|
|
132
|
+
* @throws {@link ManualCompactionError} for expected busy, agent-cancellation,
|
|
133
|
+
* changed-span, summarization/shrink, commit-stage, or persistence failures;
|
|
134
|
+
* an aborted request preserves its exact abort reason. Failed attempts remain
|
|
135
|
+
* visible in the log.
|
|
136
|
+
*/
|
|
137
|
+
abstract compactNow(agent: ManualCompactAgentContext, signal: AbortSignal, sourceCommandId?: CommandId): Promise<CompactionResult | null>;
|
|
138
|
+
/**
|
|
139
|
+
* Forcibly compact a range of surface nodes into a single summary node.
|
|
140
|
+
* `start` and `end` name an inclusive span by surface position, not numeric seq
|
|
141
|
+
* order; replacements can make visible seqs non-monotonic. Both edges must be
|
|
142
|
+
* balanced so assistant tool calls remain paired with their results. A model-
|
|
143
|
+
* backed implementation forwards cancellation and rejects active, missing,
|
|
144
|
+
* reversed, or unbalanced ranges. The target session is `agent.session`.
|
|
145
|
+
* Its replacement user message must use {@link compactCheckpointSource} with
|
|
146
|
+
* the transaction's `CompactionId`.
|
|
147
|
+
* Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter}
|
|
148
|
+
* for the edge checks.
|
|
149
|
+
*
|
|
150
|
+
* @param start - first surface seq, inclusive.
|
|
151
|
+
* @param end - last surface seq, inclusive.
|
|
152
|
+
* @param agent - context whose session is mutated and whose routing options guide summarization.
|
|
153
|
+
* @param signal - optional cancellation; model-backed implementations must forward it.
|
|
154
|
+
* @throws when compaction is active or the range is missing, reversed, or unbalanced.
|
|
155
|
+
* @returns the appended event seqs, summary, replaced range, and token accounting.
|
|
156
|
+
*/
|
|
157
|
+
abstract compactRegion(start: SessionSeq, end: SessionSeq, agent: CompactionAgentContext, signal?: AbortSignal): Promise<CompactionResult>;
|
|
158
|
+
}
|
|
159
|
+
export default CompactionEngine;
|
|
160
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction Service Definition (`ctx.compaction`): providers decide when to
|
|
3
|
+
* compact and replace a history range with one summary node by subclassing
|
|
4
|
+
* {@link CompactionEngine}. This interface necessarily depends on session and LLM
|
|
5
|
+
* vocabulary; the rationale is in the
|
|
6
|
+
* [compaction Agent Note](../../../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md).
|
|
7
|
+
* @module @qilin-agent/compaction
|
|
8
|
+
*/
|
|
9
|
+
import { Service } from '@qilin-agent/kylin';
|
|
10
|
+
export { CompactionId } from "./brand.js";
|
|
11
|
+
export { toolPairingBalancedAfter, toolPairingBalancedBefore } from "./tool-pairing.js";
|
|
12
|
+
// The checkpoint source constructor and predicate are declared on the cordis-free
|
|
13
|
+
// `./checkpoint` leaf so client and wire programs can name them without this
|
|
14
|
+
// root's Context merge; the root stays the host-side entry point for both.
|
|
15
|
+
export { compactCheckpointSource, isCompactCheckpointSource } from "./checkpoint.js";
|
|
16
|
+
/**
|
|
17
|
+
* Expected manual-compaction failure suitable for a direct human-command result.
|
|
18
|
+
* Shared durable-lock entry assertions may also throw the `busy` subtype from
|
|
19
|
+
* automatic compaction paths.
|
|
20
|
+
*/
|
|
21
|
+
export class ManualCompactionError extends Error {
|
|
22
|
+
code;
|
|
23
|
+
name = 'ManualCompactionError';
|
|
24
|
+
/**
|
|
25
|
+
* Create one classified compaction failure.
|
|
26
|
+
* @param code - stable failure class; `busy` may originate from any compaction entry path.
|
|
27
|
+
* @param message - backend diagnostic retained as the Error message.
|
|
28
|
+
* @param options - optional original failure.
|
|
29
|
+
*/
|
|
30
|
+
constructor(code, message, options) {
|
|
31
|
+
super(message, options);
|
|
32
|
+
this.code = code;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Abstract compaction service. Implementations own trigger policy, retention,
|
|
37
|
+
* and summarization, and may consume a separate measurement service. A
|
|
38
|
+
* successful run replaces the selected surface span with one summary node and
|
|
39
|
+
* prevents concurrent compaction of the same session. The replacement user
|
|
40
|
+
* message uses {@link compactCheckpointSource} with the transaction identity
|
|
41
|
+
* so consumers recognize and correlate it independently of the backend. Load
|
|
42
|
+
* one implementation per context as `ctx.compaction`.
|
|
43
|
+
*/
|
|
44
|
+
export class CompactionEngine extends Service {
|
|
45
|
+
constructor(ctx) {
|
|
46
|
+
super(ctx, 'compaction');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export default CompactionEngine;
|
|
50
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-pairing balance over a session surface. Compaction changes surface
|
|
3
|
+
* positions, so safe cuts are derived from tool-call/result content in current
|
|
4
|
+
* surface order rather than step markers.
|
|
5
|
+
* @module @qilin-agent/compaction/tool-pairing
|
|
6
|
+
*/
|
|
7
|
+
import type { Session, SessionSeq } from '@qilin-agent/session';
|
|
8
|
+
/**
|
|
9
|
+
* Whether the cut immediately before a current surface sequence is tool-pairing balanced.
|
|
10
|
+
* @param session - session whose surface is checked.
|
|
11
|
+
* @param seq - event sequence whose leading cut is checked.
|
|
12
|
+
* @returns true when no unanswered tool call crosses the cut.
|
|
13
|
+
* @throws when the seq is absent from the current surface, a surface sequence has no
|
|
14
|
+
* matching log event, or a tool result has no preceding open call.
|
|
15
|
+
*/
|
|
16
|
+
export declare function toolPairingBalancedBefore(session: Session, seq: SessionSeq): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Whether the cut immediately after a current surface sequence is tool-pairing balanced.
|
|
19
|
+
* @param session - session whose surface is checked.
|
|
20
|
+
* @param seq - event sequence whose trailing cut is checked.
|
|
21
|
+
* @returns true when no unanswered tool call crosses the cut.
|
|
22
|
+
* @throws when the seq is absent from the current surface, a surface sequence has no
|
|
23
|
+
* matching log event, or a tool result has no preceding open call.
|
|
24
|
+
*/
|
|
25
|
+
export declare function toolPairingBalancedAfter(session: Session, seq: SessionSeq): boolean;
|
|
26
|
+
//# sourceMappingURL=tool-pairing.d.ts.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-pairing balance over a session surface. Compaction changes surface
|
|
3
|
+
* positions, so safe cuts are derived from tool-call/result content in current
|
|
4
|
+
* surface order rather than step markers.
|
|
5
|
+
* @module @qilin-agent/compaction/tool-pairing
|
|
6
|
+
*/
|
|
7
|
+
const balanceCacheBySession = new WeakMap();
|
|
8
|
+
/** Return how one surface event changes the in-progress tool-call count. */
|
|
9
|
+
function eventDelta(event) {
|
|
10
|
+
switch (event.type) {
|
|
11
|
+
case 'assistant/message':
|
|
12
|
+
return event.data.message.content.filter(block => block.type === 'tool-call').length;
|
|
13
|
+
case 'tool/result':
|
|
14
|
+
return -1;
|
|
15
|
+
default:
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Fold surface sequences not yet in the cache into its balance state. */
|
|
20
|
+
function extendCache(session, cache, seqs) {
|
|
21
|
+
const processed = cache.cutBalanced.length - 1;
|
|
22
|
+
const tail = seqs.slice(processed);
|
|
23
|
+
// Validate the unseen tail before mutating the live cache, so a corrupt
|
|
24
|
+
// append cannot leave a partially advanced state behind.
|
|
25
|
+
const pendingCuts = [];
|
|
26
|
+
let inProgressToolCalls = cache.inProgressToolCalls;
|
|
27
|
+
for (const seq of tail) {
|
|
28
|
+
// oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.
|
|
29
|
+
const event = session.eventAt(seq);
|
|
30
|
+
if (event === undefined || event.seq !== seq) {
|
|
31
|
+
throw new Error(`tool-pairing balance: surface seq ${seq} has no matching session event (corrupt surface)`);
|
|
32
|
+
}
|
|
33
|
+
inProgressToolCalls += eventDelta(event);
|
|
34
|
+
if (inProgressToolCalls < 0) {
|
|
35
|
+
throw new Error(`tool-pairing balance: tool/result at surface seq ${seq} has no matching tool-call (corrupt surface)`);
|
|
36
|
+
}
|
|
37
|
+
pendingCuts.push(inProgressToolCalls === 0);
|
|
38
|
+
}
|
|
39
|
+
tail.forEach((seq, offset) => cache.indexBySeq.set(seq, processed + offset));
|
|
40
|
+
cache.cutBalanced = cache.cutBalanced.concat(pendingCuts);
|
|
41
|
+
cache.inProgressToolCalls = inProgressToolCalls;
|
|
42
|
+
return cache;
|
|
43
|
+
}
|
|
44
|
+
/** Return balance state synchronized with the current session surface. */
|
|
45
|
+
function balanceCache(session) {
|
|
46
|
+
const surface = session.surface;
|
|
47
|
+
const seqs = surface.nodes;
|
|
48
|
+
const generation = surface.replaceGeneration;
|
|
49
|
+
const cached = balanceCacheBySession.get(session);
|
|
50
|
+
if (cached === undefined || cached.generation !== generation || cached.cutBalanced.length - 1 > seqs.length) {
|
|
51
|
+
// A rebuild is the same fold started from the empty-surface state, whose
|
|
52
|
+
// single leading cut is trivially balanced.
|
|
53
|
+
const rebuilt = extendCache(session, {
|
|
54
|
+
generation,
|
|
55
|
+
cutBalanced: [true],
|
|
56
|
+
indexBySeq: new Map(),
|
|
57
|
+
inProgressToolCalls: 0,
|
|
58
|
+
}, seqs);
|
|
59
|
+
balanceCacheBySession.set(session, rebuilt);
|
|
60
|
+
return rebuilt;
|
|
61
|
+
}
|
|
62
|
+
if (cached.cutBalanced.length - 1 < seqs.length)
|
|
63
|
+
return extendCache(session, cached, seqs);
|
|
64
|
+
return cached;
|
|
65
|
+
}
|
|
66
|
+
/** Balance of the cut at a sequence's position plus offset, rejecting seqs outside current membership. */
|
|
67
|
+
function cutBalance(cache, seq, offset) {
|
|
68
|
+
const index = cache.indexBySeq.get(seq);
|
|
69
|
+
const balanced = index === undefined ? undefined : cache.cutBalanced[index + offset];
|
|
70
|
+
if (balanced === undefined) {
|
|
71
|
+
throw new Error(`tool-pairing balance: surface seq ${seq} not found`);
|
|
72
|
+
}
|
|
73
|
+
return balanced;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether the cut immediately before a current surface sequence is tool-pairing balanced.
|
|
77
|
+
* @param session - session whose surface is checked.
|
|
78
|
+
* @param seq - event sequence whose leading cut is checked.
|
|
79
|
+
* @returns true when no unanswered tool call crosses the cut.
|
|
80
|
+
* @throws when the seq is absent from the current surface, a surface sequence has no
|
|
81
|
+
* matching log event, or a tool result has no preceding open call.
|
|
82
|
+
*/
|
|
83
|
+
export function toolPairingBalancedBefore(session, seq) {
|
|
84
|
+
return cutBalance(balanceCache(session), seq, 0);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether the cut immediately after a current surface sequence is tool-pairing balanced.
|
|
88
|
+
* @param session - session whose surface is checked.
|
|
89
|
+
* @param seq - event sequence whose trailing cut is checked.
|
|
90
|
+
* @returns true when no unanswered tool call crosses the cut.
|
|
91
|
+
* @throws when the seq is absent from the current surface, a surface sequence has no
|
|
92
|
+
* matching log event, or a tool result has no preceding open call.
|
|
93
|
+
*/
|
|
94
|
+
export function toolPairingBalancedAfter(session, seq) {
|
|
95
|
+
return cutBalance(balanceCache(session), seq, 1);
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=tool-pairing.js.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction vocabulary: the result type and the `compaction/*` session events.
|
|
3
|
+
* Those declaration-merged events record the lock and summary inputs without entering the surface, so they are not
|
|
4
|
+
* surface events; a separate replacement `user/message` carries the summary.
|
|
5
|
+
* Backend packages own configuration and retention policy; see
|
|
6
|
+
* `.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md`.
|
|
7
|
+
* @module @qilin-agent/compaction/types
|
|
8
|
+
*/
|
|
9
|
+
import type { ContentBlock, TokenUsage } from '@qilin-agent/llm';
|
|
10
|
+
import type { CommandId } from '@qilin-agent/commands/brand';
|
|
11
|
+
import type { SessionSeq } from '@qilin-agent/session/types';
|
|
12
|
+
import type { CompactionId } from './brand.ts';
|
|
13
|
+
export type { CompactionId };
|
|
14
|
+
declare module '@qilin-agent/session/types' {
|
|
15
|
+
interface SessionEventMap {
|
|
16
|
+
/**
|
|
17
|
+
* Marks the start of a compaction — log-only, holds the lock until
|
|
18
|
+
* `compaction/end`. A numbered owner is strictly enclosed by that open turn;
|
|
19
|
+
* `null` identifies a standalone manual transaction between turns.
|
|
20
|
+
*/
|
|
21
|
+
'compaction/start': {
|
|
22
|
+
compactionId: CompactionId;
|
|
23
|
+
sourceCommandId?: CommandId;
|
|
24
|
+
turn: number | null;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Completed summary, its inputs, and its model call facts — log-only, no surfaceOp.
|
|
28
|
+
* The summary content is in `data.summary`; the actual surface replacement
|
|
29
|
+
* is performed by the immediately following `user/message` event that
|
|
30
|
+
* shadows the compacted range. That adjacency is contractual — the
|
|
31
|
+
* shadowed pricing fields are the replacement's shadow price, so a
|
|
32
|
+
* consumer may pair a replacement with the metering event directly
|
|
33
|
+
* before it (`compaction/prune` documents the shared protocol).
|
|
34
|
+
*/
|
|
35
|
+
'compaction/summary': {
|
|
36
|
+
compactionId: CompactionId;
|
|
37
|
+
sourceCommandId?: CommandId;
|
|
38
|
+
summary: ContentBlock[];
|
|
39
|
+
shadowedRange: {
|
|
40
|
+
start: SessionSeq;
|
|
41
|
+
end: SessionSeq;
|
|
42
|
+
};
|
|
43
|
+
shadowedSeqs: SessionSeq[];
|
|
44
|
+
shadowedTokenCount: number;
|
|
45
|
+
/** The provider route that wrote the summary. */
|
|
46
|
+
provider: string;
|
|
47
|
+
/**
|
|
48
|
+
* The model that wrote the summary — the summarize call's envelope,
|
|
49
|
+
* reported by the backend that made the call, logged so the one-shot
|
|
50
|
+
* request is reconstructable from log + code and "which model wrote
|
|
51
|
+
* this summary" has a durable answer (the reconstructability Agent Note).
|
|
52
|
+
*/
|
|
53
|
+
model: string;
|
|
54
|
+
/** The generation cap the summarize call sent, when one applied. */
|
|
55
|
+
maxTokens?: number;
|
|
56
|
+
/** Provider-reported token usage for the summarization request, when emitted. */
|
|
57
|
+
usage?: TokenUsage;
|
|
58
|
+
} & ({
|
|
59
|
+
/** Complete provider output before the backend's safe summary projection. */
|
|
60
|
+
rawOutput: ContentBlock[];
|
|
61
|
+
/** Identifies exactly one call through this context's `ctx.llm.stream()`. */
|
|
62
|
+
llmStreamCall: true;
|
|
63
|
+
} | {
|
|
64
|
+
/** Optional complete output from an unmarked template, remote, or other summarizer. */
|
|
65
|
+
rawOutput?: ContentBlock[];
|
|
66
|
+
/** An unmarked summary does not identify a call through this context's LLM seam. */
|
|
67
|
+
llmStreamCall?: never;
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* Marks the end of a compaction — log-only, releases the lock. Its owner
|
|
71
|
+
* matches `compaction/start`; `error` records an unsuccessful attempt.
|
|
72
|
+
*/
|
|
73
|
+
'compaction/end': {
|
|
74
|
+
compactionId: CompactionId;
|
|
75
|
+
sourceCommandId?: CommandId;
|
|
76
|
+
turn: number | null;
|
|
77
|
+
error?: string;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Shadow price of one model-free prune replacement — log-only, no
|
|
81
|
+
* surfaceOp. The shared shadow-price protocol: a surface `replace` event
|
|
82
|
+
* is priced by the metering event immediately before it (`compaction/summary`
|
|
83
|
+
* for a summarizing compaction, this event for a prune), which states the
|
|
84
|
+
* heuristic token price of the exact replaced range so a pure consumer
|
|
85
|
+
* can subtract it without retaining per-node prices. The replacement MUST
|
|
86
|
+
* be appended synchronously right after this event.
|
|
87
|
+
*/
|
|
88
|
+
'compaction/prune': {
|
|
89
|
+
/** The replaced range's first and last surface-node seqs (a surface-position span, like {@link CompactionResult.shadowedRange}). */
|
|
90
|
+
shadowedRange: {
|
|
91
|
+
start: SessionSeq;
|
|
92
|
+
end: SessionSeq;
|
|
93
|
+
};
|
|
94
|
+
/** The seqs of all shadowed surface nodes, in surface order. */
|
|
95
|
+
shadowedSeqs: SessionSeq[];
|
|
96
|
+
/** Heuristic price of the shadowed content under the token-meter's fixed estimator. */
|
|
97
|
+
shadowedTokenCount: number;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Result of a successful compaction operation. */
|
|
102
|
+
export interface CompactionResult {
|
|
103
|
+
/** Stable identity shared by this compaction's complete durable lifecycle. */
|
|
104
|
+
compactionId: CompactionId;
|
|
105
|
+
/** Human command that initiated this compaction, when it was manual. */
|
|
106
|
+
sourceCommandId?: CommandId;
|
|
107
|
+
/** The seq of the appended `compaction/start` event. */
|
|
108
|
+
startSeq: SessionSeq;
|
|
109
|
+
/** The seq of the appended `compaction/summary` event. */
|
|
110
|
+
summarySeq: SessionSeq;
|
|
111
|
+
/** The seq of the appended `compaction/end` event. */
|
|
112
|
+
endSeq: SessionSeq;
|
|
113
|
+
/** The summary content blocks produced by the backend. */
|
|
114
|
+
summary: ContentBlock[];
|
|
115
|
+
/**
|
|
116
|
+
* The surface-boundary pair that was shadowed: the seqs of the first
|
|
117
|
+
* (`start`) and last (`end`) surface nodes of the replaced range. A
|
|
118
|
+
* surface-POSITION span, not a numeric seq interval — after a prior replace
|
|
119
|
+
* lands a fresh high-seq summary node at an older range's position, `start`
|
|
120
|
+
* can be GREATER than `end`. {@link CompactionResult.shadowedSeqs} is the
|
|
121
|
+
* authoritative set of shadowed nodes, in surface order.
|
|
122
|
+
*/
|
|
123
|
+
shadowedRange: {
|
|
124
|
+
start: SessionSeq;
|
|
125
|
+
end: SessionSeq;
|
|
126
|
+
};
|
|
127
|
+
/** The seqs of all shadowed surface nodes, in surface order. */
|
|
128
|
+
shadowedSeqs: SessionSeq[];
|
|
129
|
+
/** Estimated token count of the shadowed content. */
|
|
130
|
+
shadowedTokenCount: number;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction vocabulary: the result type and the `compaction/*` session events.
|
|
3
|
+
* Those declaration-merged events record the lock and summary inputs without entering the surface, so they are not
|
|
4
|
+
* surface events; a separate replacement `user/message` carries the summary.
|
|
5
|
+
* Backend packages own configuration and retention policy; see
|
|
6
|
+
* `.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md`.
|
|
7
|
+
* @module @qilin-agent/compaction/types
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=types.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qilin-agent/compaction",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"description": "Abstract compaction service seam (ctx.compaction) for the QiLin",
|
|
4
|
+
"version": "3.1.3",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/kkutysllb/QiLin.git",
|
|
11
|
+
"directory": "packages/compaction/compaction"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "lib/index.js",
|
|
18
|
+
"types": "lib/types/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./lib/types/index.d.ts",
|
|
22
|
+
"default": "./lib/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./types": {
|
|
25
|
+
"types": "./lib/types/types.d.ts",
|
|
26
|
+
"default": "./lib/types/types.js"
|
|
27
|
+
},
|
|
28
|
+
"./checkpoint": {
|
|
29
|
+
"types": "./lib/types/checkpoint.d.ts",
|
|
30
|
+
"default": "./lib/types/checkpoint.js"
|
|
31
|
+
},
|
|
32
|
+
"./src/*": "./src/*",
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"lib/index.js",
|
|
37
|
+
"lib/types/**/*.js",
|
|
38
|
+
"lib/types/**/*.d.ts"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@qilin-agent/brand": "3.1.3",
|
|
43
|
+
"@qilin-agent/llm": "3.1.3",
|
|
44
|
+
"@qilin-agent/session": "3.1.3",
|
|
45
|
+
"@qilin-agent/kylin": "~4.0.3",
|
|
46
|
+
"@qilin-agent/commands": "3.1.3"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@qilin-agent/brand": "3.1.3",
|
|
50
|
+
"@qilin-agent/commands": "3.1.3",
|
|
51
|
+
"@qilin-agent/llm": "3.1.3",
|
|
52
|
+
"@qilin-agent/session": "3.1.3",
|
|
53
|
+
"@qilin-agent/kylin": "~4.0.3"
|
|
54
|
+
}
|
|
6
55
|
}
|