diff --git a/README.md b/README.md index 18ef7b1c58..7a8a2e3181 100644 --- a/README.md +++ b/README.md @@ -2246,7 +2246,7 @@ Supported commands: After each Gateway-backed run, DeerFlow evaluates the visible conversation, including the assistant's tool calls and shortened tool results, against the active goal with a non-thinking evaluator model. A successful tool result alone does not satisfy a goal, and when the assistant had to guess missing or ambiguous information the evaluator reports `needs_user_input`. The evaluator must return a typed blocker (`missing_evidence`, `needs_user_input`, `run_failed`, `external_wait`, or `goal_not_met_yet`) plus visible evidence. DeerFlow only injects a hidden continuation when the latest assistant turn is durably checkpointed, the blocker is `goal_not_met_yet`, the thread did not change during evaluation, and the no-progress breaker has not fired. The safety cap defaults to 8 hidden continuations, and repeated identical non-progress evaluations stop after 2 attempts. `/goal clear` and any user-authored new input win over queued continuations. When the evaluator considers the goal satisfied, DeerFlow clears it after the run successfully finalizes and publishes the updated thread state. The same checkpoint records the completion as `goal_outcome`, which stays until the next goal change. If required artifact delivery or its receipt persistence fails, the run reports an error and the goal remains active for a later retry; this does not start another hidden continuation. -An unmet goal also stops automatic continuation after a token or loop-detection hard stop; it stays active for a later user turn. +For tool-using turns, successful operation results drive the no-progress count: changing only reply wording or a sandbox tool's UI explanation does not reset it, while newly observed work can keep it going. Text-only turns keep their reply-based check. An unmet goal still stops automatic continuation after a token or loop-detection hard stop and stays active for a later user turn. Regenerating an earlier response keeps the goal currently set for the chat and its used continuation budget. It does not restore a goal that was cleared or achieved. The earlier answer's completion record is removed when its historical checkpoint is replayed. diff --git a/backend/docs/runtime-guidance-details.md b/backend/docs/runtime-guidance-details.md index 892de72c00..20cb5b3cb1 100644 --- a/backend/docs/runtime-guidance-details.md +++ b/backend/docs/runtime-guidance-details.md @@ -2,6 +2,28 @@ The runtime AGENTS.md indexes these detailed contracts. Preserve their behavior when changing the corresponding code. +**Goal no-progress counting** (`runtime/goal_progress.py`): pair actual tool calls +with their results in the current human/hidden-continuation interval. Normalize +success/error metadata without mutating checkpoint messages. Retain only exact +successful-operation hashes in the active Goal's `last_evaluation`; no global +cache, arbitrary operation-count cutoff, file inspection or extra model call. +Repeated results, UI-only sandbox descriptions, failed commands, receipt IDs and +timestamps alone do not reset the counter. Validate receipts against the original +arguments before excluding narration from operation identity. Shared shell-result +classification recognizes nonzero exit markers, correcting older generic success +metadata while preserving explicit producer stamps. A successful new operation is +activity, not proof of usefulness or completion. Pure-text turns retain the +existing reply comparison. + +Already evaluated hashes survive checkpoint restart and compaction. Results +removed before observation cannot be reconstructed; do not claim complete tool +history capture. Goal replacement/clearing drops the active set, while historical +checkpoints retain their existing lifecycle. Storage grows with observed work; +measure it rather than reintroducing a silent fallback to text after a fixed +number of operations. Preserve old counters on first migration, merge against +freshly read Goal state when persisting, and leave all existing stop, durability, +cancellation and racing-user checks unchanged. + **JSONL tail recovery** (`runtime/events/store/jsonl.py`, #6520): every append path goes through `_append_records`. A nonempty file whose last byte is not LF gets a separator written first, so a crash that left either valid JSON without diff --git a/backend/packages/harness/deerflow/agents/middlewares/AGENTS.md b/backend/packages/harness/deerflow/agents/middlewares/AGENTS.md index 7f5d118061..9b491b5523 100644 --- a/backend/packages/harness/deerflow/agents/middlewares/AGENTS.md +++ b/backend/packages/harness/deerflow/agents/middlewares/AGENTS.md @@ -94,7 +94,7 @@ No match: skip inspection, log ID. Tests: `test_read_mark_tool_call_correlation. 12. **SandboxAuditMiddleware** - Audits sandboxed shell/file operations before tool execution; command classification is **defense-in-depth and audit, not a security boundary** (the sandbox is the isolation boundary). Command substitution is judged by *position*, not the presence of `$(`: **command position** (`$(curl url)`, `` `curl url` ``, the word after `|`/`&&`/`;`, an `eval`/`source` argument, a command-position `.`) executes fetched content and is blocked; **value position** (`x=$(curl url)`, `echo $(curl url)`, an argument, a `for` word list) only captures output and passes (#4611). So `_HIGH_RISK_COMMAND_POSITION_PATTERNS` is matched anchored against each sub-command from `_split_compound_command(split_pipes=True)`, never the whole string; pipe-spanning rules (`| sh`, `base64 -d | ...`) still use `_classify_command`'s whole-command Pass 1. `_COMMAND_POSITION_PREFIX` extends the anchor over leading assignments and exec wrappers (`FOO=1 $(curl url)`, `env`/`command`/`builtin`/`exec`/`nohup`/`time`/`sudo`/`doas`); its assignment branch requires whitespace before the substitution, which keeps `x=$(curl url)` in value position. Two contexts are deliberately **position-blind** (matched whole-command in Pass 1, since they execute their input anywhere, e.g. `xargs sh -c "$(curl url)"`): an `eval`/`source` argument (including one that reaches the dot builtin, `eval . $(curl url)`), and an interpreter **code-string flag** — `-c` (shells, `python`), `-e` (`perl`/`ruby`/`node`), `-p` (`perl`/`node`), `-r` (`php`) — plus the here-string (`<<<`) reaching the same place via stdin. An interpreter that reads its program from stdin (`bash < <(curl url)`) is that same execution context, decided from the token list rather than one pattern: a script operand anywhere (`bash script.sh < <(curl url)`, `bash < <(curl url) consumer.sh`) or a shell noexec flag (`bash -n < <(curl url)`) keeps the fetched bytes as data, and `.`/`source` never read a program from stdin because they require a filename. All three substitution spellings (`$(`, `<(`, `` ` ``) share one `_RISKY_SUBSTITUTION` opener. An unquoted newline splits like `;` (else `echo hi\n$(curl url)` evades the anchored rules). Heredoc bodies are data: `_split_compound_command` records headers (`<