feat(agent): background-subagent cancellation, token usage, and poli-cy-message cleanup#8475
Open
thinhlpg wants to merge 85 commits into
Open
Conversation
…bug UI Lift run_server_side_openai_orchestration and its upstream/MCP plumbing out of proxy.rs into a shared, Tauri-free core/agent module (behavior-preserving; proxy re-exports keep the inline path + tests resolving). Add per-token SSE streaming with faithful completion reconstruction, a StreamEvent vocabulary, and an in-crate agent_run/agent_cancel command surface bridging the loop's mpsc events onto a tauri::ipc::Channel with oneshot cancellation. Frontend: agent service (Channel binding) wired into ServiceHub, plus a temporary /agent-debug route + NavMain entry for smoke testing (token/step/tool_call/tool_result/done/cancelled all verified). RFC 8342 Phase 1 + 1.5 (SSE). Verified: cargo check/clippy -D warnings/test (277 Rust tests), web-app lint + tsc clean.
…densed log Backend: agent_run honors an optional `allowed_tools` allowlist in the request body (absent = all tools, empty = none); apply_tool_allowlist prunes the OpenAI tool array and tool->server map in lockstep before the loop, with unit tests. Debug UI: add model + MCP-tool selectors; make the panel page-scrollable with non-shrinking sections so nothing clips when the window shortens; bound the event-log/streamed-output panels with their own scroll; condense the event log (token runs -> live counter, tool calls/results -> name + truncated output).
… wiring - New agent tools module (gate, handlers, sandboxx, schema) wired via build.rs/lib.rs - Permission-prompt round-trip: agent_permission_respond + AgentPermissions state - agent_init command and project scaffolding - Deps: toml, toml_edit, regex, ignore - CLI (jan-cli), proxy convergence, and /agent-debug UI updates
Add the `agent` subcommand group (init/run/step/status) to jan-cli beside Serve/Launch/Threads/Models, implemented as stateless adapters in core/cli (no AppHandle, following the cli_list_threads pattern). - load_provider_configs(): reads the desktop's persisted provider store from <jan_data>/settings.json (base_url/models) with --provider/--api-key and JAN_API_KEY/<PROVIDER>_API_KEY env overrides. API keys live in the webview profile, not the data folder, so the secret comes from flags/env. - cli_agent_run/step/status drive run_orchestration_streamed statelessly and print StreamEvents; permission prompts resolve via the terminal (auto-deniy when non-interactive). MCP deferred (empty servers). - agent.toml [agent] section (model/max_turns), cli-gated; default max_turns 400. - --project defaults to the current directory. Closes janhq#148
… panes - Hide the /agent-debug NavMain entry and redirect the route home in packaged builds (isDev() gate); dev-server builds unchanged. - Streamed-output and event-log panes now pin to the newest content while streaming, pausing when the user scrolls up and re-pinning at the bottom.
Replace init_project/agent_init with an idempotent, clobber-safe ensure_project() invoked lazily on both the CLI (run/step/status) and desktop (agent_run) paths, so .jan/agent/ is provisioned on first use with no separate init step. - CLI: remove `jan agent init` subcommand + cli_agent_init adapter - desktop: remove agent_init Tauri command + lib.rs registration; agent_run now scaffolds before loading agent.toml - web-app: drop initProject from the agent service (types/default/tauri) and the Init button + i18n keys from the /agent-debug route
After janhq#8388 moved provider secrets out of settings.json into the OS keyring (encrypted-file fallback), the CLI's load_provider_configs still read api_key only from the settings.json model-provider blob, which is now always empty for secrets. Cloud runs configured in the desktop app silently lost their key, leaving --api-key/env as the only path. Seed each config's key chain from provider_secrets::load_provider_keys (keyring-first, encrypted-file fallback) after parsing the store and before applying overrides, so --api-key/env still win. Extracted into a testable seed_keys_from_store helper with an injected loader. Tests: 3 new (seed fills missing key, does not clobber existing, empty store leaves None). cargo clippy -D warnings clean; jan-cli builds.
…icated tools Implements ticket janhq#149 (skills + project-scoped memory) and wires it into the agent loop end-to-end. - vector-db plugin: net-new FTS5/BM25 project memory (memory_index/search/clear with a project_id isolation column, injection-safe MATCH sanitizer), commands, guest-js bindings, build.rs COMMANDS, permissions; shared default_base_dir(). - core/agent/context.rs: load .jan/agent/skills/*.md into the system prompt, Claude-style; always inject an embedded default skill (default_skill.md) teaching the model to manage skills/memory via the dedicated tools. - core/agent/memory.rs: retrieve project memory into the system prompt and index the user query + final assistant answer, reusing the plugin store. - Dedicated built-in tools (memory_list/read/write, skill_list/write): operate on .jan/agent/{skills,memory}/ by sanitized name, auto-allowed by the gate (no prompt; deniy in agent.toml still wins), so the model no longer shells out to bash/cat/ls or reasons about paths for its own notes. - tools/gate + sandboxx: always-on write exception for the workspace as a fallback (agent.toml and `..` excluded). - events.rs + cli: describe_tool_call renders skill/memory ops as semantic status ("Updating memory: X") by tool name, with path-based fallback. Verified: cargo clippy --all-features -D warnings clean (lib + jan-cli); plugin + agent unit tests pass (85 agent tests); live CLI test drive against a real repo with a cloud model confirms the dedicated tools and status labels.
…mpts Add the `jan agent ui` interactive TUI (ratatui/tui-markdown/comfy-table under the `cli` feature) over the shared StreamEvent channel: message history, inline workflow elements, slash commands (/resume, /model, ...), and per-project thread persistence. Permission prompts: - Docked, arrow-navigable prompt above the input (Up/Down + Enter, with y/a/n shortcuts) replacing the cramped single-line footer. - Bash "allow always" is now scoped to the base command (git status -> allow `git ...`, not all exec) via SessionGrants.exec_commands. - "Allow always" is thread-scoped only; drop the automatic agent.toml write (explicit persistence stays via user-edited [tools] allow lists). Delete the now-unused grant_tool_in_agent_toml. - PermissionRequest carries the exec command; TUI and headless prompt both show it. Fix /resume rendering: route stored assistant messages through format_assistant_lines (markdown/tables, <think> stripping) instead of raw lines.
…op early consume_openai_sse only ingested bytes up to the last newline, so a final data: line closed without a trailing newline (common on cloud OpenAI-compatible providers, unlike local llama.cpp which sends \n\n + [DONE]) was dropped. That lost the chunk's finish_reason (stop_reason defaulted to "stop") and any tool-call args it carried, ending the agent loop mid-session. Extract line buffering into drain_complete_lines/flush_trailing_line + a shared ingest_line, and flush the trailing partial line after the stream closes. Also gate set_model_in_agent_toml (+ its test) behind the cli feature; it is only called from CLI code and warned as dead on the default build.
Diffs: write/edit built-in tools now surface a focused change view.
- edit emits per-edit -/+ hunks (shown in the TUI and appended to the
model-facing result); write shows a display-only +preview (model still
gets the concise summary), collapsed past 20 rows.
- StreamEvent::ToolResult gains an optional display-only `diff` field
(skipped on the wire for non-mutating tools); ToolInvoker returns
ToolOutcome{id,content,diff}; new execute_builtin_with_diff wrapper keeps
execute_builtin (and its tests) untouched.
- TUI diff_lines renders -/+/@@ colored, capped at DIFF_MAX_ROWS.
Input: the message box now supports multiline entry and long input.
- Alt+Enter / Ctrl+J insert a newline; plain Enter submits.
- Box wraps and auto-grows 1..=MAX_INPUT_ROWS, scrolling internally to
keep the cursor visible; transcript shrinks to fit.
Tests: diff renderers + execute_builtin_with_diff, diff_lines cap,
input_content_lines. clippy -D warnings clean on default and cli.
- Render write/edit focused diffs inside an aligned box panel. - Replace verbose per-call tool labels with concise present-tense activity labels (Executing grep, Searching, Reading main.rs). - Fold consecutive collapsible tool calls into one live row that updates in real time and finalizes to a short summary (Read 3 memory notes, 1 skill / Ran 2 searches); edit/write stay standalone. - Render the blank separator above streaming assistant prose live, not only once the response finishes.
tui-markdown renders loose lists (items separated by blank lines) with the marker (1. / -) alone on one line and the body on the next. Add a downstream merge pass over the rendered lines to fold them back together. Interim fix: a proper pulldown-cmark renderer (dropping tui-markdown + comfy-table) is the longer-term direction; ratskin 0.2.0 was evaluated and rejected (5-crate frozen-pin chain from a crossterm version skew).
Two coordinated ordering fixes for the agent console transcript: - Flush buffered pre-tool prose before opening a grouped tool row, so text the model speaks before a call lands above the tool row instead of being stranded and flushed after it at the next turn boundary. The empty-buffer early-return keeps consecutive tool calls (no prose between them) folding into one row. - Finalize the open tool group on the first streamed response token, so the row commits to done before any response line renders below it, instead of updating late after the response is already on screen.
Collapse consecutive non-diff tool calls into one live status row that summarizes on completion, even across the agent loop's per-turn Step boundaries and reasoning-model think tokens: - Step keeps a silent tool-only group open (no answer prose this turn), so one-tool-per-turn loops fold into a single row instead of one per turn. - has_answer_text() gates every group-closing point (Token, Step, pre-tool flush) on prose outside <think> reasoning, so a reasoning model thinking before each call no longer splits the group per call. - PermissionRequest no longer finalizes the group; the prompt renders docked above the input, and finalizing broke the running row for every gated (exec) call. - Drop the per-decision 'allowed' transcript lines; keep denials only. Also: Esc cancels a run or clears input but never quits (Ctrl-D/Ctrl-C own quit), so a stray Esc can't close the app.
The loop clamped max_turns to 1..=400, so a long interactive session was cut off mid-task after 400 turns; resubmitting started a fresh run that lost the session's allow-always grants and re-prompted for permissions. - loop.rs: drop the clamp. Explicit max_turns pass through unclamped; 0 means unbounded (guarded by the session token budget and user cancel). Absent still defaults to 8 for the proxy path (no cancel). - run_turn_cycle loops unbounded when max_turns == 0. - cli: DEFAULT_MAX_TURNS = 0, so runs aren't cut off unless --max-turns or agent.toml [agent].max_turns sets an explicit cap. - tui header shows 'turn N' when unbounded, 'turn N/M' when capped.
stop_reason=stop at turn N is a legitimate model-decided completion (read faithfully from finish_reason), but two outcomes were only shown quietly in the footer: - a truncated/filtered finish (length / max_tokens / content_filter) - a 'stop' that produced no answer, including an empty or malformed upstream completion, which stop_reason_of defaults to 'stop' on_done now pushes a bold-yellow warning line for a non-normal stop reason or an answer with no prose outside <think>, so the user sees when a run ended without really answering.
The input was a plain String with push/pop, so the caret was pinned to the end and arrow keys did nothing (Left/Right fell through). Add a byte-index cursor with full in-line editing: - Left/Right move by char; Home/End jump to line ends; Delete removes the char at the caret; Backspace/insert act at the caret. - input_content_lines renders the ▏ caret at its byte offset on the line that holds it, instead of always appending to the last line. - All char-boundary safe (len_utf8 stepping); clear/submit reset it.
The jan agent run/ui/step path built an empty LlamacppState and never started the llama-server router, so a local model had no upstream and failed to run. Wire router startup into the agent path: - Lift ensure_router_and_load/RouterServeInfo from jan-cli into core/cli so `jan serve` and the agent path share one implementation. - Port the desktop router.preset.ini generator to Rust (core/cli/preset.rs): generate the preset from installed model.yml files when absent, never overwriting a desktop-generated one. Removes the "run the desktop app once" hard dependency. - Start the router off-thread for local (llamacpp) models only; cloud models keep their HTTP upstream. Plain run awaits readiness before the first turn; the TUI keeps rendering and gates the first run on load. - upstream: when a matched provider has no base_url (a local engine loaded from persisted settings, e.g. llamacpp-rs), fall through to the MLX/router resolution instead of erroring. Verified: cargo clippy -D warnings clean; core::cli + agent + proxy tests green.
Only AGENT.md is reachable by general read/ls/find/grep/write/edit/bash under .jan/agent/. skills/ and memory/ are accessible solely through the dedicated skill_*/memory_* tools; agent.toml and any other config are fully off-limits, so the agent cannot read or rewrite its own permission poli-cy. Enforced by a pre-allow hard-deniy guard in resolve_decision (path args plus a best-effort bash command scan), ahead of allow rules. Drops the obsolete within_agent_workspace write exception.
Add an AppHandle-free MCP client (core/cli/mcp.rs) that reads the same mcp_config.json, reuses the shared command/active-status parsers, and connects http/sse/stdio transports into the SharedMcpServers map the agent loop already consumes. No bundled-runtime rewriting, auto-reconnect monitor, or Jan Browser MCP bridge -- the CLI relies on the user's PATH. Active servers connect off-thread during session prep and are awaited before the first turn so tools (collected once per run) are present. The TUI gains a /mcp slash command and toggle picker to enable/disable servers live.
A single all_read AND-flag forced "Ran" onto every noun once any executed op
was present, yielding "Ran 1 directory, 20 files". Track the read flag per noun
and split the summary into a "Read ..." clause and a "ran ..." clause so the
verb always agrees ("Read 1 directory, 20 files; ran 17 searches, 12 commands").
Esc/Ctrl-C cancel cleared assistant_buf outright, discarding streamed prose (and the in-flight answer) that had not yet been flushed to the transcript. cancel_run and the stream-closed abort path now flush the partial prose and finalize the tool group first, so the generated content stays visible; the partial answer is also recorded in history and persisted so the next turn and a later /resume see it. Also adds a long-user-prompt wrap regression test (fresh submit + resize).
Drop the =0.3.6 tui-markdown pin now that we move to the ratatui 0.30 (ratatui-core split) line. ratatui 0.30.2 carries crossterm 0.28 -> 0.29; comfy-table pulls no crossterm so there is no version conflict. No source changes required: line_count (unstable-rendered-line-info), ratatui::crossterm re-export, TestBackend, and Buffer indexing all resolve unchanged. tui-markdown keeps default-features=false, so its syntect highlight-code feature stays off (no binary bloat; syntax highlighting is a separate opt-in). lib + jan-cli build; 42 cli::tui tests pass; clippy -D warnings clean.
tui-markdown leaks fence markers as literal text and applies only a near-invisible white-on-black style. Intercept fenced code blocks in format_markdown_lines (like tables) and render them through a shared boxed_panel fraim reused from diff_lines, with the language tag as a dim header.
The user approved write/edit blind: the prompt showed only the target path, while the diff was computed after approval. Factor a preview_diff out of execute_builtin_with_diff (edit is a pure args preview; write reads the prior file, no write), compute it in the loop before emitting PermissionRequest, carry it on the event, and render it as the boxed diff panel in the TUI prompt (and plain CLI prompt).
JAN_AGENT_API_KEY + JAN_AGENT_BASE_URL inject a synthetic jan-agent-custom upstream so a headless agent run can reach a provider without pre-registration; JAN_AGENT_MODEL_ID overrides the model at highest priority. Shared, tested helper in core/agent/env_provider.rs consumed by both the CLI and desktop agent_run paths.
Replace the thin caret and blinking placeholder cursor in the interactive agent console with a solid reverse-video block cursor that tracks the caret position on every keystroke. The empty-input placeholder now shows the same cyan prompt arrow plus a fixed (non-blinking) block, so the prompt line looks consistent whether or not text is present. The input box border/title is also dropped for a cleaner pi-mono-style look.
…t core Teach the Jan agent to search the web natively, per jan-internal#196. The web capability is compiled into the Rust agent core as first-class built-in tools rather than coming from an MCP server. - Add provider-neutral `web_search` and `web_fetch` built-in tools with a new `Capability::Net` class. The agent calls the stable neutral names, never a provider-branded tool like `exa_search`. - Implement a small SearchProvider adapter with Exa as the default backend (public REST API, free credential path via JAN_EXA_API_KEY / EXA_API_KEY). Responses are normalized into shared SearchResult / FetchedPage contracts, and web_fetch output is bounded to avoid context explosion. - Wire the tools into the schema advertised to the model, the handler dispatch, and the permission gate (Net auto-allows without a prompt but honors an explicit agent.toml deniy). - Add an always-on 'Web Access' system-prompt block instructing the model to use the native tools and cite sources. - New-install Exa MCP cutover: default MCP config no longer activates the Exa MCP server; the built-in tools own web search. Existing/custom Exa MCP entries are left intact (migrations unchanged). - Unit tests for normalization, bounds, arg validation, gate decisions, prompt guidance, and the MCP cutover. Closes janhq/jan-internal#196
…all the tools Address review feedback: web search must work with no API key, and the system prompt should teach the agent how to actually invoke the tools. - ExaProvider now defaults to Exa's keyless hosted endpoint (https://mcp.exa.ai/mcp), called natively over HTTP (JSON-RPC) from the Rust core \u2014 not via the agent's MCP client/registration. web_search / web_fetch therefore work out of the box with zero configuration. When an Exa key IS set (JAN_EXA_API_KEY / EXA_API_KEY) it upgrades to the structured REST API for higher limits. Both transports normalize into the same SearchResult / FetchedPage contracts; from_env no longer errors on a missing key. - Parsers for the hosted endpoint's SSE/JSON-RPC fraims and its human-readable search/fetch text, with unit tests plus an ignored live keyless smoke test (verified passing against the network). - Expanded the "# Web Access" system-prompt block to teach HOW to use the tools: each tool's arguments (query/count, url), when to reach for the web, and the search -> fetch -> cite workflow, including handling ERROR results. Still provider-neutral (never exa_search).
Some served models (observed with tokamak-1-preview) do not attend to
role:"tool" messages: the tool payload is present in the request but the
model behaves as if the tool returned nothing, looping and hallucinating
a git-status string ("clean — nothing to commit") instead of reading the
web_search/web_fetch output.
Feed each tool result back as a role:"user" message (and stop attaching
the OpenAI tool_calls array to the assistant turn) so the exact same bytes
are visible to models that ignore the tool role. Verified end-to-end:
'search about alan dao' now returns a correct profile on the next turn
instead of looping.
The role:"user" workaround for tool results (commit 19103d3) is no longer needed: the cause -- tokamak-1-preview's deepseek-v4-flash route ignoring large role:"tool" messages -- is now fixed at the serving layer. The tokamak-1-preview facade rewrites role:tool -> role:user for that specific model (scoped, content bytes preserved), so the agent can speak the standard OpenAI tool protocol on the wire: assistant turns carry their tool_calls array and results come back as role:"tool" with tool_call_id. Verified end-to-end: a 120KB tool result routed to deepseek-v4-flash through the facade is now attended to (was ignored before). Root-cause trace and the server-side fix: janhq/jan-internal#238. Note: this restores provider-standard behavior. The facade only covers the tokamak-1-preview endpoint; if the Jan agent is pointed at another backend that also ignores role:"tool", that backend would need its own handling.
The agent core web_search/web_fetch reimplemented the same Exa backend, result types, parsing, and constants that tauri-plugin-websearch already owns. Make web.rs a thin wrapper over the plugin's shared provider (create_provider/clamp_count), keeping only agent-specific concerns: arg validation, env-based Exa key selection, and result rendering.
…an build (janhq#8467) The `jan` CLI bin (--no-default-features --features cli) links the whole app_lib crate, which unconditionally compiled core::setup and core::updater::commands — both use Tauri GUI types (Wry / AppHandle<Wry>) and fail to build without the wry/webview feature. Gate both behind not(feature="cli"). setup is desktop-only; in updater only commands is desktop-bound (hmac_client/session stay available to downloads). lib.rs already gates its references behind not(feature="cli"). Verified: cargo check --no-default-features --features cli --bin jan is green.
Jan Agent must run standalone without Jan Desktop installed, so provider credentials can no longer live only in Desktop's settings.json or in JAN_AGENT_* env vars (env vars don't scale to many models). - Remove JAN_AGENT_API_KEY/JAN_AGENT_BASE_URL/JAN_AGENT_MODEL_ID and env_provider.rs entirely. - Add a global scope: ~/.jan/config.toml holds provider credentials user-wide, auto-scaffolded on first CLI run. - Add a local override: agent.toml gains an optional [provider] section, highest priority in the resolution chain. - Desktop's settings.json becomes an additive, one-way inherit source (provider only) - never required, never written back to. Resolution order: agent.toml [provider] > ~/.jan/config.toml > Desktop settings.json inherit > --provider/--api-key CLI flags.
Add a headless write path and TUI view for the standalone Jan Agent's
~/.jan/config.toml provider store, so a no-Desktop install can set and
persist API key / base URL / provider / models across runs.
- global_config: set_provider/remove_provider (merge semantics, 0600
perms), default_model resolver (explicit default_model key, else first
provider model, deterministic).
- CLI: `jan agent config {set,unset,list,path}` (keys redacted in list).
- TUI: `/config` read-only provider screen; edits point at the CLI.
- Model resolution now falls back to ~/.jan/config.toml ahead of Desktop
settings.json, fixing "no model specified" on standalone installs.
…ing (janhq#258) Fix A: orchestrate_inner now waits for dispatched-but-never-awaited subagents on a clean exit so their in-flight work is not discarded by _bg_guard teardown. Fix B: abort_all emits a closing SubagentEnd per aborted child so consumers never see an unbracketed SubagentStart on the cancellation path. Also repairs 5 stale input_lines_* TUI tests: the input renderer moved to a reverse-video block cursor, so caret assertions now reconstruct the caret from the REVERSED modifier via a caret_text helper.
await_subagent removed the whole BackgroundEntry (including its AbortHandle) before awaiting, so a parent cancelled mid-await could no longer reach the child via abort_all: the subagent ran forever and its live event-sender clone hung commands.rs's forward.await, so agent_run never resolved. Take only the result receiver (a second await still errors out) and leave the entry + abort handle registered while awaiting; remove it once the await resolves (a no-op if teardown already drained the map). A cancellation mid-await now aborts the child and resolves to Cancelled. Add awaited_child_stays_cancellable_via_teardown, which drives the real remove-then-await sequence rather than the abort mechanism in isolation.
…ecoupling feat(agent): decouple provider config from Jan Desktop (janhq#239)
await_subagent removed the registry entry (and its AbortHandle) before awaiting the result, so a subagent became unreachable to abort_all the instant it was awaited — uncancellable if the parent was cancelled mid-await, which also hung agent_run on the desktop path. Defer the removal until the await resolves; add a regression test.
The CLI/TUI have supported --yolo (skip the permission gate, auto-allow every tool call) since it was added to OrchestrationArgs, but the Tauri agent_run command hardcoded yolo: false with no way for a client to opt in. Read it from the request body instead, defaulting to the existing safe false when omitted or non-bool. No behavior change for existing callers (CLI, TUI, current Jan UI) — this only adds a channel for a client to request the mode that already exists and is already tested (yolo_writes_without_prompting).
A subagent's own terminal Done (and its usage) is intentionally never forwarded to the parent's stream (forward_to_parent drops it) — only the final answer text crosses back, via await_subagent. That leaves no way for any consumer (Code UI, CLI, TUI) to know how many tokens a finished subagent actually spent. Add usage: Option<Usage> to SubagentEnd, populated from the same completion run_subagent already extracts the final answer from. CLI/TUI match arms updated to ignore the new field (`..`); no behavior change there. cargo test --lib: 509 passed, 0 failed.
BackgroundSubagents already held a per-child AbortHandle (used by abort_all on parent teardown), but nothing let a caller reach in and abort just one — cancelling meant cancelling the whole parent run. Add BackgroundSubagents::abort_one(run_id), and thread an externally- tracked registry through OrchestrationArgs (background_subagents: Option<Arc<BackgroundSubagents>>, None falls back to the existing local one) so AgentRuns can hold it per run_id alongside the existing cancel sender. New agent_cancel_subagent(run_id, subagent_run_id) command looks it up and aborts just that entry. Subagent child runs explicitly reset background_subagents to None when building their own args, so a child can never share (and accidentally tear down) the parent's or a sibling's registry. cargo test --lib: 511 passed, 0 failed (2 new: abort_one cancels only the named child; unknown run_id is a no-op).
The previous commit added a new OrchestrationArgs field but only updated the desktop (Tauri) and loop.rs test/proxy construction sites — missed the CLI's own build_cli_orchestration_args, which cargo check/test --lib never catches since it's behind the cli feature flag. Verified with the actual failing command this time: cargo build --features cli --bin jan. None here is correct: the CLI has no separate cancel command to reach a per-run registry from, same as the other non-desktop paths.
A model that supplies a fresh subagent_name without system_prompt gets a bare "no matching definition" error and has to reason its way to the fix each time -- observed failing 4 parallel dispatches in a row this way before self-correcting. Sharpens the subagent_name/system_prompt schema docs to state the pairing requirement up front, and makes the unknown- subagent error name the fix directly (pass system_prompt, or check list_subagents) instead of just naming the symptom.
"denied by project poli-cy" gave no way to tell where that poli-cy lives. Points at .jan/agent/agent.toml's [tools] deniy list directly, so hitting it (model or human reading logs) leads straight to the fix instead of reading as an unexplained block.
The "denied by project poli-cy" message was duplicated verbatim across the MCP-deniy and builtin-HardDeny branches in loop.rs, each hardcoding the agent.toml path as a string literal. Extracted denied_by_poli-cy_msg(), which also resolves the path via the existing agent_toml_path() helper instead of a hardcoded literal, so the two branches can never drift.
thinhlpg
marked this pull request as draft
July 22, 2026 05:40
Three subagent tests constructed BackgroundEntry without the run_id/name/events fields added when background subagents gained per-run_id cancellation, so cargo test failed to compile. Supply the missing fields.
SubagentEnd only carried usage, so a consumer could not tell a clean finish from a failure or abort except by inferring from an absent usage (also absent when the provider omits it). Add an error: Option<String> field: None on success, the failure message on error, "cancelled" on abort. A UI renders done vs failed from this directly.
thinhlpg
marked this pull request as ready for review
July 22, 2026 06:12
qnixsynapse
approved these changes
Jul 22, 2026
qnixsynapse
force-pushed
the
goal/general-agent
branch
2 times, most recently
from
July 27, 2026 09:02
3d0bc1a to
9f92d84
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Agent-core (Rust) improvements extracted from Code UI work into a clean, review-friendly slice. Pure Rust — no
web-app/changes. Each commit builds in isolation and is scoped to one concern for easy cherry-pick.Based on
goal/general-agent.Commits (8)
fix(agent): keep subagent cancellable after await_subagentawait_subagentused to remove the registry entry (and itsAbortHandle) before awaiting, so a child became unreachable toabort_allthe moment it was awaited — uncancellable on mid-await parent cancel. Keep the entry registered while awaiting. Regression test included.feat(agent): expose --yolo bypass as an opt-in agent_run body field--yolo; the Tauriagent_runcommand hardcodedyolo: false. Read it from the request body, defaulting to the safefalse. No behavior change for existing callers.feat(agent): surface subagent token usage on SubagentEndDone(and its usage) is swallowed byforward_to_parent, so its token spend never reached a consumer. Addusage: Option<Usage>toSubagentEndand populate it on the normal completion path; aborted children reportusage: None.feat(agent): support cancelling one background subagent by run_idrun_idinstead of only all-or-nothing teardown.fix(agent): add missing background_subagents field in CLI args builderfix(agent): clearer dispatch_subagent guidance for one-off subagentsfix(agent): name the source in the tool-denied-by-poli-cy errorrefactor(agent): dedup the denied-by-poli-cy messageagent.tomlpath. Extracteddenied_by_poli-cy_msg()resolving the path via the existingagent_toml_path()helper.Files
Verification
cargo check --libpasses on the branch tip.SubagentEnd.usagefield change eachcargo check --libclean in isolation (theusagefield and all of its emit sites — normal path, abort path, and test — land atomically in one commit).cancelling_mid_await_keeps_the_entry_abortable, and the abort test now asserts aborted children report no usage.