Finish Epiphany live specialist MVP

This commit is contained in:
Meta Vens 2026-04-28 17:36:18 +01:00
commit da8b7f82a0
10 changed files with 329 additions and 32 deletions

View file

@ -79,6 +79,7 @@ The landed machine now has:
- explicit Phase 6 role launch/read-back through `thread/epiphany/roleLaunch` and read-only `thread/epiphany/roleResult`, limited to fixed modeling/checkpoint and verification/review templates over the existing job-control seam
- first Phase 6 dogfood operator view through `tools/epiphany_mvp_status.py`
- first auditable Phase 6 dogfood runner through `tools/epiphany_mvp_dogfood.py`, producing local status snapshots, raw app-server transcript, final status artifacts, vanilla-reference output, and comparison notes
- first auditable Phase 6 live-specialist runner through `tools/epiphany_mvp_live_specialist.py`, proving `roleLaunch -> agent_jobs worker -> report_agent_job_result -> roleResult` without manual backend completion
- live Phase 6 reorientation app-server smoke coverage in `tools/epiphany_phase6_reorient_smoke.py`
- live Phase 6 reorient-launch app-server smoke coverage in `tools/epiphany_phase6_reorient_launch_smoke.py`
- live Phase 6 MVP status smoke coverage in `tools/epiphany_mvp_status_smoke.py`
@ -148,8 +149,8 @@ The next unknowns are:
- how the landed watcher-backed invalidation telemetry should be consumed without turning freshness into a secret worker
- how far the read-only CRRC recommendation should go before explicit client/operator action takes over
- whether future bounded CRRC consumers should keep reusing the landed `agent_jobs` backend through the explicit job-control seam or start defining a second backend contract
- what Phase 6 should prove before specialist scheduling begins
- how much specialist ergonomics can improve before crossing into broad ambient scheduling
- what concrete operator friction appears when the CLI MVP is tested on real work
## MVP Cutline
@ -191,9 +192,12 @@ launch modeling/checkpoint or verification/review workers, and read their
findings back without reading Rust. The first auditable dogfood pass then added
`tools/epiphany_mvp_dogfood.py`, rendered modeling/verification findings in the
status view, and fixed CRRC's repeat-acceptance recommendation after
`reorientAccept`. The next MVP question is human operator testing, not another
imagined organ: use the landed loop and artifact bundle, then fix only concrete
blockers.
`reorientAccept`. The live-specialist runner then proved the real worker path by
launching a modeling/checkpoint specialist, letting it inspect the smoke
workspace, report through `report_agent_job_result`, and return a completed
`checkpoint-ready` finding through `roleResult`. The next MVP question is human
operator testing, not another imagined organ: use the landed loop and artifact
bundle, then fix only concrete blockers.
## Phase 6 Direction
@ -201,7 +205,7 @@ Phase 6 should grow observable harness state outward from the typed spine.
Useful candidates:
1. Put the dogfooded MVP loop in front of a human operator through status/artifact review, then fix concrete usability blockers.
1. Put the CLI MVP in front of a human operator through status/artifact review, then fix concrete usability blockers.
2. Keep accepted worker findings review-gated; do not convert acceptance into automatic promotion of arbitrary worker output.
3. Add targeted operator-view fields only when dogfooding exposes a real gap.

View file

