626 Commits

Author SHA1 Message Date
NCBM 287b838db9 docs: drop the multi-tenant router plan 2026-10-09 21:15:07 +08:00
NCBM d3f8132b5d core/agent: note the live-probe result for the echoed blocks
A search round goes back with and without a local `tool_use` after its blocks: the endpoint takes both, and the model answers from the results it already holds. With the blocks stripped it calls its own earlier answer fabricated instead. The note sits where the ordering decision lives, so the shape needs no re-probe.
2026-10-09 20:15:38 +08:00
NCBM b0116a67c1 docs: describe the kept provider blocks 2026-10-09 20:09:43 +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 279cbe9389 core/tools: document fetch's 0-based body offset 2026-10-09 19:46:09 +08:00
NCBM 38e2dd58b0 core/config: state the 0-based offsets of skill_read and fetch 2026-10-09 19:46:02 +08:00
NCBM a89543fd35 release: plyngent 0.8.0 v0.8.0 2026-10-07 00:51:49 +08:00
NCBM 9296242881 ci/lint: ignore PLR0917 like PLR0913 2026-10-07 00:47:56 +08:00
NCBM 5a7bd55f2d lmproto: keep None last in the optional unions 2026-10-07 00:47:20 +08:00
NCBM c56449bfce deps/dev: upgrade ruff to 0.16.10 and basedpyright to 1.40.2 2026-10-07 00:47:03 +08:00
NCBM 9fc74e1866 deps: refresh the lock (msgspec 0.22, sqlalchemy 2.1, niquests 3.21.2) 2026-10-07 00:43:54 +08:00
NCBM 6e8368c169 docs: record the complexity gate and the nested-function counting 2026-10-07 00:40:31 +08:00
NCBM e0e19fde8f ci/lint: drop PLR0912 as a duplicate of C901 2026-10-07 00:40:08 +08:00
NCBM 9fac1ecfc0 core/agent+cli: give each C901 exemption a reason 2026-10-07 00:39:30 +08:00
NCBM 432b703b54 docs: describe what a repeated reasoning text does
The ``responses_bridge`` bullet records that ``response.reasoning_text.done`` repeats the fragments: the loop streams each part once, and the round's message still carries the whole chain of thought back to the API.
2026-10-07 00:26:53 +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 933d5385b9 docs: describe the search sources
The ``web_search`` bullet lists both sources, the fallback order and the
snippet, and says the tool needs a non-empty ``search_sources``.
2026-10-07 00:09:52 +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 19ab86f590 docs: say which presets ask for provider_tools
The provider-tools bullet names DeepSeek next to OpenAI, since the default and
the effective-provider gate now cover both.
2026-10-06 23:59:07 +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 27a901083a docs: describe the local search tool
The tools list gains the ``web_search`` entry (what it returns, when the CLI
registers it), and the provider-tools bullet says DeepSeek's preset asks for no
hosted tool by default.
2026-10-06 23:45:41 +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 af232283a4 docs: describe a hosted search on the Anthropic surface
The ``messages_bridge`` bullet says a server tool is the surface's own business
(only ``tool_use`` becomes a local call), the provider-tools bullet says the
Messages surface declares its own definition, the ``anthropic/model.py`` bullet
lists the blocks that now decode, and the file map says as much.
2026-10-06 23:36:41 +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 88b93a8dfe docs: describe how a DeepSeek round goes back
The DeepSeek bullet says the chat client stamps every assistant round with the
`reasoning_content` thinking mode demands, and the `responses_bridge` bullet
says a round's calls stay contiguous ahead of their outputs and that a round
which kept no chain-of-thought sends a blank line.
2026-10-06 23:23:30 +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 19e247d739 docs: describe the wrapper command-line review
`env -S "…"` / `su -c "…"` hand a command line to a wrapper instead of running
a program, so the soft confirm now covers them alongside interpreters and
detached runners, with the text under its own `env -S:` label.
2026-10-06 21:54:28 +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 e05dc5127a docs: describe how a batch of tool calls keeps its rows
The note said a second call ends the open prefix as its own line; that row was
what made the first call of a batch print twice. Record the queued behaviour:
each call of a round prints one finished row, and only output that genuinely
takes the line still closes the prefix.
2026-10-06 21:38:13 +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 66b771f87d docs: use the current DeepSeek flash id in the samples
The default catalog and the sample config still named `deepseek-v4-flash`,
which DeepSeek keeps only as a compatibility alias. Spell out the mapping
Claude-* ids get server-side (`deepseek-v4-pro` / `deepseek-flash`) instead
of the `deepseek-v4-*` glob the flash rename made stale.
2026-10-06 21:22:55 +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 35dbd30328 docs: describe the thinking-strength settings 2026-10-06 21:11:28 +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 0c9bb2e067 docs: describe merged todo batches 2026-10-06 20:51:13 +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 49ddf1c3c1 cli: correct the auto-retry schedule in the REPL help
The help said 10s/20s/30s; the delays are 5s/10s/15s/20s, then +10s each step.
2026-10-06 20:33:36 +08:00