Skip to content

Add harness-neutral sub-agent thread support - #119

Merged
marmeladema merged 1 commit into
mainfrom
feature/harness-neutral-subagent-support
Jul 24, 2026
Merged

Add harness-neutral sub-agent thread support#119
marmeladema merged 1 commit into
mainfrom
feature/harness-neutral-subagent-support

Conversation

@marmeladema

@marmeladema marmeladema commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Model harness-neutral sub-agent links with immutable parent/child ownership, spawn-turn provenance, nested navigation, readable agent names, running state, and direct child controls. Keep malformed ownership records visible for recovery.

Map both Codex collaboration protocols, including legacy lifecycle tools and current activity events. Preserve delegated prompts when available, explicitly track fallback prompts, stream command-only turns immediately, and persist terminal output when native turn events are absent.

Resolve transcript links from trusted server-side parent and item records so the browser speaks only Giskard identifiers. Redact native thread IDs from summaries and sub-agent wire payloads, and reject client-asserted ownership or lifecycle metadata on the general thread-open endpoint.

Require identity-preserving resume for linked children, with bounded retries for newly advertised Codex rollouts. Keep primary-thread recovery unchanged.

Serialize lifecycle mutations per project, bound HTTP lock contention, process linked evidence through a per-parent FIFO, materialize children off the parent forwarder, reuse existing native bindings, and prevent duplicate imports.

Coordinate passive monitors and interactive turns through one ownership gate. Use a renewable active idle bound, atomically adopt terminal evidence, persist fallback output once, suppress duplicate turnless broadcasts, and await monitor cancellation through completion signals during deletion.

Delete complete child subtrees leaf-first after repeated active-work preflights. Treat only exact missing-rollout deletion responses as idempotent native success.

Share forwarder state through RegistryShared, isolate passive pre-turn waiting, use typed lifecycle policies and native-open results, and consolidate duplicated route, prompt, item, and frontend helpers.

Recover stale live buffers on genuine new starts without dropping new events. Document both protocols, trusted link opening, navigation, monitoring, prompt, recovery, and deletion behavior in the README, adapter guide, sub-agent guide, and specification.

Summary by CodeRabbit

  • New Features

    • Added a Sub-agents monitor showing active and idle linked threads.
    • Added navigation between parent and sub-agent threads, including nested sub-agents.
    • Added live user-input display and improved transcript ordering.
    • Added safe cascade deletion for linked sub-agent threads.
  • Bug Fixes

    • Improved thread reopening and native resume reliability.
    • Prevented sensitive native thread identifiers from appearing in browser events.
    • Added clearer service-unavailable handling for timed-out operations.
  • Documentation

    • Documented sub-agent linking, monitoring, navigation, resume, and deletion behavior.
    • Updated API and quick-start guidance.

Model harness-neutral sub-agent links with immutable parent/child ownership,
spawn-turn provenance, nested navigation, readable agent names, running state,
and direct child controls. Keep malformed ownership records visible for recovery.

Map both Codex collaboration protocols, including legacy lifecycle tools and
current activity events. Preserve delegated prompts when available, explicitly
track fallback prompts, stream command-only turns immediately, and persist
terminal output when native turn events are absent.

Resolve transcript links from trusted server-side parent and item records so the
browser speaks only Giskard identifiers. Redact native thread IDs from summaries
and sub-agent wire payloads, and reject client-asserted ownership or lifecycle
metadata on the general thread-open endpoint.

Require identity-preserving resume for linked children, with bounded retries for
newly advertised Codex rollouts. Keep primary-thread recovery unchanged.

Serialize lifecycle mutations per project, bound HTTP lock contention, process
linked evidence through a per-parent FIFO, materialize children off the parent
forwarder, reuse existing native bindings, and prevent duplicate imports.

Coordinate passive monitors and interactive turns through one ownership gate.
Use a renewable active idle bound, atomically adopt terminal evidence, persist
fallback output once, suppress duplicate turnless broadcasts, and await monitor
cancellation through completion signals during deletion.

Delete complete child subtrees leaf-first after repeated active-work preflights.
Treat only exact missing-rollout deletion responses as idempotent native success.

Share forwarder state through RegistryShared, isolate passive pre-turn waiting,
use typed lifecycle policies and native-open results, and consolidate duplicated
route, prompt, item, and frontend helpers.

Recover stale live buffers on genuine new starts without dropping new events.
Document both protocols, trusted link opening, navigation, monitoring, prompt,
recovery, and deletion behavior in the README, adapter guide, sub-agent guide,
and specification.
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces harness-neutral sub-agent thread linking across the stack: new SubagentLink/ThreadKind schema types, a ResumePolicy for strict native resume, Codex adapter mapping of collaboration activity to sub-agent links, server-side passive monitoring and cascade deletion via a new thread graph module, browser UI for viewing/opening linked child threads, and accompanying docs and e2e tests.

Changes

Sub-agent thread linking

Layer / File(s) Summary
Core schema types
crates/giskard-core/src/item.rs, thread.rs, text.rs, lib.rs
Adds SubagentLink/SubagentAction/SubagentStatus, ThreadKind enum, and trimmed_non_empty helper, re-exported from the crate.
Harness contract
crates/giskard-harness/src/lib.rs
Adds ResumePolicy and extends ThreadHandle/OpenThreadOptions with sub-agent metadata fields.
Persisted thread metadata
crates/giskard-persist/src/store.rs, tests/giskard_admin.rs
Extends ThreadFile with parent_thread_id, spawned_by_turn_id, kind.
Wire protocol types
crates/giskard-proto/src/lib.rs, wire.rs
Adds WireSubagentLink types redacting native thread IDs, extends ThreadSummary/LiveTurnSnapshot, adds OpenSubagentLinkResponse.
Codex adapter
crates/giskard-harness-codex/src/lib.rs, mapping.rs, README.md
Implements strict resume retries, idempotent deletion, and collab/subagent activity mapping into SubagentLink.
Replay harness
crates/giskard-harness-replay/src/lib.rs, tests/replay_integration.rs
Populates new metadata fields and resume policy in replay flows.
Server registry passive monitoring
crates/giskard-server/src/registry.rs
Adds passive sub-agent monitor lifecycle, materialization queue, and fallback transcript persistence.
Server wiring
crates/giskard-server/src/hub.rs, lib.rs, live_buffer.rs
Boxes event payloads and adds per-turn user-input tracking.
Routes and thread graph validation
crates/giskard-server/src/routes.rs, thread_graph.rs
Adds sub-agent link opening endpoint, graph validation, and cascade subtree deletion.
Scripted replay binary
crates/giskard-server/src/bin/giskard-server-replay.rs
Emits deterministic sub-agent/nested-sub-agent event sequences.
Server test updates
crates/giskard-server/tests/*
Updates fixtures/harnesses and adds sub-agent import/passive-monitor/deletion tests.
Browser UI
crates/giskard-server/static/app.js, app.css, index.html
Adds Sub-agents monitor, parent-thread navigation, and linked-thread open actions.
Docs and e2e tests
README.md, docs/subagents.md, specs/giskard-specification.md, tests/e2e/*
Documents the feature and adds Playwright coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: claude

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding harness-neutral sub-agent thread support.
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.
✨ 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 feature/harness-neutral-subagent-support

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

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/giskard-server/static/app.js (1)

1114-1128: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cascade-delete view clearing relies on a possibly stale client cache.

descendants/deletedIds are computed from the cached knownProjectThreads(pid) before the request is sent. If a descendant was spawned after the last sidebar refresh, it won't be in this set — if the user is currently viewing that untracked descendant when an ancestor is deleted, the transcript view won't be cleared even though the server actually cascaded-deleted it, leaving a stale/orphaned view.

♻️ Suggested fix: verify against the freshly reloaded list instead of the stale pre-computed set
   try {
     await api("DELETE", `/api/projects/${pid}/threads/${tid}`);
-    const deletedIds = new Set([String(tid), ...descendants]);
-    if (state.threadId && deletedIds.has(String(state.threadId))) clearThreadView(state.threadId);
-    await loadThreads(pid);
+    const priorActiveThread = state.threadId ? String(state.threadId) : null;
+    await loadThreads(pid);
+    if (priorActiveThread && !knownThreadForId(pid, priorActiveThread)) clearThreadView(priorActiveThread);
   } catch (e) {
🤖 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 `@crates/giskard-server/static/app.js` around lines 1114 - 1128, Update
deleteThread so the active view is validated against the freshly reloaded thread
list after the DELETE completes, rather than relying on the pre-request
descendants/deletedIds cache. Reload via loadThreads(pid), then
clearThreadView(state.threadId) when the current thread no longer exists in the
refreshed results, preserving the existing confirmation and error handling.
🧹 Nitpick comments (2)
crates/giskard-server/src/routes.rs (1)

567-589: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider extracting the ThreadKind::Subagent open dispatch into a shared helper. The if thread_file.kind == ThreadKind::Subagent { open_linked_thread(...) } else { open_thread(...) } branch is duplicated across four call sites with only argument differences. A single helper (e.g. open_thread_for_kind(&registry, kind, config, ws_root, thread_id, harness_thread_id, model)) would remove the copy/paste and keep the linked-vs-primary resume contract in one place as kinds evolve.

  • crates/giskard-server/src/routes.rs#L567-L589: replace the inline branch in open_thread (existing-thread path) with the helper.
  • crates/giskard-server/src/routes.rs#L671-L693: replace the inline branch in the resume-existing path with the helper.
  • crates/giskard-server/src/routes.rs#L3302-L3324: replace the inline branch in ensure_thread_open with the helper.
  • crates/giskard-server/src/routes.rs#L3604-L3626: replace the inline branch in switch_provider_cold with the helper.
🤖 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 `@crates/giskard-server/src/routes.rs` around lines 567 - 589, Extract the
duplicated ThreadKind::Subagent dispatch into a shared open_thread_for_kind
helper that selects open_linked_thread for subagents and open_thread otherwise,
preserving the existing arguments and behavior. Replace the inline branches at
crates/giskard-server/src/routes.rs lines 567-589, 671-693, 3302-3324, and
3604-3626 with calls to the helper.
crates/giskard-server/static/app.js (1)

663-681: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cycles among visible (non-managed) threads silently vanish from the sidebar.

appendThreadRows only renders nodes reachable from a "root" (a thread whose parent isn't in the visible set). A genuine cycle among threads that aren't classified as managed sub-agents (e.g. corrupted kind/parent_thread_id combos) forms a subgraph with no root, so those threads are dropped entirely rather than shown — this contradicts the documented guarantee that malformed/dangling records "remain visible in the main sidebar so they can be repaired or deleted" (docs/subagents.md, spec SA1). Since ownership is meant to be immutable/cycle-free server-side, this is a defense-in-depth gap for corrupted on-disk data rather than a normal-path bug.

🤖 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 `@crates/giskard-server/static/app.js` around lines 663 - 681, Update
appendThreadRows so every visible thread is rendered, including nodes in
parent-child cycles with no root. Track rendered thread IDs during appendOne,
then append any unrendered threads after processing roots while preserving the
existing recursive hierarchy behavior and avoiding duplicate rows or infinite
recursion.
🤖 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 `@crates/giskard-server/src/bin/giskard-server-replay.rs`:
- Line 277: Update the conditional in the replay flow around
parent_harness_thread_id to avoid the unstable if-let chain syntax for the Rust
1.85 MSRV. Nest an if let binding inside the existing is_new check, preserving
the current behavior of handling only new entries with a present parent thread
ID.

---

Outside diff comments:
In `@crates/giskard-server/static/app.js`:
- Around line 1114-1128: Update deleteThread so the active view is validated
against the freshly reloaded thread list after the DELETE completes, rather than
relying on the pre-request descendants/deletedIds cache. Reload via
loadThreads(pid), then clearThreadView(state.threadId) when the current thread
no longer exists in the refreshed results, preserving the existing confirmation
and error handling.

---

Nitpick comments:
In `@crates/giskard-server/src/routes.rs`:
- Around line 567-589: Extract the duplicated ThreadKind::Subagent dispatch into
a shared open_thread_for_kind helper that selects open_linked_thread for
subagents and open_thread otherwise, preserving the existing arguments and
behavior. Replace the inline branches at crates/giskard-server/src/routes.rs
lines 567-589, 671-693, 3302-3324, and 3604-3626 with calls to the helper.

In `@crates/giskard-server/static/app.js`:
- Around line 663-681: Update appendThreadRows so every visible thread is
rendered, including nodes in parent-child cycles with no root. Track rendered
thread IDs during appendOne, then append any unrendered threads after processing
roots while preserving the existing recursive hierarchy behavior and avoiding
duplicate rows or infinite recursion.
🪄 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: 5a03ad8d-aff9-49cd-b34e-d3924950d9f0

📥 Commits

Reviewing files that changed from the base of the PR and between 5a93b8b and 9b09c1c.

⛔ Files ignored due to path filters (2)
  • docs/screenshots/ide-desktop.png is excluded by !**/*.png, !docs/screenshots/**
  • docs/screenshots/ide-mobile.png is excluded by !**/*.png, !docs/screenshots/**
📒 Files selected for processing (42)
  • .dockerignore
  • README.md
  • crates/giskard-core/src/item.rs
  • crates/giskard-core/src/lib.rs
  • crates/giskard-core/src/text.rs
  • crates/giskard-core/src/thread.rs
  • crates/giskard-harness-codex/README.md
  • crates/giskard-harness-codex/src/lib.rs
  • crates/giskard-harness-codex/src/mapping.rs
  • crates/giskard-harness-replay/src/lib.rs
  • crates/giskard-harness-replay/tests/replay_integration.rs
  • crates/giskard-harness/src/lib.rs
  • crates/giskard-persist/src/store.rs
  • crates/giskard-persist/tests/giskard_admin.rs
  • crates/giskard-proto/src/lib.rs
  • crates/giskard-proto/src/wire.rs
  • crates/giskard-server/src/bin/giskard-server-replay.rs
  • crates/giskard-server/src/hub.rs
  • crates/giskard-server/src/lib.rs
  • crates/giskard-server/src/live_buffer.rs
  • crates/giskard-server/src/registry.rs
  • crates/giskard-server/src/routes.rs
  • crates/giskard-server/src/running_commands.rs
  • crates/giskard-server/src/thread_graph.rs
  • crates/giskard-server/static/app.css
  • crates/giskard-server/static/app.js
  • crates/giskard-server/static/index.html
  • crates/giskard-server/tests/e2e_smoke.rs
  • crates/giskard-server/tests/interrupt.rs
  • crates/giskard-server/tests/override_propagation.rs
  • crates/giskard-server/tests/provider_switch.rs
  • crates/giskard-server/tests/read_only_thread.rs
  • crates/giskard-server/tests/running_tasks.rs
  • crates/giskard-server/tests/server_requests.rs
  • crates/giskard-server/tests/thread_lifecycle.rs
  • crates/giskard-server/tests/turn_controls.rs
  • crates/giskard-server/tests/ui.rs
  • docs/subagents.md
  • specs/giskard-specification.md
  • tests/e2e/README.md
  • tests/e2e/tests/helpers.ts
  • tests/e2e/tests/subagents.spec.ts

drop(threads);

let parent_harness_thread_id = Self::subagent_parent(&harness_thread_id);
if is_new && let Some(parent) = parent_harness_thread_id.clone() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm declared MSRV / edition and existing let-chain usage.
fd -H -t f 'rust-toolchain(.toml)?$' -x cat {}
fd -H -t f '.tool-versions$' -x cat {}
rg -n 'rust-version|edition' -g 'Cargo.toml'
# Any pre-existing if/while let-chains in the crates?
rg -nP '\b(if|while)\b[^\n{]*\b&&\s+let\b' -g 'crates/**/*.rs' -C1 | head -50

