622a296f7ae41de115900acf191ddc1a1a5de3fa
155 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
622a296f7a |
fix(agent): keep the todo predicate off the model_tools/executor import path
is_todo_tool_call lived in agent/tool_executor.py and went through
canonical_tool_name, which imports model_tools. TUI resume calls it from
_todo_state_from_history on the RPC path, so the first resume in a
gateway loaded ~405 modules (2-3s) synchronously. tui_gateway/server.py
and run_agent.py also imported agent.tool_executor at module level,
adding ~142 modules to every TUI/desktop launch and breaking run_agent's
lazy-forward rule.
The predicate now lives in tools/todo_tool.py, which both startup paths
already load. It matches TODO_TOOL_NAMES ({TODO_SCHEMA name} + the legacy
aliases) and imports the bridge parser only when a tool_call entry's
args mention "todo". model_tools._LEGACY_TOOL_ALIASES derives its todo
entry from TODO_LEGACY_ALIASES, so there is one source of truth ("todo"
is the only alias mapping to todo_list). The live tool.complete path in
tool_progress uses is_todo_tool_name and the hand-kept _TODO_TOOL_NAMES
tuple is gone. The server.py noqa import is replaced by a function-local
import next to MAX_TODO_RESULT_CHARS, so a pruned name can't be swallowed
by the broad except. run_agent imports lazily. The dead TypeError arm is
dropped, and field reads use message_sanitization._tc_field.
agent/tool_executor.py is back to its pre-stack state.
Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
|
||
|
|
3d3c1c1223 |
fix(agent): pair bridged tool_call todo results via one shared predicate
todo_list is in the default tool_search defer list, so with tool search active the model calls it through the tool_call bridge and the transcript keeps function.name == "tool_call". The canonical-name pairing check never matched those, so todos were still dropped across turns (#124960) in every tool-search-active session, and the TUI resume snapshot had the same gap. Add agent.tool_executor.is_todo_tool_call: canonicalizes legacy aliases and peels the bridge from the recorded arguments with normalize_tool_call_entries (exactly one entry required). It deliberately does not use resolve_underlying_call, which reads live config and could disagree with the defer list in force when the history was written. run_agent and tui_gateway's _todo_state_from_history now share it; the canonicalizer is public (canonical_tool_name) since it is now used across modules. Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com> |
||
|
|
c42c90552e |
fix(agent): re-gate later terminal approvals after an earlier batch failure (#113158)
Desktop terminal batching pre-collects the approval for every command in a run before any of them executes, so the user consents to a batch in which all commands are expected to run. When an earlier command then fails (or is denied/blocked), that informed consent no longer describes the world the later commands will run in — yet the executor still consumed the pre-made decision as though nothing had happened. - _TerminalBatch.failure_seen: set by the sequential publisher after a slot's failed (or blocked) result is committed — i.e. after the failure the model actually sees, never from a wedged worker's late result. - consume_prepared_guard drops the prepared decision for any later slot once a failure is published and returns None, so the guard runs its live flow again: tirith scan, allowlist, and a fresh human approval request when the command still warrants one. Nothing is auto-denied and an explicit denial of the failing command remains authoritative. - The flag is sticky for the batch: a later success must not un-stale an approval collected before an even earlier failure. Success-path batching is unchanged: with no failure, prepared decisions are consumed exactly as before (byte-for-byte the same flow). |
||
|
|
94119759c6 |
refactor(agent): derive the pruned-args recovery hint from the marker prefix
Final-gate reuse/quality: the refusal message re-typed the marker prefix the new compression_marker leaf exists to own. Also pin that the bare prefix alone is not an artifact (a prefix-only matcher now fails a test). |
||
|
|
b969fdadfb |
refactor(agent): carry one blocked-call body through the pruned-arg check
One block decision travelled as block_message + block_error_type +
block_payload, with _blocked_tool_result re-deriving the message from the
payload it was also handed, and _pruned_tool_arguments_block existing only to
package two module constants for its single caller. Replace message+payload
with a single block_body dict: scope/plugin blocks become {"error": msg}
(byte-identical to before), the pruned-arg block is built inline, and
_blocked_tool_result serialises whatever it is given. What the model sees is
unchanged.
Also: the _dispatch_authorized_once docstring now lists the pruned-arg check
between pre-hooks and guardrails where it actually runs, and the refusal text
gains one static sentence telling the model how to legitimately remove a
marker that already landed on disk (match the HERMES-CONTEXT-COMPRESSION
prefix instead of quoting the full marker, which the guard would refuse).
|
||
|
|
81561552f2 |
refactor(agent): check pruned tool args once, after plugin arg rewrites
Two identical checks (before and after pre_tool_call hooks) doubled the traversal for no extra safety: whatever reaches dispatch is the post-hook argument set, so a single check placed after _pre_tool_block covers both copied markers in the model's original arguments and markers introduced by plugin modify hooks. Co-authored-by: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> |
||
|
|
ce2a747be9 |
fix(agent): block pruned content before effectful dispatch
(cherry picked from commit ff9acb30510389561a2706653ad9f7f2aef99895) |
||
|
|
6828ffa70f | fix(desktop): persist edit previews before tool-result flush | ||
|
|
70f5dc5f46 |
feat(connectors): the backend API for the desktop Connectors page; connect an app without a chat session (#115191)
* feat(connectors): the backend serves a connector's tool list, cached for 24 hours
The Connectors page opens one app and shows every tool it has. The backend
had no way to read that list.
- `tools/connectors/portal/`: a client for the portal's tool-list route and a
JSON cache under the Hermes home, one file per portal origin and connector.
An entry is fresh for 24 hours. After that the read revalidates with the
stored ETag: 304 keeps the list, 404 deletes the entry, an upstream failure
serves the stored list marked stale, and a 401 never serves the cache.
- `connectors.tools {slug, refresh}`: account-level, routed by `profile`, no
chat session. Errors carry a fixed `reason` from one closed set on the rail.
- Every connector model that is not operation state moves into
`tui_gateway/contracts/connectors.py`. Handlers that no chat session owns
live in `tui_gateway/methods_connectors_account.py`.
The wire model is tolerant: an unknown facet reads as unclassified and one odd
tool never blanks a connector.
* feat(connectors): catalog, accounts and member tool rules by RPC
The Connectors page needs the app catalog, the connected account of one app,
a way to disconnect it, and the member's own on/off rules. None had an RPC.
- `connectors.catalog`: name, description, category and logo of each app.
- `connectors.accounts`, `connectors.accounts.remove`: read the accounts at
the tool gateway and remove one by id.
- `connectors.policy.get`: the rule layers that apply to the member, widest
first. The body is a union on `mode`, so a reader can name who turned a
tool off.
- `connectors.policy.set`: one change, a union on `type` (the tools of one
connector, or one connector on or off), with the revision the user saw. A
stale revision answers `POLICY_CONFLICT`. The backend composes the upstream
write in one pure function, so no renderer learns the upstream rules.
- Bundled MCP manifests can name their hosted twin with `connector:`, so the
page can show one card per app.
* feat(connectors): connect an app without a chat session
Every connector RPC took a `session_id`, and a connect that did not come from
the model's tool call minted a link with no watcher. The Connectors page has
no chat session, and its card must flip to connected by itself.
- `connectors.list`, `connectors.connect`, `connectors.operation.status`,
`connectors.operation.wake` and `connection.respond` take `owner`, a union
on `type`: `session` (today's behaviour and authorization) or `account`
(routed by `profile`, authorized by the live transport like `mcp.*`).
`session_id` is gone from these params; every desktop caller sends `owner`.
- An account connect runs the same operation lifecycle on a background
thread, under the profile's scope, so the watcher reads the account and
settles the operation. A second connect for an app that is already
connecting returns the open operation and mints nothing.
- `connection.update` carries `owner`. An account operation has no session to
address, so its updates go out on the session-less broadcast path.
* feat(mcp-catalog): eighteen more bundled entries name their hosted connector
A bundled MCP entry and a hosted connector for the same app are one card
on the Connectors page only when the manifest names its hosted twin.
Linear and Notion had the field. These entries get it too: airtable,
asana, attio, calendly, dropbox, figma, railway, supabase, todoist,
betterstack, canva, cloudflare, datadog, intercom, neon, sentry, stripe
and vercel. Atlassian maps to two hosted connectors and Prisma Postgres
is not clearly the same app, so both stay without one.
* refactor(connectors): the account handlers share one gate, one params model and one write table
The six account-level handlers each repeated the availability gate, the
auth catch and the catch-all reply. One decorator now owns that, and each
handler validates its params with its contract model instead of a ladder
of isinstance checks. The five connection RPCs share one guard for the
unexpected-failure reply.
The four write composers for the member rules were the same function
with a different list key and polarity. They are one table now.
The owner union lives in contracts/common.py, so the params side and the
event side stop declaring it twice and the import cycle is gone.
An account operation start carries one event and a flag, so the wait for
the sign-in link blocks instead of polling every 50 ms. run_operation
loses its two account-only parameters; drive_operation is the second
entry point.
Tests: four deleted (they exercised pydantic or the mock), three merged
into tables, two added (a client that still sends the old top-level
session_id is refused; all six account RPCs run off the server loop).
The shared reply helper and the HTTP and managed-client fakes move to
one place each. Comments are one line or gone.
* fix(connectors): a missing tool-list route reads as "unavailable", not "connector gone"
The tool-list read treated every 404 as the portal's "this connector is
not in the catalog" answer. It deleted the cache entry and answered
CONNECTOR_NOT_FOUND, so a page would offer to remove an app that is
connected and works. A portal that does not serve the route yet answers
a bare 404 for every app.
Only the portal's own {"error": "connector_not_found"} means the
connector is gone. Any other 404 is now a tool-list outage: the cached
list is served as stale, or the RPC answers TOOLS_UNAVAILABLE.
* fix(connectors): a connect from the page returns to the app after sign-in
The sign-in link carries a return target only when the session's surface
is the desktop. A chat session binds that surface. An account-owned call
has no chat session, so nothing bound it: the link was minted without a
return target and the browser ended on the portal's done page instead of
coming back to Hermes.
Every account-owned call now runs with the process's own surface bound,
next to its profile scope. The operation thread copies that context, so
the first link and every reissued link carry the return target and the
operation id.
* test(connectors): defer the new connector RPC coverage
The tests for the new account RPCs, the portal client, the tool-list cache
and the rule composer leave this PR and come back in one later change, after
the API is settled. The same was done for #111008.
Kept: the edits that existing tests need because the five connection RPCs
now take `owner` instead of `session_id`, and the rename of the managed
client seam.
Removed: six new test files, their two fakes and the gateway conftest, and
the new cases in test_mcp_catalog.py, test_connectors_gateway_client.py,
gateway-rpc.test.ts and notifications.test.ts. Reverting this commit restores
all of them.
* fix(cli): the connection panel hands the tool thread back at once
The classic CLI's connection callback waited on a queue for the user's first
decision. The operation's watcher starts only after the callback returns, and
the watcher is what polls a hosted account, runs the 300-second deadline and
sees Ctrl+C.
For a hosted connector the panel opens on the sign-in link, where the only
key that filled the queue was Cancel. The account was never polled: the user
signed in, the panel never changed, and Esc reported the app as skipped.
Ctrl+C set the interrupt flag but left the thread parked on the queue, so the
turn never ended.
The callback now opens the panel and returns, as the gateway's callback does
for the desktop and the Ink TUI. The panel's actions already reach the
operation through apply_answer on the UI thread, so the queue is removed. An
install with a form still waits for Connect, because the backend starts no
work for a pending row. Ctrl+C now settles the operation as `interrupt`, and
open rows become `not_connected`.
Checked on the e2e rig with the fake tool gateway: hosted connect completes on
the third status read; Ctrl+C ends the turn and the polling stops; an MCP
install with a plain and a secret field still saves config and both values.
* fix(connectors): "run it again" lives in the library, so the classic CLI can use it
Making a new sign-in link for a failed or expired hosted connector was
implemented only in the JSON-RPC layer (`_reissue`). The classic CLI does not
go through JSON-RPC: its Connect button on a failed row called apply_answer,
which does nothing for a hosted operation because it has no MCP runner. The
panel showed "Waiting…" until the deadline.
`tools.connectors.run.reissue(operation, names)` now holds the checks and the
per-kind action, and returns a refusal reason or None. The gateway maps each
reason to the same JSON-RPC error as before. The CLI calls it for a hosted
row; a refusal is shown on the row. MCP rows keep their path, because Connect
on a failed MCP row re-sends the form values.
Checked on the e2e rig: a scripted failed sign-in, then Connect: a second mint
with `reinitiate: true`, a new link with a new connection id, then connected.
* feat(connectors): the account list and disconnect go through the portal
`connectors.accounts` and `connectors.accounts.remove` called the tool
gateway. They now call the portal's account-management routes
(`GET /api/v1/connectors/accounts`, `DELETE /api/v1/connectors/accounts/{id}`),
which apply the organisation membership checks and write the disconnect audit
row. There is no fallback to the gateway when the portal is unavailable, and a
removal is never retried.
The read of ONE account stays on the gateway (`GET v1/connectors/accounts/{id}`):
the portal has no such route, and the operation watcher polls it once per second.
`ConnectorClient.list_accounts` and `delete_account` are removed. The removed
account's reply model carries `connector`, which both services send.
* fix(connectors): the account RPCs answer what the portal really sends
Checked against the portal source and against the staging and production
services.
- Errors are read from the upstream error code, not the HTTP status. A rule
write answered 409 for a stale revision and for a user with no organisation;
both read as "the policy changed". `org_required` is now `ORG_REQUIRED` and
403 `no_access` is `ORG_ACCESS_DENIED` on every account RPC; only a rejected
sign-in is `NEEDS_NOUS_AUTH`. `connectors.list` and `connectors.connect` with
the account owner map these too.
- `connectors.policy.get` and `connectors.policy.set` carry `effective`: the
portal's own result for this user, with its stamp and without provider or
subject ids. Nothing is recomputed locally.
- A rule write needs the revision the user saw: `expected_revision` is required
and must be a revision string; a bad one is refused before any HTTP call.
- A tool row carries `no_auth`; a list without the upstream flag is an invalid
answer, not `false`.
- `connectors.accounts.remove` returns the app of the removed account. An
invalid id is `INVALID_PARAMS`.
- The tool-list cache is per signed-in member (a hash of the token's `sub`),
so two Nous accounts on one profile do not share entries.
- A malformed slug is a local error, not a 404 from a server nobody called.
Live, staging: no revision and a malformed revision refused locally; a good
revision wrote one disabled Gmail tool and returned it in `effective`; the
same revision again answered `POLICY_CONFLICT`; the list row showed the tool;
the restore brought the member rules back to the start. Live, staging and
production, read-only: all 60 tool lists (5483 tools) parse.
* fix(connectors): the operation RPCs match their contract; a settled card cannot start a new link
Found by two adversarial reviews of the RPC layer and its types.
- `connectors.connect` from a chat session with no open operation is refused
(`UNKNOWN_OPERATION`). It used to call `manage_connections` through the tool
registry with no card: it made a link nobody watched, returned a reply
without the required `settled` field, and named an operation that was never
registered. There is one way into an operation: the agent's call, or the
account owner's `connectors.connect`. "Run it again" inside an open
operation is unchanged.
- `connection.update` for a session is routed by session key AND profile; two
profiles with the same key no longer cross-deliver a sign-in link. The event
payload gets the same redaction as the RPC replies.
- `connection.respond` runs on the long-handler pool: an approval can start MCP
OAuth discovery, which blocked every RPC of the gateway while it ran.
- `connectors.list` rows are a closed snake_case model: `connector`, `enabled`,
`connected`, `connection_status`, `status_reason`, `gateway_disabled_tools`.
The last one is display data: the gateway enforces the rules, the backend
only passes the list on. The phantom `name` and `description` are gone, and
the desktop uses the generated types instead of hand-written copies.
- `tools_listing` (model-only data) no longer rides on `connectors.operation.status`.
- `unavailable` is removed from the target states and settle reasons: nothing
produces it. The contract generator now fails when a contract enum and its
domain enum differ.
- `ConnectorErrorReason` is part of the generated TypeScript and OpenRPC.
- The desktop sends `connection.respond` on the socket that holds the session,
as wake and reissue already did.
- Contract violations are logged every time, at error level.
- An account connect whose prepare step is slow returns the live operation
instead of an error while the operation keeps running.
- The MCP-manifest `connector` field leaves this PR (it moves to a later one
on top of the catalog-reader change). `hermes_cli/mcp_catalog.py` and
`optional-mcps/` are untouched by this PR again.
anti-slop: no net-new findings (15 touched files).
* fix(connectors): the model gets no sign-in link wherever a card exists; side agents cannot connect
The flag that tells the model "a connection card exists" was the session
platform (`== "desktop"`). The Ink TUI and the classic CLI also draw a card,
so there a connector call on an unconnected app handed the model the raw
`connect_url` and told it to pass the link to the user.
- The agent turn now declares how a link can reach the user
(`tools/connectors/turn.py`): CARD when the agent was built with a
connection callback, SIDE for a subagent or a background turn, LINK for a
headless run (`-q`, cron, ACP, api_server, messaging). It is set once per
tool batch in the agent loop and read by the connector dispatch path, which
never sees the agent. The session platform decides return-to-app only.
- CARD: the result carries `connect_card_available` and our hint, never the
link and never the gateway's own hint.
- SIDE: subagents (`delegate_tool`), gateway background turns and the classic
CLI `/bg` are built with `side_agent=True`. They hold no `manage_connections`
tool on any path that derives the tool list, and a connector call on an
unconnected app gets no link, only "report this to the main agent".
- LINK is unchanged.
- The hosted path with no card builds a detached operation, as the MCP path
does, so no `connection.update` is emitted for an operation no client asked
for. Names and docstrings that said "off desktop" now say "no card".
- A settled card is dead on the desktop: `reissueConnectionTarget` and
`respondToConnectionRequest` share one guard and send nothing for a settled
or unknown operation.
- The model-facing settled result no longer carries `connection_id`; the model
repeated it to the user.
Shown on the real clients with a real model (rig, fake tool gateway): Ink TUI
and classic CLI get `connect_card_available` and no link, the model opens the
card, the account connects, the retried call succeeds; `-q` still gets the
link; a subagent and a background turn have no `manage_connections` and get
the no-link hint; on the desktop a card settled with Continue has no enabled
control and sends no RPC.
* feat(tools): every call made through tool_search + tool_call shows a real label on all three clients
A bridged call showed as a generic `tool_call` row in the Ink TUI and as
`⚡ tool_call` in the classic CLI, because the display looked the name up in
the tool registry and bridged names are made at run time. The desktop labelled
only batches that were all hosted connector calls, by parsing names itself.
- `tools/tool_labels.py` is the one place that turns a bridged call into a
label: kind, app, action, emoji and text. Hosted: `connectors__gmail__GMAIL_SEND_EMAIL`
→ "Gmail · send email". MCP: "Linear · list issues". A local deferred tool
keeps its own emoji, verb and primary-argument preview. A batch gets exactly
one label per entry, always; an entry with no name gets a generic label.
- Classic CLI: one row per inner call; the duration on the last row; the
failure text on the row of the call that failed. With friendly labels off
it prints what it printed before.
- Gateway: tool start, progress and complete events and stored transcript rows
carry a typed `labels` field. It does not depend on the classic CLI's
display setting. Clients no longer parse tool names.
- Ink TUI: rows from the labels; the verbose trail keeps Args and Result.
- Desktop: `ConnectorExecution` renders hosted, MCP and mixed turns from the
labels, one row per call. The labels reach the row under a key no tool
argument can use. The connect card it drew under a failed tool result is
gone: after `CONNECTION_REQUIRED` the one way in is the agent's own
`manage_connections` call.
- `tool_search` and `tool_describe` rows read "Searching tools · <query>" and
"Reading tool details · N tools".
Shown on the real desktop (video and screenshots), the Ink TUI and the classic
CLI with the rig: hosted rows, MCP rows, a two-entry batch, a failed entry, a
`CONNECTION_REQUIRED` row with no card under it, labels after a reload, and the
desktop rows with the classic CLI setting off.
* fix(connectors): the model can tell "hosted tools unavailable" from "no such tool"; manage_connections routes MCP names correctly
- A failed hosted search or describe used to return nothing, by design, so the
model saw only local tools and told the user that a connected app was
missing. The local results are unchanged; when the hosted leg failed, the
`tool_search` and `tool_describe` results carry
`connectors: {status: "unavailable", reason: "unreachable" | "sign_in_expired"}`
and one hint line. A rejected token is `sign_in_expired`; an entitlement
refusal or a shut gate adds nothing. `tool_describe` no longer lists those
names under `not_found` next to "search again".
- NS-932. The description now says which side a name belongs to: a bare name
is a hosted connector account; `mcp: true` only when the user asks for an MCP
server, a local server or an install, or when the name exists only in the
catalog; connect and reconnect are hosted verbs, install, enable and
authorize are MCP verbs. It names the three clients that draw a card.
- A misrouted target is refused with the call that works. Only when the
gateway does not know the connector (confirmed on that failure path) and the
name is a catalog entry does the target fail with "X is a local MCP server.
Call manage_connections with action install ...". It is a per-target
outcome: other targets of the same call keep their links and their card. A
vendor failure on a name both sides know stays an ordinary failed row. The
MCP side mirrors it, and never for an entry that is only not installed.
- "Do not re-ask after a skip or a timeout" no longer stops the model when the
USER asks for that app again; the description and the settled-result notes
say so. A builder saw the model refuse a direct user request.
Shown on the Ink TUI and the classic CLI with a real model: a dead gateway and
a 401; "connect fxmail" goes hosted; "install the fx-noauth MCP server" goes
MCP; "connect fx-noauth" reaches the MCP install card in one corrective round
with no hosted mint; a two-target call where one is misrouted still connects
the other with exactly one mint.
* fix(tui): the connection card answers every key, shows what is happening, and is dead once settled
Reproduced on the real Ink TUI with the rig, then fixed:
- The keyboard was dead during the sign-in wait: the card kept a `submitting`
flag that the normal OAuth path never cleared, and Esc went through the same
guard. The in-flight state now belongs to the answered row and clears when
that row moves, when any later frame of the operation arrives, or after
five seconds. Esc skips the row in every phase; Ctrl+C interrupts the turn
(the input handler had no branch for this overlay); Shift+arrows scroll the
transcript and the card ignores them; arrow keys no longer move the text
cursor and the field focus at once.
- The card was lost at turn idle: the overlay flag was cleared while the
operation stayed in the store, and a resume dropped the pending card. The
flag survives idle, a resume shows the pending card again, a session switch
clears it.
- States with no branch: `not_connected` and a row with no link fell into the
credential form; `expired` vanished with no note. The title and the row text
now name the action (connect, reconnect, install, enable, authorize); a
failed or expired row with no fields offers Try again / Skip; a failed row
WITH fields reopens the form over the typed draft, with the failure above it.
- A settled card is dead: at settle the overlay closes and one transcript line
per app states the outcome. A settled or dismissed operation id is
remembered, so no replay or resume can reopen its card. Esc in the last
"Finishing…" moment hides the card and still writes the outcome lines.
- A failed `connection.respond` and a browser that did not open are shown on
the card in one sentence.
Also: `tui_gateway/connector_payload.py` redacted the BOOLEAN `secret` flag of
a credential field to the string "[REDACTED]". On the desktop every credential
field therefore rendered as a password and lost its prefilled default. A
boolean is no longer redacted.
* chore(connectors): remove the comments and docstrings this branch added
Deletions only. Kept: tool directives (`# noqa`, `// eslint-disable`, ...),
`// SAFETY:` lines, and the docstrings of the contract models under
`tui_gateway/contracts/`, which become the descriptions in the generated
OpenRPC and TypeScript.
Checked that no code changed: every Python file has the same AST as before
once docstrings and `pass` are ignored (62 files), and every TypeScript file
prints the same with comments stripped by the TypeScript printer (32 files).
The generated contract files are unchanged.
* fix(connectors): a card restored after a reload answers again; every account RPC names auth and org failures
Found by the end-to-end runs on the pushed head.
- Desktop: after a window reload, Continue on the restored card sent nothing.
The answer looked up the backend that holds the session with the runtime
session id, the lookup wants the stored id, and a failed lookup returned
silently. When the lookup gives no owner the answer now goes out on the
window's active socket, which is what main does.
- `connectors.policy.get` answered `POLICY_UNAVAILABLE` for a rejected sign-in,
a refused scope, a non-member and a missing organisation alike: the handler
runs with the gateway's globals and did not import the reason enum, so its
own error mapping raised. `connectors.accounts.remove` caught auth failures
in its generic branch. `org_required` was mapped on `policy.set` only. All
six account RPCs now answer `NEEDS_NOUS_AUTH`, `FORBIDDEN_SCOPE`,
`ORG_ACCESS_DENIED` and `ORG_REQUIRED` for those four upstream answers.
|
||
|
|
188f0d4251 |
fix(plugins): fire transform_tool_result for agent-runtime tools
Agent-runtime tools (todo_list, session_search, memory, clarify, delegate_task, the preview/terminal readers, context-engine and memory-provider tools) are dispatched inline and never reach handle_function_call, which is the only place transform_tool_result ran. A registered transform silently did nothing for them, although the hook is documented as applying to every tool. Apply the same helper on both runtime executor paths, after the terminal post_tool_call so the observer still sees the untransformed result: the concurrent path in invoke_tool's inline branch, and the sequential path in _publish_sequential_result. The sequential registry dispatch marks itself transform_applied so handle_function_call stays the single invocation for registry tools and nothing double-fires. The sequential result classification (failure detection and the logged result length) moves below the transform so it reads the result the model actually sees, matching the registry and concurrent paths. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit bc31efff0a6e2fd8177edd73cfe1a7112df9f78d) Fixes #72836 Salvages #115397 (thomasscottbeck-sudo); supersedes #72860 (webtecnica, earliest — sequential path only). (cherry picked from commit d6b5e969b3e08a557a62ed85c8b828537857b3d8) |
||
|
|
a8a81f4cb9 |
refactor: log the dead compression worker's exception once; trim guard comments
_await_worker_within_budget now emits one INFO line with future.exception() when it takes the stall path for a worker that already died, so the log shows WHY the fallback chain was entered instead of silently returning (False, None) — previously the only trace was the absence of "still streaming" lines. The three 5-7-line comment blocks added by the #117261 pick restated the same alias fact each time; each is now 2 lines citing #63892 and the 3.11 alias once. No behaviour change beyond the log line. |
||
|
|
8305113328 |
fix(compression): don't mistake a worker's TimeoutError for a poll timeout
On Python 3.11+ concurrent.futures.TimeoutError IS the builtin TimeoutError
(asyncio.TimeoutError and socket.timeout alias it too). Poll loops shaped like
try:
return future.result(timeout=slice)
except concurrent.futures.TimeoutError:
...keep waiting...
therefore cannot distinguish "the wait slice expired" (worker alive) from "the
worker raised TimeoutError" (worker dead). auxiliary_client raises a bare
TimeoutError when a summary stream stalls, so this is reachable in production.
When it happened the host re-waited on an already-settled future. result() then
returned instantly every iteration, spinning at ~2k iterations/sec and logging
"Context compression still streaming" about a dead worker, until the entire idle
budget elapsed. One session burned 535s and wrote ~90k duplicate log lines
(15.6MiB) before failing with context_compression_timeout, and every later turn
re-entered the same path.
Guard each loop with future.done(): a settled future never becomes unsettled.
- _await_worker_within_budget: take the stall path at once, so the configured
fallback chain is actually reached instead of after a 120s false stall.
- _await_in_flight_commit: re-raise the worker's exception. This loop had no
ceiling, so a dead worker spun forever.
- tool_executor._poll_sequential_future: same, and with deadline=None it also
span indefinitely.
Non-timeout worker exceptions still propagate unchanged, and a live worker still
polls exactly as before.
Regression tests pin all three. Against unpatched code the two wait tests fail
and the commit-wait test hangs until the 300s harness SIGKILL, reproducing the
infinite loop directly.
(cherry picked from commit 274457304ce0393407574fe4e43c4a450f20bac3)
|
||
|
|
05e09aee68 |
fix(agent): trim after the tool batch unwinds, not inside the commit that still holds the result
Gate review (three lenses converged): inside _commit_tool_result the >=1 MB string is still referenced by the publish frames (the returned tuple, managed.result / batch.results, the tool.completed callback), so gc.collect + malloc_trim could not release it and merely spent the 60 s cooldown. The commit now only sets agent._trim_after_tool_batch; the finally of AIAgent._execute_tool_calls consumes the flag once every executor frame is gone, coalescing N large results in one batch into one trim. The import is lazy like every sibling call site (keeps ctypes out of the agent import chain). |
||
|
|
f32f651788 |
perf(tool_executor): trim memory after publishing a >=1 MB tool result
Post-compression already calls trim_memory (#77356); a huge tool result (raw stdout, file dumps) is the other allocation a turn drops and was published with no collection. _commit_tool_result is the one point both the sequential and concurrent publish paths go through, so the trim lives there, after the spill + session flush, measured on the string already in hand (multimodal dicts are never re-serialised). trim_memory's own cooldown/kill-switch apply. Salvages the intent of #80974 without its bare gc.collect(), re-serialisation and 186 LOC. Closes #70684 (tool-result half). Co-authored-by: Christopher-Schulze <210261288+Christopher-Schulze@users.noreply.github.com> |
||
|
|
ea51325186 |
fix: persist a multimodal text part once and record its spill path for the stub guard
_persist_multimodal_text_parts persisted the text part and then text_summary under the same tool_use_id, so the second write overwrote the spill file with the (shorter) summary while the part's <persisted-output> preview advertised the longer text. Reuse the part's bounded replacement for an oversized summary instead of persisting twice; the test fixture now uses distinct text vs text_summary (real browser_exec shape) and asserts the file holds the part text. _record_persisted_path_for_stub only read string results; a spilled multimodal envelope now has its persisted path extracted from text_summary / text parts so a duplicate-result reference stub can point at the spill file. |
||
|
|
876bae4d0f |
fix: spill oversized text parts of multimodal tool results like string results
A browser_exec call that captured a screenshot returns a multimodal envelope whose text part carries the full stdout. _finalize_tool_result exempted every multimodal envelope from maybe_persist_tool_result, and on vision-capable routes the part list also slips past enforce_turn_budget (len(list) counts parts, not chars). A 760K-char browser result therefore stayed inline in hot context and was re-sent on every later request (#95429: 2.47 MB requests, repeated no-first-byte stalls on retry). Route each TEXT part (and text_summary) of a multimodal envelope through the same per-tool persistence threshold; image parts stay untouched (their size is already governed by the vision embed budget). Normal-sized envelopes are returned unchanged. Same defect class as PR #95458 (@fangliquanflq), ported minimally. |
||
|
|
dac4c60fbb |
fix(checkpoints): refuse host rollback and session diff from container sessions on the gateway too
Widen the container-backend refusal salvaged from #113530 to the sibling surfaces that render the same host checkpoints: the messaging gateway's /rollback (restore refused, bare listing prefixed with the reason) and /diff session, and the CLI's /diff session. The gateway arm follows the CLI's "default" classification, i.e. the configured terminal backend. Drop the thin _checkpoint_container_backend wrapper in favour of the container_backend_for_task predicate it wrapped, trim the salvaged suite to two invariant tests (one per class: no host store touched by a container task; every surface refuses a host restore/diff from a container session, with a local control), and update the docs. Co-authored-by: fangliquan <fangliquan@qq.com> |
||
|
|
3220b9ed2f |
fix(checkpoints): classify the backend on every /rollback instead of remembering it
Review follow-up (#113530): the manager no longer records the first container backend, so a session whose terminal backend changes is answered by the backend configured now, not by the first one seen. The checkpoint hooks simply skip container-backed tasks; unsupported_backend_reason() classifies at call time. The docs state what /rollback and the rollback.* RPCs do for container sessions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit 2dc4c0d2b17d0f478118eca55742001eeed8bdb0) |
||
|
|
10ee9819f9 |
fix(checkpoints): do not feed container paths into host checkpoint storage
With a container terminal backend (docker, singularity, modal, daytona, vercel_sandbox, container plugins) file-tool paths keep container semantics, but the checkpoint hook handed them to the host-side CheckpointManager: a path that does not exist on the host produced a useless snapshot attempt, one that happens to exist on the host snapshotted the wrong tree, the destructive terminal branch did the same with the container cwd, and the post-write ledger hashed the container path on the host so safe restore could trust unrelated host content. Every failure was swallowed, so a docker user saw "No checkpoints found for /home/admin" with nothing behind it. Classify the task's backend the way the file tools do (_uses_container_paths) and, for container-backed tasks, take no checkpoint and record no ledger entry; /rollback prints the reason and refuses diff and restore for that session (a host checkpoint that predates it belongs to another tree), and the rollback.restore RPC returns the same reason as a failed restore. The refusal classifies the session's configured backend directly (in the gateway under the session's own identity and profile scope, as a turn binds them), so it holds before the first mutation of the session. Local and ssh backends are untouched. This stops the false protection; it does not add rollback support for containers (translating bind mounts is a separate contract). Tests: eight cases in tests/agent/test_tool_executor_checkpoint_paths.py through the production classifier (a fake docker environment registered for the task, or the configured backend): missing host path, colliding host tree (POSIX), destructive terminal command, post-write ledger on a real host file, /rollback and rollback.restore refusal in a fresh session, the local session still restoring, and unchanged local behavior. Five fail on main on Windows, where the collision case is skipped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit 97a5709e4980c8f85a5a640e6e5e11fe8f94affa) |
||
|
|
1b50849d27 |
docs(clarify): one clarify timeout for every surface; say where the deadline exemption lives
- hermes_cli/config_defaults.py: the agent.clarify_timeout comment claimed
"CLI clarify blocks indefinitely and ignores this"; the CLI reads it (via
resolve_clarify_timeout) — describe the real contract and the legacy
clarify.timeout precedence once.
- agent/tool_executor.py: the sequential-middleware docstring named clarify
as exempt from the generic deadline while _SEQUENTIAL_DEADLINE_EXEMPT_TOOLS
does not list it; the exemption is the _NEVER_PARALLEL_TOOLS inline path
(
|
||
|
|
ba135b366b |
fix(agent): stop the tool-activity heartbeat when the executor abandons its worker
A tool that wedges (e.g. a kernel process query that never returns) is now abandoned by the executor deadline, but the heartbeat thread started on that worker kept stamping "tool running: <name>" every 30 s for the rest of the run: its stop_event is only set when the worker returns, which a wedged worker never does. That pinned the inactivity watchdog's seconds_since_activity near zero, so the second line of defense against a hung cron run could never fire — finding (b) of #111922, still live after (a) landed in #112185. The abandoning executor already raises the per-thread interrupt bit on the worker (_interrupt_worker_tids, both sequential and concurrent paths), so the heartbeat exits once its worker tid carries that bit. No new registry. Part of #111922 |
||
|
|
dd68d17567 | feat(approval): prepare GUI terminal asks before ordered execution | ||
|
|
1d14418ea2 |
fix(agent): concurrent worker survives a dict error result
_detect_tool_failure now classifies dict results as failures, so the concurrent worker's failure log line sliced result[:200] on a dict and raised TypeError; the worker died and the model saw "thread did not return a result" instead of the tool's own error payload. Stringify the preview like the sequential path does. |
||
|
|
699037176c |
fix(agent): log the serialized size of multimodal results in the concurrent executor
The concurrent completion line logged len(result) directly, so a native-path vision_analyze envelope dict reported "4 chars" — its key count — while the sequential path already logs the serialized length. Mirror the sequential measurement so parallel multimodal calls stop looking truncated in logs. |
||
|
|
ee49b7d25d |
fix(agent): file-mutation footer states failed edits, not "files were NOT modified"
The turn-end file-mutation verifier only sees write_file/patch receipts. It asserted "N file(s) were NOT modified this turn" whenever a call had failed, which is wrong when the file was in fact changed afterwards through a path that leaves no receipt (terminal redirect, execute_code) or when the successful retry used another spelling of the same path (relative vs absolute, separator/case variants on Windows): the state dict was keyed on the model's raw `path` argument, so the pop never matched. - Header now says what the recorder knows: "N file edit(s) FAILED this turn", and asks the user to confirm what actually landed. - Failure entries carry the task-resolved, normcase'd on-disk identity plus a (mtime_ns, size) snapshot; a later success clears every entry with the same identity regardless of spelling. - At turn end `_file_mutations_still_failed` re-stats each target and drops entries whose file changed since the failed call, so a receipt-less mutation no longer produces a false footer. - `tool_executor` passes the effective task id so relative paths resolve the way the file tools resolved them. Kept the deliberate first-error-per-path semantics (the pinned test says why); did not add an "unverified" bucket for receipt-less non-error results, since the built-in tools always return a receipt on success and it would only add noise. Co-authored-by: KoNit. <124019182+KoNit-K@users.noreply.github.com> |
||
|
|
bc3df8a4d5 |
fix: NT-namespace guard fires before every sibling resolve (checkpoint, ACP bridge, @file:)
Three paths still resolved the raw model/remote-supplied string before the guard could refuse it, so on Windows the NTLM-leak trigger (resolving the path) ran anyway: the file-checkpoint helper stats write_file/patch targets before the tool executes; the ACP file bridge resolves fs/read_text_file and fs/write_text_file paths before its read/write denylists; and @file:/@folder: references resolve their target before the reference allow-check. Each now checks the raw string first and refuses. The GLOBALROOT form now requires its path separator so a GLOBALROOT-prefixed local name is not misclassified. The rationale comment names the vector instead of another product's changelog, and the security docs say the row is enforced on reads as well as writes, since it sits under the write-guard table. |
||
|
|
ee2f5629b8 |
Desktop connect runs on the connection operation: one card, no link to the model, no renderer polling (NS-868) (#110574)
* refactor(connectors): cut comments that restate the code
Connector modules (tools/connectors, tui_gateway connector RPCs, desktop
connector card/store) keep only comments that carry a non-derivable why or
a cross-module contract. No behaviour change.
* feat(connectors): managed connect runs on the connection operation
Managed `connect` / `reconnect` mint one ConnectionOperation for every target and, on a
desktop session, block the tool turn until the operation settles; the result is per-target
outcomes and never carries a connect link. Off the desktop the result carries the links and
returns at once (PR3 delivers them as their own message).
Why: the previous leg handed the model a URL and a `wait` verb, and the renderer ran its own
2s poller on top of the backend's 5s one; both walked the whole gateway catalog at two vendor
calls per page to read one row (~3 Composio calls/s per pending target). A hidden composer
message started the model's `wait` on the user's behalf. None of it was observable from the
operation the MCP leg already used.
What the operation looks like now:
- `contract.py`: TargetState / Actor / SettleReason enums and the `(kind, from) -> {to: actor}`
transition table. `operation.transition()` enforces it; a card cannot claim a managed
target `connected`, only the backend watcher can.
- `live.py`: one open operation per session, found by `op_id`. `connectors.operation.status`
reads it, `connection.respond` drives it, `pending_connection` on resume replays it.
- `run.py`: the one lifecycle for both target kinds (prepare -> card -> wake/observe loop ->
settle -> result). The managed `observe` hook polls the gateway list once per tick for the
whole operation; the exact-status route replaces that call when the gateway ships it.
- `connection.update` is emitted on every transition and on settlement; registered in the
shared event contract with the operation vocabulary typed on the TS side.
- `wait`, `_rendered_links`, `_seen_instructions`, the just-minted bounce and `_clamp_timeout`
are deleted. `force` on `reconnect` always reinitiates; plain `reconnect` repairs only what
the gateway reports disconnected.
- `connections.wait_timeout_seconds` is removed from config defaults, the example and the
docs. The deadline is `OPERATION_DEADLINE_SECONDS = 300` in `operation.py`; the key was
added on this unmerged train so no migration is needed.
- Wire model: `statusReason` parsed on connection results; the seven-state `connectionStatus`
is typed on list items and an unknown value fails validation; `CONNECTION_REQUIRED` carries
`connect_card_available` instead of the link when the session platform is `desktop`.
Session platform, not callback presence, decides whether a card exists: the GUI bridge
attaches callbacks to every backend session, terminal TUI included.
* feat(desktop): connector card subscribes to the connection operation
The card renders from the backend's operation instead of driving its own: `connector-flow.ts`
(the renderer's 2s `connectors.list` poller, its 120s client deadline and `keepWaiting`) is
deleted, and both hidden composer submits in `connector-tool.tsx` go with it. The model is
never nudged into a `wait`; the tool call is blocked on the backend until the operation
settles.
- `connection-request.ts` is the operation store: keyed by `op_id`, one entry per session,
`applyOperationStatus` / `applyConnectionUpdate` as pure reducers, `respond` leaves the
entry in place (the backend answers with `connection.update`), `ConnectionTargetOutcome`
is a discriminated union the backend's transition table accepts.
- `input-requests.ts` applies `connection.update`; `connection.expire` and the resume
snapshot correlate by `op_id` (a snapshot has no `request_id`).
- `ConnectorOffer` renders one `ConnectorCard` per target from a single
`Record<ConnectionTargetState, phase>` table; Connect opens the stored link, Try again on
failed / expired reissues through `connectors.connect` on the open operation, Not now is a
per-target `skipped`, Continue settles. A settled operation renders `ConnectorSummary` rows
with no live control.
- `tool-render-class.ts`: `manage_connections` renders the card regardless of
`HERMES_GUEST_ONBOARDING`; the flag still gates the onboarding flow, not the card. The
backend gate already decided admission; a card only exists because the tool was admitted.
- `mcp-setup-tool.tsx` speaks the same outcome vocabulary (connected / skipped / failed).
- `ConnectorRow.connectionStatus` is the seven-state literal union, not `string | null`.
- The guided-onboarding poller (`first-build-connectors.ts`) keeps its own row/phase types
and compiles unchanged; PR3 moves it onto the operation.
anti-slop: no net-new findings (17 touched files vs 11d1a12472).
* fix(connectors): the card never parks the tool thread; every update carries the snapshot
Found by the pre-PR adversarial review and a real-path E2E test (both left in the tree).
- The desktop `connection_callback` was still `_block("connection.request", ...)`, which parked
the tool thread on a private request-id Event until a `_respond` that no longer exists for
this event. `connection.respond` settled the operation but the tool waited its full deadline
before the watcher loop even started. The callback now only emits the card; the operation's
own wake loop is the wait. The MCP leg's blocking bridge goes with it: the card answers
through `connection.respond` like every other card.
- `connection.request` and every `connection.update` frame carry the full target snapshot
(state, link, detail). The initial mint happened before the card existed, so the renderer
never saw the links and Connect stayed disabled; a Continue settlement stamped
`not_connected` on the backend while the card still showed `initiated`. The store now
overlays the snapshot; no state is reconstructed from deltas.
- The `connection.update` emitter is a class-level `on_change` slot on the operation, set
once by `register()` (a second `register()` no longer stacks wrappers); session lookup takes
`_sessions_lock`; a re-minted link on an `initiated` target goes through `refresh_link()`
and emits, instead of a bare attribute write.
- `session.interrupt` is checked before the first observe, so an interrupted call settles
`interrupt`, not `all_resolved`.
- A gateway list reporting `expired` for an initiated target is recorded with actor `clock`
(the contract's owner of that edge); it raised `IllegalTransition` before.
- Dead `keepWaiting` i18n keys from the deleted renderer poller removed.
tests/tui_gateway/test_connector_operation_e2e.py runs the desktop lifecycle through the real
tool, registry, gateway RPC handlers and callback bridge with only the HTTP client faked.
* docs(connectors): prompts and docs describe the operation, not the deleted wait verb
The onboarding prompts told the model to call action="wait" with timeout_seconds and to
expect a hidden [setup]/[connectors] note; both are gone. tool-search.md and
toolsets-reference.md said the model gets a connect link on the desktop. tui_gateway/AGENTS.md
gains the connection-operation row of the surface table.
* fix(connectors): the panel re-mints only a dead link
Try again on a failed or expired target mints a fresh link on the open operation. A waiting
target keeps the link it was minted with; the card reopens it and connectors.connect refuses
to spend a second mint (LINK_STILL_VALID). The unused refresh_link() goes. The package
docstring names the new siblings; the nine-name public surface is unchanged.
* test(connectors): the local-batch test answers the operation the way the card does
The callback stopped returning an answer in f782b26d98 (the card answers through
connection.respond); this test still returned one and waited out the 300s deadline in CI.
* ci: retrigger
* fix(connectors): the desktop card appears outside guided onboarding
Live on a signed-in macOS desktop, the two-app connect never showed a card. Three
defects, each hidden by a test that bound state the running app never binds.
The backend read the surface from HERMES_SESSION_PLATFORM only. The desktop and TUI
gateway bind it as HERMES_SESSION_SOURCE (_set_session_context), so session_platform()
was "" and managed connects took the off-desktop branch: links in the model's message,
no operation. session_platform() now reads platform, then source. The E2E test binds
through server._set_session_context instead of set_session_vars(platform="desktop").
The renderer routed manage_connections to the card only under isOnboardingEnabled(),
the HERMES_GUEST_ONBOARDING launch flag, in message-parts.tsx and the run splitter in
fallback.tsx. tool-render-class.ts had already dropped that gate in this PR; the two
routers had not. Both now route on the tool name alone.
ConnectorTool resolved the session owner by the runtime id. Owner routes, hints and
session rows are keyed by the stored id, so in registry topology the owner never
resolved and the card rendered null while the tool blocked. It now resolves by the
stored id, matching the PR1.5 card and every other owner lookup.
message-parts-connectors.test.tsx mounts the real Fallback router with the onboarding
flag off and distinct runtime/stored ids; red before each renderer fix, green after.
* style(connectors): shorter comments, no module mock in the card router test
The router test mocked isOnboardingEnabled to false; jsdom has no preload bridge, so the
real function already returns false. Comments that restated the code are cut to one line.
anti-slop: no net-new findings (25 touched files)
* fix(connectors): Connect on a waiting row opens the stored link
ConnectorCard derived the button's loading state from the phase label, so a managed row that
read "Finish connecting in your browser" (every row, since links are minted up front) had a
disabled Connect button. Nothing on the desktop could open the sign-in link; every managed
connect ended skipped, not_connected, or at the deadline.
The card now takes `busy` for "the action itself is running" and keeps `phase` as a label.
The MCP card passes its in-flight flag; the connector card passes the re-mint wait. Red before:
the Connect button on an initiated row rendered disabled and a click opened nothing.
* fix(connectors): a settled card stays dead; the card binds to its tool call only
A second connect for the same apps revived the finished card on the old tool row. The
connection.request payload carried no id, so the renderer fell back to matching rows by
connector names, and any row with those names qualified, settled or not.
The operation now records the model's tool_call_id and sends it in connection.request and in
the resume snapshot. The card binds to the tool row with that id and to nothing else; the
name-match fallback is deleted. A payload without the id is rejected by the store.
`reason` is removed from the tool: it was the only text the card ever showed from the model
and its absence forked a second tool part, since `reason` doubled as the row-correlation key
in tool-parts.ts. The card never needed it.
`connection.expire` is deleted from the contract and from _EXPIRING_REQUESTS: the card is
raised with _emit, not _block, so nothing has emitted it since the operation lifecycle landed.
Sid's rule of record: a resolved card is fully dead; no path brings it back.
* fix(connectors): the watch loop settles once, on time, and never raises into the result
Three findings from the live review, one loop.
Continue racing a finished sign-in: the loop ran the gateway read, then settled. A read that
returned `connected` for an already-settled or failed target raised IllegalTransition out of
the tool and the model got a generic error instead of the per-app outcomes. The read now skips
targets that are not live (pending, initiated) and skips a settled operation; the loop checks
`settled` after every read.
Settle reason as row text: `settle()` wrote `continue`/`deadline` into each unresolved target's
`detail`, and the card printed it in red. The reason stays on the operation only.
Stop and the deadline waited for the next tick: `/stop` sets a per-thread flag with no wake
hook, so the sleep is sliced at 250 ms and the flag and clock are read each slice. The clock is
also checked before each read, not only after.
Tests: a failed mint that later reads connected settles cleanly; Continue during a read keeps
the settled result; no reason in detail; an interrupt settles within the same second.
* fix(connectors): MCP setup off the desktop returns unavailable instead of blocking
run_mcp_operation treated a non-None connection_callback as "a card exists". Every tui_gateway
session has that callback, the Ink TUI included, so an MCP install from the terminal UI blocked
until the 300 s deadline while the docs promised `unavailable` with the terminal commands.
The MCP path now reads the session surface the same way the managed path does; the callback is
never the predicate. Test binds the surface to `tui` with the callback attached.
* fix(connectors): a failed Try again shows the failure, not the old dead link
The panel's re-mint ignored the gateway's per-app status and moved the row to `initiated` with
whatever link came back, `None` included, so a mint that failed again rendered as waiting on the
link that had already died.
One reader of a mint response now serves both the first mint and Try again
(`managed.mint`, with the actor as a parameter). A repeated failure keeps the row `failed`,
drops the link, and carries the vendor's new text through `operation.refresh`, which emits a
frame without a state change so the card redraws.
* fix(connectors): a forced reconnect waits for the new sign-in before it reports connected
`reconnect` with `force: true` is the account switch. The vendor keeps the old account active
while the new link waits, so the first list read after the mint said `connected` and the
operation settled at once: the new link was dropped and the model was told the switch was done.
A forced target is marked awaiting_new_attempt after the mint. The watcher ignores its row until
the list shows the new attempt (`connectionStatus: initiated`) once, then trusts `connected`.
* fix(connectors): the operation registers under the gateway session key
The tool registered the operation under the agent's session_id; every RPC (connection.respond,
connectors.operation.status, the panel's connectors.connect) and the update emitter looked it up
by the gateway's session key. Those agree until compaction rotates the agent id mid-turn; then
the card's clicks find nothing, no update reaches it, and the tool waits out the deadline.
The registration key is now the bound HERMES_SESSION_KEY, with the agent id as the fallback for
callers with no gateway (unit tests, a bare CLI). The E2E passes a rotated agent id and drives
the card by the gateway key.
* fix(connectors): the forced-reconnect gate reads any non-active row; a failed re-mint of an expired row is failed
Three follow-ups from the verification of the fix pass.
The awaiting_new_attempt gate cleared only on the literal `connectionStatus: initiated`. The
field is optional on the wire and `initializing`, `failed`, `expired` are valid values, so a
forced reconnect could wait the full 300 s and swallow a failed new attempt. The gate now holds
only while the row still reads as the old account (`connected` or `active`) and releases on
anything else.
Try again on an `expired` row whose re-mint fails raised IllegalTransition (no expired → failed
edge). The re-mint steps through `initiated` as the user's attempt, then `failed`, then drops the
dead link.
`detail` never carries a state name any more: `failed` as detail rendered as the row label and
made agent/display.py tag the settled result as a tool error. Only vendor text goes there.
`connection.expire` removed from the renderer's unscoped-stream set; nothing emits it.
|
||
|
|
e0ef0eb9c3 |
manage_connections covers local MCP servers; setup_mcp leaves the schema (NS-867, PR1) (#109517)
* feat(connections): manage_connections covers local MCP servers; setup_mcp leaves the schema
One model tool now connects the user to apps of both kinds. A target
`{"name": "linear", "mcp": true}` is a locally configured MCP server;
`install` / `enable` / `authorize` are its verbs. Bare strings and
`{"name": ...}` stay managed connectors and that leg is unchanged.
MCP targets run through one backend-owned connection operation
(tools/connections_tool_operation.py): created with a server-side
deadline from the new config key `connections.wait_timeout_seconds`
(default 120, floor 5, no ceiling), per-target state, and exactly-once
settlement (all resolved / Continue / deadline / interrupt). Unresolved
targets freeze as `not_connected` with the settle reason.
Why the fold works now: the approval card is reached through
`agent.connection_callback` via the agent-level inline executor table,
which is the only path that carries a GUI callback. Registry dispatch
(every non-GUI surface) settles MCP targets as `unavailable` with the
`hermes mcp install / login` hint; managed targets in the same call
are unaffected.
`setup_mcp` is removed from every advertised toolset and from the
deferral list; an inline-table shim keeps calls from conversations
opened before this change dispatching (prompt-cache protection).
`_LEGACY_TOOL_ALIASES` is not the mechanism: inline tools bypass it.
Gateway: `mcp.setup.request/respond` are replaced by
`connection.request/respond/expire` (no wire compat; desktop ships
with this). The bridge waits exactly the operation's deadline. The
`session.resume` snapshot gains `pending_connection` so a reopened
window restores the card with the original deadline.
`manage_connections` joins `_SEQUENTIAL_DEADLINE_EXEMPT_TOOLS`: the
operation owns its wait; the 420s guard must not report `tool_timeout`
while the card is live.
The portal `check_fn` on the tool is dropped in favour of a
handler-level gate on the managed leg, so signed-out sessions can still
approve local MCPs.
* wip(desktop): connection.request store, resume restore, card routing for MCP targets
Renderer half of the setup_mcp fold, first slice: connection-request store
(mirrors clarify), connection.request/expire handling, pending_connection
resume restore, mcpTargets() + isCardTool(name, args) so MCP-target
manage_connections calls classify as cards. Not yet: the card component
rewrite (mcp-setup-tool.tsx), mcp-directory.ts removal, vitest, docs.
Does not typecheck until the card rewrite lands.
* fix(config): hermes update turns on the connections toolset for saved toolset lists
`hermes tools` writes an explicit `platform_toolsets.<platform>` list, and the
resolver reads absence from that list as "unchecked". The `connections`
toolset (#106842) shipped after most users last saved, so `manage_connections`
is stripped from the schema on every install that ever opened the picker.
The Nous entitlement gate never runs; the agent reports the tool as missing.
Migration 44 -> 45 (renumbered when folded into #109517; main was already at 44) appends `connections` to each explicit per-platform list
that lacks it and records the offer in `known_builtin_toolsets` where that
record exists, so a later uncheck reads as a decline. It skips: platforms
whose record already holds `connections` (the user saw the checkbox and left
it off), bare composite lists ([hermes-cli]) that already inherit it, platforms
where the toolset is not allowed, and any config whose `agent.disabled_toolsets`
names `connections` (Blank Slate, `hermes tools --disable`), because the
resolver subtracts that list last and the enable would never take effect.
The explicit-list test is the resolver's own: any configurable or plugin key.
`hermes update` runs migrations post-pull for the active profile and every
sibling, so one update is enough. Fresh installs and composite users were
never affected.
* refactor: anti-slop pass on the desktop slice; shorten added comments
Parse connection.request at the boundary with a typed wire interface instead of
unknown + typeof; mcpTargets reuses connectorText; comments cut to one or two
lines. slop-ratchet: no net-new findings in 13 touched files.
* feat(desktop): the MCP approval card answers manage_connections; MCP Directory removed
The existing card (mcp-setup-tool.tsx) now reads the connection-request store,
renders for manage_connections calls with mcp:true targets, answers through
connection.respond with a per-target outcome, and no longer calls reload.mcp
after Install; the new server's tools arrive on the between-turns refresh.
A settled operation renders the first target's frozen state.
session.resume restores a pending card with its original deadline on both the
activate and cold-resume paths.
lib/mcp-directory.ts is deleted along with its two fallback branches
(suggestion provider, card install). The catalog was already primary in both;
a catalog miss now yields no suggestion / a notInCatalog error. The GitHub
never-suggest test is rewritten on catalog-shaped data.
vitest: connection-request store (6), suggestion provider, clarify restore.
slop-ratchet: no net-new findings in 19 touched files.
* chore: drop __pycache__ files swept in by an over-broad git add
* fix(desktop): correlate the connection.request row with the model's tool call by reason
The synthetic row from connection.request and the tool.start row carried
different ids and no shared match value (op_id is not in the model's args),
so the card mounted twice. reason is the arg both sides carry.
* docs: manage_connections covers local MCP servers; connections.wait_timeout_seconds
* fix(connections): settle reason derives from target state, never from the renderer
A card that answers one of two targets and claims all_resolved must settle as
continue with the other target not_connected; found live with a two-target call.
* fix(desktop): a pending connection card re-arms on resume and activate
The store entry was restored but the transcript row was not, so navigating
away and back (or reloading) lost the card while the backend kept waiting.
restorePendingClarifyToolCall's core is generalized to any blocking tool
name and both resume paths project the connection row through it.
Verified live: card restored after navigate-away and after a full renderer
reload, deadline_at unchanged, approve settles connected.
* style: literal wording in added comments, docstrings and docs
* fix: shared gateway-event contract and config-schema category for the connection events
connection.request/expire replace mcp.setup.* in apps/shared gateway-events
(json list, BACKEND_EVENT_NAMES, GatewayEventMap) so the renderer's event
union includes them and the tui_gateway contract test passes. The new
`connections` config section folds into the agent tab like the other
single-field sections.
* style: import order (perfectionist) in the desktop and shared files this PR touches
* chore: retrigger CI (zero-job dispatch failure, auto-heal)
|
||
|
|
9c9e7ab6e5 |
fix(multiplex): a served profile's turn sees its own cwd, approvals, redaction and tool policy
Under gateway.multiplex_profiles a secondary profile's turn ran with the LAUNCH profile's working directory, command allowlist, redact_secrets switch, credential file mounts, browser engine/headed flags, LSP service, auxiliary-provider health marks and MCP stderr log, and several TERMINAL_ENV consumers read the process env instead of the routed profile's terminal scope. A standalone `hermes -p X gateway run` never behaved that way. - tools/terminal_scope.py: resolve the terminal.cwd placeholder inside the profile scope with the same rule gateway/run.py applies at import (local -> $HOME, sandbox default otherwise) so the system prompt, context files and the terminal of a routed turn start where the profile's standalone gateway would. - tools/image_source.py, credential_files.py, image_generation_tool.py, skills_tool.py, delegate_tool_progress.py, agent/tool_executor.py: read TERMINAL_ENV / TERMINAL_CWD through the terminal scope. - tools/approval.py (+ approval_floors.py): one permanent allowlist per routed profile home; the unscoped module set stays for single-profile processes. - agent/redact.py: `_redact_enabled()` resolves security.redact_secrets for the routed profile (scope .env, then config); launch snapshot kept when unscoped. - tools/credential_files.py, agent/auxiliary_health.py, agent/lsp/__init__.py, tools/browser_tool_cloud.py, tools/mcp_tool_config.py, tools/tool_result_storage.py: key process caches by profile home (or bypass the slot under an override). Tests: tests/tools/test_multiplex_turn_parity.py (4, red on base). Docs: multi-profile-gateways.md isolation table. |
||
|
|
b4d04eb8fd |
Connector tools (Gmail, Linear, Notion, ...) are searchable and callable through tool_search for signed-in Nous users (#106842)
* feat: add session-scoped connector access for onboarding
* fix(connectors): availability is the config flag AND the portal entitlement — no free-tier leg
The port carried a third availability leg from hermes-magic: a stored guest
(free-tier) identity short-circuits the managed-tool entitlement check. That
leg reads hermes_cli.anon_auth, which does not exist on hermes-agent main, so
connectors_available() raised ImportError inside its fail-closed try and the
whole connector surface was silently dark on a plain upstream checkout.
On this tree availability is the two-leg AND the design started with:
tools.connectors.enabled AND managed_nous_tools_enabled(). The free-tier leg
is a hermes-magic concern and belongs in hermes-magic's own delta over this
branch, next to the identity it depends on. Its integration test goes with it.
* docs(tool-search): connectors section — remote tools through the bridge
The squashed port carried the code but not the user-facing docs. Restores the
Connectors section of the Tool Search page and the connector-gateway host /
CONNECTOR_GATEWAY_URL override on the Tool Gateway page, updated for the
manage_connections tool and the pure-connector batch rule.
* fix(tool-search): connector tools rank with local tools in one pass instead of taking leftover slots
dispatch_tool_search ran BM25 over the local catalog, filled `limit` slots,
then appended connector hits only into slots left empty. On a 300-tool
catalog no slot was ever empty, so with Gmail and Google Calendar connected
"send gmail email" returned five betterstack tools and zero connector tools.
The gateway's hits for a query now become catalog entries (connector name,
slug words, description as the search text) and join the local catalog for
that query's BM25 pass. One ranking, one rarest-token admission rule for both
sources, `limit` as the total per query. The merge loop and the separate
record builder for connector hits are gone; `_shared_tool_record` serves both
sources.
The gateway search timeout rises from 8 s to 30 s. One request with six
use_cases measured 7 s, so 8 s sat on the edge and cut real answers off; the
failure path is unchanged (local-only results, no error to the model).
Live, 311 local tools + gateway, before -> after:
"send gmail email": 5 betterstack tools -> gmail SEND_EMAIL, CREATE_EMAIL_DRAFT
"read google calendar events": 5 betterstack tools -> googlecalendar EVENTS_LIST_ALL_CALENDARS
"linear create issue", "betterstack incident": unchanged
Benchmark (25 labelled queries): connector recall 0.09 -> 0.82, precision@5
0.18 -> 0.59, false positives on absent intents 17 -> 2.
* refactor(tool-search): connector leg into tools/connector_search.py
tools/tool_search.py is a facade. The connector leg (gateway hits as catalog
entries for tool_search, remote schemas for tool_describe, the
connections_in_scope gate) was appended to it by the port. It now lives in
its own sibling, tools/connector_search.py, and the facade imports the three
entry points: connections_in_scope, connector_entries_by_group,
remote_schemas_for.
No behaviour change. The tool_describe remote block became
remote_schemas_for(names, current_tool_defs, connector_describe) with the
same inputs, the same silent-degradation contract and the same injection
seam the tests already use.
* fix(tool-search): at most 7 queries per call, the gateway's search limit
One tool_search call sends all its queries to the connector gateway as one
search request. The gateway answers 7 use_cases per request and returns
HTTP 502 for 8 or more (measured 2026-09-09, re-measured with one-word
use_cases: it is a count limit, not a size limit). With the client cap at
10, a model sending 8 to 10 queries lost every connector hit for that call
and saw local-only results with no error.
The shared constant splits: _MAX_QUERIES_PER_CALL = 7 for search,
_MAX_DESCRIBE_NAMES_PER_CALL = 10 for describe, which has no remote count
limit. Eight or more queries now get the existing "too many queries" retry
hint before any request is made. No chunking: one call, one request.
* fix(tool-search): the model is told that connectors__ names are manage_connections accounts
tool_search results carry names like connectors__gmail__CREATE_EMAIL_DRAFT and
manage_connections is the tool that checks and connects those accounts, but
nothing told the model the two are the same thing. A model that hit
CONNECTION_REQUIRED had to infer the fix on its own.
The tool_search description gains one sentence making the link, added at
assembly only when manage_connections is in the session's tools. Signed out
or with connectors off the tool is absent and the description is unchanged,
so it never names a tool the model cannot call. This follows the existing
rule for cross-tool references (tools/AGENTS.md): they are added dynamically
from the session's actual tool set, never hardcoded in a schema.
Tool defs are fixed for the life of a conversation, so the description is
byte-stable per conversation; this is a one-time prefix change.
Live, real get_tool_definitions() against a signed-in home: sentence present.
Same home with auth.json removed: manage_connections absent, sentence absent.
* fix(connectors): /stop halts a connector batch before the next remote call
dispatch_connector_batch runs every remote entry of a tool_call batch in
sequence. The executor only checks the interrupt flag between tools, and
the whole batch is one tool to it, so a /stop landing during entry 1 of
20 still sent the other 19 to the gateway.
The loop now reads tools.interrupt.is_interrupted before each dispatch.
Once set, it stops calling handle_function_call and fills every unstarted
slot with the loop's existing error-slot shape, code INTERRUPTED and the
message "Stopped by the user before this call was made.", so the result
envelope stays valid and the counts stay honest. Entries already
dispatched keep their real results.
Test: three connector calls where the fake client sets the interrupt on
the first execute. The client sees exactly one call and slots 2 and 3
carry INTERRUPTED. Red on the base branch, green with the fix.
* test(connections): schema assertions become dispatch contracts
test_schema_documents_wait_and_its_timeout froze description fragments
("REQUIRED", "can NOT disconnect", "Nous Portal"). A wording edit fails
it while a real regression (a disconnect that reaches the gateway) does
not. That is a snapshot of prose, not a behaviour contract.
Delete it. The requirement that wait needs connectors is already covered
by test_wait_requires_connectors. The user-only disconnect boundary is
now asserted as behaviour: action disconnect with a connector returns an
error and the fake client records no call. That replaces the earlier
de-authenticate test, which only checked that the word "dashboard"
appeared in the error text.
Test count in the file goes from 26 to 25.
* docs(tool-search): connector batches are one gateway request per entry
The user guide said a connector batch travels as one gateway request. It
does not: model_tools_connectors.dispatch_connector_batch re-enters core
dispatch per entry, and each entry becomes its own execute request in
bridge._run_remote (plus at most one literal-slug retry when the gateway
reports TOOL_NOT_FOUND under the conventional slug). The docstrings in
tools/tool_gateway/bridge.py and tools/tool_gateway/__init__.py still
described the abandoned V1 plan and claimed nothing outside the package
imports it.
Rewrite those sentences to match the code: one request per entry, in
input order, dispatched from model_tools_connectors.py, with the per-entry
approval and interrupt behaviour that motivated the split. The guide also
still showed the single-call shape tool_call(name, arguments); both
places now show the `calls: [{name, arguments}]` array the schema
advertises and note that a single local call is an array of one.
Docs only, no test.
* fix(tools): the between-turns refresh never rewrites the bridge tools
The per-turn MCP refresh folds a fresh tool snapshot into the live array
with preserve_prefix: order and membership stay, but a name present in both
takes the fresh schema. That is right for ordinary tools, whose schema is a
constant. tool_search is the one tool whose description is derived from the
session: the deferred-tool count, the embedded listing, and, on this branch,
whether manage_connections was present. A late MCP server or one failed
portal lookup (manage_connections' check_fn fails closed) changed those bytes
on the next turn, and every byte after tool_search in the cached prefix was
re-prefilled. The array also contradicted itself in that case: the flapping
manage_connections was carried forward while the description lost its hint.
The bridge entries now keep the bytes they were built with for the life of
the conversation. Nothing is lost: tool_search reads the live catalog at
dispatch, so tools that arrived late are still found; connector availability
is checked at dispatch too. The compaction-boundary rebuild (content_aware,
the one sanctioned cache break) still refreshes the description.
Consequence: connector exposure in the prompt is decided once, at agent
build, by whether the user was signed in then. That is the intended
contract.
* refactor(tool-search): normalize_tool_call_entries lives with the other argument validation
The port appended the tool_call argument parser to the tool_search facade.
The family already has tools/tool_search_validation.py for exactly this
work (schema validation of deferred call arguments), so the parser moves
there and the facade imports it. No behaviour change; the one test that
imported it now imports from the defining module.
* refactor(connectors): delete the unused batch dispatcher; _run_remote becomes run_remote
bridge.dispatch_calls and its helpers (_dispatch_calls_inner, _run_pre_dispatch,
_run_local, _error_slot, _maybe_parse_json) and the LocalDispatch / PreDispatch
seams had no production caller. Connector dispatch runs through
model_tools_connectors: dispatch_connector_batch re-enters handle_function_call
once per entry, so scope, hook, approval and middleware policy fire against each
composed name inside core dispatch, and dispatch_connector_call hands the single
planned entry to the bridge's transport function. Only tests called the batch
dispatcher, and they exercised policy seams that production never wires.
The transport function is the module's real entry point, so it drops the
underscore: _run_remote becomes run_remote, body unchanged. The module
docstring now describes the two legs that exist (availability with D32 silent
degradation, and run_remote) instead of the injected seams. Imports that only
the deleted code used are gone; merge.py is untouched because every export
still has a caller.
Tests that drove dispatch_calls are deleted where they covered the removed
seams (pre_dispatch blocks and rewrites, local_dispatch classification, mixed
batches). The literal-slug fallback, the per-entry transport failure, and the
hook rewrite reaching the gateway request body are re-targeted at
handle_function_call('tool_call', ...) with the fake client swapped in at
bridge._default_client_factory, the same seam test_connector_dispatch_policy
uses. Each re-targeted test fails when the retry is disabled in run_remote.
* fix(connectors): search keeps the twin a colliding name reaches, and says so
format_connector_name strips the toolkit prefix, so GMAIL_FETCH_PROFILE and a
literal FETCH_PROFILE on gmail both compose to connectors__gmail__FETCH_PROFILE.
describe and execute decode that name to the prefixed slug first, so the
literal twin is unreachable under it. If a vendor ever shipped both, search
could describe the literal under a name that runs the prefixed tool.
Search is the one place that sees both twins in one response. It now keeps
the twin the name reaches and drops the other with a WARNING that names both
slugs, whichever the gateway listed first. Short names stay; no marker, no
per-process map, no change to describe or execute. No such pair exists in the
live catalog today; the guard turns a silent alias into a logged one.
|
||
|
|
511633be90 |
fix(agent): stop the run-budget wrap-up notice from mutating a persisted tool row
_maybe_inject_run_budget_wrapup() appends its wrap-up notice to the newest role:"tool" message in place, with no _DB_PERSISTED_MARKER check. Its sibling, _maybe_inject_iteration_budget_warning(), got exactly this guard added in the same recent saga (turn_iteration_prep.py), with the comment "an older turn may already be cached." The reachability is structural, not an edge case: _maybe_inject_run_budget_wrapup is only ever called from prepare_iteration(), at the START of the next iteration -- strictly after tool_executor.py's _flush_session_db_after_tool_progress has already flushed and marked the previous iteration's tool row persisted. So every successful injection was mutating an already-persisted row: the wire request for that turn carried the notice, but the durable transcript never did, diverging replay from the live bytes and invalidating the provider's prompt-cache prefix from that row onward. Fix: - Add the same _DB_PERSISTED_MARKER guard to _maybe_inject_run_budget_wrapup, scoped to the specific tool row the reversed scan lands on (not just messages[-1], since this function -- unlike its sibling -- scans backward for the newest tool row rather than only checking the tail). - Wire _maybe_inject_run_budget_wrapup into _flush_session_db_after_tool_progress (pre-flush), mirroring exactly how _maybe_inject_iteration_budget_warning is wired in both places. Without this, the guard alone would make the notice stop firing in the common case, since prepare_iteration's call site almost always hits an already-persisted row -- the pre-flush call site is what actually lets it land in durable bytes. Verified empirically: read the real call graph (tool_executor.py's three _flush_session_db_after_tool_progress call sites cover every tool-completion path) to confirm the guard's premise, then added an end-to-end test using a real AIAgent + SessionDB that flushes and checks the persisted row for the notice text. Mutation-verified: reverting the two production files drops exactly the 2 new/updated assertions (28 pass, 2 fail); reapplying restores green (30 passed). Also ran the sibling iteration-budget-warning and /steer suites (71 passed) to check for interaction regressions -- none. |
||
|
|
93af3db01d |
fix: checkpoint Kanban completion before tool access expires
Give dispatcher-owned workers a tool-capable reporting opportunity before the hard iteration cap, without accepting arbitrary diffs or weakening failure counting. Add opt-in per-turn iteration checkpoints for ordinary agents. Persist checkpoint text with the fresh tool result, never rewrite cached rows. Salvages the opt-in ratio and per-turn reset implementation from #104683; credits the earlier default-off signpost proposal in #92438. Local fixture wire A/B: Kanban ready/1 failure -> done/0; deliberately stuck workers still reach blocked/2 after two runs. Default-off control unchanged. Targeted and affected-directory suites queued behind campaign test lock. Co-authored-by: fangliquanflq <fangliquan@qq.com> Co-authored-by: C. Michael Gibbs <252231331+MikeGibbsOnyx@users.noreply.github.com> |
||
|
|
903b9bf187 |
fix(delegate): nested orchestrators get their workers' results back — no 420 s deadline on delegate_task, summary budget uses the current prompt not the session sum
Two defects in the same path, both measured on the 1,393-agent refactor run. 1. A nested orchestrator (depth > 0) runs delegate_task synchronously by design: it needs its workers' results inside its own turn. But the sequential tool runner put every tool call under the generic 420 s deadline, and delegate_task was not exempt, so every batch longer than seven minutes returned "timed out after 420.0s" while the children kept running as orphans. 332 such timeouts in 234 orchestrator sessions; only 89 nested delegate_task calls in the whole run ever returned a real result. The orchestrators then spent 388 h of wall time polling: 1,526 reads of the live transcript files, 551 list actions, 242 h of explicit sleep, about $4k of API turns. delegate_task is now exempt from the sequential deadline (the batch owns its liveness: per-child heartbeats, the stale monitor, delegation.child_timeout_seconds). Live A/B, depth-1 orchestrator dispatching a 75 s leaf with the deadline set to 40 s (glm-5.3-flash via Nous): main -> "Error executing tool 'delegate_task': timed out after 40.0s", leaf result lost; branch -> orchestrator blocked 161 s and returned the leaf's LEAF_DONE_MARKER. 2. _parent_summary_char_budget computed the parent's context headroom from session_prompt_tokens, which is the running SUM of prompt tokens over every API call in the session. After a few hundred calls it exceeds any window, headroom goes negative, and every child summary collapses to the 2,000-char floor with the full text spilled to disk. All 1,393 child summaries in the run were truncated this way; the orchestrator planned from stubs. The budget now reads the last call's prompt_tokens from _last_turn_usage. Tests: delegate_task is in the exempt set and the set is narrow; budget for a long-lived parent equals the budget for a fresh parent with the same current prompt, and exceeds the floor. |
||
|
|
b92308b1d2 | simplify(compat): tools-A — repoint 4 stale docstring references (tools.approval.*, tools.transcription_tools.*) to the defining modules | ||
|
|
14791b4d4e |
simplify(compat): approval — drop 43 facade re-exports + _command_detection_variants late-bind seam, repoint 30 callers + 46 test files
tools/approval.py no longer re-exports sibling names (approval_context/prompt/floors/detection/ human_wait/smart/gateway_wait); it imports only what it uses. Siblings reference sibling-defined names directly (module-attribute reads on tools.approval_context so patching the defining module still works); only facade-owned state (_lock, _gateway_queues, _permanent_approved, _denied, _denial_breaker_addendum, _gateway_notify_cb) is still read back through tools.approval. approval_detection calls its own _command_detection_variants instead of late-binding through the facade. |
||
|
|
e43402fd83 | simplify(compat): file_operations/file_tools — drop 47 re-exports/aliases, repoint 6 callers + 17 tests | ||
|
|
b818085298 | simplify(compat): doctor/status — drop 13 re-exports + the doctor_* globals() facade (97 names), repoint 6 callers / 13 tests | ||
|
|
d98c48b410 | review-fix(comments): restore lost rationale in the 7 files the sweep had to skip (docstring/comment-only) | ||
|
|
0071ba9965 |
Merge origin/main (561b053f79) into simp/forwardport: forward-port 220 main commits into the simplified tree
|
||
|
|
2252200b2a | refactor(agent/tool_executor): contextlib.suppress for best-effort blocks; _preview helper; alias _pairing_tool_call_id | ||
|
|
a64889ef3d | refactor(agent/tool_executor,tool_dispatch_helpers): compact spinner/dispatch helpers, planner closures, envelope helpers | ||
|
|
8efd6a682a | refactor(agent/tool_executor): fold observe into _commit_tool_result; _ToolOutcome carries its ref; merge sequential abandon branches | ||
|
|
3ca2955138 | refactor(agent/tool_executor): concurrent workers index parsed_calls directly; compact gate/auth helpers | ||
|
|
856d778df0 | refactor(agent/tool_executor): flatten tool_search unwrap; compact docstrings/comments by hand | ||
|
|
f974353253 | refactor(agent/tool_executor): _safe_callback guard replaces 5 try/except-log callback blocks | ||
|
|
15750e88ce | refactor(agent/tool_executor): route begin/complete/middleware kwargs through _ToolCallRef; merge batch abandon branches | ||
|
|
48c0c3a873 |
Merge pull request #96633 from afourniernv/chore/relay-0.8.0
fix(relay): upgrade to 0.8.3 |
||
|
|
12aebbfcbc | refactor(agent/tool_executor): _ToolCallRef identity carrier + emit_post funnel replace 9 hand-rolled terminal-hook blocks | ||
|
|
b66776d594 | refactor(agent/tool_executor): split executor god functions into phase helpers; unify observe/commit and start-gate handling | ||
|
|
3f93fdfc95 |
refactor(tool_executor): split execute_tool_calls_concurrent into _ConcurrentBatch + shared result helpers
Behavior-neutral extraction of the 686-LOC concurrent executor and the 429-LOC sequential executor into focused helpers: - _ConcurrentBatch (run_worker / submit_all / await_completion / run) and _StartOrderGate replace the nested closures + nonlocal counters; _ToolOutcome replaces the 7-tuple result slots; _ParsedCall/_parse_tool_call replaces the 6-tuple parsed-call rows in both executors (-> 2 sites). - _append_skipped_tool_results unifies the four cancelled/skipped-result loops (concurrent pre-flight, sequential pre-tool interrupt, sequential KeyboardInterrupt, sequential post-tool interrupt) -> 4 sites; absorbs _append_cancelled_tool_results. - _observe_tool_result / _commit_tool_result / _finalize_tool_batch / _print_tool_completed / _tool_progress_enabled unify the post-execution guardrail-observe -> append+flush -> tool.completed -> budget -> /steer tail shared by the concurrent, sequential and segmented paths (-> 2-3 sites each). - _unfinished_tool_result, _blocked_tool_result, _abandoned_sequential_result collapse the duplicated synthesize-result + terminal post_tool_call blocks. - _registered_tool_worker / _interrupt_worker_tids unify worker tid tracking and interrupt fan-out between the two middleware runners (-> 2 / 3 sites). - _run_with_activity_heartbeat extracts the heartbeat thread wrapper. - _resolve_sequential_dispatch + _SequentialDispatch turn the 5-branch inline/delegate/context-engine/memory/registry if/elif into a resolver with per-branch spinner/error/KeyboardInterrupt policy, preserving branch order. - _cancelled_tool_result and _managed_values inlined (single caller each). Public signatures (execute_tool_calls_concurrent/sequential/segmented, both middleware runners, every symbol imported by run_agent.py/agent/tests) are unchanged; middleware/hook/persistence/progress-callback order is identical. |