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.
- takopi/__init__.py +1 -0
- takopi/api.py +116 -0
- takopi/backends.py +25 -0
- takopi/backends_helpers.py +14 -0
- takopi/cli/__init__.py +228 -0
- takopi/cli/config.py +320 -0
- takopi/cli/doctor.py +173 -0
- takopi/cli/init.py +113 -0
- takopi/cli/onboarding_cmd.py +126 -0
- takopi/cli/plugins.py +196 -0
- takopi/cli/run.py +419 -0
- takopi/cli/topic.py +355 -0
- takopi/commands.py +134 -0
- takopi/config.py +142 -0
- takopi/config_migrations.py +124 -0
- takopi/config_watch.py +146 -0
- takopi/context.py +9 -0
- takopi/directives.py +146 -0
- takopi/engines.py +53 -0
- takopi/events.py +170 -0
- takopi/ids.py +17 -0
- takopi/lockfile.py +158 -0
- takopi/logging.py +283 -0
- takopi/markdown.py +298 -0
- takopi/model.py +77 -0
- takopi/plugins.py +312 -0
- takopi/presenter.py +25 -0
- takopi/progress.py +99 -0
- takopi/router.py +113 -0
- takopi/runner.py +712 -0
- takopi/runner_bridge.py +619 -0
- takopi/runners/__init__.py +1 -0
- takopi/runners/claude.py +483 -0
- takopi/runners/codex.py +656 -0
- takopi/runners/mock.py +221 -0
- takopi/runners/opencode.py +505 -0
- takopi/runners/pi.py +523 -0
- takopi/runners/run_options.py +39 -0
- takopi/runners/tool_actions.py +90 -0
- takopi/runtime_loader.py +207 -0
- takopi/scheduler.py +159 -0
- takopi/schemas/__init__.py +1 -0
- takopi/schemas/claude.py +238 -0
- takopi/schemas/codex.py +169 -0
- takopi/schemas/opencode.py +51 -0
- takopi/schemas/pi.py +117 -0
- takopi/settings.py +360 -0
- takopi/telegram/__init__.py +20 -0
- takopi/telegram/api_models.py +37 -0
- takopi/telegram/api_schemas.py +152 -0
- takopi/telegram/backend.py +163 -0
- takopi/telegram/bridge.py +425 -0
- takopi/telegram/chat_prefs.py +242 -0
- takopi/telegram/chat_sessions.py +112 -0
- takopi/telegram/client.py +409 -0
- takopi/telegram/client_api.py +539 -0
- takopi/telegram/commands/__init__.py +12 -0
- takopi/telegram/commands/agent.py +196 -0
- takopi/telegram/commands/cancel.py +116 -0
- takopi/telegram/commands/dispatch.py +111 -0
- takopi/telegram/commands/executor.py +449 -0
- takopi/telegram/commands/file_transfer.py +586 -0
- takopi/telegram/commands/handlers.py +45 -0
- takopi/telegram/commands/media.py +143 -0
- takopi/telegram/commands/menu.py +139 -0
- takopi/telegram/commands/model.py +215 -0
- takopi/telegram/commands/overrides.py +159 -0
- takopi/telegram/commands/parse.py +30 -0
- takopi/telegram/commands/plan.py +16 -0
- takopi/telegram/commands/reasoning.py +234 -0
- takopi/telegram/commands/reply.py +23 -0
- takopi/telegram/commands/topics.py +332 -0
- takopi/telegram/commands/trigger.py +143 -0
- takopi/telegram/context.py +140 -0
- takopi/telegram/engine_defaults.py +86 -0
- takopi/telegram/engine_overrides.py +105 -0
- takopi/telegram/files.py +178 -0
- takopi/telegram/loop.py +1822 -0
- takopi/telegram/onboarding.py +1088 -0
- takopi/telegram/outbox.py +177 -0
- takopi/telegram/parsing.py +239 -0
- takopi/telegram/render.py +198 -0
- takopi/telegram/state_store.py +88 -0
- takopi/telegram/topic_state.py +334 -0
- takopi/telegram/topics.py +256 -0
- takopi/telegram/trigger_mode.py +68 -0
- takopi/telegram/types.py +63 -0
- takopi/telegram/voice.py +110 -0
- takopi/transport.py +53 -0
- takopi/transport_runtime.py +323 -0
- takopi/transports.py +76 -0
- takopi/utils/__init__.py +1 -0
- takopi/utils/git.py +87 -0
- takopi/utils/json_state.py +21 -0
- takopi/utils/paths.py +47 -0
- takopi/utils/streams.py +44 -0
- takopi/utils/subprocess.py +86 -0
- takopi/worktrees.py +135 -0
- yee88-0.1.0.dist-info/METADATA +116 -0
- yee88-0.1.0.dist-info/RECORD +103 -0
- yee88-0.1.0.dist-info/WHEEL +4 -0
- yee88-0.1.0.dist-info/entry_points.txt +11 -0
- yee88-0.1.0.dist-info/licenses/LICENSE +21 -0
takopi/runner_bridge.py
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
import anyio
|
|
8
|
+
|
|
9
|
+
from .context import RunContext
|
|
10
|
+
from .logging import bind_run_context, get_logger
|
|
11
|
+
from .model import CompletedEvent, ResumeToken, StartedEvent, TakopiEvent
|
|
12
|
+
from .presenter import Presenter
|
|
13
|
+
from .markdown import render_event_cli
|
|
14
|
+
from .runner import Runner
|
|
15
|
+
from .progress import ProgressTracker
|
|
16
|
+
from .transport import (
|
|
17
|
+
ChannelId,
|
|
18
|
+
MessageId,
|
|
19
|
+
MessageRef,
|
|
20
|
+
RenderedMessage,
|
|
21
|
+
SendOptions,
|
|
22
|
+
ThreadId,
|
|
23
|
+
Transport,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _log_runner_event(evt: TakopiEvent) -> None:
|
|
30
|
+
for line in render_event_cli(evt):
|
|
31
|
+
logger.debug(
|
|
32
|
+
"runner.event.cli",
|
|
33
|
+
line=line,
|
|
34
|
+
event_type=getattr(evt, "type", None),
|
|
35
|
+
engine=getattr(evt, "engine", None),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _strip_resume_lines(text: str, *, is_resume_line: Callable[[str], bool]) -> str:
|
|
40
|
+
prompt = "\n".join(
|
|
41
|
+
line for line in text.splitlines() if not is_resume_line(line)
|
|
42
|
+
).strip()
|
|
43
|
+
return prompt or "continue"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _flatten_exception_group(error: BaseException) -> list[BaseException]:
|
|
47
|
+
if isinstance(error, BaseExceptionGroup):
|
|
48
|
+
flattened: list[BaseException] = []
|
|
49
|
+
for exc in error.exceptions:
|
|
50
|
+
flattened.extend(_flatten_exception_group(exc))
|
|
51
|
+
return flattened
|
|
52
|
+
return [error]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _format_error(error: Exception) -> str:
|
|
56
|
+
cancel_exc = anyio.get_cancelled_exc_class()
|
|
57
|
+
flattened = [
|
|
58
|
+
exc
|
|
59
|
+
for exc in _flatten_exception_group(error)
|
|
60
|
+
if not isinstance(exc, cancel_exc)
|
|
61
|
+
]
|
|
62
|
+
if len(flattened) == 1:
|
|
63
|
+
return str(flattened[0]) or flattened[0].__class__.__name__
|
|
64
|
+
if not flattened:
|
|
65
|
+
return str(error) or error.__class__.__name__
|
|
66
|
+
messages = [str(exc) for exc in flattened if str(exc)]
|
|
67
|
+
if not messages:
|
|
68
|
+
return str(error) or error.__class__.__name__
|
|
69
|
+
if len(messages) == 1:
|
|
70
|
+
return messages[0]
|
|
71
|
+
return "\n".join(messages)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True, slots=True)
|
|
75
|
+
class IncomingMessage:
|
|
76
|
+
channel_id: ChannelId
|
|
77
|
+
message_id: MessageId
|
|
78
|
+
text: str
|
|
79
|
+
reply_to: MessageRef | None = None
|
|
80
|
+
thread_id: ThreadId | None = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True, slots=True)
|
|
84
|
+
class ExecBridgeConfig:
|
|
85
|
+
transport: Transport
|
|
86
|
+
presenter: Presenter
|
|
87
|
+
final_notify: bool
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(slots=True)
|
|
91
|
+
class RunningTask:
|
|
92
|
+
resume: ResumeToken | None = None
|
|
93
|
+
resume_ready: anyio.Event = field(default_factory=anyio.Event)
|
|
94
|
+
cancel_requested: anyio.Event = field(default_factory=anyio.Event)
|
|
95
|
+
done: anyio.Event = field(default_factory=anyio.Event)
|
|
96
|
+
context: RunContext | None = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
RunningTasks = dict[MessageRef, RunningTask]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def _send_or_edit_message(
|
|
103
|
+
transport: Transport,
|
|
104
|
+
*,
|
|
105
|
+
channel_id: ChannelId,
|
|
106
|
+
message: RenderedMessage,
|
|
107
|
+
edit_ref: MessageRef | None = None,
|
|
108
|
+
reply_to: MessageRef | None = None,
|
|
109
|
+
notify: bool = True,
|
|
110
|
+
replace_ref: MessageRef | None = None,
|
|
111
|
+
thread_id: ThreadId | None = None,
|
|
112
|
+
) -> tuple[MessageRef | None, bool]:
|
|
113
|
+
msg = message
|
|
114
|
+
followups = message.extra.get("followups")
|
|
115
|
+
if followups:
|
|
116
|
+
extra = dict(message.extra)
|
|
117
|
+
if reply_to is not None:
|
|
118
|
+
extra.setdefault("followup_reply_to_message_id", reply_to.message_id)
|
|
119
|
+
if thread_id is not None:
|
|
120
|
+
extra.setdefault("followup_thread_id", thread_id)
|
|
121
|
+
extra.setdefault("followup_notify", notify)
|
|
122
|
+
msg = RenderedMessage(text=message.text, extra=extra)
|
|
123
|
+
if edit_ref is not None:
|
|
124
|
+
logger.debug(
|
|
125
|
+
"transport.edit_message",
|
|
126
|
+
channel_id=edit_ref.channel_id,
|
|
127
|
+
message_id=edit_ref.message_id,
|
|
128
|
+
rendered=msg.text,
|
|
129
|
+
)
|
|
130
|
+
edited = await transport.edit(ref=edit_ref, message=msg)
|
|
131
|
+
if edited is not None:
|
|
132
|
+
return edited, True
|
|
133
|
+
|
|
134
|
+
logger.debug(
|
|
135
|
+
"transport.send_message",
|
|
136
|
+
channel_id=channel_id,
|
|
137
|
+
reply_to_message_id=reply_to.message_id if reply_to else None,
|
|
138
|
+
rendered=msg.text,
|
|
139
|
+
)
|
|
140
|
+
sent = await transport.send(
|
|
141
|
+
channel_id=channel_id,
|
|
142
|
+
message=msg,
|
|
143
|
+
options=SendOptions(
|
|
144
|
+
reply_to=reply_to,
|
|
145
|
+
notify=notify,
|
|
146
|
+
replace=replace_ref,
|
|
147
|
+
thread_id=thread_id,
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
return sent, False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ProgressEdits:
|
|
154
|
+
def __init__(
|
|
155
|
+
self,
|
|
156
|
+
*,
|
|
157
|
+
transport: Transport,
|
|
158
|
+
presenter: Presenter,
|
|
159
|
+
channel_id: ChannelId,
|
|
160
|
+
progress_ref: MessageRef | None,
|
|
161
|
+
tracker: ProgressTracker,
|
|
162
|
+
started_at: float,
|
|
163
|
+
clock: Callable[[], float],
|
|
164
|
+
last_rendered: RenderedMessage | None,
|
|
165
|
+
resume_formatter: Callable[[ResumeToken], str] | None = None,
|
|
166
|
+
label: str = "working",
|
|
167
|
+
context_line: str | None = None,
|
|
168
|
+
) -> None:
|
|
169
|
+
self.transport = transport
|
|
170
|
+
self.presenter = presenter
|
|
171
|
+
self.channel_id = channel_id
|
|
172
|
+
self.progress_ref = progress_ref
|
|
173
|
+
self.tracker = tracker
|
|
174
|
+
self.started_at = started_at
|
|
175
|
+
self.clock = clock
|
|
176
|
+
self.last_rendered = last_rendered
|
|
177
|
+
self.resume_formatter = resume_formatter
|
|
178
|
+
self.label = label
|
|
179
|
+
self.context_line = context_line
|
|
180
|
+
self.event_seq = 0
|
|
181
|
+
self.rendered_seq = 0
|
|
182
|
+
self.signal_send, self.signal_recv = anyio.create_memory_object_stream(1)
|
|
183
|
+
|
|
184
|
+
async def run(self) -> None:
|
|
185
|
+
if self.progress_ref is None:
|
|
186
|
+
return
|
|
187
|
+
while True:
|
|
188
|
+
while self.rendered_seq == self.event_seq:
|
|
189
|
+
try:
|
|
190
|
+
await self.signal_recv.receive()
|
|
191
|
+
except anyio.EndOfStream:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
seq_at_render = self.event_seq
|
|
195
|
+
now = self.clock()
|
|
196
|
+
state = self.tracker.snapshot(
|
|
197
|
+
resume_formatter=self.resume_formatter,
|
|
198
|
+
context_line=self.context_line,
|
|
199
|
+
)
|
|
200
|
+
rendered = self.presenter.render_progress(
|
|
201
|
+
state, elapsed_s=now - self.started_at, label=self.label
|
|
202
|
+
)
|
|
203
|
+
if rendered != self.last_rendered:
|
|
204
|
+
logger.debug(
|
|
205
|
+
"transport.edit_message",
|
|
206
|
+
channel_id=self.channel_id,
|
|
207
|
+
message_id=self.progress_ref.message_id,
|
|
208
|
+
rendered=rendered.text,
|
|
209
|
+
)
|
|
210
|
+
edited = await self.transport.edit(
|
|
211
|
+
ref=self.progress_ref,
|
|
212
|
+
message=rendered,
|
|
213
|
+
wait=False,
|
|
214
|
+
)
|
|
215
|
+
if edited is not None:
|
|
216
|
+
self.last_rendered = rendered
|
|
217
|
+
|
|
218
|
+
self.rendered_seq = seq_at_render
|
|
219
|
+
|
|
220
|
+
async def on_event(self, evt: TakopiEvent) -> None:
|
|
221
|
+
if not self.tracker.note_event(evt):
|
|
222
|
+
return
|
|
223
|
+
if self.progress_ref is None:
|
|
224
|
+
return
|
|
225
|
+
self.event_seq += 1
|
|
226
|
+
try:
|
|
227
|
+
self.signal_send.send_nowait(None)
|
|
228
|
+
except anyio.WouldBlock:
|
|
229
|
+
pass
|
|
230
|
+
except (anyio.BrokenResourceError, anyio.ClosedResourceError):
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclass(frozen=True, slots=True)
|
|
235
|
+
class ProgressMessageState:
|
|
236
|
+
ref: MessageRef | None
|
|
237
|
+
last_rendered: RenderedMessage | None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
async def send_initial_progress(
|
|
241
|
+
cfg: ExecBridgeConfig,
|
|
242
|
+
*,
|
|
243
|
+
channel_id: ChannelId,
|
|
244
|
+
reply_to: MessageRef,
|
|
245
|
+
label: str,
|
|
246
|
+
tracker: ProgressTracker,
|
|
247
|
+
progress_ref: MessageRef | None = None,
|
|
248
|
+
resume_formatter: Callable[[ResumeToken], str] | None = None,
|
|
249
|
+
context_line: str | None = None,
|
|
250
|
+
thread_id: ThreadId | None = None,
|
|
251
|
+
) -> ProgressMessageState:
|
|
252
|
+
last_rendered: RenderedMessage | None = None
|
|
253
|
+
|
|
254
|
+
state = tracker.snapshot(
|
|
255
|
+
resume_formatter=resume_formatter,
|
|
256
|
+
context_line=context_line,
|
|
257
|
+
)
|
|
258
|
+
initial_rendered = cfg.presenter.render_progress(
|
|
259
|
+
state,
|
|
260
|
+
elapsed_s=0.0,
|
|
261
|
+
label=label,
|
|
262
|
+
)
|
|
263
|
+
sent_ref, _ = await _send_or_edit_message(
|
|
264
|
+
cfg.transport,
|
|
265
|
+
channel_id=channel_id,
|
|
266
|
+
message=initial_rendered,
|
|
267
|
+
edit_ref=progress_ref,
|
|
268
|
+
reply_to=reply_to,
|
|
269
|
+
notify=False,
|
|
270
|
+
replace_ref=progress_ref,
|
|
271
|
+
thread_id=thread_id,
|
|
272
|
+
)
|
|
273
|
+
if sent_ref is not None:
|
|
274
|
+
last_rendered = initial_rendered
|
|
275
|
+
logger.debug(
|
|
276
|
+
"progress.sent",
|
|
277
|
+
channel_id=sent_ref.channel_id,
|
|
278
|
+
message_id=sent_ref.message_id,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
return ProgressMessageState(
|
|
282
|
+
ref=sent_ref,
|
|
283
|
+
last_rendered=last_rendered,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@dataclass(slots=True)
|
|
288
|
+
class RunOutcome:
|
|
289
|
+
cancelled: bool = False
|
|
290
|
+
completed: CompletedEvent | None = None
|
|
291
|
+
resume: ResumeToken | None = None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
async def run_runner_with_cancel(
|
|
295
|
+
runner: Runner,
|
|
296
|
+
*,
|
|
297
|
+
prompt: str,
|
|
298
|
+
resume_token: ResumeToken | None,
|
|
299
|
+
edits: ProgressEdits,
|
|
300
|
+
running_task: RunningTask | None,
|
|
301
|
+
on_thread_known: Callable[[ResumeToken, anyio.Event], Awaitable[None]] | None,
|
|
302
|
+
) -> RunOutcome:
|
|
303
|
+
outcome = RunOutcome()
|
|
304
|
+
async with anyio.create_task_group() as tg:
|
|
305
|
+
|
|
306
|
+
async def run_runner() -> None:
|
|
307
|
+
try:
|
|
308
|
+
async for evt in runner.run(prompt, resume_token):
|
|
309
|
+
_log_runner_event(evt)
|
|
310
|
+
if isinstance(evt, StartedEvent):
|
|
311
|
+
outcome.resume = evt.resume
|
|
312
|
+
bind_run_context(resume=evt.resume.value)
|
|
313
|
+
if running_task is not None and running_task.resume is None:
|
|
314
|
+
running_task.resume = evt.resume
|
|
315
|
+
try:
|
|
316
|
+
if on_thread_known is not None:
|
|
317
|
+
await on_thread_known(evt.resume, running_task.done)
|
|
318
|
+
finally:
|
|
319
|
+
running_task.resume_ready.set()
|
|
320
|
+
elif isinstance(evt, CompletedEvent):
|
|
321
|
+
outcome.resume = evt.resume or outcome.resume
|
|
322
|
+
outcome.completed = evt
|
|
323
|
+
await edits.on_event(evt)
|
|
324
|
+
finally:
|
|
325
|
+
tg.cancel_scope.cancel()
|
|
326
|
+
|
|
327
|
+
async def wait_cancel(task: RunningTask) -> None:
|
|
328
|
+
await task.cancel_requested.wait()
|
|
329
|
+
outcome.cancelled = True
|
|
330
|
+
tg.cancel_scope.cancel()
|
|
331
|
+
|
|
332
|
+
tg.start_soon(run_runner)
|
|
333
|
+
if running_task is not None:
|
|
334
|
+
tg.start_soon(wait_cancel, running_task)
|
|
335
|
+
|
|
336
|
+
return outcome
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def sync_resume_token(
|
|
340
|
+
tracker: ProgressTracker, resume: ResumeToken | None
|
|
341
|
+
) -> ResumeToken | None:
|
|
342
|
+
resume = resume or tracker.resume
|
|
343
|
+
tracker.set_resume(resume)
|
|
344
|
+
return resume
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
async def send_result_message(
|
|
348
|
+
cfg: ExecBridgeConfig,
|
|
349
|
+
*,
|
|
350
|
+
channel_id: ChannelId,
|
|
351
|
+
reply_to: MessageRef,
|
|
352
|
+
progress_ref: MessageRef | None,
|
|
353
|
+
message: RenderedMessage,
|
|
354
|
+
notify: bool,
|
|
355
|
+
edit_ref: MessageRef | None,
|
|
356
|
+
replace_ref: MessageRef | None = None,
|
|
357
|
+
delete_tag: str = "final",
|
|
358
|
+
thread_id: ThreadId | None = None,
|
|
359
|
+
) -> None:
|
|
360
|
+
final_msg, edited = await _send_or_edit_message(
|
|
361
|
+
cfg.transport,
|
|
362
|
+
channel_id=channel_id,
|
|
363
|
+
message=message,
|
|
364
|
+
edit_ref=edit_ref,
|
|
365
|
+
reply_to=reply_to,
|
|
366
|
+
notify=notify,
|
|
367
|
+
replace_ref=replace_ref,
|
|
368
|
+
thread_id=thread_id,
|
|
369
|
+
)
|
|
370
|
+
if final_msg is None:
|
|
371
|
+
return
|
|
372
|
+
if (
|
|
373
|
+
progress_ref is not None
|
|
374
|
+
and (edit_ref is None or not edited)
|
|
375
|
+
and replace_ref is None
|
|
376
|
+
):
|
|
377
|
+
logger.debug(
|
|
378
|
+
"transport.delete_message",
|
|
379
|
+
channel_id=progress_ref.channel_id,
|
|
380
|
+
message_id=progress_ref.message_id,
|
|
381
|
+
tag=delete_tag,
|
|
382
|
+
)
|
|
383
|
+
await cfg.transport.delete(ref=progress_ref)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
async def handle_message(
|
|
387
|
+
cfg: ExecBridgeConfig,
|
|
388
|
+
*,
|
|
389
|
+
runner: Runner,
|
|
390
|
+
incoming: IncomingMessage,
|
|
391
|
+
resume_token: ResumeToken | None,
|
|
392
|
+
context: RunContext | None = None,
|
|
393
|
+
context_line: str | None = None,
|
|
394
|
+
strip_resume_line: Callable[[str], bool] | None = None,
|
|
395
|
+
running_tasks: RunningTasks | None = None,
|
|
396
|
+
on_thread_known: Callable[[ResumeToken, anyio.Event], Awaitable[None]]
|
|
397
|
+
| None = None,
|
|
398
|
+
progress_ref: MessageRef | None = None,
|
|
399
|
+
clock: Callable[[], float] = time.monotonic,
|
|
400
|
+
) -> None:
|
|
401
|
+
logger.info(
|
|
402
|
+
"handle.incoming",
|
|
403
|
+
channel_id=incoming.channel_id,
|
|
404
|
+
user_msg_id=incoming.message_id,
|
|
405
|
+
resume=resume_token.value if resume_token else None,
|
|
406
|
+
text=incoming.text,
|
|
407
|
+
)
|
|
408
|
+
started_at = clock()
|
|
409
|
+
is_resume_line = runner.is_resume_line
|
|
410
|
+
resume_strip = strip_resume_line or is_resume_line
|
|
411
|
+
runner_text = _strip_resume_lines(incoming.text, is_resume_line=resume_strip)
|
|
412
|
+
|
|
413
|
+
progress_tracker = ProgressTracker(engine=runner.engine)
|
|
414
|
+
|
|
415
|
+
user_ref = MessageRef(
|
|
416
|
+
channel_id=incoming.channel_id,
|
|
417
|
+
message_id=incoming.message_id,
|
|
418
|
+
)
|
|
419
|
+
progress_state = await send_initial_progress(
|
|
420
|
+
cfg,
|
|
421
|
+
channel_id=incoming.channel_id,
|
|
422
|
+
reply_to=user_ref,
|
|
423
|
+
label="starting",
|
|
424
|
+
tracker=progress_tracker,
|
|
425
|
+
progress_ref=progress_ref,
|
|
426
|
+
resume_formatter=runner.format_resume,
|
|
427
|
+
context_line=context_line,
|
|
428
|
+
thread_id=incoming.thread_id,
|
|
429
|
+
)
|
|
430
|
+
progress_ref = progress_state.ref
|
|
431
|
+
|
|
432
|
+
edits = ProgressEdits(
|
|
433
|
+
transport=cfg.transport,
|
|
434
|
+
presenter=cfg.presenter,
|
|
435
|
+
channel_id=incoming.channel_id,
|
|
436
|
+
progress_ref=progress_ref,
|
|
437
|
+
tracker=progress_tracker,
|
|
438
|
+
started_at=started_at,
|
|
439
|
+
clock=clock,
|
|
440
|
+
last_rendered=progress_state.last_rendered,
|
|
441
|
+
resume_formatter=runner.format_resume,
|
|
442
|
+
context_line=context_line,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
running_task: RunningTask | None = None
|
|
446
|
+
if running_tasks is not None and progress_ref is not None:
|
|
447
|
+
running_task = RunningTask(context=context)
|
|
448
|
+
running_tasks[progress_ref] = running_task
|
|
449
|
+
|
|
450
|
+
cancel_exc_type = anyio.get_cancelled_exc_class()
|
|
451
|
+
edits_scope = anyio.CancelScope()
|
|
452
|
+
|
|
453
|
+
async def run_edits() -> None:
|
|
454
|
+
try:
|
|
455
|
+
with edits_scope:
|
|
456
|
+
await edits.run()
|
|
457
|
+
except cancel_exc_type:
|
|
458
|
+
# Edits are best-effort; cancellation should not bubble into the task group.
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
outcome = RunOutcome()
|
|
462
|
+
error: Exception | None = None
|
|
463
|
+
|
|
464
|
+
async with anyio.create_task_group() as tg:
|
|
465
|
+
if progress_ref is not None:
|
|
466
|
+
tg.start_soon(run_edits)
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
outcome = await run_runner_with_cancel(
|
|
470
|
+
runner,
|
|
471
|
+
prompt=runner_text,
|
|
472
|
+
resume_token=resume_token,
|
|
473
|
+
edits=edits,
|
|
474
|
+
running_task=running_task,
|
|
475
|
+
on_thread_known=on_thread_known,
|
|
476
|
+
)
|
|
477
|
+
except Exception as exc:
|
|
478
|
+
error = exc
|
|
479
|
+
logger.exception(
|
|
480
|
+
"handle.runner_failed",
|
|
481
|
+
error=str(exc),
|
|
482
|
+
error_type=exc.__class__.__name__,
|
|
483
|
+
)
|
|
484
|
+
finally:
|
|
485
|
+
if running_task is not None and running_tasks is not None:
|
|
486
|
+
running_task.done.set()
|
|
487
|
+
if progress_ref is not None:
|
|
488
|
+
running_tasks.pop(progress_ref, None)
|
|
489
|
+
if not outcome.cancelled and error is None:
|
|
490
|
+
# Give pending progress edits a chance to flush if they're ready.
|
|
491
|
+
await anyio.sleep(0)
|
|
492
|
+
edits_scope.cancel()
|
|
493
|
+
|
|
494
|
+
elapsed = clock() - started_at
|
|
495
|
+
|
|
496
|
+
if error is not None:
|
|
497
|
+
sync_resume_token(progress_tracker, outcome.resume)
|
|
498
|
+
err_body = _format_error(error)
|
|
499
|
+
state = progress_tracker.snapshot(
|
|
500
|
+
resume_formatter=runner.format_resume,
|
|
501
|
+
context_line=context_line,
|
|
502
|
+
)
|
|
503
|
+
final_rendered = cfg.presenter.render_final(
|
|
504
|
+
state,
|
|
505
|
+
elapsed_s=elapsed,
|
|
506
|
+
status="error",
|
|
507
|
+
answer=err_body,
|
|
508
|
+
)
|
|
509
|
+
logger.debug(
|
|
510
|
+
"handle.error.rendered",
|
|
511
|
+
error=err_body,
|
|
512
|
+
rendered=final_rendered.text,
|
|
513
|
+
)
|
|
514
|
+
await send_result_message(
|
|
515
|
+
cfg,
|
|
516
|
+
channel_id=incoming.channel_id,
|
|
517
|
+
reply_to=user_ref,
|
|
518
|
+
progress_ref=progress_ref,
|
|
519
|
+
message=final_rendered,
|
|
520
|
+
notify=False,
|
|
521
|
+
edit_ref=progress_ref,
|
|
522
|
+
replace_ref=progress_ref,
|
|
523
|
+
delete_tag="error",
|
|
524
|
+
thread_id=incoming.thread_id,
|
|
525
|
+
)
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
if outcome.cancelled:
|
|
529
|
+
resume = sync_resume_token(progress_tracker, outcome.resume)
|
|
530
|
+
logger.info(
|
|
531
|
+
"handle.cancelled",
|
|
532
|
+
resume=resume.value if resume else None,
|
|
533
|
+
elapsed_s=elapsed,
|
|
534
|
+
)
|
|
535
|
+
state = progress_tracker.snapshot(
|
|
536
|
+
resume_formatter=runner.format_resume,
|
|
537
|
+
context_line=context_line,
|
|
538
|
+
)
|
|
539
|
+
final_rendered = cfg.presenter.render_progress(
|
|
540
|
+
state,
|
|
541
|
+
elapsed_s=elapsed,
|
|
542
|
+
label="`cancelled`",
|
|
543
|
+
)
|
|
544
|
+
await send_result_message(
|
|
545
|
+
cfg,
|
|
546
|
+
channel_id=incoming.channel_id,
|
|
547
|
+
reply_to=user_ref,
|
|
548
|
+
progress_ref=progress_ref,
|
|
549
|
+
message=final_rendered,
|
|
550
|
+
notify=False,
|
|
551
|
+
edit_ref=progress_ref,
|
|
552
|
+
replace_ref=progress_ref,
|
|
553
|
+
delete_tag="cancel",
|
|
554
|
+
thread_id=incoming.thread_id,
|
|
555
|
+
)
|
|
556
|
+
return
|
|
557
|
+
|
|
558
|
+
if outcome.completed is None:
|
|
559
|
+
raise RuntimeError("runner finished without a completed event")
|
|
560
|
+
|
|
561
|
+
completed = outcome.completed
|
|
562
|
+
run_ok = completed.ok
|
|
563
|
+
run_error = completed.error
|
|
564
|
+
|
|
565
|
+
final_answer = completed.answer
|
|
566
|
+
if run_ok is False and run_error:
|
|
567
|
+
if final_answer.strip():
|
|
568
|
+
final_answer = f"{final_answer}\n\n{run_error}"
|
|
569
|
+
else:
|
|
570
|
+
final_answer = str(run_error)
|
|
571
|
+
|
|
572
|
+
status = (
|
|
573
|
+
"error" if run_ok is False else ("done" if final_answer.strip() else "error")
|
|
574
|
+
)
|
|
575
|
+
resume_value = None
|
|
576
|
+
resume_token = completed.resume or outcome.resume
|
|
577
|
+
if resume_token is not None:
|
|
578
|
+
resume_value = resume_token.value
|
|
579
|
+
logger.info(
|
|
580
|
+
"runner.completed",
|
|
581
|
+
ok=run_ok,
|
|
582
|
+
error=run_error,
|
|
583
|
+
answer_len=len(final_answer or ""),
|
|
584
|
+
elapsed_s=round(elapsed, 2),
|
|
585
|
+
action_count=progress_tracker.action_count,
|
|
586
|
+
resume=resume_value,
|
|
587
|
+
)
|
|
588
|
+
sync_resume_token(progress_tracker, completed.resume or outcome.resume)
|
|
589
|
+
state = progress_tracker.snapshot(
|
|
590
|
+
resume_formatter=runner.format_resume,
|
|
591
|
+
context_line=context_line,
|
|
592
|
+
)
|
|
593
|
+
final_rendered = cfg.presenter.render_final(
|
|
594
|
+
state,
|
|
595
|
+
elapsed_s=elapsed,
|
|
596
|
+
status=status,
|
|
597
|
+
answer=final_answer,
|
|
598
|
+
)
|
|
599
|
+
logger.debug(
|
|
600
|
+
"handle.final.rendered",
|
|
601
|
+
rendered=final_rendered.text,
|
|
602
|
+
status=status,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
can_edit_final = progress_ref is not None
|
|
606
|
+
edit_ref = None if cfg.final_notify or not can_edit_final else progress_ref
|
|
607
|
+
|
|
608
|
+
await send_result_message(
|
|
609
|
+
cfg,
|
|
610
|
+
channel_id=incoming.channel_id,
|
|
611
|
+
reply_to=user_ref,
|
|
612
|
+
progress_ref=progress_ref,
|
|
613
|
+
message=final_rendered,
|
|
614
|
+
notify=cfg.final_notify,
|
|
615
|
+
edit_ref=edit_ref,
|
|
616
|
+
replace_ref=progress_ref,
|
|
617
|
+
delete_tag="final",
|
|
618
|
+
thread_id=incoming.thread_id,
|
|
619
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Runner implementations."""
|