Skip to content

feat(agent): expose ripgrep regex grep options on the grep tool#1309

Open
srtab wants to merge 31 commits into
mainfrom
feat/ripgrep-fs-grep
Open

feat(agent): expose ripgrep regex grep options on the grep tool#1309
srtab wants to merge 31 commits into
mainfrom
feat/ripgrep-fs-grep

Conversation

@srtab

@srtab srtab commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Summary

Daiv-side half of converging the agent's grep tool to a ripgrep-backed, Claude-Code-style regex search (pairs with srtab/daiv-sandbox#323).

  • Mirrors the sandbox FsGrepRequest (case_insensitive / multiline / head_limit) and the new invalid_pattern error code; regenerates schemas.dump.json and keeps the consistency test green.
  • Forwards the extended options through SandboxFileBackend.agrep and a re-implemented DAIVCompositeBackend.agrep routing. head_limit is now enforced across aggregated backends, not just per backend.
  • Adds DAIVFilesystemMiddleware, overriding the grep tool with an extended schema (regex always on, no literal mode; escape metacharacters for literals). The main agent gets it by rebinding deepagents.graph.FilesystemMiddleware — the usual exclude+re-add path is unavailable because upstream lists FilesystemMiddleware as required scaffolding.
  • Removes the now-obsolete literal-grep no-match hint (the engine now returns a real invalid_pattern error instead).

Base

Targets feat/sandbox-filebackend-scratchpad to stack on the in-flight WIP (keeps the diff to just the ripgrep work).

Tests

Full unit suite passes (3013). New coverage: composite multi-backend merge + remap + cross-backend head_limit cap, the grep-tool body (async happy path, read-permission security gate, traversal-path rejection), and an end-to-end test that create_deep_agent actually builds the main agent with the extended grep schema (fails loudly if the rebind ever no-ops).

Known follow-up (deliberately out of scope)

Disk-backed (non-sandbox) runs use upstream's literal grep while the tool description says "always regex" — needs a product call (condition the description on mode, give disk mode regex, or restore the hint on the disk path). Sandbox mode (eval/prod) is unaffected.

🤖 Generated with Claude Code

srtab and others added 30 commits June 1, 2026 17:03
…mmit/MR

Move the one true workspace into the daiv-sandbox container under /workspace
(repo at /workspace/repo, seeded skills at /workspace/skills, per-run scratchpad
at /workspace/tmp) and make the sandbox authoritative for both file and git state.

