Skip to content

feat(agent): integrate mirothinker deep_research tool#136

Merged
0xKT merged 5 commits into
mainfrom
feat/mirothinker_deepresearch_tool
Jul 16, 2026
Merged

feat(agent): integrate mirothinker deep_research tool#136
0xKT merged 5 commits into
mainfrom
feat/mirothinker_deepresearch_tool

Conversation

@0xKT

@0xKT 0xKT commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Integrate MiroThinker as an async deep_research tool, end to end.

  • Tool + streaming (feat(agent)): add the deep_research tool with CLI/TUI streaming.
  • Async channel delivery (feat(agent)): a deliver_text turn primitive delivers the research result to channels verbatim (out-of-band), since a MiroThinker run is minute-scale and must not block the turn.
  • CLI config + onboard (feat(cli)): raven deep-research enable/get/reset plus an onboard step so users configure the tool without hand-editing config.json. Key validation uses the free GET /v1/models probe (a /chat/completions ping would trigger a full billed research run).
  • Config write-safety fix (fix(config)): every config-write command shared a raw reader that swallowed a JSON parse error and returned {}, so writing over a present-but-unparseable config.json (e.g. one with // comments) wiped the whole file down to just the command's own section. Found while testing deep-research enable on a real config. A single shared read_raw_or_raise now raises ConfigReadError for 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

  • Feature

Verification

  • uv run pytest (config layer + deep-research/onboard/provider CLI + tui_rpc config + cli smoke): 340 passed.

  • uv run ruff check on all touched files: clean.

  • Real-machine (isolated): async deep_research result delivered to a WeChat channel verbatim; onboard step walked; deep-research enable against a malformed config.json refuses with a friendly message and leaves the file byte-identical (verified with cmp).

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Related Issues

N/A


Reviewer notes:

  • Mixed-topic PR on purpose: it is a feat (deep_research) that also carries the fix(config) bug it exposed; the squash commit takes the feat(agent) title and GitHub collects both Co-authored-by trailers.
  • Full uv run pytest still 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.
  • The MiroThinker test key used for real-machine checks is isolated and is not committed anywhere in this branch.

0xKT and others added 4 commits July 16, 2026 20:25
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>
@0xKT
0xKT requested a review from arelchan July 16, 2026 13:05
arelchan
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>
@0xKT
0xKT merged commit 08581cf into main Jul 16, 2026
8 checks passed
@0xKT
0xKT deleted the feat/mirothinker_deepresearch_tool branch July 16, 2026 13:51
@0xKT 0xKT mentioned this pull request Jul 16, 2026
8 tasks
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>
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.

2 participants