diff --git a/config/examples/article-review-pipeline.yaml b/config/examples/article-review-pipeline.yaml index 17158c16..6f5f9244 100644 --- a/config/examples/article-review-pipeline.yaml +++ b/config/examples/article-review-pipeline.yaml @@ -16,13 +16,8 @@ Orchestration: TriggerTurnCount: 20 KeepRecentTurns: 6 - # Isolation: Shared declared explicitly on every agent below to preserve this example's - # original behavior — the default changed to Fresh (no SharedHistory) with the agent-isolation - # protocol overhaul. See skills/craft-orchestration/references/schema-cheatsheet.md for what - # Isolation: Fresh + Context: buys you and when it's safe to switch an agent over. Agents: - Name: Writer - Isolation: Shared Description: Technical writer who drafts or revises an article based on the task and any editor feedback. Instructions: | You are a technical writer. @@ -54,7 +49,6 @@ Orchestration: FunctionChoice: none - Name: Editor - Isolation: Shared Description: Senior editor who evaluates drafts for quality, accuracy, and completeness. Instructions: | You are a senior editor. @@ -96,7 +90,6 @@ Orchestration: FunctionChoice: none - Name: Publisher - Isolation: Shared Description: Publisher who saves the approved article to disk as a Markdown file. Instructions: | You are a content publisher. diff --git a/config/examples/brownfield.yaml b/config/examples/brownfield.yaml index 6b56e1fa..fb056757 100644 --- a/config/examples/brownfield.yaml +++ b/config/examples/brownfield.yaml @@ -130,13 +130,8 @@ Orchestration: KeepRecentTurns: 10 Mode: lossless - # Isolation: Shared declared explicitly on every agent below to preserve this example's - # original behavior — the default changed to Fresh (no SharedHistory) with the agent-isolation - # protocol overhaul. See skills/craft-orchestration/references/schema-cheatsheet.md for what - # Isolation: Fresh + Context: buys you and when it's safe to switch an agent over. Agents: - Name: Archaeologist - Isolation: Shared Description: Recon agent that maps the codebase before any changes are made. Instructions: | You are a codebase archaeologist. Your job is reconnaissance — no code changes. @@ -223,7 +218,6 @@ Orchestration: Git: [read] - Name: Planner - Isolation: Shared Description: Scopes the task to the Archaeologist's findings and writes brief.json. Instructions: | You are a technical project planner working on a brownfield codebase. @@ -276,7 +270,6 @@ Orchestration: - Handoff - Name: Developer - Isolation: Shared Description: Senior engineer who implements within the enforced change envelope. Instructions: | You are an expert software developer working in a brownfield codebase. @@ -328,7 +321,6 @@ Orchestration: - Handoff - Name: Tester - Isolation: Shared Description: QA engineer who runs targeted tests and verifies acceptance criteria. Instructions: | You are an expert QA engineer. Verify everything independently. @@ -383,7 +375,6 @@ Orchestration: - Handoff - Name: Reviewer - Isolation: Shared Description: Tech lead who approves only after reading code and confirming conventions. Instructions: | You are a senior tech lead performing a final code review on a brownfield codebase. @@ -442,7 +433,6 @@ Orchestration: TextOnly: true - Name: Verifier - Isolation: Shared Description: Evidence auditor who checks for inconsistencies between claims and actions. Instructions: | You are an evidence auditor. Detect inconsistencies between what agents claim diff --git a/config/examples/cross-system-flow-analyzer.yaml b/config/examples/cross-system-flow-analyzer.yaml index e92d3ed5..66c87593 100644 --- a/config/examples/cross-system-flow-analyzer.yaml +++ b/config/examples/cross-system-flow-analyzer.yaml @@ -111,17 +111,12 @@ Orchestration: Mode: json Path: .fuseraft/checkpoints - # Isolation: Shared declared explicitly on every agent below to preserve this example's - # original behavior — the default changed to Fresh (no SharedHistory) with the agent-isolation - # protocol overhaul. See skills/craft-orchestration/references/schema-cheatsheet.md for what - # Isolation: Fresh + Context: buys you and when it's safe to switch an agent over. Agents: ## Phase 1: map the territory — orient without deep-reading files ## ── The only agent with hardcoded workspace paths. Update the "Workspace layout" ## block below; all downstream agents derive their paths from .fuseraft/recon.md. - Name: Archaeologist - Isolation: Shared Instructions: | You are a codebase cartographer. Your job is to map the territory, not read everything. @@ -197,7 +192,6 @@ Orchestration: ## Phase 2: deep-read the SQL layer ## Paths come from recon.md — no workspace-specific edits needed here. - Name: SQLAnalyst - Isolation: Shared Instructions: | You are a SQL expert. Begin by reading .fuseraft/recon.md — it lists the exact stored procedure paths and schema objects relevant to this flow. The database @@ -274,7 +268,6 @@ Orchestration: ## Phase 3: deep-read the API layer, cross-reference SQL ## Paths come from recon.md — no workspace-specific edits needed here. - Name: APIAnalyst - Isolation: Shared Instructions: | You are a web API expert. Begin by reading .fuseraft/recon.md and .fuseraft/sql-findings.md. The recon.md lists exact API route paths; the API @@ -350,7 +343,6 @@ Orchestration: ## Phase 4: extract signal from binary docs ## Paths come from recon.md — no workspace-specific edits needed here. - Name: DocAnalyst - Isolation: Shared Instructions: | You are a technical documentation analyst. Begin by reading .fuseraft/recon.md for the list of document paths. Use document_extract_text for each one @@ -398,7 +390,6 @@ Orchestration: ## are hit here in practice, consider decomposing into EntityNormalizer → ## RelationshipNormalizer → AsyncFlowNormalizer → CanonicalAssembler. - Name: Normalizer - Isolation: Shared Instructions: | You are a data normalization specialist. Read all four findings files: .fuseraft/recon.md @@ -468,7 +459,6 @@ Orchestration: ## Phase 6: connect the dots — this agent sees only text, never tool frames - Name: Synthesizer - Isolation: Shared Instructions: | You are a systems integration architect. Read .fuseraft/canonical-model.json (the normalized, deduplicated entity model) plus all findings files for prose detail: @@ -578,7 +568,6 @@ Orchestration: ## Phase 7: produce the artifact - Name: DiagramBuilder - Isolation: Shared Instructions: | You are a DrawIO expert. Read .fuseraft/flow-model.json (machine-readable graph) and .fuseraft/flow-model.md (supplemental annotation only). @@ -635,7 +624,6 @@ Orchestration: ## Phase 8: validate all outputs for consistency - Name: OutputValidator - Isolation: Shared Instructions: | You are an output validator. Read: .fuseraft/canonical-model.json diff --git a/config/examples/dev-team-structured.yaml b/config/examples/dev-team-structured.yaml index b690be53..1c4fec99 100644 --- a/config/examples/dev-team-structured.yaml +++ b/config/examples/dev-team-structured.yaml @@ -82,13 +82,8 @@ Orchestration: KeepRecentTurns: 10 Mode: lossless - # Isolation: Shared declared explicitly on every agent below to preserve this example's - # original behavior — the default changed to Fresh (no SharedHistory) with the agent-isolation - # protocol overhaul. See skills/craft-orchestration/references/schema-cheatsheet.md for what - # Isolation: Fresh + Context: buys you and when it's safe to switch an agent over. Agents: - Name: Planner - Isolation: Shared Description: Session planner who reads the task and codebase to produce a focused brief. Instructions: | You are a technical project planner. @@ -135,7 +130,6 @@ Orchestration: - Handoff - Name: Developer - Isolation: Shared Description: Senior software engineer who implements features using tools. Instructions: | You are an expert software developer. @@ -173,7 +167,6 @@ Orchestration: - Handoff - Name: Tester - Isolation: Shared Description: QA engineer who verifies changes with real tool calls. Instructions: | You are an expert QA engineer. Verify everything independently. @@ -223,7 +216,6 @@ Orchestration: - Handoff - Name: Reviewer - Isolation: Shared Description: Tech lead who approves only after reading code, running a spot-check, and confirming all criteria pass. Instructions: | You are a senior tech lead performing a final review. @@ -278,7 +270,6 @@ Orchestration: TextOnly: true - Name: Verifier - Isolation: Shared Description: Evidence auditor who checks for inconsistencies between claims and recorded actions. Instructions: | You are an evidence auditor. Detect inconsistencies between what agents claim diff --git a/config/examples/devops-team.yaml b/config/examples/devops-team.yaml index 64d9b726..3657416e 100644 --- a/config/examples/devops-team.yaml +++ b/config/examples/devops-team.yaml @@ -44,13 +44,8 @@ Orchestration: KeepRecentTurns: 8 Mode: lossless - # Isolation: Shared declared explicitly on every agent below to preserve this example's - # original behavior — the default changed to Fresh (no SharedHistory) with the agent-isolation - # protocol overhaul. See skills/craft-orchestration/references/schema-cheatsheet.md for what - # Isolation: Fresh + Context: buys you and when it's safe to switch an agent over. Agents: - Name: Architect - Isolation: Shared Description: Senior architect who analyses requirements and writes a concrete implementation plan to disk. Instructions: | You are a senior software architect. @@ -84,7 +79,6 @@ Orchestration: - Handoff - Name: Engineer - Isolation: Shared Description: Full-stack engineer who executes the plan using tools — never describes changes without making them. Instructions: | You are a full-stack engineer executing the Architect's plan. @@ -128,7 +122,6 @@ Orchestration: - Handoff - Name: Operator - Isolation: Shared Description: Site reliability engineer who executes the deployment and verifies success. Instructions: | You are a site reliability engineer. diff --git a/config/examples/etl-pipeline.yaml b/config/examples/etl-pipeline.yaml index 9a9c42f4..70a4b803 100644 --- a/config/examples/etl-pipeline.yaml +++ b/config/examples/etl-pipeline.yaml @@ -50,13 +50,8 @@ Orchestration: - "output/**" - ".fuseraft/artifacts/**" - # Isolation: Shared declared explicitly on every agent below to preserve this example's - # original behavior — the default changed to Fresh (no SharedHistory) with the agent-isolation - # protocol overhaul. See skills/craft-orchestration/references/schema-cheatsheet.md for what - # Isolation: Fresh + Context: buys you and when it's safe to switch an agent over. Agents: - Name: Extractor - Isolation: Shared Description: Reads and validates the raw input data named in the task. Instructions: | You extract and validate raw pipeline input. You never write files. @@ -82,7 +77,6 @@ Orchestration: FunctionChoice: required - Name: Transformer - Isolation: Shared Description: Normalizes the extracted data and writes it to the output path. Instructions: | You transform validated pipeline data and write the result. You run diff --git a/config/examples/magentic-team.yaml b/config/examples/magentic-team.yaml index 2e1bc0b7..f71e1339 100644 --- a/config/examples/magentic-team.yaml +++ b/config/examples/magentic-team.yaml @@ -15,14 +15,9 @@ Orchestration: Endpoint: https://api.openai.com/v1 ApiKeyEnvVar: OPENAI_API_KEY - # Isolation: Shared declared explicitly on every agent below because Magentic's manager/ - # ledger loop structurally depends on shared visibility of progress across all participants — - # the config loader rejects Isolation: Fresh (the default) under Selection.Type: magentic. - # See skills/craft-orchestration/references/schema-cheatsheet.md for details. Agents: - Name: Researcher Description: Gathers information, summarizes findings, and answers factual questions. - Isolation: Shared Instructions: | You are a Researcher. Your job is to find information, analyze data, and produce well-sourced summaries. When asked to investigate a topic, be thorough but concise. @@ -36,7 +31,6 @@ Orchestration: - Name: Developer Description: Writes code, implements features, runs tests, and fixes bugs. - Isolation: Shared Instructions: | You are a Developer. Your job is to write clean, working code that solves the problem at hand. When implementing features, write the code first, then test it. diff --git a/config/examples/open-webui.yaml b/config/examples/open-webui.yaml index 910ecc61..a9a08e35 100644 --- a/config/examples/open-webui.yaml +++ b/config/examples/open-webui.yaml @@ -73,13 +73,8 @@ Orchestration: KeepRecentTurns: 6 Mode: lossless - # Isolation: Shared declared explicitly on every agent below to preserve this example's - # original behavior — the default changed to Fresh (no SharedHistory) with the agent-isolation - # protocol overhaul. See skills/craft-orchestration/references/schema-cheatsheet.md for what - # Isolation: Fresh + Context: buys you and when it's safe to switch an agent over. Agents: - Name: Planner - Isolation: Shared Description: Session planner who reads the task and codebase to produce a focused brief for the team. Instructions: | You are a technical project planner. @@ -132,7 +127,6 @@ Orchestration: - Handoff - Name: Developer - Isolation: Shared Description: Senior software engineer who implements features using tools. Instructions: | You are an expert software developer with access to filesystem, shell, and git tools. @@ -174,7 +168,6 @@ Orchestration: - Handoff - Name: Tester - Isolation: Shared Description: QA engineer who independently verifies changes with real tool calls. Instructions: | You are an expert QA engineer. DO NOT trust the Developer's account — verify everything independently. @@ -231,7 +224,6 @@ Orchestration: - Handoff - Name: Reviewer - Isolation: Shared Description: Tech lead who approves only after reading the code, running a spot-check, and confirming all acceptance criteria are verified passing. Instructions: | You are a senior tech lead performing a final review. @@ -292,7 +284,6 @@ Orchestration: TextOnly: true - Name: Verifier - Isolation: Shared Description: Evidence auditor who checks for inconsistencies between claims and recorded actions. Instructions: | You are an evidence auditor. Detect inconsistencies between what agents claim diff --git a/config/examples/orchestration.yaml b/config/examples/orchestration.yaml index 96b6b5fe..15a96fd6 100644 --- a/config/examples/orchestration.yaml +++ b/config/examples/orchestration.yaml @@ -85,14 +85,9 @@ Orchestration: KeepRecentTurns: 10 Mode: lossless - # Isolation: Shared declared explicitly on every agent below to preserve this example's - # original behavior — the default changed to Fresh (no SharedHistory) with the agent-isolation - # protocol overhaul. See skills/craft-orchestration/references/schema-cheatsheet.md for what - # Isolation: Fresh + Context: buys you and when it's safe to switch an agent over. Agents: - Name: Planner Description: Session planner who reads the task and codebase to produce a focused brief. - Isolation: Shared Instructions: | You are a technical project planner. @@ -130,7 +125,6 @@ Orchestration: - Name: Developer Description: Senior software engineer who implements features using tools. - Isolation: Shared Instructions: | You are an expert software developer. @@ -158,7 +152,6 @@ Orchestration: - Name: Tester Description: QA engineer who runs tests and writes a structured report. - Isolation: Shared Instructions: | You are a quality assurance engineer. @@ -197,7 +190,6 @@ Orchestration: - Name: Reviewer Description: Tech lead who approves completed work or requests revisions. - Isolation: Shared Instructions: | You are a senior tech lead performing a final code review. @@ -227,7 +219,6 @@ Orchestration: - Name: Verifier Description: Evidence auditor who checks for inconsistencies between claims and recorded actions. - Isolation: Fork Instructions: | You are an evidence auditor. Detect inconsistencies between what agents claim and what is recorded in the change log. diff --git a/config/examples/playwright-mcp.yaml b/config/examples/playwright-mcp.yaml index a2ebb885..e4098121 100644 --- a/config/examples/playwright-mcp.yaml +++ b/config/examples/playwright-mcp.yaml @@ -23,13 +23,8 @@ Orchestration: - "--browser" - "chromium" # must match the browser installed via playwright-core's cli.js - # Isolation: Shared declared explicitly on every agent below to preserve this example's - # original behavior — the default changed to Fresh (no SharedHistory) with the agent-isolation - # protocol overhaul. See skills/craft-orchestration/references/schema-cheatsheet.md for what - # Isolation: Fresh + Context: buys you and when it's safe to switch an agent over. Agents: - Name: BrowserAgent - Isolation: Shared Description: Automates browser interactions using Playwright tools. Instructions: | You are a browser automation agent with access to Playwright tools. diff --git a/config/examples/research-team.yaml b/config/examples/research-team.yaml index 29393d2e..ea50f2af 100644 --- a/config/examples/research-team.yaml +++ b/config/examples/research-team.yaml @@ -38,13 +38,8 @@ Orchestration: KeepRecentTurns: 6 Mode: lossless - # Isolation: Shared declared explicitly on every agent below to preserve this example's - # original behavior — the default changed to Fresh (no SharedHistory) with the agent-isolation - # protocol overhaul. See skills/craft-orchestration/references/schema-cheatsheet.md for what - # Isolation: Fresh + Context: buys you and when it's safe to switch an agent over. Agents: - Name: Researcher - Isolation: Shared Description: Data researcher who fetches real information using HTTP and filesystem tools. Instructions: | You are a research specialist. @@ -79,7 +74,6 @@ Orchestration: - Handoff - Name: Writer - Isolation: Shared Description: Technical writer who synthesises research into a structured report. Instructions: | You are a technical writer. diff --git a/config/orchestration.yaml b/config/orchestration.yaml index 916bfcbb..804ed267 100644 --- a/config/orchestration.yaml +++ b/config/orchestration.yaml @@ -75,18 +75,9 @@ Orchestration: KeepRecentTurns: 10 Mode: lossless - # Isolation is explicit below on every agent because this config predates the Isolation: - # field (default changed to Fresh — no SharedHistory unless an agent opts in). Every agent - # here declares `Isolation: Shared` to preserve its exact original behavior: several agents' - # instructions depend on prior conversation being visible (e.g. Planner step 3, "IF THIS IS - # A RETRY: Reviewer feedback is present in context"). Migrating individual agents to - # `Isolation: Fresh` + an explicit `Context:` block is a real option — most of them already - # read their inputs from .fuseraft/artifacts/*.json by hand — but changes turn-by-turn - # behavior and needs its own validation pass; not done wholesale here. Agents: - Name: Planner Description: Session planner who reads the task and codebase to produce a focused brief for the team. - Isolation: Shared Instructions: | You are a technical project planner. @@ -146,7 +137,6 @@ Orchestration: - Name: PlannerCritic Description: Adversarially reviews the brief for completeness before the Developer starts. - Isolation: Shared Instructions: | You are an adversarial brief reviewer. Find reasons the brief will FAIL — not reasons it will succeed. A brief that passes goes directly to the Developer; one that fails @@ -198,7 +188,6 @@ Orchestration: - Name: Developer Description: Senior software engineer who implements features using tools. - Isolation: Shared Instructions: | You are an expert software developer with access to filesystem, shell, and git tools. @@ -238,7 +227,6 @@ Orchestration: - Name: Tester Description: QA engineer who independently verifies changes with real tool calls and blocks promotion on any failure. - Isolation: Shared Instructions: | You are an expert QA engineer. DO NOT trust the Developer's account — verify everything independently. @@ -300,7 +288,6 @@ Orchestration: - Name: Reviewer Description: Tech lead who approves only after reading the code, running a spot-check, and confirming all acceptance criteria are verified passing. - Isolation: Shared Instructions: | You are a senior tech lead performing a final review. @@ -359,12 +346,6 @@ Orchestration: - Name: Verifier Description: Evidence auditor who checks for inconsistencies between claims and recorded actions. - # Fork, not Shared: the Verifier's whole job is cross-checking claims made "in recent - # conversation messages" against the change log — it needs the full transcript. Fork - # additionally layers in a synthesized directive on the turns it's dispatched via a - # handoff() call; EveryNTurns-triggered runs behave the same as Shared (no directive - # available), so this is strictly no worse than Shared and correct for its stated role. - Isolation: Fork Instructions: | You are an evidence auditor. Detect inconsistencies between what agents claim and what is recorded in the change log. diff --git a/config/security/red-team.yaml b/config/security/red-team.yaml index cb2e4af3..2c1e1514 100644 --- a/config/security/red-team.yaml +++ b/config/security/red-team.yaml @@ -81,14 +81,9 @@ Orchestration: Events: Path: .fuseraft/red-team/events.jsonl - # Isolation: Shared declared explicitly on every agent below to preserve this example's - # original behavior — the default changed to Fresh (no SharedHistory) with the agent-isolation - # protocol overhaul. See skills/craft-orchestration/references/schema-cheatsheet.md for what - # Isolation: Fresh + Context: buys you and when it's safe to switch an agent over. Agents: ## Phase 1: map the attack surface - Name: Recon - Isolation: Shared Description: Maps fuseraft-cli's attack surface before adversarial probing begins. TrustScore: 0.9 Instructions: | @@ -154,7 +149,6 @@ Orchestration: ## Phase 2: static code analysis - Name: StaticAttacker - Isolation: Shared Description: Red Team Alpha — reads source code and identifies implementation vulnerabilities. TrustScore: 0.70 Instructions: | @@ -265,7 +259,6 @@ Orchestration: ## Fix 1: ProbeWriter cannot execute anything. It only creates YAML files. ## Even if prompt-injected, it has no way to run a command. - Name: ProbeWriter - Isolation: Shared Description: Red Team Bravo — writes malicious config files for the Prober to test. No shell access. TrustScore: 0.70 Instructions: | @@ -429,7 +422,6 @@ Orchestration: ## a Docker container with --network none. The validate step uses the Probe plugin ## (structured, no general-purpose scripting) rather than Shell. - Name: Prober - Isolation: Shared Description: Red Team Bravo — runs fuseraft validate on each probe and analyses YAML parsing in Docker. TrustScore: 0.75 Instructions: | @@ -524,7 +516,6 @@ Orchestration: ## Phase 4: triage and report - Name: Triage - Isolation: Shared Description: Deduplicates findings from both agents, scores by severity, and writes the security report. TrustScore: 0.9 Instructions: | diff --git a/docs/models.md b/docs/models.md index c482cda2..2827beda 100644 --- a/docs/models.md +++ b/docs/models.md @@ -108,19 +108,12 @@ For any model not matching the table, specify `Provider`, `Endpoint`, and `ApiKe { "modelId": "anthropic.claude-sonnet-4-6-20250929-v1:0", "endpoint": "http://localhost:3000/api/openai/v1", - "apiKeyEnvVar": "OPENWEBUI_API_KEY", - "replContextBudget": 400000 + "apiKeyEnvVar": "OPENWEBUI_API_KEY" } ``` Set this file via `fuseraft repl` or `fuseraft models` (the setup wizard runs automatically on first use) or edit it directly. Run `fuseraft models` to see all models available from the configured provider, or use `/models` inside a REPL session for the same list. -### `replContextBudget` — REPL working-context override - -The REPL trims conversation history against a working-context-token budget (`ctx.ContextTokenBudget`, shown in `/context`), separate from `MaxContextTokens` above. By default this budget comes from a per-model-family heuristic (150K for 1M/128K+-class frontier models like `claude-*`/`gemini-*`/`grok-*`/`gpt-5*`, 100K for ~128K-class models like `gpt-4*`/`mistral-*`/`deepseek-*`, 80K otherwise) — deliberately conservative, since the REPL's char-based token estimate doesn't account for tool-schema tokens. - -Set `replContextBudget` in `~/.fuseraft/config` (a positive integer, in tokens) to override that heuristic for every model used in the REPL session, regardless of family. Leave it unset (or `0`) to keep the built-in heuristic. This is REPL-only and does not affect `MaxContextTokens` above (a separate per-agent hard ceiling enforced before each API call in non-REPL agent/orchestration contexts), nor the unrelated `ContextBudget` YAML block used in `orchestration.yaml` (warn/cutover/tool-result trimming for multi-agent orchestration runs) — the similarly-named `replContextBudget` field intentionally carries the `Repl` prefix to keep the two apart. - ### OS keychain fallback If an agent model has neither `ApiKey` nor `ApiKeyEnvVar` set after global defaults are applied, fuseraft retrieves the key stored in the OS keychain (set via `fuseraft key set` or the REPL wizard) and injects it as a literal `ApiKey`. This means the full auth resolution order for any agent model is: diff --git a/skills/craft-orchestration/references/schema-cheatsheet.md b/skills/craft-orchestration/references/schema-cheatsheet.md index 9c6d0824..5ccea9aa 100644 --- a/skills/craft-orchestration/references/schema-cheatsheet.md +++ b/skills/craft-orchestration/references/schema-cheatsheet.md @@ -123,10 +123,9 @@ Orchestration: - FileSystem - Shell - Handoff - Isolation: Fresh # Fresh (default) | Shared | Fork — see "Isolation" below ContextWindow: - TextOnly: true # strip tool-call results from context window; ignored when Isolation: Fresh - Context: # this agent's own inputs — always used under Isolation: Fresh + TextOnly: true # strip tool-call results from context window + Context: # replaces ContextWindow when set; assembles from artifacts - Source: session_context # handoff summary from session_context_write - Source: changes_recent:5 # last 5 change-log entries - Source: brief_field:test_targets # field from brief.json @@ -143,49 +142,6 @@ Orchestration: --- -## Isolation - -Controls whether an agent sees the shared session transcript other agents have been writing -to, or only a synthesized handoff directive plus its own declared `Context:` sources — the -same fresh-by-default, fork-by-explicit-choice split Claude Code uses for its own sub-agents. - -| Mode | What the agent receives | Use for | -|---|---|---| -| `Fresh` (default) | The synthesized `AgentDirective` (see below) + its own `Context:` sources only. Never `SharedHistory` — even with an empty/absent `Context:` block. | Most agents. No inherited reasoning, dead ends, or another agent's tool-call noise. | -| `Shared` | `Context:` block if declared, else the windowed shared transcript (`ContextWindow`). Pre-overhaul behavior. | Conversational round-robin/keyword group chats; anything whose prompts assume prior turns are visible. | -| `Fork` | `Shared` behavior **plus** the synthesized directive layered on top. | Meta-agents that genuinely need the full transcript AND a clear statement of what to do with it — a Verifier auditing the session, a RecoveryAgent diagnosing a failure. | - -**`Selection.Type: magentic` requires every agent to be `Shared` or `Fork`** — the manager's -ledger loop depends on shared visibility of progress across all participants; config load fails -with `Isolation: Fresh` under Magentic. - -A `Fresh` agent with no `Context:` sources at all still runs — it just receives nothing but the -directive each turn. Fine for a terminal/leaf agent; a load-time warning flags this for anything -that looks like it needs durable state. - -### The directive: how a `Fresh` agent learns what to do - -Extend the `handoff()` call with optional structured fields instead of leaving the receiving -agent to infer intent from a bare routing keyword: - -``` -handoff( - route_keyword: "HANDOFF TO DEVELOPER", - goal: "Add pagination to GET /users.", - background: "Explored the handler in src/api/users.py — no existing page param. " + - "Auth middleware already extracts the caller; don't touch it.", - constraints: "Do not change the existing response shape for callers that omit ?page." -) -``` - -`goal`/`background`/`constraints` are optional — a bare `handoff(route_keyword: ...)` still -works exactly as before. When present, they become the receiving agent's task message under -`Isolation: Fresh` (and are layered onto the transcript under `Fork`). Write them the way you'd -brief a colleague who wasn't in the room: state what's already been learned or ruled out, don't -assume they can see your reasoning. - ---- - ## All plugin names | Plugin | What it provides | @@ -269,10 +225,9 @@ Selection: Signal: "HANDOFF TO TESTER" Contract: ImplementationComplete HandoffContext: # inject targeted artifacts when transition fires - - Source: session_context # NOTE: only takes effect for Shared/Fork agents — - - Source: changes_recent # a Fresh agent never reads SharedHistory, so this - - Source: brief_field:test_targets # never reaches it. Put the same sources in the - # target agent's own Context: block instead. + - Source: session_context + - Source: changes_recent + - Source: brief_field:test_targets - To: Planning Signal: "REPLAN REQUIRED" @@ -480,7 +435,6 @@ Termination: | `RequireReviewJudgement` | Reviewer → Done | Reviewer emitted `{"review":[...]}` with all PASS verdicts + shell run | | `RequireRelatedTestsPass` | Developer → Tester | Targeted tests for changed files pass (needs `TestSelector`) | | `RequireAcceptanceCriteriaPassedValidator` | Developer → Reviewer | Machine-testable criteria verified by real shell output | -| `RequireSessionContextWrite` | Any route/edge/transition whose source agent is `Isolation: Fresh` | At least one `session_context_write` call this turn — not auto-attached; add it explicitly so a `Fresh` agent that forgets to write a summary fails loudly instead of silently handing the next agent nothing | --- diff --git a/src/Cli/Commands/Repl/ModelContextWindow.cs b/src/Cli/Commands/Repl/ModelContextWindow.cs index 0ecce616..2c95dff7 100644 --- a/src/Cli/Commands/Repl/ModelContextWindow.cs +++ b/src/Cli/Commands/Repl/ModelContextWindow.cs @@ -35,15 +35,8 @@ internal static class ModelContextWindow /// so both bare model IDs (e.g. claude-sonnet-4-6) and provider-prefixed deployment /// IDs (e.g. Bedrock's anthropic.claude-sonnet-4-6-20250929-v1:0) resolve correctly. /// - /// The model ID whose family determines the heuristic budget. - /// - /// User-configured override (). - /// When positive, takes precedence over the per-family heuristic below. - /// - internal static int GetBudget(string? modelId, int? overrideBudget = null) + internal static int GetBudget(string? modelId) { - if (overrideBudget is > 0) return overrideBudget.Value; - if (string.IsNullOrWhiteSpace(modelId)) return DefaultBudget; if (LargeFamilyMarkers.Any(m => modelId.Contains(m, StringComparison.OrdinalIgnoreCase))) diff --git a/src/Cli/Commands/Repl/ReplSessionContext.cs b/src/Cli/Commands/Repl/ReplSessionContext.cs index 2aee84bc..682ed187 100644 --- a/src/Cli/Commands/Repl/ReplSessionContext.cs +++ b/src/Cli/Commands/Repl/ReplSessionContext.cs @@ -49,17 +49,15 @@ public string ModelId set { _modelId = value; - ContextTokenBudget = ModelContextWindow.GetBudget(value, UserCfg?.ReplContextBudget); + ContextTokenBudget = ModelContextWindow.GetBudget(value); } } // Working token budget for history trimming (TrimHistory) and the /context, /compact, - // and context-warning displays — derived from ModelId (and UserCfg.ReplContextBudget, if set) - // so a large-context model isn't held to the same ceiling as a small-context local model. - // Recomputed automatically whenever ModelId is (re)assigned, including on /provider setup, - // /model switch, and session resume. Relies on UserCfg already being current at that point - // — the constructor below sets UserCfg before ModelId for this reason, and every later - // reassignment site that changes both (e.g. /provider setup) must preserve that order. + // and context-warning displays — derived from ModelId so a large-context model isn't + // held to the same ceiling as a small-context local model. Recomputed automatically + // whenever ModelId is (re)assigned, including on /provider setup, /model switch, and + // session resume. public int ContextTokenBudget { get; private set; } = ModelContextWindow.DefaultBudget; public ModelConfig ModelConfig { get; set; } @@ -166,9 +164,9 @@ public ReplSessionContext( Cwd = cwd; SessionId = sessionId; StartedAt = startedAt; - UserCfg = userCfg; ModelId = modelId; ModelConfig = modelConfig; + UserCfg = userCfg; Client = client; Factory = factory; KeyStore = keyStore; diff --git a/src/Cli/Commands/ValidateConfigCommand.cs b/src/Cli/Commands/ValidateConfigCommand.cs index 73a1ec95..d260d94c 100644 --- a/src/Cli/Commands/ValidateConfigCommand.cs +++ b/src/Cli/Commands/ValidateConfigCommand.cs @@ -120,9 +120,7 @@ protected override async Task ExecuteAsync(CommandContext context, Validate issues.Add(("error", $"SystemPromptPath file not found: {promptPath}")); } - var magenticFreshViolations = OrchestratorConfigLoader.FindMagenticFreshIsolationViolations(config) - .ToHashSet(StringComparer.OrdinalIgnoreCase); - ValidateAgents(config, settings, issues, magenticFreshViolations); + ValidateAgents(config, settings, issues); // Selection strategy var selType = config.Selection.Type.ToLowerInvariant(); @@ -214,8 +212,7 @@ protected override async Task ExecuteAsync(CommandContext context, Validate private void ValidateAgents( OrchestrationConfig config, ValidateConfigSettings settings, - List<(string Level, string Message)> issues, - HashSet magenticFreshViolations) + List<(string Level, string Message)> issues) { if (config.Agents.Count == 0) { @@ -286,16 +283,6 @@ private void ValidateAgents( if (effort is not null && effort.Trim().Contains(' ')) issues.Add(("error", $"Agent '{agent.Name}': Model.ReasoningEffort '{effort}' looks malformed — expected a single token (e.g. none, low, medium, high, xhigh, max).")); - // Magentic's manager/ledger loop depends on every participant sharing the - // transcript — Isolation: Fresh (the default) would silently starve it. Uses - // the same OrchestratorConfigLoader.FindMagenticFreshIsolationViolations the - // real config loader hard-fails on, so this lint can't drift out of sync with it. - if (magenticFreshViolations.Contains(agent.Name)) - issues.Add(("error", $"Agent '{agent.Name}': Isolation: Fresh is incompatible with Selection.Type 'magentic' — set 'Isolation: Shared' (or 'Fork').")); - else if (agent.Isolation == fuseraft.Core.Models.Agents.AgentIsolation.Fresh - && agent.Context is not { Count: > 0 }) - issues.Add(("warning", $"Agent '{agent.Name}': Isolation: Fresh (the default) with no Context: sources declared — it will receive only the synthesized handoff directive each turn. Fine for a terminal/leaf agent; otherwise add a Context: block or set 'Isolation: Shared'.")); - if (settings.Strict) { var registered = pluginRegistry.RegisteredPlugins diff --git a/src/Cli/OrchestratorBuilder.cs b/src/Cli/OrchestratorBuilder.cs index bc5b83ee..b6773d3c 100644 --- a/src/Cli/OrchestratorBuilder.cs +++ b/src/Cli/OrchestratorBuilder.cs @@ -117,7 +117,6 @@ public static class OrchestratorBuilder { PropertyNameCaseInsensitive = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = { new JsonStringEnumConverter() }, }; /// diff --git a/src/Cli/OrchestratorConfigLoader.cs b/src/Cli/OrchestratorConfigLoader.cs index d7780c38..1b7f8a6c 100644 --- a/src/Cli/OrchestratorConfigLoader.cs +++ b/src/Cli/OrchestratorConfigLoader.cs @@ -5,7 +5,6 @@ using fuseraft.Core.Models; using fuseraft.Infrastructure.KeyStore; using fuseraft.Infrastructure.Plugins; -using fuseraft.Orchestration; namespace fuseraft.Cli; @@ -51,8 +50,6 @@ public static class OrchestratorConfigLoader if (config.Agents.Count == 0) throw new InvalidOperationException("Config must define at least one agent."); - ValidateIsolationConstraints(config, loggerFactory); - // Expand ${ENV_VAR} tokens in security and API profile config before use. config = ExpandEnvVars(config); @@ -460,53 +457,4 @@ private static void ValidateSchemaVersion(OrchestrationConfig config, ILoggerFac else logger.LogDebug("Config schema_version '{SchemaVersion}' is valid.", config.SchemaVersion); } - - // Magentic's manager/ledger loop structurally depends on every participant seeing the same - // shared transcript to coordinate — Isolation.Fresh (which never reads SharedHistory) would - // silently starve the manager of the progress signal it needs. Reject rather than degrade - // quietly; the fix (drop Isolation: Fresh or switch orchestrator type) is a one-line config - // change, not a runtime workaround. - // - // Separately, warn (do not fail) when a Fresh agent — the default — declares no Context: - // sources at all: such an agent receives only the synthesized handoff directive each turn, - // which is fine for a terminal/leaf agent but likely a misconfiguration for one that needs - // durable state (brief.json, prior changes, etc.) across turns. - // Shared with ValidateConfigCommand's lint pass so the "magentic requires Shared/Fork" - // rule can't drift out of sync between the lint-only check and this hard-throw one. - internal static IReadOnlyList FindMagenticFreshIsolationViolations(OrchestrationConfig config) - { - if (!string.Equals(config.Selection.Type, OrchestratorTypes.Magentic, StringComparison.OrdinalIgnoreCase)) - return []; - - return config.Agents - .Where(a => a.Isolation == AgentIsolation.Fresh) - .Select(a => a.Name) - .ToList(); - } - - internal static void ValidateIsolationConstraints(OrchestrationConfig config, ILoggerFactory loggerFactory) - { - var logger = loggerFactory.CreateLogger(nameof(OrchestratorBuilder)); - - var freshAgents = FindMagenticFreshIsolationViolations(config); - if (freshAgents.Count > 0) - throw new InvalidOperationException( - $"Selection.Type 'magentic' requires every agent to use Isolation: Shared or " + - $"Isolation: Fork — the manager's ledger loop depends on shared visibility of " + - $"progress across all participants. Agent(s) declaring Isolation: Fresh (the " + - $"default): {string.Join(", ", freshAgents)}. Set 'Isolation: Shared' explicitly " + - $"on these agents, or on the whole roster if none should isolate."); - - foreach (var agent in config.Agents) - { - if (agent.Isolation == AgentIsolation.Fresh && agent.Context is not { Count: > 0 }) - logger.LogWarning( - "Agent '{Agent}' uses Isolation: Fresh (the default) with no Context: sources " + - "declared — it will receive only the synthesized handoff directive each turn, " + - "nothing else. This is fine for a terminal/leaf agent; otherwise declare a " + - "Context: block (session_context, brief_field:*, changes_recent:N, own_history:N, " + - "etc.) or set 'Isolation: Shared' if this agent needs the group transcript.", - agent.Name); - } - } } diff --git a/src/Core/Models/Agents/AgentConfig.cs b/src/Core/Models/Agents/AgentConfig.cs index 43dbd310..39025d00 100644 --- a/src/Core/Models/Agents/AgentConfig.cs +++ b/src/Core/Models/Agents/AgentConfig.cs @@ -100,17 +100,6 @@ public record AgentConfig /// public List? Context { get; init; } - /// - /// Controls whether this agent sees the shared session transcript. Defaults to - /// : the agent never reads SharedHistory — its - /// context is built from the synthesized handoff plus whatever - /// sources it declares. Set for - /// orchestration styles that need shared visibility to coordinate (e.g. Magentic's - /// manager/ledger loop), or for meta-agents (Verifier, - /// RecoveryAgent) that need the full transcript plus an explicit directive. - /// - public AgentIsolation Isolation { get; init; } = AgentIsolation.Fresh; - /// /// When true, suppresses the automatic execution_state prepend that /// OrchestratorBuilder injects for all state-machine agents. Set this when an diff --git a/src/Core/Models/Agents/AgentDirective.cs b/src/Core/Models/Agents/AgentDirective.cs deleted file mode 100644 index 9f8e85ee..00000000 --- a/src/Core/Models/Agents/AgentDirective.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Text; - -namespace fuseraft.Core.Models.Agents; - -/// -/// Synthesized handoff payload passed to the next agent at a routing transition, replacing -/// the historical pattern of injecting a bare [fuseraft: A → B] marker (or a heuristic -/// excerpt of A's raw response) into the shared transcript. Populated from the optional -/// goal/background/constraints arguments on handoff() -/// (). -/// -public sealed record AgentDirective -{ - /// What the receiving agent must accomplish this turn. - public required string Goal { get; init; } - - /// What the handing-off agent already learned, tried, or ruled out. - public string? Background { get; init; } - - /// Explicit constraints the receiving agent must respect. - public IReadOnlyList Constraints { get; init; } = []; - - /// Renders this directive as a single user-facing message body. - public string Format() - { - var sb = new StringBuilder(); - sb.AppendLine(Goal); - - if (!string.IsNullOrWhiteSpace(Background)) - { - sb.AppendLine(); - sb.AppendLine("Background:"); - sb.AppendLine(Background); - } - - if (Constraints.Count > 0) - { - sb.AppendLine(); - sb.AppendLine("Constraints:"); - foreach (var c in Constraints) - sb.AppendLine($"- {c}"); - } - - return sb.ToString().TrimEnd(); - } -} diff --git a/src/Core/Models/Agents/AgentExecutionRequest.cs b/src/Core/Models/Agents/AgentExecutionRequest.cs index 75a1e4c5..f73bb701 100644 --- a/src/Core/Models/Agents/AgentExecutionRequest.cs +++ b/src/Core/Models/Agents/AgentExecutionRequest.cs @@ -17,16 +17,6 @@ public sealed record AgentExecutionRequest /// The shared conversation history accumulated so far. public required IReadOnlyList SharedHistory { get; init; } - /// - /// Synthesized handoff payload for this turn, built from the routing agent's - /// handoff(goal, background, constraints) call. Used verbatim as the agent's task - /// message when is - /// ; layered on top of shared history when - /// . Null when the routing agent's handoff call omitted - /// the optional directive fields, or on the first turn of a session. - /// - public AgentDirective? Directive { get; init; } - /// Per-agent configuration (context window, knowledge weight, context sources, etc.). public AgentConfig? AgentConfig { get; init; } diff --git a/src/Core/Models/Agents/AgentIsolation.cs b/src/Core/Models/Agents/AgentIsolation.cs deleted file mode 100644 index 651bf4bb..00000000 --- a/src/Core/Models/Agents/AgentIsolation.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace fuseraft.Core.Models.Agents; - -/// -/// Controls what context an agent receives at each invocation — specifically, whether it sees -/// the shared session transcript other agents have been writing to, or only a synthesized -/// directive plus its own declared sources. -/// -public enum AgentIsolation -{ - /// - /// The agent never sees SharedHistory. Its context is built entirely from the - /// incoming (goal/background/constraints synthesized at - /// handoff time) plus its own declared sources, if any. - /// This is the default: agents do not inherit another agent's reasoning, dead ends, or - /// tool-call noise unless a Context: source explicitly names it. - /// - Fresh = 0, - - /// - /// Legacy/pre-overhaul behavior: if declared, otherwise - /// the windowed shared transcript (SharedHistoryFallback). Required for orchestration - /// styles that depend on shared visibility to coordinate — e.g. MagenticOrchestrator's - /// manager/ledger loop, or simple conversational round-robin/keyword group chats. - /// - Shared = 1, - - /// - /// behavior plus the synthesized layered - /// on top. For meta-agents that genuinely need the full transcript AND a clear statement of - /// what to do with it — e.g. a Verifier auditing the session, or a RecoveryAgent diagnosing - /// a failure. - /// - Fork = 2, -} diff --git a/src/Core/Models/Config/UserConfig.cs b/src/Core/Models/Config/UserConfig.cs index 76ff4f9d..663f590f 100644 --- a/src/Core/Models/Config/UserConfig.cs +++ b/src/Core/Models/Config/UserConfig.cs @@ -19,18 +19,6 @@ public sealed class UserConfig [JsonPropertyName("skillCuration")] public SkillCurationConfig? SkillCuration { get; set; } - /// - /// Overrides the REPL's heuristic working-context-token budget () - /// used for history trimming and the /context, /compact, and context-warning displays. - /// REPL-only — unrelated to the orchestration-level ContextBudgetConfig - /// (warn/cutover/tool-result trimming for agent orchestration runs); the similar name is - /// coincidental, hence the Repl prefix here to keep the two unambiguous. - /// Applies to every model used in the REPL session, regardless of model family. Null or - /// <= 0 falls back to the built-in per-family heuristic. - /// - [JsonPropertyName("replContextBudget")] - public int? ReplContextBudget { get; set; } - // Never written to disk — populated at runtime from the OS keychain. [JsonIgnore] public string ApiKey { get; set; } = string.Empty; diff --git a/src/Infrastructure/Plugins/HandoffPlugin.cs b/src/Infrastructure/Plugins/HandoffPlugin.cs index e8b90607..e1052c83 100644 --- a/src/Infrastructure/Plugins/HandoffPlugin.cs +++ b/src/Infrastructure/Plugins/HandoffPlugin.cs @@ -23,16 +23,6 @@ namespace fuseraft.Infrastructure.Plugins; /// The tool itself is a no-op: it returns verbatim so that /// the legacy tool-result scanning paths also detect it as a fallback. /// -/// -/// -/// The optional // -/// arguments let the handing-off agent synthesize a self-contained directive for the receiving -/// agent instead of relying on it to infer intent from the shared transcript. Orchestrators read -/// these directly off the FunctionCallContent and build an -/// for the next turn. When omitted, the -/// receiving agent falls back to whatever its -/// mode otherwise provides. -/// /// public sealed class HandoffPlugin { @@ -45,20 +35,8 @@ public sealed class HandoffPlugin /// The argument name the model must supply (route_keyword). public const string ArgumentName = "route_keyword"; - /// The optional structured-directive argument names, for orchestrators reading raw FunctionCallContent. - public const string GoalArgumentName = "goal"; - public const string BackgroundArgumentName = "background"; - public const string ConstraintsArgumentName = "constraints"; - [Description("Signal completion and hand off to the next workflow step. Must be the last tool call.")] public string Handoff( - [Description("Exact routing keyword for the intended handoff.")] - string route_keyword, - [Description("What the receiving agent must accomplish this turn. Recommended: always set this — it becomes the receiving agent's task when it runs in isolated (Fresh) mode and cannot see this conversation.")] - string? goal = null, - [Description("What you already learned, tried, or ruled out that the receiving agent needs to know. Do not assume it can see your reasoning.")] - string? background = null, - [Description("Explicit constraints the receiving agent must respect, one per line.")] - string? constraints = null) + [Description("Exact routing keyword for the intended handoff.")] string route_keyword) => route_keyword; } diff --git a/src/Infrastructure/Plugins/SandboxEnforcementFilter.cs b/src/Infrastructure/Plugins/SandboxEnforcementFilter.cs index aa605650..2106b272 100644 --- a/src/Infrastructure/Plugins/SandboxEnforcementFilter.cs +++ b/src/Infrastructure/Plugins/SandboxEnforcementFilter.cs @@ -59,7 +59,6 @@ public sealed class SandboxEnforcementFilter private readonly Matcher? _fsDenyMatcher; private readonly Matcher? _fsReadMatcher; private readonly Matcher? _fsWriteMatcher; - private readonly IReadOnlyList _fsWritePatterns = []; // Prefixes of OS directories that contain executables and shared libraries. private static readonly string[] SystemPrefixes = OperatingSystem.IsWindows() @@ -214,7 +213,6 @@ public SandboxEnforcementFilter( { _fsWriteMatcher = new Matcher(StringComparison.OrdinalIgnoreCase); foreach (var p in write) _fsWriteMatcher.AddInclude(p); - _fsWritePatterns = write; } } @@ -332,15 +330,8 @@ public AIAgent WrapAgent(AIAgent agent) => bool applyWriteGlob = isWriteOp || (_fsWriteMatcher is not null && isMixedOp && isDestArg); if (applyWriteGlob) { - // create_directory targets a directory, never a file, so it will never - // literally match a file-shaped glob like "workspace/**" — only descendants - // of "workspace" do. Allow it here when the requested directory is an - // ancestor of (or equal to) an allowed write path, since creating it is a - // prerequisite for writes the glob already permits. - bool allowAncestor = string.Equals(functionName, "create_directory", StringComparison.OrdinalIgnoreCase); var writeDenial = CheckGlob(raw, _fsWriteMatcher!, matchMeansDeny: false, - "Path is outside the configured FileSystem write permissions.", - allowAncestorOfWriteScope: allowAncestor); + "Path is outside the configured FileSystem write permissions."); if (writeDenial is not null) return writeDenial; } @@ -536,10 +527,7 @@ private static bool TryGetArgString(object? val, out string str) // Evaluates a glob matcher against a resolved relative path. // When matchMeansDeny=true (deny list): returns a denial when the path matches. // When matchMeansDeny=false (allow list): returns a denial when the path does NOT match. - // allowAncestorOfWriteScope additionally passes a path that doesn't match the glob itself - // but is an ancestor directory of an allowed write pattern (see IsAncestorOfWriteScope). - private string? CheckGlob(string rawPath, Matcher matcher, bool matchMeansDeny, string reason, - bool allowAncestorOfWriteScope = false) + private string? CheckGlob(string rawPath, Matcher matcher, bool matchMeansDeny, string reason) { string resolved; try @@ -554,51 +542,11 @@ private static bool TryGetArgString(object? val, out string str) var relative = Path.GetRelativePath(_sandboxRoot, resolved).Replace('\\', '/'); bool matches = matcher.Match(relative).HasMatches; - if (!matches && allowAncestorOfWriteScope && IsAncestorOfWriteScope(relative, _fsWritePatterns)) - matches = true; - return (matchMeansDeny ? matches : !matches) ? PluginResult.Denied($"[DENIED] '{relative}': {reason}") : null; } - // True when `relative` is an ancestor directory of (or exactly equal to) the fixed, - // non-wildcard prefix of at least one write pattern — e.g. "workspace" is an ancestor of - // "workspace/**", and "src/gen" is an ancestor of "src/gen/*.g.cs". Lets create_directory - // succeed for directories that only exist to hold files the write glob already allows. - private static bool IsAncestorOfWriteScope(string relative, IReadOnlyList writePatterns) - { - var candidate = relative.Split('/', StringSplitOptions.RemoveEmptyEntries); - if (candidate.Length == 0) return false; - - foreach (var pattern in writePatterns) - { - var segments = pattern.Split('/', StringSplitOptions.RemoveEmptyEntries); - - int fixedCount = 0; - while (fixedCount < segments.Length && segments[fixedCount].IndexOfAny(['*', '?']) < 0) - fixedCount++; - - // A fully-literal pattern (no wildcard segment) names a file, not a directory — - // only its parent segments are directories a create_directory call could target. - int ancestorDepth = fixedCount == segments.Length ? fixedCount - 1 : fixedCount; - if (candidate.Length > ancestorDepth) continue; - - bool isPrefix = true; - for (int i = 0; i < candidate.Length; i++) - { - if (!string.Equals(candidate[i], segments[i], StringComparison.OrdinalIgnoreCase)) - { - isPrefix = false; - break; - } - } - if (isPrefix) return true; - } - - return false; - } - private string? CheckEnvelope(string rawPath) { string resolved; diff --git a/src/Infrastructure/Storage/UserConfigStore.cs b/src/Infrastructure/Storage/UserConfigStore.cs index be698825..8770a4c0 100644 --- a/src/Infrastructure/Storage/UserConfigStore.cs +++ b/src/Infrastructure/Storage/UserConfigStore.cs @@ -34,11 +34,10 @@ public static (UserConfig? Config, string? LegacyKey) Load() var config = new UserConfig { - ModelId = onDisk.ModelId ?? string.Empty, - Endpoint = onDisk.Endpoint ?? string.Empty, - Provider = onDisk.Provider ?? string.Empty, - ApiKeyEnvVar = onDisk.ApiKeyEnvVar ?? string.Empty, - ReplContextBudget = onDisk.ReplContextBudget, + ModelId = onDisk.ModelId ?? string.Empty, + Endpoint = onDisk.Endpoint ?? string.Empty, + Provider = onDisk.Provider ?? string.Empty, + ApiKeyEnvVar = onDisk.ApiKeyEnvVar ?? string.Empty, }; return (config, onDisk.ApiKey ?? legacyKeyFile); } @@ -68,11 +67,10 @@ public static void Save(UserConfig config) Directory.CreateDirectory(ConfigDir); var onDisk = new OnDiskConfig { - ModelId = config.ModelId, - Endpoint = config.Endpoint, - Provider = config.Provider, - ApiKeyEnvVar = config.ApiKeyEnvVar, - ReplContextBudget = config.ReplContextBudget, + ModelId = config.ModelId, + Endpoint = config.Endpoint, + Provider = config.Provider, + ApiKeyEnvVar = config.ApiKeyEnvVar, }; File.WriteAllText(ConfigPath, JsonSerializer.Serialize(onDisk, JsonOptions)); } @@ -93,9 +91,6 @@ private sealed class OnDiskConfig [JsonPropertyName("apiKeyEnvVar")] public string? ApiKeyEnvVar { get; set; } - [JsonPropertyName("replContextBudget")] - public int? ReplContextBudget { get; set; } - // Present only in configs created before keychain support was added. [JsonPropertyName("apiKey")] public string? ApiKey { get; set; } diff --git a/src/Orchestration/AgentOrchestrator.cs b/src/Orchestration/AgentOrchestrator.cs index be75b8c5..ef1e6af3 100644 --- a/src/Orchestration/AgentOrchestrator.cs +++ b/src/Orchestration/AgentOrchestrator.cs @@ -924,29 +924,11 @@ await EmitContextAssemblyAsync(eventEmitter, assembled.Metrics, turn, if (memoryManager is not null) instructions = await memoryManager.AugmentInstructionsAsync(agentName, instructions, cancellationToken); - var isolation = agentCfg?.Isolation ?? AgentIsolation.Fresh; - var directive = isolation is AgentIsolation.Fresh or AgentIsolation.Fork - ? OrchestratorHelpers.FindLastDirective((IReadOnlyList)history) - : null; - IReadOnlyList filtered; - if (isolation == AgentIsolation.Fresh && contextAssembler is not null) + if (agentCfg?.Context is { Count: > 0 } agentContextSources && contextAssembler is not null) { filtered = (await contextAssembler.AssembleForAgentAsync( - agentName, task, (IReadOnlyList?)agentCfg?.Context ?? [], - history, directive, cancellationToken)).Messages; - } - else if (isolation == AgentIsolation.Fresh) - { - // No assembler configured — degrade to the directive/task alone rather than - // falling back to the shared transcript. - filtered = [new ChatMessage(ChatRole.User, directive?.Format() ?? task)]; - } - else if (agentCfg?.Context is { Count: > 0 } agentContextSources && contextAssembler is not null) - { - filtered = (await contextAssembler.AssembleForAgentAsync( - agentName, task, agentContextSources, history, - isolation == AgentIsolation.Fork ? directive : null, cancellationToken)).Messages; + agentName, task, agentContextSources, history, cancellationToken)).Messages; } else { @@ -965,11 +947,6 @@ await EmitContextAssemblyAsync(eventEmitter, assembled.Metrics, turn, else filtered = raw; } else filtered = raw; - - // Fork: layer the synthesized directive on top of the full shared transcript, - // matching ContextAssemblyPipeline.AssembleAsync's equivalent branch. - if (isolation == AgentIsolation.Fork && directive is not null) - filtered = [.. filtered, new ChatMessage(ChatRole.User, directive.Format())]; } context = (hasInstructions || memoryManager is not null) && instructions is not null diff --git a/src/Orchestration/Context/ContextAssembler.cs b/src/Orchestration/Context/ContextAssembler.cs index 61e95fdc..3b346bab 100644 --- a/src/Orchestration/Context/ContextAssembler.cs +++ b/src/Orchestration/Context/ContextAssembler.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.AI; using fuseraft.Core; using fuseraft.Core.Models; -using fuseraft.Core.Models.Agents; using fuseraft.Core.Models.Context; using fuseraft.Infrastructure; @@ -148,17 +147,13 @@ public async Task AssembleForAgentAsync( string task, IReadOnlyList sources, IList sharedHistory, - AgentDirective? directive = null, CancellationToken ct = default) { var result = new List(); var emptySources = new List(); - // 1. Task message — the agent always needs to know what it's working on. When a - // synthesized directive is available (handoff() goal/background/constraints), it - // replaces the raw task string — this is what makes Fresh isolation self-contained - // rather than just "an empty Context: block with no explanation of what to do". - result.Add(new ChatMessage(ChatRole.User, directive?.Format() ?? task)); + // 1. Task message — the agent always needs to know what it's working on. + result.Add(new ChatMessage(ChatRole.User, task)); // Separate own_history sources from artifact sources. ContextSource? ownHistorySrc = null; @@ -575,12 +570,8 @@ private static IReadOnlyList ExtractOwnHistory( // ── Pending-correction extraction ─────────────────────────────────────── // Returns all correction messages in shared history that appear after the last - // assistant turn by agentName AND are addressed to agentName specifically — i.e. the - // nearest preceding assistant turn belongs to agentName. Corrections are injected - // immediately after the turn that triggered them (a blocked handoff, a validation - // failure) with no explicit "addressed to" field, so once other agents have taken turns - // since agentName last spoke (e.g. a graph loop revisits agentName later), a correction - // meant for one of those other agents must not be attributed to agentName here. + // assistant turn by agentName. These are unread corrections the agent has not yet + // acted on; they must be included in the assembled context so the agent sees them. private static IReadOnlyList ExtractPendingCorrections( string agentName, IList history) @@ -597,19 +588,9 @@ private static IReadOnlyList ExtractPendingCorrections( } var corrections = new List(); - // Corrections immediately following agentName's own last turn (before any other - // agent's turn intervenes) are addressed to agentName by construction. - string? precedingAuthor = lastOwnIdx >= 0 ? agentName : null; for (int i = lastOwnIdx + 1; i < history.Count; i++) { - if (history[i].Role == ChatRole.Assistant) - { - precedingAuthor = history[i].AuthorName; - continue; - } - - if (ContextWindowFilter.IsCorrectionMessage(history[i]) && - string.Equals(precedingAuthor, agentName, StringComparison.OrdinalIgnoreCase)) + if (ContextWindowFilter.IsCorrectionMessage(history[i])) corrections.Add(history[i]); } return corrections; diff --git a/src/Orchestration/Context/ContextAssemblyPipeline.cs b/src/Orchestration/Context/ContextAssemblyPipeline.cs index c2fb5bea..373767bd 100644 --- a/src/Orchestration/Context/ContextAssemblyPipeline.cs +++ b/src/Orchestration/Context/ContextAssemblyPipeline.cs @@ -5,7 +5,6 @@ using fuseraft.Core; using fuseraft.Core.Interfaces; using fuseraft.Core.Models; -using fuseraft.Core.Models.Agents; using fuseraft.Infrastructure; namespace fuseraft.Orchestration.Context; @@ -126,50 +125,16 @@ public async Task AssembleAsync( IReadOnlyList declaredSources = []; IReadOnlyList emptySources = []; - var isolation = agentCfg?.Isolation ?? AgentIsolation.Fresh; - var directive = request.Directive - ?? (isolation is AgentIsolation.Fresh or AgentIsolation.Fork - ? OrchestratorHelpers.FindLastDirective(history) - : null); - - if (isolation == AgentIsolation.Fresh) - { - // Fresh: never touch SharedHistory. Build from the synthesized directive (if any) - // plus whatever Context: sources this agent declares — even when that list is empty, - // this is NOT the SharedHistoryFallback path. - var contextSources = (IReadOnlyList?)agentCfg?.Context ?? []; - if (_contextAssembler is not null) - { - var assembled = await _contextAssembler.AssembleForAgentAsync( - agentName, task, contextSources, - history as IList ?? new List(history), directive, ct); - baseMessages = assembled.Messages; - historyChars = baseMessages.Sum(m => m.Text?.Length ?? 0); - historyMessages = baseMessages; - declaredSources = contextSources.Select(s => s.Source).ToList(); - emptySources = assembled.EmptySources; - } - else - { - // No assembler configured — degrade to the directive/task alone rather than - // falling back to the shared transcript, preserving the Fresh isolation invariant. - baseMessages = [new ChatMessage(ChatRole.User, directive?.Format() ?? task)]; - historyChars = baseMessages.Sum(m => m.Text?.Length ?? 0); - historyMessages = baseMessages; - } - contextStrategy = ContextAssemblyMetrics.Strategies.ArtifactSpec; - } - else if (agentCfg?.Context is { Count: > 0 } contextSources2 && _contextAssembler is not null) + if (agentCfg?.Context is { Count: > 0 } contextSources && _contextAssembler is not null) { var assembled = await _contextAssembler.AssembleForAgentAsync( - agentName, task, contextSources2, - history as IList ?? new List(history), - isolation == AgentIsolation.Fork ? directive : null, ct); + agentName, task, contextSources, + history as IList ?? new List(history), ct); baseMessages = assembled.Messages; historyChars = baseMessages.Sum(m => m.Text?.Length ?? 0); historyMessages = baseMessages; contextStrategy = ContextAssemblyMetrics.Strategies.ArtifactSpec; - declaredSources = contextSources2.Select(s => s.Source).ToList(); + declaredSources = contextSources.Select(s => s.Source).ToList(); emptySources = assembled.EmptySources; } else @@ -185,10 +150,6 @@ public async Task AssembleAsync( sessionContextChars = sessionCtx.Length; baseMessages = BuildDefaultMessages(filtered, sessionCtx); - - // Fork: layer the synthesized directive on top of the full shared transcript. - if (isolation == AgentIsolation.Fork && directive is not null) - baseMessages = [.. baseMessages, new ChatMessage(ChatRole.User, directive.Format())]; } // ── History breakdown (role + compaction) ──────────────────────────── diff --git a/src/Orchestration/Graph/SubGraphExecutor.cs b/src/Orchestration/Graph/SubGraphExecutor.cs index 3e2b4b9f..2f91643d 100644 --- a/src/Orchestration/Graph/SubGraphExecutor.cs +++ b/src/Orchestration/Graph/SubGraphExecutor.cs @@ -66,8 +66,7 @@ await eventEmitter.EmitAsync(EventTypes.AgentStart, ?? (ILogger)Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; subOrchestrator = new MapReduceOrchestrator( subConfig, services.AgentFactory, mrLogger, - services.ChangeTracker, eventEmitter, services.GovernanceKernel, - services.HumanApprovalService, services.ContextPipeline, services.RepositoryKnowledgeStore); + services.ChangeTracker, eventEmitter, services.GovernanceKernel); } else if (subSpec.IsScatterGather) { @@ -84,8 +83,7 @@ await eventEmitter.EmitAsync(EventTypes.AgentStart, ?? (ILogger)Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; subOrchestrator = new ScatterGatherOrchestrator( subConfig, services.AgentFactory, sgLogger, - services.ChangeTracker, eventEmitter, services.GovernanceKernel, - services.HumanApprovalService, services.ContextPipeline, services.RepositoryKnowledgeStore); + services.ChangeTracker, eventEmitter, services.GovernanceKernel); } else { diff --git a/src/Orchestration/OrchestratorHelpers.cs b/src/Orchestration/OrchestratorHelpers.cs index f63680c5..d45c407b 100644 --- a/src/Orchestration/OrchestratorHelpers.cs +++ b/src/Orchestration/OrchestratorHelpers.cs @@ -3,9 +3,7 @@ using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; using fuseraft.Core.Models; -using fuseraft.Core.Models.Agents; using fuseraft.Infrastructure; -using fuseraft.Infrastructure.Plugins; namespace fuseraft.Orchestration; @@ -100,64 +98,6 @@ internal static class OrchestratorHelpers return val?.ToString(); } - // Same lookup as GetArg, but against FunctionCallContent.Arguments' actual declared type - // (IDictionary) — avoids an unchecked cast to IReadOnlyDictionary that - // would throw InvalidCastException if a future Arguments implementation didn't also - // implement IReadOnlyDictionary. Kept separate from GetArg (rather than overloaded) because - // FunctionInvocationContext.Arguments is the concrete AIFunctionArguments type, which - // implements both interfaces — an overload on IDictionary would make its call sites - // ambiguous. - private static string? GetHandoffArg(IDictionary? args, string key) - { - if (args is null || !args.TryGetValue(key, out var val)) return null; - return val?.ToString(); - } - - // Builds an AgentDirective from a handoff() FunctionCallContent's optional structured - // arguments (goal/background/constraints). Returns null when the call omitted `goal` — - // callers fall back to legacy marker-message behavior in that case. - internal static AgentDirective? TryExtractDirective(FunctionCallContent fc) - { - var args = fc.Arguments; - var goal = GetHandoffArg(args, HandoffPlugin.GoalArgumentName); - if (string.IsNullOrWhiteSpace(goal)) return null; - - var background = GetHandoffArg(args, HandoffPlugin.BackgroundArgumentName); - var constraints = GetHandoffArg(args, HandoffPlugin.ConstraintsArgumentName); - - return new AgentDirective - { - Goal = goal.Trim(), - Background = string.IsNullOrWhiteSpace(background) ? null : background.Trim(), - Constraints = string.IsNullOrWhiteSpace(constraints) - ? [] - : constraints.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), - }; - } - - // Scans the tail of history for the most recent handoff() call and extracts its directive, - // if any. Used where a directive must be recovered after the fact (e.g. at context-assembly - // time) rather than at the moment the FunctionCallContent is first observed. - internal static AgentDirective? FindLastDirective(IReadOnlyList history, int lookback = AgentMessageLookback) - { - for (int i = history.Count - 1, scanned = 0; i >= 0 && scanned < lookback; i--) - { - foreach (var item in history[i].Contents) - { - if (item is FunctionCallContent fc && - string.Equals(fc.Name, HandoffPlugin.FunctionName, StringComparison.OrdinalIgnoreCase)) - { - // Stop at the most recent handoff() call regardless of whether it carried a - // directive (i.e. declared `goal`). An older handoff's goal/background was - // addressed to a *different* recipient and must not be resurrected here. - return TryExtractDirective(fc); - } - } - if (history[i].Role == ChatRole.Assistant) scanned++; - } - return null; - } - // Counts how many consecutive assistant turns from agentName appear at the tail of // history, stopping at any user message or a different agent's turn. internal static int CountConsecutiveAgentTurns(IList history, string agentName) diff --git a/src/Orchestration/Strategies/StrategyFactory.cs b/src/Orchestration/Strategies/StrategyFactory.cs index caaae2e5..f2ab7261 100644 --- a/src/Orchestration/Strategies/StrategyFactory.cs +++ b/src/Orchestration/Strategies/StrategyFactory.cs @@ -258,8 +258,7 @@ private static Dictionary BuildValidators( provenanceRegistry: provenanceRegistry), // Threshold defaults to 3; command pattern supplied per-route via RequiredCommandPattern. [ValidatorNames.BlockOnConsecutiveFail] = new ConsecutiveShellFailValidator( - changeLogPath: config?.ChangeLogPath), - [ValidatorNames.RequireSessionContextWrite] = new RequireSessionContextWriteValidator(), + changeLogPath: config?.ChangeLogPath) }; if (config is not null) diff --git a/src/Orchestration/Validation/RequireSessionContextWriteValidator.cs b/src/Orchestration/Validation/RequireSessionContextWriteValidator.cs deleted file mode 100644 index b1adbbf3..00000000 --- a/src/Orchestration/Validation/RequireSessionContextWriteValidator.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Microsoft.Extensions.AI; -using fuseraft.Core.Interfaces; - -namespace fuseraft.Orchestration.Validation; - -/// -/// Blocks a handoff route unless the source agent called session_context_write -/// () during the -/// current turn. -/// -/// -/// Not auto-attached — opt in explicitly with Validators: [RequireSessionContextWrite] -/// on a route/edge/transition whose source agent has -/// isolation. A Fresh -/// agent's own turn — tool calls, intermediate reasoning — never reaches the next agent; -/// only its session_context_write summary and the synthesized -/// do. Without this validator, an -/// agent that forgets to write a summary silently hands the next agent nothing; attaching it -/// turns that into a hard, visible failure at handoff time instead of a discovered-later -/// context gap. See skills/craft-orchestration/references/schema-cheatsheet.md's "Built-in -/// validators" table. -/// -/// -public sealed class RequireSessionContextWriteValidator : IRoutingValidator -{ - public Task ValidateAsync( - IList history, - CancellationToken cancellationToken = default) - { - for (int i = history.Count - 1; i >= 0; i--) - { - var msg = history[i]; - - // User messages mark the turn boundary — stop here. - if (msg.Role == ChatRole.User) break; - - if (msg.Role != ChatRole.Tool) continue; - - foreach (var item in msg.Contents) - { - if (item is not FunctionResultContent frc) continue; - var funcName = HistoryHelpers.FindFunctionName(history, frc.CallId, i) ?? string.Empty; - if (funcName.Equals("session_context_write", StringComparison.OrdinalIgnoreCase)) - return Task.FromResult(RoutingValidationResult.Pass()); - } - } - - return Task.FromResult(RoutingValidationResult.Fail( - "Handoff blocked: this agent runs in isolated (Fresh) context — the next agent will " + - "not see this conversation, only what you write to session_context_write.\n\n" + - " 1. Call session_context_write(summary: \"...\") — what you accomplished, files " + - "changed, and anything the next agent needs to know.\n" + - " 2. Emit the handoff keyword in the same response.")); - } -} diff --git a/src/Orchestration/Validation/ValidatorRegistry.cs b/src/Orchestration/Validation/ValidatorRegistry.cs index 291c5dad..40b14f4d 100644 --- a/src/Orchestration/Validation/ValidatorRegistry.cs +++ b/src/Orchestration/Validation/ValidatorRegistry.cs @@ -69,8 +69,6 @@ public static IReadOnlyList BuildValidatorsFromNames( sandboxRoot); else if (name.Equals(ValidatorNames.ArchitectureValidator, StringComparison.OrdinalIgnoreCase)) v = new ArchitectureValidator(projectRoot: sandboxRoot); - else if (name.Equals(ValidatorNames.RequireSessionContextWrite, StringComparison.OrdinalIgnoreCase)) - v = new RequireSessionContextWriteValidator(); if (v is not null) result.Add(v); diff --git a/src/Orchestration/ValidatorNames.cs b/src/Orchestration/ValidatorNames.cs index d84018b6..315ec936 100644 --- a/src/Orchestration/ValidatorNames.cs +++ b/src/Orchestration/ValidatorNames.cs @@ -17,7 +17,6 @@ public static class ValidatorNames public const string BlockOnConsecutiveFail = "BlockOnConsecutiveFail"; public const string TestReportValid = "TestReportValid"; public const string ArchitectureValidator = "ArchitectureValidator"; - public const string RequireSessionContextWrite = "RequireSessionContextWrite"; // Synthetic validator names emitted into ValidatorStuckException / event logs public const string StructuredRouting = "StructuredRouting"; diff --git a/src/fuseraft.csproj b/src/fuseraft.csproj index ca358187..51578c61 100644 --- a/src/fuseraft.csproj +++ b/src/fuseraft.csproj @@ -27,7 +27,7 @@ - + diff --git a/tests/FuseraftCli.Tests/ContextAssemblyPipelineIsolationTests.cs b/tests/FuseraftCli.Tests/ContextAssemblyPipelineIsolationTests.cs deleted file mode 100644 index f6ab5a1b..00000000 --- a/tests/FuseraftCli.Tests/ContextAssemblyPipelineIsolationTests.cs +++ /dev/null @@ -1,145 +0,0 @@ -using Microsoft.Extensions.AI; -using fuseraft.Core.Models.Agents; -using fuseraft.Orchestration.Context; - -namespace FuseraftCli.Tests; - -/// -/// Tests for the Isolation-aware branch of — -/// the core behavioral flip of the agent-isolation protocol overhaul: -/// (the default) must never surface SharedHistory content, while -/// preserves the pre-overhaul windowed-transcript fallback and layers -/// a synthesized directive on top of the full transcript. -/// -public sealed class ContextAssemblyPipelineIsolationTests -{ - private static List SharedHistoryWithSecret() => - [ - new ChatMessage(ChatRole.User, "Investigate the outage.") { AuthorName = "Investigator" }, - new ChatMessage(ChatRole.Assistant, "SECRET_REASONING: tried X, ruled it out, tried Y.") - { AuthorName = "Investigator" }, - ]; - - [Fact] - public async Task Fresh_agent_never_sees_shared_history_content() - { - var pipeline = new ContextAssemblyPipeline(); - var request = new AgentExecutionRequest - { - AgentName = "Fixer", - Task = "Fix the outage.", - SharedHistory = SharedHistoryWithSecret(), - AgentConfig = new AgentConfig { Name = "Fixer", Isolation = AgentIsolation.Fresh }, - }; - - var assembled = await pipeline.AssembleAsync(request); - - Assert.DoesNotContain(assembled.Messages, m => - m.Text?.Contains("SECRET_REASONING", StringComparison.Ordinal) == true); - Assert.Equal( - fuseraft.Core.Models.Context.ContextAssemblyMetrics.Strategies.ArtifactSpec, - assembled.Metrics.ContextStrategy); - } - - [Fact] - public async Task Fresh_agent_uses_directive_as_task_message_when_supplied() - { - var pipeline = new ContextAssemblyPipeline(); - var directive = new AgentDirective - { - Goal = "Patch the null check in Parser.cs.", - Background = "Root cause confirmed: line 42 dereferences before the null guard.", - Constraints = ["Do not change the public API."], - }; - var request = new AgentExecutionRequest - { - AgentName = "Fixer", - Task = "(original session task — should not appear verbatim)", - SharedHistory = SharedHistoryWithSecret(), - Directive = directive, - AgentConfig = new AgentConfig { Name = "Fixer", Isolation = AgentIsolation.Fresh }, - }; - - var assembled = await pipeline.AssembleAsync(request); - - Assert.Contains(assembled.Messages, m => - m.Text?.Contains("Patch the null check in Parser.cs.", StringComparison.Ordinal) == true); - Assert.Contains(assembled.Messages, m => - m.Text?.Contains("Do not change the public API.", StringComparison.Ordinal) == true); - } - - [Fact] - public async Task Fresh_agent_recovers_directive_from_last_handoff_call_when_not_supplied_directly() - { - var pipeline = new ContextAssemblyPipeline(); - var history = new List - { - new ChatMessage(ChatRole.User, "Investigate the outage.") { AuthorName = "Investigator" }, - new ChatMessage(ChatRole.Assistant, - [ - new FunctionCallContent("call-1", "handoff", new Dictionary - { - ["route_keyword"] = "HANDOFF TO FIXER", - ["goal"] = "Patch the parser null check.", - ["background"] = "Root cause already confirmed.", - }), - ]) - { AuthorName = "Investigator" }, - }; - var request = new AgentExecutionRequest - { - AgentName = "Fixer", - Task = "(original session task)", - SharedHistory = history, - AgentConfig = new AgentConfig { Name = "Fixer", Isolation = AgentIsolation.Fresh }, - }; - - var assembled = await pipeline.AssembleAsync(request); - - Assert.Contains(assembled.Messages, m => - m.Text?.Contains("Patch the parser null check.", StringComparison.Ordinal) == true); - } - - [Fact] - public async Task Shared_agent_with_no_context_block_keeps_legacy_history_fallback() - { - var pipeline = new ContextAssemblyPipeline(); - var request = new AgentExecutionRequest - { - AgentName = "Investigator", - Task = "Investigate the outage.", - SharedHistory = SharedHistoryWithSecret(), - AgentConfig = new AgentConfig { Name = "Investigator", Isolation = AgentIsolation.Shared }, - }; - - var assembled = await pipeline.AssembleAsync(request); - - Assert.Contains(assembled.Messages, m => - m.Text?.Contains("SECRET_REASONING", StringComparison.Ordinal) == true); - Assert.Equal( - fuseraft.Core.Models.Context.ContextAssemblyMetrics.Strategies.SharedHistoryFallback, - assembled.Metrics.ContextStrategy); - } - - [Fact] - public async Task Fork_agent_sees_full_history_plus_directive() - { - var pipeline = new ContextAssemblyPipeline(); - var directive = new AgentDirective { Goal = "Audit the session for inconsistencies." }; - var request = new AgentExecutionRequest - { - AgentName = "Verifier", - Task = "Investigate the outage.", - SharedHistory = SharedHistoryWithSecret(), - Directive = directive, - AgentConfig = new AgentConfig { Name = "Verifier", Isolation = AgentIsolation.Fork }, - }; - - var assembled = await pipeline.AssembleAsync(request); - - Assert.Contains(assembled.Messages, m => - m.Text?.Contains("SECRET_REASONING", StringComparison.Ordinal) == true); - Assert.Contains(assembled.Messages, m => - m.Text?.Contains("Audit the session for inconsistencies.", StringComparison.Ordinal) == true); - } -} diff --git a/tests/FuseraftCli.Tests/OrchestratorConfigLoaderIsolationTests.cs b/tests/FuseraftCli.Tests/OrchestratorConfigLoaderIsolationTests.cs deleted file mode 100644 index d43db242..00000000 --- a/tests/FuseraftCli.Tests/OrchestratorConfigLoaderIsolationTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using fuseraft.Cli; -using fuseraft.Core.Models.Agents; -using fuseraft.Core.Models.Orchestration; -using fuseraft.Orchestration; - -namespace FuseraftCli.Tests; - -/// -/// Tests for — the guard -/// that keeps Isolation: Fresh (the new default) from silently starving Magentic's -/// manager/ledger loop, which structurally depends on every participant sharing the transcript. -/// -public sealed class OrchestratorConfigLoaderIsolationTests -{ - private static AgentConfig Agent(string name, AgentIsolation isolation) => - new() { Name = name, Isolation = isolation }; - - [Fact] - public void Magentic_config_with_a_fresh_agent_is_rejected() - { - var config = new OrchestrationConfig - { - Selection = new SelectionStrategyConfig { Type = OrchestratorTypes.Magentic }, - Agents = - [ - Agent("Manager", AgentIsolation.Shared), - Agent("Worker", AgentIsolation.Fresh), - ], - }; - - var ex = Assert.Throws(() => - OrchestratorConfigLoader.ValidateIsolationConstraints(config, NullLoggerFactory.Instance)); - - Assert.Contains("magentic", ex.Message, StringComparison.OrdinalIgnoreCase); - Assert.Contains("Worker", ex.Message, StringComparison.Ordinal); - } - - [Fact] - public void Magentic_config_with_all_agents_shared_or_fork_is_accepted() - { - var config = new OrchestrationConfig - { - Selection = new SelectionStrategyConfig { Type = OrchestratorTypes.Magentic }, - Agents = - [ - Agent("Manager", AgentIsolation.Shared), - Agent("Worker", AgentIsolation.Fork), - ], - }; - - var exception = Record.Exception(() => - OrchestratorConfigLoader.ValidateIsolationConstraints(config, NullLoggerFactory.Instance)); - - Assert.Null(exception); - } - - [Fact] - public void Non_magentic_config_with_a_fresh_agent_is_accepted() - { - var config = new OrchestrationConfig - { - Selection = new SelectionStrategyConfig { Type = OrchestratorTypes.StateMachine }, - Agents = [Agent("Developer", AgentIsolation.Fresh)], - }; - - var exception = Record.Exception(() => - OrchestratorConfigLoader.ValidateIsolationConstraints(config, NullLoggerFactory.Instance)); - - Assert.Null(exception); - } -} diff --git a/tests/FuseraftCli.Tests/SandboxEnforcementFilterTests.cs b/tests/FuseraftCli.Tests/SandboxEnforcementFilterTests.cs index 86d75aa1..48d2e2f9 100644 --- a/tests/FuseraftCli.Tests/SandboxEnforcementFilterTests.cs +++ b/tests/FuseraftCli.Tests/SandboxEnforcementFilterTests.cs @@ -133,50 +133,4 @@ public void ShellRun_SedRegexContainingSlashes_IsNotFalselyDeniedAsAPath() Assert.Null(result); } - - // ── create_directory vs. a file-shaped write glob ────────────────────── - - [Fact] - public void CreateDirectory_BareAncestorOfWriteGlob_IsAllowed() - { - // "workspace/**" only matches files under workspace/, never the literal - // "workspace" segment itself — but creating that directory is a prerequisite - // for writes the glob already permits, so it must not be denied. - var result = MakeFilter().Inspect("create_directory", - new Dictionary { ["path"] = "workspace" }); - - Assert.Null(result); - } - - [Fact] - public void CreateDirectory_NestedAncestorOfWriteGlob_IsAllowed() - { - var result = MakeFilter().Inspect("create_directory", - new Dictionary { ["path"] = "workspace/src" }); - - Assert.Null(result); - } - - [Fact] - public void CreateDirectory_OutsideWriteGlob_IsStillDenied() - { - var result = MakeFilter().Inspect("create_directory", - new Dictionary { ["path"] = "other" }); - - Assert.NotNull(result); - Assert.Contains("DENIED", result); - } - - [Fact] - public void WriteFile_BareAncestorDirectory_IsNotAllowedByAncestorRule() - { - // The ancestor relaxation is scoped to create_directory only — write_file must - // still match the glob on its own merits, so a bare "workspace" (not a file - // under it) stays denied. - var result = MakeFilter().Inspect("write_file", - new Dictionary { ["path"] = "workspace" }); - - Assert.NotNull(result); - Assert.Contains("DENIED", result); - } } diff --git a/tests/FuseraftCli.Tests/ValidateConfigCommandTests.cs b/tests/FuseraftCli.Tests/ValidateConfigCommandTests.cs index b76690e8..217bef76 100644 --- a/tests/FuseraftCli.Tests/ValidateConfigCommandTests.cs +++ b/tests/FuseraftCli.Tests/ValidateConfigCommandTests.cs @@ -729,7 +729,7 @@ public async Task MagenticSelection_ValidConfig_Returns0() var config = """ { "Orchestration": { - "Agents": [{"Name": "Worker", "Instructions": "do work", "Model": {"ModelId": "gpt-4o"}, "Isolation": "Shared"}], + "Agents": [{"Name": "Worker", "Instructions": "do work", "Model": {"ModelId": "gpt-4o"}}], "Selection": { "Type": "magentic", "Magentic": { @@ -796,7 +796,7 @@ public async Task MagenticSelection_ModelAlias_Resolves() "ApiKeyEnvVar": "OPENAI_API_KEY" } }, - "Agents": [{"Name": "Worker", "Instructions": "ok", "Model": {"ModelId": "gpt-4o"}, "Isolation": "Shared"}], + "Agents": [{"Name": "Worker", "Instructions": "ok", "Model": {"ModelId": "gpt-4o"}}], "Selection": { "Type": "magentic", "Magentic": { @@ -824,7 +824,7 @@ public async Task MagenticSelection_TerminationConfigured_WarnsButPasses() var config = """ { "Orchestration": { - "Agents": [{"Name": "Worker", "Instructions": "do work", "Model": {"ModelId": "gpt-4o"}, "Isolation": "Shared"}], + "Agents": [{"Name": "Worker", "Instructions": "do work", "Model": {"ModelId": "gpt-4o"}}], "Selection": { "Type": "magentic", "Magentic": {"Model": {"ModelId": "gpt-4o"}}