- SandboxFileBackend: a deepagents BackendProtocol that proxies file ops to the
  sandbox fs/* endpoints over DAIVSandboxClient — one RPC per op, no local mirror,
  root-configurable and bound at session start. Retires SandboxSyncer and the
  local patch-apply path.
- Dual-mode GitManager (for_sandbox / for_local factories): runs git either via
  run_commands in /workspace/repo (sandbox-authoritative runs) or as a subprocess
  against a GitPython clone (sandbox-disabled / repoless runs), behind one async
  _git runner with exit-code discipline so a failing git command raises instead of
  folding fatal text into its output. Push failures are classified into
  GitPushPermissionError / GitPushNetworkError. open_git_manager() builds the right
  mode from the run's session_id.
- Agent-owned publishing: new commit_changes / create_merge_request tools let the
  agent author its own commit and MR; commit, push and MR-API failures surface as
  tool-visible error Commands so the agent can recover rather than crashing the run.
  GitMiddleware nudges the agent to publish (bounded by MAX_GIT_NUDGES) and falls
  back to a direct daiv publish as a safeguard. GIT_SYSTEM_PROMPT gains an
  agent_owns_commit variant; raw git mutation in bash stays hard-blocked by policy.
- gitlab/gh tool output spills verbatim to a /workspace/tmp scratch file when it
  exceeds the inline cap. SlashCommandMiddleware split out of the skills middleware.
- fs_* client methods and Fs* wire schemas mirrored byte-identically to daiv-sandbox
  (enforced by the schema-drift consistency test); regenerated schemas.dump.json.

Requires the companion daiv-sandbox fs/* endpoints to be deployed.
Agent-level middleware hooks receive a langgraph Runtime, which has no
.config attribute (unlike the ToolRuntime used in git_platform.py). Reading
runtime.config would AttributeError at runtime on every chat turn. Read the
conversation thread_id from the run config contextvar via get_config()
instead, falling back to None (reuse disabled) outside a runnable context.
PR-review follow-ups:
- Wrap all Redis cache reads/writes (_cache_get/_cache_set/_cache_delete) so a
  cache outage degrades to a cold create+seed instead of crashing the agent run
  (the calls sat inside abefore_agent's except BaseException, turning a pure
  optimization into a hard Redis dependency).
- Log when get_config() has no runnable context so silently-disabled reuse is
  diagnosable.
- Fix session_exists docstring (404=>False, any other success=>True; 204 is the
  sandbox's current answer, not a client-enforced contract) and add cross-service
  contract pointers; reframe the TTL/reaper coupling as best-effort/self-healing.
- Add resilience tests: session_exists propagates non-404; _reuse_warm_session
  swallows httpx.HTTPError without dropping the mapping; cache read/write outages
  degrade gracefully; aafter_agent swallows a 404 from close_session.
Register SlashCommandMiddleware only when slash_commands.enabled, matching
the conditional-registration pattern used by the sandbox/web/deferred
middleware, instead of always registering it and short-circuiting inside
abefore_agent. When disabled the middleware is now never instantiated or run
rather than no-op'ing every turn.
Move the thread->session cache mapping out of SandboxMiddleware into a
new SandboxSessionStore (core/sandbox/session_store.py). This separates
the persistence mechanism (key scheme, TTL, best-effort cache I/O) from
the reuse orchestration (validate liveness, remember/forget) that stays
in the middleware. The store knows nothing of the sandbox client, so the
backing store can be swapped without touching the middleware.

Behavior is unchanged. Warm-session cache tests move to the store's own
unit suite; the middleware tests now drive a fake store and assert
orchestration only.
The branch added commit_changes/create_merge_request tools plus a nudge
loop so the agent published its own work. Revert to the prior behavior:
GitMiddleware commits, pushes, and opens/updates the MR via
GitChangePublisher when the agent's turn ends.

Keep the sandbox/fs infrastructure intact — the publisher still threads
session_id through open_git_manager so it runs git in sandbox or local
mode. Remove the git_publish tools, the aafter_model nudge,
MAX_GIT_NUDGES, and the agent_owns_commit prompt variant;
GIT_SYSTEM_PROMPT returns to the "committing is automatic" contract.
The revert dropped _is_unpublished and let aafter_agent call the
publisher unconditionally. The publisher only short-circuits on
"dirty OR diff-vs-base", so on an idle follow-up turn of a warm-session
chat (clean tree, but an earlier turn's commit is already pushed and
ahead of base) it would re-run — burning a diff-to-metadata model call
and doing a no-op push — where both main and the pre-revert branch
skipped.

Restore _is_unpublished as the gate: it adds the has_unpushed check that
distinguishes "already live" from "needs pushing", so publishing now
fires under the same circumstances as before. It is sandbox-aware
publishing logic, not agent-owned, so it belongs after the revert.
The gitlab/gh platform tools previously took an agent-chosen output_file
path and wrote oversized output to a sandbox scratch file via a
short-lived client, with bespoke inline truncation and auto-eviction.

Replace that with an output_to_file boolean that writes the full result
through the agent's deepagents filesystem backend, into the same
large_tool_results dir the FilesystemMiddleware auto-evicts to. The
tools are now closures built inside GitPlatformMiddleware capturing the
bound backend; the sandbox backend is wrapped in a DAIVCompositeBackend
to carry an artifacts_root under /workspace, and _bind_backend unwraps
it to bind the live session.

Write failures (raised, backend-rejected, or a missing tool_call_id)
surface as agent-visible error strings. Drop the now-orphaned
SCRATCH_PATH constant and the legacy truncation helpers.
…on (#1287)

* feat(sandbox): own the sandbox transport per-run in set_runtime_ctx

* refactor(sandbox): inject run client by construction; state-based session reuse

* refactor(git): move GitManager into automation layer, drop lazy import

* refactor(git): remove unused GitManager.commit_and_push_changes/checkout

* perf(git): batch publish reads into status_snapshot (<=2 round-trips)

* refactor(git): fold publish decision into publisher; inject run client into git path

* perf(git): run MR create/update and context-file suggestion off the event loop

* refactor(git): extract _effective_mr_iid helper for MR resolution

* refactor(git): harden publish path and sandbox client teardown

Address review feedback on the sandbox-transport-injection branch:

- Guard the run-scoped sandbox client teardown in set_runtime_ctx so a
  transport-close error can't mask the in-flight exception, and always
  reset the contextvar.
- Fold protected_branch_fallback_source into the frozen PublishOutcome,
  dropping the mutable publisher side-channel read by both callers.
- Remove the dead GitManager query methods (is_dirty / get_diff /
  has_unpushed / remote_branches) superseded by status_snapshot.
- Add status_snapshot error-branch tests; re-target the empty-results
  and no-index hard-error tests onto the surviving helpers.
- Fix the "read once" client comments (BaseManager is a second reader),
  annotate GitMiddleware.sandbox_client, and flag the possibly orphaned
  container in _session_exists.
…on handle (#1292)

* feat(sandbox): add run_commands to SandboxFileBackend

* refactor(git): thread bound sandbox backend through git/publish path

* refactor(sandbox): run bash through the bound backend; thread it to subagents

* test(sandbox): guard that the backend never advertises execution

* feat(sandbox): classify bash failures as transient or permanent

The bash tool degraded every transport/HTTP error to the same generic
"sandbox call failed" string, so the agent could not tell a momentary
blip (worth one retry) from a non-recoverable rejection (stop using the
tool). Introduce a BashFailure enum that maps httpx errors to TRANSIENT
(no response, or a retryable status: 408/425/429/5xx) vs PERMANENT
(auth, session-gone, bad-request, not-implemented), and return distinct
agent-facing guidance for each. The transient message is byte-stable so
the system prompt's "two identical error strings => stop" backstop still
fires when a retry fails the same way.

Non-httpx failures (malformed 200 body, unbound-backend RuntimeError)
are left to propagate as loud wire/programming bugs.
Tool loading called MultiServerMCPClient.get_tools(), which gathers all
servers with no timeout, so one server whose handshake hangs blocked the
entire toolset build and froze every chat and run.

Load each server independently via asyncio.wait_for with a per-server
timeout (MCP_TOOL_LOAD_TIMEOUT, default 30s); a server that times out or
errors is skipped instead of blocking the others. CancelledError still
propagates so outer cancellation is preserved.
…tives

Filesystem tools (ls/read_file/grep/glob/write_file/edit_file) now branch on a
structured FsError/FsErrorCode from the sandbox instead of free-form strings: a
missing path reads as "does not exist" (distinct from an empty dir / no match),
reading a directory or writing over an existing file routes the agent to the
right tool, and deletes are idempotent. Requires the matching daiv-sandbox
release with the structured fs/* error responses.

Path directives and subagent prompts are now derived from the run's working
directory (REPO_PATH in a sandbox, clone basename on disk) via a single
_resolve_working_directory helper, so the main prompt and subagents address the
same repo root. SandboxFileBackend._abs normalises the virtual root and
prefix-dropped repo slips onto the workspace/repo root.
…fence (#1293)

* feat(agent-fs): add TMP_PATH and disk-mode workspace fence permissions

* feat(agent-fs): add build_disk_workspace_backend for the unified /workspace namespace

* feat(agent): unify graph workspace root to /workspace with disk-only fence

* feat(agent-skills): upload global skills under /workspace/skills

* feat(agent-subagents): fence subagents to /workspace subtrees in disk mode

* refactor(agent): drop unused GLOBAL_SKILLS_* constants after /workspace unification

Remove GLOBAL_SKILLS_PATH and GLOBAL_SKILLS_ROUTE from constants.py now that
the skills middleware addresses skills via SKILLS_PATH (/workspace/skills) directly.
Update skills/services.py and associated tests to classify skill invocations
against SKILLS_PATH instead of the removed /skills virtual route.

* fix(agent-fs): allow read-back of offloaded-artifact dirs under /workspace fence

The disk-mode fence denied everything under /workspace except the three real
subtrees, which also blocked reading the offloaded large_tool_results/
conversation_history files. deepagents eviction and git_platform's
output_to_file write those through the backend directly (bypassing the fence)
and hand the agent the path to read back — so the read-back dead-ended on the
/workspace/** deny.

Add a read-only carve-out for the artifact dirs ahead of the deny (write stays
denied; the agent never writes there itself), mirror it in the explore
permissions, and pin the prefixes with a drift-guard test against deepagents'
computed values so a framework rename fails loudly.
When the model batches tool calls, LangGraph runs them concurrently, but
the sandbox serves one op per session (a Redis lock with a short wait)
and returns 409 "Session is busy" to the loser. The file backend's
raise_for_status() propagated that 409, aborting the whole run.

The agent-facing file tools (ls/read/grep/glob/write/edit + delete) now
catch httpx transport/HTTP faults and return a soft, agent-actionable
result (transient -> retry once; permanent -> tools unavailable) like the
bash tool already does, logging WARNING for transient and ERROR for
permanent so genuine faults still reach the logs/Sentry. The bash tool's
classifier now also treats 409 as transient (was permanent). The shared
transient/permanent classifier lives in core/sandbox/client.py to avoid
an import cycle.

Pairs with a daiv-sandbox change raising the per-session lock wait so
batched ops queue instead of failing fast.
* feat(codebase): add ephemeral GitLab clone token provisioning

* fix(codebase): degrade clone-token provisioning on transport errors

* feat(codebase): clone GitLab repos with ephemeral project tokens

* feat(agent): log git push auth failures for diagnosability

* docs: document GitLab ephemeral clone token behavior

* fix(codebase): Harden clone-token provisioning failure handling

Refine how clone-token provisioning degrades when GitLab cannot mint
a project access token:

- Negative-cache transient failures (network, 429, 5xx) for 5 minutes
  instead of an hour so a single blip doesn't park clones on the PAT.
- Name the real culprit when GitLab rejects the configured PAT (401)
  rather than claiming a benign fallback, and guard against a created
  token arriving without a secret.
- Raise ImproperlyConfigured when neither an ephemeral token nor a PAT
  is available instead of building an oauth2:None@ clone URL, and log
  per clone which credential was embedded.
- Expand the git push auth-failure message with expired-clone-token and
  branch-protection guidance; log push network failures too.

* chore(docker): Build sandbox locally and pin compose network name

Build the sandbox service from the sibling daiv-sandbox checkout with a
source mount so local development runs against the unreleased sandbox
the clone-token work depends on. Pin the default compose network to
daiv_default so sandbox-launched containers can attach to it by a
stable name, and pin the gitlab host via DAIV_SANDBOX_EXTRA_HOSTS since
gVisor can't use Docker's embedded DNS resolver.
Tool calls made inside subagents were indistinguishable from main-agent
ones in the logs. ToolCallLoggingMiddleware now prefixes each line with
the agent name read from the run config's lc_agent_name metadata, set
by create_agent(name=...) and deepagents' SubAgentMiddleware, falling
back to <unknown-agent> when it cannot be resolved.

Two contract tests pin the undocumented upstream key location and the
task tool's metadata isolation, so a langchain/deepagents upgrade that
breaks name resolution fails CI instead of silently degrading logs.
With the sandbox as the authoritative workspace, the local-clone diff the
SWE-bench harness read at run end was always empty. GitMiddleware now
optionally captures the working-tree diff (vs HEAD, untracked included)
at turn end via GitManager.get_diff and exposes it as model_patch in the
output state; evals/swebench.py reads it from ainvoke's result and fails
loudly if the wiring drifts.

Also:
- Refresh OpenRouter model list (GLM 5.1, MiniMax M3, Kimi K2.6) and
  default the eval harness to MiniMax M3.
- Bump swerebench dataset split to 2026_03 and sample size to 10.
- Coerce site_settings.agent_thinking_level into ThinkingLevel in
  RepositoryConfig so raw-string site settings stop triggering pydantic
  serialization warnings on every config load.
SWE-bench eval runs logged a spurious '404 Project Not Found' traceback
on every instance: GitMiddleware's pre-run MR lookup ran on the
commit-pinned (detached HEAD) checkout and asked the settings-default
platform about a repo living elsewhere.

- Short-circuit the lookup on detached HEAD: no branch, no MR.
- Create the repo client with the run's git_platform instead of the
  settings default, which could 404 or match a same-named repo's MR.
- SWERepoClient: get_merge_request_by_branches returns None (SWE runs
  have no MR concept); repo_host is now optional at construction so the
  kwargs-less factory path works, but get_repository fails fast without
  it so a wrong-host clone can't silently corrupt an eval.
- Add requests.RequestException to the soft-fail tuples in GitMiddleware
  and chat.repo_state: the platform SDKs are requests-based, so network
  blips surfaced as uncaught transport errors and crashed the run.
Runs that hit recursion_limit raise GraphRecursionError mid-flight,
skipping after_agent hooks (patch capture, sandbox teardown) and
discarding otherwise-finished work. StepBudgetMiddleware injects a
step-budget reminder into the in-flight model request when the run
nears the limit, escalating to a finalize-now notice close to the
hard stop. Implemented via wrap_model_call so it adds no graph node
and the reminder is never persisted to conversation state.
Replace the after_model no-op tool-call injection with a
wrap_model_call retry. The hook variant added a graph node to every
model/tools cycle, raising the superstep cost per turn from 2 to 3
and silently cutting the effective tool-call budget under
recursion_limit by a third. Retrying within the node costs zero
extra supersteps and keeps the synthetic no-op round-trip out of
the persisted history; after MAX_EMPTY_RESPONSE_RETRIES the empty
response is returned as-is so the loop ends instead of spinning.
The captured model_patch is a plain working-tree diff vs HEAD, so any
dirt present before the agent acts (e.g. seeding artifacts) poisons
every patch and can break eval grading. GitMiddleware now checks the
workspace at run start on capture_patch runs and exposes the dirty
file list as pre_run_dirty_files in state and on the LangSmith run
tree; the SWE-bench harness prints a warning next to the run. Adds
GitManager.get_changed_files (one batched round-trip, exact names
straight from git) and a dedicated SandboxGitProtocolError so degrade
paths stop swallowing bare RuntimeError programming bugs.
SWE-bench style evals check out a historical base commit of a full
clone; the upstream fix for the issue under evaluation is often
already merged, and agents were observed reading it via
git log --all on remote refs. Detach HEAD at the base commit and
drop branches, remotes, reflogs and non-ancestor tags after checkout.
Tags pointing at ancestors are kept so VCS-versioning tools
(setuptools-scm, hatch-vcs) still work for editable installs.
Models routinely send 'foo|bar'-style patterns to the literal grep
backends, get a silent zero-match back, and conclude the symbol does
not exist. When an aggregate grep finds nothing, none of the backends
errored, and the pattern contains unambiguous regex fragments, return
an explicit literal-semantics reminder instead of a bare no-matches.
The hint lives on DAIVCompositeBackend.agrep only: a per-sub-backend
error would abort composite aggregation and suppress real matches
from sibling backends.
Add execution-constraint and how-to-work guidance to the eval prompt
(verify reporter analysis, provision test deps in one step, prefer
minimal fixes, never revert the offending feature, one final check
then stop) and a matching one-batch dependency-install line to the
agent system prompt. Trim the langsmith-fetch trace limit to 7.
Mirror the sandbox `FsGrepRequest` (`case_insensitive`/`multiline`/
`head_limit`, `invalid_pattern`) and forward them through
`SandboxFileBackend.agrep` and the re-implemented composite routing.

Add `DAIVFilesystemMiddleware`, which overrides the grep tool with an
extended schema (regex always on, no literal mode, escaping for literals),
and rebind `deepagents.graph.FilesystemMiddleware` so `create_deep_agent`
builds it for the main agent (the exclude+re-add path is unavailable —
upstream lists it as required scaffolding). Remove the now-obsolete
literal-grep no-match hint. `head_limit` is enforced across aggregated
backends, not just per backend.
@srtab srtab self-assigned this Jun 13, 2026
Base automatically changed from feat/sandbox-filebackend-scratchpad to main June 15, 2026 23:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant