626 Commits

Author SHA1 Message Date
NCBM be8a0111ec core/prompting: answer a question with a notification reply
`ask` now reads its answer from the wait's source as well, which is what makes
the notification's reply field mean something: the reply typed into it is the
answer the caller gets. The readline path stays whenever the source offers no
answers, so line editing and Tab completion only go away when the human asked
for a surface that can answer — and a terminal answer still wins over one that
arrives through the notification.

The read itself is the poll-based one this module already had, so the same
"no deadline, wait for a line or an answer" rule and the same Ctrl+C handling
apply; a cancelled read raises `NonInteractiveError`, as the backend's own
cancellation does.
2026-10-21 20:02:56 +08:00
NCBM 35595d8b04 core/cli: raise the wait signal from every confirm prompt
Every place the CLI blocks on the human now announces itself: the single and
batched tool confirms, the command-denylist and `fetch_local` policies, a skill
write, a directory-access request, the `ask_user_*` questions and the
limit-continue prompt. `_setup_hooks` installs the notifier from `[notify]`, and
only for an interactive run — a one-shot run never waits on a human.

An answer that came back through the notification is read as if it had been
typed: `allow` / `deny` map onto the `y` / `n` the prompts already accept, and
the confirmation prompts switch from click's readline to the poll-based read
only when the notification actually offered answers — otherwise the terminal
experience is untouched. "Go to terminal" never reaches the prompt at all.

The stdin read behind the timed prompts now watches in 100ms slices instead of
one full-length `select`, which is what lets a notification answer end it; an
answer still loses to a human typing at the terminal. A `None` timeout waits
for whichever arrives first, and a non-pollable stdin still gives up at once.

`prompt_wait` announces only the outermost prompt of a nested read: a confirm
that asks a follow-up question while it is already on screen would otherwise
buzz the human a second time.
2026-10-21 20:01:38 +08:00
NCBM 036e0b40ad cli/notify: notify a waiting prompt through the desktop or the terminal
A prompt that has waited `grace_seconds` is now announced, and where the
platform allows it the notification carries the answer: allow / deny / review
for a confirmation, a reply field for an `ask_user_*` question. Picking
"review" withdraws the notification and keeps waiting at the terminal, and a
notification is dismissed as soon as the prompt returns — answered or not.

Two channels. The desktop one is the optional `notify` extra, run on its own
event loop in a daemon thread because a notification's callbacks only fire
while the loop that owns it is running (the library's synchronous wrapper does
not provide that). The terminal one needs nothing: it writes OSC 777 / OSC 9
to `/dev/tty` — so a redirect cannot swallow it — picking the protocol from the
environment and wrapping it for tmux / screen, falling back to a plain bell.
kitty speaks OSC 99 only, which is not implemented, so it keeps the bell.

