yee88 0.1.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.
Files changed (103) hide show
  1. takopi/__init__.py +1 -0
  2. takopi/api.py +116 -0
  3. takopi/backends.py +25 -0
  4. takopi/backends_helpers.py +14 -0
  5. takopi/cli/__init__.py +228 -0
  6. takopi/cli/config.py +320 -0
  7. takopi/cli/doctor.py +173 -0
  8. takopi/cli/init.py +113 -0
  9. takopi/cli/onboarding_cmd.py +126 -0
  10. takopi/cli/plugins.py +196 -0
  11. takopi/cli/run.py +419 -0
  12. takopi/cli/topic.py +355 -0
  13. takopi/commands.py +134 -0
  14. takopi/config.py +142 -0
  15. takopi/config_migrations.py +124 -0
  16. takopi/config_watch.py +146 -0
  17. takopi/context.py +9 -0
  18. takopi/directives.py +146 -0
  19. takopi/engines.py +53 -0
  20. takopi/events.py +170 -0
  21. takopi/ids.py +17 -0
  22. takopi/lockfile.py +158 -0
  23. takopi/logging.py +283 -0
  24. takopi/markdown.py +298 -0
  25. takopi/model.py +77 -0
  26. takopi/plugins.py +312 -0
  27. takopi/presenter.py +25 -0
  28. takopi/progress.py +99 -0
  29. takopi/router.py +113 -0
  30. takopi/runner.py +712 -0
  31. takopi/runner_bridge.py +619 -0
  32. takopi/runners/__init__.py +1 -0
  33. takopi/runners/claude.py +483 -0
  34. takopi/runners/codex.py +656 -0
  35. takopi/runners/mock.py +221 -0
  36. takopi/runners/opencode.py +505 -0
  37. takopi/runners/pi.py +523 -0
  38. takopi/runners/run_options.py +39 -0
  39. takopi/runners/tool_actions.py +90 -0
  40. takopi/runtime_loader.py +207 -0
  41. takopi/scheduler.py +159 -0
  42. takopi/schemas/__init__.py +1 -0
  43. takopi/schemas/claude.py +238 -0
  44. takopi/schemas/codex.py +169 -0
  45. takopi/schemas/opencode.py +51 -0
  46. takopi/schemas/pi.py +117 -0
  47. takopi/settings.py +360 -0
  48. takopi/telegram/__init__.py +20 -0
  49. takopi/telegram/api_models.py +37 -0
  50. takopi/telegram/api_schemas.py +152 -0
  51. takopi/telegram/backend.py +163 -0
  52. takopi/telegram/bridge.py +425 -0
  53. takopi/telegram/chat_prefs.py +242 -0
  54. takopi/telegram/chat_sessions.py +112 -0
  55. takopi/telegram/client.py +409 -0
  56. takopi/telegram/client_api.py +539 -0
  57. takopi/telegram/commands/__init__.py +12 -0
  58. takopi/telegram/commands/agent.py +196 -0
  59. takopi/telegram/commands/cancel.py +116 -0
  60. takopi/telegram/commands/dispatch.py +111 -0
  61. takopi/telegram/commands/executor.py +449 -0
  62. takopi/telegram/commands/file_transfer.py +586 -0
  63. takopi/telegram/commands/handlers.py +45 -0
  64. takopi/telegram/commands/media.py +143 -0
  65. takopi/telegram/commands/menu.py +139 -0
  66. takopi/telegram/commands/model.py +215 -0
  67. takopi/telegram/commands/overrides.py +159 -0
  68. takopi/telegram/commands/parse.py +30 -0
  69. takopi/telegram/commands/plan.py +16 -0
  70. takopi/telegram/commands/reasoning.py +234 -0
  71. takopi/telegram/commands/reply.py +23 -0
  72. takopi/telegram/commands/topics.py +332 -0
  73. takopi/telegram/commands/trigger.py +143 -0
  74. takopi/telegram/context.py +140 -0
  75. takopi/telegram/engine_defaults.py +86 -0
  76. takopi/telegram/engine_overrides.py +105 -0
  77. takopi/telegram/files.py +178 -0
  78. takopi/telegram/loop.py +1822 -0
  79. takopi/telegram/onboarding.py +1088 -0
  80. takopi/telegram/outbox.py +177 -0
  81. takopi/telegram/parsing.py +239 -0
  82. takopi/telegram/render.py +198 -0
  83. takopi/telegram/state_store.py +88 -0
  84. takopi/telegram/topic_state.py +334 -0
  85. takopi/telegram/topics.py +256 -0
  86. takopi/telegram/trigger_mode.py +68 -0
  87. takopi/telegram/types.py +63 -0
  88. takopi/telegram/voice.py +110 -0
  89. takopi/transport.py +53 -0
  90. takopi/transport_runtime.py +323 -0
  91. takopi/transports.py +76 -0
  92. takopi/utils/__init__.py +1 -0
  93. takopi/utils/git.py +87 -0
  94. takopi/utils/json_state.py +21 -0
  95. takopi/utils/paths.py +47 -0
  96. takopi/utils/streams.py +44 -0
  97. takopi/utils/subprocess.py +86 -0
  98. takopi/worktrees.py +135 -0
  99. yee88-0.1.0.dist-info/METADATA +116 -0
  100. yee88-0.1.0.dist-info/RECORD +103 -0
  101. yee88-0.1.0.dist-info/WHEEL +4 -0
  102. yee88-0.1.0.dist-info/entry_points.txt +11 -0
  103. yee88-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,449 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
