How Native Agent Runners Implement the Agent Interface and SSE Streaming in No-Mistakes

Native agent runners in no-mistakes satisfy the Agent interface by launching vendor CLI binaries and parsing line-delimited JSON streams to forward partial output in real time via the RunOpts.OnChunk callback.

The no-mistakes repository provides a unified abstraction for AI-driven automation through a minimal Go interface. Native agent runners for Claude and Copilot implement this contract by wrapping vendor-provided CLI tools, enabling Server-Sent Events (SSE)-style streaming of AI-generated content without buffering entire responses.

The Agent Interface Contract

At internal/agent/agent.go, the repository defines the core abstraction that every AI-backed runner must satisfy:

type Agent interface {
    Name() string
    Run(context.Context, RunOpts) (*Result, error)
    Close() error
}

The RunOpts struct includes an OnChunk callback function that enables streaming. When provided, native agents invoke this callback immediately upon receiving partial text from the underlying CLI process, allowing the UI to display results incrementally rather than waiting for the complete response.

Claude and Copilot Implementation Patterns

Both native agents follow an identical architectural pattern while adapting to their respective CLI formats. They implement the Agent interface through concrete structs that manage binary paths and execution arguments.

Struct Definition and Identity

In internal/agent/claude.go, the Claude runner defines a struct that holds the binary path and extra arguments:

type claudeAgent struct {
    bin       string
    extraArgs []string
}

The Name() method returns a constant identifier used for logging and configuration:

func (a *claudeAgent) Name() string { return "claude" }

Similarly, in internal/agent/copilot.go, the Copilot runner mirrors this structure with its own distinct name:

type copilotAgent struct {
    bin       string
    extraArgs []string
}

func (a *copilotAgent) Name() string { return "copilot" }

Process Execution with Retry Logic

Both implementations delegate execution to a shared retry wrapper defined in internal/agent/native_command.go. The Run() method calls runWithRetry(), which attempts the operation up to claudeMaxRetries (currently 3) before surfacing transient failures like network blips or rate limits.

The runOnce() method handles the actual CLI invocation. For Claude, the implementation builds arguments and configures the command context:

func (a *claudeAgent) runOnce(ctx context.Context, opts RunOpts) (*Result, error) {
    args := a.buildArgs(opts.Prompt, opts.JSONSchema)
    cmd := exec.CommandContext(ctx, a.bin, args...)
    cmd.Dir = opts.CWD
    cmd.Env = gitSafeEnv(opts.CWD)
    shellenv.ConfigureShellCommand(cmd)
    
    started, err := startNativeAgentCommand(cmd)
    // ... error handling and parsing
    if err := parseClaudeEvents(ctx, started.stdout, opts.OnChunk, &usage, &result); err != nil {
        // ...
    }
    return finalizeClaudeResult(result, opts.JSONSchema, usage)
}

Copilot follows the identical pattern in internal/agent/copilot.go, using buildCopilotPrompt() to construct the input and buildArgs() to assemble command-line flags.

Process Isolation and Pipe Management

The startNativeAgentCommand() helper in internal/agent/native_command.go wraps exec.Cmd with proper pipe management, returning separate stdout and stderr readers along with a wait method. This ensures that error messages are captured without blocking the streaming output.

Both agents utilize shellenv.ConfigureShellCommand() from the internal/shellenv/ package to place the spawned process in its own process group. This guarantees that child processes are properly terminated when the context is cancelled, preventing orphaned CLI processes.

SSE Streaming Implementation

Native agents implement Server-Sent Events-style streaming by parsing the CLI's line-oriented JSON output incrementally. Rather than buffering the entire response, they process each line as it arrives and forward partial content through the OnChunk callback.

Claude's Stream-JSON Parsing

The Claude CLI emits events in a line-delimited JSON format (--output-format stream-json). The parseClaudeEvents() function in internal/agent/claude.go consumes this stream using a bufio.Scanner:

func parseClaudeEvents(
    ctx context.Context,
    r io.Reader,
    onChunk func(string),
    usage *TokenUsage,
    result **claudeResult,
) error {
    scanner := bufio.NewScanner(r)
    for scanner.Scan() {
        // Parse JSON event
        switch event.Type {
        case "assistant":
            if onChunk != nil {
                onChunk(event.Text)
            }
        case "result":
            // Capture final result event
        }
    }
    return scanner.Err()
}

Each assistant event appends text to a local buffer and immediately invokes onChunk if configured, providing real-time updates to the caller. The final result event is stored for later conversion via finalizeClaudeResult().

Copilot's JSONL Event Handling

Copilot uses a similar JSON Lines format but with distinct event types. The parseCopilotEvents() function in internal/agent/copilot.go handles the assistant.message_delta type for streaming chunks:

func parseCopilotEvents(
    ctx context.Context,
    r io.Reader,
    onChunk func(string),
    usage *TokenUsage,
    messages *[]string,
    copilotErr *string,
    exitCode *int,
) error {
    scanner := bufio.NewScanner(r)
    // ...
    switch event.Type {
    case "assistant.message_delta":
        if onChunk != nil {
            onChunk(event.Data.DeltaContent)
        }
    case "assistant.message":
        *messages = append(*messages, event.Data.Content)
        usage.Add(TokenUsage{OutputTokens: event.Data.OutputTokens})
    }
    return nil
}

Unlike Claude, Copilot explicitly tracks exit codes and accumulates complete messages separately from delta chunks. The finalizeCopilotResult() function walks the list of assistant messages in reverse order, attempting to parse each against the JSON schema before falling back to raw text.

Shared Infrastructure

Common Retry Classification

The runWithRetry() function in internal/agent/native_command.go accepts a classify callback that evaluates error types to determine if they represent transient failures warranting retry. This shared logic standardizes resilience across all native agents while allowing agent-specific error detection.

Resource Cleanup

Both native agents implement Close() as a no-op since the underlying CLI processes are ephemeral and managed through context cancellation. No persistent resources require explicit cleanup beyond the process management handled by the os/exec package and shellenv utilities.

Summary

  • Minimal Interface: Native agents implement Name(), Run(), and Close() from the Agent interface defined in internal/agent/agent.go.
  • CLI Wrapping: Both Claude and Copilot runners launch vendor binaries via exec.CommandContext and manage process groups through shellenv.ConfigureShellCommand.
  • Real-time Streaming: Line-delimited JSON parsers (parseClaudeEvents and parseCopilotEvents) consume CLI output incrementally, forwarding partial text via the OnChunk callback.
  • Resilient Execution: The shared runWithRetry wrapper in internal/agent/native_command.go provides automatic retry logic for transient failures up to three attempts.
  • Schema Validation: Finalize functions convert raw CLI output into structured Result objects, validating against JSON schemas when specified.

Frequently Asked Questions

How do native agent runners handle process cleanup?

Native agents rely on context cancellation and process group isolation rather than explicit resource management. The shellenv.ConfigureShellCommand() helper places CLI processes in dedicated process groups, ensuring that subprocesses terminate when the parent context is cancelled. The Close() method is a no-op since no persistent handles require manual cleanup.

What distinguishes Claude's streaming format from Copilot's?

Claude emits --output-format stream-json with event types like assistant and result, while Copilot uses a JSONL format with assistant.message_delta events for streaming chunks and assistant.message for complete responses. Despite these structural differences, both parsers deliver identical real-time behavior through the OnChunk callback.

How does the retry mechanism determine which errors are transient?

The runWithRetry() function accepts a classifier callback that evaluates error types against agent-specific criteria. While the implementation identifies network interruptions and rate limits as transient, the architecture allows each runner to define its own error classification logic while sharing the retry loop infrastructure and the claudeMaxRetries constant (set to 3).

Can developers customize the CLI arguments passed to native agents?

Yes. Both claudeAgent and copilotAgent structs maintain an extraArgs slice that accumulates user-provided flags. The buildArgs() method (and buildCopilotPrompt() for Copilot) merges these with default parameters such as --output-format stream-json for Claude, allowing injection of vendor-specific flags while preserving required streaming configuration.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →