Commit Graph

623 Commits

Author SHA1 Message Date
NCBM 81d443cf40 cli: answer a cancelled slash command at the boundary 2026-11-02 16:30:12 +08:00
NCBM 770e08e56e cli/interrupt: rebind the router to the loop that owns it 2026-11-02 16:29:59 +08:00
NCBM 0d71923472 tools: run batch continuations where their parent ran 2026-11-02 14:52:57 +08:00
NCBM 9027901168 tools: check every prompting tool's arguments before the prompt 2026-11-02 14:47:50 +08:00
NCBM 809715bff6 docs: catch up with the batch grammar and the pre-prompt check 2026-11-02 14:35:02 +08:00
NCBM 2729f12d26 tools: answer a call's shape before the confirm prompt 2026-11-02 14:34:30 +08:00
NCBM 5c13d10989 tools: model batch steps as nested executions 2026-11-02 14:21:26 +08:00
NCBM b897bac52b release: plyngent 0.15.0 v0.15.0 2026-10-29 23:50:36 +08:00
NCBM 725caaada4 deps: refresh the lock (platformdirs 4.14.0, qh3 2.1.0, pytest 9.2.0, pywinpty 3.1.2, winrt 4) 2026-10-29 23:50:20 +08:00
NCBM dd678d213b docs: describe how a refusal reads
Which four answers a denial names, and that a timeout is never reported as a
decision.
2026-10-29 23:44:02 +08:00
NCBM 4bab780603 test: cover declined, timed out, cancelled and absent prompts
The four kinds and the words each produces, at the registry level and through
the four policy gates: a timeout names its deadline, a decline carries the
comment, and neither is reported as the other. The existing denial assertions
now expect the specific clause.
2026-10-29 23:43:49 +08:00
NCBM 8cbb98602e cli: report each prompt's own denial
The prompts knew the difference already — one branch printed "timed out",
another fell through to a plain no — and then threw it away by returning
False/None.

Each returns a `Denial` now: the timed policies name their deadline, a cancel
is its own answer, and a non-interactive run says nobody could be asked. The
batch decision and the skill write decision carry it too.
2026-10-29 23:43:20 +08:00
NCBM 4c67f6bb04 tools: carry the denial reason into the tool message
The four policy gates phrased their refusal themselves, and all four said
"declined or timed out". They read the `Denial` their prompt returned instead:
the command denylist, the private-host fetch policy, the skill write and the
directory-access request each name the answer they got.

`get_fetch_policy_confirm_hook` no longer coerces the answer to `bool`, which
would have reported a timeout as an allow.
2026-10-29 23:43:04 +08:00
NCBM 79ccf593f6 core/agent: name why a confirmation answered no
A refused tool call read "denied by user confirm", whether the human had said
no or the prompt had timed out — the model was told a decision it never got.

`Denial` (`agent/tools.py`) carries the kind — declined / timeout / cancelled /
no prompt — plus the deadline and the human's comment, and `Denial.clause()`
spells it once for every message that needs it. The soft gate and the batch
decision pass it through, so a timeout says its deadline and a missing hook
says nobody was asked.
2026-10-29 23:42:29 +08:00
NCBM 329fec8b3e docs: say YOLO never answers a directory-access request
Also stop crediting `yolo_allow` to `--yes`: the store is what a host fills
when it approves without keeping the grant.
2026-10-29 23:19:54 +08:00
NCBM 2614f8ca99 test: cover YOLO not answering the directory-access prompt
The answer is a human one under `--yes` (session scope, not a process grant),
and a run with no terminal to ask denies.
2026-10-29 23:19:46 +08:00
NCBM 7b3f55ea65 cli: leave a directory-access request to the human under YOLO
The access hook short-circuited on YOLO mode and answered its own question
with a process-only grant, so `--yes` (and `/yolo on`) reached any path outside
the workspace without asking. That grant is a filesystem capability, not one
risky call: the command denylist and a skill write are asked either way, and so
is this one now.

`yolo_allow` stays as the process-scoped store a host fills when it approves
without persisting; nothing auto-approves it any more. A non-interactive run
denies, like the other policy prompts.
2026-10-29 23:19:38 +08:00
NCBM b75ee88764 docs: describe the approval line
What a confirmed result leads with, which answers each policy path records,
and what never gets a line.
2026-10-29 18:30:00 +08:00
NCBM 6d7c239718 test: cover the approval line and the policy records
Registry cases (none / once / round / grant / YOLO / denial / error / handler
record / closest answer wins), the wire cap keeping the line, the three policy
paths that record their answer, and a pretty detail that ignores the header.
The existing confirm tests now assert the line they gain.
2026-10-29 18:29:28 +08:00
NCBM ed8013d6ce cli/display: read past the approval line when rendering a detail
The line is for the model, and the human already saw the prompt. A detail that
parses the result (write_file's char count) must not see a header in front of
it; the verbose `[tool ok]` view keeps the whole text.
2026-10-29 18:29:20 +08:00
NCBM 05166f2f76 tools: record the policy answers that let a call through
The command denylist, the private-host fetch policy and the skill write grant
asked the human and then said nothing about it in the result.

Each now records the answer for the running call: `user (process)` for this
call's override and `grant (process)` when an earlier one already covers the
program or host, `user (once|session)` / `grant (session)` / `config (policy)`
for a skill write. `request_directory_access` is unchanged: its own text already
names the level and scope it granted.
2026-10-29 18:28:48 +08:00
NCBM f9ec812275 core/agent: lead a confirmed tool result with how it was approved
A tool result said nothing about the answer that let the call run: the human
approved a delete, the model saw only "deleted x", and a resumed transcript
could not tell a YOLO auto-approval from a prompt.

The soft gate now returns an `Approval` (`user` / `grant` / `yolo` / `config`
with the scope that answer reaches) and `execute_output` leads the result with
`[approval] user (once)`. A handler that prompts on its own records its answer
through `tools/context.py` (`collect_approvals` around one call), and the one
line printed is the answer closest to a human. A call that needed no
confirmation, a denial and an `error:` result carry no line.
2026-10-29 18:28:13 +08:00
NCBM 1c2587db82 docs: say what the storage ceiling leaves readable
The comments claimed a result above the ceiling has no document, which is not
what the code does: it has one, and the dropped tail is what that document
cannot serve.
2026-10-29 15:17:33 +08:00
NCBM 828c2610cb test: cover the ceiling notice at the head and in the document marker
A cut body must lead with its notice, and a wire-capped cut body must say so
in the marker; a body the ceiling left whole must not.
2026-10-29 15:17:25 +08:00
NCBM 7abc7c2cb8 core/agent: lead a ceiling-cut tool result with its dropped-tail notice
The storage ceiling's notice trailed the head it was cut from, and the wire
cap sends the head: the only way to learn that a result had been cut was to
fetch a 2 MB body back through its document, which reads to the very end.

The notice now leads, says the tail is not readable, and `cap_tool_message`
names the same fact in the document marker, so neither the head nor the marker
presents a cut body as the whole output.
2026-10-29 15:17:12 +08:00
NCBM 7d29498dfa docs: describe the document model for tool results
AGENTS.md, README, and /status now describe the request-prep cap and the
document tools (`read_document` / `search_document`). /status also shows the
storage ceiling.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-10-29 14:24:35 +08:00
NCBM 140268f983 tools/truncate_token: scope the token cursor to file reads and mentions
Tool results no longer use a truncate token, so the shared `truncate_generic`
wrapper goes. `read_file` keeps its file token, and a user `@path` mention
keeps the memory token (its inlined text has no re-readable source).

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-10-29 14:16:40 +08:00
NCBM 7d6e367143 tools: keep tool output whole and let the request-prep pass cap it
run_argv, run_argv_batch, fetch, fetch_local, and the vcs tools now return
their output whole, so the request-prep cap trims it on the wire and names
its document. A fetched page or a long log reads on from the tool message,
with no re-fetch and no re-run.

The run_argv_batch run budget stays as an execution guard: it stops a
runaway batch from spawning further huge-output steps.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-10-29 14:13:36 +08:00
NCBM 63109ef9f2 core/agent: cap tool results in the request-prep pass
A tool result now stays whole in its message (up to a storage ceiling) and
is trimmed on the request copy by the request-prep pass, where soft compact
already runs. The trimmed copy names the document that holds the rest, so
every surface (chat-completions, Responses, Messages) shares one rule.

New [agent] hard_max_tool_result_chars bounds the stored body; the wire cap
stays [agent] max_tool_result_chars.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-10-29 10:04:32 +08:00
NCBM dfaaad563c tools/documents: add document tools over capped tool results
A tool result stays whole in its own tool message; a request-prep marker
names a document_id (the message's tool_call_id). read_document,
search_document, document_info, and list_documents resolve that id back to
the message body, so reading and searching a capped result needs no re-run.

The store binds on the host instance and reads the agent's live history,
which carries whole results and re-reads whatever a resume reloaded.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-10-29 02:36:36 +08:00
NCBM 4676b9af32 release: plyngent 0.14.2 v0.14.2 2026-10-26 00:54:34 +08:00
NCBM 865d314234 docs: say the terminal keeps its own prompt line
A prompt answered from the notification reads both surfaces; the terminal still
shows the keys it takes, which a button cannot say.
2026-10-26 00:53:08 +08:00
NCBM 9139655e54 test: cover the limit prompt line while a notification offers buttons
The answer now comes from the notification hook, and the terminal still has to
spell out the keys it accepts.
2026-10-26 00:52:58 +08:00
NCBM 741d1a8526 core/cli: keep the limit prompt line on the terminal
Once the limit prompt offered its three answers as notification buttons, the
read took the button path and skipped the prompt line the terminal used to
print: the human at the terminal saw the reason and then an indefinite wait,
with nothing saying what to type. It writes the line again, as every other
prompt that reads both surfaces already does.
2026-10-26 00:52:48 +08:00
NCBM 44b6ab90ed test: cover the scratch hint in the escape refusal
One test for the write path that now names `new_temporary_workspace`, one
parametrized over read and exec, which must not.
2026-10-26 00:52:38 +08:00
NCBM fb77fd676b core/tools: point a refused write at a temporary workspace
A path outside the workspace is usually rejected while the model only wants a
scratch file: the refusal named the grant tool, so the model had to ask a human
for access it never needed. A write now also names `new_temporary_workspace`,
whose path needs no grant. Reads and commands keep the grant hint alone.
2026-10-26 00:52:28 +08:00
NCBM a79039f06f release: plyngent 0.14.1 v0.14.1 2026-10-25 00:21:04 +08:00
NCBM ff656b800a test: cover a numbered read surviving a retry into a fresh attempt task 2026-10-25 00:19:14 +08:00
NCBM 2039d4f31f core: rebind the lineno tracker when retry continues a turn in a new task 2026-10-25 00:19:01 +08:00
NCBM 0e8d404642 release: plyngent 0.14.0 v0.14.0 2026-10-24 04:43:15 +08:00
NCBM 19707c3bd6 docs: drop "Go to terminal" and explain the bell
The answer tables lose the third button (two answers, or the prompt's own),
the popup's close and a body click both withdraw it, and the desktop section
says why every popup rings the terminal: the human is at another window by
construction — the announcement waits out the grace period — and no desktop
lets a program raise one.
2026-10-24 04:41:01 +08:00
NCBM ae20f5131f test: cover the bell at announcement and the two-button answer set
A confirmation offers Allow once / Deny and a prompt's own ``choices`` are
the whole button set; a popup that goes out rings the bell once, and
withdrawing it afterwards — by click or close — adds nothing and never
answers the prompt. A reply field's text is an answer whatever it spells.
2026-10-24 04:40:50 +08:00
NCBM a7abc50aa5 core/cli: drop the go-to-terminal button and ring the bell when a popup goes out
"Go to terminal" promised something no desktop can do — raise another
application's window — and the popup's own close button does as much, so it
was a third button that answered nothing. `REVIEW` leaves the answer
vocabulary; a click on the body (the platform's default action) or a close
still withdraws the notification and leaves the prompt waiting.

The bell now rings as the notification is shown. A prompt is only announced
after `grace_seconds` of nobody answering it, so the human is at some other
window — possibly another virtual desktop, where a popup is easy to miss — and
the terminal's bell is the only portable cue that says where to look. It rings
once per announcement; withdrawing the popup afterwards stays silent.
2026-10-24 04:40:39 +08:00
NCBM 7452224793 docs: name the workspace line, the popup deadline and the bell
The body table gains the workspace (last line, `~` for home, following a
rebind), the desktop section says which popups expire and why, and "Go to
terminal" is described as what it does: withdraw, ring, leave the prompt
waiting — no window is raised, because no desktop allows it.
2026-10-24 04:35:26 +08:00
NCBM 474bf57093 test: cover the workspace line, the popup deadline and the bell
That a home path under `~` names the chat and follows a rebind, that a
timed prompt hands its deadline to the popup while an untimed one keeps it,
and that going to the terminal — by button or by clicking the body —
withdraws the notification, rings the bell and leaves the prompt waiting.
2026-10-24 04:35:15 +08:00
NCBM ea0827ddfb core/cli: name the workspace, let the popup expire, ring the bell
A notification said which prompt was waiting but not which chat, and two
windows look alike. The body's last line is now the workspace the chat runs
in, with the home directory shown as `~`, and it follows a session resumed
into — or rebound to — another directory: `ReplState` retargets the installed
notifier instead of freezing the directory the process started in.

A timed prompt has a deadline, the popup did not, so it could stay on screen
announcing an answer window that had already closed. The deadline is now the
popup's own timeout (rounded up to whole seconds); a prompt without one keeps
the old "until dismissed" behaviour.

"Go to terminal" (and a click on the body) withdrew the notification and left
the prompt waiting, but nothing showed the human *where*. No desktop lets a
program raise another window — Wayland has no such call and the X11 ones need
a helper per WM — so the terminal rings its bell instead.
2026-10-24 04:35:04 +08:00
NCBM 39258b2670 docs: say what a notification now carries and when silence answers
The body is no longer "the reason or nothing": it is the question, the
reason behind `include_reason`, the object of the prompt and the deadline,
with the desktop keeping the lines apart and the terminal flattening them.
The tables gain the answers a question offers (a reply field and the way
back), the channels' click-to-terminal behaviour, and the secret prompt that
is announced nowhere at all.
2026-10-24 04:22:42 +08:00
NCBM f511c08afb test: cover the body a prompt notification carries
What a prompt declares (question, object, deadline), how the two channels
render it (lines apart on the desktop, one line in a terminal), that a long
reason goes before the deadline does, that a question keeps its reply field
*and* the way back to the terminal, that a body click leaves the prompt
waiting, that a reply spelling "review" is still an answer, and that a secret
is never announced.
2026-10-24 04:22:00 +08:00
NCBM 04d34b34f3 core/cli: name what a confirm is about, and when it denies itself
A tool confirm's notification said which tool asked and nothing else, and
the three timed prompts (denylist, directory access, skill write) hid the
clock: each of them denies on timeout, so a human reading the notification
late was answering a prompt that had already given up.

`_tool_facts` lifts the object out of the call — the program's name, or the
file's — and never an argument, a URL or a piece of code: those stay in the
reason, which only travels with `include_reason`. The timed prompts pass
their timeout as `expires_in`, and a batch names the tools it asks about the
same way instead of keeping them behind the reason.
2026-10-24 04:21:37 +08:00
NCBM 9ed4620579 core/cli: render a prompt's body from what it declares
The body was the reason line or nothing, so with the default
`include_reason = false` every notification was a bare title. It is now the
question, then the reason when the config allows it, then what the prompt is
about, and last the deadline — filled in that order until the budget is gone,
so a long reason drops out before "no answer in 30s = deny" does. A desktop
popup wraps and keeps the lines apart; a terminal escape sequence carries them
on one line.

Two answers were missing from the surface: a reply prompt had no button at all
(a question could not be sent back to the terminal), and the notification body
was not clickable. Clicking it is the freedesktop default action, so it now
withdraws the notification and leaves the prompt waiting — the same as the
"Go to terminal" button. A typed reply is recorded by `reply()`, which keeps a
reply that spells "review" an answer; a secret is never announced at all.
2026-10-24 04:21:25 +08:00