mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-14 02:42:04 +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) {
|
if (options.final) {
|
||||||
// Final result only with usage hint
|
// Final result only with usage hint
|
||||||
// Prefer parsedOutput (filtered, intermediate content removed) over raw stdout
|
// Prefer finalOutput (agent_message only) > parsedOutput (filtered) > raw stdout
|
||||||
const outputContent = result.parsedOutput?.content || result.stdout?.content;
|
const outputContent = result.finalOutput?.content || result.parsedOutput?.content || result.stdout?.content;
|
||||||
if (outputContent) {
|
if (outputContent) {
|
||||||
console.log(outputContent);
|
console.log(outputContent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -982,12 +982,18 @@ async function executeCliTool(
|
|||||||
// Create new turn - cache full output when not streaming (default)
|
// Create new turn - cache full output when not streaming (default)
|
||||||
const shouldCache = !parsed.data.stream;
|
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, {
|
const computedParsedOutput = flattenOutputUnits(allOutputUnits, {
|
||||||
excludeTypes: ['stderr', 'progress', 'metadata', 'system', 'tool_call', 'thought', 'code', 'file_diff', 'streaming_content'],
|
excludeTypes: ['stderr', 'progress', 'metadata', 'system', 'tool_call', 'thought', 'code', 'file_diff', 'streaming_content'],
|
||||||
stripCommandJsonBlocks: true // Strip embedded command execution JSON from agent_message
|
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 = {
|
const newTurnOutput = {
|
||||||
stdout: stdout.substring(0, 10240), // Truncate preview to 10KB
|
stdout: stdout.substring(0, 10240), // Truncate preview to 10KB
|
||||||
stderr: stderr.substring(0, 2048), // Truncate preview to 2KB
|
stderr: stderr.substring(0, 2048), // Truncate preview to 2KB
|
||||||
@@ -995,7 +1001,8 @@ async function executeCliTool(
|
|||||||
cached: shouldCache,
|
cached: shouldCache,
|
||||||
stdout_full: shouldCache ? stdout : undefined,
|
stdout_full: shouldCache ? stdout : undefined,
|
||||||
stderr_full: shouldCache ? stderr : 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
|
structured: allOutputUnits // Save structured IR units
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1156,7 +1163,8 @@ async function executeCliTool(
|
|||||||
exit_code: code,
|
exit_code: code,
|
||||||
duration_ms: duration,
|
duration_ms: duration,
|
||||||
output: newTurnOutput,
|
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({
|
resolve({
|
||||||
@@ -1165,7 +1173,8 @@ async function executeCliTool(
|
|||||||
conversation,
|
conversation,
|
||||||
stdout,
|
stdout,
|
||||||
stderr,
|
stderr,
|
||||||
parsedOutput: execution.parsedOutput
|
parsedOutput: execution.parsedOutput,
|
||||||
|
finalOutput: execution.finalOutput
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export interface ExecutionRecord {
|
|||||||
truncated: boolean;
|
truncated: boolean;
|
||||||
};
|
};
|
||||||
parsedOutput?: string; // Extracted clean text from structured output units
|
parsedOutput?: string; // Extracted clean text from structured output units
|
||||||
|
finalOutput?: string; // Agent message only (for --final flag)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HistoryIndex {
|
interface HistoryIndex {
|
||||||
@@ -109,6 +110,7 @@ export interface ExecutionOutput {
|
|||||||
stdout: string;
|
stdout: string;
|
||||||
stderr: string;
|
stderr: string;
|
||||||
parsedOutput?: string; // Extracted text from stream JSON response
|
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;
|
stdout_full?: string;
|
||||||
stderr_full?: string;
|
stderr_full?: string;
|
||||||
parsed_output?: string; // Filtered output (intermediate content removed)
|
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
|
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 hasStdoutFull = turnsInfo.some(col => col.name === 'stdout_full');
|
||||||
const hasStderrFull = turnsInfo.some(col => col.name === 'stderr_full');
|
const hasStderrFull = turnsInfo.some(col => col.name === 'stderr_full');
|
||||||
const hasParsedOutput = turnsInfo.some(col => col.name === 'parsed_output');
|
const hasParsedOutput = turnsInfo.some(col => col.name === 'parsed_output');
|
||||||
|
const hasFinalOutput = turnsInfo.some(col => col.name === 'final_output');
|
||||||
|
|
||||||
if (!hasCached) {
|
if (!hasCached) {
|
||||||
console.log('[CLI History] Migrating database: adding cached column to turns table...');
|
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;');
|
this.db.exec('ALTER TABLE turns ADD COLUMN parsed_output TEXT;');
|
||||||
console.log('[CLI History] Migration complete: parsed_output column added');
|
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) {
|
} catch (err) {
|
||||||
console.error('[CLI History] Migration error:', (err as Error).message);
|
console.error('[CLI History] Migration error:', (err as Error).message);
|
||||||
// Don't throw - allow the store to continue working with existing schema
|
// Don't throw - allow the store to continue working with existing schema
|
||||||
@@ -455,8 +462,8 @@ export class CliHistoryStore {
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
const upsertTurn = this.db.prepare(`
|
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)
|
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)
|
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
|
ON CONFLICT(conversation_id, turn_number) DO UPDATE SET
|
||||||
timestamp = @timestamp,
|
timestamp = @timestamp,
|
||||||
prompt = @prompt,
|
prompt = @prompt,
|
||||||
@@ -469,7 +476,8 @@ export class CliHistoryStore {
|
|||||||
cached = @cached,
|
cached = @cached,
|
||||||
stdout_full = @stdout_full,
|
stdout_full = @stdout_full,
|
||||||
stderr_full = @stderr_full,
|
stderr_full = @stderr_full,
|
||||||
parsed_output = @parsed_output
|
parsed_output = @parsed_output,
|
||||||
|
final_output = @final_output
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const transaction = this.db.transaction(() => {
|
const transaction = this.db.transaction(() => {
|
||||||
@@ -505,7 +513,8 @@ export class CliHistoryStore {
|
|||||||
cached: turn.output.cached ? 1 : 0,
|
cached: turn.output.cached ? 1 : 0,
|
||||||
stdout_full: turn.output.stdout_full || null,
|
stdout_full: turn.output.stdout_full || null,
|
||||||
stderr_full: turn.output.stderr_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,
|
cached: !!t.cached,
|
||||||
stdout_full: t.stdout_full || undefined,
|
stdout_full: t.stdout_full || undefined,
|
||||||
stderr_full: t.stderr_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 };
|
stdout?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||||
stderr?: { 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 };
|
parsedOutput?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||||
|
finalOutput?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||||
cached: boolean;
|
cached: boolean;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
status: string;
|
status: string;
|
||||||
@@ -626,6 +637,7 @@ export class CliHistoryStore {
|
|||||||
stdout?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
stdout?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||||
stderr?: { 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 };
|
parsedOutput?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||||
|
finalOutput?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||||
cached: boolean;
|
cached: boolean;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
status: 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) {
|
if (turn.parsed_output) {
|
||||||
const parsedContent = turn.parsed_output;
|
const parsedContent = turn.parsed_output;
|
||||||
const totalBytes = parsedContent.length;
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export type CliOutputUnitType =
|
|||||||
| 'metadata' // Session/execution metadata
|
| 'metadata' // Session/execution metadata
|
||||||
| 'system' // System events/messages
|
| 'system' // System events/messages
|
||||||
| 'tool_call' // Tool invocation/result (Gemini tool_use/tool_result)
|
| '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)
|
| '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) {
|
if (json.type === 'message' && json.role) {
|
||||||
// Gemini assistant/user message
|
// Gemini assistant/user message
|
||||||
if (json.role === 'assistant') {
|
if (json.role === 'assistant') {
|
||||||
// Delta messages use 'streaming_content' type - only last one is used in final output
|
// Delta messages use 'streaming_content' type - aggregated to agent_message later
|
||||||
// Non-delta (final) messages use 'stdout' type
|
// Non-delta (final) messages use 'agent_message' type directly
|
||||||
const outputType = json.delta === true ? 'streaming_content' : 'stdout';
|
const outputType = json.delta === true ? 'streaming_content' : 'agent_message';
|
||||||
return {
|
return {
|
||||||
type: outputType,
|
type: outputType,
|
||||||
content: json.content || '',
|
content: json.content || '',
|
||||||
@@ -420,7 +421,7 @@ export class JsonLinesParser implements IOutputParser {
|
|||||||
|
|
||||||
if (item.type === 'agent_message') {
|
if (item.type === 'agent_message') {
|
||||||
return {
|
return {
|
||||||
type: 'stdout',
|
type: 'agent_message', // Use dedicated type for final agent response
|
||||||
content: item.text || '',
|
content: item.text || '',
|
||||||
timestamp
|
timestamp
|
||||||
};
|
};
|
||||||
@@ -495,7 +496,7 @@ export class JsonLinesParser implements IOutputParser {
|
|||||||
.join('\n') || '';
|
.join('\n') || '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'stdout',
|
type: 'agent_message', // Use dedicated type for Claude final response
|
||||||
content: textContent,
|
content: textContent,
|
||||||
timestamp
|
timestamp
|
||||||
};
|
};
|
||||||
@@ -537,7 +538,7 @@ export class JsonLinesParser implements IOutputParser {
|
|||||||
|
|
||||||
if (json.type === 'text' && json.part) {
|
if (json.type === 'text' && json.part) {
|
||||||
return {
|
return {
|
||||||
type: 'stdout',
|
type: 'agent_message', // Use dedicated type for OpenCode text response
|
||||||
content: json.part.text || '',
|
content: json.part.text || '',
|
||||||
timestamp
|
timestamp
|
||||||
};
|
};
|
||||||
@@ -658,7 +659,7 @@ export class JsonLinesParser implements IOutputParser {
|
|||||||
.join('\n') || '';
|
.join('\n') || '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'stdout',
|
type: 'agent_message', // Use dedicated type for legacy Codex response
|
||||||
content,
|
content,
|
||||||
timestamp
|
timestamp
|
||||||
};
|
};
|
||||||
@@ -682,9 +683,16 @@ export class JsonLinesParser implements IOutputParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for Gemini/Qwen message format (role-based)
|
// Check for Gemini/Qwen message format (role-based)
|
||||||
if (json.role === 'user' || json.role === 'assistant') {
|
if (json.role === 'assistant') {
|
||||||
return {
|
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 || '',
|
content: json.content || json.text || '',
|
||||||
timestamp
|
timestamp
|
||||||
};
|
};
|
||||||
@@ -1128,7 +1136,7 @@ export function flattenOutputUnits(
|
|||||||
stripCommandJsonBlocks = false
|
stripCommandJsonBlocks = false
|
||||||
} = options || {};
|
} = 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)
|
// Gemini delta messages are incremental (each contains partial content to append)
|
||||||
let processedUnits = units;
|
let processedUnits = units;
|
||||||
const streamingUnits = units.filter(u => u.type === 'streaming_content');
|
const streamingUnits = units.filter(u => u.type === 'streaming_content');
|
||||||
@@ -1138,9 +1146,9 @@ export function flattenOutputUnits(
|
|||||||
.map(u => typeof u.content === 'string' ? u.content : '')
|
.map(u => typeof u.content === 'string' ? u.content : '')
|
||||||
.join('');
|
.join('');
|
||||||
processedUnits = units.filter(u => u.type !== 'streaming_content');
|
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({
|
processedUnits.push({
|
||||||
type: 'stdout',
|
type: 'agent_message',
|
||||||
content: concatenatedContent,
|
content: concatenatedContent,
|
||||||
timestamp: streamingUnits[streamingUnits.length - 1].timestamp
|
timestamp: streamingUnits[streamingUnits.length - 1].timestamp
|
||||||
});
|
});
|
||||||
@@ -1168,7 +1176,7 @@ export function flattenOutputUnits(
|
|||||||
let content = unit.content;
|
let content = unit.content;
|
||||||
|
|
||||||
// Strip command execution JSON code blocks if requested (codex agent_message often includes these)
|
// 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
|
// Pattern 1: Backtick-wrapped JSON blocks
|
||||||
// Format: ```...{"command":"...","output":"...","exitCode":N,"status":"..."...}...```
|
// Format: ```...{"command":"...","output":"...","exitCode":N,"status":"..."...}...```
|
||||||
// Uses [\s\S]*? to match any characters (including newlines) non-greedily
|
// Uses [\s\S]*? to match any characters (including newlines) non-greedily
|
||||||
|
|||||||
Reference in New Issue
Block a user