@persistio/openclaw-plugin 0.1.8 → 0.2.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 +84 -70
- package/dist/capture.d.ts +17 -0
- package/dist/capture.js +112 -0
- package/dist/client.d.ts +34 -57
- package/dist/client.js +43 -81
- package/dist/config.d.ts +29 -0
- package/dist/config.js +86 -0
- package/dist/index.js +293 -742
- package/dist/memory-format.d.ts +8 -0
- package/dist/memory-format.js +121 -0
- package/openclaw.plugin.json +67 -103
- package/package.json +10 -11
- package/src/capture.ts +132 -0
- package/src/client.ts +70 -128
- package/src/config.ts +125 -0
- package/src/index.ts +301 -860
- package/src/memory-format.ts +127 -0
- package/dist/ingest-policy.d.ts +0 -48
- package/dist/ingest-policy.js +0 -380
- package/src/ingest-policy.ts +0 -508
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { RecallBundle } from './client.js';
|
|
2
|
+
|
|
3
|
+
const BUNDLE_SECTIONS: Array<{ key: keyof RecallBundle; title: string }> = [
|
|
4
|
+
{ key: 'global_user_rules', title: 'User Rules' },
|
|
5
|
+
{ key: 'user_rules', title: 'User Rules' },
|
|
6
|
+
{ key: 'user_preferences', title: 'User Preferences' },
|
|
7
|
+
{ key: 'constraints', title: 'Constraints' },
|
|
8
|
+
{ key: 'decisions', title: 'Decisions' },
|
|
9
|
+
{ key: 'task_patterns', title: 'Task Patterns' },
|
|
10
|
+
{ key: 'workflows', title: 'Workflows' },
|
|
11
|
+
{ key: 'project', title: 'Project Memory' },
|
|
12
|
+
{ key: 'system_facts', title: 'Facts' },
|
|
13
|
+
{ key: 'domain_knowledge', title: 'Domain Knowledge' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function normalizeRecallQuery(text: string, maxChars: number): string {
|
|
17
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
18
|
+
if (normalized.length <= maxChars) return normalized;
|
|
19
|
+
return `${normalized.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function extractTextFromMessage(message: unknown): string {
|
|
23
|
+
if (typeof message === 'string') return message;
|
|
24
|
+
if (typeof message !== 'object' || message === null) return '';
|
|
25
|
+
|
|
26
|
+
const record = message as Record<string, unknown>;
|
|
27
|
+
const content = record['content'];
|
|
28
|
+
if (typeof content === 'string') return content;
|
|
29
|
+
if (!Array.isArray(content)) return '';
|
|
30
|
+
|
|
31
|
+
return content
|
|
32
|
+
.map((part) => {
|
|
33
|
+
if (typeof part === 'string') return part;
|
|
34
|
+
if (typeof part !== 'object' || part === null) return '';
|
|
35
|
+
const partRecord = part as Record<string, unknown>;
|
|
36
|
+
return partRecord['type'] === 'text' && typeof partRecord['text'] === 'string'
|
|
37
|
+
? partRecord['text']
|
|
38
|
+
: '';
|
|
39
|
+
})
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildRecallQuery(event: { prompt?: string; messages?: unknown[] }, maxChars: number): string {
|
|
45
|
+
const prompt = typeof event.prompt === 'string' ? event.prompt : '';
|
|
46
|
+
if (prompt.trim()) return normalizeRecallQuery(prompt, maxChars);
|
|
47
|
+
|
|
48
|
+
const latestUser = Array.isArray(event.messages)
|
|
49
|
+
? findLatestRoleText(event.messages, 'user')
|
|
50
|
+
: '';
|
|
51
|
+
return normalizeRecallQuery(latestUser, maxChars);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildMemoryBlock(
|
|
55
|
+
bundle: RecallBundle | undefined,
|
|
56
|
+
tokenBudget: number,
|
|
57
|
+
relatedBundle?: RecallBundle,
|
|
58
|
+
): string {
|
|
59
|
+
if ((!bundle && !relatedBundle) || tokenBudget <= 0) return '';
|
|
60
|
+
|
|
61
|
+
const lines: string[] = [
|
|
62
|
+
'## Persistio Memory',
|
|
63
|
+
'Relevant durable memory:',
|
|
64
|
+
];
|
|
65
|
+
let usedTokens = estimateTokens(lines.join('\n'));
|
|
66
|
+
|
|
67
|
+
for (const section of BUNDLE_SECTIONS) {
|
|
68
|
+
const items = uniqueStrings([
|
|
69
|
+
...arrayStrings(bundle?.[section.key]),
|
|
70
|
+
...arrayStrings(relatedBundle?.[section.key]),
|
|
71
|
+
]);
|
|
72
|
+
for (const item of items) {
|
|
73
|
+
const line = `- ${truncateOneLine(item, 360)}`;
|
|
74
|
+
const lineTokens = estimateTokens(line);
|
|
75
|
+
if (usedTokens + lineTokens > tokenBudget) {
|
|
76
|
+
lines.push('', 'Use these only when relevant. Do not mention memory unless asked.');
|
|
77
|
+
return lines.join('\n');
|
|
78
|
+
}
|
|
79
|
+
lines.push(line);
|
|
80
|
+
usedTokens += lineTokens;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (lines.length <= 2) return '';
|
|
85
|
+
lines.push('', 'Use these only when relevant. Do not mention memory unless asked.');
|
|
86
|
+
return lines.join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function findLatestRoleText(messages: unknown[], role: string): string {
|
|
90
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
91
|
+
const message = messages[index];
|
|
92
|
+
if (typeof message !== 'object' || message === null) continue;
|
|
93
|
+
const record = message as Record<string, unknown>;
|
|
94
|
+
if (record['role'] !== role) continue;
|
|
95
|
+
const text = extractTextFromMessage(message).trim();
|
|
96
|
+
if (text) return text;
|
|
97
|
+
}
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function arrayStrings(value: unknown): string[] {
|
|
102
|
+
return Array.isArray(value)
|
|
103
|
+
? value.filter((item): item is string => typeof item === 'string')
|
|
104
|
+
: [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function uniqueStrings(value: string[]): string[] {
|
|
108
|
+
const seen = new Set<string>();
|
|
109
|
+
const out: string[] = [];
|
|
110
|
+
for (const item of value) {
|
|
111
|
+
const normalized = item.replace(/\s+/g, ' ').trim();
|
|
112
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
113
|
+
seen.add(normalized);
|
|
114
|
+
out.push(normalized);
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function truncateOneLine(text: string, maxChars: number): string {
|
|
120
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
121
|
+
if (normalized.length <= maxChars) return normalized;
|
|
122
|
+
return `${normalized.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function estimateTokens(text: string): number {
|
|
126
|
+
return Math.ceil(text.length / 4);
|
|
127
|
+
}
|
package/dist/ingest-policy.d.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
export type OpenClawMessageRole = 'user' | 'assistant' | 'tool';
|
|
2
|
-
export interface PersistioIngestPolicy {
|
|
3
|
-
timeoutMs: number;
|
|
4
|
-
maxChunkChars: number;
|
|
5
|
-
maxChunksPerTurn: number;
|
|
6
|
-
skipSubagentSessions: boolean;
|
|
7
|
-
user: {
|
|
8
|
-
maxCharsPerMessage: number;
|
|
9
|
-
};
|
|
10
|
-
agent: {
|
|
11
|
-
mode: 'bounded' | 'raw';
|
|
12
|
-
maxCharsPerMessage: number;
|
|
13
|
-
maxCharsAfterFiltering: number;
|
|
14
|
-
maxCharsPerTurn: number;
|
|
15
|
-
largeBlockThresholdChars: number;
|
|
16
|
-
largeBlockThresholdLines: number;
|
|
17
|
-
maxTableRows: number;
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
export interface OmissionSummary {
|
|
21
|
-
label: string;
|
|
22
|
-
chars: number;
|
|
23
|
-
lines: number;
|
|
24
|
-
}
|
|
25
|
-
export interface PreparedIngestMessage {
|
|
26
|
-
chunks: string[];
|
|
27
|
-
originalChars: number;
|
|
28
|
-
preparedChars: number;
|
|
29
|
-
truncated: boolean;
|
|
30
|
-
omissions: OmissionSummary[];
|
|
31
|
-
}
|
|
32
|
-
export interface PrepareMessageInput {
|
|
33
|
-
role: OpenClawMessageRole;
|
|
34
|
-
text: string;
|
|
35
|
-
policy: PersistioIngestPolicy;
|
|
36
|
-
remainingAgentChars: number;
|
|
37
|
-
remainingChunks: number;
|
|
38
|
-
}
|
|
39
|
-
export declare const DEFAULT_INGEST_POLICY: PersistioIngestPolicy;
|
|
40
|
-
export declare function resolveIngestPolicy(raw: unknown): PersistioIngestPolicy;
|
|
41
|
-
export declare function shouldIngestSession(sessionId: string, policy: PersistioIngestPolicy): boolean;
|
|
42
|
-
export declare function filterAssistantContent(text: string, policy: PersistioIngestPolicy): {
|
|
43
|
-
text: string;
|
|
44
|
-
omissions: OmissionSummary[];
|
|
45
|
-
truncated: boolean;
|
|
46
|
-
};
|
|
47
|
-
export declare function chunkText(text: string, maxChunkChars: number): string[];
|
|
48
|
-
export declare function prepareMessageForIngest(input: PrepareMessageInput): PreparedIngestMessage;
|
package/dist/ingest-policy.js
DELETED
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_INGEST_POLICY = {
|
|
2
|
-
timeoutMs: 30000,
|
|
3
|
-
maxChunkChars: 6000,
|
|
4
|
-
maxChunksPerTurn: 12,
|
|
5
|
-
skipSubagentSessions: true,
|
|
6
|
-
user: {
|
|
7
|
-
maxCharsPerMessage: 24000,
|
|
8
|
-
},
|
|
9
|
-
agent: {
|
|
10
|
-
mode: 'bounded',
|
|
11
|
-
maxCharsPerMessage: 24000,
|
|
12
|
-
maxCharsAfterFiltering: 9000,
|
|
13
|
-
maxCharsPerTurn: 24000,
|
|
14
|
-
largeBlockThresholdChars: 1200,
|
|
15
|
-
largeBlockThresholdLines: 80,
|
|
16
|
-
maxTableRows: 12,
|
|
17
|
-
},
|
|
18
|
-
};
|
|
19
|
-
function readNumber(value, fallback, min = 1) {
|
|
20
|
-
return typeof value === 'number' && Number.isFinite(value) && value >= min
|
|
21
|
-
? Math.floor(value)
|
|
22
|
-
: fallback;
|
|
23
|
-
}
|
|
24
|
-
function readBoolean(value, fallback) {
|
|
25
|
-
return typeof value === 'boolean' ? value : fallback;
|
|
26
|
-
}
|
|
27
|
-
function readObject(value) {
|
|
28
|
-
return typeof value === 'object' && value !== null
|
|
29
|
-
? value
|
|
30
|
-
: {};
|
|
31
|
-
}
|
|
32
|
-
export function resolveIngestPolicy(raw) {
|
|
33
|
-
const ingest = readObject(raw);
|
|
34
|
-
const user = readObject(ingest['user']);
|
|
35
|
-
const agent = readObject(ingest['agent']);
|
|
36
|
-
const mode = agent['mode'] === 'raw' ? 'raw' : DEFAULT_INGEST_POLICY.agent.mode;
|
|
37
|
-
return {
|
|
38
|
-
timeoutMs: readNumber(ingest['timeoutMs'], DEFAULT_INGEST_POLICY.timeoutMs),
|
|
39
|
-
maxChunkChars: readNumber(ingest['maxChunkChars'], DEFAULT_INGEST_POLICY.maxChunkChars, 256),
|
|
40
|
-
maxChunksPerTurn: readNumber(ingest['maxChunksPerTurn'], DEFAULT_INGEST_POLICY.maxChunksPerTurn),
|
|
41
|
-
skipSubagentSessions: readBoolean(ingest['skipSubagentSessions'], DEFAULT_INGEST_POLICY.skipSubagentSessions),
|
|
42
|
-
user: {
|
|
43
|
-
maxCharsPerMessage: readNumber(user['maxCharsPerMessage'], DEFAULT_INGEST_POLICY.user.maxCharsPerMessage),
|
|
44
|
-
},
|
|
45
|
-
agent: {
|
|
46
|
-
mode,
|
|
47
|
-
maxCharsPerMessage: readNumber(agent['maxCharsPerMessage'], DEFAULT_INGEST_POLICY.agent.maxCharsPerMessage),
|
|
48
|
-
maxCharsAfterFiltering: readNumber(agent['maxCharsAfterFiltering'], DEFAULT_INGEST_POLICY.agent.maxCharsAfterFiltering),
|
|
49
|
-
maxCharsPerTurn: readNumber(agent['maxCharsPerTurn'], DEFAULT_INGEST_POLICY.agent.maxCharsPerTurn),
|
|
50
|
-
largeBlockThresholdChars: readNumber(agent['largeBlockThresholdChars'], DEFAULT_INGEST_POLICY.agent.largeBlockThresholdChars),
|
|
51
|
-
largeBlockThresholdLines: readNumber(agent['largeBlockThresholdLines'], DEFAULT_INGEST_POLICY.agent.largeBlockThresholdLines),
|
|
52
|
-
maxTableRows: readNumber(agent['maxTableRows'], DEFAULT_INGEST_POLICY.agent.maxTableRows),
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
export function shouldIngestSession(sessionId, policy) {
|
|
57
|
-
if (!policy.skipSubagentSessions)
|
|
58
|
-
return true;
|
|
59
|
-
return !sessionId.startsWith('agent:') || sessionId.startsWith('agent:main:');
|
|
60
|
-
}
|
|
61
|
-
function countLines(text) {
|
|
62
|
-
return text.length === 0 ? 0 : text.split('\n').length;
|
|
63
|
-
}
|
|
64
|
-
function marker(label, text, extra) {
|
|
65
|
-
const suffix = extra ? `, ${extra}` : '';
|
|
66
|
-
return `[${label} omitted: ${countLines(text)} lines, ${text.length} chars${suffix}]`;
|
|
67
|
-
}
|
|
68
|
-
function normalizeText(text) {
|
|
69
|
-
return text
|
|
70
|
-
.replace(/\r\n?/g, '\n')
|
|
71
|
-
.replace(/[ \t]+\n/g, '\n')
|
|
72
|
-
.replace(/\n{4,}/g, '\n\n\n')
|
|
73
|
-
.trim();
|
|
74
|
-
}
|
|
75
|
-
function pushOmission(omissions, label, text) {
|
|
76
|
-
omissions.push({ label, chars: text.length, lines: countLines(text) });
|
|
77
|
-
}
|
|
78
|
-
function collapseLargeFencedBlocks(text, policy, omissions) {
|
|
79
|
-
return text.replace(/```([^\n`]*)\n([\s\S]*?)```/g, (block, language) => {
|
|
80
|
-
if (block.length < policy.agent.largeBlockThresholdChars &&
|
|
81
|
-
countLines(block) < policy.agent.largeBlockThresholdLines) {
|
|
82
|
-
return block;
|
|
83
|
-
}
|
|
84
|
-
pushOmission(omissions, 'Code block', block);
|
|
85
|
-
const lang = language.trim();
|
|
86
|
-
return marker('Code block', block, lang ? `language=${lang}` : undefined);
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
function isBase64LikeLine(line) {
|
|
90
|
-
const compact = line.trim();
|
|
91
|
-
if (compact.length < 500 || /\s/.test(compact))
|
|
92
|
-
return false;
|
|
93
|
-
if (!/^[A-Za-z0-9+/=_-]+$/.test(compact))
|
|
94
|
-
return false;
|
|
95
|
-
const alphaNumeric = compact.replace(/[^A-Za-z0-9]/g, '').length / compact.length;
|
|
96
|
-
return alphaNumeric > 0.85;
|
|
97
|
-
}
|
|
98
|
-
function collapseBase64Lines(text, omissions) {
|
|
99
|
-
return text.split('\n').map((line) => {
|
|
100
|
-
if (!isBase64LikeLine(line))
|
|
101
|
-
return line;
|
|
102
|
-
pushOmission(omissions, 'Encoded blob', line);
|
|
103
|
-
return `[Encoded blob omitted: 1 line, ${line.length} chars]`;
|
|
104
|
-
}).join('\n');
|
|
105
|
-
}
|
|
106
|
-
function looksLikeDiffStart(line) {
|
|
107
|
-
return /^diff --git\b/.test(line) || line === '*** Begin Patch';
|
|
108
|
-
}
|
|
109
|
-
function isDiffMetadataLine(line) {
|
|
110
|
-
return /^(?:index|new file mode|deleted file mode|old mode|new mode|similarity index|dissimilarity index|rename from|rename to|copy from|copy to)\b/.test(line)
|
|
111
|
-
|| /^(?:---|\+\+\+) /.test(line)
|
|
112
|
-
|| /^Binary files .+ differ$/.test(line)
|
|
113
|
-
|| /^\*\*\* (?:Add|Update|Delete) File: /.test(line)
|
|
114
|
-
|| /^\*\*\* End of File$/.test(line);
|
|
115
|
-
}
|
|
116
|
-
function isDiffBodyLine(line) {
|
|
117
|
-
return /^@@/.test(line)
|
|
118
|
-
|| /^[ +\\-]/.test(line);
|
|
119
|
-
}
|
|
120
|
-
function collapseDiffBlocks(text, policy, omissions) {
|
|
121
|
-
const lines = text.split('\n');
|
|
122
|
-
const result = [];
|
|
123
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
124
|
-
const line = lines[i];
|
|
125
|
-
if (!looksLikeDiffStart(line)) {
|
|
126
|
-
result.push(line);
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
const block = [line];
|
|
130
|
-
i += 1;
|
|
131
|
-
for (; i < lines.length; i += 1) {
|
|
132
|
-
const next = lines[i];
|
|
133
|
-
if (looksLikeDiffStart(next)) {
|
|
134
|
-
i -= 1;
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
if (next === '*** End Patch') {
|
|
138
|
-
block.push(next);
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
if (next.trim() === '') {
|
|
142
|
-
i -= 1;
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
if (!isDiffMetadataLine(next) && !isDiffBodyLine(next)) {
|
|
146
|
-
i -= 1;
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
block.push(next);
|
|
150
|
-
}
|
|
151
|
-
const blockText = block.join('\n');
|
|
152
|
-
if (blockText.length < policy.agent.largeBlockThresholdChars &&
|
|
153
|
-
block.length < policy.agent.largeBlockThresholdLines) {
|
|
154
|
-
result.push(blockText);
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
pushOmission(omissions, 'Diff', blockText);
|
|
158
|
-
result.push(marker('Diff', blockText));
|
|
159
|
-
}
|
|
160
|
-
return result.join('\n');
|
|
161
|
-
}
|
|
162
|
-
function isLogLikeLine(line) {
|
|
163
|
-
return /^\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}/.test(line)
|
|
164
|
-
|| /^\s*(ERROR|WARN|INFO|DEBUG|TRACE)\b/.test(line)
|
|
165
|
-
|| /^\s*at\s+.+\(.+:\d+:\d+\)/.test(line)
|
|
166
|
-
|| /^\s*at\s+.+:\d+:\d+/.test(line)
|
|
167
|
-
|| /^Traceback \(most recent call last\):/.test(line)
|
|
168
|
-
|| /^[A-Za-z]*Error: .+/.test(line);
|
|
169
|
-
}
|
|
170
|
-
function isShellOutputLine(line) {
|
|
171
|
-
return /^\s*(PASS|FAIL|RUNS|Test Files|Tests|Duration|stderr|stdout)\b/.test(line)
|
|
172
|
-
|| /^>\s+[\w@/.-]+/.test(line)
|
|
173
|
-
|| /^\$\s+\S+/.test(line)
|
|
174
|
-
|| /^npm (ERR!|WARN|notice)\b/.test(line);
|
|
175
|
-
}
|
|
176
|
-
function collapseLineRuns(text, label, predicate, policy, omissions) {
|
|
177
|
-
const lines = text.split('\n');
|
|
178
|
-
const result = [];
|
|
179
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
180
|
-
const line = lines[i];
|
|
181
|
-
if (!predicate(line)) {
|
|
182
|
-
result.push(line);
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
const block = [line];
|
|
186
|
-
i += 1;
|
|
187
|
-
for (; i < lines.length; i += 1) {
|
|
188
|
-
const next = lines[i];
|
|
189
|
-
if (!predicate(next)) {
|
|
190
|
-
i -= 1;
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
block.push(next);
|
|
194
|
-
}
|
|
195
|
-
const blockText = block.join('\n');
|
|
196
|
-
if (blockText.length < policy.agent.largeBlockThresholdChars &&
|
|
197
|
-
block.length < policy.agent.largeBlockThresholdLines) {
|
|
198
|
-
result.push(blockText);
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
pushOmission(omissions, label, blockText);
|
|
202
|
-
const firstUsefulLine = block.find((candidate) => candidate.trim().length > 0)?.trim();
|
|
203
|
-
result.push(marker(label, blockText, firstUsefulLine ? `first="${firstUsefulLine.slice(0, 120)}"` : undefined));
|
|
204
|
-
}
|
|
205
|
-
return result.join('\n');
|
|
206
|
-
}
|
|
207
|
-
function isMarkdownTableLine(line) {
|
|
208
|
-
const trimmed = line.trim();
|
|
209
|
-
return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.split('|').length >= 4;
|
|
210
|
-
}
|
|
211
|
-
function isMarkdownTableSeparator(line) {
|
|
212
|
-
return /^\s*\|?(?:\s*:?-{3,}:?\s*\|)+\s*:?-{3,}:?\s*\|?\s*$/.test(line);
|
|
213
|
-
}
|
|
214
|
-
function truncateMarkdownTables(text, policy, omissions) {
|
|
215
|
-
const lines = text.split('\n');
|
|
216
|
-
const result = [];
|
|
217
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
218
|
-
if (!isMarkdownTableLine(lines[i]) || !lines[i + 1] || !isMarkdownTableSeparator(lines[i + 1])) {
|
|
219
|
-
result.push(lines[i]);
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
const table = [lines[i], lines[i + 1]];
|
|
223
|
-
i += 2;
|
|
224
|
-
for (; i < lines.length && isMarkdownTableLine(lines[i]); i += 1) {
|
|
225
|
-
table.push(lines[i]);
|
|
226
|
-
}
|
|
227
|
-
i -= 1;
|
|
228
|
-
if (table.length <= policy.agent.maxTableRows + 2) {
|
|
229
|
-
result.push(...table);
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
const omitted = table.slice(policy.agent.maxTableRows + 2).join('\n');
|
|
233
|
-
pushOmission(omissions, 'Table rows', omitted);
|
|
234
|
-
result.push(...table.slice(0, policy.agent.maxTableRows + 2));
|
|
235
|
-
result.push(`[Table truncated: ${table.length - policy.agent.maxTableRows - 2} more rows]`);
|
|
236
|
-
}
|
|
237
|
-
return result.join('\n');
|
|
238
|
-
}
|
|
239
|
-
function maybeCollapseWholeBlob(text, omissions) {
|
|
240
|
-
const trimmed = text.trim();
|
|
241
|
-
if (trimmed.length < 2000)
|
|
242
|
-
return text;
|
|
243
|
-
try {
|
|
244
|
-
const parsed = JSON.parse(trimmed);
|
|
245
|
-
pushOmission(omissions, 'JSON blob', text);
|
|
246
|
-
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
247
|
-
const keys = Object.keys(parsed).slice(0, 12).join(',');
|
|
248
|
-
return `[JSON blob omitted: ${countLines(text)} lines, ${text.length} chars${keys ? `, keys=${keys}` : ''}]`;
|
|
249
|
-
}
|
|
250
|
-
return marker('JSON blob', text);
|
|
251
|
-
}
|
|
252
|
-
catch {
|
|
253
|
-
// Continue with XML-ish shape detection below.
|
|
254
|
-
}
|
|
255
|
-
const angleRatio = (trimmed.match(/[<>/]/g)?.length ?? 0) / trimmed.length;
|
|
256
|
-
const lineCount = countLines(trimmed);
|
|
257
|
-
if (lineCount >= 20 &&
|
|
258
|
-
angleRatio > 0.08 &&
|
|
259
|
-
/^<\??[A-Za-z!]/.test(trimmed) &&
|
|
260
|
-
/<\/[A-Za-z][^>]*>/.test(trimmed)) {
|
|
261
|
-
pushOmission(omissions, 'XML blob', text);
|
|
262
|
-
return marker('XML blob', text);
|
|
263
|
-
}
|
|
264
|
-
return text;
|
|
265
|
-
}
|
|
266
|
-
function fitToBudget(text, budget) {
|
|
267
|
-
if (text.length <= budget) {
|
|
268
|
-
return { text, truncated: false };
|
|
269
|
-
}
|
|
270
|
-
const markerText = `\n\n[Content truncated: original ${text.length} chars, kept ${budget} chars]\n\n`;
|
|
271
|
-
const available = Math.max(0, budget - markerText.length);
|
|
272
|
-
const headLength = Math.ceil(available * 0.6);
|
|
273
|
-
const tailLength = Math.max(0, available - headLength);
|
|
274
|
-
return {
|
|
275
|
-
text: `${text.slice(0, headLength).trimEnd()}${markerText}${text.slice(text.length - tailLength).trimStart()}`.trim(),
|
|
276
|
-
truncated: true,
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
export function filterAssistantContent(text, policy) {
|
|
280
|
-
const omissions = [];
|
|
281
|
-
let filtered = normalizeText(text);
|
|
282
|
-
if (policy.agent.mode === 'bounded') {
|
|
283
|
-
filtered = collapseLargeFencedBlocks(filtered, policy, omissions);
|
|
284
|
-
filtered = collapseDiffBlocks(filtered, policy, omissions);
|
|
285
|
-
filtered = collapseLineRuns(filtered, 'Log output', isLogLikeLine, policy, omissions);
|
|
286
|
-
filtered = collapseLineRuns(filtered, 'Command output', isShellOutputLine, policy, omissions);
|
|
287
|
-
filtered = truncateMarkdownTables(filtered, policy, omissions);
|
|
288
|
-
filtered = collapseBase64Lines(filtered, omissions);
|
|
289
|
-
filtered = maybeCollapseWholeBlob(filtered, omissions);
|
|
290
|
-
}
|
|
291
|
-
const budgeted = fitToBudget(filtered, policy.agent.maxCharsAfterFiltering);
|
|
292
|
-
return {
|
|
293
|
-
text: budgeted.text,
|
|
294
|
-
omissions,
|
|
295
|
-
truncated: budgeted.truncated,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
export function chunkText(text, maxChunkChars) {
|
|
299
|
-
const normalized = normalizeText(text);
|
|
300
|
-
if (!normalized)
|
|
301
|
-
return [];
|
|
302
|
-
const chunks = [];
|
|
303
|
-
let current = '';
|
|
304
|
-
const flush = () => {
|
|
305
|
-
if (!current.trim())
|
|
306
|
-
return;
|
|
307
|
-
chunks.push(current.trim());
|
|
308
|
-
current = '';
|
|
309
|
-
};
|
|
310
|
-
const appendUnit = (unit) => {
|
|
311
|
-
const separator = current ? '\n\n' : '';
|
|
312
|
-
if (current.length + separator.length + unit.length <= maxChunkChars) {
|
|
313
|
-
current = `${current}${separator}${unit}`;
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
flush();
|
|
317
|
-
if (unit.length <= maxChunkChars) {
|
|
318
|
-
current = unit;
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
for (let start = 0; start < unit.length; start += maxChunkChars) {
|
|
322
|
-
chunks.push(unit.slice(start, start + maxChunkChars).trim());
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
for (const paragraph of normalized.split(/\n{2,}/)) {
|
|
326
|
-
if (paragraph.length <= maxChunkChars) {
|
|
327
|
-
appendUnit(paragraph);
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
for (const line of paragraph.split('\n')) {
|
|
331
|
-
appendUnit(line);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
flush();
|
|
335
|
-
return chunks.filter((chunk) => chunk.length > 0);
|
|
336
|
-
}
|
|
337
|
-
export function prepareMessageForIngest(input) {
|
|
338
|
-
const original = normalizeText(input.text);
|
|
339
|
-
const omissions = [];
|
|
340
|
-
let prepared = original;
|
|
341
|
-
let truncated = false;
|
|
342
|
-
if (input.role === 'assistant') {
|
|
343
|
-
const messageBudget = input.remainingAgentChars;
|
|
344
|
-
if (messageBudget <= 0 || input.remainingChunks <= 0) {
|
|
345
|
-
return {
|
|
346
|
-
chunks: [],
|
|
347
|
-
originalChars: original.length,
|
|
348
|
-
preparedChars: 0,
|
|
349
|
-
truncated: true,
|
|
350
|
-
omissions: [],
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
const preBudgeted = fitToBudget(prepared, input.policy.agent.maxCharsPerMessage);
|
|
354
|
-
prepared = preBudgeted.text;
|
|
355
|
-
truncated = preBudgeted.truncated;
|
|
356
|
-
const filtered = filterAssistantContent(prepared, input.policy);
|
|
357
|
-
prepared = filtered.text;
|
|
358
|
-
omissions.push(...filtered.omissions);
|
|
359
|
-
truncated = truncated || filtered.truncated || filtered.omissions.length > 0;
|
|
360
|
-
const budgeted = fitToBudget(prepared, messageBudget);
|
|
361
|
-
prepared = budgeted.text;
|
|
362
|
-
truncated = truncated || budgeted.truncated;
|
|
363
|
-
}
|
|
364
|
-
else if (input.role === 'user') {
|
|
365
|
-
const budgeted = fitToBudget(prepared, input.policy.user.maxCharsPerMessage);
|
|
366
|
-
prepared = budgeted.text;
|
|
367
|
-
truncated = budgeted.truncated;
|
|
368
|
-
}
|
|
369
|
-
const chunks = chunkText(prepared, input.policy.maxChunkChars).slice(0, input.remainingChunks);
|
|
370
|
-
if (chunks.join('\n\n').length < prepared.length) {
|
|
371
|
-
truncated = true;
|
|
372
|
-
}
|
|
373
|
-
return {
|
|
374
|
-
chunks,
|
|
375
|
-
originalChars: original.length,
|
|
376
|
-
preparedChars: chunks.reduce((sum, chunk) => sum + chunk.length, 0),
|
|
377
|
-
truncated,
|
|
378
|
-
omissions,
|
|
379
|
-
};
|
|
380
|
-
}
|