Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions backend/.deer-flow/agents/harness/SOUL.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 13 additions & 0 deletions backend/.deer-flow/agents/harness/config.yaml
Original file line number Diff line number Diff line change
@@ -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
58 changes: 58 additions & 0 deletions backend/src/agents/middlewares/_runtime_helpers.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 4 additions & 3 deletions backend/src/agents/middlewares/memory_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions backend/src/agents/middlewares/thread_data_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion backend/src/agents/middlewares/uploads_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading