Commit Graph

384 Commits

Author SHA1 Message Date
NCBM 0dd70ff31b core/cli: prompt for a round's tool calls once
prompt_confirm_batch_async answers a ToolBatchConfirmHook with one box that numbers every call the round wants to make (continuation lines stay under their entry). The prompt is the plain ``Allow these N tool calls?`` confirm: a deny offers the same one comment for all of them, a non-interactive host denies, and the next round asks again.

The ReplState registry installs the hook next to the per-call one, and format_tool_confirm_box now shares the box drawing with the batch header.
2026-10-17 17:32:44 +08:00
NCBM 86596633e8 core/agent: confirm a round's mutating calls in one prompt
ToolRegistry.prepare_batch_confirm pre-scans a round of tool calls, drops the ones that cannot run (unknown tool, undecodable arguments, missing required argument), and turns two or more soft reasons into a single ToolBatchConfirmHook prompt. The BatchConfirmDecision it returns is applied per call through execute(..., decision=...): allow, or deny with one comment shared by every call of that round.

_execute_tool_calls asks before it gathers, so a batch of moves or deletes no longer stacks one confirm box per call. A single request keeps the old one-call prompt, a host without a batch hook keeps the per-call behaviour, and the answer never outlives the round it was asked in.
2026-10-17 17:32:44 +08:00
NCBM f5cff9d90d core/agent: tell the compact summarizer that tools are unavailable 2026-10-17 17:13:42 +08:00
NCBM 9742a1ae7a core/config: default the local search to DeepSeek only 2026-10-17 17:06:56 +08:00
NCBM cfe36b024b core/tools+cli: split fetch into remote fetch and fetch_local 2026-10-17 17:06:43 +08:00
NCBM cb19652b2a core/tools+config: search through the DeepSeek Search API 2026-10-17 16:52:31 +08:00
NCBM 7a4205a588 core/lmproto: add the DeepSeek Search API client 2026-10-17 16:52:20 +08:00
NCBM 3cd95504c6 core/agent+cli: keep a round cut off at the output limit 2026-10-16 22:29:25 +08:00
NCBM f39ec5fb1a core/agent+anthropic: drop the thinking block a model refuses to disable 2026-10-16 22:16:25 +08:00
NCBM 9156df2b02 core/agent+anthropic: retry a refused thinking budget as adaptive thinking 2026-10-16 22:12:00 +08:00
NCBM 85b0318b77 core/agent+tools: apply a batch's same-file writes in call order 2026-10-16 19:37:29 +08:00
NCBM 1c84d63f37 core/tools+config: name the access mode in the workspace escape error 2026-10-16 19:29:11 +08:00
NCBM 6232925c2c cli/display: list a batch's commands above the '* Run batch' row 2026-10-16 19:08:14 +08:00
NCBM 2eb99a079d core/agent+tools+cli: number todos by listing order, not the internal id 2026-10-16 18:52:36 +08:00
NCBM ac33e92d33 core/tools+config: refuse write_file overwrite unless overwrite=true 2026-10-16 18:44:14 +08:00
NCBM 8729330120 core/skills: announce the skill tools in a notice, not a catalog 2026-10-09 21:40:45 +08:00
NCBM e72d2359d7 core/skills: read the filesystem live, drop the cache and reload 2026-10-09 21:40:38 +08:00
NCBM 1f04889449 cli/skills: open the skill directory instead of printing SKILL.md 2026-10-09 21:40:27 +08:00
NCBM 6807055b9f cli/slash: remove the /clear command 2026-10-09 21:15:48 +08:00
NCBM f89c8dec31 lmproto/openai_compatible: drop provider blocks on a chat wire
`provider_blocks` means something only to the surface that produced them (Anthropic `thinking` / server-tool blocks). A session that switched surface would otherwise send those dicts to a chat-completions provider as an assistant-message key it does not know; the request normalization drops the field, on a copy, so the history keeps it.
2026-10-09 20:09:36 +08:00
NCBM 7a54642ecf core/agent: rebuild a streamed round's provider blocks
Streaming is the default path: `thinking` fragments, a `server_tool_use`'s `partial_json` query and a `web_search_tool_result`'s entries arrive as separate SSE events, so the blocks the round has to hand back exist only as fragments. The dispatch rebuilds each one from its start payload (field for field, under the stream's own names) plus the fragments that follow, and passes it to the loop as a chunk; the loop copies it onto the round's message, where a round that kept only those blocks counts as payload rather than an empty completion.

The thinking text streams as reasoning as it arrives — the search query and the signature go back with the round only.
2026-10-09 20:09:29 +08:00
NCBM 2fdbc3ce5d core/agent: keep a Messages round's provider blocks on the assistant message
DeepSeek's Anthropic compat answers a hosted search inside the turn: the response carries the `thinking` that picked the query, the `server_tool_use`, and the `web_search_tool_result`. The response mapping kept text and tool_use only, so the round lost its evidence — and the `thinking` signature, which only the provider can verify, could not go back at all. `AssistantChatMessage.provider_blocks` carries those blocks as raw JSON now (`thinking` text *and* signature, the query, the result entries with an `encrypted_content` the API rejects when trimmed) and the assistant turn hands them back first, ahead of its text and tool_use blocks; `thinking` also lands in `reasoning_content`, so the chain of thought streams as reasoning like every other surface's.

`AnthropicRawContentBlock` names the provider fields a streamed block carries, and `AnthropicServerToolUseContent` keeps the `caller` metadata the wire sends.
2026-10-09 20:09:20 +08:00
NCBM 6c4c8b9328 core/tools: edit_lineno rejections name the read_file call 2026-10-09 19:46:16 +08:00
NCBM bf8203ae37 core/agent: stream a repeated reasoning text only once
DeepSeek's Responses stream sends ``response.reasoning_text.delta`` fragments and then ``response.reasoning_text.done`` carrying the whole chain of thought those fragments spelled out. The loop folded a full delta into the round — it replaces the fragments, and that is what the assistant message hands back to the API — but streamed every delta verbatim, so the chain of thought was printed twice: once as it arrived and again as the ``.done`` text.

A full delta now streams only a tail the fragments did not cover, and nothing at all when it merely repeats them; a round whose fragments never arrived still streams the full text once. The message keeps the complete text either way.
2026-10-07 00:26:38 +08:00
NCBM 4b5f67f5f3 cli/display: render web_search as a pretty line
``web_search`` is composed by the CLI host rather than the builtin catalog, so it had no ``_PRETTY_LINES`` entry and fell back to the ``[tool] …`` / ``[tool ok] …`` whole lines. It now leads with ``* Web Search '<query>' `` (the query as a 60-character preview, like the ask and skill subjects) and lands on the hit count — or on ``(no results)`` when every source came up empty, an emptiness the tool reports as ``error: the search found nothing`` and which is therefore not shown as a failure. A real error still prints in red with its message.

The guard test that keeps a builtin tool from shipping without a renderer now checks this host-composed tool too, since a catalog walk cannot see it.
2026-10-07 00:20:41 +08:00
NCBM 2bb6e06171 core/tools: search a list of sources, DeepSeek's index then Bing
``search_sources`` (a provider list, ``["deepseek", "bing"]`` by default,
``[]`` to not offer the tool at all) names what the local ``web_search`` tool
searches with, in fallback order: ``search_in_order`` asks each source in turn
and keeps the first answer, so a source that errors — a rate limit, a rejected
key — or finds nothing hands over to the next instead of failing the tool. The
handler reports the failures only when every source came up empty.

The Bing leg answers with its RSS results (``?format=rss``), which need no key
and carry a description — the snippet DeepSeek's own index does not hand out —
and it goes through the ``fetch`` client, so the SSRF and host-grant policy
applies to it too (a host that binds tool instance state, as the CLI does).
2026-10-07 00:09:46 +08:00
NCBM e877df9d94 config: default DeepSeek to web_search and honour its provider_tools
``provider_tools`` reached the effective provider only for the OpenAI preset,
so a DeepSeek ``provider_tools`` in TOML was dropped on the way — and the preset
defaulted to none, leaving search unavailable for every convention.

DeepSeek now defaults to ``[{type="web_search"}]`` like OpenAI does (``[]``
turns it off) and its list reaches the agent: the Anthropic convention runs the
hosted search inside the response, the Responses and chat conventions get the
local ``web_search`` tool (see ``config.routing.local_web_search``).
2026-10-06 23:59:01 +08:00
NCBM a0d39bba6d core/tools: search through a local tool where a hosted one is ignored
Only DeepSeek's Anthropic-compatible surface runs a hosted ``web_search``: the
Responses and chat-completions surfaces answer "I can't browse the web" with
``{"type": "web_search"}`` and with the server-tool shape alike, so a DeepSeek
provider that talks them had no search at all.

``config.routing.local_web_search`` decides per provider: a DeepSeek provider
whose ``provider_tools`` ask for ``web_search`` and whose convention is not
``anthropic`` gets the local tool (``tools/net/search.py``), which asks the
search-capable endpoint behind the scenes and hands the model one ``title —
url`` line per hit — read a page with ``fetch``, the bodies stay server-side.
DeepSeek's preset asks for no hosted tool by default, so this stays opt-in.
2026-10-06 23:45:35 +08:00
NCBM e2ee937d8f lmproto: search through DeepSeek's Anthropic-compatible endpoint
DeepSeek runs ``web_search`` inside a turn — the request declares the server
tool, the response carries a ``server_tool_use`` per query and the
``web_search_tool_result`` it produced — so a search is one ``/messages`` call
whose hits are read out of the content blocks. The turn stops as soon as it has
searched (a live probe with 64 output tokens spent all of them before the search
and returned nothing, 512 answers with the hits).

Hits carry a title, a URL and an ``encrypted_content`` only the index can read,
so the hit list is all a caller gets: the page bodies stay server-side.
2026-10-06 23:45:29 +08:00
NCBM 21f59ea33c core/agent: count the prompt a search turn really sent
The compat opens with the question alone (``message_start`` reports 165 tokens)
and puts what the search then pulled in on ``message_delta`` (12468); merging
preferred the earlier value, so the CLI's context gauge and the auto-compact
threshold ran ~12k short of the request. The larger of the two wins now (output
tokens are the delta's cumulative count either way).
2026-10-06 23:36:35 +08:00
NCBM 45246bf949 lmproto: keep the thinking block's type on the wire
``omit_defaults`` dropped the block's defaulted discriminator, and a request
that carried thinking was answered with 422 "thinking: missing field ``type``" —
which is what the compat sent for any configured ``reasoning_effort``. The field
has no default now, so it always encodes (both call sites pass it).
2026-10-06 23:36:29 +08:00
NCBM 1fa41c9fde core/agent: run a hosted search as the surface's own tool
DeepSeek's Anthropic compat runs a hosted ``web_search`` itself and answers with
the blocks that search produced — ``thinking``, ``server_tool_use`` (the query)
and ``web_search_tool_result`` — so the content union that knew only text and
tool_use could not decode a live search turn. It carries them now, ``tools``
accepts the opaque server-tool dicts the surface declares, and
``web_search_tool_result.content`` stays loose on purpose: its
``web_search_result`` entries hold an ``encrypted_content`` the API demands back
unchanged (a trimmed result is answered with 422 "missing field
``encrypted_content``").

``provider_tools`` reached only the Responses surface, and the Anthropic one
wants its own definition of the same intent: a bare ``web_search`` becomes
``{type="web_search_20250305", name="web_search"}`` (the server tool Claude
declares and DeepSeek's compat reads, keeping a ``max_uses`` cap or any other
key), merged after the local function tools.

Such a tool is the server's: it streams ``input_json_delta`` fragments like a
call, and reading them as one asked the registry for a tool the server had
already run. Block types are now tracked, so only a ``tool_use`` block becomes a
tool call; the search blocks and ``thinking`` are skipped by the response
mapping (the answer the model wrote already used them) and never executed.
2026-10-06 23:35:46 +08:00
NCBM 0da6e8da8d lmproto: give every DeepSeek assistant round its reasoning field
Thinking mode checks the field per round, not per call: an answer whose message
carried no `reasoning_content` came back 400 with the wording a call gets ("the
`reasoning_content` … must be passed back to the API"), while an empty string
satisfies the check. The injection now touches every assistant message instead
of only the ones carrying `tool_calls`.
2026-10-06 23:23:24 +08:00
NCBM c06fb57e72 core/agent: return each round's reasoning in the Responses input
A request carrying `tools` must return every round's chain-of-thought — "必须完整
回传 `reasoning_content` … 即使该轮模型未实际进行工具调用" — and the API merges a
`reasoning` item into the assistant message its round forms: a missing item and
an empty `reasoning_text` are both answered with "The `reasoning_text` in the
thinking mode must be passed back to the API". Each round now goes back as its
reasoning, its text, then its calls, with a blank line standing in when the
round kept none (a `rollback_tail` carrier, a forged todo call, the seed a
compacted session starts with) — never words of the bridge's own for a round
that may genuinely have reasoned nothing. A round's calls stay contiguous ahead
of their outputs: an interleaved batch reads as a second round without
reasoning and is rejected the same way, and an output recorded after another
item moves up beside its own call.

The reasoning is read back from the item's `content` parts, the only part the
API documents for `reasoning` items (`summary` and `encrypted_content` are not
supported).
2026-10-06 23:23:18 +08:00
NCBM 932b4f0a7f core/agent: keep a rolled-back turn's reasoning with its calls
A failed turn keeps the calls that already ran as a bare pairing carrier, and
clipping the chain-of-thought off that carrier sent the call back to the API
with no reasoning at all — thinking mode refuses a call whose reasoning never
came back. The carrier keeps the reasoning and drops only the text.
2026-10-06 23:23:12 +08:00
NCBM 135c28a622 core/tools: review a wrapper option that carries a command line
`env -S "rm -rf /"` ran a command nothing looked at: the option belongs to the
wrapper, so the scan skipped it and the denylist found no program to block while
the soft confirm had no interpreter to review. Collect those values — `env
-S`/`--split-string` and `su -c`/`--command`/`--session-command`, jammed
`--opt=value` and `-Svalue` forms included — and confirm the call the way an
interpreter's `-c` one-liner is confirmed, printing the text under its own
`env -S` label. Recursing into the text for a hard denylist is left out for now:
the review asks the human, who sees the command.
2026-10-06 21:54:18 +08:00
NCBM 2b3b2073a0 core/tools: only value-taking options may swallow the program
The wrapper option table listed flags as if they consumed the next token, so
the program behind them was skipped: `exec -c git -c init.defaultBranch=main
init` resolved to `init` and the policy denied it as a denylisted basename,
while `exec -c rm -rf /`, `command -p rm -rf /` and `command -v rm` hid `rm`
from the denylist completely and `exec -c python -c …` lost the interpreter
review. Drop the entries that take no value — `exec -c`/`-l`, the whole
`command` entry, `pkexec --disable-internal-agent`, `su -p`, and `xargs -e`
(its argument is optional, and the jammed form needs no entry) — so the
resolved program is the one that really runs.
2026-10-06 21:50:21 +08:00
NCBM 768b310a0b cli/display: hold a batch's later prefixes until the open row is done
A round that asked for several calls printed the first one as an abandoned
half-line: the next prefix closed the still-open row (a bare newline), the
result then printed whole and repeated that prefix, and the rest of the batch
was suppressed. The model edits files in batches, so the duplicate row showed
up there most. Queue the later prefixes instead: each one opens its own row
once the row before it has taken its detail, which keeps one finished row per
call in call order and leaves the bytes identical to the non-interactive path.
Only output that genuinely takes the line — a prompt box, streamed text, an
error — still ends the open prefix as its own line. The cursor is still never
moved.
2026-10-06 21:38:03 +08:00
NCBM ec5340bdce config: rename the DeepSeek flash model to deepseek-flash
DeepSeek dropped the `v4` prefix from its flash model, and the old id stays
valid only as a compatibility alias. Seed the default catalog with the current
name so a provider that omits `models` no longer offers a deprecated id, and
follow the rename in the wire fixtures and the minimal config template. An
explicit `deepseek-v4-flash` in TOML still passes through unchanged.
2026-10-06 21:22:35 +08:00
NCBM 60bc5957a7 cli: resolve thinking settings for the chat session 2026-10-06 21:11:28 +08:00
NCBM fb8ef88d9f core/agent: send the resolved reasoning effort with each request 2026-10-06 21:11:28 +08:00
NCBM 445aba7e5f lmproto: carry thinking options on the wire 2026-10-06 21:11:28 +08:00
NCBM ffd4dbbe26 config: resolve thinking strength across agent, provider, and model layers 2026-10-06 21:11:28 +08:00
NCBM 7cd5fcc576 cli/display: merge a batch of todo calls into one line
A round that updated its todo list printed one header plus a full stack
render per call, so the same list appeared several times. When a batch only
touches todo tools, print one `* Todo x<N>: Push, Update` line and the last
result, whose text already ends with the final stack. A batch holding any
other tool keeps the per-call lines, and a todo call never prints a prefix
at call time (it would dangle when the batch is merged).
2026-10-06 20:51:07 +08:00
NCBM 7d9b979604 core/agent: record every tool result before yielding them
A caller that goes away mid-batch (cancel, display error) left the results of
tools that had already run out of history, so the retry ran them again. Append
the collected results first, then emit the events.
2026-10-06 20:33:30 +08:00
NCBM cecd5e5ae3 core/agent: keep executed tool results when a failed turn is rolled back
A tool batch counted as committed only once every call had its result, so a
batch cut short was dropped whole and the retry ran tools that had already
had effects. Keep the calls that did report a result: their assistant row
stays as the pairing carrier (a request must answer every tool_calls id),
without the text of the unfinished turn, and the calls without a result are
absent so the retry re-issues them instead of assuming they ran. The kept
rows go to the store right away, and a failed rollback write is not allowed
to mask the failure being rolled back.
2026-10-06 20:32:09 +08:00
NCBM c7680491c9 core/tools: denylist power-management subcommands and privileged programs 2026-10-06 19:55:14 +08:00
NCBM 7862a555bc core/tools: review interpreters and detached runners past wrappers 2026-10-06 19:44:29 +08:00
NCBM 9501efb3d7 core/tools: block a denylisted program behind env wrappers and run launchers 2026-10-06 19:42:06 +08:00
NCBM 6453a8ac89 core/tools: resolve the program behind env wrappers and run launchers 2026-10-06 19:41:17 +08:00