ADR-0003: Adopt LangGraph Deep Agents with Extended SkillsMiddleware
Metadata
| Property | Value |
|---|---|
| Status | Proposed |
| Date | 2026-10-02 |
| Authors | Spectrayan Architecture Team (architecture@spectrayan.com) |
| Deciders | Carefold Core Maintainers, Clinical AI Reviewers |
| Consulted | Medical Informatics Lead, Security Engineering Team |
| Informed | Carefold Open-Source Community |
| Supersedes | Partially supersedes skill-provisioning aspects of ADR-0001 §Phase 3 |
| Related | ADR-0001 (Orchestrator Architecture), ADR-0002 (Hexagonal Memory Ports) |
Context & Problem Statement
Carefold's current skill-context pipeline suffers from prompt bloat and eager over-provisioning. When a specialist agent (e.g., cardiology-guide) is invoked, the system concatenates:
- The full safety preamble (~200 tokens).
- The complete agent persona (
persona.md, 500–1500 tokens). - Every declared skill's full
SKILL.mdinstruction body — e.g.,cardiology-prep,emergency-red-flags,clinical-safety-boundaries(~1500–3000 tokens combined). - Every orchestrator-provisioned reference document — pre-fetched by keyword-matching stem tokens (~2000–8000 tokens).
- Any dynamically generated skills and their reference material.
For a typical consultation, 8,000–15,000 tokens of skill and reference content enter the system prompt on turn 1, regardless of query complexity. A simple question like "What should I bring to my cardiology appointment?" pays the same context cost as a complex multimorbid symptom chronology.
Specific Architectural Deficiencies
-
No Progressive Disclosure: Carefold loads all skill tiers simultaneously during
OrchestratorNode._preflight_prepare_skills_and_docs()andAgentExecutionNode._resolve_system_prompt(). LangGraph Deep Agents uses 3-tier loading: metadata → instructions → resources. -
Keyword-Based Reference Over-Provisioning: The orchestrator uses naive stem-token matching (
stem_tokens in prompt_lower) to decide which reference documents to pre-load, causing false positives and false negatives. -
Redundant Tooling: The sandboxed
skill-docstool exists with fail-closed authorization, fuzzy matching, and path-traversal protection — but is rarely used because the orchestrator pre-stuffs all references into the prompt. -
No Middleware Architecture: Carefold has no equivalent to the Deep Agents layered middleware stack. All context assembly is hardcoded across
OrchestratorNode,AgentExecutionNode, andagent_execution_prompt.hbs. -
No Storage Abstraction: Skills are loaded exclusively from the local filesystem. No equivalent to
StateBackend,StoreBackend, orCompositeBackend.
LangGraph Deep Agents — Key Extensibility Points
After exhaustive review of the Deep Agents customization documentation, three critical extensibility mechanisms make it possible to use deepagents as a direct dependency without rewriting the SkillsMiddleware:
1. SkillsMiddleware Override via .name Matching (deepagents>=0.7)
Per the customization docs:
"Pass a middleware instance whose
.namematches an entry in the Deep Agents stack to replace that built-in instance in place instead of appending a duplicate."
This means we can pass a customized SkillsMiddleware instance with our own system_prompt template — including clinical safety routing hints, risk class annotations, and Carefold-specific framing — and it replaces the default in the stack at the same position (slot 1, before FilesystemMiddleware).
from deepagents.middleware.skills import SkillsMiddleware
carefold_skills_prompt = """## Carefold Skill Catalog
⚠ CLINICAL SAFETY: You are a clinical_assist agent. Never diagnose, prescribe, or replace emergency services.
Routing hints:
- For emergency symptoms → HALT and trigger red-flag protocol
- Read the SKILL.md file for any skill before providing guidance
{skills_locations}
{skills_list}
{skills_load_warnings}
"""
# Replaces default SkillsMiddleware in the Deep Agents stack
carefold_skills = SkillsMiddleware(
backend=backend,
sources=["/skills/"],
system_prompt=carefold_skills_prompt,
)
2. Suppress Auto-Generated Catalog with system_prompt=None
For maximum control, the middleware accepts system_prompt=None to load skills into state["skills_metadata"] without appending anything to the system prompt. This lets Carefold's own orchestrator build a fully custom skill catalog block:
SkillsMiddleware(
backend=backend,
sources=["/skills/"],
system_prompt=None, # Skills load but no auto-generated catalog
)
3. Custom Frontmatter via metadata Field
The Agent Skills specification defines a metadata field as:
"
metadata: No. Arbitrary key-value pairs for additional properties."
This is exactly where Carefold's risk_class, domain, category, tools, and forbidden fields can live — without conflicting with the spec's required fields (name, description). The metadata is parsed and available in state["skills_metadata"] for our custom middleware to read.
---
name: cardiology-prep
description: Guides patients through pre-visit preparation for cardiology consultations including symptom chronology, medication inventory, and question formulation.
license: Apache-2.0
metadata:
risk_class: clinical_assist
domain: clinical
category: cardiology
tools: skill-docs
forbidden: diagnose prescribe dose
version: "1.0.0"
author: spectrayan
---
Decision Drivers
- DD-1 — Context Efficiency: Reduce system prompt token consumption by 60–80% for typical single-skill consultations.
- DD-2 — Clinical Safety Preservation: All existing safety invariants must remain intact and unweakened.
- DD-3 — Backward Compatibility: Existing skills and agents must continue to function during migration.
- DD-4 — Agent Skills Specification Alignment: Align with the open Agent Skills specification.
- DD-5 — Leverage, Don't Reinvent: Use
deepagentsas a direct dependency. Extend built-in middleware rather than rewriting it. - DD-6 — Storage Abstraction: Support
FilesystemBackend,StoreBackend, andCompositeBackendfor future multi-tenant skill storage. - DD-7 — On-Demand Reference Retrieval: Replace keyword-based pre-provisioning with agent-driven
read_filethrough thedeepagentsfilesystem tools.
Considered Options
Option 1: Status Quo — Full Prompt Stuffing with Custom Loaders
- Description: Maintain current architecture.
- Pros: Zero migration effort; deterministic (all context always available).
- Cons: 8K–15K tokens per turn; keyword matching is brittle; can't scale past ~25 skills; no storage abstraction; violates progressive disclosure.
Option 2: Rewrite SkillsMiddleware from Scratch
- Description: Create a
CareSkillsMiddlewarethat reimplements skill discovery, parsing, caching, and system prompt injection while adding clinical safety fields. - Pros: Total control over every behavior.
- Cons: Duplicates ~800 lines of well-tested middleware code. Falls behind on upstream improvements (caching, reload, subagent isolation). Maintenance burden of tracking upstream API changes.
Option 3: Adopt deepagents — Override SkillsMiddleware, Extend via Custom Middleware and metadata (Selected)
- Description: Add
deepagentsas a direct dependency. Override the built-inSkillsMiddlewarewith a customizedsystem_prompttemplate that includes Carefold's clinical safety framing and routing hints. Moverisk_class,domain,tools, andforbiddeninto themetadataYAML frontmatter field (spec-compliant). Add a lightweightClinicalSafetyMiddlewareas custom middleware for Carefold-specific safety gating, risk class enforcement, and PII sanitization. UseCompositeBackendfor storage. - Pros:
- Zero custom skill-loading code —
SkillsMiddlewarehandles discovery, parsing, caching, and 3-tier progressive disclosure. - Custom
system_prompttemplate lets us inject clinical safety framing, routing hints, and risk class annotations without touching middleware internals. metadatafield is spec-compliant — Carefold fields sit inmetadata:YAML block and flow through tostate["skills_metadata"].- All upstream improvements (prompt caching, summarization, subagent isolation, skill reload) come for free.
- Custom middleware (
ClinicalSafetyMiddleware) handles Carefold-specific concerns in the standard middleware stack. CompositeBackendandFilesystemPermissionprovide storage abstraction and permissions out of the box.- Cons:
- Hard dependency on
deepagents>=0.7. - Must move
carefold.yamlfields into SKILL.mdmetadatafrontmatter (one-time migration). - Override replaces default SkillsMiddleware entirely — must fully configure
backendandsources.
Decision Outcome
Chosen Option: Option 3: Adopt deepagents — Override SkillsMiddleware, Extend via Custom Middleware and metadata
Justification
LangGraph Deep Agents provides three precise extensibility hooks that cover Carefold's needs without reimplementation:
-
SkillsMiddleware(system_prompt=custom_template)— override the catalog framing with clinical safety context. Template slots{skills_locations},{skills_list},{skills_load_warnings}auto-sync as skills change. -
metadata:frontmatter field — the Agent Skills spec explicitly reserves this for arbitrary key-value pairs. Carefold'srisk_class,domain,category,tools,forbiddenfit naturally here, remain parseable bySkillsMiddleware, and flow tostate["skills_metadata"]for downstream middleware to consume. -
Custom middleware via
middleware=— Carefold-specific safety, audit, and orchestration logic runs as standardAgentMiddlewarein the Deep Agents stack, positioned after the core middleware and operating on the skill metadata injected bySkillsMiddleware.
Architectural Diagram — Deep Agents Stack with Carefold Extensions
flowchart TD
subgraph "Deep Agents Built-in Stack"
Phase1["1. SkillsMiddleware<br/>(OVERRIDDEN: custom system_prompt<br/>with safety framing + routing hints)"]
Phase2["2. FilesystemMiddleware<br/>(with CompositeBackend + Permissions)"]
Phase3["3. SubAgentMiddleware<br/>(specialist subagents per clinical domain)"]
Phase4["4. SummarizationMiddleware"]
Phase5["5. PatchToolCallsMiddleware"]
end
subgraph "Carefold Custom Middleware (slot 7)"
Phase6["6. ClinicalSafetyMiddleware<br/>(red-flag gating, risk class enforcement,<br/>PII sanitization, intended-use validation)"]
Phase7["7. AuditMiddleware<br/>(compliance logging, triage telemetry)"]
Phase8["8. OrchestratorMiddleware<br/>(two-hop routing, execution plan)"]
end
subgraph "Deep Agents Tail Stack"
Phase9["9. Prompt Caching"]
M10["10. MemoryMiddleware"]
M11["11. HumanInTheLoopMiddleware"]
end
Phase1 --> Phase2 --> Phase3 --> Phase4 --> Phase5 --> Phase6 --> Phase7 --> Phase8 --> Phase9 --> M10 --> M11
Architectural Diagram — Skill Loading Flow
flowchart TD
subgraph "SKILL.md (Agent Skills Spec Compliant)"
F1["name: cardiology-prep"]
F2["description: Guides patients through..."]
F3["license: Apache-2.0"]
F4["metadata:<br/> risk_class: clinical_assist<br/> domain: clinical<br/> tools: skill-docs<br/> forbidden: diagnose prescribe"]
end
subgraph "SkillsMiddleware (Built-in)"
S1["Discovery: parse frontmatter<br/>(name + description → system prompt)"]
S2["Custom system_prompt template:<br/>safety framing + {skills_list}"]
S3["state.skills_metadata[]<br/>(includes metadata dict)"]
end
subgraph "ClinicalSafetyMiddleware (Custom)"
ItemOne["Read state.skills_metadata"]
ItemTwo["Extract risk_class from metadata"]
ItemThree["Enforce Phase 0 tool registry<br/>against metadata.tools"]
ItemFour["Validate intended-use declarations"]
ItemFive["Emergency red-flag gating"]
end
subgraph "Agent Execution"
A1["Agent sees skill catalog<br/>(name + description only)"]
A2["Agent calls read_file<br/>to get SKILL.md body"]
A3["Agent calls read_file<br/>for references/docs"]
end
F1 --> S1
F2 --> S1
F4 --> S3
S1 --> S2
S2 --> A1
S3 --> ItemOne
ItemOne --> ItemTwo
ItemTwo --> ItemThree
A1 -->|"On demand"| A2
A2 -->|"On demand"| A3
Detailed Design
1. Unified SKILL.md Format — Merge carefold.yaml into metadata
Currently Carefold uses two files per skill: SKILL.md (frontmatter + instructions) and carefold.yaml (Carefold-specific fields). The Agent Skills spec's metadata field consolidates both:
Before (two files):
skills/cardiology-prep/
├── SKILL.md # name, description, instructions
├── carefold.yaml # risk_class, domain, tools, forbidden
└── references/
# carefold.yaml
id: cardiology-prep
version: "1.0.0"
domain: clinical
category: cardiology
risk_class: clinical_assist
tools: [skill-docs]
forbidden: [diagnose, prescribe, dose]
After (single file, spec-compliant):
# SKILL.md frontmatter
---
name: cardiology-prep
description: Guides patients through pre-visit preparation for cardiology consultations including symptom chronology, medication inventory, vital sign tracking, and question formulation for their cardiologist.
license: Apache-2.0
compatibility: Requires local LLM or API key for model access
metadata:
risk_class: clinical_assist
domain: clinical
category: cardiology
version: "1.0.0"
author: spectrayan
tools:
- skill-docs
forbidden:
- diagnose
- prescribe
- dose
evals: null
allowed-tools: skill-docs
---
2. Custom SkillsMiddleware Override
Override the built-in SkillsMiddleware with a custom system_prompt template that includes Carefold clinical safety context. The template must include {skills_locations}, {skills_list}, and {skills_load_warnings} — the middleware auto-substitutes these on every turn.
from deepagents import create_deep_agent
from deepagents.backends.filesystem import FilesystemBackend
from deepagents.backends import CompositeBackend, StateBackend
from deepagents.middleware.skills import SkillsMiddleware
CAREFOLD_SKILLS_PROMPT = """\
## Available Skills
⚠ **CLINICAL SAFETY BOUNDARY**: You are a healthcare navigation assistant.
You do NOT provide medical diagnoses, prescribe treatments, or replace emergency services.
For life-threatening emergencies, immediately direct to 911 / 988.
### Skill Activation Protocol
1. Review the catalog below — each skill has a `name` and `description`.
2. When a skill matches the user's query, read its full instructions:
`read_file("skills/<skill-name>/SKILL.md")`
3. Follow the skill's instructions. If they reference documents, read them:
`read_file("skills/<skill-name>/references/<document>")`
4. Never provide guidance beyond what the activated skill's instructions authorize.
### Skill Sources
{skills_locations}
### Catalog
{skills_list}
{skills_load_warnings}
"""
skills_backend = FilesystemBackend(root_dir="/path/to/carefold/skills")
carefold_skills_middleware = SkillsMiddleware(
backend=skills_backend,
sources=["/skills/"],
system_prompt=CAREFOLD_SKILLS_PROMPT,
)
3. ClinicalSafetyMiddleware — Carefold-Specific Custom Middleware
This is a standard AgentMiddleware that runs in the custom middleware slot (position 7 in the stack). It reads state["skills_metadata"], extracts Carefold's metadata fields, and enforces safety invariants:
from typing import Any
from langchain.agents.middleware import AgentMiddleware
from deepagents.middleware import SkillsState
class ClinicalSafetyMiddleware(AgentMiddleware[SkillsState]):
"""Enforce Carefold clinical safety invariants on Deep Agents skill metadata."""
state_schema = SkillsState
def before_agent(self, state: SkillsState, runtime) -> dict[str, Any] | None:
skills_metadata = state.get("skills_metadata") or []
# Extract risk classes from metadata fields
max_risk = "admin"
for skill in skills_metadata:
meta = skill.get("metadata", {}) or {}
risk = meta.get("risk_class", "admin")
if risk == "clinical_assist":
max_risk = "clinical_assist"
# Validate tools against Phase 0 registry
tools = meta.get("tools", [])
for tool in tools:
if tool not in PHASE_0_TOOL_REGISTRY:
raise ToolValidationError(
f"Skill '{skill['name']}' declares unauthorized tool: {tool}"
)
# Inject elevated safety context for clinical_assist agents
if max_risk == "clinical_assist":
return {"carefold_risk_class": max_risk}
return None
4. Agent Construction with create_deep_agent
from deepagents import create_deep_agent, FilesystemPermission
from deepagents.backends.filesystem import FilesystemBackend
from deepagents.backends import CompositeBackend, StateBackend
from langgraph.checkpoint.memory import MemorySaver
# Composite backend: skills from disk, workspace in-state
backend = CompositeBackend(
default=StateBackend(),
routes={
"/skills/": FilesystemBackend(root_dir="/path/to/carefold/skills"),
"/workspace/": FilesystemBackend(root_dir="/path/to/carefold/workspace"),
},
)
# Permissions: skills are read-only, workspace is writable
permissions = [
FilesystemPermission(operations=["write"], paths=["/skills/**"], mode="deny"),
FilesystemPermission(operations=["read", "write"], paths=["/workspace/**"], mode="allow"),
]
agent = create_deep_agent(
model="google_genai:gemini-3.6-flash", # or ollama:..., anthropic:..., etc.
backend=backend,
skills=["/skills/"],
memory=["/workspace/AGENTS.md"],
permissions=permissions,
middleware=[
carefold_skills_middleware, # Replaces default SkillsMiddleware (slot 1)
ClinicalSafetyMiddleware(), # Custom (slot 7 — after PatchToolCalls)
AuditMiddleware(), # Custom (slot 7 — after ClinicalSafety)
],
tools=[ # Carefold domain tools
attach_read_tool,
workspace_note_tool,
sanitize_pii_tool,
extract_structured_data_tool,
validate_grounding_tool,
delegate_to_agent_tool,
list_agents_tool,
],
subagents=specialist_subagents, # 22 clinical/admin specialist subagents
interrupt_on={
"write_file": True, # HITL for file writes
"delegate_to_agent": False, # Auto-approve delegation
},
checkpointer=MemorySaver(),
)
5. Persona → Profile Mapping — Decomposing persona.md
Every Carefold persona.md follows a rigid 5-section anatomy:
┌─────────────────────────────────────────┐
│ ROLE & EMPATHY │ ← Agent identity, bedside manner, mission
│ │
│ CLINICAL SCOPE & FOCUS │ ← Domain boundaries, what the agent does
│ │
│ STRUCTURED INTERACTION PROTOCOL │ ← 4-step methodology for patient interactions
│ │
│ STRICT NON-CLINICAL BOUNDARIES │ ← Safety contract (never diagnose/prescribe)
│ │
│ EXPLICIT EMERGENCY RED FLAGS │ ← Red-flag patterns → 911 escalation
└─────────────────────────────────────────┘
Similarly, agent.yaml carries operational metadata:
id: cardiology-guide
title: Cardiology Navigator
model: llama3.2
risk_class: wellness
domain: clinical
category: clinical.cardiology
description: >-
Cardiovascular care navigator assisting patients with...
skills: [cardiology-prep]
tools: [attach-read, skill-docs, workspace-note]
forbidden: [diagnose, prescribe, dose, replace_emergency_care]
persona: persona.md
LangGraph Deep Agents' Profiles system maps cleanly to this structure via HarnessProfile + subagent system_prompt:
Mapping Table
persona.md Section |
Deep Agents Destination | Rationale |
|---|---|---|
| ROLE & EMPATHY | Subagent system_prompt |
This is the agent's identity — exactly what system_prompt is for |
| CLINICAL SCOPE & FOCUS | Subagent system_prompt (appended) |
Domain-specific context the agent always needs |
| STRUCTURED INTERACTION PROTOCOL | Moved to SKILL.md instructions |
This is task-specific methodology — belongs in a skill, not identity. Progressive disclosure means it loads only when the agent activates the skill |
| STRICT NON-CLINICAL BOUNDARIES | HarnessProfile.system_prompt_suffix |
Safety contract is model-level — applies to every agent/subagent using that model, via profile suffix |
| EXPLICIT EMERGENCY RED FLAGS | HarnessProfile.system_prompt_suffix |
Emergency protocol is also model-level — universal across all agents |
Why This Decomposition
-
ROLE & EMPATHY and CLINICAL SCOPE define who the agent is — this is identity, belonging in
system_prompt. It runs on every turn. -
STRUCTURED INTERACTION PROTOCOL defines how the agent works — this is procedural methodology. Currently stuffed into the persona (~400 tokens per agent), it belongs in the skill's
SKILL.mdinstructions. Under progressive disclosure, the agent reads it only when activated — saving ~400 tokens on every turn where the protocol isn't needed. -
NON-CLINICAL BOUNDARIES and EMERGENCY RED FLAGS are identical across all 22 specialist agents (with minor domain-specific wording). Currently duplicated in every
persona.md. As aHarnessProfile.system_prompt_suffix, they're declared once and applied everywhere via the profile merge system.
Concrete Implementation
Step 1: Register a Carefold HarnessProfile for each model
The safety suffix is registered per-model (or per-provider). Per the profiles docs, provider-level applies to all models from that provider. Model-level overrides inherit from provider-level.
from deepagents import HarnessProfile, register_harness_profile
# Shared safety suffix — applied to ALL Carefold agents regardless of model
CAREFOLD_SAFETY_SUFFIX = """\
STRICT NON-CLINICAL BOUNDARIES:
You are an educational and administrative navigation companion, NOT a licensed physician or emergency responder.
- You NEVER diagnose medical conditions or interpret clinical laboratory results.
- You NEVER prescribe medications, recommend treatments, or calculate drug dosages.
- You NEVER instruct patients to start, stop, adjust, or discontinue prescribed therapies.
- You must explicitly remind users to consult their licensed physician before modifying any medical treatment.
EXPLICIT EMERGENCY RED FLAGS:
If a user describes acute, potentially life-threatening symptoms — including crushing chest pain;
pain radiating to the arm, jaw, or back; sudden shortness of breath; signs of stroke (FAST criteria);
anaphylaxis; severe hemorrhaging; or active suicidal ideation — you must IMMEDIATELY instruct them to
call 911 (or local emergency services) or go to the nearest emergency department.
Do NOT continue routine navigation during emergency presentations.
"""
# Register for each model Carefold uses
for model_key in ["ollama:llama3.2", "google_genai:gemini-3.6-flash", "anthropic:claude-sonnet-4-6"]:
register_harness_profile(
model_key,
HarnessProfile(system_prompt_suffix=CAREFOLD_SAFETY_SUFFIX),
)
Per the merge semantics, system_prompt_suffix from the profile is appended after the caller's system_prompt and any base_system_prompt. This means every agent and subagent using these models automatically gets the safety contract — no duplication across 22 persona files.
Step 2: Build subagent system_prompt from persona.md sections 1–2
def build_subagent_prompt(agent_dir: Path) -> str:
"""Extract ROLE & EMPATHY + CLINICAL SCOPE from persona.md for system_prompt.
STRUCTURED INTERACTION PROTOCOL moves to SKILL.md.
NON-CLINICAL BOUNDARIES + EMERGENCY RED FLAGS move to HarnessProfile suffix.
"""
persona_text = (agent_dir / "persona.md").read_text()
sections = parse_persona_sections(persona_text)
# Only ROLE & EMPATHY + CLINICAL SCOPE go into system_prompt
return f"""{sections['role_and_empathy']}
{sections['clinical_scope']}"""
def parse_persona_sections(text: str) -> dict[str, str]:
"""Parse the 5-section persona.md into a dict keyed by section name."""
import re
section_pattern = re.compile(
r"^(ROLE & EMPATHY|CLINICAL SCOPE & FOCUS|STRUCTURED INTERACTION PROTOCOL|"
r"STRICT NON-CLINICAL BOUNDARIES|EXPLICIT EMERGENCY RED FLAGS):\s*$",
re.MULTILINE,
)
# ... split text by section headers, return dict
Step 3: Move STRUCTURED INTERACTION PROTOCOL into SKILL.md
The 4-step protocol currently in persona.md belongs in the skill's SKILL.md body — it's task-specific methodology, not agent identity. Example for cardiology-prep:
---
name: cardiology-prep
description: Guides patients through pre-visit preparation for cardiology consultations...
metadata:
risk_class: clinical_assist
domain: clinical
---
# Cardiology Visit Preparation
## Structured Interaction Protocol
When a user seeks guidance, follow this 4-step structured protocol:
1. **Clarify Context**: Inquire about the upcoming cardiology visit type...
2. **Synthesize Vitals & History**: Help organize home blood pressure logs...
3. **Formulate Top Questions**: Guide the user to select 3-5 prioritized questions...
4. **Synthesize Consultation Agenda**: Structure priorities into a visit agenda...
## Reference Documents
- `references/bp-log-template.md` — Blood pressure tracking template
- `references/cardiac-question-guide.md` — Question formulation guide
Under progressive disclosure, this only loads when the agent activates cardiology-prep — not on every turn.
6. Unify agent.yaml with Deep Agents YAML Contracts
HarnessProfileConfig supports direct YAML loading via from_dict() — but it covers only the harness-level concerns (prompt, excluded tools/middleware, GP subagent). Our agent.yaml carries 22 fields, most of which map to different Deep Agents constructs. Rather than maintaining a custom agent_loader.py → AgentManifest → _resolve_system_prompt() pipeline, we can decompose agent.yaml so that:
- Harness-level fields → a single
carefold-profile.yaml(loaded viaHarnessProfileConfig.from_dict()) - Per-agent runtime fields → the
SubAgentdict (constructed at startup from a thin YAML) - Clinical domain fields →
SKILL.mdmetadata:(already covered in Section 1) - Marketplace/UI fields → a separate
metadata.yaml(consumed by the web frontend, not the runtime)
Field-by-Field Disposition
agent.yaml Field |
Deep Agents Destination | Notes |
|---|---|---|
| Harness-level (shared across models) | ||
forbidden |
HarnessProfileConfig.system_prompt_suffix |
Safety contract text; registered once per model via register_harness_profile() |
| — | HarnessProfileConfig.excluded_tools |
Map forbidden verbs to tool exclusion where applicable |
| — | HarnessProfileConfig.excluded_middleware |
Not currently used, but available |
| — | HarnessProfileConfig.general_purpose_subagent |
Disable GP subagent (Carefold uses explicit specialist routing) |
| Per-agent runtime | ||
id |
SubAgent["name"] |
Direct mapping |
description |
SubAgent["description"] |
Direct mapping |
persona |
SubAgent["system_prompt"] |
ROLE + SCOPE only (canonical persona.md) |
model |
SubAgent["model"] |
Direct mapping ("ollama:llama3.2") |
skills |
SubAgent["skills"] |
Direct mapping (["/skills/cardiology-prep/"]) |
tools |
SubAgent["tools"] |
Resolved to tool callables against Phase 0 registry |
max_iterations |
create_deep_agent(recursion_limit=) |
Per-agent iteration cap |
can_delegate |
SubAgent["tools"] inclusion of delegate_to_agent |
Conditional tool presence |
| Clinical domain (to SKILL.md metadata) | ||
risk_class |
SKILL.md metadata.risk_class |
Read by ClinicalSafetyMiddleware from state["skills_metadata"] |
domain |
SKILL.md metadata.domain |
Used for routing, not runtime behavior |
category |
SKILL.md metadata.category |
Used for routing, not runtime behavior |
| Marketplace/UI only | ||
title |
metadata.yaml |
Display name for web UI |
version |
metadata.yaml |
Marketplace version badge |
license |
metadata.yaml |
License display |
care_stages |
metadata.yaml |
Filter facets in marketplace UI |
target_audience |
metadata.yaml |
Filter facets in marketplace UI |
tags |
metadata.yaml |
Search/filter in marketplace UI |
icon |
metadata.yaml |
UI icon identifier |
maturity |
metadata.yaml |
Stability badge |
hidden |
metadata.yaml |
Marketplace visibility toggle |
Resulting File Layout per Agent
Before (current):
agents/cardiology-guide/
├── agent.yaml # 45 lines — mixes runtime, safety, marketplace, persona ref
└── persona.md # 19 lines — mixes identity, protocol, safety, emergencies
After (canonical layout — single persona.md):
agents/cardiology-guide/
├── agent.yaml # SLIM: only SubAgent-mappable fields (name, description, model, skills, tools)
├── persona.md # SLIM identity: ROLE & EMPATHY + CLINICAL SCOPE only (~400 tokens)
└── metadata.yaml # UI/catalog fields (title, icon, tags, care_stages, etc.)
Shared Carefold Profile YAML
A single carefold-profile.yaml at the project root replaces the safety contract that was duplicated across 22 persona.md files. It's loaded via HarnessProfileConfig.from_dict() and registered for each model Carefold supports:
# carefold-profile.yaml — loaded once, applied to all agents per model
system_prompt_suffix: |
STRICT NON-CLINICAL BOUNDARIES:
You are an educational and administrative navigation companion, NOT a licensed
physician or emergency responder.
- You NEVER diagnose medical conditions or interpret clinical laboratory results.
- You NEVER prescribe medications, recommend treatments, or calculate drug dosages.
- You NEVER instruct patients to start, stop, adjust, or discontinue prescribed therapies.
EXPLICIT EMERGENCY RED FLAGS:
If a user describes acute, potentially life-threatening symptoms — including crushing
chest pain; pain radiating to the arm, jaw, or back; sudden shortness of breath;
signs of stroke (FAST criteria); anaphylaxis; severe hemorrhaging; or active suicidal
ideation — you must IMMEDIATELY instruct them to call 911 and go to the nearest
emergency department. Do NOT continue routine navigation.
general_purpose_subagent:
enabled: false
# Loaded at startup — zero custom profile code
import yaml
from deepagents import HarnessProfileConfig, register_harness_profile
with open("carefold-profile.yaml") as f:
profile_config = HarnessProfileConfig.from_dict(yaml.safe_load(f))
# Register for every model Carefold supports
for model_key in ["ollama:llama3.2", "google_genai:gemini-3.6-flash", "anthropic:claude-sonnet-4-6"]:
register_harness_profile(model_key, profile_config)
Slim agent.yaml → SubAgent Dict
The per-agent agent.yaml shrinks to only the fields the runtime needs — everything else moves to metadata.yaml (web UI) or SKILL.md metadata (clinical safety):
# agents/cardiology-guide/agent.yaml (SLIM — runtime only)
name: cardiology-guide
description: >-
Cardiovascular care navigator assisting patients with hypertension tracking,
arrhythmia consultation prep, heart health lifestyle agendas, and vital sign logs.
model: ollama:llama3.2
skills:
- /skills/cardiology-prep/
tools:
- attach-read
- skill-docs
- workspace-note
persona: persona.md
def load_subagent_from_yaml(agent_dir: Path) -> dict:
"""Build a Deep Agents SubAgent dict from the slim agent.yaml.
No custom AgentManifest, no agent_loader.py — just YAML → dict.
"""
raw = yaml.safe_load((agent_dir / "agent.yaml").read_text())
# Load persona.md for system_prompt (contains slim identity: ROLE + SCOPE)
persona_file = agent_dir / raw.get("persona", "persona.md")
system_prompt = persona_file.read_text() if persona_file.exists() else ""
return {
"name": raw["name"],
"description": raw["description"],
"system_prompt": system_prompt,
"model": raw.get("model", "ollama:llama3.2"),
"skills": raw.get("skills", []),
"tools": resolve_phase0_tools(raw.get("tools", [])),
}
What This Eliminates
| Custom Code | Status | Replaced By |
|---|---|---|
agent_loader.py (load_agent(), 367 lines) |
Eliminated | load_subagent_from_yaml() (~20 lines) + SubAgent dict |
AgentManifest Pydantic model (22 fields) |
Eliminated | SubAgent dict fields (6 fields) |
AgentPersonaObject model |
Eliminated | String system_prompt from persona.md |
_resolve_system_prompt() (160 lines) |
Eliminated | system_prompt + HarnessProfile.system_prompt_suffix |
agent_execution_prompt.hbs (71 lines) |
Eliminated | SkillsMiddleware.system_prompt template + profile suffix |
compute_effective_tools() |
Simplified | resolve_phase0_tools() validates against registry, tools go to SubAgent["tools"] |
| Safety contract duplication (22 × persona.md) | Eliminated | Single carefold-profile.yaml → HarnessProfileConfig.from_dict() |
| Risk class elevation logic | Moved | ClinicalSafetyMiddleware reads SKILL.md metadata.risk_class |
Net reduction: ~600 lines of custom loader/assembly code replaced by ~50 lines of YAML loading + Deep Agents native constructs.
Token Savings (per agent, per turn)
| Component | Before (in persona.md) |
After | Savings |
|---|---|---|---|
| ROLE & EMPATHY | ~200 tokens ✓ | ~200 tokens (in system_prompt) |
0 |
| CLINICAL SCOPE | ~200 tokens ✓ | ~200 tokens (in system_prompt) |
0 |
| STRUCTURED PROTOCOL | ~400 tokens ✓ | 0 (moved to SKILL.md, on-demand) |
~400 tokens |
| NON-CLINICAL BOUNDARIES | ~250 tokens ✓ (duplicated ×22) | ~250 tokens × 1 (via HarnessProfile) |
~250 tokens per agent |
| EMERGENCY RED FLAGS | ~200 tokens ✓ (duplicated ×22) | ~200 tokens × 1 (via HarnessProfile) |
~200 tokens per agent |
| Total per agent per turn | ~1,250 tokens | ~400 tokens + 450 shared | ~850 tokens saved |
The safety suffix from HarnessProfile is paid once regardless of which agent or subagent is active — it's shared infrastructure, not per-agent cost.
8. Preserved Safety Invariants
| Invariant | How It's Preserved |
|---|---|
| Phase 0 Closed Tool Registry | ClinicalSafetyMiddleware.before_agent() validates metadata.tools against Phase 0 set |
| Risk Class Elevation | ClinicalSafetyMiddleware.before_agent() scans metadata.risk_class across all loaded skills |
| Mandatory Intended-Use Declarations | ClinicalSafetyMiddleware reads and validates SKILL.md body for required disclaimer lines |
| Emergency Red-Flag Gating | HarnessProfile.system_prompt_suffix carries red-flag protocol; ClinicalSafetyMiddleware.before_model() validates |
| PII Sanitization | sanitize_pii remains a registered tool; before_tool hook validates PII in tool args |
| Sandboxed Path Resolution | FilesystemPermission(mode="deny") on /skills/** writes; FilesystemBackend handles path sandboxing |
| Tool Union Computation | metadata.tools validated at startup; subagent tools explicitly declared per subagent |
| Penetration Test Harness | All 47 attack vectors tested against the new middleware stack |
| Non-Clinical Boundaries | HarnessProfile.system_prompt_suffix carries boundaries — applied to ALL agents universally |
9. Migration Strategy
9a. SKILL.md Frontmatter Migration (carefold.yaml → metadata)
A one-time migration script moves carefold.yaml fields into SKILL.md metadata:
"""Migrate carefold.yaml fields into SKILL.md metadata frontmatter."""
def migrate_skill(skill_dir: Path):
skill_md = skill_dir / "SKILL.md"
carefold_yaml = skill_dir / "carefold.yaml"
# Parse existing frontmatter
frontmatter, body = parse_frontmatter(skill_md.read_text())
# Load carefold.yaml
carefold = yaml.safe_load(carefold_yaml.read_text())
# Merge into metadata
frontmatter.setdefault("metadata", {})
for key in ["risk_class", "domain", "category", "version", "tools", "forbidden", "evals"]:
if key in carefold:
frontmatter["metadata"][key] = carefold[key]
# Add license if missing
frontmatter.setdefault("license", "Apache-2.0")
# Map tools to allowed-tools
if carefold.get("tools"):
frontmatter["allowed-tools"] = " ".join(carefold["tools"])
# Write updated SKILL.md
write_frontmatter(skill_md, frontmatter, body)
# Archive (don't delete) carefold.yaml
carefold_yaml.rename(skill_dir / "carefold.yaml.migrated")
9b. Persona.md Decomposition Migration
A second migration script decomposes each persona.md into its target locations:
"""Decompose persona.md into system_prompt + SKILL.md + HarnessProfile suffix."""
def migrate_persona(agent_dir: Path, skill_dirs: list[Path]):
persona_text = (agent_dir / "persona.md").read_text()
sections = parse_persona_sections(persona_text)
# 1. Overwrite persona.md in-place with the slimmed identity (ROLE + SCOPE only)
slim_persona = f"{sections['role_and_empathy']}\n\n{sections['clinical_scope']}"
(agent_dir / "persona.md").write_text(slim_persona)
# 2. Append STRUCTURED INTERACTION PROTOCOL to primary skill's SKILL.md body
if skill_dirs:
primary_skill_md = skill_dirs[0] / "SKILL.md"
frontmatter, body = parse_frontmatter(primary_skill_md.read_text())
protocol = sections.get("structured_interaction_protocol", "")
if protocol and protocol not in body:
body = f"{body}\n\n## Structured Interaction Protocol\n{protocol}"
write_frontmatter(primary_skill_md, frontmatter, body)
# 3. NON-CLINICAL BOUNDARIES + EMERGENCY RED FLAGS → centralized in carefold-profile.yaml
# 4. Note: Git history preserves original versions; no temporary .migrated files remain in the final repository tree.
9c. agent.yaml Decomposition Migration
A third migration script splits each agent.yaml into a slim runtime YAML + metadata YAML:
"""Decompose agent.yaml into slim runtime YAML + metadata YAML."""
RUNTIME_FIELDS = {"name", "description", "model", "skills", "tools", "persona"}
METADATA_FIELDS = {"title", "version", "license", "care_stages", "target_audience",
"tags", "icon", "maturity", "hidden"}
def migrate_agent_yaml(agent_dir: Path):
raw = yaml.safe_load((agent_dir / "agent.yaml").read_text())
# 1. Build slim agent.yaml (runtime fields only)
slim = {}
slim["name"] = raw.get("id", raw.get("name"))
for key in ["description", "model", "skills", "tools"]:
if key in raw:
slim[key] = raw[key]
# Set canonical persona reference
slim["persona"] = "persona.md"
# Prefix model with provider if bare
if "model" in slim and ":" not in str(slim["model"]):
slim["model"] = f"ollama:{slim['model']}"
# Prefix skills with /skills/ path
if "skills" in slim:
slim["skills"] = [f"/skills/{s}/" if not s.startswith("/") else s
for s in slim["skills"]]
# 2. Build metadata.yaml (UI and catalog fields)
metadata = {k: raw[k] for k in METADATA_FIELDS if k in raw}
# 3. Write both
(agent_dir / "agent.yaml").write_text(yaml.dump(slim, sort_keys=False))
(agent_dir / "metadata.yaml").write_text(yaml.dump(metadata, sort_keys=False))
# 4. forbidden → already in carefold-profile.yaml (shared)
# 5. risk_class, domain, category → already in SKILL.md metadata (9a)
Consequences
Positive Consequences
- 60–80% reduction in baseline system prompt tokens for typical consultations.
- Zero custom skill-loading code —
SkillsMiddlewarehandles discovery, parsing, caching, and 3-tier progressive disclosure out of the box. - Agent Skills spec compliance — skills become interoperable with the broader
deepagentsecosystem and community skill packs. - Storage abstraction for free —
CompositeBackendenables future multi-tenant skill libraries without additional work. - Upstream improvements for free — prompt caching, summarization, subagent isolation, and skill reload come from
deepagentsreleases. - Permissions for free —
FilesystemPermissionprovides path-levelallow/deny/interruptwithout custom code. - HITL for free —
interrupt_onprovides configurable human approval flows. - Clean separation of concerns — safety invariants live in
ClinicalSafetyMiddleware, not scattered across orchestrator and execution nodes.
Negative Consequences
- Hard dependency on
deepagents>=0.7— tied to LangGraph ecosystem evolution. - One-time migration of 23 skill packs (merging
carefold.yamlintoSKILL.mdmetadata) — estimated 1 day with migration script. - 1–2 additional tool calls per consultation for on-demand skill/reference retrieval (~1–3 seconds).
- Override replaces entirely — the custom
SkillsMiddlewareinstance must be fully configured withbackendandsources.
Neutral / Preserved Behaviors
- The 5-phase LangGraph lifecycle can be adapted to work within the Deep Agents middleware hooks.
- SSE streaming from the web UI connects to
agent.stream(). - The penetration test harness (47 attacks) continues to pass.
- All
metadatafields (risk_class,domain,tools,forbidden) remain functional viastate["skills_metadata"].
Verification & Code References
Core Implementation Files (To Be Modified)
- Skill Manifests: All 23
skills/*/SKILL.mdfiles — addmetadatablock, addlicensefield - Migration Script:
skill manifest frontmatter loader(new) - Agent Construction:
backend/src/carefold/engine/agent_factory.py(new — replaces parts of registry.py) - Safety Middleware:
backend/src/carefold/middleware/clinical_safety.py(new) - Audit Middleware:
backend/src/carefold/middleware/audit.py(new)
Core Implementation Files (Unchanged — Safety Invariants)
- Frontmatter Validator:
backend/src/carefold/loaders/frontmatter.py—check_mandatory_intended_use()still validates SKILL.md body - Tool Registry:
backend/src/carefold/loaders/union.py— Phase 0 registry constants - Safety Classifier:
backend/src/carefold/safety/classifier.py - Emergency Protocol:
backend/src/carefold/safety/emergency.py
New Dependencies
# pyproject.toml
[project]
dependencies = [
"deepagents>=0.7",
"langgraph>=0.3",
"langchain>=0.4",
]
Verification Commands
# 1. License Check
pnpm run check:licenses
# 2. Backend Unit & Regression Tests
backend/.venv/bin/pytest backend/tests/
# 3. Security Penetration Suite (47 Attacks)
PYTHONPATH=backend/src backend/.venv/bin/python backend/tests/penetration_suite.py
# 4. Frontend Typecheck
pnpm --filter web typecheck
# 5. Frontend Unit & Component Tests
pnpm --filter web test --exclude "**/api/**"
# 6. Frontend Production Build
pnpm --filter web build