diff --git a/backend/.deer-flow/agents/harness/SOUL.md b/backend/.deer-flow/agents/harness/SOUL.md new file mode 100644 index 00000000000..fbb6ae8201b --- /dev/null +++ b/backend/.deer-flow/agents/harness/SOUL.md @@ -0,0 +1,24 @@ +You are a precise code agent. You make changes through structured patches, not free-form edits. + +## Rules + +1. **Read before you edit.** Always use harness_read_file to understand the code before proposing changes. +2. **Propose, don't write.** Use harness_propose_patch with a unified diff. Direct file writes are not available. +3. **Fix validation errors.** If your patch is rejected, read the error carefully, fix the issue, and propose again. +4. **Test before you commit.** Run harness_run_tests after applying a patch. You cannot commit until tests pass. +5. **One change at a time.** Make small, focused patches. Don't combine unrelated changes. + +## Workflow + +``` +harness_read_file → understand the code +harness_search → find related code +harness_propose_patch → submit your change as a unified diff +harness_apply_patch → apply after validation passes +harness_run_tests → verify nothing is broken +harness_commit → commit when tests pass +``` + +## Output Format + +When asked to fix a bug or implement a feature, your final output should include the unified diff in a ```diff code block. This is how your work is evaluated. diff --git a/backend/.deer-flow/agents/harness/config.yaml b/backend/.deer-flow/agents/harness/config.yaml new file mode 100644 index 00000000000..ba8ca9d6217 --- /dev/null +++ b/backend/.deer-flow/agents/harness/config.yaml @@ -0,0 +1,13 @@ +# Harness agent — restricted ACI tool layer +# Only has harness tools + web search. No bash, no free-form file edits. +# All code changes go through propose_patch → validate → apply_patch. + +name: harness +description: > + Restricted agent for code tasks. Uses harness tools with inline validation, + linting, and sequenced execution (read → propose → apply → test → commit). + Cannot write files directly or run arbitrary shell commands. + +tool_groups: + - harness + - web diff --git a/backend/src/agents/middlewares/_runtime_helpers.py b/backend/src/agents/middlewares/_runtime_helpers.py new file mode 100644 index 00000000000..6cd08ce78bc --- /dev/null +++ b/backend/src/agents/middlewares/_runtime_helpers.py @@ -0,0 +1,58 @@ +"""Helpers for safely reading run-scoped values from a LangGraph Runtime. + +LangGraph passes per-run information through two channels: + +- ``runtime.context`` — populated only if the caller explicitly passes a + ``context`` dict on run creation. Many standard callers (the LangGraph + Server REST API, the LangGraph SDK, the LangGraph Studio UI) do not set + it; in that case ``runtime.context`` is ``None``. +- ``config.configurable`` — always populated by LangGraph Server with at + minimum ``thread_id`` (taken from the URL path). Accessed via + ``langgraph.config.get_config()`` from inside a node or middleware. + +DeerFlow's middlewares historically read ``runtime.context.get("thread_id")`` +unconditionally, which raised ``AttributeError: 'NoneType' object has no +attribute 'get'`` for any caller that did not pass a context dict. These +helpers provide a single, defensive accessor that prefers ``runtime.context`` +when present, falls back to the LangGraph-injected config, and raises a +meaningful error when the value really is missing. +""" + +from __future__ import annotations + +from typing import Any + + +def resolve_runtime_value(runtime: Any, key: str) -> Any | None: + """Look up ``key`` in ``runtime.context`` first, then ``config.configurable``. + + Returns ``None`` if absent in both. Never raises. + """ + context = getattr(runtime, "context", None) or {} + value = context.get(key) if isinstance(context, dict) else None + if value is not None: + return value + + try: + from langgraph.config import get_config + + config = get_config() or {} + except Exception: + return None + + configurable = config.get("configurable", {}) if isinstance(config, dict) else {} + if isinstance(configurable, dict): + return configurable.get(key) + return None + + +def require_thread_id(runtime: Any) -> str: + """Resolve ``thread_id`` or raise ``ValueError`` with a helpful message.""" + thread_id = resolve_runtime_value(runtime, "thread_id") + if not thread_id: + raise ValueError( + "Thread ID is required. Set context.thread_id when creating the run " + "or rely on LangGraph Server to populate config.configurable.thread_id " + "from the thread URL." + ) + return str(thread_id) diff --git a/backend/src/agents/middlewares/memory_middleware.py b/backend/src/agents/middlewares/memory_middleware.py index 5fa2e24561a..3520683bbb2 100644 --- a/backend/src/agents/middlewares/memory_middleware.py +++ b/backend/src/agents/middlewares/memory_middleware.py @@ -8,6 +8,7 @@ from langgraph.runtime import Runtime from src.agents.memory.queue import get_memory_queue +from src.agents.middlewares._runtime_helpers import resolve_runtime_value from src.config.memory_config import get_memory_config @@ -119,10 +120,10 @@ def after_agent(self, state: MemoryMiddlewareState, runtime: Runtime) -> dict | if not config.enabled: return None - # Get thread ID from runtime context - thread_id = runtime.context.get("thread_id") + # Get thread ID from runtime context (or LangGraph-injected config fallback) + thread_id = resolve_runtime_value(runtime, "thread_id") if not thread_id: - print("MemoryMiddleware: No thread_id in context, skipping memory update") + print("MemoryMiddleware: No thread_id in context or config, skipping memory update") return None # Get messages from state diff --git a/backend/src/agents/middlewares/thread_data_middleware.py b/backend/src/agents/middlewares/thread_data_middleware.py index ec8f934ab93..b46ed201ed1 100644 --- a/backend/src/agents/middlewares/thread_data_middleware.py +++ b/backend/src/agents/middlewares/thread_data_middleware.py @@ -4,6 +4,7 @@ from langchain.agents.middleware import AgentMiddleware from langgraph.runtime import Runtime +from src.agents.middlewares._runtime_helpers import require_thread_id from src.agents.thread_state import ThreadDataState from src.config.paths import Paths, get_paths @@ -71,9 +72,7 @@ def _create_thread_directories(self, thread_id: str) -> dict[str, str]: @override def before_agent(self, state: ThreadDataMiddlewareState, runtime: Runtime) -> dict | None: - thread_id = runtime.context.get("thread_id") - if thread_id is None: - raise ValueError("Thread ID is required in the context") + thread_id = require_thread_id(runtime) if self._lazy_init: # Lazy initialization: only compute paths, don't create directories diff --git a/backend/src/agents/middlewares/uploads_middleware.py b/backend/src/agents/middlewares/uploads_middleware.py index 3703f3a5ca7..2bd5f6a4362 100644 --- a/backend/src/agents/middlewares/uploads_middleware.py +++ b/backend/src/agents/middlewares/uploads_middleware.py @@ -9,6 +9,7 @@ from langchain_core.messages import HumanMessage from langgraph.runtime import Runtime +from src.agents.middlewares._runtime_helpers import resolve_runtime_value from src.config.paths import Paths, get_paths logger = logging.getLogger(__name__) @@ -146,7 +147,7 @@ def before_agent(self, state: UploadsMiddlewareState, runtime: Runtime) -> dict return None # Resolve uploads directory for existence checks - thread_id = runtime.context.get("thread_id") + thread_id = resolve_runtime_value(runtime, "thread_id") uploads_dir = self._paths.sandbox_uploads_dir(thread_id) if thread_id else None # Get newly uploaded files from the current message's additional_kwargs.files diff --git a/backend/src/sandbox/harness_tools.py b/backend/src/sandbox/harness_tools.py new file mode 100644 index 00000000000..095393fc346 --- /dev/null +++ b/backend/src/sandbox/harness_tools.py @@ -0,0 +1,329 @@ +"""Harness tools — restricted ACI tool layer for DeerFlow agents. + +Replaces free-form sandbox tools (bash, write_file, str_replace) with +structured, validated, sequenced tools that give inline feedback. + +Each tool wraps a HarnessTools method and returns structured feedback +that the LLM sees and acts on in real time. + +Tool set: + harness_read_file — read with line numbers, tracks files_read + harness_search — capped search with "refine" feedback + harness_propose_patch — validate diff via linters + git apply --check + harness_apply_patch — only if proposal validated + harness_run_tests — structured test results + harness_commit — blocked unless tests pass + +Removed (compared to default sandbox tools): + bash, write_file, str_replace — all writes go through propose_patch +""" + +import logging +import sys +from pathlib import Path + +from langchain.tools import ToolRuntime, tool +from langgraph.typing import ContextT + +from src.agents.thread_state import ThreadState +from src.sandbox.tools import ( + ensure_sandbox_initialized, + ensure_thread_directories_exist, + get_thread_data, + is_local_sandbox, + replace_virtual_path, +) + +logger = logging.getLogger(__name__) + +# ── HarnessTools instance management ───────────────────────────────── + +# Lazy import to avoid hard dependency on harness at module load +_harness_tools_class = None +_harness_linters = None + + +def _get_harness_tools_class(): + global _harness_tools_class, _harness_linters + if _harness_tools_class is None: + harness_root = str(Path.home() / "sona" / ".harness") + if harness_root not in sys.path: + sys.path.insert(0, harness_root) + from tools import HarnessTools + _harness_tools_class = HarnessTools + try: + from linters import get_all_linters + _harness_linters = get_all_linters() + except ImportError: + _harness_linters = [] + return _harness_tools_class + + +def _get_or_create_harness(runtime: ToolRuntime) -> "HarnessTools": + """Get or create a HarnessTools instance for this thread. + + Stored in runtime.state so it persists across tool calls within + a single agent run (preserving files_read, patches, etc). + """ + if "harness_tools" not in runtime.state: + HarnessTools = _get_harness_tools_class() + # Resolve repo root from thread workspace + thread_data = get_thread_data(runtime) + workspace = thread_data.get("workspace_path", str(Path.home() / "sona")) + runtime.state["harness_tools"] = HarnessTools( + repo_root=workspace, + linters=_harness_linters, + ) + return runtime.state["harness_tools"] + + +def _resolve_path(path: str, runtime: ToolRuntime) -> str: + """Resolve virtual path to actual path if using local sandbox.""" + if is_local_sandbox(runtime): + thread_data = get_thread_data(runtime) + return replace_virtual_path(path, thread_data) + return path + + +# ── Tools ──────────────────────────────────────────────────────────── + + +@tool("harness_read_file", parse_docstring=True) +def harness_read_file_tool( + runtime: ToolRuntime[ContextT, ThreadState], + description: str, + path: str, + offset: int = 0, + limit: int = 200, +) -> str: + """Read a file with line numbers. The harness tracks which files you've read — you can only propose patches to files you've read first. + + Shows up to 200 lines at a time with line numbers prepended. Use offset to paginate through large files. + + Args: + description: Why you are reading this file. ALWAYS PROVIDE THIS FIRST. + path: Absolute path to the file to read. + offset: Line offset to start reading from (0-indexed). Default: 0. + limit: Maximum number of lines to return. Default: 200. + """ + try: + ensure_sandbox_initialized(runtime) + ensure_thread_directories_exist(runtime) + harness = _get_or_create_harness(runtime) + actual_path = _resolve_path(path, runtime) + result = harness.read_file(actual_path, offset=offset, limit=limit) + if "error" in result: + return f"Error: {result['error']}" + return ( + f"File: {path} ({result['total_lines']} lines, showing {result['showing']})\n" + f"Files read so far: {len(harness.state.files_read)}\n\n" + f"{result['content']}" + ) + except Exception as e: + return f"Error: {type(e).__name__}: {e}" + + +@tool("harness_search", parse_docstring=True) +def harness_search_tool( + runtime: ToolRuntime[ContextT, ThreadState], + description: str, + query: str, + path: str = ".", + max_results: int = 30, +) -> str: + """Search the repository for a pattern. Results are CAPPED at 30 — if you get too many results, narrow your search. + + Searches .py, .yaml, .json, and .md files. + + Args: + description: Why you are searching. ALWAYS PROVIDE THIS FIRST. + query: The search pattern (grep-compatible). + path: Directory to search in. Default: current directory. + max_results: Maximum results to return. Default: 30. + """ + try: + ensure_sandbox_initialized(runtime) + ensure_thread_directories_exist(runtime) + harness = _get_or_create_harness(runtime) + actual_path = _resolve_path(path, runtime) + result = harness.search_repo(query, path=actual_path, max_results=max_results) + if "error" in result: + return f"Error: {result['error']}" + if result.get("message"): + return f"⚠ {result['message']} Narrow your search query." + if not result.get("results"): + return f"No matches for '{query}'" + header = f"Found {result['total_matches']} matches (showing {result['showing']}):\n" + return header + "\n".join(result["results"]) + except Exception as e: + return f"Error: {type(e).__name__}: {e}" + + +@tool("harness_propose_patch", parse_docstring=True) +def harness_propose_patch_tool( + runtime: ToolRuntime[ContextT, ThreadState], + description: str, + diff: str, + reason: str, +) -> str: + """Propose a code change as a unified diff. This is the ONLY way to modify files. + + The patch is validated before it can be applied: + - All files in the diff must have been read first (use harness_read_file) + - The diff must be valid unified diff format + - The patch must apply cleanly (git apply --check) + - Architectural linters check for secrets, boundary violations, style issues + + If validation fails, you'll get specific error messages. Fix the issues and propose again. + + Args: + description: Why you are proposing this change. ALWAYS PROVIDE THIS FIRST. + diff: The unified diff. Must use --- a/ and +++ b/ format with @@ hunk headers. + reason: A 1-2 sentence explanation of what this change does and why. + """ + try: + harness = _get_or_create_harness(runtime) + result = harness.propose_patch(diff, reason) + if "error" in result: + return ( + f"❌ REJECTED: {result['error']}\n" + f"Hint: {result.get('hint', 'Check the error and fix your diff.')}" + ) + if result.get("status") == "rejected": + errors = "\n".join(f" • {e}" for e in result.get("errors", [])) + return ( + f"❌ PATCH REJECTED — {len(result.get('errors', []))} validation error(s):\n" + f"{errors}\n\n" + f"Fix these issues and propose again. Patch ID: {result['patch_id']}" + ) + return ( + f"✓ PATCH VALIDATED — ready to apply.\n" + f" Patch ID: {result['patch_id']}\n" + f" Files: {', '.join(result.get('files', []))}\n" + f" Reason: {reason}\n\n" + f"Use harness_apply_patch with patch_id='{result['patch_id']}' to apply." + ) + except Exception as e: + return f"Error: {type(e).__name__}: {e}" + + +@tool("harness_apply_patch", parse_docstring=True) +def harness_apply_patch_tool( + runtime: ToolRuntime[ContextT, ThreadState], + description: str, + patch_id: str, +) -> str: + """Apply a previously validated patch. The patch must have been proposed and validated first. + + After applying, tests_passed is reset to False — you must run harness_run_tests before committing. + + Args: + description: Why you are applying this patch. ALWAYS PROVIDE THIS FIRST. + patch_id: The patch ID returned by harness_propose_patch (e.g. 'patch-001'). + """ + try: + harness = _get_or_create_harness(runtime) + result = harness.apply_patch(patch_id) + if "error" in result: + return f"❌ APPLY FAILED: {result['error']}" + return ( + f"✓ PATCH APPLIED: {patch_id}\n" + f" Files modified: {', '.join(result.get('files', []))}\n" + f" ⚠ tests_passed is now False — run harness_run_tests to verify." + ) + except Exception as e: + return f"Error: {type(e).__name__}: {e}" + + +@tool("harness_run_tests", parse_docstring=True) +def harness_run_tests_tool( + runtime: ToolRuntime[ContextT, ThreadState], + description: str, + scope: str = "all", + timeout: int = 120, +) -> str: + """Run tests to verify your changes. You must run tests before committing. + + Args: + description: Why you are running tests. ALWAYS PROVIDE THIS FIRST. + scope: Test scope — 'all' for full suite, or a specific path like 'tests/test_foo.py'. + timeout: Timeout in seconds. Default: 120. + """ + try: + harness = _get_or_create_harness(runtime) + result = harness.run_tests(scope=scope, timeout=timeout) + if result.get("error"): + return f"❌ TEST ERROR: {result['error']}" + if result.get("passed"): + return ( + f"✓ TESTS PASSED\n" + f" You can now use harness_commit to commit your changes.\n\n" + f"{result.get('stdout', '')[-500:]}" + ) + return ( + f"❌ TESTS FAILED (exit code {result.get('returncode', '?')})\n" + f" Fix the failing tests and run again.\n\n" + f"STDOUT:\n{result.get('stdout', '')[-800:]}\n" + f"STDERR:\n{result.get('stderr', '')[-400:]}" + ) + except Exception as e: + return f"Error: {type(e).__name__}: {e}" + + +@tool("harness_commit", parse_docstring=True) +def harness_commit_tool( + runtime: ToolRuntime[ContextT, ThreadState], + description: str, + message: str, +) -> str: + """Commit the current changes. Only allowed if tests have passed. + + Args: + description: Why you are committing. ALWAYS PROVIDE THIS FIRST. + message: The commit message. + """ + try: + harness = _get_or_create_harness(runtime) + result = harness.commit_change(message) + if "error" in result: + return f"❌ COMMIT BLOCKED: {result['error']}\nHint: {result.get('hint', '')}" + return f"✓ COMMITTED: {message}" + except Exception as e: + return f"Error: {type(e).__name__}: {e}" + + +@tool("harness_status", parse_docstring=True) +def harness_status_tool( + runtime: ToolRuntime[ContextT, ThreadState], + description: str, +) -> str: + """Check your current harness state — files read, patches proposed, tests status. + + Use this to understand what you can do next in the read→propose→apply→test→commit sequence. + + Args: + description: Why you are checking status. ALWAYS PROVIDE THIS FIRST. + """ + try: + harness = _get_or_create_harness(runtime) + state = harness.state + files = sorted(state.files_read) if state.files_read else ["(none)"] + patches = [] + for pid, p in state.patches_proposed.items(): + status = "✓ applied" if p.applied else ("✓ validated" if p.validated else "❌ rejected") + patches.append(f" {pid}: {status} — {', '.join(p.files_affected)}") + if not patches: + patches = [" (none)"] + + return ( + f"Harness State:\n" + f" Files read: {len(state.files_read)}\n" + f" {chr(10).join(files)}\n" + f" Patches:\n" + f" {chr(10).join(patches)}\n" + f" Tests passed: {state.tests_passed}\n" + f" Actions taken: {len(state.action_log)}\n\n" + f"Sequence: read → propose_patch → apply_patch → run_tests → commit" + ) + except Exception as e: + return f"Error: {type(e).__name__}: {e}" diff --git a/backend/src/sandbox/middleware.py b/backend/src/sandbox/middleware.py index a6c7e8df124..5c6d7148dc4 100644 --- a/backend/src/sandbox/middleware.py +++ b/backend/src/sandbox/middleware.py @@ -5,6 +5,10 @@ from langchain.agents.middleware import AgentMiddleware from langgraph.runtime import Runtime +from src.agents.middlewares._runtime_helpers import ( + require_thread_id, + resolve_runtime_value, +) from src.agents.thread_state import SandboxState, ThreadDataState from src.sandbox import get_sandbox_provider @@ -56,7 +60,7 @@ def before_agent(self, state: SandboxMiddlewareState, runtime: Runtime) -> dict # Eager initialization (original behavior) if "sandbox" not in state or state["sandbox"] is None: - thread_id = runtime.context["thread_id"] + thread_id = require_thread_id(runtime) sandbox_id = self._acquire_sandbox(thread_id) logger.info(f"Assigned sandbox {sandbox_id} to thread {thread_id}") return {"sandbox": {"sandbox_id": sandbox_id}} @@ -71,8 +75,8 @@ def after_agent(self, state: SandboxMiddlewareState, runtime: Runtime) -> dict | get_sandbox_provider().release(sandbox_id) return None - if runtime.context.get("sandbox_id") is not None: - sandbox_id = runtime.context.get("sandbox_id") + sandbox_id = resolve_runtime_value(runtime, "sandbox_id") + if sandbox_id is not None: logger.info(f"Releasing sandbox {sandbox_id} from context") get_sandbox_provider().release(sandbox_id) return None diff --git a/config.yaml b/config.yaml new file mode 100644 index 00000000000..c4d31f043cb --- /dev/null +++ b/config.yaml @@ -0,0 +1,252 @@ +# DeerFlow — DGX Spark config +# Default model: deepseek (best value for agentic tasks) +# Local models: devstral-small-2 (best local for agentic/tool-calling) + +models: + - name: deepseek + display_name: DeepSeek V3.2 (API) + use: langchain_openai:ChatOpenAI + model: deepseek-chat + api_key: $DEEPSEEK_API_KEY + base_url: https://api.deepseek.com/v1 + max_tokens: 8192 + temperature: 0.7 + supports_thinking: false + supports_vision: false + + - name: deepseek-thinking + display_name: DeepSeek R1 (Thinking) + use: src.models.patched_deepseek:PatchedChatDeepSeek + model: deepseek-reasoner + api_key: $DEEPSEEK_API_KEY + max_tokens: 16384 + supports_thinking: true + supports_vision: false + when_thinking_enabled: + extra_body: + thinking: + type: enabled + + - name: gpt-5-mini + display_name: GPT-5 Mini (GitHub Copilot) + use: langchain_openai:ChatOpenAI + model: gpt-5-mini + api_key: $COPILOT_SESSION_TOKEN + base_url: https://api.individual.githubcopilot.com + max_tokens: 4096 + temperature: 0.7 + supports_thinking: false + supports_vision: false + default_headers: + editor-version: "vscode/1.96.0" + copilot-integration-id: "vscode-chat" + User-Agent: "GitHubCopilotChat/0.22.0" + + - name: llama-nano + display_name: Llama Nano (Nemotron 30B) + use: langchain_openai:ChatOpenAI + model: nvidia/nemotron-3-nano + api_key: not-needed + base_url: http://127.0.0.1:11800/v1 + max_tokens: 4096 + temperature: 0.7 + supports_thinking: false + supports_vision: false + + - name: llama-brain + display_name: Llama Brain (Qwen 35B MoE) + use: langchain_openai:ChatOpenAI + model: qwen/qwen3.5-35b-a3b + api_key: not-needed + base_url: http://127.0.0.1:11810/v1 + max_tokens: 4096 + temperature: 0.7 + supports_thinking: false + supports_vision: false + + - name: llama-agent + display_name: Llama Agent (Llama 3.3 70B) + use: langchain_openai:ChatOpenAI + model: meta-llama/Llama-3.3-70B-Instruct + api_key: not-needed + base_url: http://127.0.0.1:11820/v1 + max_tokens: 4096 + temperature: 0.7 + supports_thinking: false + supports_vision: false + + - name: qwen3-coder + display_name: Qwen3 Coder (30B MoE, 3B active) + use: langchain_ollama:ChatOllama + model: qwen3-coder:30b + base_url: http://127.0.0.1:11434 + num_predict: 4096 + temperature: 0.7 + num_ctx: 8192 + supports_thinking: false + supports_vision: false + + - name: devstral + display_name: Devstral Small 2 (24B, Ollama) + use: langchain_ollama:ChatOllama + model: devstral-small-2 + base_url: http://127.0.0.1:11434 + num_predict: 8192 + temperature: 0.7 + num_ctx: 32768 + supports_thinking: false + supports_vision: false + + - name: hermes4 + display_name: Hermes 4 (14B, NousResearch) + use: src.models.hermes_chat_ollama:HermesChatOllama + model: hermes4:14b + base_url: http://127.0.0.1:11434 + num_predict: 4096 + temperature: 0.6 + num_ctx: 8192 + supports_thinking: false + supports_vision: false + +tool_groups: + - name: web + - name: file:read + - name: file:write + - name: bash + - name: harness + +tools: + # Web search + fetch via local SearxNG (no API key needed) + - name: web_search + group: web + use: src.community.searxng.tools:web_search_tool + base_url: http://127.0.0.1:8080 + max_results: 5 + + - name: web_fetch + group: web + use: src.community.searxng.tools:web_fetch_tool + timeout: 10 + + - name: image_search + group: web + use: src.community.image_search.tools:image_search_tool + max_results: 5 + + - name: ls + group: file:read + use: src.sandbox.tools:ls_tool + + - name: read_file + group: file:read + use: src.sandbox.tools:read_file_tool + + - name: write_file + group: file:write + use: src.sandbox.tools:write_file_tool + + - name: str_replace + group: file:write + use: src.sandbox.tools:str_replace_tool + + - name: bash + group: bash + use: src.sandbox.tools:bash_tool + + # Harness tools — restricted ACI layer (no free-form edits) + - name: harness_read_file + group: harness + use: src.sandbox.harness_tools:harness_read_file_tool + + - name: harness_search + group: harness + use: src.sandbox.harness_tools:harness_search_tool + + - name: harness_propose_patch + group: harness + use: src.sandbox.harness_tools:harness_propose_patch_tool + + - name: harness_apply_patch + group: harness + use: src.sandbox.harness_tools:harness_apply_patch_tool + + - name: harness_run_tests + group: harness + use: src.sandbox.harness_tools:harness_run_tests_tool + + - name: harness_commit + group: harness + use: src.sandbox.harness_tools:harness_commit_tool + + - name: harness_status + group: harness + use: src.sandbox.harness_tools:harness_status_tool + +# Local sandbox — direct execution on Spark +sandbox: + use: src.sandbox.local:LocalSandboxProvider + +# Subagent config +subagents: + timeout_seconds: 600 # 10 min default (local models are slower) + agents: + general-purpose: + timeout_seconds: 900 + bash: + timeout_seconds: 300 + +skills: + container_path: /mnt/skills + +title: + enabled: false + max_words: 6 + max_chars: 60 + model_name: null + +summarization: + enabled: true + model_name: null # resolved from ~/sona/config/models.yaml role "summarization" + trigger: + - type: tokens + value: 15564 + keep: + type: messages + value: 10 + trim_tokens_to_summarize: 15564 + +memory: + enabled: true + storage_path: memory.json + debounce_seconds: 30 + model_name: null # resolved from ~/sona/config/models.yaml role "memory" + max_facts: 100 + fact_confidence_threshold: 0.7 + injection_enabled: true + max_injection_tokens: 2000 + +# Postgres persistence — threads survive restarts +checkpointer: + type: postgres + connection_string: postgresql://postgres:postgres@127.0.0.1:5432/deerflow + +# IM Channels — DeerFlow's own Telegram presence +channels: + langgraph_url: http://localhost:2024 + gateway_url: http://localhost:8011 + + session: + assistant_id: lead_agent + config: + recursion_limit: 150 + context: + thinking_enabled: true + is_plan_mode: false + subagent_enabled: true + self_evaluation_enabled: true + + telegram: + enabled: true + bot_token: $DEERFLOW_TELEGRAM_BOT_TOKEN + allowed_users: + - 1498269988 # Toni