Repository: marmeladema/Giskard

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== toolchain files =="
fd -HI -t f 'rust-toolchain(.toml)?$|\.tool-versions$' . -x sh -c 'echo "--- {}"; cat "{}"'

echo "== Cargo.toml rust-version/edition =="
rg -n '^(edition|rust-version)\s*=' -g 'Cargo.toml' .

echo "== let-chain usage in crates =="
rg -nP '\b(if|while)\b[^\n{]*\b&&\s+let\b' crates -C 2 || true

Repository: marmeladema/Giskard

Length of output: 320


🌐 Web query:

Rust if let chains stabilized version 1.88 let chains stable

💡 Result:

Yes, let chains were stabilized in Rust 1.88.0 [1][2]. This feature allows you to chain multiple let expressions using the && operator within if and while conditions, and to intermix them with boolean expressions [1][3]. Crucially, let chains are only available in the Rust 2024 edition [4][1]. This limitation exists because the feature relies on edition-specific changes to temporary scope and drop order, which ensure consistent behavior that could not be reliably implemented for older editions [1][5]. If you attempt to use let chains in a project configured for an older edition (e.g., 2021 or 2018), the compiler will emit an error and advise you to upgrade your crate's edition [6][1][5].

Citations:


Replace the if let-chain here
Cargo.toml pins rust-version = "1.85", but if/while let-chains are stabilized in Rust 1.88. This will not compile on the declared MSRV; nest the binding instead.

🤖 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 `@crates/giskard-server/src/bin/giskard-server-replay.rs` at line 277, Update
the conditional in the replay flow around parent_harness_thread_id to avoid the
unstable if-let chain syntax for the Rust 1.85 MSRV. Nest an if let binding
inside the existing is_new check, preserving the current behavior of handling
only new entries with a present parent thread ID.

Sources: Coding guidelines, Path instructions

@marmeladema
marmeladema merged commit 169a1f2 into main Jul 24, 2026
5 checks passed
@marmeladema
marmeladema deleted the feature/harness-neutral-subagent-support branch July 24, 2026 09:53
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.

1 participant