4
+ from dataclasses import dataclass
5
+ from functools import partial
6
+ from typing import cast
7
+
8
+ import anyio
9
+
10
+ from ...commands import CommandExecutor, RunMode, RunRequest, RunResult
11
+ from ...config import ConfigError
12
+ from ...context import RunContext
13
+ from ...logging import bind_run_context, clear_context, get_logger
14
+ from ...model import Action, ActionEvent, EngineId, ResumeToken, TakopiEvent
15
+ from ...progress import ProgressTracker
16
+ from ...router import RunnerUnavailableError
17
+ from ...runner import Runner
18
+ from ...runners.run_options import EngineRunOptions, apply_run_options
19
+ from ...runner_bridge import (
20
+ ExecBridgeConfig,
21
+ IncomingMessage as RunnerIncomingMessage,
22
+ RunningTasks,
23
+ handle_message,
24
+ )
25
+ from ...scheduler import ThreadScheduler
26
+ from ...transport import MessageRef, RenderedMessage, SendOptions
27
+ from ...transport_runtime import TransportRuntime
28
+ from ...utils.paths import reset_run_base_dir, set_run_base_dir
29
+ from ..bridge import send_plain
30
+ from ..engine_overrides import supports_reasoning
31
+
32
+ logger = get_logger(__name__)
33
+
34
+
35
+ @dataclass(slots=True)
36
+ class _ResumeLineProxy:
37
+ runner: Runner
38
+
39
+ @property
40
+ def engine(self) -> str:
41
+ return self.runner.engine
42
+
43
+ def is_resume_line(self, line: str) -> bool:
44
+ return self.runner.is_resume_line(line)
45
+
46
+ def format_resume(self, _: ResumeToken) -> str:
47
+ return ""
48
+
49
+ def extract_resume(self, text: str | None) -> ResumeToken | None:
50
+ return self.runner.extract_resume(text)
51
+
52
+ def run(
53
+ self, prompt: str, resume: ResumeToken | None
54
+ ) -> AsyncIterator[TakopiEvent]:
55
+ return self.runner.run(prompt, resume)
56
+
57
+
58
+ @dataclass(slots=True)
59
+ class _PreludeRunner:
60
+ runner: Runner
61
+ prelude_events: Sequence[TakopiEvent]
62
+
63
+ @property
64
+ def engine(self) -> str:
65
+ return self.runner.engine
66
+
67
+ def is_resume_line(self, line: str) -> bool:
68
+ return self.runner.is_resume_line(line)
69
+
70
+ def format_resume(self, token: ResumeToken) -> str:
71
+ return self.runner.format_resume(token)
72
+
73
+ def extract_resume(self, text: str | None) -> ResumeToken | None:
74
+ return self.runner.extract_resume(text)
75
+
76
+ async def run(
77
+ self, prompt: str, resume: ResumeToken | None
78
+ ) -> AsyncIterator[TakopiEvent]:
79
+ for event in self.prelude_events:
80
+ yield event
81
+ async for event in self.runner.run(prompt, resume):
82
+ yield event
83
+
84
+
85
+ def _reasoning_warning(
86
+ *, engine: str, run_options: EngineRunOptions | None
87
+ ) -> ActionEvent | None:
88
+ if run_options is None or not run_options.reasoning:
89
+ return None
90
+ if supports_reasoning(engine):
91
+ return None
92
+ message = f"reasoning override is not supported for `{engine}`; ignoring."
93
+ return ActionEvent(
94
+ engine=engine,
95
+ action=Action(
96
+ id=f"{engine}.override.reasoning",
97
+ kind="note",
98
+ title=message,
99
+ detail={},
100
+ ),
101
+ phase="completed",
102
+ ok=True,
103
+ )
104
+
105
+
106
+ def _should_show_resume_line(
107
+ *,
108
+ show_resume_line: bool,
109
+ stateful_mode: bool,
110
+ context: RunContext | None,
111
+ ) -> bool:
112
+ if show_resume_line:
113
+ return True
114
+ return not stateful_mode
115
+
116
+
117
+ async def _send_runner_unavailable(
118
+ exec_cfg: ExecBridgeConfig,
119
+ *,
120
+ chat_id: int,
121
+ user_msg_id: int,
122
+ resume_token: ResumeToken | None,
123
+ runner: Runner,
124
+ reason: str,
125
+ thread_id: int | None = None,
126
+ ) -> None:
127
+ tracker = ProgressTracker(engine=runner.engine)
128
+ tracker.set_resume(resume_token)
129
+ state = tracker.snapshot(resume_formatter=runner.format_resume)
130
+ message = exec_cfg.presenter.render_final(
131
+ state,
132
+ elapsed_s=0.0,
133
+ status="error",
134
+ answer=f"error:\n{reason}",
135
+ )
136
+ reply_to = MessageRef(channel_id=chat_id, message_id=user_msg_id)
137
+ await exec_cfg.transport.send(
138
+ channel_id=chat_id,
139
+ message=message,
140
+ options=SendOptions(reply_to=reply_to, notify=True, thread_id=thread_id),
141
+ )
142
+
143
+
144
+ async def _run_engine(
145
+ *,
146
+ exec_cfg: ExecBridgeConfig,
147
+ runtime: TransportRuntime,
148
+ running_tasks: RunningTasks | None,
149
+ chat_id: int,
150
+ user_msg_id: int,
151
+ text: str,
152
+ resume_token: ResumeToken | None,
153
+ context: RunContext | None,
154
+ reply_ref: MessageRef | None = None,
155
+ on_thread_known: Callable[[ResumeToken, anyio.Event], Awaitable[None]]
156
+ | None = None,
157
+ engine_override: EngineId | None = None,
158
+ thread_id: int | None = None,
159
+ show_resume_line: bool = True,
160
+ progress_ref: MessageRef | None = None,
161
+ run_options: EngineRunOptions | None = None,
162
+ ) -> None:
163
+ reply = partial(
164
+ send_plain,
165
+ exec_cfg.transport,
166
+ chat_id=chat_id,
167
+ user_msg_id=user_msg_id,
168
+ thread_id=thread_id,
169
+ )
170
+ try:
171
+ try:
172
+ entry = runtime.resolve_runner(
173
+ resume_token=resume_token,
174
+ engine_override=engine_override,
175
+ )
176
+ except RunnerUnavailableError as exc:
177
+ await reply(text=f"error:\n{exc}")
178
+ return
179
+ runner: Runner = entry.runner
180
+ if not show_resume_line:
181
+ runner = cast(Runner, _ResumeLineProxy(runner))
182
+ warning = _reasoning_warning(engine=runner.engine, run_options=run_options)
183
+ if warning is not None:
184
+ runner = cast(Runner, _PreludeRunner(runner, [warning]))
185
+ if not entry.available:
186
+ reason = entry.issue or "engine unavailable"
187
+ await _send_runner_unavailable(
188
+ exec_cfg,
189
+ chat_id=chat_id,
190
+ user_msg_id=user_msg_id,
191
+ resume_token=resume_token,
192
+ runner=runner,
193
+ reason=reason,
194
+ thread_id=thread_id,
195
+ )
196
+ return
197
+ try:
198
+ cwd = runtime.resolve_run_cwd(context)
199
+ except ConfigError as exc:
200
+ await reply(text=f"error:\n{exc}")
201
+ return
202
+ run_base_token = set_run_base_dir(cwd)
203
+ try:
204
+ run_fields = {
205
+ "chat_id": chat_id,
206
+ "user_msg_id": user_msg_id,
207
+ "engine": runner.engine,
208
+ "resume": resume_token.value if resume_token else None,
209
+ }
210
+ if context is not None:
211
+ run_fields["project"] = context.project
212
+ run_fields["branch"] = context.branch
213
+ if cwd is not None:
214
+ run_fields["cwd"] = str(cwd)
215
+ bind_run_context(**run_fields)
216
+ context_line = runtime.format_context_line(context)
217
+ incoming = RunnerIncomingMessage(
218
+ channel_id=chat_id,
219
+ message_id=user_msg_id,
220
+ text=text,
221
+ reply_to=reply_ref,
222
+ thread_id=thread_id,
223
+ )
224
+ with apply_run_options(run_options):
225
+ await handle_message(
226
+ exec_cfg,
227
+ runner=runner,
228
+ incoming=incoming,
229
+ resume_token=resume_token,
230
+ context=context,
231
+ context_line=context_line,
232
+ strip_resume_line=runtime.is_resume_line,
233
+ running_tasks=running_tasks,
234
+ on_thread_known=on_thread_known,
235
+ progress_ref=progress_ref,
236
+ )
237
+ finally:
238
+ reset_run_base_dir(run_base_token)
239
+ except Exception as exc:
240
+ logger.exception(
241
+ "handle.worker_failed",
242
+ error=str(exc),
243
+ error_type=exc.__class__.__name__,
244
+ )
245
+ finally:
246
+ clear_context()
247
+
248
+
249
+ class _CaptureTransport:
250
+ def __init__(self) -> None:
251
+ self._next_id = 1
252
+ self.last_message: RenderedMessage | None = None
253
+
254
+ async def send(
255
+ self,
256
+ *,
257
+ channel_id: int | str,
258
+ message: RenderedMessage,
259
+ options: SendOptions | None = None,
260
+ ) -> MessageRef:
261
+ thread_id = options.thread_id if options is not None else None
262
+ ref = MessageRef(channel_id=channel_id, message_id=self._next_id)
263
+ self._next_id += 1
264
+ self.last_message = message
265
+ return MessageRef(
266
+ channel_id=ref.channel_id,
267
+ message_id=ref.message_id,
268
+ thread_id=thread_id,
269
+ )
270
+
271
+ async def edit(
272
+ self, *, ref: MessageRef, message: RenderedMessage, wait: bool = True
273
+ ) -> MessageRef:
274
+ self.last_message = message
275
+ return ref
276
+
277
+ async def delete(self, *, ref: MessageRef) -> bool:
278
+ return True
279
+
280
+ async def close(self) -> None:
281
+ return None
282
+
283
+
284
+ class _TelegramCommandExecutor(CommandExecutor):
285
+ def __init__(
286
+ self,
287
+ *,
288
+ exec_cfg: ExecBridgeConfig,
289
+ runtime: TransportRuntime,
290
+ running_tasks: RunningTasks,
291
+ scheduler: ThreadScheduler,
292
+ on_thread_known: Callable[[ResumeToken, anyio.Event], Awaitable[None]] | None,
293
+ engine_overrides_resolver: Callable[
294
+ [EngineId], Awaitable[EngineRunOptions | None]
295
+ ]
296
+ | None,
297
+ chat_id: int,
298
+ user_msg_id: int,
299
+ thread_id: int | None,
300
+ show_resume_line: bool,
301
+ stateful_mode: bool,
302
+ default_engine_override: EngineId | None,
303
+ ) -> None:
304
+ self._exec_cfg = exec_cfg
305
+ self._runtime = runtime
306
+ self._running_tasks = running_tasks
307
+ self._scheduler = scheduler
308
+ self._on_thread_known = on_thread_known
309
+ self._engine_overrides_resolver = engine_overrides_resolver
310
+ self._chat_id = chat_id
311
+ self._user_msg_id = user_msg_id
312
+ self._thread_id = thread_id
313
+ self._show_resume_line = show_resume_line
314
+ self._stateful_mode = stateful_mode
315
+ self._default_engine_override = default_engine_override
316
+ self._reply_ref = MessageRef(
317
+ channel_id=chat_id,
318
+ message_id=user_msg_id,
319
+ thread_id=thread_id,
320
+ )
321
+
322
+ def _apply_default_context(self, request: RunRequest) -> RunRequest:
323
+ if request.context is not None:
324
+ return request
325
+ context = self._runtime.default_context_for_chat(self._chat_id)
326
+ if context is None:
327
+ return request
328
+ return RunRequest(
329
+ prompt=request.prompt,
330
+ engine=request.engine,
331
+ context=context,
332
+ )
333
+
334
+ def _apply_default_engine(self, request: RunRequest) -> RunRequest:
335
+ if request.engine is not None or self._default_engine_override is None:
336
+ return request
337
+ return RunRequest(
338
+ prompt=request.prompt,
339
+ engine=self._default_engine_override,
340
+ context=request.context,
341
+ )
342
+
343
+ async def send(
344
+ self,
345
+ message: RenderedMessage | str,
346
+ *,
347
+ reply_to: MessageRef | None = None,
348
+ notify: bool = True,
349
+ ) -> MessageRef | None:
350
+ rendered = (
351
+ message
352
+ if isinstance(message, RenderedMessage)
353
+ else RenderedMessage(text=message)
354
+ )
355
+ reply_ref = self._reply_ref if reply_to is None else reply_to
356
+ return await self._exec_cfg.transport.send(
357
+ channel_id=self._chat_id,
358
+ message=rendered,
359
+ options=SendOptions(
360
+ reply_to=reply_ref,
361
+ notify=notify,
362
+ thread_id=self._thread_id,
363
+ ),
364
+ )
365
+
366
+ async def run_one(
367
+ self, request: RunRequest, *, mode: RunMode = "emit"
368
+ ) -> RunResult:
369
+ request = self._apply_default_context(request)
370
+ request = self._apply_default_engine(request)
371
+ effective_show_resume_line = _should_show_resume_line(
372
+ show_resume_line=self._show_resume_line,
373
+ stateful_mode=self._stateful_mode,
374
+ context=request.context,
375
+ )
376
+ engine = self._runtime.resolve_engine(
377
+ engine_override=request.engine,
378
+ context=request.context,
379
+ )
380
+ run_options = None
381
+ if self._engine_overrides_resolver is not None:
382
+ run_options = await self._engine_overrides_resolver(engine)
383
+ on_thread_known = (
384
+ self._scheduler.note_thread_known
385
+ if self._on_thread_known is None
386
+ else self._on_thread_known
387
+ )
388
+ if mode == "capture":
389
+ capture = _CaptureTransport()
390
+ exec_cfg = ExecBridgeConfig(
391
+ transport=capture,
392
+ presenter=self._exec_cfg.presenter,
393
+ final_notify=False,
394
+ )
395
+ await _run_engine(
396
+ exec_cfg=exec_cfg,
397
+ runtime=self._runtime,
398
+ running_tasks={},
399
+ chat_id=self._chat_id,
400
+ user_msg_id=self._user_msg_id,
401
+ text=request.prompt,
402
+ resume_token=None,
403
+ context=request.context,
404
+ reply_ref=self._reply_ref,
405
+ on_thread_known=on_thread_known,
406
+ engine_override=engine,
407
+ thread_id=self._thread_id,
408
+ show_resume_line=effective_show_resume_line,
409
+ run_options=run_options,
410
+ )
411
+ return RunResult(engine=engine, message=capture.last_message)
412
+ await _run_engine(
413
+ exec_cfg=self._exec_cfg,
414
+ runtime=self._runtime,
415
+ running_tasks=self._running_tasks,
416
+ chat_id=self._chat_id,
417
+ user_msg_id=self._user_msg_id,
418
+ text=request.prompt,
419
+ resume_token=None,
420
+ context=request.context,
421
+ reply_ref=self._reply_ref,
422
+ on_thread_known=on_thread_known,
423
+ engine_override=engine,
424
+ thread_id=self._thread_id,
425
+ show_resume_line=effective_show_resume_line,
426
+ run_options=run_options,
427
+ )
428
+ return RunResult(engine=engine, message=None)
429
+
430
+ async def run_many(
431
+ self,
432
+ requests: Sequence[RunRequest],
433
+ *,
434
+ mode: RunMode = "emit",
435
+ parallel: bool = False,
436
+ ) -> list[RunResult]:
437
+ if not parallel:
438
+ return [await self.run_one(request, mode=mode) for request in requests]
439
+ results: list[RunResult | None] = [None] * len(requests)
440
+
441
+ async with anyio.create_task_group() as tg:
442
+
443
+ async def run_idx(idx: int, request: RunRequest) -> None:
444
+ results[idx] = await self.run_one(request, mode=mode)
445
+
446
+ for idx, request in enumerate(requests):
447
+ tg.start_soon(run_idx, idx, request)
448
+
449
+ return [result for result in results if result is not None]