Nothing here can break a prompt: a missing extra, a desktop that refuses, a
failed write all fall through to the next channel and never raise. Titles are
flattened and stripped of control characters, and the reason line (which can
name a command or a URL) only travels with `include_reason`.
2026-10-21 19:57:17 +08:00
NCBM 10402f5be6 core/config: add the [notify] section
`[notify]` decides what the CLI does when a prompt is waiting on the human:
`grace_seconds` (default 5s, roughly Claude Code's permission notice) before
anything is shown, `min_interval_seconds` between two notifications,
`channels` (`auto` = desktop then terminal), `actions` for the allow/deny/review
buttons, `include_reason` for the one-line reason, and `protocol` to pin the
terminal escape protocol.

The reason stays out by default because it names commands, argv and URLs, and a
notification outlives the prompt and may be mirrored to a lock screen. Unknown
keys degrade to the defaults, the way `[mcp]` does, and `reload()` re-reads the
section.

Desktop notifications need the optional `notify` extra (`desktop-notifier`),
which is the only new dependency and stays off the default install.
2026-10-21 19:53:19 +08:00
NCBM 557300ff4f core/prompting: let a host notify and answer a waiting prompt
A prompt that blocks on the human now announces itself through one process-wide
hook: `prompt_wait(kind, title, detail)` wraps `ask`, `ask_secret`, `choose` and
`confirm` and yields the answer source the host offers. `kind` names the prompt
family (`ask`, `secret`, `choose`, `confirm`, and the CLI's own `policy` /
`access` / `skills` / `limit`) so a host can pick a surface, while `title` stays
short enough to show anywhere and `detail` carries one line the caller has
already redacted. No hook installed means no behaviour change, which keeps
library hosts and the test suite exactly as they were.

The read side of the seam is `read_line_with_timeout`: an optional `source` ends
the read with the host's answer (echoed on the prompt line like a typed one) and
a `None` timeout now means "wait until a line or an answer arrives" instead of
giving up. Stdin keeps priority, so a human typing at the terminal is never
raced by a notification answer that shows up at the same moment.

`reset_prompting` clears the hook along with the output hook and the pause
factory.
2026-10-21 19:52:03 +08:00
NCBM aafbc207a3 test: make the suite run under Windows
Windows spells a path with backslashes, and three fixtures took the POSIX shape: a config value written into a TOML basic string read `C:\users\…` as an escape, tool arguments built with an f-string broke the same way, and a root was compared as `str(path)` against `/srv/skills`. Paths are encoded with `json.dumps` (whose escaping is valid TOML and valid JSON) and compared as `Path` objects, so the same root matches in either spelling.

`test_interrupt.py` delivers SIGINT with `os.kill(pid, SIGINT)`, which is `TerminateProcess` on Windows, and the router itself needs `add_signal_handler`: the module is skipped there.

`python scripts/wine_pdm.py ci` is green: ruff, basedpyright 0 errors, 1156 passed / 30 skipped.
2026-10-21 19:28:49 +08:00
NCBM 4c597274e7 docs: describe YOLO skipping YOLO-tagged confirms
Session YOLO skips the soft confirms of `YOLO`-tagged tools — interpreter runs included — while untagged tools keep prompting and a command that escalates privileges keeps its own confirm through `DangerReason(yolo_skip=False)`. A plugin now also sees that answer shape where the danger classifier is documented, and the round's aggregated prompt leaves the covered calls out.
2026-10-21 19:10:56 +08:00
NCBM 8ec63aa6a2 core/agent: let YOLO mode skip YOLO-tagged soft confirms
`--yes` / `/yolo on|off|once` clears the prompt of a `YOLO`-tagged tool: interpreter runs (`run_argv`, `run_argv_batch`, `open_pty`), the file mutations (overwrite / copy / move / delete) and `fetch_local`'s cleartext and mutating requests. A tool without the tag still asks, and hard path/command denylists stay untouched.

`classify_danger` now answers with a `DangerReason` — the reason text plus `yolo_skip` — so one call can refuse that skip: a command that runs through `sudo` / `su` / `doas` / `pkexec` (as the program or a wrapper) keeps its confirm whatever YOLO mode says. It never adds a reason of its own, so the program the denylist gate already asks about is not asked twice.

`prepare_batch_confirm` leaves the covered calls out, so a skipped call neither prompts on its own nor takes a slot in the round's aggregated prompt, and one escalating step keeps the whole batch in front of the human.
2026-10-21 19:10:46 +08:00
NCBM 029ac47350 docs: describe image input across the three surfaces
`doc/images.md` holds the message shape, how `@path` attaches a file, the
per-surface mapping, the provider limits, what a live probe against DeepSeek
confirmed (`tool_result` and `function_call_output` images work, `detail: low`
is 3.4x cheaper, an assistant-message image is a 400), and why the Files API is
not used.

AGENTS.md gets the invariants and the mention/config keys; `doc/architecture.md`
and the example TOML point at it.
2026-10-21 18:53:47 +08:00
NCBM 154509b929 core/cli+config: attach files to a turn with @path mentions
Typing `@shot.png` in the line attaches that picture to the turn — no separate
command — and `@notes.txt` appends the file as text under a `--- @path ---`
header, capped like a tool result with a resumable `truncate_token`.
`@"my shot.png"` is the quoted form, and Tab completes the path after `@`
(directories end in `/`, a name with a space completes quoted).

An image is skipped, with a note naming the key to set, on a model without
`image_in`; a missing or denylisted path is a note too, and the turn still goes.
The default catalogs mark what can take a picture (`deepseek-flash` yes,
`deepseek-v4-pro` no, the OpenAI defaults yes).

`Agent.run` takes the parts, so the turn is stored with its images and shows up
as `[image]` wherever only prose is wanted.
2026-10-21 18:53:47 +08:00
NCBM e9ee71e853 core/tools: show images instead of describing them
New `view_image` reads a local picture (JPEG, PNG, GIF, WebP, sniffed from its
bytes) and hands it to the model as an image part, with an optional `detail`
level — `low` measured 3.4x cheaper on a 1200x900 picture. `read_file` now
points at it instead of returning decode noise.

`fetch_local` returns an image response as the picture itself, and MCP image
content blocks become image parts instead of `[image: ...]` placeholders, so a
screenshot server finally reaches the model.
2026-10-21 18:43:29 +08:00
NCBM c6a94e0b66 core/agent: let a tool result carry images beside its text
`ToolOutput` pairs the model-facing text with the images a tool produced.
`ToolRegistry.execute` keeps its text-only contract; `execute_output` returns
the pair and the loop stores the images on the tool message, so a screenshot
tool can answer in pixels instead of a hex placeholder.

Only the text is capped by the tool-result limit; the char estimator now counts
a present image at its 1024-token ceiling, because the shrinkers never drop one.
2026-10-21 18:43:29 +08:00
NCBM d7293eeb16 core/lmproto+agent: carry image parts on user and tool messages
A user turn's `content` may now be a part array (`ChatTextPart` +
`ChatImagePart`), which chat completions take as-is; the Anthropic and
Responses bridges translate it into `image` blocks and `input_image` parts.

