feat(agent): integrate mirothinker deep_research tool#136
Merged
Conversation
New HTTP tool that delegates broad, multi-source research questions to the MiroThinker deep-research API over SSE. It returns a structured result (status / content / report_ref / usage) and also saves the full report under <workspace>/deep_research/. Registered opt-in: only when tools.deep_research.apiKey (or MIROTHINKER_API_KEY) is set, since it is a paid, minute-scale engine. Defaults to the mini model; endpoint and model are overridable via config. On local interactive surfaces (CLI REPL / TUI) the tool streams its progress live (coarse search/read milestones as Reasoning) and delivers the finished answer inline, handing the main model only a compact receipt so it relays a short ack instead of re-emitting or rewriting the answer (which would drop the citations, as observed with a plain full return). This is gated by inline_tool_stream, wired on for CLI/TUI and off for channels/gateway, which keep the full-return path. Known limitations (follow-ups): the tool blocks the turn for the whole research duration, so on a channel the conversation is frozen until it finishes; async background delivery and multi-provider backends are planned separately. Co-authored-by: Claude (claude-opus-4-8) <noreply@anthropic.com>
On a channel (weixin, ...) deep_research used to block the whole turn for
the minutes-long research, freezing the conversation. It now runs async:
the tool submits to the MiroThinker /v1/responses background endpoint and
returns immediately, so the model just acks ("started, I'll send it when
ready") and the user keeps chatting. When the research finishes,
DeepResearchManager delivers the finished report back to the conversation
verbatim -- through the scheduler lane (so the session write stays
serialized with real turns) and skipping the model (so the report and its
citations are not rewritten).
New primitive: TurnRequest.deliver_text plus a run_turn short-circuit that
emits the text as-is, records it as an assistant message, and indexes it
without calling the model. Consecutive plain-assistant messages are
coalesced at assembly so the ack and the delivered report do not trip
providers that reject adjacent same-role messages. The async transport is
wired gateway-only; CLI/TUI keep the inline-streaming path and any other
surface keeps the full synchronous return.
Known limits (accepted for now): a gateway restart drops in-flight
research (no resp_id persistence yet) and delivery to a closed channel is
dropped without retry.
Co-authored-by: Claude (claude-opus-4-8) <noreply@anthropic.com>
The deep_research tool (MiroThinker) shipped with a config schema but no way to set it up short of hand-editing config.json. Add two CLI entry points, sharing one interactive flow: - `raven deep-research enable/get/reset` -- configure, inspect (key redacted), or clear it. `enable` with --key/--model writes directly; with no flags it runs the interactive wizard. - An onboard Step 5 (opt-in) that reuses the same flow. Key validation is a free `GET /v1/models`, NOT a chat completion: MiroThinker's chat endpoint always runs a real, minute-scale, billed research, so a chat "ping" would both time out and cost money. The URL reuses the provider models-test /v1 de-dup so the default base (already ending in /v1) is not doubled. Config lands camelCase (tools.deepResearch.apiKey) through a validate + model_dump(by_alias=True) round-trip. A present-but-unparseable config makes the write refuse and leave the file untouched instead of overwriting it (update_tools only; the shared read path is unified in a follow-up). Co-authored-by: Claude (claude-opus-4-8) <noreply@anthropic.com>
A config-write command run against an existing but unparseable config.json
(e.g. one carrying // comments) wiped the whole file, leaving only the
command's own section. Root cause: every read-modify-write module had its
own raw reader that swallowed a JSON parse error and returned {}, so the
write path treated a broken-but-present file as empty and clobbered it.
Add a single shared reader in the config loader, read_raw_or_raise: it
returns {} only for an absent or empty file and raises ConfigReadError for
a present-but-unparseable one, so no writer can silently replace a real
config with just its slice. Converge all six raw read-modify-write points
(update.py, update_tools.py, update_channels.py, update_providers.py,
onboard _load_raw_config, tui_rpc config _load_config) onto it. ConfigReadError
is a plain Exception, not a RuntimeError, so the CLI's broad
`except RuntimeError` OAuth-refusal guards do not intercept it and it reaches
the single run() handler, which prints a friendly message and exits 1; the
tui_rpc path maps it to ConfigValidationError. load_config still boots on
defaults for a malformed file but now warns loudly on stderr instead of
failing silently.
Raising on boot from load_config is left for a follow-up: it needs an atomic
save_config first.
Co-authored-by: Claude (claude-opus-4-8) <noreply@anthropic.com>
arelchan
previously approved these changes
Jul 16, 2026
The pre-commit ruff hooks reformatted eight files and applied two safe lint fixes (import ordering) that were committed earlier without running ruff format. Formatting and import order only; no behavior change. Co-authored-by: Claude (claude-opus-4-8) <noreply@anthropic.com>
arelchan
approved these changes
Jul 16, 2026
0xKT
added a commit
that referenced
this pull request
Jul 16, 2026
## Summary Bump version from 0.1.4 to 0.1.5 for release. Two feature PRs landed on main since v0.1.4: - #136 feat(agent): integrate mirothinker deep_research tool - #135 feat(tracing): in-tree tracing subsystem with decoupled adopter contract Only pyproject.toml and uv.lock change (version field synced via `uv lock`). ## Type - [ ] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [x] Other ## Verification - [x] Relevant lint / type checks pass locally Ran `uv lock` to sync the lockfile; commit-message lint passes (`check_commit_messages.py`). ## Risk - [x] Backward compatibility considered Version-only bump; no source changes. ## Related Issues N/A Co-authored-by: Claude (claude-opus-4-8) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Integrate MiroThinker as an async
deep_researchtool, end to end.feat(agent)): add thedeep_researchtool with CLI/TUI streaming.feat(agent)): adeliver_textturn primitive delivers the research result to channels verbatim (out-of-band), since a MiroThinker run is minute-scale and must not block the turn.feat(cli)):raven deep-research enable/get/resetplus an onboard step so users configure the tool without hand-editingconfig.json. Key validation uses the freeGET /v1/modelsprobe (a/chat/completionsping would trigger a full billed research run).fix(config)): every config-write command shared a raw reader that swallowed a JSON parse error and returned{}, so writing over a present-but-unparseableconfig.json(e.g. one with//comments) wiped the whole file down to just the command's own section. Found while testingdeep-research enableon a real config. A single sharedread_raw_or_raisenow raisesConfigReadErrorfor a present-but-unparseable file; all six raw read-modify-write points converge on it, and the CLI surfaces it once, cleanly, leaving the file untouched.Type
Verification
uv run pytest(config layer + deep-research/onboard/provider CLI + tui_rpc config + cli smoke): 340 passed.uv run ruff checkon all touched files: clean.Real-machine (isolated): async
deep_researchresult delivered to a WeChat channel verbatim; onboard step walked;deep-research enableagainst a malformedconfig.jsonrefuses with a friendly message and leaves the file byte-identical (verified withcmp).Relevant tests pass locally
Relevant lint / type checks pass locally
User-facing docs or screenshots are updated when needed
Risk
Related Issues
N/A
Reviewer notes:
feat(deep_research) that also carries thefix(config)bug it exposed; the squash commit takes thefeat(agent)title and GitHub collects bothCo-authored-bytrailers.uv run pyteststill shows env-dependent failures (sandbox VM, TUI Ink/Node E2E, missing channel SDKs) that predate this branch; the tests covering the code changed here (340) pass. A base comparison to confirm none are new is still pending.