Implemented the next missing parity slice around SDK core types.

This commit is contained in:
Abdelrahman Abdallah
2026-04-20 00:27:41 +02:00
parent 9541d5d701
commit f770b794ae
3 changed files with 495 additions and 1 deletions
+1 -1
View File
@@ -181,7 +181,7 @@ Missing:
- [x] Release notes checking from `setup.ts` (local CHANGELOG.md, no network/cache layer) - [x] Release notes checking from `setup.ts` (local CHANGELOG.md, no network/cache layer)
- [ ] Full `entrypoints/cli.tsx` parity (version flag, feature flags, env setup, dynamic imports) - [ ] Full `entrypoints/cli.tsx` parity (version flag, feature flags, env setup, dynamic imports)
- [ ] Full `entrypoints/init.ts` parity (settings validation, OAuth, policy limits, telemetry, cleanup handlers) - [ ] Full `entrypoints/init.ts` parity (settings validation, OAuth, policy limits, telemetry, cleanup handlers)
- [ ] SDK entrypoint (`entrypoints/sdk/` — controlTypes, coreTypes, runtimeTypes, settingsTypes, toolTypes) - [ ] SDK entrypoint (`entrypoints/sdk/` — controlTypes, coreTypes, runtimeTypes, settingsTypes, toolTypes) — partial: HOOK_EVENTS, EXIT_REASONS, ModelUsage, ThinkingConfig, MCP server configs, JsonSchemaOutputFormat ported in `src/sdk_core_types.py`
- [x] Sandbox types/network config schema (`entrypoints/sandboxTypes.ts`) - [x] Sandbox types/network config schema (`entrypoints/sandboxTypes.ts`)
## 3. Prompt Assembly ## 3. Prompt Assembly
+319
View File
@@ -0,0 +1,319 @@
"""Foundational SDK types — Python port of entrypoints/sdk/coreTypes.ts.
Mirrors the public constants and a starter set of dataclasses for the
serializable types defined in `entrypoints/sdk/coreSchemas.ts`. The full
schema set (1800+ lines of Zod) is too large to port wholesale; this file
covers the foundational pieces other modules already reference and gives
later slices a place to extend.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Literal
# ---------------------------------------------------------------------------
# Const arrays (HOOK_EVENTS, EXIT_REASONS) — mirror coreTypes.ts top-level
# ---------------------------------------------------------------------------
HOOK_EVENTS: tuple[str, ...] = (
'PreToolUse',
'PostToolUse',
'PostToolUseFailure',
'Notification',
'UserPromptSubmit',
'SessionStart',
'SessionEnd',
'Stop',
'StopFailure',
'SubagentStart',
'SubagentStop',
'PreCompact',
'PostCompact',
'PermissionRequest',
'PermissionDenied',
'Setup',
'TeammateIdle',
'TaskCreated',
'TaskCompleted',
'Elicitation',
'ElicitationResult',
'ConfigChange',
'WorktreeCreate',
'WorktreeRemove',
'InstructionsLoaded',
'CwdChanged',
'FileChanged',
)
EXIT_REASONS: tuple[str, ...] = (
'clear',
'resume',
'logout',
'prompt_input_exit',
'other',
'bypass_permissions_disabled',
)
API_KEY_SOURCES: tuple[str, ...] = ('user', 'project', 'org', 'temporary', 'oauth')
CONFIG_SCOPES: tuple[str, ...] = ('local', 'user', 'project')
SDK_BETAS: tuple[str, ...] = ('context-1m-2025-08-07',)
ApiKeySource = Literal['user', 'project', 'org', 'temporary', 'oauth']
ConfigScope = Literal['local', 'user', 'project']
SdkBeta = Literal['context-1m-2025-08-07']
# ---------------------------------------------------------------------------
# Usage / model
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class ModelUsage:
input_tokens: int
output_tokens: int
cache_read_input_tokens: int
cache_creation_input_tokens: int
web_search_requests: int
cost_usd: float
context_window: int
max_output_tokens: int
@classmethod
def from_dict(cls, data: dict[str, Any]) -> 'ModelUsage':
return cls(
input_tokens=int(data['inputTokens']),
output_tokens=int(data['outputTokens']),
cache_read_input_tokens=int(data['cacheReadInputTokens']),
cache_creation_input_tokens=int(data['cacheCreationInputTokens']),
web_search_requests=int(data['webSearchRequests']),
cost_usd=float(data['costUSD']),
context_window=int(data['contextWindow']),
max_output_tokens=int(data['maxOutputTokens']),
)
def to_dict(self) -> dict[str, Any]:
return {
'inputTokens': self.input_tokens,
'outputTokens': self.output_tokens,
'cacheReadInputTokens': self.cache_read_input_tokens,
'cacheCreationInputTokens': self.cache_creation_input_tokens,
'webSearchRequests': self.web_search_requests,
'costUSD': self.cost_usd,
'contextWindow': self.context_window,
'maxOutputTokens': self.max_output_tokens,
}
# ---------------------------------------------------------------------------
# Thinking config
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class ThinkingAdaptive:
type: Literal['adaptive'] = 'adaptive'
def to_dict(self) -> dict[str, Any]:
return {'type': self.type}
@dataclass(frozen=True)
class ThinkingEnabled:
budget_tokens: int | None = None
type: Literal['enabled'] = 'enabled'
def to_dict(self) -> dict[str, Any]:
out: dict[str, Any] = {'type': self.type}
if self.budget_tokens is not None:
out['budgetTokens'] = self.budget_tokens
return out
@dataclass(frozen=True)
class ThinkingDisabled:
type: Literal['disabled'] = 'disabled'
def to_dict(self) -> dict[str, Any]:
return {'type': self.type}
ThinkingConfig = ThinkingAdaptive | ThinkingEnabled | ThinkingDisabled
def thinking_config_from_dict(data: dict[str, Any]) -> ThinkingConfig:
kind = data.get('type')
if kind == 'adaptive':
return ThinkingAdaptive()
if kind == 'enabled':
budget = data.get('budgetTokens')
return ThinkingEnabled(
budget_tokens=int(budget) if budget is not None else None,
)
if kind == 'disabled':
return ThinkingDisabled()
raise ValueError(f'unknown thinking config type: {kind!r}')
# ---------------------------------------------------------------------------
# MCP server configs
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class McpStdioServerConfig:
command: str
args: list[str] | None = None
env: dict[str, str] | None = None
type: Literal['stdio'] = 'stdio'
def to_dict(self) -> dict[str, Any]:
out: dict[str, Any] = {'type': self.type, 'command': self.command}
if self.args is not None:
out['args'] = list(self.args)
if self.env is not None:
out['env'] = dict(self.env)
return out
@dataclass(frozen=True)
class McpSSEServerConfig:
url: str
headers: dict[str, str] | None = None
type: Literal['sse'] = 'sse'
def to_dict(self) -> dict[str, Any]:
out: dict[str, Any] = {'type': self.type, 'url': self.url}
if self.headers is not None:
out['headers'] = dict(self.headers)
return out
@dataclass(frozen=True)
class McpHttpServerConfig:
url: str
headers: dict[str, str] | None = None
type: Literal['http'] = 'http'
def to_dict(self) -> dict[str, Any]:
out: dict[str, Any] = {'type': self.type, 'url': self.url}
if self.headers is not None:
out['headers'] = dict(self.headers)
return out
@dataclass(frozen=True)
class McpSdkServerConfig:
name: str
type: Literal['sdk'] = 'sdk'
def to_dict(self) -> dict[str, Any]:
return {'type': self.type, 'name': self.name}
@dataclass(frozen=True)
class McpClaudeAIProxyServerConfig:
url: str
id: str
type: Literal['claudeai-proxy'] = 'claudeai-proxy'
def to_dict(self) -> dict[str, Any]:
return {'type': self.type, 'url': self.url, 'id': self.id}
McpServerConfigForProcessTransport = (
McpStdioServerConfig
| McpSSEServerConfig
| McpHttpServerConfig
| McpSdkServerConfig
)
McpServerStatusConfig = (
McpServerConfigForProcessTransport | McpClaudeAIProxyServerConfig
)
def mcp_server_config_from_dict(data: dict[str, Any]) -> McpServerStatusConfig:
"""Discriminate by `type` and route to the matching dataclass."""
raw_type = data.get('type')
if raw_type is None or raw_type == 'stdio':
command = data.get('command')
if not isinstance(command, str):
raise ValueError('stdio server config requires a string command')
return McpStdioServerConfig(
command=command,
args=list(data['args']) if isinstance(data.get('args'), list) else None,
env=dict(data['env']) if isinstance(data.get('env'), dict) else None,
)
if raw_type == 'sse':
return McpSSEServerConfig(
url=str(data['url']),
headers=dict(data['headers']) if isinstance(data.get('headers'), dict) else None,
)
if raw_type == 'http':
return McpHttpServerConfig(
url=str(data['url']),
headers=dict(data['headers']) if isinstance(data.get('headers'), dict) else None,
)
if raw_type == 'sdk':
return McpSdkServerConfig(name=str(data['name']))
if raw_type == 'claudeai-proxy':
return McpClaudeAIProxyServerConfig(
url=str(data['url']),
id=str(data['id']),
)
raise ValueError(f'unknown MCP server config type: {raw_type!r}')
# ---------------------------------------------------------------------------
# Output format
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class JsonSchemaOutputFormat:
schema: dict[str, Any] = field(default_factory=dict)
type: Literal['json_schema'] = 'json_schema'
def to_dict(self) -> dict[str, Any]:
return {'type': self.type, 'schema': dict(self.schema)}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> 'JsonSchemaOutputFormat':
if data.get('type') != 'json_schema':
raise ValueError(
f'expected output format type "json_schema", got {data.get("type")!r}'
)
schema = data.get('schema')
if not isinstance(schema, dict):
raise ValueError('json_schema output format requires a "schema" mapping')
return cls(schema=dict(schema))
OutputFormat = JsonSchemaOutputFormat
__all__ = [
'HOOK_EVENTS',
'EXIT_REASONS',
'API_KEY_SOURCES',
'CONFIG_SCOPES',
'SDK_BETAS',
'ApiKeySource',
'ConfigScope',
'SdkBeta',
'ModelUsage',
'ThinkingAdaptive',
'ThinkingEnabled',
'ThinkingDisabled',
'ThinkingConfig',
'thinking_config_from_dict',
'McpStdioServerConfig',
'McpSSEServerConfig',
'McpHttpServerConfig',
'McpSdkServerConfig',
'McpClaudeAIProxyServerConfig',
'McpServerConfigForProcessTransport',
'McpServerStatusConfig',
'mcp_server_config_from_dict',
'JsonSchemaOutputFormat',
'OutputFormat',
]
+175
View File
@@ -0,0 +1,175 @@
"""Tests for SDK core types ported from entrypoints/sdk/coreTypes.ts."""
from __future__ import annotations
import unittest
from src.sdk_core_types import (
API_KEY_SOURCES,
CONFIG_SCOPES,
EXIT_REASONS,
HOOK_EVENTS,
SDK_BETAS,
JsonSchemaOutputFormat,
McpClaudeAIProxyServerConfig,
McpHttpServerConfig,
McpSdkServerConfig,
McpSSEServerConfig,
McpStdioServerConfig,
ModelUsage,
ThinkingAdaptive,
ThinkingDisabled,
ThinkingEnabled,
mcp_server_config_from_dict,
thinking_config_from_dict,
)
class HookEventsTest(unittest.TestCase):
def test_includes_known_events(self) -> None:
for required in (
'PreToolUse',
'PostToolUse',
'UserPromptSubmit',
'SessionStart',
'PreCompact',
'WorktreeCreate',
'CwdChanged',
'FileChanged',
):
self.assertIn(required, HOOK_EVENTS)
def test_no_duplicates(self) -> None:
self.assertEqual(len(HOOK_EVENTS), len(set(HOOK_EVENTS)))
class ExitReasonsTest(unittest.TestCase):
def test_known_exit_reasons(self) -> None:
for required in (
'clear', 'resume', 'logout', 'prompt_input_exit',
'other', 'bypass_permissions_disabled',
):
self.assertIn(required, EXIT_REASONS)
class EnumLiteralsTest(unittest.TestCase):
def test_api_key_sources(self) -> None:
self.assertEqual(set(API_KEY_SOURCES), {'user', 'project', 'org', 'temporary', 'oauth'})
def test_config_scopes(self) -> None:
self.assertEqual(set(CONFIG_SCOPES), {'local', 'user', 'project'})
def test_sdk_betas_known(self) -> None:
self.assertIn('context-1m-2025-08-07', SDK_BETAS)
class ModelUsageTest(unittest.TestCase):
def test_round_trip(self) -> None:
raw = {
'inputTokens': 100, 'outputTokens': 50,
'cacheReadInputTokens': 10, 'cacheCreationInputTokens': 5,
'webSearchRequests': 0, 'costUSD': 0.001,
'contextWindow': 200000, 'maxOutputTokens': 8192,
}
usage = ModelUsage.from_dict(raw)
self.assertEqual(usage.input_tokens, 100)
self.assertEqual(usage.cost_usd, 0.001)
self.assertEqual(usage.to_dict(), raw)
class ThinkingConfigTest(unittest.TestCase):
def test_adaptive(self) -> None:
cfg = thinking_config_from_dict({'type': 'adaptive'})
self.assertIsInstance(cfg, ThinkingAdaptive)
self.assertEqual(cfg.to_dict(), {'type': 'adaptive'})
def test_enabled_with_budget(self) -> None:
cfg = thinking_config_from_dict({'type': 'enabled', 'budgetTokens': 1024})
self.assertIsInstance(cfg, ThinkingEnabled)
self.assertEqual(cfg.to_dict(), {'type': 'enabled', 'budgetTokens': 1024})
def test_enabled_without_budget(self) -> None:
cfg = thinking_config_from_dict({'type': 'enabled'})
self.assertEqual(cfg.to_dict(), {'type': 'enabled'})
def test_disabled(self) -> None:
cfg = thinking_config_from_dict({'type': 'disabled'})
self.assertIsInstance(cfg, ThinkingDisabled)
def test_unknown_type_raises(self) -> None:
with self.assertRaises(ValueError):
thinking_config_from_dict({'type': 'something-else'})
class McpServerConfigTest(unittest.TestCase):
def test_stdio_default_type(self) -> None:
cfg = mcp_server_config_from_dict({'command': 'npx'})
self.assertIsInstance(cfg, McpStdioServerConfig)
self.assertEqual(cfg.command, 'npx')
def test_stdio_with_args_env(self) -> None:
cfg = mcp_server_config_from_dict({
'type': 'stdio',
'command': 'node',
'args': ['./mcp.js'],
'env': {'TOKEN': 'xyz'},
})
out = cfg.to_dict()
self.assertEqual(out['command'], 'node')
self.assertEqual(out['args'], ['./mcp.js'])
self.assertEqual(out['env'], {'TOKEN': 'xyz'})
def test_sse(self) -> None:
cfg = mcp_server_config_from_dict({'type': 'sse', 'url': 'https://x.example'})
self.assertIsInstance(cfg, McpSSEServerConfig)
def test_http_with_headers(self) -> None:
cfg = mcp_server_config_from_dict({
'type': 'http',
'url': 'https://x.example',
'headers': {'Auth': 'Bearer 1'},
})
self.assertIsInstance(cfg, McpHttpServerConfig)
self.assertEqual(cfg.headers, {'Auth': 'Bearer 1'})
def test_sdk(self) -> None:
cfg = mcp_server_config_from_dict({'type': 'sdk', 'name': 'my-sdk'})
self.assertIsInstance(cfg, McpSdkServerConfig)
def test_claudeai_proxy(self) -> None:
cfg = mcp_server_config_from_dict({
'type': 'claudeai-proxy',
'url': 'https://claude.ai/p',
'id': 'abc',
})
self.assertIsInstance(cfg, McpClaudeAIProxyServerConfig)
def test_unknown_type_raises(self) -> None:
with self.assertRaises(ValueError):
mcp_server_config_from_dict({'type': 'mystery'})
def test_stdio_requires_command(self) -> None:
with self.assertRaises(ValueError):
mcp_server_config_from_dict({'type': 'stdio'})
class JsonSchemaOutputFormatTest(unittest.TestCase):
def test_round_trip(self) -> None:
fmt = JsonSchemaOutputFormat.from_dict({
'type': 'json_schema',
'schema': {'type': 'object', 'properties': {}},
})
self.assertEqual(fmt.schema['type'], 'object')
self.assertEqual(fmt.to_dict()['type'], 'json_schema')
def test_rejects_wrong_type(self) -> None:
with self.assertRaises(ValueError):
JsonSchemaOutputFormat.from_dict({'type': 'text', 'schema': {}})
def test_requires_schema_mapping(self) -> None:
with self.assertRaises(ValueError):
JsonSchemaOutputFormat.from_dict({'type': 'json_schema'})
if __name__ == '__main__':
unittest.main()