groove-dev 0.27.102 → 0.27.104
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/CLAUDE.md +0 -7
- 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/index.js +1 -0
- 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/step-classifier.js +1 -1
- package/moe-training/client/trajectory-capture.js +109 -8
- 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/step-classifier.test.js +42 -0
- package/moe-training/test/client/trajectory-capture.test.js +440 -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/api.js +19 -3
- package/node_modules/@groove-dev/gui/dist/assets/{index-8gdXdRnq.js → index-oUBAPJv6.js} +15 -15
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/settings/ProviderSetupWizard.jsx +28 -2
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +23 -2
- 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/index.js +1 -0
- 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/step-classifier.js +1 -1
- package/node_modules/moe-training/client/trajectory-capture.js +109 -8
- 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/step-classifier.test.js +42 -0
- package/node_modules/moe-training/test/client/trajectory-capture.test.js +440 -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/api.js +19 -3
- package/packages/gui/dist/assets/{index-8gdXdRnq.js → index-oUBAPJv6.js} +15 -15
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/settings/ProviderSetupWizard.jsx +28 -2
- package/packages/gui/src/views/settings.jsx +23 -2
|
@@ -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) {
|
|
@@ -38,7 +38,7 @@ export class StepClassifier {
|
|
|
38
38
|
|
|
39
39
|
const content = step.content || '';
|
|
40
40
|
|
|
41
|
-
if ((step.type === 'action' || step.type === 'observation') && ERROR_SIGNAL_RE.test(content)) {
|
|
41
|
+
if ((step.type === 'action' || step.type === 'observation') && step.is_error !== false && ERROR_SIGNAL_RE.test(content)) {
|
|
42
42
|
step.type = 'error';
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -8,7 +8,17 @@ import { StepClassifier } from './step-classifier.js';
|
|
|
8
8
|
import { EnvelopeBuilder } from './envelope-builder.js';
|
|
9
9
|
import { SessionAttestation } from './session-attestation.js';
|
|
10
10
|
import { TransmissionQueue } from './transmission-queue.js';
|
|
11
|
-
import {
|
|
11
|
+
import { DomainTagger } from './domain-tagger.js';
|
|
12
|
+
import {
|
|
13
|
+
CHUNK_TIMEOUT_MS,
|
|
14
|
+
CENTRAL_COMMAND_URL,
|
|
15
|
+
TIER_A_MIN_QUALITY,
|
|
16
|
+
TIER_B_MIN_QUALITY,
|
|
17
|
+
TRAINING_MIN_STEPS,
|
|
18
|
+
TRAINING_MIN_TOKENS,
|
|
19
|
+
TRAINING_MIN_DURATION,
|
|
20
|
+
TRAINING_EXCLUSION_REASONS,
|
|
21
|
+
} from '../shared/constants.js';
|
|
12
22
|
|
|
13
23
|
const OFFLINE_RETRY_INTERVAL_MS = 60_000;
|
|
14
24
|
|
|
@@ -24,7 +34,7 @@ export class TrajectoryCapture {
|
|
|
24
34
|
this._contexts = new Map();
|
|
25
35
|
}
|
|
26
36
|
|
|
27
|
-
init() {
|
|
37
|
+
async init() {
|
|
28
38
|
if (!ConsentManager.isCaptureEnabled()) {
|
|
29
39
|
this._enabled = false;
|
|
30
40
|
return;
|
|
@@ -34,6 +44,8 @@ export class TrajectoryCapture {
|
|
|
34
44
|
this._attestation = new SessionAttestation(this._centralCommandUrl);
|
|
35
45
|
this._transmissionQueue = new TransmissionQueue(this._centralCommandUrl);
|
|
36
46
|
this._transmissionQueue.start();
|
|
47
|
+
this._domainTagger = new DomainTagger();
|
|
48
|
+
await this._domainTagger.init();
|
|
37
49
|
this._offlineRetryTimer = setInterval(() => {
|
|
38
50
|
this._retryOfflineQueue();
|
|
39
51
|
}, OFFLINE_RETRY_INTERVAL_MS);
|
|
@@ -56,6 +68,7 @@ export class TrajectoryCapture {
|
|
|
56
68
|
team_size: teamSize || 1,
|
|
57
69
|
session_quality: 0,
|
|
58
70
|
groove_version: this._grooveVersion,
|
|
71
|
+
leaf_context: null,
|
|
59
72
|
};
|
|
60
73
|
|
|
61
74
|
const builder = new EnvelopeBuilder(sessionId, contributorId, metadata);
|
|
@@ -78,6 +91,7 @@ export class TrajectoryCapture {
|
|
|
78
91
|
startTime,
|
|
79
92
|
chunkTimer: null,
|
|
80
93
|
allSteps: [],
|
|
94
|
+
revisionRounds: 0,
|
|
81
95
|
};
|
|
82
96
|
|
|
83
97
|
ctx.chunkTimer = setInterval(() => {
|
|
@@ -115,10 +129,6 @@ export class TrajectoryCapture {
|
|
|
115
129
|
if (resolved) ctx.metadata.model_engine = resolved;
|
|
116
130
|
}
|
|
117
131
|
|
|
118
|
-
const tokens = ctx.parser.extractTokens(jsonEvent);
|
|
119
|
-
if (tokens) {
|
|
120
|
-
ctx.totalTokens += (tokens.input || 0) + (tokens.output || 0);
|
|
121
|
-
}
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
onUserMessage(agentId, text) {
|
|
@@ -126,6 +136,8 @@ export class TrajectoryCapture {
|
|
|
126
136
|
const ctx = this._contexts.get(agentId);
|
|
127
137
|
if (!ctx) return;
|
|
128
138
|
|
|
139
|
+
ctx.revisionRounds++;
|
|
140
|
+
|
|
129
141
|
const classified = ctx.classifier.classifyUserMessage(text);
|
|
130
142
|
if (!classified) return;
|
|
131
143
|
|
|
@@ -185,6 +197,7 @@ export class TrajectoryCapture {
|
|
|
185
197
|
...ev,
|
|
186
198
|
};
|
|
187
199
|
|
|
200
|
+
ctx.totalTokens += ev.token_count;
|
|
188
201
|
if (ev.type === 'error') ctx.errorsEncountered++;
|
|
189
202
|
ctx.allSteps.push(step);
|
|
190
203
|
|
|
@@ -238,22 +251,42 @@ export class TrajectoryCapture {
|
|
|
238
251
|
|
|
239
252
|
ctx.metadata.session_quality = this._computeQuality(ctx);
|
|
240
253
|
|
|
254
|
+
const userInterventions = StepClassifier.countUserInterventions(ctx.allSteps);
|
|
255
|
+
const durationSeconds = Math.round((Date.now() - ctx.startTime) / 1000);
|
|
256
|
+
|
|
257
|
+
if (this._domainTagger) {
|
|
258
|
+
const role = ctx.metadata.agent_role || '';
|
|
259
|
+
const firstPrompt = ctx.allSteps.find((s) => s.type === 'thought')?.content || '';
|
|
260
|
+
const thoughtSteps = ctx.allSteps.filter((s) => s.type === 'thought');
|
|
261
|
+
const routingText = DomainTagger.buildRoutingText(role, firstPrompt, thoughtSteps);
|
|
262
|
+
ctx.metadata.domain_tags = await this._domainTagger.tag(routingText);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const { tier, reason: tierReason } = this._computeQualityTier(ctx, status, userInterventions);
|
|
266
|
+
const { eligible, exclusionReason } = this._computeTrainingEligibility(ctx, durationSeconds);
|
|
267
|
+
|
|
241
268
|
const closeEnvelope = ctx.builder.buildSessionClose({
|
|
242
269
|
status,
|
|
243
270
|
session_quality: ctx.metadata.session_quality,
|
|
244
|
-
|
|
271
|
+
quality_tier: tier,
|
|
272
|
+
quality_tier_reason: tierReason,
|
|
273
|
+
user_interventions: userInterventions,
|
|
245
274
|
total_steps: ctx.stepCount,
|
|
246
275
|
total_chunks: ctx.chunkCount,
|
|
247
276
|
total_tokens: ctx.totalTokens,
|
|
248
|
-
duration_seconds:
|
|
277
|
+
duration_seconds: durationSeconds,
|
|
249
278
|
files_modified: extra?.files_modified || ctx.filesModified,
|
|
250
279
|
errors_encountered: ctx.errorsEncountered,
|
|
251
280
|
errors_recovered: ctx.errorsRecovered,
|
|
252
281
|
coordination_events: ctx.coordinationEvents,
|
|
282
|
+
training_eligible: eligible,
|
|
283
|
+
training_exclusion_reason: exclusionReason,
|
|
253
284
|
});
|
|
254
285
|
|
|
255
286
|
this._signAndTransmit(ctx.sessionId, closeEnvelope);
|
|
256
287
|
|
|
288
|
+
this._emitUserFeedback(ctx, status, userInterventions);
|
|
289
|
+
|
|
257
290
|
try {
|
|
258
291
|
await this._transmissionQueue.waitForDrain();
|
|
259
292
|
} catch {
|
|
@@ -269,6 +302,74 @@ export class TrajectoryCapture {
|
|
|
269
302
|
this._contexts.delete(agentId);
|
|
270
303
|
}
|
|
271
304
|
|
|
305
|
+
_computeQualityTier(ctx, status, userInterventions) {
|
|
306
|
+
const quality = ctx.metadata.session_quality;
|
|
307
|
+
if (quality >= TIER_A_MIN_QUALITY && (ctx.errorsEncountered === 0 || ctx.errorsEncountered <= ctx.errorsRecovered) && userInterventions === 0 && status === 'SUCCESS') {
|
|
308
|
+
const reason = ctx.errorsEncountered > 0 ? 'high_quality_errors_recovered' : 'high_quality_no_errors';
|
|
309
|
+
return { tier: 'TIER_A', reason };
|
|
310
|
+
}
|
|
311
|
+
if (status !== 'SUCCESS') {
|
|
312
|
+
return { tier: 'TIER_C', reason: 'non_success_status' };
|
|
313
|
+
}
|
|
314
|
+
if (quality >= TIER_B_MIN_QUALITY || (ctx.errorsEncountered > 0 && ctx.errorsRecovered === ctx.errorsEncountered)) {
|
|
315
|
+
return { tier: 'TIER_B', reason: quality >= TIER_B_MIN_QUALITY ? 'moderate_quality' : 'errors_recovered' };
|
|
316
|
+
}
|
|
317
|
+
return { tier: 'TIER_C', reason: quality < TIER_B_MIN_QUALITY ? 'low_quality' : 'unrecovered_errors' };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
_computeTrainingEligibility(ctx, durationSeconds) {
|
|
321
|
+
if (ctx.stepCount < TRAINING_MIN_STEPS) {
|
|
322
|
+
return { eligible: false, exclusionReason: 'too_few_steps' };
|
|
323
|
+
}
|
|
324
|
+
const hasAction = ctx.allSteps.some((s) => s.type === 'action' && s.tool);
|
|
325
|
+
if (!hasAction) {
|
|
326
|
+
return { eligible: false, exclusionReason: 'no_actions' };
|
|
327
|
+
}
|
|
328
|
+
const hasObservation = ctx.allSteps.some((s) => s.type === 'observation' && s.content);
|
|
329
|
+
if (!hasObservation) {
|
|
330
|
+
return { eligible: false, exclusionReason: 'no_observations' };
|
|
331
|
+
}
|
|
332
|
+
if (ctx.totalTokens < TRAINING_MIN_TOKENS) {
|
|
333
|
+
return { eligible: false, exclusionReason: 'insufficient_tokens' };
|
|
334
|
+
}
|
|
335
|
+
if (durationSeconds < TRAINING_MIN_DURATION) {
|
|
336
|
+
return { eligible: false, exclusionReason: 'too_short' };
|
|
337
|
+
}
|
|
338
|
+
return { eligible: true, exclusionReason: null };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
_emitUserFeedback(ctx, status, userInterventions) {
|
|
342
|
+
let signal;
|
|
343
|
+
let context;
|
|
344
|
+
|
|
345
|
+
if (status === 'SUCCESS' && userInterventions === 0 && ctx.revisionRounds === 0) {
|
|
346
|
+
signal = 'accepted';
|
|
347
|
+
context = 'session completed successfully with no user interventions';
|
|
348
|
+
} else if (status === 'SUCCESS' && ctx.revisionRounds > 0) {
|
|
349
|
+
signal = 'iterated';
|
|
350
|
+
context = `user requested ${ctx.revisionRounds} revision(s) before completion`;
|
|
351
|
+
} else {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const feedbackEnvelope = {
|
|
356
|
+
envelope_id: `env_${randomUUID()}`,
|
|
357
|
+
session_id: ctx.sessionId,
|
|
358
|
+
type: 'USER_FEEDBACK',
|
|
359
|
+
attestation: { session_hmac: '', sequence: 0, app_version_hash: '' },
|
|
360
|
+
feedback: {
|
|
361
|
+
signal,
|
|
362
|
+
timestamp: Date.now() / 1000,
|
|
363
|
+
context,
|
|
364
|
+
target_step: ctx.stepCount,
|
|
365
|
+
revision_rounds: ctx.revisionRounds,
|
|
366
|
+
delta_summary: null,
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
this._signAndTransmit(ctx.sessionId, feedbackEnvelope);
|
|
371
|
+
}
|
|
372
|
+
|
|
272
373
|
async _retryOfflineQueue() {
|
|
273
374
|
if (!this._enabled || !this._transmissionQueue || this._transmissionQueue.offlineQueueSize === 0) return;
|
|
274
375
|
try {
|
|
@@ -15,6 +15,32 @@ export function createIngestRoutes(verifier, storage, stitcher, scorer, enrichme
|
|
|
15
15
|
return res.status(400).json({ accepted: false, reason: 'malformed request: missing envelope or session_id' });
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
if (envelope.type === 'USER_FEEDBACK') {
|
|
19
|
+
const feedbackResult = verifier.verifyFeedback(envelope);
|
|
20
|
+
if (!feedbackResult.valid) {
|
|
21
|
+
return res.json({ accepted: false, reason: feedbackResult.reason });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const envelopeId = `env_${uuidv4()}`;
|
|
25
|
+
envelope.envelope_id = envelopeId;
|
|
26
|
+
|
|
27
|
+
if (sessionRegistry.isEnvelopeProcessed(envelopeId)) {
|
|
28
|
+
return res.json({ accepted: false, reason: 'duplicate envelope' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
storage.store(envelope);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (err.message === 'STORAGE_QUOTA_EXCEEDED') {
|
|
35
|
+
return res.status(507).json({ accepted: false, reason: 'storage quota exceeded' });
|
|
36
|
+
}
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
sessionRegistry.recordProcessedEnvelope(envelopeId, envelope.session_id);
|
|
41
|
+
return res.json({ accepted: true, envelope_id: envelopeId });
|
|
42
|
+
}
|
|
43
|
+
|
|
18
44
|
if (envelope.type === 'SESSION_CLOSE') {
|
|
19
45
|
const closeResult = verifier.verifyClose(envelope);
|
|
20
46
|
if (!closeResult.valid) {
|
|
@@ -99,4 +99,38 @@ export class EnvelopeVerifier {
|
|
|
99
99
|
this.sessionRegistry.closeSession(sessionId);
|
|
100
100
|
return { valid: true };
|
|
101
101
|
}
|
|
102
|
+
|
|
103
|
+
verifyFeedback(envelope) {
|
|
104
|
+
const sessionId = envelope.session_id;
|
|
105
|
+
if (!sessionId) return { valid: false, reason: 'missing session_id' };
|
|
106
|
+
|
|
107
|
+
const session = this.sessionRegistry.getSession(sessionId);
|
|
108
|
+
if (!session) return { valid: false, reason: 'unknown session_id' };
|
|
109
|
+
|
|
110
|
+
const attestation = envelope.attestation;
|
|
111
|
+
if (!attestation) return { valid: false, reason: 'missing attestation' };
|
|
112
|
+
|
|
113
|
+
if (!attestation.session_hmac || typeof attestation.session_hmac !== 'string' || attestation.session_hmac.length === 0) {
|
|
114
|
+
return { valid: false, reason: 'empty or missing HMAC' };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const envelopeForHmac = { ...envelope };
|
|
118
|
+
delete envelopeForHmac.attestation;
|
|
119
|
+
const envelopeBytes = JSON.stringify(envelopeForHmac);
|
|
120
|
+
|
|
121
|
+
const hmacValid = verifyEnvelope(
|
|
122
|
+
session.shared_secret,
|
|
123
|
+
envelopeBytes,
|
|
124
|
+
attestation.sequence,
|
|
125
|
+
attestation.session_hmac
|
|
126
|
+
);
|
|
127
|
+
if (!hmacValid) return { valid: false, reason: 'HMAC verification failed' };
|
|
128
|
+
|
|
129
|
+
const schemaResult = validateEnvelope(envelope);
|
|
130
|
+
if (!schemaResult.valid) {
|
|
131
|
+
return { valid: false, reason: `schema validation failed: ${schemaResult.reason || schemaResult.errors?.join(', ')}` };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { valid: true };
|
|
135
|
+
}
|
|
102
136
|
}
|
|
@@ -13,6 +13,7 @@ export const MODEL_TIERS = {
|
|
|
13
13
|
'claude-opus-4-7': 5,
|
|
14
14
|
'claude-sonnet-4-6': 3,
|
|
15
15
|
'gpt-4.5': 5,
|
|
16
|
+
'gpt-5.5': 5,
|
|
16
17
|
'o3': 5,
|
|
17
18
|
'o4-mini': 2,
|
|
18
19
|
'gemini-2.5-pro': 3,
|
|
@@ -27,4 +28,12 @@ export const QUALITY_MULTIPLIERS = {
|
|
|
27
28
|
highQuality: 1.5,
|
|
28
29
|
};
|
|
29
30
|
|
|
31
|
+
export const OBSERVATION_TOKEN_LIMIT = 4096;
|
|
32
|
+
export const TIER_A_MIN_QUALITY = 70;
|
|
33
|
+
export const TIER_B_MIN_QUALITY = 50;
|
|
34
|
+
export const TRAINING_MIN_STEPS = 5;
|
|
35
|
+
export const TRAINING_MIN_TOKENS = 500;
|
|
36
|
+
export const TRAINING_MIN_DURATION = 10;
|
|
37
|
+
export const TRAINING_EXCLUSION_REASONS = ['too_few_steps', 'no_actions', 'no_observations', 'insufficient_tokens', 'too_short'];
|
|
38
|
+
|
|
30
39
|
export const CENTRAL_COMMAND_URL = process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai';
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
|
|
3
|
-
import { SUPPORTED_PROVIDERS, MODEL_TIERS } from './constants.js';
|
|
3
|
+
import { SUPPORTED_PROVIDERS, MODEL_TIERS, TRAINING_EXCLUSION_REASONS } from './constants.js';
|
|
4
4
|
|
|
5
|
-
export const STEP_TYPES = ['thought', 'action', 'observation', 'correction', 'resolution', 'error', 'coordination'];
|
|
5
|
+
export const STEP_TYPES = ['thought', 'action', 'observation', 'correction', 'resolution', 'error', 'coordination', 'edit'];
|
|
6
|
+
const VALID_QUALITY_TIERS = ['TIER_A', 'TIER_B', 'TIER_C'];
|
|
7
|
+
const VALID_FEEDBACK_SIGNALS = ['accepted', 'modified', 'rejected', 'iterated'];
|
|
6
8
|
|
|
7
9
|
const VALID_MODEL_ENGINES = Object.keys(MODEL_TIERS);
|
|
8
10
|
const VALID_COMPLEXITIES = ['light', 'medium', 'heavy'];
|
|
@@ -61,6 +63,10 @@ export function validateEnvelope(envelope) {
|
|
|
61
63
|
return validateSessionClose(envelope);
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
if (envelope.type === 'USER_FEEDBACK') {
|
|
67
|
+
return validateUserFeedback(envelope);
|
|
68
|
+
}
|
|
69
|
+
|
|
64
70
|
if (!envelope.session_id || typeof envelope.session_id !== 'string') {
|
|
65
71
|
errors.push('Missing or invalid session_id');
|
|
66
72
|
}
|
|
@@ -123,6 +129,43 @@ export function validateEnvelope(envelope) {
|
|
|
123
129
|
}
|
|
124
130
|
}
|
|
125
131
|
// session_quality is ignored from client — server derives quality
|
|
132
|
+
if (m.domain_tags !== undefined && m.domain_tags !== null) {
|
|
133
|
+
if (typeof m.domain_tags !== 'object') {
|
|
134
|
+
errors.push('metadata.domain_tags must be an object or null');
|
|
135
|
+
} else {
|
|
136
|
+
for (const level of ['primary', 'secondary', 'tertiary']) {
|
|
137
|
+
const tag = m.domain_tags[level];
|
|
138
|
+
if (!tag || typeof tag !== 'object') {
|
|
139
|
+
errors.push(`metadata.domain_tags.${level} must be an object`);
|
|
140
|
+
} else {
|
|
141
|
+
if (typeof tag.domain !== 'string' || tag.domain.length === 0) {
|
|
142
|
+
errors.push(`metadata.domain_tags.${level}.domain must be a non-empty string`);
|
|
143
|
+
}
|
|
144
|
+
if (typeof tag.confidence !== 'number' || tag.confidence < 0 || tag.confidence > 1) {
|
|
145
|
+
errors.push(`metadata.domain_tags.${level}.confidence must be a number 0-1`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (m.leaf_context !== undefined && m.leaf_context !== null) {
|
|
152
|
+
if (typeof m.leaf_context !== 'object') {
|
|
153
|
+
errors.push('metadata.leaf_context must be an object or null');
|
|
154
|
+
} else {
|
|
155
|
+
if (!m.leaf_context.leaf_id || typeof m.leaf_context.leaf_id !== 'string') {
|
|
156
|
+
errors.push('metadata.leaf_context.leaf_id must be a non-empty string');
|
|
157
|
+
}
|
|
158
|
+
if (!m.leaf_context.leaf_version || typeof m.leaf_context.leaf_version !== 'string') {
|
|
159
|
+
errors.push('metadata.leaf_context.leaf_version must be a non-empty string');
|
|
160
|
+
}
|
|
161
|
+
if (typeof m.leaf_context.confidence_at_route !== 'number' || m.leaf_context.confidence_at_route < 0 || m.leaf_context.confidence_at_route > 1) {
|
|
162
|
+
errors.push('metadata.leaf_context.confidence_at_route must be a number 0-1');
|
|
163
|
+
}
|
|
164
|
+
if (!m.leaf_context.chassis_model || typeof m.leaf_context.chassis_model !== 'string') {
|
|
165
|
+
errors.push('metadata.leaf_context.chassis_model must be a non-empty string');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
126
169
|
}
|
|
127
170
|
|
|
128
171
|
// Trajectory log validation
|
|
@@ -161,6 +204,14 @@ export function validateEnvelope(envelope) {
|
|
|
161
204
|
errors.push(`step.token_count must be 0-${MAX_TOKEN_COUNT} at index ${i}`);
|
|
162
205
|
}
|
|
163
206
|
}
|
|
207
|
+
if (step.truncated !== undefined && typeof step.truncated !== 'boolean') {
|
|
208
|
+
errors.push(`step.truncated must be a boolean at index ${i}`);
|
|
209
|
+
}
|
|
210
|
+
if (step.original_token_count !== undefined) {
|
|
211
|
+
if (typeof step.original_token_count !== 'number' || step.original_token_count < 0 || step.original_token_count > MAX_TOKEN_COUNT) {
|
|
212
|
+
errors.push(`step.original_token_count must be 0-${MAX_TOKEN_COUNT} at index ${i}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
164
215
|
}
|
|
165
216
|
}
|
|
166
217
|
|
|
@@ -214,6 +265,81 @@ function validateSessionClose(envelope) {
|
|
|
214
265
|
}
|
|
215
266
|
}
|
|
216
267
|
}
|
|
268
|
+
if (envelope.outcome.quality_tier !== undefined && envelope.outcome.quality_tier !== null) {
|
|
269
|
+
if (!VALID_QUALITY_TIERS.includes(envelope.outcome.quality_tier)) {
|
|
270
|
+
errors.push(`outcome.quality_tier must be one of: ${VALID_QUALITY_TIERS.join(', ')}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (envelope.outcome.quality_tier_reason !== undefined && envelope.outcome.quality_tier_reason !== null) {
|
|
274
|
+
if (typeof envelope.outcome.quality_tier_reason !== 'string' || envelope.outcome.quality_tier_reason.length > 200) {
|
|
275
|
+
errors.push('outcome.quality_tier_reason must be a string, max 200 characters');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (envelope.outcome.training_eligible !== undefined && envelope.outcome.training_eligible !== null) {
|
|
279
|
+
if (typeof envelope.outcome.training_eligible !== 'boolean') {
|
|
280
|
+
errors.push('outcome.training_eligible must be a boolean');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (envelope.outcome.training_exclusion_reason !== undefined && envelope.outcome.training_exclusion_reason !== null) {
|
|
284
|
+
if (!TRAINING_EXCLUSION_REASONS.includes(envelope.outcome.training_exclusion_reason)) {
|
|
285
|
+
errors.push(`outcome.training_exclusion_reason must be one of: ${TRAINING_EXCLUSION_REASONS.join(', ')}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { valid: errors.length === 0, errors };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function validateUserFeedback(envelope) {
|
|
294
|
+
const errors = [];
|
|
295
|
+
|
|
296
|
+
if (!envelope.session_id || typeof envelope.session_id !== 'string') {
|
|
297
|
+
errors.push('Missing or invalid session_id');
|
|
298
|
+
}
|
|
299
|
+
if (envelope.type !== 'USER_FEEDBACK') {
|
|
300
|
+
errors.push('Invalid type for USER_FEEDBACK envelope');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!envelope.attestation || typeof envelope.attestation !== 'object') {
|
|
304
|
+
errors.push('Missing attestation');
|
|
305
|
+
} else {
|
|
306
|
+
if (typeof envelope.attestation.session_hmac !== 'string' || !HEX_64.test(envelope.attestation.session_hmac)) {
|
|
307
|
+
errors.push('attestation.session_hmac must be exactly 64 hex characters');
|
|
308
|
+
}
|
|
309
|
+
if (typeof envelope.attestation.sequence !== 'number' || !Number.isInteger(envelope.attestation.sequence) || envelope.attestation.sequence < 0 || envelope.attestation.sequence > 1_000_000) {
|
|
310
|
+
errors.push('attestation.sequence must be a non-negative integer, max 1000000');
|
|
311
|
+
}
|
|
312
|
+
if (typeof envelope.attestation.app_version_hash !== 'string' || !HEX_64.test(envelope.attestation.app_version_hash)) {
|
|
313
|
+
errors.push('attestation.app_version_hash must be exactly 64 hex characters');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!envelope.feedback || typeof envelope.feedback !== 'object') {
|
|
318
|
+
errors.push('Missing feedback');
|
|
319
|
+
} else {
|
|
320
|
+
const f = envelope.feedback;
|
|
321
|
+
if (!VALID_FEEDBACK_SIGNALS.includes(f.signal)) {
|
|
322
|
+
errors.push(`feedback.signal must be one of: ${VALID_FEEDBACK_SIGNALS.join(', ')}`);
|
|
323
|
+
}
|
|
324
|
+
if (typeof f.timestamp !== 'number') {
|
|
325
|
+
errors.push('feedback.timestamp must be a number');
|
|
326
|
+
}
|
|
327
|
+
if (f.context !== undefined && f.context !== null && typeof f.context !== 'string') {
|
|
328
|
+
errors.push('feedback.context must be a string or null');
|
|
329
|
+
}
|
|
330
|
+
if (f.target_step !== undefined && f.target_step !== null) {
|
|
331
|
+
if (typeof f.target_step !== 'number' || !Number.isInteger(f.target_step) || f.target_step < 0) {
|
|
332
|
+
errors.push('feedback.target_step must be a non-negative integer');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (f.revision_rounds !== undefined && f.revision_rounds !== null) {
|
|
336
|
+
if (typeof f.revision_rounds !== 'number' || !Number.isInteger(f.revision_rounds) || f.revision_rounds < 0) {
|
|
337
|
+
errors.push('feedback.revision_rounds must be a non-negative integer');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (f.delta_summary !== undefined && f.delta_summary !== null && typeof f.delta_summary !== 'string') {
|
|
341
|
+
errors.push('feedback.delta_summary must be a string or null');
|
|
342
|
+
}
|
|
217
343
|
}
|
|
218
344
|
|
|
219
345
|
return { valid: errors.length === 0, errors };
|