98 lines
2.8 KiB
TypeScript
98 lines
2.8 KiB
TypeScript
import { randomBytes } from 'crypto';
|
|
|
|
import {
|
|
CLAUDE_SERVICE_ID,
|
|
CODEX_REVIEW_SERVICE_ID,
|
|
normalizeServiceId,
|
|
} from './config.js';
|
|
import type { StructuredAgentOutput } from './types.js';
|
|
|
|
const ANY_SUPPRESS_TOKEN_PATTERN = /__EJ_SUPPRESS_[a-f0-9]{24,}(?:__)?/g;
|
|
const EXACT_ANY_SUPPRESS_TOKEN_PATTERN = /^__EJ_SUPPRESS_[a-f0-9]{24,}(?:__)?$/;
|
|
export const STRUCTURED_SILENT_OUTPUT_ENVELOPE =
|
|
'{"ejclaw":{"visibility":"silent"}}';
|
|
|
|
export function createSuppressToken(): string {
|
|
return `__EJ_SUPPRESS_${randomBytes(12).toString('hex')}__`;
|
|
}
|
|
|
|
export function shouldEnableSuppressOutputForService(
|
|
serviceId: string | undefined,
|
|
): boolean {
|
|
if (!serviceId) return false;
|
|
const normalized = normalizeServiceId(serviceId);
|
|
return (
|
|
normalized === CLAUDE_SERVICE_ID || normalized === CODEX_REVIEW_SERVICE_ID
|
|
);
|
|
}
|
|
|
|
export function classifySuppressTokenOutput(
|
|
rawText: string,
|
|
suppressToken: string | undefined,
|
|
): 'exact' | 'mixed' | 'none' {
|
|
const trimmed = rawText.trim();
|
|
const structured = parseStructuredOutputEnvelope(trimmed);
|
|
if (
|
|
(suppressToken && trimmed === suppressToken) ||
|
|
EXACT_ANY_SUPPRESS_TOKEN_PATTERN.test(trimmed) ||
|
|
structured?.visibility === 'silent'
|
|
) {
|
|
return 'exact';
|
|
}
|
|
if (suppressToken && rawText.includes(suppressToken)) {
|
|
return 'mixed';
|
|
}
|
|
ANY_SUPPRESS_TOKEN_PATTERN.lastIndex = 0;
|
|
return ANY_SUPPRESS_TOKEN_PATTERN.test(rawText) ? 'mixed' : 'none';
|
|
}
|
|
|
|
export function parseStructuredOutputEnvelope(
|
|
rawText: string,
|
|
): StructuredAgentOutput | null {
|
|
try {
|
|
const parsed = JSON.parse(rawText) as {
|
|
ejclaw?: { visibility?: unknown; text?: unknown };
|
|
};
|
|
const envelope = parsed?.ejclaw;
|
|
if (!envelope || typeof envelope !== 'object' || Array.isArray(envelope)) {
|
|
return null;
|
|
}
|
|
if (envelope.visibility === 'silent') {
|
|
return { visibility: 'silent' };
|
|
}
|
|
if (
|
|
envelope.visibility === 'public' &&
|
|
typeof envelope.text === 'string' &&
|
|
envelope.text.length > 0
|
|
) {
|
|
return { visibility: 'public', text: envelope.text };
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function buildStructuredOutputPrompt(
|
|
prompt: string,
|
|
options?: {
|
|
reviewerMode?: boolean;
|
|
},
|
|
): string {
|
|
const lines = [
|
|
'[OUTPUT CONTROL]',
|
|
`If you have no user-visible content to send for this turn, output exactly this JSON and nothing else: ${STRUCTURED_SILENT_OUTPUT_ENVELOPE}`,
|
|
'Do not wrap the JSON in backticks or code fences.',
|
|
'Do not combine the JSON with any other text.',
|
|
];
|
|
|
|
if (options?.reviewerMode) {
|
|
lines.push(
|
|
'If you are only agreeing, mirroring, or restating without adding a concrete correction, risk, missing prerequisite, test gap, or code change, output only the JSON object.',
|
|
);
|
|
}
|
|
|
|
return `${lines.join('\n')}\n\n${prompt}`;
|
|
}
|