A tool result keeps its text in `content` and carries `images` beside it:
Anthropic and Responses put them in the result item itself (a live probe reads
both back), while chat completions cannot, so
`compat.chat_messages_with_images` moves them into the user turn after the tool
run. Prose-only readers go through `content_to_text`, which renders an image as
`[image]`.

`agent.images` sniffs the real format from the bytes and builds the data URL,
capped at 8 MiB inline because every stored message is re-sent on each round.
2026-10-21 18:43:29 +08:00
NCBM a0aba700e2 release: plyngent 0.10.0 v0.10.0 2026-10-17 17:47:03 +08:00
NCBM 729627eee9 docs: spell out the turn and round model
doc/turns-and-rounds.md defines the two nested units the loop is built on: a round is one request→response plus the tool batch it asked for, a turn is one user message through the explicit stop (0..N rounds), and a session is a stored sequence of turns. It records what bounds each unit (max_rounds, truncation continues, retries), which work is round-scoped (request assembly, context reduction, the aggregated soft confirm) and which state resets per turn (read-before-edit, yyy auto-continue, /yolo once, the todo touched flag, usage totals) versus what outlives it (rows, todo stack, grants, reminder bands).

Linked from the architecture doc, with the short version in AGENTS.md.
2026-10-17 17:37:22 +08:00
NCBM 78b764f70e docs: describe the aggregated round confirm 2026-10-17 17:32:44 +08:00
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 f4c0c45403 docs: describe the compact summarizer tool notice 2026-10-17 17:13:49 +08:00
NCBM f5cff9d90d core/agent: tell the compact summarizer that tools are unavailable 2026-10-17 17:13:42 +08:00
NCBM eef1d3cd29 docs: describe fetch/fetch_local and the DeepSeek search default 2026-10-17 17:08:11 +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 37e130a7dc docs: describe the DeepSeek Search API backend 2026-10-17 16:52:40 +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 2054423ebb release: plyngent 0.9.1 v0.9.1 2026-10-16 22:31:50 +08:00
NCBM 3c8ca5fcde docs: describe the truncated-round continuation 2026-10-16 22:29:35 +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 3c555278d5 docs: describe the disabled-thinking fallback 2026-10-16 22:16:35 +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 06a5bafab7 docs: describe the adaptive-thinking retry 2026-10-16 22:12:09 +08:00
NCBM 9156df2b02 core/agent+anthropic: retry a refused thinking budget as adaptive thinking 2026-10-16 22:12:00 +08:00
NCBM 3e7acbaf18 release: plyngent 0.9.0 v0.9.0 2026-10-16 19:46:54 +08:00
NCBM c55c5be779 deps: refresh the lock (sqlalchemy 2.1.4, platformdirs 4.13.3, tomlkit 0.15.2, pywinpty 3.1.0) 2026-10-16 19:46:44 +08:00
NCBM b0afed612f docs: describe the same-file write ordering of a batch 2026-10-16 19:37:38 +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 ab6b5d590c docs: describe the escape error's access mode 2026-10-16 19:29:20 +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 a4f852eec6 docs: describe the batch command listing 2026-10-16 19:08:24 +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 60dd96329d docs: describe the numbered todo listing 2026-10-16 18:53:10 +08:00
NCBM 3bc585ad3d docs: describe the write_file overwrite rule 2026-10-16 18:52:59 +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 64a8cf7062 docs: drop the /clear references 2026-10-09 21:15:55 +08:00
NCBM 6807055b9f cli/slash: remove the /clear command 2026-10-09 21:15:48 +08:00