Repository navigation
feat(ts): port vended subagent tool to TypeScript [replica of harness-sdk#4971] - #12
poshinchen wants to merge 7 commits into
Conversation
Port the Python vended subagent tool to TypeScript and align _defaultBuilder with the Python behavior (skip unknown tools/MCP servers with a warning, contextManager 'auto' unless the model is stateful, propagate sandbox/printer/trace attributes). Add README and TypeScript examples to the vended tools docs.
- _defaultBuilder skips the parent's context-manager tools so the child's own 'auto' manager can register retrieve_context (fixes nested delegation and parents with a context manager) - resume only applies responses supplied on the current resume, so a child that interrupts more than once can be resumed again - export AgentBuilder type from multiagent - validate defaultPreset against presets - TS-style factory error messages; depth-limit message uses the tool name
- makeSubagent rejects context modes other than none/all/no_tools - non-string instructions from the model return a clear error result - document that children get their own context-manager tools
poshinchen
left a comment
There was a problem hiding this comment.
Strandly v3-reason (optimized) review
Assessment: comment
Assessment: Comment
This is a well-structured, thoroughly documented port that mirrors the Python feature closely, with strong test coverage (depth capping, context forking, interrupts, schema derivation). My feedback centers on one recurring theme plus an API-process note.
Review Categories
- Maintainability: The default builder and its tests reach into
Agent's private fields (_sandbox,_printer,_tracer._traceAttributes) viaas unknown ascasts. This bypasses type checking, so a field rename breaks propagation silently. Prefer public/internal accessors. - Behavior correctness:
printer: printer !== undefinedcollapses a possibly-customPrinterto a boolean; please verify this matches the "built the way the parent was" intent and Python's behavior for string/router models. - Testing: Where propagated child config is deterministic, prefer a single object-shape assertion over per-field checks that can miss regressed fields.
- API process: New public subpath — please confirm it carries
api/needs-review.
Nice work bringing TypeScript to parity with the Python subagent tool, including the matching docs and README.
File-level comments:
strands-ts/src/vended-tools/subagent/index.ts: Important
Issue: This PR introduces a new public subpath export @strands-agents/sdk/vended-tools/subagent with new customer-facing APIs (makeSubagent, MakeSubagentOptions, SubagentInput, etc.). I can't confirm from the diff whether the PR carries the api/needs-review label.
Suggestion: Please ensure this change goes through API bar-raising review and carries the api/needs-review label, since it adds public primitives customers will depend on. The PR description's API section is thorough, which should make that review straightforward.
Inline comments (could not anchor to the diff):
strands-ts/src/multiagent/spec.ts:410: Important
Issue: The default builder reaches into the parent's private fields via as unknown as casts (_sandbox, _printer, _tracer, _traceAttributes) to propagate sandbox, printer, and trace attributes. This couples the builder to Agent's internal representation, so a rename or refactor of those fields silently breaks propagation with no compile-time signal.
Suggestion: Please consider exposing these via public/internal getters on Agent (e.g. agent.sandbox, agent.traceAttributes) and reading those instead, so the contract is checked by the type system. If public accessors are intentionally avoided, please document why here.
strands-ts/src/multiagent/spec.ts:441: Important
Issue: printer: printer !== undefined collapses the parent's printer configuration to a boolean. If the parent was constructed with a custom Printer instance (not just true/false), the child receives a default printer rather than the parent's, which differs from "built the way the parent was."
Suggestion: Please verify the intended behavior — if the goal is to mirror the parent's printer, consider passing the actual printer instance through (when available) rather than coercing to a boolean. If a boolean is deliberate for isolation, a brief comment explaining the tradeoff would help.
strands-ts/src/multiagent/spec.ts:436: Suggestion
Issue: The stateful check typeof childModel === 'object' && 'stateful' in childModel && childModel.stateful can match on a string model name that was resolved from a Choice only via the typeof === 'object' guard, which is fine, but when childModel is a bare model string the 'auto' context manager is always applied regardless of statefulness.
Suggestion: Please verify this matches Python's behavior for string/router models. If statefulness can only be known for Model objects, a short comment noting that string/router models always get 'auto' would clarify intent for future readers.
strands-ts/src/multiagent/spec.ts:440: Suggestion
Issue: traceAttributes: { ...traceAttributes } is spread from a value read via unchecked private-field casts (_tracer._traceAttributes). If _tracer is ever undefined at build time, this throws; the cast asserts it is always present.
Suggestion: Please verify _tracer is guaranteed to be initialized on the parent at the point _defaultBuilder runs. A defensive ?? {} on the attributes (or reading from a public accessor) would avoid a runtime crash if that invariant ever changes.
strands-ts/src/multiagent/__tests__/spec.test.ts:33: Suggestion
Issue: internals() reaches into _printer and _tracer._traceAttributes via as unknown casts, mirroring the production code's private-field access. These tests will pass even if the public contract regresses, and break on an internal rename unrelated to behavior.
Suggestion: If Agent exposes (or can expose) public getters for sandbox/trace attributes/printer, please assert against those instead so the test validates observable behavior rather than internal field names.
strands-ts/src/multiagent/__tests__/spec.test.ts:268: Suggestion
Issue: This asserts statefulness/context-manager behavior only via a boolean (contextManager !== undefined), while the sandbox/printer test (lines 265-278) uses three separate expect calls on child.sandbox, trace attributes, and printer. Where the full set of propagated fields is deterministic, a single object-shape assertion would catch an unexpected or regressed field that per-field checks silently miss.
Suggestion: Please consider asserting the relevant child configuration as one object equality where feasible, reserving per-field checks for genuinely non-deterministic values.
strands-ts/src/vended-tools/subagent/subagent.ts:105: Suggestion
Issue: presets is shallow-copied with { ...options.presets }, but each Preset value is shared by reference. If a Preset instance is mutable and reused across multiple makeSubagent calls, state could leak between tools. Please verify Preset is immutable after construction.
Suggestion: If Preset is immutable, no change needed — a one-line note would confirm intent. If it is mutable, consider documenting that callers must not mutate presets after passing them.
site/src/content/docs/user-guide/sdk/tools/vended-tools.ts:303: Suggestion
Issue: In this example the local const subagent = makeSubagent({...}) shadows the imported subagent default tool. Since the custom import block only imports makeSubagent, this works, but naming the custom tool subagent (same as the exported default) in docs may confuse readers into thinking they must reuse that name.
Suggestion: Consider naming the local variable something role-specific (e.g. delegate) in the doc example to make clear it's an independent tool instance, matching the PR description's own delegate naming.
Documentation Preview FailedThe documentation deployment encountered an error. Please check the deployment logs for more details. |
poshinchen
left a comment
There was a problem hiding this comment.
Strandly v4-reason (optimized) review — run af1746, Opus 4.8, upgraded harness (read_file + suggestion-aware reward)
Assessment: approve
Assessment: Approve
This is a well-structured, thorough port that mirrors the Python behavior and ships with strong test coverage (metadata, validation, resolution, context modes, depth, interrupts, and real-agent integration) plus clear README/docs.
Review Categories
- Correctness: Logic reads cleanly; depth tracking, interrupt namespacing, and context forking are well tested with full-object assertions.
- Maintainability: The default builder reaches into
Agentprivate fields (_sandbox,_printer,_tracer) viaas unknowncasts, which is the main coupling concern. - Behavior nuances: Flagged two things to confirm — propagating
sandbox: falseto children, and collapsing a custom parent printer to a boolean. - API surface:
descriptionoption is implemented/documented but missing from the PR's public API summary.
None of the comments are blocking; nice attention to parity with Python and to edge-case testing.
| for (const clientName of spec.mcpServers) { | ||
| const client = mcpClients.get(clientName) | ||
| if (client) { | ||
| childTools.push(client) |
There was a problem hiding this comment.
Suggestion
Issue: The child's printer is collapsed to a boolean (printer: printer !== undefined). If the parent was constructed with a custom Printer instance, the child silently gets the default printer rather than the parent's configured one, which differs from how sandbox/trace attributes are passed through by reference.
Suggestion: Consider passing the parent's actual printer through when present (e.g. ...(printer !== undefined && { printer })) so custom printers propagate like the other inherited attributes — or document explicitly why only the on/off state is intended to be inherited.
| if (spec.mcpServers === undefined) { | ||
| childTools.push(...mcpClients.values()) | ||
| } else { | ||
| for (const clientName of spec.mcpServers) { |
There was a problem hiding this comment.
Important
Issue: _sandbox can legitimately be false (sandbox explicitly disabled), and the spread ...(sandbox !== undefined && { sandbox }) will forward sandbox: false to the child. If the Agent constructor treats false differently from "unset", a parent that explicitly disabled its sandbox would propagate that disabled state — which may or may not be intended.
Suggestion: Please confirm sandbox: false is the desired value to pass to children, and if so add a brief comment; otherwise narrow the condition to only forward truthy sandbox instances.
| childTools.push(...mcpClients.values()) | ||
| } else { | ||
| for (const clientName of spec.mcpServers) { | ||
| const client = mcpClients.get(clientName) |
There was a problem hiding this comment.
Suggestion
Issue: The private-field access via as unknown as { _sandbox?; _printer?; _tracer } reaches into Agent internals in three places. This couples the default builder tightly to the Agent class's private field names, so an internal rename would silently break propagation (the as unknown cast suppresses any compiler signal).
Suggestion: If Agent exposes (or could expose) public getters for sandbox, printer, and traceAttributes, prefer those. Otherwise, consider a single typed internals helper (as the test file already does) to centralize the coupling and make the dependency explicit.
| /** | ||
| * Tool description shown to the model. The available presets are appended to it. | ||
| * @defaultValue {@link DEFAULT_SUBAGENT_DESCRIPTION} | ||
| */ |
There was a problem hiding this comment.
Suggestion
Issue: MakeSubagentOptions exposes a public description option (and makeSubagent validates/uses it), but it isn't listed in the PR description's "Also exported" / public API summary even though the README documents it. Minor inconsistency in the surface being advertised for API review.
Suggestion: Add description to the PR's Public API Changes section so API reviewers evaluate the full exported option set.
| } | ||
| this._pending.delete(toolUseId) | ||
| if (result.stopReason === 'cancelled') { | ||
| return this._errorResult(toolUseId, 'Subagent was cancelled.') |
There was a problem hiding this comment.
Suggestion
Issue: _resolveContext parses last_messages for every mode, but a non-numeric string like 'bogus' passes typeof rawLast === 'string', goes through Math.trunc(Number('bogus')) → NaN, and is then discarded by the Number.isFinite guard. This is correct but relies on the reader tracing two conversions; the test last_messages: 'bogus' covers it well.
Suggestion: Optional: a brief comment noting that string inputs are coerced and invalid/<1 values fall back to "share all" would make the intent clearer at a glance.
|
Live Strandly's actual comments on the original PR 4971 (copied for comparison; mentions escaped and links de-linked so nothing is pinged or cross-referenced) Review (CHANGES_REQUESTED):
Review (APPROVED):
Review (APPROVED):
Inline at
Inline at
Inline at
Inline at
Inline at
|
Replica of
https://github.com/strands-agents/harness-sdk/pull/4971(URL in backticks to avoid a cross-reference on the original PR) for reviewer evaluation. Branches mirror the original PR's base and head commits.The review below was produced by the optimized Strandly reviewer (variant
v3-reason, optimization runaeb3ee, agent model Opus 4.8) during validation on PRs it never saw in training. Compare with the human and Strandly reviews on the original PR.