Skip to content

Add Go M0 agent runtime#51

Merged
Vasanthdev2004 merged 2 commits into
mainfrom
feat/go-m0-agent-runtime
Jun 4, 2026
Merged

Add Go M0 agent runtime#51
Vasanthdev2004 merged 2 commits into
mainfrom
feat/go-m0-agent-runtime

Conversation

@Vasanthdev2004

@Vasanthdev2004 Vasanthdev2004 commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the first Go agent runtime spine for M0:

  • Defines Go provider messages, stream events, tool calls, tool definitions, usage, and runtime options.
  • Adds a bounded ReAct-style loop that streams text, collects tool calls, executes tools through tools.Registry.RunWithOptions, feeds tool results back into provider messages, and stops at MaxTurns.
  • Supports permission modes for safe reads, prompt-gated denial by default, and unsafe grants.
  • Wires a minimal zero exec <prompt> Go CLI command through the new runtime with a deterministic offline provider until the real Go provider lands in the next PR.

Tests

  • git diff --check
  • go test ./...
  • bun run build:go
  • bun run smoke:go
  • bun run typecheck
  • bun test ./tests --timeout 15000
  • bun run build
  • bun run smoke:build

Notes

The Go zero exec provider is intentionally offline in this PR. It proves the CLI -> agent runtime path without adding the OpenAI-compatible provider in the same review. The next PR should replace that provider stub with real provider/config wiring.

Summary by CodeRabbit

  • New Features

    • Interactive agent with streaming responses that supports multi-turn tool calls, emits incremental text and usage updates, and enforces configurable permission modes for tool execution.
    • Tool discovery/advertising with ordered tool-call handling.
    • New CLI command "zero exec" to run a prompt and print the agent's final answer.
  • Chores / Tests

    • Added comprehensive unit tests covering streaming behavior, tool execution, permission handling, CLI execution, and error scenarios.

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Typecheck: bun run typecheck
  • [pass] Tests: bun run test
  • [pass] Build: bun run build
  • [pass] Smoke build: bun run smoke:build

Scope

Head: 7d91a7df0376
Changed files (5): internal/agent/loop.go, internal/agent/loop_test.go, internal/agent/types.go, internal/cli/app.go, internal/cli/app_test.go

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 889f9092-6d46-4b63-97d9-a5809b60e2c7

📥 Commits

Reviewing files that changed from the base of the PR and between ecaf73c and 7d91a7d.

📒 Files selected for processing (2)
  • internal/cli/app.go
  • internal/cli/app_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/cli/app.go
  • internal/cli/app_test.go

Walkthrough

Adds an agent runtime (streaming collection, ordered tool-call extraction and execution with permission modes, message-copying) with unit tests, and integrates it into the CLI as a new zero exec command using an offline provider.

Changes

Agent system and CLI exec integration

Layer / File(s) Summary
Agent type system and contracts
internal/agent/types.go
Defines Role, Message, ToolCall, ToolDefinition; streaming EventType/StreamEvent; Provider interface; PermissionMode constants; Options with callbacks; ToolResult, Usage, CompletionRequest, and Result.
Agent streaming loop and tool execution
internal/agent/loop.go
Implements Run, collectTurn, buildToolCalls, executeToolCall, toolDefinitions, isAdvertised, and copyMessages: iteratively collects streamed assistant text and tool calls, executes tools with permission logic via registry, appends tool outputs as tool messages, and handles max-turns and error paths.
Agent loop test coverage
internal/agent/loop_test.go
Adds mockProvider and tests for final answer assembly, OnText/OnUsage callbacks, multi-turn tool execution, permission gating (Ask vs Unsafe), and MaxTurns termination; includes file-I/O helpers for tool tests.
CLI exec command implementation
internal/cli/app.go
Adds exec subcommand and runExec: validates prompt, resolves workspace, builds core tools registry, invokes agent.Run with offlineProvider, prints FinalAnswer, and returns specific exit codes. offlineProvider emits one ready text event and EventDone.
CLI exec command tests
internal/cli/app_test.go
Extends help assertions to include exec; adds tests that zero exec <prompt> prints the offline response and that zero exec without prompt fails with exit code 2 and "Prompt required".

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Run
  participant collectTurn
  participant Provider
  participant executeToolCall
  participant Registry
  Client->>Run: Run(ctx,prompt,provider,options)
  Run->>Provider: StreamCompletion(CompletionRequest with Messages+Tools)
  Provider->>collectTurn: StreamEvent stream
  collectTurn-->>Run: assistant text + ordered ToolCalls
  Run->>Run: append assistant Message (ToolCalls)
  alt Has tool calls
    Run->>executeToolCall: execute ToolCall
    executeToolCall->>Registry: RunWithOptions(permission)
    Registry-->>executeToolCall: ToolResult(status,output)
    executeToolCall-->>Run: ToolResult
    Run->>Run: append RoleTool message with ToolCallID
    Run->>Provider: next turn (stream)
  else No tool calls
    Run-->>Client: Result{FinalAnswer}
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Gitlawb/zero#45: Prior PR that added CLI scaffolding; related to this PR's exec command integration.

Suggested reviewers

  • gnanam1990
  • anandh8x
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add Go M0 agent runtime' directly reflects the main change: introducing a new agent runtime system for Go with core types, streaming loop logic, and CLI integration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/go-m0-agent-runtime

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/cli/app_test.go (1)

95-110: ⚡ Quick win

Add exec tests for quoted-empty and whitespace-only prompts.

Current coverage only checks missing args; it should also assert rejection for Run([]string{"exec", ""}, ...) and whitespace-only prompt input.

🧪 Suggested test extension
 func TestRunExecRequiresPrompt(t *testing.T) {
 	var stdout bytes.Buffer
 	var stderr bytes.Buffer

 	exitCode := Run([]string{"exec"}, &stdout, &stderr)

 	if exitCode != 2 {
 		t.Fatalf("expected exit code 2, got %d", exitCode)
 	}
 	if stdout.Len() != 0 {
 		t.Fatalf("expected empty stdout, got %q", stdout.String())
 	}
 	if !strings.Contains(stderr.String(), "Prompt required") {
 		t.Fatalf("expected prompt error, got %q", stderr.String())
 	}
 }
+
+func TestRunExecRejectsEmptyOrWhitespacePrompt(t *testing.T) {
+	for _, args := range [][]string{
+		{"exec", ""},
+		{"exec", "   "},
+	} {
+		var stdout bytes.Buffer
+		var stderr bytes.Buffer
+		exitCode := Run(args, &stdout, &stderr)
+		if exitCode != 2 {
+			t.Fatalf("expected exit code 2 for args %#v, got %d", args, exitCode)
+		}
+		if stdout.Len() != 0 {
+			t.Fatalf("expected empty stdout for args %#v, got %q", args, stdout.String())
+		}
+		if !strings.Contains(stderr.String(), "Prompt required") {
+			t.Fatalf("expected prompt error for args %#v, got %q", args, stderr.String())
+		}
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/app_test.go` around lines 95 - 110, Extend
TestRunExecRequiresPrompt to also call Run with Run([]string{"exec", ""},
&stdout, &stderr) and Run([]string{"exec", "   "}, &stdout, &stderr), verifying
each returns exit code 2, produces empty stdout, and writes a "Prompt required"
error to stderr; you can add them as separate subtests or sequential assertions
within TestRunExecRequiresPrompt and reuse the stdout/stderr buffers (resetting
them between calls) to locate the behavior around the Run function.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cli/app.go`:
- Around line 69-74: The current arg-check only verifies len(args) and allows
empty or whitespace-only prompts (e.g., `zero exec ""`) to proceed; update the
validation where args is inspected (the block using fmt.Fprintln(stderr, "Prompt
required...") and the similar block at the later check) to trim whitespace from
the joined or first argument (strings.TrimSpace) and treat an empty result as
missing input: write the same error to stderr and return the same exit code(s)
as present now so whitespace-only or empty prompts are rejected before runtime
invocation.

---

Nitpick comments:
In `@internal/cli/app_test.go`:
- Around line 95-110: Extend TestRunExecRequiresPrompt to also call Run with
Run([]string{"exec", ""}, &stdout, &stderr) and Run([]string{"exec", "   "},
&stdout, &stderr), verifying each returns exit code 2, produces empty stdout,
and writes a "Prompt required" error to stderr; you can add them as separate
subtests or sequential assertions within TestRunExecRequiresPrompt and reuse the
stdout/stderr buffers (resetting them between calls) to locate the behavior
around the Run function.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9c81e4df-ab26-40af-924f-c67dcb470fa6

📥 Commits

Reviewing files that changed from the base of the PR and between af1d292 and ecaf73c.

📒 Files selected for processing (5)
  • internal/agent/loop.go
  • internal/agent/loop_test.go
  • internal/agent/types.go
  • internal/cli/app.go
  • internal/cli/app_test.go

Comment thread internal/cli/app.go
@Vasanthdev2004

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Vasanthdev2004 Vasanthdev2004 dismissed coderabbitai[bot]’s stale review June 4, 2026 10:16

Fixed in 7d91a7d: exec now trims prompts and rejects empty or whitespace-only input with tests.

@anandh8x anandh8x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's good

  • Clean ReAct spine: Run (internal/agent/loop.go:23-99) drives a maxTurns-bounded loop, copies the message log defensively on every CompletionRequest (copyMessages, internal/agent/loop.go:214-224), and returns a Result with FinalAnswer, Turns, and the full message log. Provider errors and max-turns termination both return the partial message log so a TUI can render what happened.
  • collectTurn (internal/agent/loop.go:103-145) handles every event type correctly: EventText accumulates and fans out via OnText; EventToolCallStart / EventToolCallDelta register tool calls in arrival order via the order []string + pending map[string]*pendingToolCall pair, and a missing Start doesn't break Delta accumulation. EventToolCallEnd is consumed but intentionally a no-op (the loop terminates on EventDone or ctx.Done).
  • Permission model is the right shape: PermissionModeAuto advertises only PermissionAllow tools via isAdvertised (internal/agent/loop.go:202-209); PermissionModeAsk advertises everything non-Deny (so the model can call write tools, but the registry's RunWithOptions will refuse without a grant); PermissionModeUnsafe grants every tool. executeToolCall (internal/agent/loop.go:172-200) layers an unconditional grant on top of PermissionAllow tools, so reads always run regardless of mode.
  • Tool call results are appended to the message log as RoleTool with the ToolCallID, and the provider sees them on the next turn's CompletionRequest. JSON parse errors are returned as tool results (not Go errors), which is the right signal for the model — the loop continues and the provider can self-correct.
  • The offlineProvider (internal/cli/app.go:118-131) is a clean stub: it picks the most recent RoleUser message, echoes it back as EventText, and emits EventDone. Good for proving the CLI ↔ runtime path end-to-end without pulling in a real provider in this PR.
  • CLI: zero exec joins args with a single space, trims, rejects empty / whitespace-only / missing prompts with exit code 2 and a Prompt required. message (internal/cli/app.go:69-83). Tests iterate over the three failure cases via t.Run subtests, addressing CodeRabbit's nitpick directly.
  • Test coverage: text deltas, usage events, tool execution through the registry, permission denial in Ask mode, grant in Unsafe mode, max-turns termination, and the full zero exec happy path with the offline provider. CI green on all four matrices; CodeRabbit reports SUCCESS on the latest SHA (7d91a7d).

Observations (non-blocking)

  1. System prompt is a hard-coded package constant. defaultSystemPrompt (internal/agent/loop.go:11) is the only prompt the agent ever sees, and Run builds the seed message list with it inline. A real provider will want to inject the workspace path, the available tool list, repo conventions, etc. The minimal fix is a SystemPrompt field on Options (with a fallback to defaultSystemPrompt); a slightly bigger fix is to let the caller pass a fully-formed seed []Message and have Run only append the user message. The latter is more flexible and matches how OpenAI-style agents usually do it.

  2. PermissionModeAuto and PermissionModeAsk are currently identical at the runtime level. isAdvertised distinguishes them (Auto advertises only PermissionAllow; Ask advertises everything non-Deny) and executeToolCall grants PermissionAllow tools unconditionally, but neither mode grants a PermissionPrompt tool — both require PermissionModeUnsafe for that. The intent is presumably "Ask = interactively prompt the user, Auto = silently deny", with the interactive prompt living in the TUI layer that consumes OnToolCall and chooses to abort vs. re-run with PermissionGranted: true. Worth a one-line comment on the PermissionMode constants in internal/agent/types.go:69-75 so a future reader doesn't assume Ask should auto-grant anything, and a follow-up test that asserts PermissionModeAuto + a PermissionPrompt tool returns the same Permission required for ... message we already test for Ask.

  3. runExec runs the agent on context.Background(). No timeout, no signal handling, no --max-duration flag (internal/cli/app.go:97-101). A runaway agent loop or a slow model would tie up the terminal indefinitely; Ctrl+C would kill the process but not surface the partial Result. Pairing context.WithCancel with a SIGINT handler that prints result.FinalAnswer and result.Turns on interrupt would make the CLI usable for real prompts.

  4. copyMessages is called per turn and at every error/return path. For a 12-turn run with a 50-message log, that's 12+ deep copies, each allocating a fresh []ToolCall per message. Not a perf concern at the volumes involved today, but a single owned messages slice plus a shallow Result.Messages capture at the end would be cheaper. If you ever stream Messages to a TUI mid-turn, you'll need a clean ownership story anyway.

  5. No test for EventToolCallEnd without a matching EventToolCallStart or EventToolCallDelta. The current collectTurn is resilient to that (it auto-creates the pending entry on the first event for an id), but the test mock always sends the full Start → Delta → End sequence. A one-line case where only End is sent (and the resulting tool call has empty args) would document the resilience.

  6. The OnToolCall callback is fired before executeToolCall, but OnToolResult only fires after the synchronous execution completes. A tool that takes 30 seconds (e.g. bash with a long timeout) would block the UI from updating for the full duration. If the TUI wants a "running" indicator, the runtime would need to either expose an OnToolStart channel that the tool's goroutine signals, or move tool execution into a goroutine. For v1 with the TUI still ahead, fine; flag it now so the design choice is conscious.

  7. runExec joins args with a single space. prompt := strings.TrimSpace(strings.Join(args, " ")) (internal/cli/app.go:85) loses the user's whitespace, so zero exec "hello world" becomes hello world (after the shell strips the quotes) which happens to be the same in this case, but zero exec 'hello\nworld' would arrive as hello\nworld (literal backslash-n) and any multi-line prompt construction via argv will not survive. A future --prompt-file - or a - stdin convention would round this out.

  8. offlineProvider ignores request.Tools entirely. That's fine for a stub, but the stub doesn't exercise the tool-advertising path on the provider side — a real provider that mis-handles the Tools slice (e.g. drops a tool, renames a parameter) wouldn't be caught. The mockProvider in loop_test.go does receive the full CompletionRequest and stores it in requests, so a tool-advertising regression would be caught there; worth a follow-up assertion in TestRunExecutesToolCallThroughRegistry that the second-turn request includes the read_file ToolDefinition.

No blockers. The runtime is a clean foundation for the real provider; the only change I'd want to see before this lands in front of users is making the system prompt configurable (observation #1). The rest are good follow-ups for when the TUI / real provider land.

@Vasanthdev2004 Vasanthdev2004 merged commit b8e194d into main Jun 4, 2026
6 checks passed
@Vasanthdev2004 Vasanthdev2004 deleted the feat/go-m0-agent-runtime branch June 4, 2026 10:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants