Skip to content

Commit 3acc1fa

Browse files
kevinelliottclaude
andcommitted
Add JSON-only output format with --json flag (v0.5.3)
Feature: - Added `--json` flag to output conversation events as streaming JSONL to stdout - Clean programmatic output for CI/CD pipelines, monitoring tools, and automation - Suppresses all UI elements when enabled (logo, messages, session summary) Events emitted (matching bridge format): - bridge.connected: System information - conversation.started: Participants, mode, command info - message.created: Agent messages with tokens, cost, duration - conversation.completed: Status, totals, summary (with short_text and text) Implementation: - Created internal/bridge/stdout_emitter.go for stdout JSON output - Created internal/bridge/interface.go with BridgeEmitter interface - Updated orchestrator to use BridgeEmitter interface (supports both HTTP and stdout) - Modified cmd/run.go to suppress UI output when --json is set Usage: agentpipe run --json -a gemini:Bot --prompt "test" | jq '.type' Benefits: - Real-time streaming (see events as they happen) - Easy to pipe to jq, log aggregators, monitoring tools - CI/CD friendly - No breaking changes - opt-in via flag 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 24f2725 commit 3acc1fa

File tree

5 files changed

+332
-41
lines changed

5 files changed

+332
-41
lines changed

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.5.3] - 2025-01-26
11+
12+
### Added
13+
- **JSON-only output format (`--json` flag)**
14+
- Output conversation events as streaming JSONL (one JSON object per line) to stdout
15+
- Clean programmatic output for CI/CD pipelines, monitoring tools, and automation
16+
- Suppresses all UI elements (logo, initialization messages, session summary, agent messages)
17+
- Events emitted: `bridge.connected`, `conversation.started`, `message.created`, `conversation.completed`
18+
- Matches bridge events format for consistency
19+
- Usage: `agentpipe run --json -a gemini:Bot --prompt "test" | jq`
20+
- Benefits:
21+
- ✅ Real-time streaming (see events as they happen)
22+
- ✅ Easy to pipe to `jq`, log aggregators, monitoring tools
23+
- ✅ CI/CD friendly
24+
- ✅ No breaking changes - opt-in via flag
25+
26+
### Technical Details
27+
- **New Module**: `internal/bridge/stdout_emitter.go`
28+
- Implements `BridgeEmitter` interface for stdout JSON output
29+
- Uses same event schemas as HTTP bridge emitter
30+
- **New Interface**: `internal/bridge/interface.go`
31+
- `BridgeEmitter` interface allows both HTTP and stdout emitters
32+
- **Updated**: `pkg/orchestrator/orchestrator.go`
33+
- Changed `bridgeEmitter` field to use `BridgeEmitter` interface
34+
- **Updated**: `cmd/run.go`
35+
- Added `--json` flag
36+
- Suppresses UI output when `--json` is set
37+
- Passes `nil` writers to logger and orchestrator in JSON mode
38+
1039
## [0.5.2] - 2025-01-26
1140

1241
### Fixed

cmd/run.go

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ var (
4747
noStream bool
4848
noSummary bool
4949
summaryAgent string
50+
jsonOutput bool
5051
)
5152

