@parad0x_labs/openclaw-context-capsule 0.1.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/README.md +62 -0
- package/openclaw.plugin.json +23 -0
- package/package.json +14 -0
- package/src/index.ts +224 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# context-capsule — OpenClaw ContextEngine plugin
|
|
2
|
+
|
|
3
|
+
Compresses agent session history before it reaches the LLM using
|
|
4
|
+
[`@parad0x_labs/context-capsule`](https://github.com/Parad0x-Labs/dna-x402/tree/main/packages/context-capsule).
|
|
5
|
+
Sessions under 20 messages pass through unchanged. Longer sessions have their
|
|
6
|
+
older history compressed into a capsule summary (injected as a system message)
|
|
7
|
+
while the last 10 messages are kept verbatim — giving the model full coherence
|
|
8
|
+
on recent turns without paying for the full transcript.
|
|
9
|
+
|
|
10
|
+
**Most useful for:** local models (Ollama, LM Studio) and GPT-4 where context
|
|
11
|
+
cost matters. Claude users with a 200k context window and built-in compaction
|
|
12
|
+
enabled may not need this.
|
|
13
|
+
|
|
14
|
+
## Benchmark
|
|
15
|
+
|
|
16
|
+
| Metric | Result | CI gate |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| Token savings | 99.3% | >= 95% |
|
|
19
|
+
| Recovery score | 100% | >= 90% |
|
|
20
|
+
| Runtime | 29ms | < 1000ms |
|
|
21
|
+
|
|
22
|
+
Reproduce locally:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
cd packages/context-capsule
|
|
26
|
+
npm run bench:public
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
CI fails if savings drop below 95% or recovery falls below 90%.
|
|
30
|
+
|
|
31
|
+
## Activation
|
|
32
|
+
|
|
33
|
+
```jsonc
|
|
34
|
+
// openclaw.json
|
|
35
|
+
{
|
|
36
|
+
"plugins": {
|
|
37
|
+
"slots": {
|
|
38
|
+
"contextEngine": "context-capsule"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Config options
|
|
45
|
+
|
|
46
|
+
| Key | Default | Description |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `minMessages` | `20` | Sessions shorter than this pass through unchanged |
|
|
49
|
+
| `keepRecentMessages` | `10` | Recent messages kept verbatim after compression |
|
|
50
|
+
|
|
51
|
+
```jsonc
|
|
52
|
+
{
|
|
53
|
+
"plugins": {
|
|
54
|
+
"entries": {
|
|
55
|
+
"context-capsule": {
|
|
56
|
+
"minMessages": 15,
|
|
57
|
+
"keepRecentMessages": 8
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "context-capsule",
|
|
3
|
+
"activation": {
|
|
4
|
+
"onStartup": false
|
|
5
|
+
},
|
|
6
|
+
"enabledByDefault": false,
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"additionalProperties": false,
|
|
10
|
+
"properties": {
|
|
11
|
+
"minMessages": {
|
|
12
|
+
"type": "integer",
|
|
13
|
+
"minimum": 1,
|
|
14
|
+
"description": "Minimum session length (messages) before compression is applied. Default: 20."
|
|
15
|
+
},
|
|
16
|
+
"keepRecentMessages": {
|
|
17
|
+
"type": "integer",
|
|
18
|
+
"minimum": 1,
|
|
19
|
+
"description": "Number of most-recent messages kept verbatim after compression. Default: 10."
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@parad0x_labs/openclaw-context-capsule",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "context-capsule ContextEngine for OpenClaw — 99.3% token reduction with recovery-score gate. Works with Ollama, GPT, Mistral, Claude.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"files": ["src/", "openclaw.plugin.json", "README.md"],
|
|
8
|
+
"keywords": ["openclaw", "openclaw-plugin", "context-compression", "llm", "token-savings", "ollama", "context-capsule"],
|
|
9
|
+
"openclaw": { "extensions": ["./src/index.ts"] },
|
|
10
|
+
"dependencies": { "@parad0x_labs/context-capsule": "^0.1.0" },
|
|
11
|
+
"engines": { "node": ">=22" },
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": { "type": "git", "url": "https://github.com/Parad0x-Labs/dna-x402" }
|
|
14
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-capsule ContextEngine plugin for OpenClaw.
|
|
3
|
+
*
|
|
4
|
+
* Compresses session history before it reaches the LLM, achieving ~99% token
|
|
5
|
+
* reduction while keeping a verbatim tail of recent messages for coherence.
|
|
6
|
+
* Sessions under the minMessages threshold pass through unchanged.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
10
|
+
import type {
|
|
11
|
+
AssembleResult,
|
|
12
|
+
CompactResult,
|
|
13
|
+
ContextEngine,
|
|
14
|
+
ContextEngineInfo,
|
|
15
|
+
IngestResult,
|
|
16
|
+
} from "openclaw/plugin-sdk/context-engine";
|
|
17
|
+
import type { AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
18
|
+
// @ts-expect-error — external package, types may not be present at build time
|
|
19
|
+
import { compressContext, injectCapsule } from "@parad0x_labs/context-capsule";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Configuration defaults
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const DEFAULT_MIN_MESSAGES = 20;
|
|
26
|
+
const DEFAULT_KEEP_RECENT = 10;
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Helpers
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
type SimpleMessage = { role: string; content: string };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Convert an OpenClaw AgentMessage to the plain {role, content} shape expected
|
|
36
|
+
* by @parad0x_labs/context-capsule.
|
|
37
|
+
*
|
|
38
|
+
* Handles:
|
|
39
|
+
* - "toolResult" role → "tool"
|
|
40
|
+
* - Content that is already a string → used as-is
|
|
41
|
+
* - Content that is an array of content blocks → joined to a single string
|
|
42
|
+
*/
|
|
43
|
+
function normalizeMessages(messages: AgentMessage[]): SimpleMessage[] {
|
|
44
|
+
return messages.map((msg) => {
|
|
45
|
+
const role = msg.role === "toolResult" ? "tool" : (msg.role as string);
|
|
46
|
+
|
|
47
|
+
let content: string;
|
|
48
|
+
if (!("content" in msg) || (msg as { content: unknown }).content == null) {
|
|
49
|
+
content = "";
|
|
50
|
+
} else if (typeof (msg as { content: unknown }).content === "string") {
|
|
51
|
+
content = (msg as { content: string }).content;
|
|
52
|
+
} else if (Array.isArray((msg as { content: unknown[] }).content)) {
|
|
53
|
+
const blocks = (msg as { content: unknown[] }).content;
|
|
54
|
+
content = blocks
|
|
55
|
+
.map((block) => {
|
|
56
|
+
if (!block || typeof block !== "object") return "";
|
|
57
|
+
const b = block as Record<string, unknown>;
|
|
58
|
+
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
59
|
+
if (b.type === "toolResult" || b.type === "tool_result") {
|
|
60
|
+
const inner = b.content;
|
|
61
|
+
if (typeof inner === "string") return inner;
|
|
62
|
+
if (Array.isArray(inner)) {
|
|
63
|
+
return (inner as unknown[])
|
|
64
|
+
.map((ib) => {
|
|
65
|
+
if (!ib || typeof ib !== "object") return "";
|
|
66
|
+
const ibr = ib as Record<string, unknown>;
|
|
67
|
+
return ibr.type === "text" && typeof ibr.text === "string" ? ibr.text : "";
|
|
68
|
+
})
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join("\n");
|
|
71
|
+
}
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
return "";
|
|
75
|
+
})
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.join("\n");
|
|
78
|
+
} else {
|
|
79
|
+
content = "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { role, content };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Rough token estimate: ~4 chars per token */
|
|
87
|
+
function estimateTokens(messages: AgentMessage[]): number {
|
|
88
|
+
return messages.reduce((sum, m) => {
|
|
89
|
+
const c =
|
|
90
|
+
"content" in m && typeof (m as { content: unknown }).content === "string"
|
|
91
|
+
? ((m as { content: string }).content).length
|
|
92
|
+
: 0;
|
|
93
|
+
return sum + Math.ceil(c / 4);
|
|
94
|
+
}, 0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// ContextEngine implementation
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
type CapsuleConfig = {
|
|
102
|
+
minMessages: number;
|
|
103
|
+
keepRecentMessages: number;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
class ContextCapsuleEngine implements ContextEngine {
|
|
107
|
+
readonly info: ContextEngineInfo = {
|
|
108
|
+
id: "context-capsule",
|
|
109
|
+
name: "Context Capsule",
|
|
110
|
+
version: "0.1.0",
|
|
111
|
+
ownsCompaction: false,
|
|
112
|
+
turnMaintenanceMode: "background",
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
private readonly cfg: CapsuleConfig;
|
|
116
|
+
|
|
117
|
+
constructor(cfg: Partial<CapsuleConfig> = {}) {
|
|
118
|
+
this.cfg = {
|
|
119
|
+
minMessages: cfg.minMessages ?? DEFAULT_MIN_MESSAGES,
|
|
120
|
+
keepRecentMessages: cfg.keepRecentMessages ?? DEFAULT_KEEP_RECENT,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Required: ingest — accept each message as the transcript grows
|
|
125
|
+
async ingest(_params: {
|
|
126
|
+
sessionId: string;
|
|
127
|
+
sessionKey?: string;
|
|
128
|
+
message: AgentMessage;
|
|
129
|
+
isHeartbeat?: boolean;
|
|
130
|
+
}): Promise<IngestResult> {
|
|
131
|
+
return { ingested: true };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Required: assemble — build the context window for the next model call
|
|
135
|
+
async assemble(params: {
|
|
136
|
+
sessionId: string;
|
|
137
|
+
sessionKey?: string;
|
|
138
|
+
messages: AgentMessage[];
|
|
139
|
+
tokenBudget?: number;
|
|
140
|
+
availableTools?: Set<string>;
|
|
141
|
+
model?: string;
|
|
142
|
+
prompt?: string;
|
|
143
|
+
}): Promise<AssembleResult> {
|
|
144
|
+
const { messages } = params;
|
|
145
|
+
|
|
146
|
+
// Short sessions: pass through unchanged
|
|
147
|
+
if (messages.length < this.cfg.minMessages) {
|
|
148
|
+
return {
|
|
149
|
+
messages,
|
|
150
|
+
estimatedTokens: estimateTokens(messages),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Compress the older history, keep the tail verbatim
|
|
155
|
+
const tail = messages.slice(-this.cfg.keepRecentMessages);
|
|
156
|
+
const older = messages.slice(0, -this.cfg.keepRecentMessages);
|
|
157
|
+
const normalized = normalizeMessages(older);
|
|
158
|
+
|
|
159
|
+
let summaryText: string;
|
|
160
|
+
try {
|
|
161
|
+
const capsule = await compressContext(normalized);
|
|
162
|
+
const injected = await injectCapsule(capsule);
|
|
163
|
+
summaryText = typeof injected === "string" ? injected : JSON.stringify(injected);
|
|
164
|
+
} catch {
|
|
165
|
+
// Fallback: skip compression, return original messages
|
|
166
|
+
return {
|
|
167
|
+
messages,
|
|
168
|
+
estimatedTokens: estimateTokens(messages),
|
|
169
|
+
promptAuthority: "preassembly_may_overflow",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Prepend capsule as a system context message
|
|
174
|
+
const capsuleSystemMessage = {
|
|
175
|
+
role: "system",
|
|
176
|
+
content: `[Context Capsule — compressed history]\n${summaryText}`,
|
|
177
|
+
} as unknown as AgentMessage;
|
|
178
|
+
|
|
179
|
+
const assembled = [capsuleSystemMessage, ...tail];
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
messages: assembled,
|
|
183
|
+
estimatedTokens: estimateTokens(assembled),
|
|
184
|
+
systemPromptAddition:
|
|
185
|
+
"Earlier conversation history has been compressed into the context capsule above.",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Required: compact — delegate to runtime (engine does not own compaction)
|
|
190
|
+
async compact(_params: {
|
|
191
|
+
sessionId: string;
|
|
192
|
+
sessionKey?: string;
|
|
193
|
+
sessionFile: string;
|
|
194
|
+
tokenBudget?: number;
|
|
195
|
+
force?: boolean;
|
|
196
|
+
currentTokenCount?: number;
|
|
197
|
+
compactionTarget?: "budget" | "threshold";
|
|
198
|
+
customInstructions?: string;
|
|
199
|
+
runtimeContext?: unknown;
|
|
200
|
+
abortSignal?: AbortSignal;
|
|
201
|
+
}): Promise<CompactResult> {
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
compacted: false,
|
|
205
|
+
reason: "delegated-to-runtime",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Plugin registration
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
export default definePluginEntry({
|
|
215
|
+
id: "context-capsule",
|
|
216
|
+
name: "Context Capsule",
|
|
217
|
+
description:
|
|
218
|
+
"99.3% token reduction on agent sessions via @parad0x_labs/context-capsule. " +
|
|
219
|
+
"Works with Ollama, LM Studio, GPT-4, Mistral, and Claude. " +
|
|
220
|
+
"Public benchmark with recovery-score gate in CI.",
|
|
221
|
+
register(api) {
|
|
222
|
+
api.registerContextEngine("context-capsule", (_ctx) => new ContextCapsuleEngine());
|
|
223
|
+
},
|
|
224
|
+
});
|