feat(subagents): programmatic dispatch API with per-dispatch controls (incremental) - #831
Open
northwang-lucky wants to merge 7 commits into
Open
northwang-lucky wants to merge 7 commits into
northwang-lucky wants to merge 7 commits into
Conversation
- Port the dispatch runtime (createDispatchRuntime /
registerDispatchRuntime, globalThis.__piSubagentDispatch) onto the
upstream SubagentQueue base, keeping the API contract from the
incremental branch.
- Concurrency gating (former G5) is intentionally absent: queueing and
per-parent limits belong to the upstream SubagentQueue.
- Lifecycle-event effective values use a two-step fallback (dispatch
param -> parent state) as an interim; the profile tier ships with the
runtime-integration slice, which must also restore the three-tier
assertions and switch events to run-authoritative values.
- Suites: S1 (10) and S4 (6) ported/adapted; S5 dropped with G5;
S2/S3/S6 deferred to their own slices.
…re filter key G2: Add request.tools (whitelist override) and request.disallowedTools (blacklist post-merge) to StartSubagentRequest. When request.tools is present it replaces profile.tools as the base tool list; extension tool names are merged via withSubagentExtensionTools. disallowedTools is applied after the merge and takes absolute precedence. G3: Add request.extensions and request.denyExtensions to filter which loaded extensions contribute tool names. The pipeline uses filterExtensionsBySource() which derives stable match keys via extensionFilterKey(): npm sources yield the package name, file-path and auto-discovered sources yield the basename without extension (extracted from sourceInfo.path since sourceInfo.source is "auto" for all auto-discovered extensions). Dispatch params override profile params; profile has no extensions/denyExtensions in upstream so dispatch is the only filter layer. C fix: extensionFilterKey() handles scoped npm packages (@scope/name@version) correctly by stripping the trailing version suffix instead of splitting on "@" which would lose the scope. For file-path sources, basename without extension is used. The filterExtensionsBySource() helper applies both allow and deny lists with deny winning over allow. Also adds request.excludeTools alongside the unconditional reserved control name exclusion (Agent/get_subagent_result/steer_subagent). Tests (12 cases, all drive real exported functions): withSubagentExtensionTools: 4 (merge/filter/dedup/empty) extensionFilterKey: 4 (scoped npm/unscoped npm/file path/no-ext) filterExtensionsBySource: 4 (path fallback/deny wins/no filter/graceful) Mirror-style ported cases (10) that recreated production logic locally were dropped — they would not turn red on a runtime revert and one (:170) fossilised the old file-path semantic opposite to the C fix. Source-inspection patterns in rpc-manager.test.mjs updated to match the new pipeline shape (excludeTools array and baseTools variable).
…eTools in lifecycle events (G4) - Runtime resolves effective model via three-level fallback (dispatch param → profile → parent) early in start(), before resourceSnapshot and initialRun construction. - resourceSnapshot now carries model and thinking for audit trail. - initialRun carries model, thinking, and activeTools so downstream consumers see the authoritative resolved values. - Dispatch layer reads model/thinking/activeTools from the run object instead of re-resolving from params or parentState (transitional fallback removed). - SubagentRunInfo and SubagentResourceSnapshot gain model/thinking/activeTools fields. - S4 tests restored with three-level semantic: fake controller supplies _runValues distinct from request params, proving dispatch reads from run. - G4 behavior tests added: thinking fallback, request thinking bypasses parent invalid, request model wins chain, unresolvable model rejects, activeTools carried on run. - Source inspection tests verify runtime structure and dispatch read path.
…e audit trail - Add persistSession?: boolean to StartSubagentRequest and SubagentDispatchParams (default true; false = in-memory only). - When persistSession=false: SessionManager.inMemory() is used instead of create(), so no .jsonl file is written to disk. - Audit metadata (SUBAGENT_META_TYPE, STATUS, RESULT) is redirected to the parent session when persistSession=false, preserving the dispatch trail after the in-memory session is garbage-collected. - Three-level fallback: request.persistSession ?? profile.persistSession ?? true, aligning with upstream's profile-level persistSession field. - S6 contract tests: persistSession forwarding through dispatch layer. - G6 behavior tests: in-memory vs persisted session creation, audit metadata routing to parent sessionManager (real SDK services). - G6 source test: structural guard for three-level fallback resolution. - S4 edge-case assertions: empty model → effectiveModel '', null thinking → effectiveThinking null.
The production dispatch module mutated the request object to attach a sessionId for test fake correlation — a side-channel from the old branch that was never part of the public API. Remove it and have test fakes track run ids through their own _runIds array instead.
The ported feature branches had dropped the AGENTS.md coverage their predecessor carried: add the file-map row for lib/subagent-dispatch.ts plus a short section describing the programmatic dispatch entry point and the per-dispatch parameter overrides.
- The dispatch runtime never reads settings; concurrency policy belongs
to the upstream SubagentQueue. Remove the interface member, the
production binding, the fake key (kept byte-identical with the
pipeline branch's contract suite), and the now-unused import.
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.
Part of the discussion in #829 — this is the incremental of the two reference implementations (the pipeline variant is #832). Either one is ready to walk forward; whichever the maintainer prefers, we'll rework against your review (or close both without hard feelings).
What it adds (5 commits, branched from
main@8366762):lib/subagent-dispatch.ts— a versioned programmatic dispatch API (registerDispatchRuntime()->globalThis.__piSubagentDispatch):startSubagentDispatch(parentSessionId, params)with per-dispatchtools/disallowedTools,extensions/denyExtensions(source-aware filter keys),excludeTools(reserved control names always excluded),signal/onUpdate, and lifecycle events carrying the runtime-resolved effective model/thinking/activeTools — not request echoespersistSession: false— child session runs in-memory (nothing hits disk); the run's audit metadata is redirected to the parent session, reusing the existingpi-web:subagentcustom-entry formatSubagentQueue(b77a25f)Tests: shared contract suite (
subagent-dispatch*.test.mjs,subagent-tools-resolution.test.mjs) is byte-identical with the pipeline branch; this branch lands at 1064 passing (upstream 1020 + 44),tsc/eslintclean. Behavior validated live against a real model (dispatch round-trip, non-echoed effective values, in-memory run leaves no.jsonl).Known coverage note: the profile-level
persistSession=falsecase is pinned by a source-level assertion here; a driven behavioral test for it exists on #831 and we'll port it over if this branch becomes the chosen one.