mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-29 12:11:09 +00:00
* Codex worktree snapshot: startup-cleanup Co-authored-by: Codex * Add Python SDK real smoke test Adds a repository-only real E2E smoke script for the Python SDK, plus npm and developer documentation entry points. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): address review findings — bugs, type safety, and test coverage - Fix prepare_spawn_info: JS files now use "node" instead of sys.executable - Fix protocol.py: correct total=False misuse on 7 TypedDicts (required fields were optional) - Fix query.py: add _closed guard in _ensure_started, suppress exceptions in close() - Fix sync_query.py: prevent close() deadlock, add context manager, add timeouts - Fix transport.py: handle malformed JSON lines, add _closed guard in start() - Fix validation.py: use uuid.RFC_4122 instead of magic UUID - Fix __init__.py: export TextBlock, widen query_sync signature - Remove dead code: ensure_not_aborted, write_json_line, _thread_error - Add 12 new tests (29 → 41): context managers, JSON skip, closed guards, spawn info, timeouts Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): address wenshao review — session_id, bool validation, debug stderr - Fix continue_session=True generating a wrong random session_id - Add _as_optional_bool helper for strict type validation on bool fields - Default debug stderr to sys.stderr when no custom callback is provided Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): address remaining wenshao review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): harden settings dialog restart prompt test Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): review fixes — UUID compat, stderr fallback, sync cleanup - Remove UUID version restriction to support v6/v7/v8 (RFC 9562) - Always write to sys.stderr when stderr callback raises (was silent when debug=False) - Prevent duplicate _STOP sentinel in SyncQuery.close() via _stop_sent flag - Add ruff format --check to CI workflow - Fix smoke_real.py version guard: fail early before imports instead of NameError - Apply ruff format to existing files Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): remaining review fixes — exit_code attr, guard strictness, sync timeout - Add exit_code attribute to ProcessExitError for programmatic access - Strengthen is_control_response/is_control_cancel guards to require payload fields, preventing misrouting of malformed messages - Expose control_request_timeout property on Query so SyncQuery uses the configured timeout instead of a hardcoded 30s default - Use dataclasses.replace() instead of direct mutation on frozen-style QueryOptions in query() factory - Add ResourceWarning in SyncQuery.__del__ when not properly closed Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): add exit_code default and guard __del__ against partial GC - Give ProcessExitError.exit_code a default value (-1) so user code can construct the exception with just a message string - Wrap SyncQuery.__del__ in try/except AttributeError to prevent crashes when the object is partially garbage-collected Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): review fixes — resource leak, type safety, CI matrix, docs - Fix SyncQuery.__del__ to call close() on GC instead of only warning - Replace hasattr duck-type check with isinstance(prompt, AsyncIterable) - Type-validate permission_mode/auth_type in QueryOptions.from_mapping - Use TypeGuard return types on all is_sdk_*/is_control_* predicates - Add 5s margin to sync wrapper timeouts to prevent error type masking - Expand CI matrix to test Python 3.10, 3.11, 3.12 - Change ProcessExitError.exit_code default from -1 to None - Add stderr to docs QueryOptions listing - Update README sync example to use context manager pattern Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): preserve iterator exhaustion state and suppress detached task warning - Add _exhausted flag to Query.__anext__ and SyncQuery.__next__ so repeated iteration after end-of-stream raises Stop(Async)Iteration instead of blocking forever. - Remove re-raise in _initialize() to prevent asyncio "Task exception was never retrieved" warning on detached tasks; the error is already surfaced via _finish_with_error(). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): reject mcp_servers at validation time and add iterator/init tests - Reject mcp_servers in validate_query_options() with a clear error instead of advertising MCP support to the CLI and then failing at runtime when mcp_message arrives. - Remove dead mcp_servers branch from _initialize(). - Add tests for async/sync iterator exhaustion, detached init task warning suppression, and mcp_servers validation. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): fix ruff lint errors in new tests - Use ControlRequestTimeoutError instead of bare Exception (B017) - Fix import sorting for stdlib vs third-party (I001) - Break long line to stay within 88-char limit (E501) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * style(sdk-python): apply ruff format to new tests Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
202be6ec7d
commit
e384338145
25 changed files with 4676 additions and 14 deletions
357
packages/sdk-python/src/qwen_code_sdk/protocol.py
Normal file
357
packages/sdk-python/src/qwen_code_sdk/protocol.py
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
"""Protocol message types and helpers for qwen stream-json."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal, TypeAlias, TypeGuard
|
||||
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
from .types import PermissionMode, PermissionSuggestion
|
||||
|
||||
|
||||
class Annotation(TypedDict):
|
||||
type: str
|
||||
value: str
|
||||
|
||||
|
||||
class Usage(TypedDict):
|
||||
input_tokens: int
|
||||
output_tokens: int
|
||||
cache_creation_input_tokens: NotRequired[int]
|
||||
cache_read_input_tokens: NotRequired[int]
|
||||
total_tokens: NotRequired[int]
|
||||
|
||||
|
||||
class ExtendedUsage(Usage, total=False):
|
||||
server_tool_use: dict[str, int]
|
||||
service_tier: str
|
||||
cache_creation: dict[str, int]
|
||||
|
||||
|
||||
class CLIPermissionDenial(TypedDict):
|
||||
tool_name: str
|
||||
tool_use_id: str
|
||||
tool_input: Any
|
||||
|
||||
|
||||
class TextBlock(TypedDict):
|
||||
type: Literal["text"]
|
||||
text: str
|
||||
annotations: NotRequired[list[Annotation]]
|
||||
|
||||
|
||||
class ThinkingBlock(TypedDict):
|
||||
type: Literal["thinking"]
|
||||
thinking: str
|
||||
signature: NotRequired[str]
|
||||
annotations: NotRequired[list[Annotation]]
|
||||
|
||||
|
||||
class ToolUseBlock(TypedDict):
|
||||
type: Literal["tool_use"]
|
||||
id: str
|
||||
name: str
|
||||
input: Any
|
||||
annotations: NotRequired[list[Annotation]]
|
||||
|
||||
|
||||
class ToolResultBlock(TypedDict):
|
||||
type: Literal["tool_result"]
|
||||
tool_use_id: str
|
||||
content: NotRequired[str | list[ContentBlock]]
|
||||
is_error: NotRequired[bool]
|
||||
annotations: NotRequired[list[Annotation]]
|
||||
|
||||
|
||||
ContentBlock: TypeAlias = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock
|
||||
|
||||
|
||||
class APIUserMessage(TypedDict):
|
||||
role: Literal["user"]
|
||||
content: str | list[ContentBlock]
|
||||
|
||||
|
||||
class APIAssistantMessage(TypedDict):
|
||||
role: Literal["assistant"]
|
||||
content: list[ContentBlock]
|
||||
id: NotRequired[str]
|
||||
type: NotRequired[Literal["message"]]
|
||||
model: NotRequired[str]
|
||||
stop_reason: NotRequired[str | None]
|
||||
usage: NotRequired[Usage]
|
||||
|
||||
|
||||
class SDKUserMessage(TypedDict):
|
||||
type: Literal["user"]
|
||||
session_id: str
|
||||
message: APIUserMessage
|
||||
parent_tool_use_id: str | None
|
||||
uuid: NotRequired[str]
|
||||
options: NotRequired[dict[str, Any]]
|
||||
|
||||
|
||||
class SDKAssistantMessage(TypedDict):
|
||||
type: Literal["assistant"]
|
||||
uuid: str
|
||||
session_id: str
|
||||
message: APIAssistantMessage
|
||||
parent_tool_use_id: str | None
|
||||
|
||||
|
||||
class MCPServerState(TypedDict):
|
||||
name: str
|
||||
status: str
|
||||
|
||||
|
||||
class SDKSystemMessage(TypedDict):
|
||||
type: Literal["system"]
|
||||
subtype: str
|
||||
uuid: str
|
||||
session_id: str
|
||||
data: NotRequired[Any]
|
||||
cwd: NotRequired[str]
|
||||
tools: NotRequired[list[str]]
|
||||
mcp_servers: NotRequired[list[MCPServerState]]
|
||||
model: NotRequired[str]
|
||||
permission_mode: NotRequired[str]
|
||||
slash_commands: NotRequired[list[str]]
|
||||
qwen_code_version: NotRequired[str]
|
||||
output_style: NotRequired[str]
|
||||
agents: NotRequired[list[str]]
|
||||
skills: NotRequired[list[str]]
|
||||
capabilities: NotRequired[dict[str, Any]]
|
||||
|
||||
|
||||
class SDKResultMessageSuccess(TypedDict):
|
||||
type: Literal["result"]
|
||||
subtype: Literal["success"]
|
||||
uuid: str
|
||||
session_id: str
|
||||
is_error: Literal[False]
|
||||
duration_ms: int
|
||||
duration_api_ms: int
|
||||
num_turns: int
|
||||
result: str
|
||||
usage: ExtendedUsage
|
||||
permission_denials: list[CLIPermissionDenial]
|
||||
|
||||
|
||||
class ResultErrorObject(TypedDict):
|
||||
message: str
|
||||
type: NotRequired[str]
|
||||
|
||||
|
||||
class SDKResultMessageError(TypedDict):
|
||||
type: Literal["result"]
|
||||
subtype: Literal["error_max_turns", "error_during_execution"]
|
||||
uuid: str
|
||||
session_id: str
|
||||
is_error: Literal[True]
|
||||
duration_ms: int
|
||||
duration_api_ms: int
|
||||
num_turns: int
|
||||
usage: ExtendedUsage
|
||||
permission_denials: list[CLIPermissionDenial]
|
||||
error: NotRequired[ResultErrorObject]
|
||||
|
||||
|
||||
SDKResultMessage: TypeAlias = SDKResultMessageSuccess | SDKResultMessageError
|
||||
|
||||
|
||||
class MessageStartStreamEvent(TypedDict):
|
||||
type: Literal["message_start"]
|
||||
message: dict[str, Any]
|
||||
|
||||
|
||||
class ContentBlockStartEvent(TypedDict):
|
||||
type: Literal["content_block_start"]
|
||||
index: int
|
||||
content_block: ContentBlock
|
||||
|
||||
|
||||
class ContentBlockDeltaEvent(TypedDict):
|
||||
type: Literal["content_block_delta"]
|
||||
index: int
|
||||
delta: dict[str, Any]
|
||||
|
||||
|
||||
class ContentBlockStopEvent(TypedDict):
|
||||
type: Literal["content_block_stop"]
|
||||
index: int
|
||||
|
||||
|
||||
class MessageStopStreamEvent(TypedDict):
|
||||
type: Literal["message_stop"]
|
||||
|
||||
|
||||
StreamEvent: TypeAlias = (
|
||||
MessageStartStreamEvent
|
||||
| ContentBlockStartEvent
|
||||
| ContentBlockDeltaEvent
|
||||
| ContentBlockStopEvent
|
||||
| MessageStopStreamEvent
|
||||
)
|
||||
|
||||
|
||||
class SDKPartialAssistantMessage(TypedDict):
|
||||
type: Literal["stream_event"]
|
||||
uuid: str
|
||||
session_id: str
|
||||
event: StreamEvent
|
||||
parent_tool_use_id: str | None
|
||||
|
||||
|
||||
class CLIControlInterruptRequest(TypedDict):
|
||||
subtype: Literal["interrupt"]
|
||||
|
||||
|
||||
class CLIControlPermissionRequest(TypedDict):
|
||||
subtype: Literal["can_use_tool"]
|
||||
tool_name: str
|
||||
tool_use_id: str
|
||||
input: Any
|
||||
permission_suggestions: list[PermissionSuggestion] | None
|
||||
blocked_path: str | None
|
||||
|
||||
|
||||
class CLIControlInitializeRequest(TypedDict):
|
||||
subtype: Literal["initialize"]
|
||||
hooks: NotRequired[Any]
|
||||
mcpServers: NotRequired[dict[str, dict[str, Any]]]
|
||||
|
||||
|
||||
class CLIControlSetPermissionModeRequest(TypedDict):
|
||||
subtype: Literal["set_permission_mode"]
|
||||
mode: PermissionMode
|
||||
|
||||
|
||||
class CLIControlSetModelRequest(TypedDict):
|
||||
subtype: Literal["set_model"]
|
||||
model: str
|
||||
|
||||
|
||||
class CLIControlMcpStatusRequest(TypedDict):
|
||||
subtype: Literal["mcp_server_status"]
|
||||
|
||||
|
||||
class CLIControlSupportedCommandsRequest(TypedDict):
|
||||
subtype: Literal["supported_commands"]
|
||||
|
||||
|
||||
ControlRequestPayload: TypeAlias = (
|
||||
CLIControlInterruptRequest
|
||||
| CLIControlPermissionRequest
|
||||
| CLIControlInitializeRequest
|
||||
| CLIControlSetPermissionModeRequest
|
||||
| CLIControlSetModelRequest
|
||||
| CLIControlMcpStatusRequest
|
||||
| CLIControlSupportedCommandsRequest
|
||||
| dict[str, Any]
|
||||
)
|
||||
|
||||
|
||||
class CLIControlRequest(TypedDict):
|
||||
type: Literal["control_request"]
|
||||
request_id: str
|
||||
request: ControlRequestPayload
|
||||
|
||||
|
||||
class ControlResponseSuccess(TypedDict):
|
||||
subtype: Literal["success"]
|
||||
request_id: str
|
||||
response: Any
|
||||
|
||||
|
||||
class ControlResponseError(TypedDict):
|
||||
subtype: Literal["error"]
|
||||
request_id: str
|
||||
error: str | dict[str, Any]
|
||||
|
||||
|
||||
class CLIControlResponse(TypedDict):
|
||||
type: Literal["control_response"]
|
||||
response: ControlResponseSuccess | ControlResponseError
|
||||
|
||||
|
||||
class ControlCancelRequest(TypedDict):
|
||||
type: Literal["control_cancel_request"]
|
||||
request_id: NotRequired[str]
|
||||
|
||||
|
||||
SDKMessage: TypeAlias = (
|
||||
SDKUserMessage
|
||||
| SDKAssistantMessage
|
||||
| SDKSystemMessage
|
||||
| SDKResultMessage
|
||||
| SDKPartialAssistantMessage
|
||||
)
|
||||
|
||||
|
||||
ControlMessage: TypeAlias = (
|
||||
CLIControlRequest | CLIControlResponse | ControlCancelRequest
|
||||
)
|
||||
|
||||
|
||||
def is_sdk_user_message(msg: Any) -> TypeGuard[SDKUserMessage]:
|
||||
return isinstance(msg, dict) and msg.get("type") == "user" and "message" in msg
|
||||
|
||||
|
||||
def is_sdk_assistant_message(msg: Any) -> TypeGuard[SDKAssistantMessage]:
|
||||
return (
|
||||
isinstance(msg, dict)
|
||||
and msg.get("type") == "assistant"
|
||||
and "session_id" in msg
|
||||
and "message" in msg
|
||||
)
|
||||
|
||||
|
||||
def is_sdk_system_message(msg: Any) -> TypeGuard[SDKSystemMessage]:
|
||||
return (
|
||||
isinstance(msg, dict)
|
||||
and msg.get("type") == "system"
|
||||
and "subtype" in msg
|
||||
and "session_id" in msg
|
||||
)
|
||||
|
||||
|
||||
def is_sdk_result_message(msg: Any) -> TypeGuard[SDKResultMessage]:
|
||||
return (
|
||||
isinstance(msg, dict)
|
||||
and msg.get("type") == "result"
|
||||
and "subtype" in msg
|
||||
and "session_id" in msg
|
||||
)
|
||||
|
||||
|
||||
def is_sdk_partial_assistant_message(msg: Any) -> TypeGuard[SDKPartialAssistantMessage]:
|
||||
return (
|
||||
isinstance(msg, dict)
|
||||
and msg.get("type") == "stream_event"
|
||||
and "session_id" in msg
|
||||
and "event" in msg
|
||||
)
|
||||
|
||||
|
||||
def is_control_request(msg: Any) -> TypeGuard[CLIControlRequest]:
|
||||
return (
|
||||
isinstance(msg, dict)
|
||||
and msg.get("type") == "control_request"
|
||||
and "request_id" in msg
|
||||
and "request" in msg
|
||||
)
|
||||
|
||||
|
||||
def is_control_response(msg: Any) -> TypeGuard[CLIControlResponse]:
|
||||
return (
|
||||
isinstance(msg, dict)
|
||||
and msg.get("type") == "control_response"
|
||||
and "response" in msg
|
||||
)
|
||||
|
||||
|
||||
def is_control_cancel(msg: Any) -> TypeGuard[ControlCancelRequest]:
|
||||
return (
|
||||
isinstance(msg, dict)
|
||||
and msg.get("type") == "control_cancel_request"
|
||||
and "request_id" in msg
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue