feat: Switchyard-native goal mode (self-driven + claude/codex native passthrough + durable resume)#5
Merged
Merged
Conversation
Switchyard's claude provider passed Claude Code's stream-json through raw, so tool_use blocks (inside `assistant` messages) and tool_result blocks (inside `user` messages) never became tool cards: the per-turn path emitted them as unrecognized raw items and the persistent path dropped the `assistant` event entirely. Both paths now curate the stream into normalized events the GUI renders + monitors live: - tool_use -> a `tool_call` card (id = Claude tool_use.id, name, arguments) - tool_result -> merged into that card by id (output + completed/failed) - thinking / redacted_thinking -> a `reasoning` item - text still streams token-by-token; the raw stdout protocol is no longer mirrored to the terminal channel (it was flooding the live card) — only genuine non-JSON output is. Applies to both run_claude_turn (turn.rs) and the persistent ClaudeLiveInstance (live.rs). Background Bash (run_in_background) and the Task subagent are just tool_use calls, so they now surface as cards too. Tests: new fake_claude fixture + claude_integration.rs (deterministic, CI) asserting text streaming, tool_use->tool_call, tool_result merge-by-id, no protocol terminal mirror, and failure propagation; unit tests for the content-block helpers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lightweight cross-turn awareness for Claude Code background tasks (Step 2). A Bash with `run_in_background` survives across turns on the persistent provider instance (the core is warm-started as a live instance), but its launch tool_result "completes" immediately, so the card read as a finished call. - ToolCard badges such tools "后台运行中" (amber) and shows a running status instead of a green completed check, so a background task reads as long-lived; the badged card persists in history, making it visible across turns. - The live-execution card summarizes "N 个后台任务运行中". Detection is frontend-only off the tool_call arguments (run_in_background:true), which Step 1's Claude tool_use normalization already surfaces. Reliable cross-turn completion tracking (auto-clearing the count when a task is killed) needs Bash<->BashOutput/KillShell shell-id correlation — deferred to the full monitor panel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… output Two kohaku-adapter fixes, both entangled in turn.rs: - Thinking level was dropped: kohaku_runtime_args ignored thinking_level (`_thinking_level`). It now emits `kt run --reasoning <effort>`, normalized to KT's vocabulary (max→xhigh; auto/unknown→omit) and independent of --llm. Doc comments + the GUI capability hint updated. - Direct-tool output was dropped: KT keys a DIRECT (foreground) tool's body under metadata.output (only sub-agents/background use `result`), but the adapter read only `result` — so every kt tool (read, edit, write, multi_edit, …) reached the GUI with empty output, hiding tool bodies and file diffs. Now reads `output` first, then `result`. fake_kt uses the real `output` key + an edit tool whose output carries a unified diff; kohaku_integration asserts the body (and the diff) survive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The session loader caps the initial event pull (full load = oldest 1000 events). In a long session most turns' tool/diff events fall outside that window, so renderTurnEvents finds no activity and their cards vanish — even though the events are persisted. New `get_turn_events(turn_id)` Tauri command (backed by the existing EventLog::list_events + events(turn_id) index). When a historical turn row scrolls into view (the list is virtualized) with activity but no loaded events, the frontend fetches just that turn's events and merges them in — deduped by event_id, at most once per turn, reset on session switch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Session-level goal mode state: goal text, goal_status lifecycle, goal_iteration counter, and a persisted goal_plan (Vec<GoalPlanItem>) tracked outside the chat. All #[serde(default)] so existing stored sessions deserialize without migration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add GOAL_DONE_MARKER (self-signal completion) and GOAL_PLAN_BEGIN/END delimiters, plus parse_goal_plan() which reads the last plan block's markdown checkbox lines into (text, done) pairs. Kept decoupled from session types (plain tuples). Unit-tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
goal_runner: provider-agnostic goal loop (run_goal_loop) with no hard caps by default (unbounded until achieved/stopped; stuck-guard only), evaluator-gated completion (Claude-Code-style read-only judge turn) + self-signal fast-path + feedback re-injection, and native /goal passthrough (run_native_slash_goal) for Claude. Maintains + persists session.goal_plan each turn. router: run_routed_turn_observable_with_display_and_prompt_injection adds an optional display_message so goal turns store a clean bubble while the verbose autonomous-mode instructions drive the model. runtime_events: GoalIterationStarted / GoalCompleted / GoalPlanUpdated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add match arms for GoalIterationStarted / GoalCompleted / GoalPlanUpdated: CLI host-job mapper treats them as non-job events; TUI surfaces brief goal progress lines in the event log. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tauri start_goal (blocking goal run on the persistent Core instance; native passthrough for Claude, self-driven loop otherwise, with a fresh isolated evaluator provider) and stop_goal (cooperative cancel). GoalRunState + ActiveTurnState mutual exclusion; ensure_persistent_core_instance helper shared with run_turn; goal RuntimeEvent arms in the runtime bridge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Register /goal (activate/stop/status) as a slash command with autocomplete. ControlCenter Goal card: activate/stop + live plan checklist (persisted goal_plan). ChatArea header shows a live goal elapsed timer (no iteration counter). App.tsx wires start/stop + consumes goal runtime events (iteration/status/plan) live. RenderHelpers strips the goal plan block + completion marker from chat bubbles (surfaced only in the plan panel). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completion now relies on the agent's own <<<SWITCHYARD_GOAL_DONE>>> marker (codex/agy-style) — one turn per iteration, no doubled cost. The Claude-Code-style evaluator stays available via run_goal_loop's judge arg but is opt-in; start_goal passes None. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Goals now survive an app restart. Extracted start_goal's body into drive_goal_run (fetches state from AppHandle so it can be spawned standalone). New resume_active_goals command scans the current workspace for sessions left goal_status=running and re-enters the goal loop for each (continuing from the persisted goal + goal_iteration + goal_plan); idempotent via GoalRunState. Frontend calls it after loadSessions. Uses session-persisted state rather than a RuntimeDb host_job since GUI goals run in-process with no separate owning worker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New LiveInstance::drive_native_goal (default: unsupported) implemented for codex: sends thread/goal/set (active), streams every autonomous continuation turn's events (re-minting the canonical turn_id per turn/started so the caller can persist one Switchyard Turn per codex turn), and stops on a terminal thread/goal/updated status (complete/blocked/budget_limited/usage_limited). Cancel interrupts the in-flight turn (turn/interrupt) then pauses the goal (thread/goal/set status:paused). Spike: server requests auto-acked (permissive); pure protocol helpers (status classification, param builders, goal/updated parsing) are unit-tested. Consumer-side per-turn persistence + start_goal wiring is the next increment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
run_native_codex_goal checks out the persistent codex instance, calls drive_native_goal, and persists each autonomous continuation turn as a canonical Turn (map_provider_event for events, one Turn per re-minted turn_id), stopping on a terminal thread/goal/updated status (complete->achieved, blocked->failed, budget/usage_limited->stopped). start_goal routes codex backends here; claude stays native /goal passthrough, others self-driven. Spike: run-level diffs via the caller's file-watcher; codex keeps its own plan (panel empty). Needs live codex validation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a goal mode to Switchyard: give a high-level objective and the active Core provider runs turn-after-turn until it's achieved or you stop it. Provider-agnostic, with native passthrough where a CLI has its own reachable goal engine.
Investigated all three CLIs' native
/goal(source/binary verified) and mirrored the good parts:<<<SWITCHYARD_GOAL_DONE>>>(codex/agy-style), no hard iteration/time cap (stuck-guard only). Optional Claude-Code-style evaluator turn is available but off by default./goalpassthrough: sends/goal <cond>to the persistent live instance; Claude's own evaluator loop runs within one turn.thread/goal/set(spike): drives codex's own goal engine over the app-server, persisting each autonomous continuation turn as a canonical Turn; stops on thethread/goal/updatedterminal status.runningwhen the app closes auto-resumes on next workspace open (session-persisted state, no RuntimeDb host_job).UI
/goal <cond>//goal stopslash command with autocomplete.Layers touched
session (goal + goal_plan fields) · provider-api (goal markers + plan parse +
LiveInstance::drive_native_goal) · core (goal_runner: self-driven loop, native slash, native codex, router display split, runtime events) · cli/tui (event arms) · gui (start_goal/stop_goal/resume_active_goals+ codex drive) · frontend (slash command, plan panel, timer, bubble strip).Notes
feat/claude-backend-capturecommits (its base, not yet on main).cargo fmt --check,clippy -D warnings, goal unit tests (7 core + 3 codex + sentinel), frontendtsc. Codex-native logic is a spike pending live validation.🤖 Generated with Claude Code