Skip to content

Commit 2bf89a3

Browse files
bugerclaude
andcommitted
feat: RAW_OUTPUT passthrough for DSL output() through workflow chain
When execute_plan uses output(), the data must reach the end user without any intermediate LLM rewriting it. This commit implements the full passthrough chain: - parseAIResponse: extract <<<RAW_OUTPUT>>> blocks before JSON parsing, attach as _rawOutput on the parsed output object - workflow-check-provider: propagate _rawOutput from step outputs to workflow-level output - workflow-tool-executor: wrap _rawOutput in <<<RAW_OUTPUT>>> delimiters when returning tool result, so the calling ProbeAgent extracts it via extractRawOutputBlocks (bypassing the LLM) - slack-frontend: append _rawOutput to rendered text so file sections (--- filename.ext ---) get extracted and uploaded Also updates @probelabs/probe to 0.6.0-rc245 which includes the ProbeAgent-side change (appending output buffer with RAW_OUTPUT delimiters for schema responses). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9d8f5f7 commit 2bf89a3

File tree

8 files changed

+565
-10
lines changed

8 files changed

+565
-10
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
"@octokit/auth-app": "^8.1.0",
103103
"@octokit/core": "^7.0.3",
104104
"@octokit/rest": "^22.0.0",
105-
"@probelabs/probe": "^0.6.0-rc240",
105+
"@probelabs/probe": "^0.6.0-rc245",
106106
"@types/commander": "^2.12.0",
107107
"@types/uuid": "^10.0.0",
108108
"ajv": "^8.17.1",

src/ai-review-service.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2438,6 +2438,26 @@ ${'='.repeat(60)}
24382438
// Handle different schema types differently
24392439
let reviewData: AIResponseFormat = undefined as unknown as AIResponseFormat;
24402440

2441+
// Extract <<<RAW_OUTPUT>>> blocks appended by ProbeAgent after schema JSON.
2442+
// These carry DSL output() content that must bypass LLM rewriting and be
2443+
// propagated directly to the user (Slack, CLI, etc.).
2444+
// Declared here so it's accessible across all parsing paths below.
2445+
const RAW_OUTPUT_RE = /\n<<<RAW_OUTPUT>>>\n([\s\S]*?)\n<<<END_RAW_OUTPUT>>>/g;
2446+
const rawOutputBlocks: string[] = [];
2447+
let responseForParsing = response;
2448+
{
2449+
let rawMatch: RegExpExecArray | null;
2450+
while ((rawMatch = RAW_OUTPUT_RE.exec(response)) !== null) {
2451+
rawOutputBlocks.push(rawMatch[1]);
2452+
}
2453+
if (rawOutputBlocks.length > 0) {
2454+
responseForParsing = response.replace(RAW_OUTPUT_RE, '');
2455+
log(
2456+
`📦 Extracted ${rawOutputBlocks.length} RAW_OUTPUT blocks (${rawOutputBlocks.reduce((s, b) => s + b.length, 0)} chars) from response`
2457+
);
2458+
}
2459+
}
2460+
24412461
// Handle plain schema or no schema - no JSON parsing, treat as assistant-style text output
24422462
if (_schema === 'plain' || !_schema) {
24432463
log(
@@ -2465,7 +2485,7 @@ ${'='.repeat(60)}
24652485

24662486
// Sanitize response: strip BOM, zero-width chars, and other invisible characters
24672487
// that can cause JSON parsing to fail even when the text looks valid
2468-
const sanitizedResponse = response
2488+
const sanitizedResponse = responseForParsing
24692489
.replace(/^\uFEFF/, '') // BOM
24702490
.replace(/[\u200B-\u200D\uFEFF\u00A0]/g, '') // Zero-width chars, NBSP
24712491
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Control chars (except \t \n \r)
@@ -2522,10 +2542,12 @@ ${'='.repeat(60)}
25222542
response.toLowerCase().includes('unable to')
25232543
) {
25242544
console.error('🚫 AI refused to analyze - returning refusal as output');
2525-
const trimmed = response.trim();
2545+
const trimmed = responseForParsing.trim();
2546+
const out: any = trimmed ? { text: trimmed } : {};
2547+
if (rawOutputBlocks.length > 0) out._rawOutput = rawOutputBlocks.join('\n\n');
25262548
return {
25272549
issues: [],
2528-
output: trimmed ? { text: trimmed } : {},
2550+
output: out,
25292551
debug: debugInfo,
25302552
};
25312553
}
@@ -2534,10 +2556,12 @@ ${'='.repeat(60)}
25342556
// This allows Probe (or other AI providers) to handle JSON validation
25352557
// and avoids false positives from bracket-matching (e.g., mermaid diagrams)
25362558
log('🔧 Treating response as plain text (no JSON extraction)');
2537-
const trimmed = response.trim();
2559+
const trimmed = responseForParsing.trim();
2560+
const fallbackOut: any = { text: trimmed };
2561+
if (rawOutputBlocks.length > 0) fallbackOut._rawOutput = rawOutputBlocks.join('\n\n');
25382562
return {
25392563
issues: [],
2540-
output: { text: trimmed },
2564+
output: fallbackOut,
25412565
debug: debugInfo,
25422566
};
25432567
}
@@ -2627,6 +2651,11 @@ ${'='.repeat(60)}
26272651
}
26282652
}
26292653

2654+
// Attach raw output blocks from DSL execute_plan so frontends can render them
2655+
if (rawOutputBlocks.length > 0) {
2656+
(out as any)._rawOutput = rawOutputBlocks.join('\n\n');
2657+
}
2658+
26302659
const result: ReviewSummary & { output?: unknown } = {
26312660
// Keep issues empty for custom-schema rendering; consumers read from output.*
26322661
issues: [],

src/frontends/slack-frontend.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,11 @@ export class SlackFrontend implements Frontend {
529529
text = String(out);
530530
}
531531
}
532+
// Append raw output from DSL execute_plan (bypasses LLM rewriting chain)
533+
if (out && typeof out._rawOutput === 'string' && out._rawOutput.trim().length > 0) {
534+
text = (text || '') + '\n\n' + out._rawOutput.trim();
535+
}
536+
532537
if (!text) {
533538
ctx.logger.info(
534539
`[slack-frontend] skip posting AI reply for ${checkId}: no renderable text in check output`

src/providers/workflow-check-provider.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,27 @@ export class WorkflowCheckProvider extends CheckProvider {
866866
);
867867
}
868868

869+
// Propagate _rawOutput from any step — this carries DSL output() content
870+
// that must bypass all LLM processing and reach the end user directly.
871+
if (!(outputs as any)._rawOutput) {
872+
const rawParts: string[] = [];
873+
for (const stepOutput of Object.values(outputsMap)) {
874+
if (
875+
stepOutput &&
876+
typeof stepOutput === 'object' &&
877+
typeof (stepOutput as any)._rawOutput === 'string'
878+
) {
879+
rawParts.push((stepOutput as any)._rawOutput);
880+
}
881+
}
882+
if (rawParts.length > 0) {
883+
(outputs as any)._rawOutput = rawParts.join('\n\n');
884+
logger.debug(
885+
`[WorkflowProvider] Propagated _rawOutput from steps (${rawParts.length} blocks, ${(outputs as any)._rawOutput.length} chars)`
886+
);
887+
}
888+
}
889+
869890
return outputs;
870891
}
871892

src/providers/workflow-tool-executor.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,20 @@ export async function executeWorkflowAsTool(
243243
logger.debug(`[WorkflowToolExecutor] Workflow '${workflowId}' output preview: ${outputPreview}`);
244244

245245
if (output !== undefined) {
246+
// If the workflow output carries _rawOutput (from DSL execute_plan output()),
247+
// extract it and wrap in <<<RAW_OUTPUT>>> delimiters so the calling ProbeAgent
248+
// can extract it via extractRawOutputBlocks before the LLM sees the tool result.
249+
// This ensures raw data passes through the full chain without any LLM touching it.
250+
if (output && typeof output === 'object' && typeof (output as any)._rawOutput === 'string') {
251+
const rawOutput = (output as any)._rawOutput;
252+
const cleanOutput = { ...output };
253+
delete (cleanOutput as any)._rawOutput;
254+
const jsonStr = JSON.stringify(cleanOutput, null, 2);
255+
logger.debug(
256+
`[WorkflowToolExecutor] Wrapping _rawOutput (${rawOutput.length} chars) in RAW_OUTPUT delimiters for '${workflowId}'`
257+
);
258+
return jsonStr + '\n<<<RAW_OUTPUT>>>\n' + rawOutput + '\n<<<END_RAW_OUTPUT>>>';
259+
}
246260
return output;
247261
}
248262

0 commit comments

Comments
 (0)