Commit graph

25 commits

Author SHA1 Message Date
易良
d44030a4c0
feat(core): add model grade selection for subagent spawn (#7685) (#7702)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* docs: add design placeholder for subagent model grade selection (#7685)

* feat(core): add subagent model grade selection

* test(subagent): cover resolveModelGrade deep guards and resume else branch

- subagent-manager: add tests for non-string grade values, blank values,
  array-shaped modelGrades, and missing modelGrades (all return undefined)
- background-agent-resume: assert configured subagent model is preserved
  (not forced to 'inherit') when launch flags (model + authType) are absent

Addresses test-coverage review findings.

* refactor(subagent): extract normalizeModelGradeSettings and merge model validate

- Extract normalizeModelGradeSettings helper shared by resolveModelGrade
  and the Agent tool schema build, so the advertised grades and runtime
  resolution cannot drift (addresses duplicated shape invariant).
- Merge the three model-parameter validate branches under a single
  `params.model !== undefined` guard.
- Update agent.test.ts mock to preserve the real helper while still
  mocking SubagentManager.

* refactor(core): simplify model grade resolution

* fix(core): reject unknown model grades

* docs(core): clarify model grade precedence

* docs: explain subagent model grades

* test(core): update subagent manager mock

* fix(core): list available model grades

* fix(core): trim model grade keys and cover schema removal

Grade keys were checked for emptiness via grade.trim() but stored in the
map and advertised in the tool schema enum untrimmed, while values were
trimmed. A padded key like ' small ' published a padded enum name the
model had to reproduce verbatim, and the allowlist check silently
excluded it. Normalize the key before storing, allowlist matching, and
schema publication.

Also adds a test for the delete schema.properties.model branch that fires
when grades transition from available to empty, so a regression that
breaks the delete leaves no stale model enum in the tool schema.

* fix(core): trim allowed model grade filters

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-07-26 16:23:54 +00:00
Shaojin Wen
45c8d8f8cc
docs: refresh subagent lifecycle guidance (#7624)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-24 03:03:27 +00:00
Dragon
22963d5777
feat(core): add fork_turns to fork subagents (#7346)
* feat(core): add fork_turns to subagents

* fix(core): preserve nested agent context inheritance

* fix(core): isolate inherited subagent history

* refactor(core): scope fork_turns to fork agents

* test(core): cover zero-real-turns branch in selectForkHistory

Add a regression guard asserting selectForkHistory returns [] when a
numeric fork window finds no real user turns after the synthetic prefix
(e.g. only startup context present). This pins the
realUserTurnIndexes.length === 0 branch so a future refactor cannot
silently return the full history instead of an empty selection.

* docs(core): address fork_turns review feedback

- Explain the curated vs uncurated history split between the fork_turns
  'all' and numeric paths in createForkSubagent.
- Document why includeCompressed is load-bearing in selectForkHistory.
- Gate the 'forks inherit ...' prose in the Writing-the-prompt section
  behind isForkSubagentEnabled so non-interactive sessions no longer
  advertise fork behavior, and lock it with description assertions.

* test(core): cover fork_turns 'all' and getHistoryForForkWindow fallback

Add two integration tests for prepareForkConfig fork-history selection:

- 'all' path: verify getHistoryShallow(true) sources the curated history
  and selectForkHistory(history, 'all') seeds the fork with the full
  history verbatim.
- numeric path: verify the getHistoryForForkWindow?.() ?? getHistory(true)
  fallback still produces a correct bounded window (startup + latest real
  turn) when getHistoryForForkWindow is unavailable.

* fix(core): use uncurated history for fork bounded-window fallback

The numeric fork_turns path falls back to geminiClient.getHistory(true)
when getHistoryForForkWindow is unavailable. Curated history coalesces
the leading startup reminder into the first real user turn, so
getStartupContextLength can no longer detect it as a pure prefix.
selectForkHistory then leaves the startup text embedded in the first
selected turn while the startupContext prefix is prepended separately,
duplicating startup context in the fork's initial messages.

Fall back to uncurated getHistory() instead, which keeps the startup
reminder as its own pure entry that selectForkHistory strips cleanly.
Update the fallback-path test to assert the uncurated call and document
why curated history is unsafe here.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-21 07:08:12 +00:00
Dragon
d4db5fcfab
feat(core): improve subagent delegation defaults and guardrails (#7048)
* docs(design): define default background subagents

* feat(core): improve subagent delegation defaults

* docs(core): cross-reference the three background-classification sites

Add pointer comments linking the core dispatch source of truth
(AgentTool.execute) and its two UI mirrors (web-shell
isBackgroundSubAgentToolCall, desktop detectBackgroundEvents) so the
replicated top-level-agent background heuristic is not changed in
isolation. Addresses PR review feedback.

* fix(core): align background classification for fork and named-teammate launches

Address review feedback on the background-classification rule so core dispatch
and the two UI classifiers stay consistent:

- core: exclude a name-without-active-team launch from the default-background
  path so it stays foreground, matching both UI classifiers (which exclude
  name). Previously such a launch was backgrounded by core but tracked as
  foreground by the UIs.
- web-shell and desktop classifiers: exclude subagent_type "fork" from the
  default-background heuristic, mirroring core's !isForkRequested guard. A
  top-level fork request with an omitted flag runs foreground in core but was
  classified as background by the UIs.
- add a core dispatch test asserting a working_dir launch with an omitted
  run_in_background flag stays in the foreground.

* test: cover fork/background classification and precedence per review feedback

Address unresolved review threads on PR #7048:
- Add web-shell and desktop UI classifier tests asserting an omitted-flag
  `subagent_type: "fork"` launch stays in the foreground, verifying the
  documented `!isForkRequested` parity with core dispatch.
- Add a core AgentTool test asserting an explicit `run_in_background: false`
  overrides a subagent config with `background: true`, locking in the
  `run_in_background ?? config` precedence against a `||` regression.
- Harden the Explore read-only prompt: pipelines must not send data to a
  network endpoint (no curl/wget/nc), closing the `cat file | curl`
  exfiltration gap.

* fix(core): restore general no-unnecessary-files guard in general-purpose prompt

Address review feedback: the rewritten general-purpose prompt dropped the
broad guard against creating unrequested files, keeping only the
documentation-specific line. Restore a general 'do not create files unless
necessary' guard so speculative utility/config files are not created.

* test(desktop): cover named-teammate foreground guard in detectBackgroundEvents

Add a desktop tool-matching test asserting a top-level Agent with a
`name` set (named teammate) stays foreground and emits no
task_backgrounded event, mirroring the web-shell classifier's
named-teammate coverage and the existing fork-exclusion test.

* test(core): cover named-teammate foreground dispatch when flag omitted

Add a core-dispatch test asserting a top-level Agent launch with `name`
set and `run_in_background` omitted stays foreground when no team is
active, guarding the `this.params.name === undefined` exclusion in
backgroundRequested directly (previously only covered by the UI
classifiers).

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-18 08:52:48 +00:00
tanzhenxin
220fba7917
feat(subagents): make Explore inherit the main model by default (#6807) 2026-07-14 01:23:35 +00:00
Shaojin Wen
e97be6c669
fix(agent): stop forking result-bearing work; keep omitted subagent_type awaitable (#5155)
Enabling fork-by-default for every interactive session (#4963) made an omitted `subagent_type` mean a fire-and-forget background fork. A fork's findings never flow back into the parent turn, so any caller that spawns parallel agents and then aggregates their results breaks — the bundled `review`/`simplify` skills, third-party skills, and the model's own delegation. The orchestrator waits forever for results that never arrive; nudged by the periodic todo reminder it can also spin on identical tool calls until DashScope rejects the request with `InternalError.Algo.InvalidParameter: Repetitive tool calls detected` (HTTP 400).

Make forking an explicit, deliberate choice and stop steering the model toward it when it needs the results:
- Dispatch: `subagent_type: "fork"` selects a fork (interactive only; otherwise general-purpose). Omitting `subagent_type` always resolves to the awaitable general-purpose subagent whose result returns inline — never a fork. `/fork` passes `subagent_type: "fork"` explicitly.
- Prompt: drop the "launch parallel forks for research" guidance, reframe the tool description and "When to fork" around "never fork work whose output you need", and stop advertising `fork` in the subagent_type enum (it stays valid via validation for `/fork` and intentional use).
- Skills: `review` and `simplify` launch their parallel agents with `subagent_type: "general-purpose"` and are told explicitly not to fork, since they aggregate findings.

Forking stays a first-class, default-available feature for genuinely fire-and-forget work — it just isn't the silent default, an enum pick, or something the model is nudged toward when it must read the results.
2026-06-16 09:38:10 +08:00
qqqys
4ae788623e
feat(core,cli): bubble background subagent permission prompts to the parent session (#4955)
* feat(core,cli): bubble background subagent permission prompts to the parent session

Background subagents auto-deny any tool call that needs interactive confirmation, so a single permission-gated step (a git push, an rm, a network call) silently fails and the work bounces back to the parent turn — defeating the point of backgrounding. This adds an opt-in approvalMode value for subagent definitions, `bubble`: instead of denying, the call is parked on the BackgroundTaskRegistry and surfaced in the Background tasks dialog, where the user answers it through the shared ToolConfirmationMessage; the agent then resumes.

- `bubble` is a subagent-only approvalMode (deliberately not a session-level ApprovalMode value); it resolves to `default` run behavior and only flips the background path from deny to surface, in interactive sessions. Headless / non-interactive contexts keep auto-deny.
- BackgroundTaskRegistry grows a parked-approval queue (add/resolve/clear), an approval-change callback, and an event bridge (TOOL_WAITING_APPROVAL parks, TOOL_RESULT clears stale prompts). Every terminal transition auto-rejects parked calls so the agent loop never hangs on an unanswerable prompt; cancel() rejects before aborting so respond(Cancel) actually fires ahead of the abort-driven queue clear. Auto-reject failures are caught on the promise, not via try/catch around a voided async call.
- The launch path (agent.ts) and the resume path (background-agent-resume.ts) share the same gate, so a resumed agent of the same definition keeps bubbling instead of silently reverting to auto-deny.
- TUI: the footer pill shows a "needs approval" marker, dialog list rows are flagged, and the detail view embeds the confirmation prompt. While a prompt is up, left (back) and x (stop agent) remain available as escape hatches so a re-parking agent cannot trap the keyboard.

Closes #4928

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(i18n): add zh-TW translations for background approval strings

check-i18n requires every zh key to have a zh-TW counterpart; the three
strings added for permission bubbling were registered in en/zh only.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): harden background approval edge cases from review

- resolvePendingApproval: if respond() rejects, the tool call is still parked in the scheduler, so re-add the approval and re-emit instead of silently clearing the prompt (which left the UI showing nothing pending while the agent hung). Returns false on failure.
- reset() and finalizeCancellationIfPending(): reject parked approvals defensively. The /resume and /clear paths already gate on hasBlockingBackgroundWork() so these only run on terminal entries today, but rejecting here means a future caller dropping that guard can't strand an unanswered respond() callback.
- resolveSubagentApprovalMode: resolve the subagent-only 'bubble' mode to Default explicitly rather than via approvalModeToPermissionMode's default fall-through, so a future ApprovalMode.BUBBLE enum member can't silently change it.

Adds tests for the fail / finalizeCancelled / reset auto-reject paths and the respond()-rejection re-park.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): include args in approval test events

* test(core): update nested yaml parser expectations

* fix(cli): reuse selected background agent id

* fix(core): fail consumed background approval retries

* fix(core): prevent persistent bubbled approvals

* fix(core): harden bubbled approval handling

* fix(core): cover background approval edge cases

* fix(cli): isolate bubbled question approval keys

* fix(cli): localize background approval labels

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-06-13 01:50:26 +08:00
顾盼
e25d7eec04
feat(core): port declarative-agent mcpServers + hooks (CC 2.1.168 parity follow-up) (#4996)
* fix(core): replace yaml-parser stringify with eemeli/yaml for safe nested round-trip

PR #4870 swapped `parse` over to the `yaml` library so block scalars and
nested structures load correctly, but left the hand-rolled `stringify`
in place. The hand-rolled serializer only walks one level of nesting and
emits `[object Object]` for any value below — so any caller that does
`parse → modify → stringify` (e.g. SubagentManager saving a frontmatter
file, or a Claude-Code-format converter writing back a `.qwen/agents/*.md`)
silently corrupts nested fields like `mcpServers` or `hooks` on disk.

This commit:
- Delegates `stringify` to `yaml.stringify` with `lineWidth: 0`, matching
  the parse side's library choice and unlocking arbitrary-depth round-trip.
- Drops the now-unused `formatValue` helper.
- Replaces the byte-exact escape-sequence assertions with property-based
  `parse(stringify(x)) === x` round-trip checks — the previous tests
  pinned hand-rolled quote output that eemeli/yaml legitimately chooses
  differently from. The contract that matters at the public API boundary
  is round-trip, not stable bytes.
- Adds two CC-shape nested round-trip tests (mcpServers + hooks) that
  would have been impossible to express under the old serializer.

The parse-side safety guards added in PR #4870 (schema 'core', timestamp
/ binary tag filtering, `Object.create(null)`, Date/Uint8Array sanitize,
parseSimple fallback) are preserved untouched.

* feat(core): port declarative-agent mcpServers + hooks end-to-end

Builds on PR #4842 (which deferred these two fields) and PR #4870
(which made `parse` nested-safe) by adding the remaining surface +
runtime wiring so a `.qwen/agents/*.md` with per-agent MCP servers
and hooks works the same way as the equivalent `.claude/agents/*.md`.

## Schema layer

`agent-frontmatter-schema.ts` gains two lenient DL7-parity parsers:

- `parseAgentMcpServers` — keeps a record-of-records shape and drops
  per-key entries whose value is a scalar / array / null (mirrors CC's
  `gS8` shallow validation; per-spec union is enforced later by the
  MCP loader).
- `parseAgentHooks` — keeps a record-of-arrays shape and drops events
  whose value isn't an array (mirrors CC's `TKO` / `_u`).

Both return `undefined` when no entries survive shape filtering, so the
caller can omit the field entirely rather than emit an empty object.

## SubagentConfig surface

- `types.ts`: adds optional `mcpServers?: Record<string, unknown>` and
  `hooks?: Record<string, unknown>`.
- `subagent-manager.ts` `parseSubagentContent`: extracts both fields
  with warn-and-drop on top-level shape failure, matching the existing
  posture for `permissionMode` / `maxTurns` / `color`.
- `subagent-manager.ts` `serializeSubagent`: emits both back to YAML.
  The previous skip-list carve-out in `claude-converter.ts`
  (`NESTED_FIELDS_NOT_ROUND_TRIPPABLE`) is gone — round-trip is safe
  now that the YAML stringifier is eemeli/yaml.
- `claude-converter.ts`: emits `mcpServers` (was missing) when
  converting from a CC plugin agent file.

## Runtime wiring

- `hookRegistry.ts`: new public `addAgentHooks(hooks, scopeId): () => void`
  appends ephemeral entries tagged with `agentScope`, runs them through
  the same per-definition validation pipeline as session/user/project
  hooks (so a malformed entry is logged + dropped instead of breaking
  the spawn), and returns an unregister callback. The duplicate-detection
  key includes `agentScope` so identical hooks from different subagents —
  or from a subagent and the session — coexist instead of swallowing
  one another.

  v1 scope limitation: while a subagent's entries live in the registry
  they fire for every event of their declared type regardless of which
  agent is currently active. Per-agent scope filtering at firing time
  is a follow-up; the limitation is documented in the user-facing doc.

- `subagent-manager.ts` `buildSubagentContextOverride`: now takes the
  `SubagentConfig` and, when per-agent `mcpServers` are present,
  overrides `getMcpServers()` on the subagent's Config wrapper to return
  the union of session + agent servers (agent wins on key collision,
  matching CC's `scope: 'agent'` semantics). The skip-rebuild
  optimization is bypassed in this case — without a fresh
  `rebuildToolRegistryOnOverride` anchored on the override Config, the
  pre-existing wrapper-owned `McpClientManager` would still see only
  the session set and the discovery loop below would silently no-op.
  After rebuild, the loop explicitly discovers each per-agent server so
  its tools land in the subagent's registry before `AgentHeadless.run`.

- `subagent-manager.ts` `createAgentHeadless`: when `config.hooks` is
  set, registers via `HookRegistry.addAgentHooks` with a per-spawn
  scope ID (`agent:<name>:<uuid>`) and wraps the caller-provided
  `AgentHooks.onStop` so the unregister callback fires after the
  caller's handler, in a `finally` block to survive a throwing user
  handler. Errors from `AgentHeadless.create` itself unregister the
  hooks before re-throwing.

## Tests

- `agent-frontmatter-schema.test.ts`: +8 tests covering shape filtering,
  drop-on-bad-top-level, drop-when-empty for both new parsers.
- `subagent-manager.test.ts`: +7 tests covering parse, serialize, and
  drop-on-malformed for both fields.
- `subagent-manager-override.test.ts`: +2 tests for the
  session+agent MCP merge and the no-override pass-through case.
- `hookRegistry.test.ts`: +4 tests for `addAgentHooks` — scope tag,
  no-collision with existing same-identity entries, two concurrent
  agents keep their own copies, empty payload no-op.

* docs(core): declarative agents follow-up + yaml-parser audit

- `docs/yaml-parser-replacement.md` is the new audit doc covering the
  PR #4870 → eemeli/yaml decision (parse-side), the security probe
  results (`maxAliasCount` default, `!!js/function` becomes literal
  string + warning, merge keys disabled by default, custom-tag filter
  for timestamp/binary), and the stringify-side gap this follow-up
  closes.
- `docs/declarative-agents-port.md` status table now marks `mcpServers`
  and `hooks` as **shipped (follow-up)** with a one-line note on the
  runtime wiring strategy + v1 scope limitation for hooks. The
  reverse-engineering record below the table is unchanged and remains
  the reference for the still-deferred fields (`effort`, `memory`,
  `isolation`, `initialPrompt`, `skills`).
- `docs/users/features/sub-agents.md` adds `mcpServers` + `hooks` rows
  to the CC-compatibility table and a full example frontmatter showing
  all four shipped fields composed together. The v1 hooks scope
  limitation is called out as a blockquote so users picking up
  per-agent hooks know to prefer fire-globally-safe handlers (logging)
  over behavior-mutating ones until the firing-time scope filter lands.

* fix(core): self-review round 1 — proto-pollution defense + parallel MCP discovery

Audited the three commits adversarially across four lenses (correctness,
security, reuse, test quality). Two real findings, two test gaps.

## Real bugs fixed

### `parseAgentMcpServers` / `parseAgentHooks` could pollute the result's prototype

Both helpers wrote into a plain `{}` while iterating an input object that
yaml-parser hands back as null-prototype. A YAML key of literal
`__proto__` survives that null-prototype guarantee as an own property,
so `Object.entries(record)` walks it, and the assignment
`result['__proto__'] = spec` triggers the inherited setter — silently
re-wiring `result`'s prototype chain to point at the attacker's spec.

Object.prototype itself stays untouched (the setter is per-instance) and
the downstream spread `{ ...result }` only copies own enumerables, so the
pollution doesn't directly leak through current callers. But returning
an object with a hijacked prototype is a latent footgun: a future caller
that uses `for…in`, `result.someKey`, or any property access that misses
the own table would pick up values from the polluted chain.

Switching to `Object.create(null)` makes the assignment a plain own
property — matching the null-prototype invariant the rest of
`yaml-parser.ts` already maintains. Two regression tests pin the
defense (one per parser), each constructing a null-prototype input the
way `yaml.parse` actually produces it (`{ __proto__: … }` in an
object literal triggers the setter at construction and so does NOT
reproduce the attack input shape).

### Per-agent MCP discovery serialised through every server

`buildSubagentContextOverride` called `discoverToolsForServer` in a
sequential `for…of` loop with `await`. A misbehaving server (stdio
command that hangs at startup, remote endpoint that times out at the
MCP layer's default 30s) blocks every following server, so the subagent
spawn paid the sum of every per-server timeout instead of the max.

Switched to `Promise.allSettled` over the server list. Each call still
carries the MCP layer's own per-server timeout (`stdio` default 30s,
remote default 5s, per-spec `discoveryTimeoutMs` override); `allSettled`
only removes the serialisation between siblings. Rejections still
log-and-drop so a single bad server doesn't block its siblings'
tools from landing in the subagent's registry.

## Test gaps closed

### `addAgentHooks` coexistence test only asserted count

The "coexists with session/user hooks of the same identity" test
asserted `getAllHooks()` had length 2 after the add but did NOT verify
which entries were present. A regression that dropped `agentScope`
from the dedup key would still leave two entries by ordering luck and
silently break concurrent-agent isolation. Replaced the bare count
check with explicit assertions for `(source: User, agentScope: undefined)`
+ `(source: Session, agentScope: 'agent:test:def')`.

### Empty-record edge cases for `parseAgentMcpServers` / `parseAgentHooks`

The existing "returns undefined when nothing survives shape filtering"
tests covered the case where every key had a bad shape. The empty
input case `parseAgent…({})` was on the same code path but never
exercised — callers rely on the `undefined → omit field` behaviour for
both. Added one test per parser.

* fix(core): pr #4996 review round 1 — leak fixes via explicit dispose contract

Reviewer flagged two Criticals + one Suggestion + one Nice-to-have. The two
Criticals share a root cause (subagent execute()'s inner try/finally
doesn't fire on every exit path), so they fold into a single API change.

## [Critical] Hook cleanup leak on AgentHeadless.execute() early exits

`wrapAgentHooksForCleanup` relied on `onStop` firing inside execute()'s
inner try/finally. Two early-exit paths bypass that finally:

1. `createChat()` returning null at agent-headless.ts:224-226 — returns
   before the outer `try` at 233 is even entered.
2. `prepareTools()` throwing at 234 — propagates through the outer
   `finally` at 335, which only calls `abortController.abort()` and
   never reaches the inner finally that fires `onStop`.

The pre-fix `catch` block only guarded `AgentHeadless.create()`, not
`execute()`. Leaked HookRegistry entries fire globally for every matching
event in the session, polluting unrelated tool calls.

## [Critical] Per-agent MCP server processes leak after every spawn

`discoverToolsForServer` connects real MCP clients (stdio child
processes, HTTP/SSE sockets) in the force-rebuilt subagent ToolRegistry.
Nothing stopped that registry: `Config.shutdown` only reaches the root's
`this.toolRegistry`, and AgentTool's existing `agentConfig.getToolRegistry()
.stop()` (fg + bg + resume finally blocks) only stops the parent's
registry, not the override's distinct fresh one. Every subagent
invocation that declared `mcpServers` orphaned a child process for the
rest of the host process's lifetime.

## Shared fix — caller-driven `dispose` contract

`SubagentManager.createAgentHeadless` now returns
`{ subagent, dispose }`. Callers MUST invoke `dispose()` in the same
`finally` block that wraps `subagent.execute()`. That `finally` lives
in the caller's scope (AgentTool fg/bg, BackgroundAgentResumeService),
which is reachable on every execute() exit — including the two early-
exit paths the previous `onStop` hook never reached.

`dispose` is a single closure that calls the previously-separate
cleanup callbacks in order:

1. `unregisterAgentHooks` returned from `HookRegistry.addAgentHooks`
   (when per-agent hooks were registered).
2. `disposeRegistry` returned alongside the new `buildSubagentContextOverride`
   return shape `{ context, disposeRegistry }` (set only when this call
   force-rebuilt the registry for `mcpServers`).

Both cleanups are wrapped in `try/finally` that logs and re-arms so an
exception in one path doesn't block the other and doesn't double-fire.
The pre-existing constructor-failure catch in `createAgentHeadless` now
runs the same closure directly — the caller never received the return
value, so it cannot fire `dispose` itself.

The three callers gain one variable + one `void dispose?.().catch()`
inside their existing finally:

- `agent.ts:2039` (foreground) — finally at `agent.ts:2904`
- `agent.ts:2131` (background) — finally at `agent.ts:2502`
- `background-agent-resume.ts:630` (resume) — finally at `:852`

Fork subagents share the parent's lifecycle; their `dispose` stays
undefined and the `?.()` is a no-op.

`wrapAgentHooksForCleanup` is removed — it was load-bearing only for
the happy + inner-reasoning-loop-failure paths and is now obsolete.

## [Suggestion] Repeated guard condition

`config.hooks && Object.keys(config.hooks).length > 0` no longer appears
in both `if` / `else if` arms. Single outer guard + nested branch on
`hookRegistry`.

## [Nice-to-have] claude-converter.ts:290 missing `.trim()`

`stringifyYaml(newFrontmatter).trim()` brings the converter into line
with `subagent-manager.ts:651`. Without trim, eemeli/yaml's trailing
newline produced an extra blank line before the closing `---`
delimiter — cosmetic (both readers tolerate it) but the asymmetry
between the two writers was a real consistency bug.

## Tests

3 new RED-first tests in `subagent-manager.test.ts` pin the dispose
contract:

1. `returns { subagent, dispose }; dispose unregisters per-agent hooks`
2. `dispose unregisters even when execute() never runs (early-exit leak fix)`
   — the case where `createChat()` → null or `prepareTools()` throws
3. `dispose is a safe no-op when neither hooks nor mcpServers are declared`

Override test helper updated to destructure the new
`buildSubagentContextOverride` return shape. AgentTool + BackgroundAgent
test mocks updated to return `{ subagent, dispose }`. All 2087 in-scope
tests pass.

* refactor(core): /simplify cleanup pass on the dispose contract

Three cleanups from a multi-angle quality review (reuse / simplification /
altitude lenses, all on commit 720f0e4a1):

1. Drop the null-out guard inside `runCleanup`. Both inner callbacks are
   already idempotent at the source (`HookRegistry.addAgentHooks` filters
   removal by `agentScope`; `ToolRegistry.stop` documents itself
   idempotent), so the outer `unregisterAgentHooks = undefined` /
   `disposeSubagentRegistry = undefined` finally blocks were buying nothing
   beyond a marginal short-circuit on duplicate `dispose()` calls — at the
   cost of 8 LOC and a "is this load-bearing?" question for readers.
   Comment now states the idempotency guarantee explicitly.

2. Rename the `buildSubagentContextOverride` return field
   `disposeRegistry` → `cleanup`. Pairs the sibling override builder
   `createApprovalModeOverride` whose return shape is
   `ApprovalModeOverrideHandle = { config, cleanup }`. The `context` field
   stays as-is because `config` would shadow the same-named parameter
   inside this method's scope (the parent helper doesn't take a `config`
   parameter, which is why it can use that name).

3. Add a 4th test pinning the constructor-failure cleanup path. The
   `try { ... } catch { await runCleanup(); throw }` block at the end of
   `createAgentHeadless` runs when `AgentHeadless.create` rejects — at
   which point the caller has not received `{ subagent, dispose }` and
   cannot run cleanup itself. The three existing tests covered the
   happy-path and execute()-never-runs scenarios; this one closes the
   "constructor blows up after hooks were registered" gap.

No behavior change to callers — same `{ subagent, dispose }` return
shape, same dispose semantics. Findings skipped:

- Parallelizing the two cleanups inside `runCleanup`: synchronous
  unregister + async registry stop, the `await` only blocks the registry
  stop; the order has no measurable cost.
- Parallelizing the parent/agent registry stops at the 3 call sites:
  they already run concurrently because the call sites use
  `void X.stop().catch(...)` (fire-and-forget), not `await`.
- Extracting a `executeHeadlessSubagent` helper that owns the dispose
  lifecycle: real win against future call-site drift, but reaches well
  outside the round-1 review diff into AgentTool's three execution
  shapes (fg sync / bg fire-and-forget / resume embedded).
- Fixing `AgentHeadless.execute()`'s early-exit paths upstream: the
  round-1 commit's explicit altitude choice; revisiting it would re-open
  a settled design call.
2026-06-12 14:15:51 +08:00
顾盼
5c54a2cf8e
feat(core): declarative agent frontmatter v1 — permissionMode bridge + maxTurns wiring + color allowlist (CC 2.1.168 parity) (#4842)
* docs: add declarative agents port design doc for #4821

* feat(core): add declarative agent frontmatter schema constants and 9 new fields

Adds agent-frontmatter-schema.ts as the single source of truth for the CC 2.1.168
declarative-agent enum constants (EFFORT_VALUES, PERMISSION_MODE_VALUES,
MEMORY_VALUES, ISOLATION_VALUES, COLOR_VALUES) and lenient parsers (parseEffort,
parseMaxTurns, parseBackground, parseStringOrArray) that mirror DL7's warn-and-drop
posture. Also adds a permissionModeToApprovalMode bridge to map CC's permission
modes onto qwen-code's existing approvalMode semantics.

Extends SubagentConfig with 9 optional fields carried verbatim from CC frontmatter:
permissionMode, effort, maxTurns, skills, initialPrompt, memory, isolation,
mcpServers, hooks. Runtime semantics for the metadata-only fields are deferred
to follow-up PRs; this lands the data carrier.

Refs #4821 #4721 #4732

* feat(core): parse and serialize 9 new declarative agent frontmatter fields

Extends parseSubagentContent and serializeSubagent to round-trip the 9 CC
2.1.168 fields previously added to the SubagentConfig type. Parsing follows
DL7's lenient warn-and-drop posture (vs the existing strict throw posture
used for approvalMode), so a Claude Code agent file with an invalid optional
field still parses with that field dropped — matching CC behavior so users
can drop CC agent files into .qwen/agents/ unchanged.

Adds a permissionMode → approvalMode bridge: when frontmatter has
permissionMode but no approvalMode, the bridge resolves the approvalMode at
parse time using the same mapping as claude-converter.ts. If both are set,
approvalMode wins (already-explicit values take precedence over inferred
ones).

Tests:
- 22 new parser cases covering happy paths, invalid drops, permissionMode
  bridge precedence, color allowlist (with auto sentinel preserved), and
  loose-validation passthrough for mcpServers / hooks.
- 9 new serializer cases asserting round-trip and omit-when-unset behavior.

Refs #4821 #4721 #4732

* feat(core): promote top-level maxTurns to runConfig.max_turns at convert time

Wires the new top-level `maxTurns` field into the existing runtime configuration
pipeline so the value declared in agent frontmatter actually limits the agent's
turn budget at run time. When both the top-level `maxTurns` and the legacy
nested `runConfig.max_turns` are set, the top-level field wins (more specific
and matches the CC frontmatter shape upstream agent files use). When only the
nested field is set, behavior is unchanged.

Refs #4821 #4721 #4732

* refactor(core): use shared permissionMode bridge and parseStringOrArray in claude-converter

Replaces the inline claudeToQwenMode table and the local parseStringOrArray
helper in claude-converter.ts with the shared exports from
agent-frontmatter-schema.ts. One source of truth for the CC ↔ qwen mapping
keeps the two import paths (.qwen/agents/*.md frontmatter and Claude plugin
import) in sync — when the upstream CC schema changes, only one table needs
updating.

Adds explicit tests for the bridge mapping (six known modes + unknown
fallback). The fallback now happens at the call site rather than inside the
table, but the observable behavior is unchanged.

Refs #4821

* docs(subagents): document CC-compatible frontmatter fields

Adds a Claude Code Compatibility Fields section to the user-facing subagents
reference, covering the 9 new frontmatter fields landed in this PR. The intent
is for users with existing Claude Code agents to know they can drop the files
into `.qwen/agents/` and have them parse identically.

Refs #4821

* refactor(core): reuse parseBackground in declarative agent loader

Replaces the inline boolean-or-string lenient parse for the background field
with the shared parseBackground util added in the schema module. Same observable
behavior; one fewer place to keep in sync when the upstream shape changes.

Refs #4821

* fix(core): address adversarial review of declarative agents PR

Four self-review findings caught and fixed before opening the PR:

1. **mcpServers/hooks round-trip claim was broken.** The local yaml-parser only
   formats one level of nesting, so serializing an agent with nested mcpServers
   or hooks would mangle the value into '[object Object]'. The fields are still
   carried verbatim in memory (read path is fine), but the serializer no longer
   emits them — losing the field through '/agents' edits is strictly safer than
   corrupting it. Documented the limitation in docs/users/features/sub-agents.md.

2. **approvalMode throw vs permissionMode lenience asymmetry.** The pre-existing
   approvalMode parser threw on invalid values, killing the entire agent file.
   With the new permissionMode bridge, a CC-imported file with 'permissionMode:
   bypassPermissions' + 'approvalMode: tpyo' would reject the file instead of
   dropping the typo and using the bridge. Demoted approvalMode to the same
   DL7-parity warn-and-drop posture all the new fields use; the bridge now runs
   when approvalMode is invalid.

3. **parseEffort missed numeric strings.** CC's DL7 falls back to parseInt for
   non-enum effort strings so 'effort: "5"' (quoted YAML) round-trips like
   'effort: 5'. Added the same fallback and tests for floats / partial numeric
   strings / valid numeric strings.

4. **convertAgentFiles stripped 6/9 of the new fields.** The Claude plugin
   import path read frontmatter into a fixed ClaudeAgentConfig shape, so
   'effort', 'maxTurns', 'initialPrompt', 'memory', 'isolation', 'mcpServers',
   and 'background' were silently dropped when installing a CC plugin agent.
   Build the rewritten frontmatter from the original keys first, then overlay
   the converter's transformations for the keys it owns — unknown CC fields
   now passthrough verbatim, future-proofing against later CC additions.

Refs #4821 #4721 #4732

* fix(core): round-2 self-review fixes for declarative agents PR

Second adversarial review surfaced 5 more findings; all fixed:

1. **convertAgentFiles passthrough corrupted nested objects.** Round-1 fixed
   serializeSubagent to skip emitting mcpServers/hooks (the local yaml-parser
   collapses nested values to '[object Object]'). The plugin-import passthrough
   I added to claude-converter.ts had the same mangle bug — a CC plugin agent
   with nested mcpServers got written to disk as 'mcpServers:\n  - [object
   Object]'. Now skips both fields via a NESTED_FIELDS_NOT_ROUND_TRIPPABLE set.

2. **parseEffort accepted 0 and negative integers.** Docs say 'positive integer'
   and parseMaxTurns already rejects <= 0. parseEffort didn't, accepting
   '0' / '-5' / 0 / -1 as valid efforts. Aligned both helpers and added tests
   for the boundary values.

3. **Function name collision: permissionModeToApprovalMode.** packages/core/src/
   tools/agent/agent.ts has a module-private permissionModeToApprovalMode that
   maps the qwen PermissionMode enum to ApprovalMode enum (different domain
   entirely). My new exported helper used the same name; IDE auto-import would
   silently return undefined for every qwen enum value. Renamed the export to
   claudePermissionModeToApprovalMode and updated 4 callers.

4. **color: 'auto' round-trip asymmetry.** Round-1 added a test pinning that
   the parser preserves the legacy 'auto' sentinel. But serializeSubagent
   already had a pre-existing 'skip emit when auto' branch, so a parse →
   serialize → parse cycle dropped the sentinel. The CLI helpers
   (shouldShowColor / getColorForDisplay) already treat 'auto' identically to
   undefined, so normalizing 'auto' to undefined at parse time has no
   downstream effect AND makes round-trip cleanly idempotent.

5. **Converter approvalMode precedence diverged from loader.** When a CC source
   file had both 'permissionMode: bypassPermissions' AND 'approvalMode: default',
   the convertClaudeAgentConfig path wrote 'approvalMode: yolo' (bridge wins).
   The loader's rule is 'approvalMode wins over bridge'. Extended
   ClaudeAgentConfig with an approvalMode field and gated the bridge emit on
   'source approvalMode is unset', aligning convert and load precedence.

Refs #4821 #4721 #4732

* fix(core): round-3 self-review fixes — drift, API hygiene, converter contract

Three quality findings from the round-3 adversarial pass (after rounds 1+2
fixed 9 issues each). All low-severity but cheap; the runtime was correct,
the file-on-disk + public-API surface needed tightening.

1. **Stale runConfig.max_turns duplicated on every write.** Top-level maxTurns
   was promoted in 9b903ee41 but the serializer still emitted runConfig.max_turns
   verbatim. A file with both fields kept both indefinitely. The runtime
   already prefers top-level (so behavior is correct), but a third-party tool
   or future viewer reading the legacy nested field would act on stale data.
   Prune nested max_turns from the serialized runConfig when top-level
   maxTurns is set; drop the runConfig block entirely if pruning leaves it
   empty. Three new tests pin the prune, the drop-empty-block path, and the
   no-top-level fallback.

2. **14 internal symbols leaked into the public package API.** The PR
   re-exported every helper from agent-frontmatter-schema.ts via
   subagents/index.ts. grep against packages/cli + packages/vscode-ide-companion
   finds zero cross-package consumers — both internal users (subagent-manager,
   claude-converter) already import directly from the schema file. Locking
   constant names like EFFORT_VALUES and parseEffort in the public API would
   constrain follow-up PRs (e.g. when js-yaml lands and the schema shape
   changes). Removed the entire export block; left a comment explaining the
   choice so a future PR knows the bar for re-introducing it.

3. **convertClaudeAgentConfig silently dropped a standalone approvalMode.**
   Round-2 gated the permissionMode bridge on !claudeAgent.approvalMode for
   precedence parity with the loader, but never added the explicit copy for
   approvalMode-only callers. Result: caller passes {approvalMode: 'plan'} →
   output has no approvalMode at all. The in-tree convertAgentFiles path
   recovered via the unowned-key passthrough, but exported direct-call surface
   needs the explicit copy. Two new tests pin both the standalone case and
   the precedence-when-both case.

Also: pinned yaml-parser limitation tests (added in the round-3 e2e
independent check) document that the lightweight parser cannot round-trip
nested mcpServers/hooks; documentation + types comments updated to call out
the carve-out so users know the field works only for flat forms until
js-yaml lands.

Refs #4821 #4721 #4732

* refactor(core): shrink declarative agents PR to vertically-sliced v1

Reduces this PR to ship only the fields that have an end-to-end runtime path
today: permissionMode (bridges to existing approvalMode), maxTurns (wires
into runConfig.max_turns), and a tightened color allowlist. Everything else
gets deferred to follow-up PRs that first land the prerequisite infra they
need.

Removed:
- effort (no model-layer effort param in qwen providers)
- mcpServers / hooks (need nested-aware YAML parser — yaml-parser.ts is
  hand-rolled and only handles 1 level)
- memory (qwen's auto-memory has no user/project/local scope distinction)
- isolation (workflow PR #4732 owns the runtime)
- initialPrompt (needs --agent CLI flag, no main-session-agent infra)
- skills (needs SkillManager consumption of config.skills)

This drops 7 fields from SubagentConfig + types.ts, deletes their parser /
serializer / test code, deletes the unused enum constants and helpers from
agent-frontmatter-schema.ts (EFFORT_VALUES, EFFORT_ALIASES, MEMORY_VALUES,
ISOLATION_VALUES, parseEffort, isMemory, isIsolation), and trims the user
docs to the supported field set with a pointer to the design doc for the
deferred fields.

Why ship the design doc alongside such a narrow v1: the full
reverse-engineering record in docs/declarative-agents-port.md (DL7/Ig5/GN/_Y
constants, error messages, schema parity decisions, coordination matrix with
workflow PR #4732) stays load-bearing for the follow-up PRs. A status table
at the top discloses what's deferred and why.

Net: -559 LOC across 7 files. Final PR is permissionMode bridge + maxTurns
wiring + color allowlist + design reference doc + the yaml-parser
nested-limitation pin tests added during round-3 review.

Refs #4821 #4721 #4732

* refactor(core): revert round-X residuals to hit v1's actual minimum LOC

The previous shrink left round-1/2/3 fixes in place that were originally
for the wider 9-field scope; with the carry-only fields gone they're no
longer needed. Reverting them all back to pre-PR behaviour:

- approvalMode strict throw (round-1 demoted to lenient; defer the
  symmetry fix to a separate PR if wanted)
- runConfig.max_turns prune on serialize (round-3 cleanup; defer)
- color: 'auto' normalised to undefined (round-2 round-trip fix; defer)
- claude-converter NESTED_FIELDS_NOT_ROUND_TRIPPABLE + CONVERTER_OWNED_KEYS
  + passthrough loop (round-1/2 — all for the deleted carry-only fields)
- ClaudeAgentConfig.approvalMode + precedence gating (round-3)
- parseBackground in shared schema module (the dedup wasn't pulling weight)
- parseStringOrArray in shared schema module (same)

The shared bridge claudePermissionModeToApprovalMode + the disambiguated
name stay — converter and loader use the same map and the name collision
risk is real.

Net: -336 LOC vs the prior shrink commit.

* docs: sync user-facing docs to actual v1 behaviour

The previous shrink left two stale claims in docs/users/features/sub-agents.md
that referenced behaviour reverted in the round-X-revert commit:

- maxTurns row claimed 'the legacy nested value is pruned from the on-disk
  file on save to avoid two sources of truth' — the prune was reverted
- color row claimed 'auto is accepted on read and normalised to undefined
  for round-trip parity' — the normalisation was reverted; auto is preserved
  as-is for backward compat

Updated both rows to match what the parser actually does now.

The design doc (docs/declarative-agents-port.md) is unchanged: its top-line
disclaimer already labels the body as reference material for follow-up PRs,
so the references to the wider plan (D7 bridge, P1-P4 phases) are
accurate-as-reference even though some details are deferred.

* fix(core): use Map.get in claudePermissionModeToApprovalMode (prototype-chain footgun)

The PERMISSION_MODE_TO_APPROVAL_MODE table was a plain object accessed with
`[key]`. Calling claudePermissionModeToApprovalMode('__proto__') walked the
prototype chain and returned Object.prototype — a non-string value that
violated the declared return type. The current loader path was safe because
isPermissionMode() filtered prototype-key strings before the bridge ran, but
the exported function itself was a latent footgun for any future caller
that didn't know to pre-filter.

Switched the lookup table to a Map so prototype keys cannot be reached.
Added explicit tests for __proto__, constructor, hasOwnProperty, toString.

Refs #4821

* fix(core): address round-2 review on declarative agents PR #4842

Two findings:

1. `serializeSubagent` wrote both `permissionMode` and the bridge-derived
   `approvalMode` into the same frontmatter block. On the next load the parser
   takes `approvalMode` (explicit wins over bridge), so `permissionMode`
   silently became dead frontmatter — a user editing it later in the file
   would be ignored. Skip the `permissionMode` emit when `approvalMode` is
   also being written; `permissionMode` still round-trips when it's the
   user's only intent. Two new tests pin both branches.

2. `subagents/index.ts` had a NOTE comment referencing `EFFORT_VALUES` and
   `parseEffort` as example schema helpers that intentionally aren't
   re-exported. Those symbols were removed during the v1 scope shrink
   (637b8b70c) and don't exist in the current module. Updated the comment to
   reference `claudePermissionModeToApprovalMode`, `parseMaxTurns`, and
   `isPermissionMode`, which are the actual current contents.

Sibling-drift note: the same dual-emit pattern exists for `maxTurns` vs
`runConfig.max_turns` — round-X revert (70a876d38) explicitly deferred the
`runConfig.max_turns` prune. Not addressed here per that earlier decision.

Refs #4842
2026-06-11 05:03:31 +08:00
tanzhenxin
5fe12d4cc0
feat(core): extend cross-auth fast models to agents (#4153)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(core): extend cross-auth fast models to agents

* fix(core): tighten cross-auth model resolution fallbacks

When a forked-agent caller passes a selector that cannot resolve (e.g.
`fast` with no fast model configured), fall back to the parent session
model instead of forwarding the raw selector string to the provider.
Matches the subagent path, where unresolvable selectors mean "inherit
parent".

In BaseLlmClient.createContentGeneratorForModel, do not cache the
unregistered-model fallback. getCurrentContentGenerator() reads the
runtime view from AsyncLocalStorage, which can differ between calls;
caching would pin the first call's view-bound generator under the
selector key and reuse it on later calls after that view has unwound.

* docs(core): drop stale getFastModelForSideQuery from sideQuery JSDoc

The function was removed when fast-model resolution collapsed onto getFastModel(); the JSDoc fallback chain still mentioned it.
2026-05-20 00:25:29 +08:00
顾盼
aeeb2976d6
feat(web-search): remove built-in web_search tool, replace with MCP-based approach (#3502)
* feat(web-search): add GLM (ZhipuAI) web search provider

- Add GlmProvider class implementing BaseWebSearchProvider using the
  ZhipuAI Web Search API (https://open.bigmodel.cn/api/paas/v4/web_search)
- Support multiple search engines: search_std, search_pro, search_pro_sogou,
  search_pro_quark
- Support optional config: maxResults, searchIntent, searchRecencyFilter,
  contentSize, searchDomainFilter
- Truncate query to 70 characters per API limit
- Register 'glm' in the provider discriminated union (types.ts) and
  createProvider() switch (index.ts)
- Add GlmProviderConfig to settingsSchema, ConfigParams, and Config class
- Add --glm-api-key CLI flag and GLM_API_KEY env var support in webSearch.ts
- Forward GLM_API_KEY in sandbox environment
- Update provider priority list: Tavily > Google > GLM > DashScope
- Add 17 unit tests for GlmProvider and 4 integration tests in index.test.ts
- Update docs/developers/tools/web-search.md with GLM configuration,
  env vars, CLI args, pricing, and corrected DashScope billing info
- Fix stale OAuth/free-tier references in web-search.md

Closes #3496

* docs(web-search): fix DashScope note and add GLM server-side limitations

* fix(web-search): make DashScope provider work with standard API key, remove qwen-oauth dependency

- DashScopeProvider.isAvailable() now checks config.apiKey instead of authType
- Remove OAuth credential file reading and resource_url requirement
- Use standard DashScope endpoint: dashscope.aliyuncs.com/api/v1/indices/plugin/web_search
- Read DASHSCOPE_API_KEY env var and --dashscope-api-key CLI flag
- Forward DASHSCOPE_API_KEY into sandbox environment
- Update integration test to detect DASHSCOPE_API_KEY
- Update docs to reflect new API key based configuration

* feat(web-search): remove built-in web search tool

The web_search tool and all related provider implementations are removed.
Web search functionality will be provided via MCP integrations instead,
which is the direction the broader agent ecosystem is moving.

Removed:
- packages/core/src/tools/web-search/ (entire directory)
- packages/cli/src/config/webSearch.ts
- integration-tests/cli/web_search.test.ts
- ToolNames.WEB_SEARCH, ToolErrorCode.WEB_SEARCH_FAILED
- webSearch config in ConfigParams, Config class, settingsSchema
- CLI options: --tavily-api-key, --google-api-key, --google-search-engine-id,
  --glm-api-key, --dashscope-api-key, --web-search-default
- Sandbox env forwarding for TAVILY/GLM/DASHSCOPE/GOOGLE search keys
- web_search from rule-parser, permission-manager, speculation gate,
  microcompact tool set, and builtin-agents tool list

* fix: remove websearch reference

* docs: remove websearch tool

* docs: add break change guide

* fix review
2026-04-24 11:29:02 +08:00
Shaojin Wen
83b394e423
feat(core): implement fork subagent for context sharing (#2936)
* feat(core): implement fork subagent for context sharing

- Make subagent_type optional in AgentTool
- Add forkSubagent.ts to build identical tool result prefixes
- Run fork processes in the background to preserve UX

* fix(core): fix test failures related to root execution and optional subagent_type

- Skip pathReader and edit tool permission tests when running as root
- Fix agent.test.ts to correctly mock execute call with extraHistory
- Remove unused imports in forkSubagent.ts

* fix(core): fix fork subagent bugs and add CacheSafeParams integration

Bug fixes:
- Fix AgentParams.subagent_type type: string -> string? (match schema)
- Fix undefined agentType passed to hook system (fallback to subagentConfig.name)
- Fix hook continuation missing extraHistory parameter
- Fix functionResponse missing id field (match coreToolScheduler pattern)
- Fix consecutive user messages in Gemini API (ensure history ends with model)
- Fix duplicate task_prompt when directive already in extraHistory
- Fix FORK_AGENT.systemPrompt empty string causing createChat to throw
- Fix redundant dynamic import of forkSubagent.js (merge into single import)
- Fix non-fork agent returning empty string on execution failure
- Fix misleading fork child rule referencing non-existent system prompt config
- Fix functionResponse.response key from {result:} to {output:} for consistency

CacheSafeParams integration:
- Retrieve parent's generationConfig via getCacheSafeParams() for cache sharing
- Add generationConfigOverride to CreateChatOptions and AgentHeadless.execute()
- Add toolsOverride to AgentHeadless.execute() for parent tool declarations
- Fork API requests now share byte-identical prefix with parent (DashScope cache hits)
- Graceful degradation when CacheSafeParams unavailable (first turn)

Docs:
- Add Fork Subagent section to sub-agents.md user manual
- Add fork-subagent-design.md design document

* fix(core): apply subagent tool exclusion to forked agents

Fork children were inheriting parent's cached tool declarations directly,
bypassing prepareTools() filtering and gaining access to AgentTool and
cron tools. Extract EXCLUDED_TOOLS_FOR_SUBAGENTS as a shared constant
and apply it to forkToolsOverride.

* fix(core): skip env history whenever extraHistory is provided

Previously gated on generationConfigOverride, which meant the no-cache
fallback path (CacheSafeParams unavailable) still ran getInitialChatHistory
and duplicated env bootstrap messages already present in the parent's
history. Gate on extraHistory instead so both fork paths skip env init.

* fix(core): use explicit skipEnvHistory flag for fork env handling

The previous fix gated env-init skipping on the presence of extraHistory,
but agent-interactive (arena) also passes extraHistory — its chatHistory is
env-stripped by stripStartupContext() and DOES need fresh env init for the
child's working directory. Skipping env there broke the interactive path.

Replace the implicit gate with an explicit skipEnvHistory option that only
fork sets (when extraHistory is present, since fork's history comes from
getHistory(true) and already contains env).

* fix(core): defend skipEnvHistory gate against empty extraHistory

Edge case: when the parent's rawHistory ends with a user message and has
length 1, extraHistory becomes []. The previous gate (extraHistory !==
undefined) would set skipEnvHistory: true, leaving the fork with neither
env bootstrap nor parent history. Check length > 0 so empty arrays fall
through to the normal env-init path.

* fix(core): apply skipEnvHistory to stop-hook retry execute

The second subagent.execute() call in the SubagentStop retry loop was
missing skipEnvHistory, so on retry the fork's env context would be
duplicated — same bug as the initial tanzhenxin report, just on a less
common code path.
2026-04-14 14:27:38 +08:00
tanzhenxin
8d74a0cf0a
feat(subagents): add disallowedTools field to agent definitions (#3064)
* feat(subagents): add disallowedTools field to agent definitions

Add a `disallowedTools` blocklist to agent frontmatter, letting agents
specify tools they should not have access to. Supports exact tool names,
MCP server-level patterns (e.g., `mcp__slack`), and display name aliases.

Applied as a post-filter in AgentCore.prepareTools() after the existing
`tools` allowlist. Persisted through serialize/parse roundtrips.

* docs: document disallowedTools and MCP tool behavior for subagents

Add Tool Configuration section to sub-agents docs explaining:
- tools allowlist and disallowedTools blocklist
- How MCP tools follow the same allowlist/blocklist rules
- MCP server-level patterns in disallowedTools

* fix(subagents): validate disallowedTools in SubagentValidator

Reuse the existing validateTools() method to validate disallowedTools
entries at config validation time, catching non-string and empty entries
before they reach runtime.

* test: remove flaky BaseSelectionList scroll test on Windows
2026-04-13 18:24:02 +08:00
tanzhenxin
0026777828
feat(subagents): propagate approval mode to sub-agents (#3066)
* feat(subagents): propagate approval mode to sub-agents

Replace hardcoded PermissionMode.Default with resolution logic:
- Permissive parent modes (yolo, auto-edit) always win
- Plan-mode parents keep sub-agents in plan mode
- Agent definitions can declare approvalMode in frontmatter
- Default fallback is auto-edit in trusted folders
- Untrusted folders block privileged mode escalation

Also maps Claude permission aliases (acceptEdits, bypassPermissions,
dontAsk) to qwen-code approval modes in the converter.

* fix(subagents): correct dontAsk mapping and add approval mode resolution tests

Map Claude's `dontAsk` to `default` instead of `auto-edit` — `dontAsk`
denies prompts (restrictive) so `default` is a closer semantic match.

Add 9 unit tests covering the full `resolveSubagentApprovalMode` decision
matrix: permissive parent override, agent-declared modes, trusted/untrusted
folder blocking, and plan-mode fallback.

* test: remove flaky InputPrompt tab-suggestion test on Windows
2026-04-13 17:50:26 +08:00
tanzhenxin
7b29d1f4a3 fix(subagents): ensure model selection works for bare model IDs
- Handle bare model IDs by inheriting parent's authType
- Create dedicated ContentGenerator for any explicit model selection
- Add tests for model override scenarios

Previously, only cross-provider prefixed models (e.g., "openai:gpt-4o")
triggered ContentGenerator creation. Bare IDs like "qwen-coder" were
ignored, causing subagents to always use the parent's model.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-30 09:33:54 +00:00
tanzhenxin
5d58b2f112 feat: simplify subagent model configuration with model selector
Refactor subagent model configuration from nested modelConfig object to a simple model string field for better UX and clarity.

Changes:
- Replace modelConfig object with model string in SubagentConfig interface
- Add model-selection.ts utility for parsing and validating model selectors
- Support 'inherit' keyword and bare model IDs (e.g., 'glm-5', 'claude-sonnet-4-6')
- Maintain backward compatibility by parsing legacy modelConfig frontmatter
- Update validation to reject cross-provider authType-prefixed selectors
- Update SDK types (TypeScript and Java) to reflect new schema
- Add comprehensive tests for model selection and validation
- Update documentation with model selection examples

Breaking changes:
- modelConfig.frontmatter field deprecated in favor of model field
- Cross-provider model selectors (e.g., 'openai:gpt-4') not supported for subagents

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-27 11:49:45 +08:00
tanzhenxin
1852a73a3f fix(subagents): change limits from hard errors to soft warnings
- Increase description warning threshold from 500 to 1,000 characters
- Change system prompt 10,000 char limit from error to warning
- Remove intermediate 5,000 char warning threshold for system prompts
- Update documentation to reflect soft warning behavior

This provides more flexibility for users while still guiding them
toward better practices.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-15 20:56:25 +08:00
hs-ye
ed59831213 fix: correct sub-agent limits in documentation
- Change description field limit from 301 to 300 characters
- Verified limits from source code in CreationSummary.tsx

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-28 14:21:28 +11:00
hs-ye
8e72c4fb87 Add undocumented limits to sub-agents documentation
- Document the 301 character limit for description field
- Document the 10,000 character limit for system prompt

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-27 11:00:54 +11:00
LaZzyMan
6e641b8def feat: add docs 2026-01-19 14:51:49 +08:00
pomelo-nwu
cf3c020e5b feat: rename Sub Agents to Subagents 2025-12-15 22:22:25 +08:00
pomelo-nwu
e5a3670ed3 feat: update docs 2025-12-15 19:12:56 +08:00
pomelo-nwu
5b16cd5945 feat: update docs 2025-12-15 19:10:40 +08:00
joeytoday
ad9e286806 docs: update common workflows for clarity and formatting; remove sub-commands documentation 2025-12-12 16:04:46 +08:00
joeytoday
70b5aee381 docs: Add documentation for Sub Agents feature and update user guides
- Introduced a new documentation file for Sub Agents, detailing their purpose, benefits, configuration, and usage examples.
- Updated the overview and quickstart guides to improve clarity and remove outdated information.
- Created a comprehensive command reference document for Qwen Code, detailing slash commands, at commands, and exclamation commands for better user guidance.
- Enhanced the formatting and organization of existing documentation for improved readability and usability.
2025-12-09 14:05:26 +08:00
Renamed from docs/users/features/subagents.md (Browse further)