yee88 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yee88/__init__.py +1 -0
- yee88/api.py +116 -0
- yee88/backends.py +25 -0
- yee88/backends_helpers.py +14 -0
- yee88/cli/__init__.py +228 -0
- yee88/cli/config.py +320 -0
- yee88/cli/doctor.py +173 -0
- yee88/cli/init.py +113 -0
- yee88/cli/onboarding_cmd.py +126 -0
- yee88/cli/plugins.py +196 -0
- yee88/cli/run.py +419 -0
- yee88/cli/topic.py +355 -0
- yee88/commands.py +134 -0
- yee88/config.py +142 -0
- yee88/config_migrations.py +124 -0
- yee88/config_watch.py +146 -0
- yee88/context.py +9 -0
- yee88/directives.py +146 -0
- yee88/engines.py +53 -0
- yee88/events.py +170 -0
- yee88/ids.py +17 -0
- yee88/lockfile.py +158 -0
- yee88/logging.py +283 -0
- yee88/markdown.py +298 -0
- yee88/model.py +77 -0
- yee88/plugins.py +312 -0
- yee88/presenter.py +25 -0
- yee88/progress.py +99 -0
- yee88/router.py +113 -0
- yee88/runner.py +712 -0
- yee88/runner_bridge.py +619 -0
- yee88/runners/__init__.py +1 -0
- yee88/runners/claude.py +483 -0
- yee88/runners/codex.py +656 -0
- yee88/runners/mock.py +221 -0
- yee88/runners/opencode.py +505 -0
- yee88/runners/pi.py +523 -0
- yee88/runners/run_options.py +39 -0
- yee88/runners/tool_actions.py +90 -0
- yee88/runtime_loader.py +207 -0
- yee88/scheduler.py +159 -0
- yee88/schemas/__init__.py +1 -0
- yee88/schemas/claude.py +238 -0
- yee88/schemas/codex.py +169 -0
- yee88/schemas/opencode.py +51 -0
- yee88/schemas/pi.py +117 -0
- yee88/settings.py +360 -0
- yee88/telegram/__init__.py +20 -0
- yee88/telegram/api_models.py +37 -0
- yee88/telegram/api_schemas.py +152 -0
- yee88/telegram/backend.py +163 -0
- yee88/telegram/bridge.py +425 -0
- yee88/telegram/chat_prefs.py +242 -0
- yee88/telegram/chat_sessions.py +112 -0
- yee88/telegram/client.py +409 -0
- yee88/telegram/client_api.py +539 -0
- yee88/telegram/commands/__init__.py +12 -0
- yee88/telegram/commands/agent.py +196 -0
- yee88/telegram/commands/cancel.py +116 -0
- yee88/telegram/commands/dispatch.py +111 -0
- yee88/telegram/commands/executor.py +449 -0
- yee88/telegram/commands/file_transfer.py +586 -0
- yee88/telegram/commands/handlers.py +45 -0
- yee88/telegram/commands/media.py +143 -0
- yee88/telegram/commands/menu.py +139 -0
- yee88/telegram/commands/model.py +215 -0
- yee88/telegram/commands/overrides.py +159 -0
- yee88/telegram/commands/parse.py +30 -0
- yee88/telegram/commands/plan.py +16 -0
- yee88/telegram/commands/reasoning.py +234 -0
- yee88/telegram/commands/reply.py +23 -0
- yee88/telegram/commands/topics.py +332 -0
- yee88/telegram/commands/trigger.py +143 -0
- yee88/telegram/context.py +140 -0
- yee88/telegram/engine_defaults.py +86 -0
- yee88/telegram/engine_overrides.py +105 -0
- yee88/telegram/files.py +178 -0
- yee88/telegram/loop.py +1822 -0
- yee88/telegram/onboarding.py +1088 -0
- yee88/telegram/outbox.py +177 -0
- yee88/telegram/parsing.py +239 -0
- yee88/telegram/render.py +198 -0
- yee88/telegram/state_store.py +88 -0
- yee88/telegram/topic_state.py +334 -0
- yee88/telegram/topics.py +256 -0
- yee88/telegram/trigger_mode.py +68 -0
- yee88/telegram/types.py +63 -0
- yee88/telegram/voice.py +110 -0
- yee88/transport.py +53 -0
- yee88/transport_runtime.py +323 -0
- yee88/transports.py +76 -0
- yee88/utils/__init__.py +1 -0
- yee88/utils/git.py +87 -0
- yee88/utils/json_state.py +21 -0
- yee88/utils/paths.py +47 -0
- yee88/utils/streams.py +44 -0
- yee88/utils/subprocess.py +86 -0
- yee88/worktrees.py +135 -0
- yee88-0.3.0.dist-info/METADATA +116 -0
- yee88-0.3.0.dist-info/RECORD +103 -0
- yee88-0.3.0.dist-info/WHEEL +4 -0
- yee88-0.3.0.dist-info/entry_points.txt +11 -0
- yee88-0.3.0.dist-info/licenses/LICENSE +21 -0
yee88/runtime_loader.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
from collections.abc import Iterable, Mapping
|
|
8
|
+
|
|
9
|
+
from .backends import EngineBackend
|
|
10
|
+
from .config import ConfigError, ProjectsConfig
|
|
11
|
+
from .engines import get_backend, list_backend_ids
|
|
12
|
+
from .ids import RESERVED_CHAT_COMMANDS
|
|
13
|
+
from .logging import get_logger
|
|
14
|
+
from .router import AutoRouter, EngineStatus, RunnerEntry
|
|
15
|
+
from .settings import TakopiSettings
|
|
16
|
+
from .transport_runtime import TransportRuntime
|
|
17
|
+
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class RuntimeSpec:
|
|
23
|
+
router: AutoRouter
|
|
24
|
+
projects: ProjectsConfig
|
|
25
|
+
allowlist: list[str] | None
|
|
26
|
+
plugin_configs: Mapping[str, Any] | None
|
|
27
|
+
watch_config: bool = False
|
|
28
|
+
|
|
29
|
+
def to_runtime(self, *, config_path: Path | None) -> TransportRuntime:
|
|
30
|
+
return TransportRuntime(
|
|
31
|
+
router=self.router,
|
|
32
|
+
projects=self.projects,
|
|
33
|
+
allowlist=self.allowlist,
|
|
34
|
+
config_path=config_path,
|
|
35
|
+
plugin_configs=self.plugin_configs,
|
|
36
|
+
watch_config=self.watch_config,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def apply(self, runtime: TransportRuntime, *, config_path: Path | None) -> None:
|
|
40
|
+
runtime.update(
|
|
41
|
+
router=self.router,
|
|
42
|
+
projects=self.projects,
|
|
43
|
+
allowlist=self.allowlist,
|
|
44
|
+
config_path=config_path,
|
|
45
|
+
plugin_configs=self.plugin_configs,
|
|
46
|
+
watch_config=self.watch_config,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def resolve_plugins_allowlist(
|
|
51
|
+
settings: TakopiSettings | None,
|
|
52
|
+
) -> list[str] | None:
|
|
53
|
+
if settings is None:
|
|
54
|
+
return None
|
|
55
|
+
enabled = list(settings.plugins.enabled)
|
|
56
|
+
return enabled or None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def resolve_default_engine(
|
|
60
|
+
*,
|
|
61
|
+
override: str | None,
|
|
62
|
+
settings: TakopiSettings,
|
|
63
|
+
config_path: Path,
|
|
64
|
+
engine_ids: list[str],
|
|
65
|
+
) -> str:
|
|
66
|
+
default_engine = override or settings.default_engine or "codex"
|
|
67
|
+
if default_engine not in engine_ids:
|
|
68
|
+
available = ", ".join(sorted(engine_ids))
|
|
69
|
+
raise ConfigError(
|
|
70
|
+
f"Unknown default engine {default_engine!r}. Available: {available}."
|
|
71
|
+
)
|
|
72
|
+
return default_engine
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def build_router(
|
|
76
|
+
*,
|
|
77
|
+
settings: TakopiSettings,
|
|
78
|
+
config_path: Path,
|
|
79
|
+
backends: list[EngineBackend],
|
|
80
|
+
default_engine: str,
|
|
81
|
+
) -> AutoRouter:
|
|
82
|
+
entries: list[RunnerEntry] = []
|
|
83
|
+
warnings: list[str] = []
|
|
84
|
+
|
|
85
|
+
for backend in backends:
|
|
86
|
+
engine_id = backend.id
|
|
87
|
+
issue: str | None = None
|
|
88
|
+
status: EngineStatus = "ok"
|
|
89
|
+
engine_cfg: dict
|
|
90
|
+
try:
|
|
91
|
+
engine_cfg = settings.engine_config(engine_id, config_path=config_path)
|
|
92
|
+
except ConfigError as exc:
|
|
93
|
+
if engine_id == default_engine:
|
|
94
|
+
raise
|
|
95
|
+
issue = str(exc)
|
|
96
|
+
status = "bad_config"
|
|
97
|
+
engine_cfg = {}
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
runner = backend.build_runner(engine_cfg, config_path)
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
if engine_id == default_engine:
|
|
103
|
+
raise
|
|
104
|
+
issue = issue or str(exc)
|
|
105
|
+
if engine_cfg:
|
|
106
|
+
try:
|
|
107
|
+
runner = backend.build_runner({}, config_path)
|
|
108
|
+
except Exception as fallback_exc: # noqa: BLE001
|
|
109
|
+
warnings.append(f"{engine_id}: {issue or str(fallback_exc)}")
|
|
110
|
+
continue
|
|
111
|
+
status = "bad_config"
|
|
112
|
+
else:
|
|
113
|
+
status = "load_error"
|
|
114
|
+
warnings.append(f"{engine_id}: {issue}")
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
cmd = backend.cli_cmd or backend.id
|
|
118
|
+
if shutil.which(cmd) is None:
|
|
119
|
+
status = "missing_cli"
|
|
120
|
+
if issue:
|
|
121
|
+
issue = f"{issue}; {cmd} not found on PATH"
|
|
122
|
+
else:
|
|
123
|
+
issue = f"{cmd} not found on PATH"
|
|
124
|
+
|
|
125
|
+
if status != "ok" and engine_id == default_engine:
|
|
126
|
+
raise ConfigError(f"Default engine {engine_id!r} unavailable: {issue}")
|
|
127
|
+
|
|
128
|
+
if status != "ok" and engine_id != default_engine:
|
|
129
|
+
warnings.append(f"{engine_id}: {issue}")
|
|
130
|
+
|
|
131
|
+
entries.append(
|
|
132
|
+
RunnerEntry(
|
|
133
|
+
engine=engine_id,
|
|
134
|
+
runner=runner,
|
|
135
|
+
status=status,
|
|
136
|
+
issue=issue,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
for warning in warnings:
|
|
141
|
+
logger.warning("setup.warning", issue=warning)
|
|
142
|
+
|
|
143
|
+
return AutoRouter(entries=entries, default_engine=default_engine)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def load_backends(
|
|
147
|
+
*,
|
|
148
|
+
engine_ids: list[str],
|
|
149
|
+
allowlist: list[str] | None,
|
|
150
|
+
default_engine: str,
|
|
151
|
+
) -> list[EngineBackend]:
|
|
152
|
+
backends: list[EngineBackend] = []
|
|
153
|
+
load_issues: list[str] = []
|
|
154
|
+
for engine_id in engine_ids:
|
|
155
|
+
try:
|
|
156
|
+
backend = get_backend(engine_id, allowlist=allowlist)
|
|
157
|
+
except ConfigError as exc:
|
|
158
|
+
if engine_id == default_engine:
|
|
159
|
+
raise
|
|
160
|
+
load_issues.append(f"{engine_id}: {exc}")
|
|
161
|
+
continue
|
|
162
|
+
backends.append(backend)
|
|
163
|
+
if not backends:
|
|
164
|
+
raise ConfigError("No engine backends are available.")
|
|
165
|
+
for issue in load_issues:
|
|
166
|
+
logger.warning("setup.warning", issue=issue)
|
|
167
|
+
return backends
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def build_runtime_spec(
|
|
171
|
+
*,
|
|
172
|
+
settings: TakopiSettings,
|
|
173
|
+
config_path: Path,
|
|
174
|
+
default_engine_override: str | None = None,
|
|
175
|
+
reserved: Iterable[str] = RESERVED_CHAT_COMMANDS,
|
|
176
|
+
) -> RuntimeSpec:
|
|
177
|
+
allowlist = resolve_plugins_allowlist(settings)
|
|
178
|
+
engine_ids = list_backend_ids(allowlist=allowlist)
|
|
179
|
+
projects = settings.to_projects_config(
|
|
180
|
+
config_path=config_path,
|
|
181
|
+
engine_ids=engine_ids,
|
|
182
|
+
reserved=reserved,
|
|
183
|
+
)
|
|
184
|
+
default_engine = resolve_default_engine(
|
|
185
|
+
override=default_engine_override,
|
|
186
|
+
settings=settings,
|
|
187
|
+
config_path=config_path,
|
|
188
|
+
engine_ids=engine_ids,
|
|
189
|
+
)
|
|
190
|
+
backends = load_backends(
|
|
191
|
+
engine_ids=engine_ids,
|
|
192
|
+
allowlist=allowlist,
|
|
193
|
+
default_engine=default_engine,
|
|
194
|
+
)
|
|
195
|
+
router = build_router(
|
|
196
|
+
settings=settings,
|
|
197
|
+
config_path=config_path,
|
|
198
|
+
backends=backends,
|
|
199
|
+
default_engine=default_engine,
|
|
200
|
+
)
|
|
201
|
+
return RuntimeSpec(
|
|
202
|
+
router=router,
|
|
203
|
+
projects=projects,
|
|
204
|
+
allowlist=allowlist,
|
|
205
|
+
plugin_configs=settings.plugins.model_extra,
|
|
206
|
+
watch_config=settings.watch_config,
|
|
207
|
+
)
|
yee88/scheduler.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Protocol
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
|
|
8
|
+
import anyio
|
|
9
|
+
|
|
10
|
+
from .context import RunContext
|
|
11
|
+
from .logging import get_logger
|
|
12
|
+
from .model import ResumeToken
|
|
13
|
+
from .transport import ChannelId, MessageId, MessageRef, ThreadId
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class ThreadJob:
|
|
20
|
+
chat_id: ChannelId
|
|
21
|
+
user_msg_id: MessageId
|
|
22
|
+
text: str
|
|
23
|
+
resume_token: ResumeToken
|
|
24
|
+
context: RunContext | None = None
|
|
25
|
+
thread_id: ThreadId | None = None
|
|
26
|
+
session_key: tuple[int, int | None] | None = None
|
|
27
|
+
progress_ref: MessageRef | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
RunJob = Callable[[ThreadJob], Awaitable[None]]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TaskGroup(Protocol):
|
|
34
|
+
def start_soon(
|
|
35
|
+
self, func: Callable[..., Awaitable[object]], *args: Any
|
|
36
|
+
) -> None: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ThreadScheduler:
|
|
40
|
+
def __init__(self, *, task_group: TaskGroup, run_job: RunJob) -> None:
|
|
41
|
+
self._task_group = task_group
|
|
42
|
+
self._run_job = run_job
|
|
43
|
+
self._lock = anyio.Lock()
|
|
44
|
+
self._pending_by_thread: dict[str, deque[ThreadJob]] = {}
|
|
45
|
+
self._queued_by_progress: dict[tuple[ChannelId, MessageId], ThreadJob] = {}
|
|
46
|
+
self._active_threads: set[str] = set()
|
|
47
|
+
self._busy_until: dict[str, anyio.Event] = {}
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def thread_key(token: ResumeToken) -> str:
|
|
51
|
+
return f"{token.engine}:{token.value}"
|
|
52
|
+
|
|
53
|
+
async def note_thread_known(self, token: ResumeToken, done: anyio.Event) -> None:
|
|
54
|
+
key = self.thread_key(token)
|
|
55
|
+
async with self._lock:
|
|
56
|
+
current = self._busy_until.get(key)
|
|
57
|
+
if current is None or current.is_set():
|
|
58
|
+
self._busy_until[key] = done
|
|
59
|
+
self._task_group.start_soon(self._clear_busy, key, done)
|
|
60
|
+
|
|
61
|
+
async def enqueue(self, job: ThreadJob) -> None:
|
|
62
|
+
key = self.thread_key(job.resume_token)
|
|
63
|
+
async with self._lock:
|
|
64
|
+
queue = self._pending_by_thread.get(key)
|
|
65
|
+
if queue is None:
|
|
66
|
+
queue = deque()
|
|
67
|
+
self._pending_by_thread[key] = queue
|
|
68
|
+
queue.append(job)
|
|
69
|
+
if job.progress_ref is not None:
|
|
70
|
+
progress_key = (job.chat_id, job.progress_ref.message_id)
|
|
71
|
+
self._queued_by_progress[progress_key] = job
|
|
72
|
+
if key in self._active_threads:
|
|
73
|
+
return
|
|
74
|
+
self._active_threads.add(key)
|
|
75
|
+
self._task_group.start_soon(self._thread_worker, key)
|
|
76
|
+
|
|
77
|
+
async def enqueue_resume(
|
|
78
|
+
self,
|
|
79
|
+
chat_id: ChannelId,
|
|
80
|
+
user_msg_id: MessageId,
|
|
81
|
+
text: str,
|
|
82
|
+
resume_token: ResumeToken,
|
|
83
|
+
context: RunContext | None = None,
|
|
84
|
+
thread_id: ThreadId | None = None,
|
|
85
|
+
session_key: tuple[int, int | None] | None = None,
|
|
86
|
+
progress_ref: MessageRef | None = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
await self.enqueue(
|
|
89
|
+
ThreadJob(
|
|
90
|
+
chat_id=chat_id,
|
|
91
|
+
user_msg_id=user_msg_id,
|
|
92
|
+
text=text,
|
|
93
|
+
resume_token=resume_token,
|
|
94
|
+
context=context,
|
|
95
|
+
thread_id=thread_id,
|
|
96
|
+
session_key=session_key,
|
|
97
|
+
progress_ref=progress_ref,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
async def cancel_queued(
|
|
102
|
+
self, chat_id: ChannelId, progress_msg_id: MessageId
|
|
103
|
+
) -> ThreadJob | None:
|
|
104
|
+
progress_key = (chat_id, progress_msg_id)
|
|
105
|
+
async with self._lock:
|
|
106
|
+
job = self._queued_by_progress.pop(progress_key, None)
|
|
107
|
+
if job is None:
|
|
108
|
+
return None
|
|
109
|
+
thread_key = self.thread_key(job.resume_token)
|
|
110
|
+
queue = self._pending_by_thread.get(thread_key)
|
|
111
|
+
if queue is None:
|
|
112
|
+
return None
|
|
113
|
+
try:
|
|
114
|
+
queue.remove(job)
|
|
115
|
+
except ValueError:
|
|
116
|
+
return None
|
|
117
|
+
if not queue:
|
|
118
|
+
self._pending_by_thread.pop(thread_key, None)
|
|
119
|
+
return job
|
|
120
|
+
|
|
121
|
+
async def _clear_busy(self, key: str, done: anyio.Event) -> None:
|
|
122
|
+
await done.wait()
|
|
123
|
+
async with self._lock:
|
|
124
|
+
if self._busy_until.get(key) is done:
|
|
125
|
+
self._busy_until.pop(key, None)
|
|
126
|
+
|
|
127
|
+
async def _thread_worker(self, key: str) -> None:
|
|
128
|
+
try:
|
|
129
|
+
while True:
|
|
130
|
+
async with self._lock:
|
|
131
|
+
done = self._busy_until.get(key)
|
|
132
|
+
queue = self._pending_by_thread.get(key)
|
|
133
|
+
if not queue:
|
|
134
|
+
self._pending_by_thread.pop(key, None)
|
|
135
|
+
self._active_threads.discard(key)
|
|
136
|
+
return
|
|
137
|
+
job = queue.popleft()
|
|
138
|
+
if job.progress_ref is not None:
|
|
139
|
+
progress_key = (job.chat_id, job.progress_ref.message_id)
|
|
140
|
+
self._queued_by_progress.pop(progress_key, None)
|
|
141
|
+
|
|
142
|
+
if done is not None and not done.is_set():
|
|
143
|
+
await done.wait()
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
await self._run_job(job)
|
|
147
|
+
except Exception as exc: # noqa: BLE001
|
|
148
|
+
logger.exception(
|
|
149
|
+
"scheduler.job_failed",
|
|
150
|
+
key=key,
|
|
151
|
+
tag=job.resume_token.engine,
|
|
152
|
+
chat_id=job.chat_id,
|
|
153
|
+
user_msg_id=job.user_msg_id,
|
|
154
|
+
error=str(exc),
|
|
155
|
+
error_type=exc.__class__.__name__,
|
|
156
|
+
)
|
|
157
|
+
finally:
|
|
158
|
+
async with self._lock:
|
|
159
|
+
self._active_threads.discard(key)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Event schemas for runner JSONL streams."""
|
yee88/schemas/claude.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Msgspec models and decoder for Claude Code stream-json output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
import msgspec
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StreamTextBlock(
|
|
11
|
+
msgspec.Struct, tag="text", tag_field="type", forbid_unknown_fields=False
|
|
12
|
+
):
|
|
13
|
+
text: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StreamThinkingBlock(
|
|
17
|
+
msgspec.Struct, tag="thinking", tag_field="type", forbid_unknown_fields=False
|
|
18
|
+
):
|
|
19
|
+
thinking: str
|
|
20
|
+
signature: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class StreamToolUseBlock(
|
|
24
|
+
msgspec.Struct, tag="tool_use", tag_field="type", forbid_unknown_fields=False
|
|
25
|
+
):
|
|
26
|
+
id: str
|
|
27
|
+
name: str
|
|
28
|
+
input: dict[str, Any]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class StreamToolResultBlock(
|
|
32
|
+
msgspec.Struct, tag="tool_result", tag_field="type", forbid_unknown_fields=False
|
|
33
|
+
):
|
|
34
|
+
tool_use_id: str
|
|
35
|
+
content: str | list[dict[str, Any]] | None = None
|
|
36
|
+
is_error: bool | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
type StreamContentBlock = (
|
|
40
|
+
StreamTextBlock | StreamThinkingBlock | StreamToolUseBlock | StreamToolResultBlock
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class StreamUserMessageBody(msgspec.Struct, forbid_unknown_fields=False):
|
|
45
|
+
role: Literal["user"]
|
|
46
|
+
content: str | list[StreamContentBlock]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class StreamAssistantMessageBody(msgspec.Struct, forbid_unknown_fields=False):
|
|
50
|
+
role: Literal["assistant"]
|
|
51
|
+
content: list[StreamContentBlock]
|
|
52
|
+
model: str
|
|
53
|
+
error: str | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class StreamUserMessage(
|
|
57
|
+
msgspec.Struct, tag="user", tag_field="type", forbid_unknown_fields=False
|
|
58
|
+
):
|
|
59
|
+
message: StreamUserMessageBody
|
|
60
|
+
uuid: str | None = None
|
|
61
|
+
parent_tool_use_id: str | None = None
|
|
62
|
+
session_id: str | None = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class StreamAssistantMessage(
|
|
66
|
+
msgspec.Struct, tag="assistant", tag_field="type", forbid_unknown_fields=False
|
|
67
|
+
):
|
|
68
|
+
message: StreamAssistantMessageBody
|
|
69
|
+
parent_tool_use_id: str | None = None
|
|
70
|
+
uuid: str | None = None
|
|
71
|
+
session_id: str | None = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class StreamSystemMessage(
|
|
75
|
+
msgspec.Struct, tag="system", tag_field="type", forbid_unknown_fields=False
|
|
76
|
+
):
|
|
77
|
+
subtype: str
|
|
78
|
+
session_id: str | None = None
|
|
79
|
+
uuid: str | None = None
|
|
80
|
+
cwd: str | None = None
|
|
81
|
+
tools: list[str] | None = None
|
|
82
|
+
mcp_servers: list[Any] | None = None
|
|
83
|
+
model: str | None = None
|
|
84
|
+
permissionMode: str | None = None
|
|
85
|
+
output_style: str | None = None
|
|
86
|
+
apiKeySource: str | None = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class StreamResultMessage(
|
|
90
|
+
msgspec.Struct, tag="result", tag_field="type", forbid_unknown_fields=False
|
|
91
|
+
):
|
|
92
|
+
subtype: str
|
|
93
|
+
duration_ms: int
|
|
94
|
+
duration_api_ms: int
|
|
95
|
+
is_error: bool
|
|
96
|
+
num_turns: int
|
|
97
|
+
session_id: str
|
|
98
|
+
total_cost_usd: float | None = None
|
|
99
|
+
usage: dict[str, Any] | None = None
|
|
100
|
+
result: str | None = None
|
|
101
|
+
structured_output: Any = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class StreamEventMessage(
|
|
105
|
+
msgspec.Struct, tag="stream_event", tag_field="type", forbid_unknown_fields=False
|
|
106
|
+
):
|
|
107
|
+
uuid: str
|
|
108
|
+
session_id: str
|
|
109
|
+
event: dict[str, Any]
|
|
110
|
+
parent_tool_use_id: str | None = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ControlInterruptRequest(
|
|
114
|
+
msgspec.Struct, tag="interrupt", tag_field="subtype", forbid_unknown_fields=False
|
|
115
|
+
):
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ControlCanUseToolRequest(
|
|
120
|
+
msgspec.Struct, tag="can_use_tool", tag_field="subtype", forbid_unknown_fields=False
|
|
121
|
+
):
|
|
122
|
+
tool_name: str
|
|
123
|
+
input: dict[str, Any]
|
|
124
|
+
permission_suggestions: list[Any] | None = None
|
|
125
|
+
blocked_path: str | None = None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ControlInitializeRequest(
|
|
129
|
+
msgspec.Struct, tag="initialize", tag_field="subtype", forbid_unknown_fields=False
|
|
130
|
+
):
|
|
131
|
+
hooks: dict[str, Any] | None = None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ControlSetPermissionModeRequest(
|
|
135
|
+
msgspec.Struct,
|
|
136
|
+
tag="set_permission_mode",
|
|
137
|
+
tag_field="subtype",
|
|
138
|
+
forbid_unknown_fields=False,
|
|
139
|
+
):
|
|
140
|
+
mode: str
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ControlHookCallbackRequest(
|
|
144
|
+
msgspec.Struct,
|
|
145
|
+
tag="hook_callback",
|
|
146
|
+
tag_field="subtype",
|
|
147
|
+
forbid_unknown_fields=False,
|
|
148
|
+
):
|
|
149
|
+
callback_id: str
|
|
150
|
+
input: Any
|
|
151
|
+
tool_use_id: str | None = None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class ControlMcpMessageRequest(
|
|
155
|
+
msgspec.Struct, tag="mcp_message", tag_field="subtype", forbid_unknown_fields=False
|
|
156
|
+
):
|
|
157
|
+
server_name: str
|
|
158
|
+
message: Any
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class ControlRewindFilesRequest(
|
|
162
|
+
msgspec.Struct, tag="rewind_files", tag_field="subtype", forbid_unknown_fields=False
|
|
163
|
+
):
|
|
164
|
+
user_message_id: str
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
type ControlRequest = (
|
|
168
|
+
ControlInterruptRequest
|
|
169
|
+
| ControlCanUseToolRequest
|
|
170
|
+
| ControlInitializeRequest
|
|
171
|
+
| ControlSetPermissionModeRequest
|
|
172
|
+
| ControlHookCallbackRequest
|
|
173
|
+
| ControlMcpMessageRequest
|
|
174
|
+
| ControlRewindFilesRequest
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class StreamControlRequest(
|
|
179
|
+
msgspec.Struct, tag="control_request", tag_field="type", forbid_unknown_fields=False
|
|
180
|
+
):
|
|
181
|
+
request_id: str
|
|
182
|
+
request: ControlRequest
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class ControlSuccessResponse(
|
|
186
|
+
msgspec.Struct, tag="success", tag_field="subtype", forbid_unknown_fields=False
|
|
187
|
+
):
|
|
188
|
+
request_id: str
|
|
189
|
+
response: dict[str, Any] | None = None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class ControlErrorResponse(
|
|
193
|
+
msgspec.Struct, tag="error", tag_field="subtype", forbid_unknown_fields=False
|
|
194
|
+
):
|
|
195
|
+
request_id: str
|
|
196
|
+
error: str
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
type ControlResponse = ControlSuccessResponse | ControlErrorResponse
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class StreamControlResponse(
|
|
203
|
+
msgspec.Struct,
|
|
204
|
+
tag="control_response",
|
|
205
|
+
tag_field="type",
|
|
206
|
+
forbid_unknown_fields=False,
|
|
207
|
+
):
|
|
208
|
+
response: ControlResponse
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class StreamControlCancelRequest(
|
|
212
|
+
msgspec.Struct,
|
|
213
|
+
tag="control_cancel_request",
|
|
214
|
+
tag_field="type",
|
|
215
|
+
forbid_unknown_fields=False,
|
|
216
|
+
):
|
|
217
|
+
request_id: str | None = None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
type StreamJsonMessage = (
|
|
221
|
+
StreamUserMessage
|
|
222
|
+
| StreamAssistantMessage
|
|
223
|
+
| StreamSystemMessage
|
|
224
|
+
| StreamResultMessage
|
|
225
|
+
| StreamEventMessage
|
|
226
|
+
| StreamControlRequest
|
|
227
|
+
| StreamControlResponse
|
|
228
|
+
| StreamControlCancelRequest
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
STREAM_JSON_SCHEMA = msgspec.json.schema(StreamJsonMessage)
|
|
233
|
+
|
|
234
|
+
_DECODER = msgspec.json.Decoder(StreamJsonMessage)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def decode_stream_json_line(line: str | bytes) -> StreamJsonMessage:
|
|
238
|
+
return _DECODER.decode(line)
|