Skip to content

feat: Task Graph Executor#503

Merged
mmogr merged 72 commits into
mainfrom
epic/orchestrator
May 31, 2026
Merged

feat: Task Graph Executor#503
mmogr merged 72 commits into
mainfrom
epic/orchestrator

Conversation

@mmogr
Copy link
Copy Markdown
Owner

@mmogr mmogr commented May 22, 2026

Task Graph Executor — Orchestrator Epic

Closes #496

This PR merges the complete Orchestrator Epic into main. It introduces a local-first Director/Worker DAG execution engine that lets gglib decompose a high-level user goal into a structured task graph, execute each node in parallel with strict resource scoping, checkpoint state to SQLite for resumability, and surface the entire lifecycle through a native Tauri/React UI with optional human-in-the-loop approval gates.

All 6 phases were developed on individual feature branches, reviewed, and squash-merged into epic/orchestrator. Each phase individually passed strict Clippy (-D warnings), cargo fmt, boundary checks, and its own integration test suite before merging.


Phase A — Core Types & Planning (feat/orchestrator-phase-a) #490

Defines the foundational domain model that all later phases build on.

  • TaskGraph / TaskNode with dependency edges, tool allowlists, and worker concurrency caps
  • OrchestratorEvent discriminated union (SSE wire format) covering every lifecycle signal: planning, node lifecycle, tool calls, synthesis, HITL approval requests, and completion
  • PlanningClient trait + LlamaPlanningClient implementation calling the LLM to produce a structured plan from a natural-language goal
  • HitlMode enum: None | ApprovePlan | ApproveEachNode | ApproveTools
  • Merged in feat(orchestrator A): ResponseFormat port, StructuredOutputError, TaskGraph domain model #497

Phase B — Graph Execution Engine (feat/orchestrator-phase-b) #491

The core DAG executor that turns a TaskGraph into a live SSE stream.

  • Topological scheduling with configurable per-graph max_worker_concurrency
  • Per-node WorkerAgent executing its goal within a strict tool allowlist; cannot call tools or access context outside its node scope
  • Node outputs injected as read-only context into downstream dependents
  • Streaming OrchestratorEvents over an mpsc channel — every state change (start, text delta, tool call, compaction, completion, failure) is observable in real time
  • OrchestratorError hierarchy covering planning, execution, and persistence failures
  • Merged in feat(orchestrator B): director plan(), CLI gglib plan, Axum SSE /orchestrator/plan, Tauri preview, eval harness #498

Phase C — SQLite Persistence & Resumability (feat/orchestrator-phase-c) #492