@ -31,6 +31,8 @@ Do not trust this file for the exact live HEAD. Always check git.
- Phase 6 has read-only `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/roles`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/pressure`, `thread/epiphany/reorient`, `thread/epiphany/crrc`, `thread/epiphany/reorientResult`, and `thread/epiphany/roleResult`; durable `jobBindings` now act as a thin Epiphany-owned launcher seam with launcher id, authority scope, and backend kind/job id, `thread/epiphany/jobLaunch` / `thread/epiphany/jobInterrupt` / `thread/epiphany/roleLaunch` now provide explicit bounded authority over that seam, jobs can overlay it onto live runtime `agent_jobs` progress for loaded threads, and `thread/epiphany/jobsUpdated` now translates live `agent_job_progress:{json}` background events into changed launcher-bound notifications without polling or scheduling. Freshness carries watcher-backed invalidation inputs, roles project implementation/modeling/verification/reorientation ownership from existing signals without becoming a scheduler, `roleLaunch` can launch only fixed modeling/checkpoint or verification/review specialists, `roleResult` reads those outputs back as reviewable findings without mutation, reorient turns checkpoint plus freshness/pressure/watcher signals into a read-only resume-versus-regather verdict, `thread/epiphany/reorientLaunch` is the explicit runtime consumer over that verdict, `thread/epiphany/reorientResult` reads the launched worker output back as a reviewable finding without mutation or promotion, `thread/epiphany/reorientAccept` explicitly banks completed findings into accepted observation/evidence plus optional scratch/checkpoint state, and `thread/epiphany/crrc` recommends the next explicit CRRC action without launching, accepting, compacting, scheduling, or mutating.
- `tools/epiphany_mvp_status.py` is the first dogfood operator view. It starts or reads a thread through app-server and prints scene, pressure, reorient, jobs, roles, modeling/verification role result read-backs, reorient result, and CRRC recommendation as text or JSON.
- `tools/epiphany_mvp_dogfood.py` is the first auditable dogfood runner. It drives a bounded state/role/CRRC/reorientation loop and writes local artifacts under `.epiphany-dogfood/mvp-loop`, including raw app-server transcript, rendered snapshots, final status, vanilla-reference prompt/output, and comparison notes.
- `tools/epiphany_mvp_live_specialist.py` is the first auditable live-specialist runner. It launches a real role specialist through `thread/epiphany/roleLaunch`, lets the spawned worker report through `report_agent_job_result`, reads it back through `thread/epiphany/roleResult`, and writes local artifacts under `.epiphany-dogfood/live-specialist`.
- Internal `agent_job:` workers now get the reporting tool independent of the user-facing CSV spawn feature, and ephemeral worker sessions can initialize the sqlite state runtime on demand so specialist reports land in the shared backend.
- Durable in-flight investigation checkpointing is now landed in authoritative typed state, writable through `thread/epiphany/update` or accepted `thread/epiphany/promote`, rendered into the prompt, and reflected through scene/context.
- The repo is an Epiphany fork of Codex, not a Codex preset.
- `vendor/codex` is tracked directly, not a submodule.
@ -93,6 +95,7 @@ The current spine:
- live reorient-guided worker launch smoke through `tools/epiphany_phase6_reorient_launch_smoke.py`
- live MVP operator status smoke through `tools/epiphany_mvp_status_smoke.py`
- live job-control app-server smoke through `tools/epiphany_phase6_job_control_smoke.py`
- live specialist MVP pass through `tools/epiphany_mvp_live_specialist.py`
The exact current control flow is documented in
`notes/epiphany-current-algorithmic-map.md`.
@ -268,10 +271,17 @@ produced `.epiphany-dogfood/mvp-loop` artifacts, ran a read-only Vanilla Codex
reference, and fixed the concrete blocker where CRRC recommended accepting the
same reorientation result again after `reorientAccept`.
When the user asks to continue, put the testable MVP in front of a human
operator through `tools/epiphany_mvp_status.py` and the dogfood artifact bundle.
Fix concrete blockers in that operator loop; do not build a broad hidden
scheduler, marketplace, or GUI-as-source-of-truth.
The first live-specialist pass has also run. It produced
`.epiphany-dogfood/live-specialist` artifacts and proved the real worker path:
`roleLaunch` created a bound `agent_jobs` row, the spawned modeling worker
inspected the smoke workspace, called `report_agent_job_result`, and
`roleResult` projected a completed `checkpoint-ready` finding.
The CLI MVP is now testable. Put it in front of a human operator through
`tools/epiphany_mvp_status.py`, `.epiphany-dogfood/mvp-loop`, and
`.epiphany-dogfood/live-specialist`. Fix concrete blockers in that operator
loop; do not build a broad hidden scheduler, marketplace, or
GUI-as-source-of-truth.
Live `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/roles`,
`thread/epiphany/freshness`, `thread/epiphany/context`,
@ -283,7 +293,7 @@ guardrails, not the next organs.
- automatic watcher-driven semantic invalidation
- automatic observation promotion
- specialist-agent scheduling
- broad specialist-agent scheduling beyond explicit roleLaunch
- GUI-as-source-of-truth
- automatic runtime CRRC execution acting on the landed recommendation/verdict and launch/read-back path
- Epiphany-owned long-running job execution beyond the current runtime `agent_jobs` seam

View file

@ -56,3 +56,4 @@
{"ts": "2026-04-28T13:55:36+00:00", "type": "implementation", "status": "ok", "note": "Landed read-only thread/epiphany/roles as the harness-native MVP role ownership surface. It projects implementation, modeling/checkpoint, verification/review, and reorientation lanes from typed state plus jobs/pressure/reorient/CRRC/result signals without scheduling or mutation; the MVP status tool now consumes that surface. Focused protocol/app-server tests, schema fixtures, app-server build, and live MVP status smoke passed."}
{"ts": "2026-04-28T14:48:10+00:00", "type": "implementation", "status": "ok", "note": "Landed explicit thread/epiphany/roleLaunch and read-only thread/epiphany/roleResult for fixed modeling/checkpoint and verification/review specialists over the existing job-control seam. Launch mutates only jobBindings; result projects reviewable findings without mutation. Focused protocol/app-server tests, schema fixtures, app-server build, and tools/epiphany_phase6_role_smoke.py passed."}
{"ts": "2026-04-28T15:32:47+00:00", "type": "implementation", "status": "ok", "note": "Ran the first auditable Epiphany MVP dogfood pass through state, modeling/verification roleLaunch/roleResult, CRRC drift, reorientLaunch/reorientResult/reorientAccept, final status rendering, and a read-only Vanilla Codex reference. Dogfood artifacts live under .epiphany-dogfood/mvp-loop. The pass exposed and fixed a concrete operator-loop blocker: after reorientAccept, CRRC no longer recommends accepting the same completed reorientation finding again; it recognizes the accepted evidence and recommends regatherManually for accepted regather findings."}
{"ts": "2026-04-28T16:33:43+00:00", "type": "implementation", "status": "ok", "note": "Landed live-specialist MVP execution: internal agent_job workers now expose report_agent_job_result independently of the CSV front door, ephemeral workers can initialize sqlite state runtime on demand, and tools/epiphany_mvp_live_specialist.py proved roleLaunch -> agent_jobs worker -> report_agent_job_result -> roleResult with auditable artifacts under .epiphany-dogfood/live-specialist."}

View file

@ -90,6 +90,8 @@ accepted_design:
- `thread/epiphany/reorientAccept` is the first explicit acceptance write for completed reorient-worker findings: it appends accepted observation/evidence records, can optionally update scratch and investigation checkpoint state, and emits `stateUpdated` with source `reorientAccept` without accepting pending work or launching follow-up work.
- After `thread/epiphany/reorientAccept`, CRRC recognizes the accepted reorientation finding and does not recommend accepting the same completed result again.
- `tools/epiphany_mvp_dogfood.py` is the auditable MVP dogfood runner: it drives state, role launch/result, CRRC drift, reorient launch/result/accept, rendered status snapshots, and raw app-server transcript into a local artifact bundle for comparison against a vanilla Codex reference.
- `tools/epiphany_mvp_live_specialist.py` is the auditable live-specialist MVP runner: it launches a real modeling/checkpoint or verification/review specialist through `thread/epiphany/roleLaunch`, lets the spawned worker report through `report_agent_job_result`, reads the finding back through `thread/epiphany/roleResult`, and writes transcript/status artifacts without manually completing backend rows.
- Internal `agent_job:` workers expose `report_agent_job_result` even when the user-facing CSV spawn feature is disabled, and ephemeral worker sessions can initialize the sqlite state runtime on demand so role specialists can report into the shared `agent_jobs` backend.
- Epiphany safety doctrine is to separate cognition from actuation, keep authority explicit, make interruption sacred, stage capability release, and treat operator misuse as a normal design input rather than a rude hypothetical.
- Epiphany is built in the open, but publication mode is not one-size-fits-all: use FOSS where viable, source-available licensing where economics or capability risk demand it, and commercial terms where organizations extract real enterprise value.
- `thread/epiphany/jobLaunch` and `thread/epiphany/jobInterrupt` are the first bounded authority surfaces over the thin job seam: they mutate durable `job_bindings`, create or cancel the current `agent_jobs` backend binding, and preserve explicit launcher identity and interruption without turning `thread/epiphany/jobs` into a scheduler.
@ -112,6 +114,7 @@ landed_baseline:
- Phase 6 acceptance: explicit reorient-worker finding acceptance through `thread/epiphany/reorientAccept`.
- Phase 6 dogfood view: first CLI operator status over scene/pressure/reorient/jobs/result/CRRC.
- Phase 6 dogfood runner: auditable scripted MVP loop over state, role results, CRRC, reorientation acceptance, status snapshots, and transcript artifacts.
- Phase 6 live specialist runner: auditable real worker loop over roleLaunch, agent_jobs worker execution, report_agent_job_result, roleResult, status rendering, and transcript artifacts.
- Phase 6 role ownership: read-only `thread/epiphany/roles` over implementation, modeling/checkpoint, verification/review, and reorientation lanes.
- Phase 6 role specialists: explicit `thread/epiphany/roleLaunch` and read-only `thread/epiphany/roleResult` templates for modeling/checkpoint and verification/review.
guardrails:
@ -129,6 +132,7 @@ landed_baseline:
- Use `tools/epiphany_phase6_reorient_launch_smoke.py` before modifying the read-only CRRC coordinator recommendation surface.
- Use `tools/epiphany_mvp_status_smoke.py` before modifying the MVP operator status view.
- Use `tools/epiphany_phase6_role_smoke.py` before modifying explicit role launch/read-back behavior.
- Use `tools/epiphany_mvp_live_specialist.py` before claiming live specialist execution still works end to end.
- Use focused scene/jobs/context/pressure/reorient/protocol/schema checks for reflection-only changes.
- Use `tools/epiphany_prepare_compaction.py` before and after imminent-compaction persistence passes.
- Use `CARGO_TARGET_DIR=C:\Users\Meta\.cargo-target-codex` for Codex Rust work on this Windows machine.
@ -147,5 +151,5 @@ rejected_paths:
reason: persistent state that accumulates every local truth becomes memory cruft and weakens rehydration
current_status:
summary: Phase 1 durable state, Phase 2 prompt integration, Phase 3 typed read, Phase 4 retrieval/indexing, Phase 5 update/distill/propose/promote/read/notification, and Phase 6 read-only scene, jobs, roles, freshness, targeted context, pressure, reorient, CRRC, reorient launch/result/accept, explicit modeling/checkpoint and verification/review role launch/result templates, first dogfood MVP status, and auditable MVP dogfood runner are landed. The dogfood runner exercised state, role launch/result, CRRC drift, reorient launch/result/accept, and rendered status snapshots; it exposed and fixed the repeat-acceptance CRRC recommendation after `reorientAccept`. Implementation remains main-agent work; reorientation remains on the CRRC-specific launch/result path. Persistent state remains distilled; evidence is a belief ledger, not an activity feed.
next_action: Put the testable MVP in front of a human operator using the dogfood artifact bundle and `tools/epiphany_mvp_status.py`; fix only concrete blockers in the operator loop. Automatic broad specialist scheduling, GUI-as-source-of-truth, a specialist marketplace, autonomous specialist quality, and a second job backend remain future work.
summary: Phase 1 durable state, Phase 2 prompt integration, Phase 3 typed read, Phase 4 retrieval/indexing, Phase 5 update/distill/propose/promote/read/notification, and Phase 6 read-only scene, jobs, roles, freshness, targeted context, pressure, reorient, CRRC, reorient launch/result/accept, explicit modeling/checkpoint and verification/review role launch/result templates, first dogfood MVP status, auditable MVP dogfood runner, and live specialist MVP runner are landed. The live specialist runner proved a real modeling specialist can launch, inspect, report through `report_agent_job_result`, and be read back through `roleResult`; the earlier deterministic dogfood runner exercised state, role launch/result, CRRC drift, reorient launch/result/accept, rendered status snapshots, and Vanilla Codex comparison. Implementation remains main-agent work; reorientation remains on the CRRC-specific launch/result path. Persistent state remains distilled; evidence is a belief ledger, not an activity feed.
next_action: Treat the CLI MVP as testable: run `tools/epiphany_mvp_status.py`, inspect `.epiphany-dogfood/mvp-loop` and `.epiphany-dogfood/live-specialist`, and iterate only on concrete operator-loop blockers. Automatic broad specialist scheduling, GUI-as-source-of-truth, a specialist marketplace, autonomous specialist quality, and a second job backend remain future work.

View file

@ -0,0 +1,213 @@
from __future__ import annotations
import argparse
import json
import os
import shutil
import sys
import time
from pathlib import Path
from typing import Any
from epiphany_mvp_status import DEFAULT_APP_SERVER
from epiphany_mvp_status import collect_status
from epiphany_mvp_status import render_status
from epiphany_phase5_smoke import AppServerClient
from epiphany_phase5_smoke import ROOT
from epiphany_phase5_smoke import require
from epiphany_phase6_reorient_smoke import prepare_workspace
from epiphany_phase6_reorient_smoke import reorient_patch
DEFAULT_ARTIFACT_DIR = ROOT / ".epiphany-dogfood" / "live-specialist"
DEFAULT_CODEX_HOME = Path(os.environ.get("CODEX_HOME", Path.home() / ".codex"))
DEFAULT_ROLES = ("modeling",)
TERMINAL_STATUSES = {"completed", "failed", "cancelled"}
def reset_artifact_dir(path: Path) -> None:
root = (ROOT / ".epiphany-dogfood").resolve()
resolved = path.resolve()
if resolved == root or root not in resolved.parents:
raise ValueError(f"refusing to delete non-dogfood artifact dir: {path}")
if path.exists():
shutil.rmtree(path)
path.mkdir(parents=True, exist_ok=True)
def write_json(path: Path, value: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
def write_text(path: Path, value: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(value, encoding="utf-8")
def launch_role(
client: AppServerClient,
*,
thread_id: str,
role_id: str,
expected_revision: int,
max_runtime_seconds: int,
) -> dict[str, Any]:
launch = client.send(
"thread/epiphany/roleLaunch",
{
"threadId": thread_id,
"roleId": role_id,
"expectedRevision": expected_revision,
"maxRuntimeSeconds": max_runtime_seconds,
},
)
assert launch is not None
require(launch["roleId"] == role_id, f"{role_id} launch should echo role id")
require(
launch["changedFields"] == ["jobBindings"],
f"{role_id} launch should only mutate job bindings",
)
return launch
def wait_for_role_result(
client: AppServerClient,
*,
thread_id: str,
role_id: str,
timeout_seconds: int,
poll_seconds: float,
) -> tuple[dict[str, Any], list[str]]:
deadline = time.time() + timeout_seconds
seen_statuses: list[str] = []
result: dict[str, Any] | None = None
while time.time() < deadline:
response = client.send(
"thread/epiphany/roleResult",
{"threadId": thread_id, "roleId": role_id},
)
assert response is not None
result = response
status = response.get("status", "<missing>")
seen_statuses.append(status)
if status in TERMINAL_STATUSES:
return response, seen_statuses
time.sleep(poll_seconds)
assert result is not None
return result, seen_statuses
def run_live_specialist(args: argparse.Namespace) -> dict[str, Any]:
app_server = args.app_server.resolve()
if not app_server.exists():
raise FileNotFoundError(f"codex app-server binary not found: {app_server}")
codex_home = args.codex_home.resolve()
if not codex_home.exists():
raise FileNotFoundError(f"codex home not found: {codex_home}")
artifact_dir = args.artifact_dir.resolve()
reset_artifact_dir(artifact_dir)
workspace = artifact_dir / "workspace"
transcript_path = artifact_dir / "epiphany-transcript.jsonl"
stderr_path = artifact_dir / "epiphany-server.stderr.log"
prepare_workspace(workspace)
role_results: list[dict[str, Any]] = []
with AppServerClient(app_server, codex_home, transcript_path, stderr_path) as client:
client.send(
"initialize",
{
"clientInfo": {
"name": "epiphany-mvp-live-specialist",
"title": "Epiphany MVP Live Specialist",
"version": "0.1.0",
},
"capabilities": {"experimentalApi": True},
},
)
client.send("initialized", expect_response=False)
started = client.send("thread/start", {"cwd": str(workspace), "ephemeral": True})
assert started is not None
thread_id = started["thread"]["id"]
update = client.send(
"thread/epiphany/update",
{"threadId": thread_id, "expectedRevision": 0, "patch": reorient_patch()},
)
assert update is not None
revision = update["revision"]
for role_id in args.roles:
launch = launch_role(
client,
thread_id=thread_id,
role_id=role_id,
expected_revision=revision,
max_runtime_seconds=args.max_runtime_seconds,
)
revision = launch["revision"]
result, seen_statuses = wait_for_role_result(
client,
thread_id=thread_id,
role_id=role_id,
timeout_seconds=args.timeout_seconds,
poll_seconds=args.poll_seconds,
)
role_results.append(
{
"roleId": role_id,
"launch": launch,
"seenStatuses": seen_statuses,
"result": result,
}
)
final_status = collect_status(client, thread_id=thread_id, cwd=workspace, ephemeral=True)
final_rendered = render_status(final_status)
summary = {
"objective": "Run a real Epiphany role specialist through roleLaunch, agent_jobs, report_agent_job_result, and roleResult.",
"artifactDir": str(artifact_dir),
"codexHome": str(codex_home),
"threadId": thread_id,
"workspace": str(workspace),
"roles": role_results,
"finalStatus": final_status,
"artifactManifest": [
"live-specialist-summary.json",
"epiphany-final-status.json",
"epiphany-final-status.txt",
"epiphany-transcript.jsonl",
"epiphany-server.stderr.log",
],
}
write_json(artifact_dir / "live-specialist-summary.json", summary)
write_json(artifact_dir / "epiphany-final-status.json", final_status)
write_text(artifact_dir / "epiphany-final-status.txt", final_rendered)
return summary
def main() -> int:
parser = argparse.ArgumentParser(
description="Run an auditable live Epiphany specialist through the real agent_jobs worker path."
)
parser.add_argument("--app-server", type=Path, default=DEFAULT_APP_SERVER)
parser.add_argument("--artifact-dir", type=Path, default=DEFAULT_ARTIFACT_DIR)
parser.add_argument("--codex-home", type=Path, default=DEFAULT_CODEX_HOME)
parser.add_argument("--roles", nargs="+", choices=["modeling", "verification"], default=list(DEFAULT_ROLES))
parser.add_argument("--max-runtime-seconds", type=int, default=180)
parser.add_argument("--timeout-seconds", type=int, default=240)
parser.add_argument("--poll-seconds", type=float, default=5.0)
args = parser.parse_args()
result = run_live_specialist(args)
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -236,7 +236,7 @@ mod spawn_agents_on_csv {
));
}
let db = required_state_db(&session)?;
let db = required_state_db(&session).await?;
let input_path = turn.resolve_path(Some(args.csv_path));
let input_path_display = input_path.display().to_string();
let csv_content = tokio::fs::read_to_string(&input_path)
@ -478,7 +478,7 @@ mod report_agent_job_result {
"result must be a JSON object".to_string(),
));
}
let db = required_state_db(&session)?;
let db = required_state_db(&session).await?;
let reporting_thread_id = session.conversation_id.to_string();
let accepted = db
.report_agent_job_item_result(
@ -511,11 +511,23 @@ mod report_agent_job_result {
}
}
fn required_state_db(
async fn required_state_db(
session: &Arc<Session>,
) -> Result<Arc<codex_state::StateRuntime>, FunctionCallError> {
session.state_db().ok_or_else(|| {
FunctionCallError::Fatal("sqlite state db is unavailable for this session".to_string())
if let Some(state_db) = session.state_db() {
return Ok(state_db);
}
let config = session.get_config().await;
codex_state::StateRuntime::init(
config.codex_home.to_path_buf(),
config.model_provider_id.clone(),
)
.await
.map_err(|err| {
FunctionCallError::Fatal(format!(
"sqlite state db is unavailable for this session and fallback init failed: {err}"
))
})
}

View file

@ -195,12 +195,11 @@ impl ToolsConfig {
None => include_apply_patch_tool.then_some(ApplyPatchToolType::Freeform),
};
let agent_jobs_worker_tools = include_agent_jobs
&& matches!(
session_source,
SessionSource::SubAgent(SubAgentSource::Other(label))
if label.starts_with("agent_job:")
);
let agent_jobs_worker_tools = matches!(
session_source,
SessionSource::SubAgent(SubAgentSource::Other(label))
if label.starts_with("agent_job:")
);
Self {
available_models: available_models.to_vec(),

View file

@ -157,6 +157,29 @@ fn subagents_keep_request_user_input_mode_config_and_agent_jobs_workers_opt_in_b
assert!(tools_config.agent_jobs_worker_tools);
}
#[test]
fn agent_job_workers_get_report_tool_even_when_csv_front_door_disabled() {
let model_info = model_info();
let features = Features::with_defaults();
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::SubAgent(SubAgentSource::Other(
"agent_job:test".to_string(),
)),
sandbox_policy: &SandboxPolicy::DangerFullAccess,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
assert!(!tools_config.agent_jobs_tools);
assert!(tools_config.agent_jobs_worker_tools);
}
#[test]
fn image_generation_requires_feature_and_supported_model() {
let supported_model_info = model_info();

View file

@ -484,14 +484,15 @@ pub fn build_tool_registry_plan(
config.code_mode_enabled,
);
plan.register_handler("spawn_agents_on_csv", ToolHandlerKind::AgentJobs);
if config.agent_jobs_worker_tools {
plan.push_spec(
create_report_agent_job_result_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler("report_agent_job_result", ToolHandlerKind::AgentJobs);
}
}
if config.agent_jobs_worker_tools {
plan.push_spec(
create_report_agent_job_result_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler("report_agent_job_result", ToolHandlerKind::AgentJobs);
}
if let Some(mcp_tools) = params.mcp_tools {

View file

@ -511,6 +511,36 @@ fn test_build_specs_agent_job_worker_tools_enabled() {
);
}
#[test]
fn test_build_specs_agent_job_worker_report_tool_without_csv_front_door() {
let model_info = model_info();
let mut features = Features::with_defaults();
features.normalize_dependencies();
features.enable(Feature::Sqlite);
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::SubAgent(SubAgentSource::Other(
"agent_job:test".to_string(),
)),
sandbox_policy: &SandboxPolicy::DangerFullAccess,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
assert_contains_tool_names(&tools, &["report_agent_job_result"]);
assert_lacks_tool_name(&tools, "spawn_agents_on_csv");
}
#[test]
fn request_user_input_description_reflects_default_mode_feature_flag() {
let model_info = model_info();