mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat(cli): add agent_message type for precise --final output filtering
Introduce dedicated agent_message IR type to distinguish final AI responses from generic stdout. This enables --final flag to show only agent messages, filtering out all intermediate content (JSONL events, reasoning, tool calls). Changes: - Add agent_message type to CliOutputUnitType - Update JsonLinesParser to map final responses from all tools (codex, gemini, claude, opencode) to agent_message type - Add final_output field to database schema with migration - Update getCachedOutput and getConversation to return finalOutput - Prefer finalOutput in outputAction for --final flag Fixes issue where --final showed raw JSONL instead of filtered content.
This commit is contained in:
@@ -386,8 +386,8 @@ async function outputAction(conversationId: string | undefined, options: OutputV
|
||||
|
||||
if (options.final) {
|
||||
// Final result only with usage hint
|
||||
// Prefer parsedOutput (filtered, intermediate content removed) over raw stdout
|
||||
const outputContent = result.parsedOutput?.content || result.stdout?.content;
|
||||
// Prefer finalOutput (agent_message only) > parsedOutput (filtered) > raw stdout
|
||||
const outputContent = result.finalOutput?.content || result.parsedOutput?.content || result.stdout?.content;
|
||||
if (outputContent) {
|
||||
console.log(outputContent);
|
||||
}
|
||||
|
||||
@@ -982,12 +982,18 @@ async function executeCliTool(
|
||||
// Create new turn - cache full output when not streaming (default)
|
||||
const shouldCache = !parsed.data.stream;
|
||||
|
||||
// Compute parsed output (filtered, intermediate content removed) for final display
|
||||
// Compute parsed output (filtered, intermediate content removed) for general display
|
||||
const computedParsedOutput = flattenOutputUnits(allOutputUnits, {
|
||||
excludeTypes: ['stderr', 'progress', 'metadata', 'system', 'tool_call', 'thought', 'code', 'file_diff', 'streaming_content'],
|
||||
stripCommandJsonBlocks: true // Strip embedded command execution JSON from agent_message
|
||||
});
|
||||
|
||||
// Compute final output (only agent_message) for --final flag
|
||||
const computedFinalOutput = flattenOutputUnits(allOutputUnits, {
|
||||
includeTypes: ['agent_message'],
|
||||
stripCommandJsonBlocks: true // Strip embedded command execution JSON from agent_message
|
||||
});
|
||||
|
||||
const newTurnOutput = {
|
||||
stdout: stdout.substring(0, 10240), // Truncate preview to 10KB
|
||||
stderr: stderr.substring(0, 2048), // Truncate preview to 2KB
|
||||
@@ -995,7 +1001,8 @@ async function executeCliTool(
|
||||
cached: shouldCache,
|
||||
stdout_full: shouldCache ? stdout : undefined,
|
||||
stderr_full: shouldCache ? stderr : undefined,
|
||||
parsed_output: computedParsedOutput || undefined, // Filtered output for final display
|
||||
parsed_output: computedParsedOutput || undefined, // Filtered output for general display
|
||||
final_output: computedFinalOutput || undefined, // Agent message only for --final flag
|
||||
structured: allOutputUnits // Save structured IR units
|
||||
};
|
||||
|
||||
@@ -1156,7 +1163,8 @@ async function executeCliTool(
|
||||
exit_code: code,
|
||||
duration_ms: duration,
|
||||
output: newTurnOutput,
|
||||
parsedOutput: computedParsedOutput // Use already-computed filtered output
|
||||
parsedOutput: computedParsedOutput, // Use already-computed filtered output
|
||||
finalOutput: computedFinalOutput // Use already-computed agent_message only output
|
||||
};
|
||||
|
||||
resolve({
|
||||
@@ -1165,7 +1173,8 @@ async function executeCliTool(
|
||||
conversation,
|
||||
stdout,
|
||||
stderr,
|
||||
parsedOutput: execution.parsedOutput
|
||||
parsedOutput: execution.parsedOutput,
|
||||
finalOutput: execution.finalOutput
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ export interface ExecutionRecord {
|
||||
truncated: boolean;
|
||||
};
|
||||
parsedOutput?: string; // Extracted clean text from structured output units
|
||||
finalOutput?: string; // Agent message only (for --final flag)
|
||||
}
|
||||
|
||||
interface HistoryIndex {
|
||||
@@ -109,6 +110,7 @@ export interface ExecutionOutput {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
parsedOutput?: string; // Extracted text from stream JSON response
|
||||
finalOutput?: string; // Agent message only (for --final flag)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface ConversationTurn {
|
||||
stdout_full?: string;
|
||||
stderr_full?: string;
|
||||
parsed_output?: string; // Filtered output (intermediate content removed)
|
||||
final_output?: string; // Agent message only (for --final flag)
|
||||
structured?: CliOutputUnit[]; // Structured IR sequence for advanced parsing
|
||||
};
|
||||
}
|
||||
@@ -328,6 +329,7 @@ export class CliHistoryStore {
|
||||
const hasStdoutFull = turnsInfo.some(col => col.name === 'stdout_full');
|
||||
const hasStderrFull = turnsInfo.some(col => col.name === 'stderr_full');
|
||||
const hasParsedOutput = turnsInfo.some(col => col.name === 'parsed_output');
|
||||
const hasFinalOutput = turnsInfo.some(col => col.name === 'final_output');
|
||||
|
||||
if (!hasCached) {
|
||||
console.log('[CLI History] Migrating database: adding cached column to turns table...');
|
||||
@@ -349,6 +351,11 @@ export class CliHistoryStore {
|
||||
this.db.exec('ALTER TABLE turns ADD COLUMN parsed_output TEXT;');
|
||||
console.log('[CLI History] Migration complete: parsed_output column added');
|
||||
}
|
||||
if (!hasFinalOutput) {
|
||||
console.log('[CLI History] Migrating database: adding final_output column to turns table...');
|
||||
this.db.exec('ALTER TABLE turns ADD COLUMN final_output TEXT;');
|
||||
console.log('[CLI History] Migration complete: final_output column added');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[CLI History] Migration error:', (err as Error).message);
|
||||
// Don't throw - allow the store to continue working with existing schema
|
||||
@@ -455,8 +462,8 @@ export class CliHistoryStore {
|
||||
`);
|
||||
|
||||
const upsertTurn = this.db.prepare(`
|
||||
INSERT INTO turns (conversation_id, turn_number, timestamp, prompt, duration_ms, status, exit_code, stdout, stderr, truncated, cached, stdout_full, stderr_full, parsed_output)
|
||||
VALUES (@conversation_id, @turn_number, @timestamp, @prompt, @duration_ms, @status, @exit_code, @stdout, @stderr, @truncated, @cached, @stdout_full, @stderr_full, @parsed_output)
|
||||
INSERT INTO turns (conversation_id, turn_number, timestamp, prompt, duration_ms, status, exit_code, stdout, stderr, truncated, cached, stdout_full, stderr_full, parsed_output, final_output)
|
||||
VALUES (@conversation_id, @turn_number, @timestamp, @prompt, @duration_ms, @status, @exit_code, @stdout, @stderr, @truncated, @cached, @stdout_full, @stderr_full, @parsed_output, @final_output)
|
||||
ON CONFLICT(conversation_id, turn_number) DO UPDATE SET
|
||||
timestamp = @timestamp,
|
||||
prompt = @prompt,
|
||||
@@ -469,7 +476,8 @@ export class CliHistoryStore {
|
||||
cached = @cached,
|
||||
stdout_full = @stdout_full,
|
||||
stderr_full = @stderr_full,
|
||||
parsed_output = @parsed_output
|
||||
parsed_output = @parsed_output,
|
||||
final_output = @final_output
|
||||
`);
|
||||
|
||||
const transaction = this.db.transaction(() => {
|
||||
@@ -505,7 +513,8 @@ export class CliHistoryStore {
|
||||
cached: turn.output.cached ? 1 : 0,
|
||||
stdout_full: turn.output.stdout_full || null,
|
||||
stderr_full: turn.output.stderr_full || null,
|
||||
parsed_output: turn.output.parsed_output || null
|
||||
parsed_output: turn.output.parsed_output || null,
|
||||
final_output: turn.output.final_output || null
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -553,7 +562,8 @@ export class CliHistoryStore {
|
||||
cached: !!t.cached,
|
||||
stdout_full: t.stdout_full || undefined,
|
||||
stderr_full: t.stderr_full || undefined,
|
||||
parsed_output: t.parsed_output || undefined
|
||||
parsed_output: t.parsed_output || undefined,
|
||||
final_output: t.final_output || undefined // Agent message only for --final flag
|
||||
}
|
||||
}))
|
||||
};
|
||||
@@ -599,6 +609,7 @@ export class CliHistoryStore {
|
||||
stdout?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||
stderr?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||
parsedOutput?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||
finalOutput?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||
cached: boolean;
|
||||
prompt: string;
|
||||
status: string;
|
||||
@@ -626,6 +637,7 @@ export class CliHistoryStore {
|
||||
stdout?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||
stderr?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||
parsedOutput?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||
finalOutput?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||
cached: boolean;
|
||||
prompt: string;
|
||||
status: string;
|
||||
@@ -664,7 +676,7 @@ export class CliHistoryStore {
|
||||
};
|
||||
}
|
||||
|
||||
// Add parsed output if available (filtered output for final display)
|
||||
// Add parsed output if available (filtered output for general display)
|
||||
if (turn.parsed_output) {
|
||||
const parsedContent = turn.parsed_output;
|
||||
const totalBytes = parsedContent.length;
|
||||
@@ -677,6 +689,19 @@ export class CliHistoryStore {
|
||||
};
|
||||
}
|
||||
|
||||
// Add final output if available (agent_message only for --final flag)
|
||||
if (turn.final_output) {
|
||||
const finalContent = turn.final_output;
|
||||
const totalBytes = finalContent.length;
|
||||
const content = finalContent.substring(offset, offset + limit);
|
||||
result.finalOutput = {
|
||||
content,
|
||||
totalBytes,
|
||||
offset,
|
||||
hasMore: offset + limit < totalBytes
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export type CliOutputUnitType =
|
||||
| 'metadata' // Session/execution metadata
|
||||
| 'system' // System events/messages
|
||||
| 'tool_call' // Tool invocation/result (Gemini tool_use/tool_result)
|
||||
| 'agent_message' // Final agent response (for --final output)
|
||||
| 'streaming_content'; // Streaming delta content (only last one used in final output)
|
||||
|
||||
/**
|
||||
@@ -293,9 +294,9 @@ export class JsonLinesParser implements IOutputParser {
|
||||
if (json.type === 'message' && json.role) {
|
||||
// Gemini assistant/user message
|
||||
if (json.role === 'assistant') {
|
||||
// Delta messages use 'streaming_content' type - only last one is used in final output
|
||||
// Non-delta (final) messages use 'stdout' type
|
||||
const outputType = json.delta === true ? 'streaming_content' : 'stdout';
|
||||
// Delta messages use 'streaming_content' type - aggregated to agent_message later
|
||||
// Non-delta (final) messages use 'agent_message' type directly
|
||||
const outputType = json.delta === true ? 'streaming_content' : 'agent_message';
|
||||
return {
|
||||
type: outputType,
|
||||
content: json.content || '',
|
||||
@@ -420,7 +421,7 @@ export class JsonLinesParser implements IOutputParser {
|
||||
|
||||
if (item.type === 'agent_message') {
|
||||
return {
|
||||
type: 'stdout',
|
||||
type: 'agent_message', // Use dedicated type for final agent response
|
||||
content: item.text || '',
|
||||
timestamp
|
||||
};
|
||||
@@ -495,7 +496,7 @@ export class JsonLinesParser implements IOutputParser {
|
||||
.join('\n') || '';
|
||||
|
||||
return {
|
||||
type: 'stdout',
|
||||
type: 'agent_message', // Use dedicated type for Claude final response
|
||||
content: textContent,
|
||||
timestamp
|
||||
};
|
||||
@@ -537,7 +538,7 @@ export class JsonLinesParser implements IOutputParser {
|
||||
|
||||
if (json.type === 'text' && json.part) {
|
||||
return {
|
||||
type: 'stdout',
|
||||
type: 'agent_message', // Use dedicated type for OpenCode text response
|
||||
content: json.part.text || '',
|
||||
timestamp
|
||||
};
|
||||
@@ -658,7 +659,7 @@ export class JsonLinesParser implements IOutputParser {
|
||||
.join('\n') || '';
|
||||
|
||||
return {
|
||||
type: 'stdout',
|
||||
type: 'agent_message', // Use dedicated type for legacy Codex response
|
||||
content,
|
||||
timestamp
|
||||
};
|
||||
@@ -682,9 +683,16 @@ export class JsonLinesParser implements IOutputParser {
|
||||
}
|
||||
|
||||
// Check for Gemini/Qwen message format (role-based)
|
||||
if (json.role === 'user' || json.role === 'assistant') {
|
||||
if (json.role === 'assistant') {
|
||||
return {
|
||||
type: 'stdout',
|
||||
type: 'agent_message', // Use dedicated type for Gemini/Qwen assistant response
|
||||
content: json.content || json.text || '',
|
||||
timestamp
|
||||
};
|
||||
}
|
||||
if (json.role === 'user') {
|
||||
return {
|
||||
type: 'stdout', // User messages remain as stdout
|
||||
content: json.content || json.text || '',
|
||||
timestamp
|
||||
};
|
||||
@@ -1128,7 +1136,7 @@ export function flattenOutputUnits(
|
||||
stripCommandJsonBlocks = false
|
||||
} = options || {};
|
||||
|
||||
// Special handling for streaming_content: concatenate all into a single stdout unit
|
||||
// Special handling for streaming_content: concatenate all into a single agent_message unit
|
||||
// Gemini delta messages are incremental (each contains partial content to append)
|
||||
let processedUnits = units;
|
||||
const streamingUnits = units.filter(u => u.type === 'streaming_content');
|
||||
@@ -1138,9 +1146,9 @@ export function flattenOutputUnits(
|
||||
.map(u => typeof u.content === 'string' ? u.content : '')
|
||||
.join('');
|
||||
processedUnits = units.filter(u => u.type !== 'streaming_content');
|
||||
// Add concatenated content as stdout type for inclusion
|
||||
// Add concatenated content as agent_message type for final output
|
||||
processedUnits.push({
|
||||
type: 'stdout',
|
||||
type: 'agent_message',
|
||||
content: concatenatedContent,
|
||||
timestamp: streamingUnits[streamingUnits.length - 1].timestamp
|
||||
});
|
||||
@@ -1168,7 +1176,7 @@ export function flattenOutputUnits(
|
||||
let content = unit.content;
|
||||
|
||||
// Strip command execution JSON code blocks if requested (codex agent_message often includes these)
|
||||
if (stripCommandJsonBlocks && unit.type === 'stdout') {
|
||||
if (stripCommandJsonBlocks && (unit.type === 'stdout' || unit.type === 'agent_message')) {
|
||||
// Pattern 1: Backtick-wrapped JSON blocks
|
||||
// Format: ```...{"command":"...","output":"...","exitCode":N,"status":"..."...}...```
|
||||
// Uses [\s\S]*? to match any characters (including newlines) non-greedily
|
||||
|
||||
Reference in New Issue
Block a user