Full checkpoint/resume support backed by gglib-db.

  • OrchestratorRun persisted to SQLite: run ID, status, goal, serialized graph snapshot, partial node outputs, timestamps
  • RunRepository with create, update_status, save_node_output, list, get operations
  • Executor checkpoints after every node completion so a crash mid-run loses at most one node
  • resume_run(run_id) replays already-completed node outputs as synthetic events, then continues live execution from the first incomplete node
  • REST endpoints: POST /api/orchestrator/run, GET /api/orchestrator/runs, GET /api/orchestrator/runs/:id, POST /api/orchestrator/runs/:id/resume
  • Merged in feat(orchestrator): Phase C — executor, compaction, synthesis, CLI/Axum/GUI wiring (#492) #499

Phase D — Human-in-the-Loop Approval Gates (feat/orchestrator-phase-d) #493

Blocking approval checkpoints at configurable granularity.

  • ApprovalGate middleware intercepts execution at plan, node, or tool level depending on HitlMode
  • ApprovalStore maps live approval requests to oneshot channels; the executor suspends and holds the SSE stream open while waiting
  • POST /api/orchestrator/approve/:id accepts { decision: approve | approve_with_edits | reject, edited_graph?, reason? }
  • approve_with_edits replaces the live graph mid-run; reject terminates the run with a user-facing error message
  • Run status transitions to awaiting_approval in SQLite while suspended; fully resumable after approval
  • Merged in feat(orchestrator): Phase D — HITL approval gates, SQLite persistence, run-resumption (#493) #500

Phase E — Virtual Proxy Routing & OpenWebUI Integration (feat/orchestrator-phase-e) #494

Transparent integration with the existing proxy layer and OpenWebUI.

  • Virtual model gglib-orchestrator registered in the proxy routing table
  • Incoming /v1/chat/completions requests addressed to gglib-orchestrator are intercepted and routed to the orchestrator engine rather than a llama.cpp backend
  • Response is re-encoded as a standard OpenAI streaming chat completion so any OpenAI-compatible client (including OpenWebUI) receives well-formed SSE without knowing about the orchestrator internals
  • HITL approval requests surface as assistant messages with structured JSON, allowing approval via chat reply in OpenWebUI
  • Integration tests cover end-to-end virtual model routing with SSE frame validation
  • Merged in feat(proxy): Phase E — virtual model routing in the OpenAI-compatible proxy #501

Phase F — Native Tauri/React UI (feat/orchestrator-phase-f) #495

Full native frontend for the orchestrator, following all existing UI conventions.

  • OrchestratorContext — reducer over the full OrchestratorEvent stream; state includes phase, per-node states, synthesis text, pending approval, and runs list
  • useOrchestrator hook — bridges SSE → context dispatch, manages AbortController lifecycle, exposes run / resume / cancel / approve / loadRuns
  • DagView — topologically-sorted indented tree of TaskGraph nodes with live status badges; no new heavy dependencies
  • NodePanel — collapsible per-node panel: goal, tool allowlist, streaming text, tool call log with timing, output preview, error display
  • HitlApprovalModal — plan/node/tool approval modals with plan JSON viewer, optional approve_with_edits flow with JSON validation, and reject-with-reason path
  • RunsList — resumable runs entry with status filter tabs and click-to-resume
  • #orchestrator hash route wired in App.tsx; HTTP transport only — no isTauriApp branching, no new #[tauri::command]
  • 21 new reducer unit tests; all 606 frontend tests pass; zero TypeScript errors
  • Merged in feat(orchestrator): Phase F — native frontend polish, DAG visualization, HITL modals, run management #502

Phase G — Hierarchical Subgraphs, Role Catalog & Team Events (feat/orchestrator-phase-g) #508

Foundation for multi-tier agent hierarchies.

  • TaskNodeKind enum: Leaf (default) | Team { subgraph: Box<TaskGraph> } — serde snake_case, #[serde(default)] for backward compat with Phase F runs
  • RoleId / RoleSpec / RoleCatalog — 7 built-in specialist roles (researcher, red-team, fact-checker, writer, editor, critic, synthesizer) with system prompt fragments, default tool allowlists, and temperature hints
  • TeamStarted { team_id, role } / TeamSynthesized { team_id, compacted_output } SSE events
  • validate_acyclic() recurses into Team subgraphs; total_node_count() aggregates across depth
  • Phase F regression fixture verifies backward-compat deserialization
  • Merged in feat(core): Phase G — hierarchical subgraphs, role catalog, TaskNodeKind #511

Phase H — Hierarchical Planner (Chief of Staff + Department Directors) (feat/orchestrator-phase-h) #506

Two-tier planning layer that decomposes a goal into departments before assigning leaf tasks.

  • chief_of_staff::brief() — single structured-output LLM call returning ≤5 DepartmentBrief structs; validates, deduplicates, and filters roles to catalog entries; graceful fallback on failure
  • planner::plan() — new canonical entry point; fans out one director::plan per department in parallel via JoinSet; assembles results into nested TaskGraph (Team nodes + synthesizer leaf)
  • director::plan() accepts optional DepartmentBrief to inject context and round-robin assign roles to leaf nodes
  • 4 integration tests: 3-dept × 4-leaf fan-out, single-dept, empty CoS fallback, round-robin role assignment
  • Merged in feat(agent): Phase H — Hierarchical Planner (Chief of Staff + Department Directors) #512

Phase I — Executor Recursion & spawn_subteam Dynamic Interrupt (feat/orchestrator-phase-i) #504

Workers can dynamically request child sub-teams at runtime.

  • spawn_subteam tool definition registered in worker tool allowlist; SpawnCapturingExecutor intercepts calls into a per-worker sink
  • Post-wave spawn-request loop: optional HITL gate (ApprovalKind::SpawnSubteam), plan() call, SubteamSpawned event, recursive run_wave_loop
  • max_team_depth guard (default: 9) prevents unbounded recursion; MaxDepthExceeded error variant
  • Tests: 3-dept/12-leaf hierarchical run; auto-approve spawn path; HITL-gated spawn path
  • Merged in feat(orchestrator): Phase I — executor recursion and spawn_subteam dynamic interrupt #513

Phase J — NodeBudget & warn-only RunCostEstimate (feat/orchestrator-phase-j) #509

Soft advisory budget and token-cost estimator — execution always proceeds.

  • NodeBudget enum (Solo / SmallTeam / TaskForce / Department / Custom) with const upper_bound()
  • RunCostEstimate { node_count, est_tokens, est_wall_seconds } SSE event emitted after PlanProposed; tracing::warn! fires only when plan exceeds budget — approve is never disabled
  • estimator.rs — pure cost module, 2000 tokens/node at 50 tps, two doc-tests
  • Frontend: costEstimate session field in OrchestratorContext; yellow advisory banner in HitlApprovalModal and Orchestrator page when estWallSeconds > 60 or node count > 80% of budget
  • 7 new Vitest tests for banner show/hide logic; approve button always enabled
  • Merged in feat(orchestrator): Phase J — NodeBudget + warn-only RunCostEstimate (#509) #514

Phase K — Conversational Steering & GraphDiff Preview (feat/orchestrator-phase-k) #507

Side-channel note injection during live runs and LLM-driven graph mutation with visual preview.

  • GraphDiff enum (7 variants: AddNode, RemoveNode, SplitNode, RerouteEdge, SetRole, SetTools, WrapInTeam) with apply_diff clone-and-swap rollback
  • NoteQueueArc<Mutex<VecDeque<String>>> injected per run; wave-drain loop applies queued steering notes before each wave
  • POST /api/orchestrator/steer — accepts { graph, instruction }, returns { diff: GraphDiff } via LLM structured output
  • POST /api/orchestrator/runs/{run_id}/note — appends instruction to in-flight run queue; 202 Accepted or 404
  • SteeringPanel component — instruction textarea, diff preview with colour-coded DiffBadge (green/red/amber), Apply / Discard; applyDiff pure helper also used in HitlApprovalModal
  • 9 Rust apply_diff tests (7 happy-path + 2 error); 10 Vitest component tests
  • Merged in feat(orchestrator): Phase K — Conversational Steering + GraphDiff Preview #515

Phase L — Casting Sheet, Collapsible Team Canvas & NodePanel Quick Actions (feat/orchestrator-phase-l) #505

Frontend polish; no backend changes.

  • CastingSheet — actor-card grid; recursive collectLeafNodes() traversal through Team subgraphs; 7-role icon map with User fallback; tool/dependency chips; full keyboard/ARIA accessibility
  • DagView rewrite — team nodes as collapsible group headers with aria-expanded; sessionStorage persistence of expanded-team set keyed by runId
  • NodePanel quick actions — 4 context-aware buttons (Add critic, Split into 3 parallel, Wrap in team, Re-run with feedback); fires /note when runId present, /steer with inline diff preview otherwise
  • Orchestrator page view toggle (Team Canvas / Casting Sheet) with aria-pressed
  • OrchestratorPlanPreview.tsx (legacy Phase C page) deleted
  • 28 new Vitest tests across CastingSheet, DagViewTeams, NodePanelQuickActions; 651 frontend tests passing
  • Merged in feat(orchestrator): Phase L — Casting Sheet, Team Canvas, NodePanel Quick Actions #516

Phase M — Time-Travel Rewind to a Previous Wave (feat/orchestrator-phase-m) #510

Rewind a completed run to an earlier wave checkpoint and re-execute from there.

  • Every persisted event tagged with wave_index; WaveCompleted { wave_index, node_count } SSE event emitted at the end of each wave
  • OrchestratorRepositoryPort::truncate_events_after_wave — new port method; SQLite implementation deletes events with wave_index > target; wave_index INTEGER NOT NULL DEFAULT 0 column with silent additive migration
  • OrchestratorConfig::rewind_to_wave: Option<u32> — executor resumes wave numbering from rewind_to_wave + 1
  • POST /api/orchestrator/runs/{id}/rewind — validates run is not active (409 if Running/AwaitingApproval), truncates DB, re-executes via SSE stream
  • WaveScrubber component — horizontal waypoint row (W0, W1, …) from the event log; confirmation alertdialog before rewind; hidden when fewer than 2 waves exist; disabled prop blocks interaction during active run
  • rewindOrchestratorRun() client helper; useOrchestrator exposes rewind(runId, waveIndex, steeringNote?)
  • DB test: inserts events across 3 waves, truncates after wave 0, asserts only wave-0 events remain; 8 Vitest component tests; 659 frontend tests passing
  • No breaking changes to existing runs (DEFAULT 0 handles legacy rows)
  • Merged in feat(orchestrator): Phase M — time-travel rewind to a previous wave #517

GUI Cleanup & CLI Orchestrator Refactor (post-merge polish)

After all sub-PRs were merged into epic/orchestrator, two final workstreams cleaned up loose ends and added a first-class CLI surface for the engine.

GUI — Remove redundant top-bar button (430ddc3)

The top-bar "Orchestrator" button became redundant once the DAG renders inline inside the chat thread (Phase K/L). Removed OrchestratorButton from Header and the standalone nav route; the in-chat CouncilEventRenderer is now the single rendering surface.

CLI — 6-phase orchestrator refactor (9720ef1d94a5fe)

Built a first-class council command family in gglib-cli that fully surfaces the DAG executor from the terminal:

Commit What landed
9720ef1 presentation/dag.rs — shared DAG tree renderer; node_color(status) stable ANSI colours on every PlanProposed event
deb9dc6 CouncilCmd enum (Run, List, Show, Resume, Rewind); split council.rs into handlers/council/ directory module (one file per subcommand + shared approve.rs / render.rs)
880ae3a --approval-timeout <SECS> + --approval-timeout-action <reject|approve>; [e]dit plan gate opens JSON in $EDITOR; async stdin via tokio::io::stdin() + AsyncBufReadExt
e52db27 --json flag — every CouncilEvent serialised as one JSONL line on stdout; all ANSI/progress output suppressed from stdout; requires --hitl none
7808241 presentation/input.rs background stdin router: /note <text>NoteQueue; other lines → mpsc::UnboundedReceiver for HITL prompts; full rewind.rs (truncate events, reset nodes, pre-seed note queue, re-execute)
d94a5fe README rewrite (new "Orchestrator (council)" section; remove stale debate-REPL docs); mod.rs architecture doc; commands.rs doc cleanup; cargo fmt

Quality gates (all phases)

  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo fmt --check
  • ./scripts/check_boundaries.sh
  • cargo test --workspace
  • npm run test:run (606 tests) ✅

mmogr added 19 commits May 22, 2026 15:16
…kGraph domain model

- Add ResponseFormat enum to gglib-core::ports::llm_completion (JsonSchema + Grammar variants)
- Extend LlmCompletionPort::chat_stream with response_format: Option<&ResponseFormat>
- Update LlmCompletionAdapter in gglib-runtime to inject response_format/grammar into
  llama-server request body; all existing callers pass None (no behaviour change)
- Add StructuredOutputError to gglib-core::ports::structured_llm (Stream/Parse/MaxRetries)
- Add get_structured<T>() to gglib-agent::structured_output: JSON-schema-constrained LLM
  call with retry-with-feedback loop (collects stream deltas, parses, retries on error)
- Add orchestrator domain model to gglib-core::domain::orchestrator:
  - task_graph: NodeId, NodeStatus, HitlMode, TaskNode, TaskGraphError, TaskGraph
    with validate_acyclic (DFS cycle detection), validate_tool_allowlist, roots,
    ready_nodes; MAX_NODES=8, MAX_DEPTH=3 safety limits
  - events: OrchestratorEvent (PlanProposed through OrchestratorComplete),
    ApprovalKind (Plan/Node/Tool)
- Re-export all new types from crate root (lib.rs) and module tables regenerated

Closes #490
feat(orchestrator A): ResponseFormat port, StructuredOutputError, TaskGraph domain model
…SSE /orchestrator/plan, Tauri preview page, eval harness

- crates/gglib-agent/src/orchestrator/prompts.rs: DIRECTOR_SYSTEM_PROMPT +
  director_plan_schema() with 3 few-shot examples
- crates/gglib-agent/src/orchestrator/director.rs: plan() async fn with
  PlanError, replan loop (max_replans), OrchestratorEvent SSE emission
- crates/gglib-agent/src/orchestrator/mod.rs: pub re-exports
- crates/gglib-agent/src/lib.rs: pub mod orchestrator; + cfg_attr unused_crate_dependencies
- crates/gglib-agent/Cargo.toml: thiserror dep; gglib-runtime + reqwest dev-deps
- crates/gglib-agent/tests/orchestrator_plan_eval.rs: eval harness, 20-prompt
  corpus, #[ignore]-gated, soft 70% validity floor
- crates/gglib-axum/src/handlers/orchestrator/mod.rs: plan_sse handler,
  PlanRequest, semaphore guard, SSE stream via ReceiverStream
- crates/gglib-axum/src/handlers/mod.rs: pub mod orchestrator
- crates/gglib-axum/src/routes.rs: POST /api/orchestrator/plan
- crates/gglib-cli/src/handlers/plan.rs: execute(), render_tree(), render_mermaid()
- crates/gglib-cli/src/handlers/mod.rs: pub mod plan
- crates/gglib-cli/src/commands.rs: Commands::Plan variant
- crates/gglib-cli/src/dispatch.rs: dispatch arm for Plan
- src/types/orchestrator.ts: OrchestratorEvent, TaskGraph, TaskNode TS types
- src/services/clients/orchestrator.ts: planOrchestrator() SSE client
- src/pages/OrchestratorPlanPreview.tsx: Tauri WebView plan preview page
- src/App.tsx: hash-based routing to OrchestratorPlanPreview

No execution logic — planning only (Phase B scope).
All three surfaces in same commit (GUI Parity Principle).
gglib-core has 2 pre-existing clippy issues on epic/orchestrator base (not
introduced by this PR).
- events.rs: wrap CoT in backticks in doc comment (doc_markdown)
- task_graph.rs:386: remove redundant .clone() on consumed String (redundant_clone)
- task_graph.rs:640: dereference &&str before to_string() (inefficient_to_string)
…l harness

gglib-agent/orchestrator/director.rs:
- use Self::ValidationFailed instead of PlanError:: (use_self)
- HashMap → \`HashMap\` in doc comment (doc_markdown)
- allow literal_string_with_formatting_args on intentional template replace
- last_error.clone_from(&err) x3 (assigning_clones)

gglib-agent/orchestrator/prompts.rs:
- HashMap → \`HashMap\` in module doc (doc_markdown)

gglib-agent/structured_output.rs:
- use Self { .. } instead of gglib_core::AssistantContent { .. } (use_self)

gglib-agent/tests/orchestrator_plan_eval.rs:
- #[ignore = "..."] reasons on both test fns (ignore_without_reason)
- split_whitespace().count() instead of collect+len (needless_collect)
- #[allow(cast_precision_loss)] on summarise + eval_full_corpus (usize→f64)
- f64::from(total_replans) instead of as cast (cast_lossless)
feat(orchestrator B): director plan(), CLI gglib plan, Axum SSE /orchestrator/plan, Tauri preview, eval harness
…um/Tauri wiring (#492)

- Add executor.rs with topological wave loop and strict context scoping
- Add compaction.rs for hard-error post-worker output compaction
- Add synthesis.rs for final leaf-node synthesis pass
- Wire POST /api/orchestrator/run SSE endpoint in gglib-axum
- Wire 'gglib orchestrate <goal>' CLI subcommand with terminal rendering
- Update OrchestratorPlanPreview.tsx with Execute button, NodePanel components,
and synthesis streaming display (GUI parity)
- Add integration tests (5 tests) covering happy path, topological order,
fail-fast, PlanApproved, and SynthesisStart/Complete events
- All clippy (strict -D warnings) and fmt checks pass
- All boundary checks pass
feat(orchestrator): Phase C — executor, compaction, synthesis, CLI/Axum/GUI wiring (#492)
…, run-resume (#493)

## What

Implements the three acceptance criteria from issue #493:

### HITL approval gates (ApprovePlan / ApproveEachNode / ApproveTools)
- New  variants wired into
- Executor pauses at gates, emits  event with unique
  , then parks on a oneshot channel until resolved
-  handled in
  both plan and node gates
-  trait +
  (DashMap-backed) concrete implementation

### SQLite persistence
-  +  tables added to schema
-  trait (8 methods)
-  implementing the full trait with unit tests
- Executor persists run status transitions and all events
- On startup,  transitions stale Running → Interrupted

### Run resume
-  CLI flag: loads run from DB, resets non-Done nodes
  to Pending, re-invokes executor with  +
- POST  Axum handler (SSE stream)

## GUI parity (CLI + Axum + Tauri)
-  /  CLI flags with stdin approve/reject prompt
-  route
-  +  routes
-  hitlMode selector + approval banner
- TypeScript types (, , etc.)
- TypeScript client (, , etc.)

## Integration tests (orchestrator_hitl.rs — 4 tests)
-  — approve gate → OrchestratorComplete
-  — reject gate → PlanRejected + ExecuteError
-  — mark_interrupted_runs() transitions correctly
-  — HitlMode::None skips gate, auto-approves

## Quality
-  clean
-  applied
- 🔍 Checking workspace crate boundaries...

📦 gglib-core (pure domain - no adapters, no sqlx)
�[0;32mPASS�[0m: gglib-core

📦 gglib-db (core + sqlx - no adapters)
�[0;32mPASS�[0m: gglib-db

📦 gglib-cli (core + db + clap - no web/gui)
�[0;32mPASS�[0m: gglib-cli

📦 gglib-axum (core + db + axum - no cli/gui)
�[0;32mPASS�[0m: gglib-axum

📦 gglib-tauri (core + db + tauri - no cli/web)
�[0;32mPASS�[0m: gglib-tauri

📦 gglib-download (domain/adapter - no UI adapters)
�[0;32mPASS�[0m: gglib-download

📦 gglib-gguf (parser - no adapters)
�[0;32mPASS�[0m: gglib-gguf

📦 gglib-app-services (service facade - no UI adapters)
�[0;32mPASS�[0m: gglib-app-services

📦 gglib-hf (HTTP adapter - no UI adapters)
�[0;32mPASS�[0m: gglib-hf

📦 gglib-mcp (domain service - no adapters)
�[0;32mPASS�[0m: gglib-mcp

📦 gglib-agent (pure domain agentic loop - no adapters, no infra crates)
�[0;32mPASS�[0m: gglib-agent

📦 gglib-runtime (process runner - no UI adapters)
�[0;32mPASS�[0m: gglib-runtime

📦 gglib-bootstrap (sole call site for shared infra entry points)
�[0;32mPASS�[0m: gglib-bootstrap source guard

✅ �[0;32mAll boundary checks passed�[0m

Results written to: boundary-status.json all PASS
- Scanning for READMEs with module-table markers... READMEs updated
feat(orchestrator): Phase D — HITL approval gates, SQLite persistence, run-resumption (#493)
Phase E plumbing — all three bootstrap paths (Axum, Tauri bootstrap,
Tauri bootstrap_early, and bootstrap_with) now pass OrchestratorDeps to
ProxySupervisor::start(), sharing the same approval_registry and
orchestrator_repo with the REST API handlers.

Changes:
- gglib-proxy: add orchestrator_proxy.rs (virtual model routing, auto/
  interactive/native modes, OrchestratorEvent→SSE mapping, sentinel
  logic); wire into server.rs and export from lib.rs
- gglib-runtime: add OrchestratorRunnerAdapter (implements
  OrchestratorRunnerPort over gglib-agent execute()); fix manual Debug
  impl for McpService field; update supervisor.start() signature;
  update standalone proxy to build OrchestratorDeps with in-memory
  backends; update supervisor tests with NoopRunner/Registry/Repo helpers
- gglib-app-services: add approval_registry + orchestrator_repo to
  ProxyDeps/ProxyOps; add gglib-proxy + reqwest deps; build
  OrchestratorRunnerAdapter in ProxyOps::start()
- gglib-axum: hoist orchestrator_repo + approval_registry creation
  before ProxyDeps so they are shared
- gglib-tauri: update all three bootstrap variants to pass new fields
- Integration tests: add NoopRunner/Registry/Repo helpers; update
  gglib_proxy::serve() call sites
- Remove two needless_return statements in orchestrator_proxy.rs
  (resume_interactive_run match arms)
- Replace map_or(true, ...) with is_none_or in InMemoryOrchestratorRepo
  list_runs() filter (proxy/mod.rs)
Five end-to-end HTTP tests covering the three virtual orchestrator models:

- GET /v1/models includes all three virtual model entries
- gglib-orchestrator:native returns HTTP 400 with /api/orchestrator/run hint
- gglib-orchestrator auto mode streams PlanProposed → NodeStarted →
  NodeTextDelta → SynthesisStart → SynthesisTextDelta → OrchestratorComplete
  as markdown SSE chunks
- gglib-orchestrator:interactive embeds the gglib-run-id sentinel on
  AwaitingApproval and terminates the stream
- Auto mode rejects requests with no user message with HTTP 400

Uses a ScriptedRunner mock that emits pre-configured OrchestratorEvent
sequences over mpsc, exercising the full SSE translation path.
feat(proxy): Phase E — virtual model routing in the OpenAI-compatible proxy
- OrchestratorContext.tsx: reducer for full OrchestratorEvent stream
  with phases idle/running/awaiting_approval/synthesizing/complete/error,
  per-node state tracking, HITL pendingApproval, and runs list state.

- useOrchestrator hook: bridges SSE events → context dispatch,
  manages AbortController lifecycle, exposes run/resume/cancel/reset/
  approve/loadRuns.

- src/pages/Orchestrator/index.tsx: main page with goal input form,
  HITL mode selector, live phase indicator, DAG tree section, synthesis
  streaming output, final answer panel, and resumable-runs sidebar.

- DagView.tsx: topologically-sorted indented tree of TaskGraph nodes
  with status badges and dependency labels. No new heavy dependencies.

- NodePanel.tsx: collapsible per-node panel showing goal, tool
  allowlist, streaming text, tool call log with timing, output preview,
  and error display.

- HitlApprovalModal.tsx: plan/node/tool HITL approval modal with
  plan JSON viewer + optional edit-and-approve flow (approve_with_edits),
  reject-with-reason path, and node/tool context display.

- RunsList.tsx: resumable runs entry with status filter tabs,
  formatted timestamps, and click-to-resume.

- App.tsx: adds #orchestrator hash routing and wraps the new page in
  <OrchestratorProvider>.

- tests/ts/hooks/OrchestratorContext.test.ts: 21 reducer state
  transition tests covering all major event types.

All 606 frontend tests pass. No TypeScript errors. No new backend
endpoints — HTTP transport only, no isTauriApp branching.

Closes #495
feat(orchestrator): Phase F — native frontend polish, DAG visualization, HITL modals, run management
@mmogr mmogr linked an issue May 22, 2026 that may be closed by this pull request
13 tasks
@mmogr mmogr self-assigned this May 22, 2026
@mmogr mmogr added epic:orchestrator epic type:epic type: feature New functionality or enhancement priority: high Important for next release size: xl > 3 days, needs breakdown labels May 22, 2026
mmogr added 2 commits May 22, 2026 22:16
- Header: GitBranch button in desktop nav and mobile menu to open Orchestrator page
- App.tsx: wire onOpenOrchestrator handler; pass hasRunningServers prop
- OrchestratorPage: show a warning banner when no model is running and phase is idle
mmogr added 13 commits May 25, 2026 13:56
…itor HITL wiring

Add council_engine: 'legacy' | 'v2' preference to AppSettings so users can
opt-in to the new DAG orchestrator engine without touching the legacy Council
flow. Defaults to 'legacy' for full backward compatibility.

Key changes:

types/index.ts
- New CouncilEngine = 'legacy' | 'v2' union type
- council_engine field on AppSettings and UpdateSettingsRequest

types/messages.ts
- orchestratorRunId?: string on GglibMessageCustom — set on assistant
messages produced by a v2 run; drives historical replay via
HistoricalOrchestratorThread

OrchestratorThread
- New hosted-mode props: startRunRef (parent calls startRun via ref) and
onRunComplete (notified on terminal phase: complete | error)
- useEffect fills startRunRef.current with a setGoal + startRun callback
- useEffect fires onRunComplete once per run (guarded by completionFiredRef)
- Idle goal form and 'New run' button hidden when isHosted === true
- Plan-kind HITL approval renders PlanEditor inline instead of the modal;
all other kinds (node, tool, spawn_subteam) keep HitlApprovalModal

CouncilToggle
- engine?: CouncilEngine prop — v2 shows Network icon + 'v2' badge and
different tooltip; legacy keeps existing Users icon (backward-compatible)

ChatMessagesPanel
- Reads council_engine from SettingsContext; derives councilEngine
- orchestratorStartRef + pendingOrchestratorGoalRef added as refs
- councilSubmitRef.current routing branches on councilEngine:
v2  → stores goal in pendingGoalRef, calls orchestratorStartRef.current
legacy → original refine/suggest path unchanged
- OrchestratorThread rendered alongside CouncilThread when councilEngine === 'v2'
- CouncilToggle receives engine={councilEngine}
- New onOrchestratorRunComplete prop (runId, goal, finalAnswer)

MessageBubbles.tsx (AssistantMessageBubble)
- Detects orchestratorRunId on message custom metadata BEFORE the existing
councilSession check; renders HistoricalOrchestratorThread inline for v2
messages loaded from persistence

ChatPage.tsx
- handleOrchestratorRunComplete callback mirrors handleCouncilComplete pattern:
synthesises a user/assistant message pair and appends to the thread
- Passes callback down to ChatMessagesPanel as onOrchestratorRunComplete
…uncil

Flip the council_engine default to v2 and completely remove the legacy
Council-of-Agents engine from both frontend and backend.

Frontend deletions:
  - src/components/Council/ (14 files) — CouncilThread, CouncilToggle, etc.
  - src/types/council.ts — SerializableCouncilSession, CouncilEvent types
  - src/hooks/useCouncil/ — useCouncil hook and its SSE event mapper
  - src/contexts/CouncilContext.tsx — CouncilProvider, councilReducer
  - src/services/clients/council.ts — suggestCouncil(), runCouncil()

Frontend modifications:
  - messages.ts: isCouncilMode -> isOrchestratorMode; councilSession removed
  - types/index.ts: CouncilEngine type and council_engine setting removed
  - OrchestratorToggle: new minimal toggle component (replaces CouncilToggle)
  - ChatMessagesPanel: stripped of all council state, effects, and JSX
  - MessageBubbles: legacy councilSession -> plain-text fallback renderer
  - ChatPage: CouncilProvider removed; orchestratorSubmitRef replaces councilSubmitRef
  - useGglibRuntime: onCouncilSubmit -> onOrchestratorSubmit; isOrchestratorMode routing
  - buildSaveMetadata: persists orchestratorRunId instead of councilSession

Backend deletions:
  - crates/gglib-agent/src/council/ (15 files) — full council orchestrator
  - crates/gglib-axum/src/handlers/council/ — HTTP handlers
  - crates/gglib-cli/src/handlers/council/ — CLI handlers

Backend modifications:
  - routes.rs: /council/suggest and /council/run routes removed
  - commands.rs: Council CLI subcommand removed
  - dispatch.rs: Council dispatch arm removed
  - lib.rs files: pub mod council removed

Note: compose_council_ports() and CouncilPorts in gglib-runtime are retained
as they are used by the orchestrator handlers.
The legacy Council engine was purged in the previous commit (62042e6).
The new engine is now universally renamed from "Orchestrator" back to
"Council", reclaiming the product name.

File renames:
  - crates/gglib-agent/src/orchestrator/ → council/
  - crates/gglib-axum/src/handlers/orchestrator/ → council/
  - crates/gglib-core/src/domain/orchestrator/ → council/
  - crates/gglib-core/src/ports/orchestrator_{approvals,repository}.rs → council_*
  - crates/gglib-app-services/src/orchestrator_approvals.rs → council_approvals.rs
  - crates/gglib-db/src/repositories/sqlite_orchestrator_repository.rs → sqlite_council_repository.rs
  - crates/gglib-proxy/src/orchestrator_proxy.rs → council_proxy.rs
  - crates/gglib-runtime/src/orchestrator_runner.rs → council_runner.rs
  - crates/gglib-cli/src/handlers/orchestrate.rs → council.rs
  - crates/gglib-agent/tests/orchestrator_*.rs → council_*.rs
  - src/components/Orchestrator/ → Council/
  - src/components/Council/Thread/OrchestratorThread.tsx → CouncilThread.tsx
  - src/components/Council/Thread/HistoricalOrchestratorThread.tsx → HistoricalCouncilThread.tsx
  - src/components/Council/Thread/useOrchestratorRunStream.ts → useCouncilRunStream.ts
  - src/contexts/OrchestratorRegistry.tsx → CouncilRegistry.tsx
  - src/contexts/OrchestratorContext.tsx → CouncilContext.tsx
  - src/types/orchestrator.ts → council.ts
  - src/services/clients/orchestrator.ts → council.ts
  - src/hooks/useOrchestrator/ → useCouncil/

Symbol renames:
  - OrchestratorEvent → CouncilEvent
  - OrchestratorRun → CouncilRun (Rust domain types)
  - OrchestratorRunStatus → CouncilRunStatus
  - OrchestratorConfig → CouncilConfig
  - OrchestratorRunnerPort/Adapter → CouncilRunnerPort/Adapter
  - OrchestratorDeps → CouncilDeps
  - OrchestratorProxy → CouncilProxy
  - OrchestratorRepositoryPort → CouncilRepositoryPort
  - SqliteOrchestratorRepository → SqliteCouncilRepository
  - OrchestratorApprovalRegistry → CouncilApprovalRegistry
  - ORCHESTRATOR_EVENT_CHANNEL_CAPACITY → COUNCIL_EVENT_CHANNEL_CAPACITY
  - orchestratorRunId → councilRunId (message metadata)
  - isOrchestratorMode → isCouncilMode (routing flag)
  - useOrchestrator → useCouncil (hook)
  - OrchestratorRegistryProvider → CouncilRegistryProvider
  - OrchestratorToggle → CouncilToggle
  - CLI: `gglib orchestrate` → `gglib council`
  - Virtual model: gglib-orchestrator → gglib-council
  - Routes: /api/orchestrator/* → /api/council/*

DB tables preserved (historical names):
  - orchestrator_runs — historical name, kept for schema compatibility
  - orchestrator_events — historical name, kept for schema compatibility
… import paths

- mv src/pages/Orchestrator → src/pages/Council (6 page components)
- mv OrchestratorToggle.tsx → CouncilToggle.tsx
- Fix types/orchestrator → types/council across all consumers
- Fix ../Council/components/* → ../../pages/Council/components/* (path depth)
- Fix HistoricalCouncilThread named→default import in MessageBubbles
- Fix CouncilThread import path in pages/Council/index.tsx
- Remove stale SerializableCouncilSession import (purged in Phase 4+5)
- Remove dead imports in ChatMessagesPanel (useCouncil, stale CouncilThread)
Backend (Phases 1-4):
- gglib-core: add DebateAgent, DebateConfig, TaskNodeKind::Debate, 15 Debate* SSE events
- gglib-agent: implement full debate engine in council/debate/ (11 files)
  - agents, judge, synthesis, round manager, streaming turns, stance tracking
- gglib-agent: wire run_debate_worker into executor dispatch
- gglib-agent: update director schema + prompt for debate node kind
- gglib-cli: extend council handler for debate streams
- gglib-proxy: route all 15 Debate* event variants (previously suppressed)

Frontend (Phases 7-8):
- types/council.ts: add DebateAgent, DebateConfig, AgentStance, StanceOutcome,
  extend TaskNodeKind, add 15 debate SSE event interfaces
- types/graph-diff.ts: add SetDebateConfigOp (apply + describe + union)
- CouncilContext.tsx: add DebateNodeState hierarchy, 15 DEBATE_* actions + reducer
- useCouncil.ts + useCouncilRunStream.ts: map all 15 debate events to actions
- DebateRosterEditor.tsx: PlanEditor right-pane for debate nodes
  (agent roster 2-4, name/perspective/persona/contentiousness, rounds, judge toggle)
- PlanEditor.tsx: branch right-pane on node kind (debate vs leaf)
- DebateNodeBody.tsx: live debate stream renderer
  (round blocks, agent colour streams, judge assessment, stance map, synthesis)
- NodePanel.tsx: render DebateNodeBody when debateState present
- Extract src/utils/councilEventToAction.ts — single source of truth
  mapping CouncilEvent → OrchestratorAction, covering all 15 debate
  events that were missing from HistoricalCouncilThread.tsx (fixing
  broken historical replay of debate runs)
- Update hooks/useCouncil/useCouncil.ts to import from shared util
- Update components/Council/Thread/useCouncilRunStream.ts to import
  from shared util (removes the three-copy duplication)
- Update components/Council/Thread/HistoricalCouncilThread.tsx to
  import from shared util; remove misplaced OrchestratorAction import
  and stale 'Phase 7 deduplication' comment
- Add crates/gglib-agent/tests/unit_debate_node.rs with 3 tests:
happy-path event sequence, 1-based round numbers, pre-cancel path
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	crates/gglib-cli/src/handlers/council/mod.rs
#	crates/gglib-core/src/lib.rs
#	package.json
#	src-tauri/tauri.conf.json
The full orchestrator DAG experience (CompactRunCard, CollapsibleDagView,
NodePanel, HitlApprovalModal, synthesis) already renders inline inside
the chat thread via CouncilThread. The top-bar GitBranch button and the
standalone #council full-page route were redundant.

- Remove onOpenOrchestrator prop + button from Header (desktop + mobile)
- Remove showCouncilPage state, hashchange effect, and #council route
  branch from App.tsx; always render ModelControlCenterPage
- Delete pages/Council/index.tsx (full-page orchestrator)
- Delete pages/Council/components/{RunsList,WaveScrubber}.tsx (full-page-only)
- Delete hooks/useCouncil/ (only consumed by the full page)

Entry point is now solely the Network icon toggle next to the chat
composer (CouncilToggle), consistent with the original council UX.
@mmogr mmogr changed the title feat: Task Graph Executor (Orchestrator Epic) feat: Task Graph Executor May 31, 2026
mmogr added 6 commits May 31, 2026 17:50
- Add presentation/dag.rs with render_tree(), render_mermaid(),
  topological_order(), and node_color() — shared by both the plan
  and council commands
- Node-id labels in the tree and all [node_id] streaming prefixes
  now use a stable per-node ANSI colour derived from the node-id
  string, so the same node is always the same colour throughout
  a run
- plan.rs delegates to the shared helpers (no behaviour change)
- council.rs renders the DAG tree on PlanProposed (stderr) and
  applies node colours to all 19 worker/debate event variants
- Replace flat `Council { goal, model, … }` variant with
  `Council { #[command(subcommand)] cmd: CouncilCmd }`
- Add CouncilCmd enum: Run, List, Show, Resume, Rewind (Phase 5 stub)
- Split handlers/council.rs into handlers/council/:
    mod.rs    — shared init_session / resolve_port / stop_server / parse_hitl_mode
    render.rs — render_event + prompt_and_resolve (pub(crate))
    run.rs    — council run "<goal>"
    resume.rs — council resume <run-id>
    list.rs   — council list [--status]  (new)
    show.rs   — council show <run-id>    (new: metadata + DAG tree + timeline)
    rewind.rs — council rewind stub      (Phase 5)
- Update dispatch.rs to route CouncilCmd variants
- cargo check + clippy: zero warnings
- Extract prompt_and_resolve into handlers/council/approve.rs
    ApproveOpts { timeout_secs, timeout_action }
    TimeoutAction { Reject, Approve } + parse_timeout_action()
- Add --approval-timeout <SECS> and --approval-timeout-action <ACTION>
  (default: reject) to CouncilCmd::Run and CouncilCmd::Resume
- Implement [e]dit option for Plan gates:
    writes graph to $TMPDIR/gglib-plan-<pid>.json
    opens $EDITOR (fallback: nano → vim → vi)
    parses the saved file → ApprovalDecision::ApproveWithEdits
- Async-correct stdin: use tokio::io::stdin() + AsyncBufReadExt::read_line
  wrapped in tokio::time::timeout — NOT spawn_blocking which would block
  the executor and silently ignore the timeout
- Update render_event signature: add last_graph + opts params
    PlanProposed arm now caches the graph for [e]dit availability
    AwaitingApproval arm routes to approve::prompt_and_resolve
- cargo check + clippy: zero warnings
- Add --json (bool) to CouncilCmd::Run and CouncilCmd::Resume in
  commands.rs; defaults to false
- Thread json_mode: bool through dispatch.rs → run::execute /
  resume::execute → render_event
- Guard: if --json is set and --hitl is anything other than "none",
  bail! with "--json output requires --hitl none" before any I/O
- In render_event: when json_mode == true, serialise the CouncilEvent
  to a JSON line via serde_json::to_string and println! it to stdout,
  then return immediately — all ASCII art, colors, DAG trees, and
  interactive prompts are suppressed
- Serialisation errors go to stderr (eprintln!) so stdout stays clean JSONL
- CouncilEvent already derives Serialize with serde tag="type" /
  rename_all="snake_case", so no domain changes needed
- cargo check + clippy: zero warnings
Part A: Live Steering
- Add presentation/input.rs: spawn_input_router(NoteQueue) starts a
  background tokio task that reads stdin lines via tokio::io::stdin()
  + AsyncBufReadExt::lines(); routes /note <text> to NoteQueue,
  all other lines to an mpsc::UnboundedReceiver<String>
- Export pub mod input from presentation/mod.rs
- approve.rs: replace direct tokio::io::stdin reads with
  mpsc::UnboundedReceiver<String>; prompt_and_resolve,
  read_line_with_timeout, read_line_async all accept input_rx as a
  &mut reference — tokio::time::timeout wraps recv() (correct; no
  executor-blocking risk)
- render.rs: add input_rx: &mut mpsc::UnboundedReceiver<String> to
  render_event; thread it into approve::prompt_and_resolve;
  improve SteeringApplied rendering (JSON-serialise diff via
  serde_json::to_string instead of {:?})
- run.rs + resume.rs: create NoteQueue, spawn_input_router, wire
  note_queue into CouncilConfig and input_rx into render_event

Part B: Rewind
- rewind.rs: full implementation (replacing Phase 5 stub)
    1. Load run + guard against Running/AwaitingApproval status
    2. List events; identify node_ids completed after target wave
    3. Reset those nodes to Pending (clear output + error)
    4. truncate_events_after_wave on the repository
    5. update_graph with the reset graph
    6. Pre-seed NoteQueue with optional --note argument
    7. spawn_input_router for live steering during re-execution
    8. Re-execute engine with graph_override, rewind_to_wave,
       run_id, note_queue from the original run's hitl_mode

- cargo check + clippy: zero warnings
…tion

README.md (crates/gglib-cli):
- Add 'Orchestrator (council)' section with full command examples:
    council run, list, show, resume, rewind
    --hitl modes table (none/plan/node/tools)
    approval prompt options (y/n/e/timeout)
    Live steering (/note) explanation
    --json JSONL output usage + jq example
- Update Commands table: replace stale 'chat council' rows with the
  five orchestrator subcommands
- Remove the stale 'Council Command' subsection (debate REPL editor
  commands, tools filter expressions, refine command) that predated
  the DAG orchestrator

handlers/council/mod.rs:
- Expand module-level doc comment into a full architecture reference:
    subcommand → file table
    shared helpers table
    ASCII data-flow diagram (stdin router → NoteQueue/mpsc → approve)

commands.rs:
- Remove '(Phase 5)' from Rewind variant description

cargo fmt: all formatting normalised across council handler files
@mmogr mmogr changed the title feat: Task Graph Executor feat: Task Graph Executor — GUI cleanup + 6-phase CLI orchestrator refactor May 31, 2026
@mmogr mmogr changed the title feat: Task Graph Executor — GUI cleanup + 6-phase CLI orchestrator refactor feat: Task Graph Executor (Orchestrator Epic) May 31, 2026
@mmogr mmogr changed the title feat: Task Graph Executor (Orchestrator Epic) feat: Task Graph Executor May 31, 2026
mmogr and others added 4 commits May 31, 2026 18:47
- Hoist status_color() from list.rs and show.rs into mod.rs shared
  helpers (pub(crate)); was byte-for-byte identical in both files
- Remove now-unused CouncilRunStatus import from show.rs
- Demote topological_order() in dag.rs from pub to private; it is
  only ever called internally by render_tree() in the same module
…tput

Replace the per-token '[node-id]<think> delta' pattern in the
NodeReasoningDelta handler with a single collapsible-style header:

  [node-id] (thinking…)

followed by the reasoning content streamed in dim text (no re-prefix).
NodeTextDelta closes the thinking block with a blank line before
the first output token.

State is tracked in a HashSet<String> (thinking_nodes) threaded
through render_event into all three callers: run, resume, rewind.
Co-authored-by: mmogr <172192206+mmogr@users.noreply.github.com>
@mmogr mmogr enabled auto-merge (squash) May 31, 2026 13:46
@mmogr mmogr merged commit e338740 into main May 31, 2026
11 checks passed
@mmogr mmogr deleted the epic/orchestrator branch May 31, 2026 14:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

epic:orchestrator epic priority: high Important for next release size: xl > 3 days, needs breakdown type:epic type: feature New functionality or enhancement version-bump

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[EPIC] Task Graph Executor — Director/Worker Orchestrator for local agentic workflows

2 participants