5253
var runCmd = &cobra.Command{
@@ -80,6 +81,7 @@ func init() {
8081
runCmd.Flags().BoolVar(&noStream, "no-stream", false, "Disable streaming to AgentPipe Web for this run (overrides config)")
8182
runCmd.Flags().BoolVar(&noSummary, "no-summary", false, "Disable conversation summary generation (overrides config)")
8283
runCmd.Flags().StringVar(&summaryAgent, "summary-agent", "", "Agent to use for summary generation (default: gemini, overrides config)")
84+
runCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output events in JSON format (JSONL)")
8385
}
8486

8587
func runConversation(cobraCmd *cobra.Command, args []string) {
@@ -243,7 +245,9 @@ func startConversation(cmd *cobra.Command, cfg *config.Config) error {
243245

244246
verbose := viper.GetBool("verbose")
245247

246-
fmt.Println("🔍 Initializing agents...")
248+
if !jsonOutput {
249+
fmt.Println("🔍 Initializing agents...")
250+
}
247251

248252
for _, agentCfg := range cfg.Agents {
249253
if verbose {
@@ -319,7 +323,9 @@ func startConversation(cmd *cobra.Command, cfg *config.Config) error {
319323
return fmt.Errorf("no agents configured")
320324
}
321325

322-
fmt.Printf("✅ All %d agents initialized successfully\n\n", len(agentsList))
326+
if !jsonOutput {
327+
fmt.Printf("✅ All %d agents initialized successfully\n\n", len(agentsList))
328+
}
323329

324330
orchConfig := orchestrator.OrchestratorConfig{
325331
Mode: orchestrator.ConversationMode(cfg.Orchestrator.Mode),
@@ -334,7 +340,12 @@ func startConversation(cmd *cobra.Command, cfg *config.Config) error {
334340
var chatLogger *logger.ChatLogger
335341
if cfg.Logging.Enabled {
336342
var err error
337-
chatLogger, err = logger.NewChatLogger(cfg.Logging.ChatLogDir, cfg.Logging.LogFormat, os.Stdout, cfg.Logging.ShowMetrics)
343+
// Suppress console output when --json is set
344+
var consoleWriter io.Writer = os.Stdout
345+
if jsonOutput {
346+
consoleWriter = nil
347+
}
348+
chatLogger, err = logger.NewChatLogger(cfg.Logging.ChatLogDir, cfg.Logging.LogFormat, consoleWriter, cfg.Logging.ShowMetrics)
338349
if err != nil {
339350
fmt.Fprintf(os.Stderr, "Warning: Failed to create chat logger: %v\n", err)
340351
// Continue without logging
@@ -345,8 +356,8 @@ func startConversation(cmd *cobra.Command, cfg *config.Config) error {
345356

346357
// Create orchestrator with appropriate writer
347358
var writer io.Writer = os.Stdout
348-
if chatLogger != nil {
349-
writer = nil // Logger will handle console output
359+
if chatLogger != nil || jsonOutput {
360+
writer = nil // Logger will handle console output, or suppress for JSON mode
350361
}
351362

352363
orch := orchestrator.NewOrchestrator(orchConfig, writer)
@@ -358,31 +369,40 @@ func startConversation(cmd *cobra.Command, cfg *config.Config) error {
358369
commandInfo := buildCommandInfo(cmd, cfg)
359370
orch.SetCommandInfo(commandInfo)
360371

361-
// Set up streaming bridge if enabled
362-
shouldStream := determineShouldStream(streamEnabled, noStream)
363-
if shouldStream {
364-
bridgeConfig := bridge.LoadConfig()
365-
if bridgeConfig.Enabled || streamEnabled {
366-
// Override config enabled setting if --stream was specified
367-
if streamEnabled {
368-
bridgeConfig.Enabled = true
369-
}
372+
// Set up JSON stdout emitter if --json flag is set
373+
if jsonOutput {
374+
stdoutEmitter := bridge.NewStdoutEmitter(version.GetShortVersion())
375+
orch.SetBridgeEmitter(stdoutEmitter)
376+
} else {
377+
// Set up streaming bridge if enabled (only when not in JSON mode)
378+
shouldStream := determineShouldStream(streamEnabled, noStream)
379+
if shouldStream {
380+
bridgeConfig := bridge.LoadConfig()
381+
if bridgeConfig.Enabled || streamEnabled {
382+
// Override config enabled setting if --stream was specified
383+
if streamEnabled {
384+
bridgeConfig.Enabled = true
385+
}
370386

371-
emitter := bridge.NewEmitter(bridgeConfig, version.GetShortVersion())
372-
orch.SetBridgeEmitter(emitter)
387+
emitter := bridge.NewEmitter(bridgeConfig, version.GetShortVersion())
388+
orch.SetBridgeEmitter(emitter)
373389

374-
if verbose {
375-
fmt.Printf("🌐 Streaming enabled (conversation ID: %s)\n", emitter.GetConversationID())
390+
if verbose {
391+
fmt.Printf("🌐 Streaming enabled (conversation ID: %s)\n", emitter.GetConversationID())
392+
}
376393
}
377394
}
378395
}
379396

380-
fmt.Println("🚀 Starting AgentPipe conversation...")
381-
fmt.Printf("Mode: %s | Max turns: %d | Agents: %d\n", cfg.Orchestrator.Mode, cfg.Orchestrator.MaxTurns, len(agentsList))
382-
if !cfg.Logging.Enabled {
383-
fmt.Println("📝 Chat logging disabled (use --log-dir to enable)")
397+
// Only show UI elements when not in JSON output mode
398+
if !jsonOutput {
399+
fmt.Println("🚀 Starting AgentPipe conversation...")
400+
fmt.Printf("Mode: %s | Max turns: %d | Agents: %d\n", cfg.Orchestrator.Mode, cfg.Orchestrator.MaxTurns, len(agentsList))
401+
if !cfg.Logging.Enabled {
402+
fmt.Println("📝 Chat logging disabled (use --log-dir to enable)")
403+
}
404+
fmt.Println(strings.Repeat("=", 60))
384405
}
385-
fmt.Println(strings.Repeat("=", 60))
386406

387407
log.WithFields(map[string]interface{}{
388408
"mode": cfg.Orchestrator.Mode,
@@ -404,8 +424,10 @@ func startConversation(cmd *cobra.Command, cfg *config.Config) error {
404424
log.Info("conversation completed successfully")
405425
}
406426

407-
// Print summary
408-
fmt.Println("\n" + strings.Repeat("=", 60))
427+
// Only print UI summary when not in JSON mode
428+
if !jsonOutput {
429+
fmt.Println("\n" + strings.Repeat("=", 60))
430+
}
409431

410432
// Save conversation state if requested
411433
if saveState || stateFile != "" {
@@ -415,16 +437,19 @@ func startConversation(cmd *cobra.Command, cfg *config.Config) error {
415437
}
416438
}
417439

418-
// Always print session summary (whether interrupted or completed normally)
419-
if gracefulShutdown {
420-
fmt.Println("📊 Session Summary (Interrupted)")
421-
} else if err != nil {
422-
fmt.Println("📊 Session Summary (Ended with Error)")
423-
} else {
424-
fmt.Println("📊 Session Summary (Completed)")
440+
// Only print session summary when not in JSON output mode
441+
if !jsonOutput {
442+
// Always print session summary (whether interrupted or completed normally)
443+
if gracefulShutdown {
444+
fmt.Println("📊 Session Summary (Interrupted)")
445+
} else if err != nil {
446+
fmt.Println("📊 Session Summary (Ended with Error)")
447+
} else {
448+
fmt.Println("📊 Session Summary (Completed)")
449+
}
450+
fmt.Println(strings.Repeat("=", 60))
451+
printSessionSummary(orch, cfg)
425452
}
426-
fmt.Println(strings.Repeat("=", 60))
427-
printSessionSummary(orch, cfg)
428453

429454
if err != nil {
430455
return fmt.Errorf("orchestrator error: %w", err)

internal/bridge/interface.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package bridge
2+
3+
import (
4+
"time"
5+
)
6+
7+
// BridgeEmitter is the interface for emitting conversation events.
8+
// Both the HTTP-based Emitter and the stdout-based StdoutEmitter implement this interface.
9+
type BridgeEmitter interface {
10+
GetConversationID() string
11+
EmitConversationStarted(
12+
mode string,
13+
initialPrompt string,
14+
maxTurns int,
15+
participants []AgentParticipant,
16+
commandInfo *CommandInfo,
17+
)
18+
EmitMessageCreated(
19+
agentID string,
20+
agentType string,
21+
agentName string,
22+
content string,
23+
model string,
24+
turnNumber int,
25+
tokensUsed int,
26+
inputTokens int,
27+
outputTokens int,
28+
cost float64,
29+
duration time.Duration,
30+
)
31+
EmitConversationCompleted(
32+
status string,
33+
totalMessages int,
34+
totalTurns int,
35+
totalTokens int,
36+
totalCost float64,
37+
duration time.Duration,
38+
summary *SummaryMetadata,
39+
)
40+
EmitConversationError(errorMessage string, errorType string, agentType string)
41+
Close() error
42+
}

0 commit comments

Comments
 (0)