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.
Files changed (59) hide show
  1. package/moe-training/client/domain-tagger.js +205 -0
  2. package/moe-training/client/edit-normalizer.js +188 -0
  3. package/moe-training/client/envelope-builder.js +1 -1
  4. package/moe-training/client/parsers/claude-code.js +56 -9
  5. package/moe-training/client/parsers/codex.js +25 -5
  6. package/moe-training/client/parsers/gemini.js +21 -2
  7. package/moe-training/client/parsers/grok.js +18 -0
  8. package/moe-training/client/trajectory-capture.js +95 -3
  9. package/moe-training/server/routes/ingest.js +26 -0
  10. package/moe-training/server/verifier.js +34 -0
  11. package/moe-training/shared/constants.js +9 -0
  12. package/moe-training/shared/envelope-schema.js +128 -2
  13. package/moe-training/test/client/domain-tagger.test.js +203 -0
  14. package/moe-training/test/client/edit-normalizer.test.js +376 -0
  15. package/moe-training/test/client/envelope-builder.test.js +28 -0
  16. package/moe-training/test/client/parsers/claude-code.test.js +248 -38
  17. package/moe-training/test/client/parsers/codex.test.js +2 -0
  18. package/moe-training/test/client/parsers/gemini.test.js +2 -0
  19. package/moe-training/test/client/trajectory-capture.test.js +345 -0
  20. package/moe-training/test/server/verifier.test.js +94 -0
  21. package/moe-training/test/shared/envelope-schema.test.js +291 -0
  22. package/node_modules/@groove-dev/cli/package.json +1 -1
  23. package/node_modules/@groove-dev/daemon/package.json +1 -1
  24. package/node_modules/@groove-dev/daemon/src/preview.js +148 -2
  25. package/node_modules/@groove-dev/gui/package.json +1 -1
  26. package/node_modules/moe-training/client/domain-tagger.js +205 -0
  27. package/node_modules/moe-training/client/edit-normalizer.js +188 -0
  28. package/node_modules/moe-training/client/envelope-builder.js +1 -1
  29. package/node_modules/moe-training/client/parsers/claude-code.js +56 -9
  30. package/node_modules/moe-training/client/parsers/codex.js +25 -5
  31. package/node_modules/moe-training/client/parsers/gemini.js +21 -2
  32. package/node_modules/moe-training/client/parsers/grok.js +18 -0
  33. package/node_modules/moe-training/client/trajectory-capture.js +95 -3
  34. package/node_modules/moe-training/server/routes/ingest.js +26 -0
  35. package/node_modules/moe-training/server/verifier.js +34 -0
  36. package/node_modules/moe-training/shared/constants.js +9 -0
  37. package/node_modules/moe-training/shared/envelope-schema.js +128 -2
  38. package/node_modules/moe-training/test/client/domain-tagger.test.js +203 -0
  39. package/node_modules/moe-training/test/client/edit-normalizer.test.js +376 -0
  40. package/node_modules/moe-training/test/client/envelope-builder.test.js +28 -0
  41. package/node_modules/moe-training/test/client/parsers/claude-code.test.js +248 -38
  42. package/node_modules/moe-training/test/client/parsers/codex.test.js +2 -0
  43. package/node_modules/moe-training/test/client/parsers/gemini.test.js +2 -0
  44. package/node_modules/moe-training/test/client/trajectory-capture.test.js +345 -0
  45. package/node_modules/moe-training/test/server/verifier.test.js +94 -0
  46. package/node_modules/moe-training/test/shared/envelope-schema.test.js +291 -0
  47. package/package.json +1 -1
  48. package/packages/cli/package.json +1 -1
  49. package/packages/daemon/package.json +1 -1
  50. package/packages/daemon/src/preview.js +148 -2
  51. package/packages/gui/package.json +1 -1
  52. package/packages/launch-page/dist/assets/index-Bo186ysq.js +4180 -0
  53. package/packages/launch-page/dist/assets/index-CP4c4yxe.css +1 -0
  54. package/packages/launch-page/dist/index.html +2 -2
  55. package/packages/launch-page/src/App.css +438 -137
  56. package/packages/launch-page/src/App.tsx +171 -123
  57. package/packages/launch-page/src/index.css +9 -2
  58. package/packages/launch-page/dist/assets/index-BK3nAvHG.js +0 -4180
  59. 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 { OBSERVATION_TRUNCATE_HEAD, OBSERVATION_TRUNCATE_TAIL } from '../../shared/constants.js';
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 lines = text.split('\n');
8
- if (lines.length <= OBSERVATION_TRUNCATE_HEAD + OBSERVATION_TRUNCATE_TAIL) return text;
9
- const head = lines.slice(0, OBSERVATION_TRUNCATE_HEAD);
10
- const tail = lines.slice(-OBSERVATION_TRUNCATE_TAIL);
11
- const omitted = lines.length - OBSERVATION_TRUNCATE_HEAD - OBSERVATION_TRUNCATE_TAIL;
12
- return [...head, `[... ${omitted} lines omitted ...]`, ...tail].join('\n');
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: truncateObservation(resultContent),
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 output = (item.aggregated_output || '').slice(0, 2000);
58
+ const rawOutput = item.aggregated_output || '';
41
59
  if (item.exit_code !== 0) {
42
- return { type: 'error', content: output || `Exit code: ${item.exit_code}` };
60
+ return { type: 'error', content: rawOutput.slice(0, 2000) || `Exit code: ${item.exit_code}` };
43
61
  }
44
- return { type: 'observation', content: output };
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 output = (item.output || item.content || '').slice(0, 2000);
48
- return { type: 'observation', content: output };
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 content = contentParts.map((p) => p.text || '').join('').slice(0, 2000);
48
- return { type: 'observation', content };
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) {