Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/sdk-python/src/qwen_code_sdk/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ async def _ensure_started(self) -> None:
async def _initialize(self) -> None:
try:
payload: dict[str, Any] = {"hooks": None}
if self._options.agents:
payload["agents"] = self._options.agents
await self._send_control_request("initialize", payload)
except Exception as exc:
await self._finish_with_error(exc)
Expand Down
3 changes: 3 additions & 0 deletions packages/sdk-python/src/qwen_code_sdk/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ def build_cli_arguments(options: QueryOptions) -> list[str]:
if options.max_session_turns is not None:
args.extend(["--max-session-turns", str(options.max_session_turns)])

if options.max_subagent_depth is not None:
args.extend(["--max-subagent-depth", str(options.max_subagent_depth)])

if options.core_tools:
args.extend(["--core-tools", ",".join(options.core_tools)])

Expand Down
45 changes: 45 additions & 0 deletions packages/sdk-python/src/qwen_code_sdk/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,29 @@
]


class RunConfig(TypedDict, total=False):
"""Run configuration for a sub-agent."""

max_time_minutes: int
max_turns: int


class SubagentConfig(TypedDict, total=False):
"""Configuration for a sub-agent.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] SubagentConfig and RunConfig TypedDicts are defined here but never referenced — QueryOptions.agents is typed as list[dict[str, Any]] (lines 140 and 167), so these TypedDicts provide zero type-safety benefit. Callers get no IDE autocomplete or static checking.

Either wire them in:

agents: list[SubagentConfig] | None = None

Or remove them and keep list[dict[str, Any]], consistent with how mcp_servers is typed as dict[str, dict[str, Any]].

— qwen3.7-max via Qwen Code /review


Required fields: name, description, systemPrompt.
Field names match the CLI wire format (camelCase).
"""

name: str
description: str
systemPrompt: str
tools: list[str]
model: str
runConfig: RunConfig
color: str


class PermissionSuggestion(TypedDict):
type: Literal["allow", "deny", "modify"]
label: str
Expand Down Expand Up @@ -114,6 +137,8 @@ class QueryOptionsDict(TypedDict, total=False):
timeout: TimeoutOptionsDict
mcp_servers: dict[str, dict[str, Any]]
stderr: Callable[[str], None]
agents: list[dict[str, Any]]
max_subagent_depth: int


@dataclass
Expand All @@ -139,6 +164,8 @@ class QueryOptions:
timeout: TimeoutOptions = TimeoutOptions()
mcp_servers: dict[str, dict[str, Any]] | None = None
stderr: Callable[[str], None] | None = None
agents: list[dict[str, Any]] | None = None
max_subagent_depth: int | None = None

@classmethod
def from_mapping(cls, value: Mapping[str, Any] | None) -> QueryOptions:
Expand Down Expand Up @@ -183,6 +210,8 @@ def from_mapping(cls, value: Mapping[str, Any] | None) -> QueryOptions:
Callable[[str], None] | None,
_as_optional_callable(data, "stderr"),
),
agents=_as_optional_list_of_dicts(data, "agents"),
max_subagent_depth=_as_optional_int(data, "max_subagent_depth"),
)


Expand Down Expand Up @@ -321,3 +350,19 @@ def _as_optional_nested_dict(
raise TypeError(f"{key} must be a mapping of string to mapping")
parsed[k] = dict(v)
return parsed


def _as_optional_list_of_dicts(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No tests added for the new code in this PR. _as_optional_list_of_dicts has 4 branches (None, not-list, non-mapping items, valid) with zero coverage. The validation blocks for max_subagent_depth (bool exclusion, range 1–100) and agents (required-field checks) in validation.py are also untested, as are the --max-subagent-depth CLI arg in transport.py and the agents payload in query.py.

Consider adding tests covering:

  • _as_optional_list_of_dicts — valid list, non-list input, list with non-mapping items
  • validate_query_optionsmax_subagent_depth boundaries (0, 1, 100, 101), bool rejection, agents missing required fields
  • build_cli_arguments--max-subagent-depth flag emission
  • _initializeagents included in control request payload when set

— qwen3.7-max via Qwen Code /review

data: Mapping[str, Any], key: str
) -> list[dict[str, Any]] | None:
raw = data.get(key)
if raw is None:
return None
if not isinstance(raw, list):
raise TypeError(f"{key} must be a list of mappings")
parsed: list[dict[str, Any]] = []
for item in raw:
if not isinstance(item, Mapping):
raise TypeError(f"{key} must be a list of mappings")
parsed.append(dict(item))
return parsed
22 changes: 22 additions & 0 deletions packages/sdk-python/src/qwen_code_sdk/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,28 @@ def validate_query_options(options: QueryOptions) -> None:
"Remove the mcp_servers option or use the TypeScript SDK."
)

if options.max_subagent_depth is not None:
if not isinstance(options.max_subagent_depth, int) or isinstance(
options.max_subagent_depth, bool
):
raise ValidationError("max_subagent_depth must be an integer")
if options.max_subagent_depth < 1 or options.max_subagent_depth > 100:
raise ValidationError(
"max_subagent_depth must be between 1 and 100"
)

if options.agents:
for i, agent in enumerate(options.agents):
if not isinstance(agent, dict):
raise ValidationError(
f"agents[{i}] must be a mapping"
)
for required_field in ("name", "description", "systemPrompt"):
if not agent.get(required_field):
raise ValidationError(
f"agents[{i}] must have a non-empty '{required_field}'"
)


def _validate_optional_callable(
value: object,
Expand Down
1 change: 1 addition & 0 deletions packages/sdk-typescript/src/query/createQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export function query({
stderr: options.stderr,
logLevel: options.logLevel,
maxSessionTurns: options.maxSessionTurns,
maxSubagentDepth: options.maxSubagentDepth,
coreTools: options.coreTools,
excludeTools: options.excludeTools,
allowedTools: options.allowedTools,
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk-typescript/src/transport/ProcessTransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,10 @@ export class ProcessTransport implements Transport {
args.push('--max-session-turns', String(this.options.maxSessionTurns));
}

if (this.options.maxSubagentDepth !== undefined) {
args.push('--max-subagent-depth', String(this.options.maxSubagentDepth));
}

if (this.options.coreTools && this.options.coreTools.length > 0) {
args.push('--core-tools', this.options.coreTools.join(','));
}
Expand Down
1 change: 1 addition & 0 deletions packages/sdk-typescript/src/types/queryOptionsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,5 +182,6 @@ export const QueryOptionsSchema = z
resume: z.string().optional(),
sessionId: z.string().optional(),
timeout: TimeoutConfigSchema.optional(),
maxSubagentDepth: z.number().int().min(1).max(100).optional(),
})
.strict();
14 changes: 14 additions & 0 deletions packages/sdk-typescript/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ export type TransportOptions = {
* When resume is provided, this should match the resume ID.
*/
sessionId?: string;
/**
* Maximum sub-agent nesting depth (1-based).
* 1 keeps sub-agents available but disables nesting; capped at 100.
* @default 5 (CLI default)
*/
maxSubagentDepth?: number;
};

export interface QuerySystemPromptPreset {
Expand Down Expand Up @@ -503,4 +509,12 @@ export interface QueryOptions {
*/
streamClose?: number;
};

/**
* Maximum sub-agent nesting depth (1-based).
* 1 keeps sub-agents available but disables nesting; capped at 100.
* Equivalent to CLI's `--max-subagent-depth` flag.
* @default 5 (CLI default)
*/
maxSubagentDepth?: number;
}