groove-dev 0.27.101 → 0.27.103
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/moe-training/client/domain-tagger.js +205 -0
- package/moe-training/client/edit-normalizer.js +188 -0
- package/moe-training/client/envelope-builder.js +1 -1
- package/moe-training/client/parsers/claude-code.js +56 -9
- package/moe-training/client/parsers/codex.js +25 -5
- package/moe-training/client/parsers/gemini.js +21 -2
- package/moe-training/client/parsers/grok.js +18 -0
- package/moe-training/client/trajectory-capture.js +95 -3
- package/moe-training/server/routes/ingest.js +26 -0
- package/moe-training/server/verifier.js +34 -0
- package/moe-training/shared/constants.js +9 -0
- package/moe-training/shared/envelope-schema.js +128 -2
- package/moe-training/test/client/domain-tagger.test.js +203 -0
- package/moe-training/test/client/edit-normalizer.test.js +376 -0
- package/moe-training/test/client/envelope-builder.test.js +28 -0
- package/moe-training/test/client/parsers/claude-code.test.js +248 -38
- package/moe-training/test/client/parsers/codex.test.js +2 -0
- package/moe-training/test/client/parsers/gemini.test.js +2 -0
- package/moe-training/test/client/trajectory-capture.test.js +345 -0
- package/moe-training/test/server/verifier.test.js +94 -0
- package/moe-training/test/shared/envelope-schema.test.js +291 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/preview.js +148 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/moe-training/client/domain-tagger.js +205 -0
- package/node_modules/moe-training/client/edit-normalizer.js +188 -0
- package/node_modules/moe-training/client/envelope-builder.js +1 -1
- package/node_modules/moe-training/client/parsers/claude-code.js +56 -9
- package/node_modules/moe-training/client/parsers/codex.js +25 -5
- package/node_modules/moe-training/client/parsers/gemini.js +21 -2
- package/node_modules/moe-training/client/parsers/grok.js +18 -0
- package/node_modules/moe-training/client/trajectory-capture.js +95 -3
- package/node_modules/moe-training/server/routes/ingest.js +26 -0
- package/node_modules/moe-training/server/verifier.js +34 -0
- package/node_modules/moe-training/shared/constants.js +9 -0
- package/node_modules/moe-training/shared/envelope-schema.js +128 -2
- package/node_modules/moe-training/test/client/domain-tagger.test.js +203 -0
- package/node_modules/moe-training/test/client/edit-normalizer.test.js +376 -0
- package/node_modules/moe-training/test/client/envelope-builder.test.js +28 -0
- package/node_modules/moe-training/test/client/parsers/claude-code.test.js +248 -38
- package/node_modules/moe-training/test/client/parsers/codex.test.js +2 -0
- package/node_modules/moe-training/test/client/parsers/gemini.test.js +2 -0
- package/node_modules/moe-training/test/client/trajectory-capture.test.js +345 -0
- package/node_modules/moe-training/test/server/verifier.test.js +94 -0
- package/node_modules/moe-training/test/shared/envelope-schema.test.js +291 -0
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/preview.js +148 -2
- package/packages/gui/package.json +1 -1
- package/packages/launch-page/dist/assets/index-Bo186ysq.js +4180 -0
- package/packages/launch-page/dist/assets/index-CP4c4yxe.css +1 -0
- package/packages/launch-page/dist/index.html +2 -2
- package/packages/launch-page/src/App.css +438 -137
- package/packages/launch-page/src/App.tsx +171 -123
- package/packages/launch-page/src/index.css +9 -2
- package/packages/launch-page/dist/assets/index-BK3nAvHG.js +0 -4180
- package/packages/launch-page/dist/assets/index-jrLVZW5U.css +0 -2
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
|
|
3
|
+
const DEFAULT_DOMAINS = [
|
|
4
|
+
'python', 'typescript_node', 'react_frontend', 'postgresql_database',
|
|
5
|
+
'devops_docker', 'rust', 'data_science_ml', 'security_pentest',
|
|
6
|
+
'mobile_swift', 'system_design',
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MODEL = 'sentence-transformers/all-MiniLM-L6-v2';
|
|
10
|
+
const DEFAULT_TOP_K = 3;
|
|
11
|
+
|
|
12
|
+
const DOMAIN_KEYWORDS = {
|
|
13
|
+
python: ['python', 'pip', 'pytest', 'django', 'flask', 'fastapi', '.py', 'pandas', 'numpy', 'venv', 'poetry', 'pyproject', '__init__', 'def ', 'import '],
|
|
14
|
+
typescript_node: ['typescript', 'node', 'npm', 'express', '.ts', 'tsconfig', 'package.json', 'nestjs', 'prisma', 'tsc', 'deno', 'bun'],
|
|
15
|
+
react_frontend: ['react', 'jsx', 'tsx', 'component', 'hook', 'usestate', 'useeffect', 'tailwind', 'css', 'vite', 'nextjs', 'styled', 'frontend', 'dom', 'html'],
|
|
16
|
+
postgresql_database: ['postgresql', 'postgres', 'sql', 'database', 'query', 'schema', 'migration', 'table', 'index', 'select', 'insert', 'join', 'foreign key', 'sequelize', 'knex'],
|
|
17
|
+
devops_docker: ['docker', 'kubernetes', 'k8s', 'ci/cd', 'github actions', 'deployment', 'terraform', 'ansible', 'nginx', 'dockerfile', 'compose', 'helm', 'aws', 'gcp', 'pipeline'],
|
|
18
|
+
rust: ['rust', 'cargo', 'ownership', 'lifetime', 'borrow', '.rs', 'impl ', 'fn ', 'struct ', 'enum ', 'trait ', 'crate', 'tokio'],
|
|
19
|
+
data_science_ml: ['machine learning', 'pytorch', 'tensorflow', 'ml', 'training', 'dataset', 'neural', 'deep learning', 'transformer', 'huggingface', 'sklearn', 'prediction', 'epoch', 'loss'],
|
|
20
|
+
security_pentest: ['security', 'vulnerability', 'cve', 'authentication', 'authorization', 'encryption', 'xss', 'sql injection', 'pentest', 'exploit', 'firewall', 'oauth', 'csrf'],
|
|
21
|
+
mobile_swift: ['swift', 'ios', 'swiftui', 'xcode', 'cocoapod', 'uikit', 'storyboard', 'watchos', 'macos', 'apple', 'carthage', 'spm'],
|
|
22
|
+
system_design: ['architecture', 'system design', 'scalability', 'microservice', 'distributed', 'load balancer', 'cache', 'message queue', 'api gateway', 'monorepo', 'design pattern', 'event driven'],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export class DomainTagger {
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this._serviceUrl = options.serviceUrl || process.env.EMBEDDING_SERVICE_URL || null;
|
|
28
|
+
this._model = options.model || DEFAULT_MODEL;
|
|
29
|
+
this._topK = options.topK || DEFAULT_TOP_K;
|
|
30
|
+
this._domains = options.domains || DEFAULT_DOMAINS;
|
|
31
|
+
this._ready = false;
|
|
32
|
+
this._mode = null;
|
|
33
|
+
this._centroids = null;
|
|
34
|
+
this._lastError = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async init() {
|
|
38
|
+
this._lastError = null;
|
|
39
|
+
|
|
40
|
+
if (this._serviceUrl) {
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(this._serviceUrl, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify({ input: 'health check', model: this._model }),
|
|
46
|
+
signal: AbortSignal.timeout(5_000),
|
|
47
|
+
});
|
|
48
|
+
if (res.ok) {
|
|
49
|
+
this._mode = 'http';
|
|
50
|
+
await this._buildCentroids();
|
|
51
|
+
this._ready = true;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// HTTP service unavailable
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this._mode = 'keyword';
|
|
60
|
+
this._ready = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async tag(routingText) {
|
|
64
|
+
if (!this._ready || !routingText || typeof routingText !== 'string') return null;
|
|
65
|
+
|
|
66
|
+
this._lastError = null;
|
|
67
|
+
try {
|
|
68
|
+
if (this._mode === 'http') {
|
|
69
|
+
return await this._tagWithEmbeddings(routingText);
|
|
70
|
+
}
|
|
71
|
+
return this._tagWithKeywords(routingText);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
this._lastError = err.message || String(err);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get lastError() {
|
|
79
|
+
return this._lastError;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get ready() {
|
|
83
|
+
return this._ready;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get mode() {
|
|
87
|
+
return this._mode;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static buildRoutingText(taskTitle, firstPrompt, thoughtSteps = []) {
|
|
91
|
+
const parts = [];
|
|
92
|
+
if (taskTitle) parts.push(taskTitle);
|
|
93
|
+
if (firstPrompt) parts.push(firstPrompt);
|
|
94
|
+
for (const step of thoughtSteps.slice(0, 2)) {
|
|
95
|
+
if (step?.content) parts.push(step.content);
|
|
96
|
+
}
|
|
97
|
+
return parts.join('\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async _tagWithEmbeddings(routingText) {
|
|
101
|
+
const embedding = await this._embed(routingText);
|
|
102
|
+
if (!embedding) return null;
|
|
103
|
+
|
|
104
|
+
const scores = [];
|
|
105
|
+
for (const [domain, centroid] of Object.entries(this._centroids)) {
|
|
106
|
+
scores.push({ domain, confidence: cosineSimilarity(embedding, centroid) });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
scores.sort((a, b) => b.confidence - a.confidence);
|
|
110
|
+
const top = scores.slice(0, this._topK);
|
|
111
|
+
|
|
112
|
+
if (top.length < 3) return null;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
primary: { domain: top[0].domain, confidence: round4(top[0].confidence) },
|
|
116
|
+
secondary: { domain: top[1].domain, confidence: round4(top[1].confidence) },
|
|
117
|
+
tertiary: { domain: top[2].domain, confidence: round4(top[2].confidence) },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_tagWithKeywords(routingText) {
|
|
122
|
+
const text = routingText.toLowerCase();
|
|
123
|
+
const scores = [];
|
|
124
|
+
|
|
125
|
+
for (const domain of this._domains) {
|
|
126
|
+
const keywords = DOMAIN_KEYWORDS[domain];
|
|
127
|
+
if (!keywords) {
|
|
128
|
+
scores.push({ domain, confidence: 0 });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let hits = 0;
|
|
133
|
+
for (const kw of keywords) {
|
|
134
|
+
if (text.includes(kw.toLowerCase())) hits++;
|
|
135
|
+
}
|
|
136
|
+
scores.push({ domain, confidence: keywords.length > 0 ? hits / keywords.length : 0 });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
scores.sort((a, b) => b.confidence - a.confidence);
|
|
140
|
+
|
|
141
|
+
if (scores[0].confidence === 0) return null;
|
|
142
|
+
|
|
143
|
+
const top = scores.slice(0, this._topK);
|
|
144
|
+
return {
|
|
145
|
+
primary: { domain: top[0].domain, confidence: round4(top[0].confidence) },
|
|
146
|
+
secondary: { domain: top[1].domain, confidence: round4(top[1].confidence) },
|
|
147
|
+
tertiary: { domain: top[2].domain, confidence: round4(top[2].confidence) },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async _buildCentroids() {
|
|
152
|
+
this._centroids = {};
|
|
153
|
+
for (const domain of this._domains) {
|
|
154
|
+
const kws = DOMAIN_KEYWORDS[domain];
|
|
155
|
+
const description = kws ? `${domain}: ${kws.join(', ')}` : domain;
|
|
156
|
+
const embedding = await this._embed(description);
|
|
157
|
+
if (embedding) {
|
|
158
|
+
this._centroids[domain] = embedding;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async _embed(text) {
|
|
164
|
+
try {
|
|
165
|
+
const res = await fetch(this._serviceUrl, {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
headers: { 'Content-Type': 'application/json' },
|
|
168
|
+
body: JSON.stringify({ input: text, model: this._model }),
|
|
169
|
+
signal: AbortSignal.timeout(10_000),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (!res.ok) {
|
|
173
|
+
this._lastError = `Embedding service returned ${res.status}`;
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const data = await res.json();
|
|
178
|
+
const embedding = data?.data?.[0]?.embedding;
|
|
179
|
+
if (!Array.isArray(embedding)) {
|
|
180
|
+
this._lastError = 'Invalid embedding response format';
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return embedding;
|
|
184
|
+
} catch (err) {
|
|
185
|
+
this._lastError = err.message || String(err);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function cosineSimilarity(a, b) {
|
|
192
|
+
if (!a || !b || a.length !== b.length) return 0;
|
|
193
|
+
let dot = 0, magA = 0, magB = 0;
|
|
194
|
+
for (let i = 0; i < a.length; i++) {
|
|
195
|
+
dot += a[i] * b[i];
|
|
196
|
+
magA += a[i] * a[i];
|
|
197
|
+
magB += b[i] * b[i];
|
|
198
|
+
}
|
|
199
|
+
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
200
|
+
return denom === 0 ? 0 : dot / denom;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function round4(n) {
|
|
204
|
+
return Math.round(n * 10000) / 10000;
|
|
205
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
|
|
3
|
+
const APPLY_PATCH_RE = /apply_patch/;
|
|
4
|
+
const BEGIN_PATCH_RE = /^\*{3}\s*Begin Patch\s*$/;
|
|
5
|
+
const END_PATCH_RE = /^\*{3}\s*End Patch\s*$/;
|
|
6
|
+
const ADD_FILE_RE = /^\*{3}\s*Add File:\s*(.+)$/;
|
|
7
|
+
const UPDATE_FILE_RE = /^\*{3}\s*Update File:\s*(.+)$/;
|
|
8
|
+
const DELETE_FILE_RE = /^\*{3}\s*Delete File:\s*(.+)$/;
|
|
9
|
+
const HUNK_HEADER_RE = /^@@@.*@@@/;
|
|
10
|
+
|
|
11
|
+
export class EditNormalizer {
|
|
12
|
+
detectApplyPatch(actionContent) {
|
|
13
|
+
const text = extractText(actionContent);
|
|
14
|
+
if (!text) return false;
|
|
15
|
+
return APPLY_PATCH_RE.test(text);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
normalize(actionContent, timestamp, startStep) {
|
|
19
|
+
const text = extractText(actionContent);
|
|
20
|
+
if (!text) return [];
|
|
21
|
+
|
|
22
|
+
const patchBody = extractPatchBody(text);
|
|
23
|
+
if (!patchBody) return [];
|
|
24
|
+
|
|
25
|
+
const sections = parseSections(patchBody);
|
|
26
|
+
const edits = [];
|
|
27
|
+
let step = startStep || 1;
|
|
28
|
+
|
|
29
|
+
for (const section of sections) {
|
|
30
|
+
if (section.type === 'add') {
|
|
31
|
+
edits.push({
|
|
32
|
+
step: step++,
|
|
33
|
+
type: 'edit',
|
|
34
|
+
timestamp: timestamp || Date.now() / 1000,
|
|
35
|
+
file_path: section.filePath,
|
|
36
|
+
edit_type: 'create',
|
|
37
|
+
content: section.content,
|
|
38
|
+
old_string: null,
|
|
39
|
+
new_string: null,
|
|
40
|
+
token_count: estimateTokens(section.content),
|
|
41
|
+
});
|
|
42
|
+
} else if (section.type === 'update') {
|
|
43
|
+
const hunks = parseHunks(section.lines);
|
|
44
|
+
for (const hunk of hunks) {
|
|
45
|
+
edits.push({
|
|
46
|
+
step: step++,
|
|
47
|
+
type: 'edit',
|
|
48
|
+
timestamp: timestamp || Date.now() / 1000,
|
|
49
|
+
file_path: section.filePath,
|
|
50
|
+
edit_type: 'modify',
|
|
51
|
+
content: null,
|
|
52
|
+
old_string: hunk.oldString,
|
|
53
|
+
new_string: hunk.newString,
|
|
54
|
+
token_count: estimateTokens(hunk.oldString + hunk.newString),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
} else if (section.type === 'delete') {
|
|
58
|
+
edits.push({
|
|
59
|
+
step: step++,
|
|
60
|
+
type: 'edit',
|
|
61
|
+
timestamp: timestamp || Date.now() / 1000,
|
|
62
|
+
file_path: section.filePath,
|
|
63
|
+
edit_type: 'delete',
|
|
64
|
+
content: null,
|
|
65
|
+
old_string: section.content || null,
|
|
66
|
+
new_string: null,
|
|
67
|
+
token_count: estimateTokens(section.content || ''),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return edits;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function extractText(actionContent) {
|
|
77
|
+
if (typeof actionContent === 'string') return actionContent;
|
|
78
|
+
if (actionContent?.arguments?.command) return actionContent.arguments.command;
|
|
79
|
+
if (actionContent?.content) return actionContent.content;
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function extractPatchBody(text) {
|
|
84
|
+
const lines = text.split('\n');
|
|
85
|
+
let startIdx = -1;
|
|
86
|
+
let endIdx = -1;
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < lines.length; i++) {
|
|
89
|
+
if (BEGIN_PATCH_RE.test(lines[i].trim())) {
|
|
90
|
+
startIdx = i + 1;
|
|
91
|
+
}
|
|
92
|
+
if (END_PATCH_RE.test(lines[i].trim())) {
|
|
93
|
+
endIdx = i;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (startIdx === -1) return null;
|
|
98
|
+
if (endIdx === -1) endIdx = lines.length;
|
|
99
|
+
|
|
100
|
+
return lines.slice(startIdx, endIdx).join('\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseSections(body) {
|
|
104
|
+
const lines = body.split('\n');
|
|
105
|
+
const sections = [];
|
|
106
|
+
let current = null;
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < lines.length; i++) {
|
|
109
|
+
const line = lines[i];
|
|
110
|
+
const trimmed = line.trim();
|
|
111
|
+
|
|
112
|
+
let match;
|
|
113
|
+
if ((match = ADD_FILE_RE.exec(trimmed))) {
|
|
114
|
+
if (current) sections.push(current);
|
|
115
|
+
current = { type: 'add', filePath: match[1].trim(), lines: [], content: '' };
|
|
116
|
+
} else if ((match = UPDATE_FILE_RE.exec(trimmed))) {
|
|
117
|
+
if (current) sections.push(current);
|
|
118
|
+
current = { type: 'update', filePath: match[1].trim(), lines: [] };
|
|
119
|
+
} else if ((match = DELETE_FILE_RE.exec(trimmed))) {
|
|
120
|
+
if (current) sections.push(current);
|
|
121
|
+
current = { type: 'delete', filePath: match[1].trim(), lines: [], content: '' };
|
|
122
|
+
} else if (current) {
|
|
123
|
+
current.lines.push(line);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (current) sections.push(current);
|
|
128
|
+
|
|
129
|
+
for (const section of sections) {
|
|
130
|
+
if (section.type === 'add' || section.type === 'delete') {
|
|
131
|
+
section.content = section.lines.join('\n').trim();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return sections;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function parseHunks(lines) {
|
|
139
|
+
const hunks = [];
|
|
140
|
+
let inHunk = false;
|
|
141
|
+
let oldLines = [];
|
|
142
|
+
let newLines = [];
|
|
143
|
+
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
if (HUNK_HEADER_RE.test(line.trim())) {
|
|
146
|
+
if (inHunk && (oldLines.length > 0 || newLines.length > 0)) {
|
|
147
|
+
hunks.push(buildHunk(oldLines, newLines));
|
|
148
|
+
}
|
|
149
|
+
inHunk = true;
|
|
150
|
+
oldLines = [];
|
|
151
|
+
newLines = [];
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!inHunk && (line.startsWith('-') || line.startsWith('+') || line.startsWith(' '))) {
|
|
156
|
+
inHunk = true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!inHunk) continue;
|
|
160
|
+
|
|
161
|
+
if (line.startsWith('-')) {
|
|
162
|
+
oldLines.push(line.slice(1));
|
|
163
|
+
} else if (line.startsWith('+')) {
|
|
164
|
+
newLines.push(line.slice(1));
|
|
165
|
+
} else if (line.startsWith(' ')) {
|
|
166
|
+
oldLines.push(line.slice(1));
|
|
167
|
+
newLines.push(line.slice(1));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (inHunk && (oldLines.length > 0 || newLines.length > 0)) {
|
|
172
|
+
hunks.push(buildHunk(oldLines, newLines));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return hunks;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildHunk(oldLines, newLines) {
|
|
179
|
+
return {
|
|
180
|
+
oldString: oldLines.join('\n'),
|
|
181
|
+
newString: newLines.join('\n'),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function estimateTokens(text) {
|
|
186
|
+
if (!text) return 0;
|
|
187
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
188
|
+
}
|
|
@@ -7,7 +7,7 @@ export class EnvelopeBuilder {
|
|
|
7
7
|
constructor(sessionId, contributorId, metadata) {
|
|
8
8
|
this._sessionId = sessionId;
|
|
9
9
|
this._contributorId = contributorId;
|
|
10
|
-
this._metadata = metadata;
|
|
10
|
+
this._metadata = { ...metadata, leaf_context: metadata.leaf_context ?? null };
|
|
11
11
|
this._buffer = [];
|
|
12
12
|
this._chunkSequence = 0;
|
|
13
13
|
}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { OBSERVATION_TOKEN_LIMIT } from '../../shared/constants.js';
|
|
4
|
+
|
|
5
|
+
function estimateTokens(text) {
|
|
6
|
+
if (!text) return 0;
|
|
7
|
+
return Math.ceil(text.length / 4);
|
|
8
|
+
}
|
|
4
9
|
|
|
5
10
|
function truncateObservation(text) {
|
|
6
|
-
if (!text || typeof text !== 'string') return text;
|
|
7
|
-
const
|
|
8
|
-
if (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
11
|
+
if (!text || typeof text !== 'string') return { content: text, truncated: false, original_token_count: estimateTokens(text) };
|
|
12
|
+
const originalTokens = estimateTokens(text);
|
|
13
|
+
if (originalTokens <= OBSERVATION_TOKEN_LIMIT) {
|
|
14
|
+
return { content: text, truncated: false, original_token_count: originalTokens };
|
|
15
|
+
}
|
|
16
|
+
const charLimit = OBSERVATION_TOKEN_LIMIT * 4;
|
|
17
|
+
const truncated = text.slice(0, charLimit) + `\n[TRUNCATED — original output was ${originalTokens} tokens]`;
|
|
18
|
+
return { content: truncated, truncated: true, original_token_count: originalTokens };
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
export class ClaudeCodeParser {
|
|
@@ -50,10 +56,51 @@ export class ClaudeCodeParser {
|
|
|
50
56
|
if (block.is_error) {
|
|
51
57
|
results.push({ type: 'error', content: resultContent, is_error: true });
|
|
52
58
|
} else {
|
|
59
|
+
const obs = truncateObservation(resultContent);
|
|
60
|
+
results.push({
|
|
61
|
+
type: 'observation',
|
|
62
|
+
content: obs.content,
|
|
63
|
+
truncated: obs.truncated,
|
|
64
|
+
original_token_count: obs.original_token_count,
|
|
65
|
+
is_error: false,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return results.length === 1 ? results[0] : results.length > 1 ? results : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (jsonEvent.type === 'user') {
|
|
75
|
+
const contentBlocks = jsonEvent.message?.content;
|
|
76
|
+
if (!Array.isArray(contentBlocks)) return null;
|
|
77
|
+
|
|
78
|
+
const results = [];
|
|
79
|
+
for (const block of contentBlocks) {
|
|
80
|
+
if (block.type === 'tool_result') {
|
|
81
|
+
const toolUse = this._pendingToolUse.get(block.tool_use_id);
|
|
82
|
+
if (toolUse) this._pendingToolUse.delete(block.tool_use_id);
|
|
83
|
+
|
|
84
|
+
const resultContent = Array.isArray(block.content)
|
|
85
|
+
? block.content.map((c) => c.text || '').join('\n')
|
|
86
|
+
: (typeof block.content === 'string' ? block.content : '');
|
|
87
|
+
|
|
88
|
+
if (block.is_error) {
|
|
89
|
+
results.push({
|
|
90
|
+
type: 'error',
|
|
91
|
+
content: resultContent,
|
|
92
|
+
is_error: true,
|
|
93
|
+
tool: toolUse?.name,
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
const obs = truncateObservation(resultContent);
|
|
53
97
|
results.push({
|
|
54
98
|
type: 'observation',
|
|
55
|
-
content:
|
|
99
|
+
content: obs.content,
|
|
100
|
+
truncated: obs.truncated,
|
|
101
|
+
original_token_count: obs.original_token_count,
|
|
56
102
|
is_error: false,
|
|
103
|
+
tool: toolUse?.name,
|
|
57
104
|
});
|
|
58
105
|
}
|
|
59
106
|
}
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
|
|
3
|
+
import { OBSERVATION_TOKEN_LIMIT } from '../../shared/constants.js';
|
|
4
|
+
|
|
5
|
+
function estimateTokens(text) {
|
|
6
|
+
if (!text) return 0;
|
|
7
|
+
return Math.ceil(text.length / 4);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function truncateObservation(text) {
|
|
11
|
+
if (!text || typeof text !== 'string') return { content: text || '', truncated: false, original_token_count: estimateTokens(text) };
|
|
12
|
+
const originalTokens = estimateTokens(text);
|
|
13
|
+
if (originalTokens <= OBSERVATION_TOKEN_LIMIT) {
|
|
14
|
+
return { content: text, truncated: false, original_token_count: originalTokens };
|
|
15
|
+
}
|
|
16
|
+
const charLimit = OBSERVATION_TOKEN_LIMIT * 4;
|
|
17
|
+
const truncated = text.slice(0, charLimit) + `\n[TRUNCATED — original output was ${originalTokens} tokens]`;
|
|
18
|
+
return { content: truncated, truncated: true, original_token_count: originalTokens };
|
|
19
|
+
}
|
|
20
|
+
|
|
3
21
|
export class CodexParser {
|
|
4
22
|
constructor() {
|
|
5
23
|
this._sessionId = null;
|
|
@@ -37,15 +55,17 @@ export class CodexParser {
|
|
|
37
55
|
return { type: 'thought', content: item.text || '' };
|
|
38
56
|
}
|
|
39
57
|
if (item.type === 'command_execution') {
|
|
40
|
-
const
|
|
58
|
+
const rawOutput = item.aggregated_output || '';
|
|
41
59
|
if (item.exit_code !== 0) {
|
|
42
|
-
return { type: 'error', content:
|
|
60
|
+
return { type: 'error', content: rawOutput.slice(0, 2000) || `Exit code: ${item.exit_code}` };
|
|
43
61
|
}
|
|
44
|
-
|
|
62
|
+
const obs = truncateObservation(rawOutput);
|
|
63
|
+
return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
45
64
|
}
|
|
46
65
|
if (item.type === 'file_edit' || item.type === 'file_write' || item.type === 'file_read') {
|
|
47
|
-
const
|
|
48
|
-
|
|
66
|
+
const rawOutput = item.output || item.content || '';
|
|
67
|
+
const obs = truncateObservation(rawOutput);
|
|
68
|
+
return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
49
69
|
}
|
|
50
70
|
return null;
|
|
51
71
|
}
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
|
|
3
|
+
import { OBSERVATION_TOKEN_LIMIT } from '../../shared/constants.js';
|
|
4
|
+
|
|
5
|
+
function estimateTokens(text) {
|
|
6
|
+
if (!text) return 0;
|
|
7
|
+
return Math.ceil(text.length / 4);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function truncateObservation(text) {
|
|
11
|
+
if (!text || typeof text !== 'string') return { content: text || '', truncated: false, original_token_count: estimateTokens(text) };
|
|
12
|
+
const originalTokens = estimateTokens(text);
|
|
13
|
+
if (originalTokens <= OBSERVATION_TOKEN_LIMIT) {
|
|
14
|
+
return { content: text, truncated: false, original_token_count: originalTokens };
|
|
15
|
+
}
|
|
16
|
+
const charLimit = OBSERVATION_TOKEN_LIMIT * 4;
|
|
17
|
+
const truncated = text.slice(0, charLimit) + `\n[TRUNCATED — original output was ${originalTokens} tokens]`;
|
|
18
|
+
return { content: truncated, truncated: true, original_token_count: originalTokens };
|
|
19
|
+
}
|
|
20
|
+
|
|
3
21
|
export class GeminiParser {
|
|
4
22
|
constructor() {
|
|
5
23
|
this._sessionId = null;
|
|
@@ -44,8 +62,9 @@ export class GeminiParser {
|
|
|
44
62
|
case 'tool_response': {
|
|
45
63
|
const rawContent = jsonEvent.content;
|
|
46
64
|
const contentParts = Array.isArray(rawContent) ? rawContent : (typeof rawContent === 'string' ? [{ text: rawContent }] : rawContent ? [rawContent] : []);
|
|
47
|
-
const
|
|
48
|
-
|
|
65
|
+
const rawText = contentParts.map((p) => p.text || '').join('');
|
|
66
|
+
const obs = truncateObservation(rawText);
|
|
67
|
+
return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
49
68
|
}
|
|
50
69
|
|
|
51
70
|
case 'error': {
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
|
|
3
|
+
import { OBSERVATION_TOKEN_LIMIT } from '../../shared/constants.js';
|
|
4
|
+
|
|
5
|
+
function estimateTokens(text) {
|
|
6
|
+
if (!text) return 0;
|
|
7
|
+
return Math.ceil(text.length / 4);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function truncateObservation(text) {
|
|
11
|
+
if (!text || typeof text !== 'string') return { content: text || '', truncated: false, original_token_count: estimateTokens(text) };
|
|
12
|
+
const originalTokens = estimateTokens(text);
|
|
13
|
+
if (originalTokens <= OBSERVATION_TOKEN_LIMIT) {
|
|
14
|
+
return { content: text, truncated: false, original_token_count: originalTokens };
|
|
15
|
+
}
|
|
16
|
+
const charLimit = OBSERVATION_TOKEN_LIMIT * 4;
|
|
17
|
+
const truncated = text.slice(0, charLimit) + `\n[TRUNCATED — original output was ${originalTokens} tokens]`;
|
|
18
|
+
return { content: truncated, truncated: true, original_token_count: originalTokens };
|
|
19
|
+
}
|
|
20
|
+
|
|
3
21
|
export class GrokParser {
|
|
4
22
|
// TODO: Grok agentic CLI not yet available. Wire up when headless CLI is built.
|
|
5
23
|
parseEvent(_jsonEvent) {
|