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/runners/claude.py
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import msgspec
|
|
11
|
+
|
|
12
|
+
from ..backends import EngineBackend, EngineConfig
|
|
13
|
+
from ..events import EventFactory
|
|
14
|
+
from ..logging import get_logger
|
|
15
|
+
from ..model import Action, ActionKind, EngineId, ResumeToken, TakopiEvent
|
|
16
|
+
from ..runner import JsonlSubprocessRunner, ResumeTokenMixin, Runner
|
|
17
|
+
from .run_options import get_run_options
|
|
18
|
+
from ..schemas import claude as claude_schema
|
|
19
|
+
from .tool_actions import tool_input_path, tool_kind_and_title
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
ENGINE: EngineId = "claude"
|
|
24
|
+
DEFAULT_ALLOWED_TOOLS = ["Bash", "Read", "Edit", "Write"]
|
|
25
|
+
|
|
26
|
+
_RESUME_RE = re.compile(
|
|
27
|
+
r"(?im)^\s*`?claude\s+(?:--resume|-r)\s+(?P<token>[^`\s]+)`?\s*$"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True)
|
|
32
|
+
class ClaudeStreamState:
|
|
33
|
+
factory: EventFactory = field(default_factory=lambda: EventFactory(ENGINE))
|
|
34
|
+
pending_actions: dict[str, Action] = field(default_factory=dict)
|
|
35
|
+
last_assistant_text: str | None = None
|
|
36
|
+
note_seq: int = 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _normalize_tool_result(content: Any) -> str:
|
|
40
|
+
if content is None:
|
|
41
|
+
return ""
|
|
42
|
+
if isinstance(content, str):
|
|
43
|
+
return content
|
|
44
|
+
if isinstance(content, list):
|
|
45
|
+
parts: list[str] = []
|
|
46
|
+
for item in content:
|
|
47
|
+
if isinstance(item, dict):
|
|
48
|
+
text = item.get("text")
|
|
49
|
+
if isinstance(text, str) and text:
|
|
50
|
+
parts.append(text)
|
|
51
|
+
elif isinstance(item, str):
|
|
52
|
+
parts.append(item)
|
|
53
|
+
return "\n".join(part for part in parts if part)
|
|
54
|
+
if isinstance(content, dict):
|
|
55
|
+
text = content.get("text")
|
|
56
|
+
if isinstance(text, str):
|
|
57
|
+
return text
|
|
58
|
+
return str(content)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _coerce_comma_list(value: Any) -> str | None:
|
|
62
|
+
if value is None:
|
|
63
|
+
return None
|
|
64
|
+
if isinstance(value, (list, tuple, set)):
|
|
65
|
+
parts = [str(item) for item in value if item is not None]
|
|
66
|
+
joined = ",".join(part for part in parts if part)
|
|
67
|
+
return joined or None
|
|
68
|
+
text = str(value)
|
|
69
|
+
return text or None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _tool_kind_and_title(
|
|
73
|
+
name: str, tool_input: dict[str, Any]
|
|
74
|
+
) -> tuple[ActionKind, str]:
|
|
75
|
+
return tool_kind_and_title(name, tool_input, path_keys=("file_path", "path"))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _tool_action(
|
|
79
|
+
content: claude_schema.StreamToolUseBlock,
|
|
80
|
+
*,
|
|
81
|
+
parent_tool_use_id: str | None,
|
|
82
|
+
) -> Action:
|
|
83
|
+
tool_id = content.id
|
|
84
|
+
tool_name = str(content.name or "tool")
|
|
85
|
+
tool_input = content.input
|
|
86
|
+
|
|
87
|
+
kind, title = _tool_kind_and_title(tool_name, tool_input)
|
|
88
|
+
|
|
89
|
+
detail: dict[str, Any] = {
|
|
90
|
+
"name": tool_name,
|
|
91
|
+
"input": tool_input,
|
|
92
|
+
}
|
|
93
|
+
if parent_tool_use_id:
|
|
94
|
+
detail["parent_tool_use_id"] = parent_tool_use_id
|
|
95
|
+
|
|
96
|
+
if kind == "file_change":
|
|
97
|
+
path = tool_input_path(tool_input, path_keys=("file_path", "path"))
|
|
98
|
+
if path:
|
|
99
|
+
detail["changes"] = [{"path": path, "kind": "update"}]
|
|
100
|
+
|
|
101
|
+
return Action(id=tool_id, kind=kind, title=title, detail=detail)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _tool_result_event(
|
|
105
|
+
content: claude_schema.StreamToolResultBlock,
|
|
106
|
+
*,
|
|
107
|
+
action: Action,
|
|
108
|
+
factory: EventFactory,
|
|
109
|
+
) -> TakopiEvent:
|
|
110
|
+
is_error = content.is_error is True
|
|
111
|
+
raw_result = content.content
|
|
112
|
+
normalized = _normalize_tool_result(raw_result)
|
|
113
|
+
preview = normalized
|
|
114
|
+
|
|
115
|
+
detail = action.detail | {
|
|
116
|
+
"tool_use_id": content.tool_use_id,
|
|
117
|
+
"result_preview": preview,
|
|
118
|
+
"result_len": len(normalized),
|
|
119
|
+
"is_error": is_error,
|
|
120
|
+
}
|
|
121
|
+
return factory.action_completed(
|
|
122
|
+
action_id=action.id,
|
|
123
|
+
kind=action.kind,
|
|
124
|
+
title=action.title,
|
|
125
|
+
ok=not is_error,
|
|
126
|
+
detail=detail,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _extract_error(event: claude_schema.StreamResultMessage) -> str | None:
|
|
131
|
+
if event.is_error:
|
|
132
|
+
if isinstance(event.result, str) and event.result:
|
|
133
|
+
return event.result
|
|
134
|
+
subtype = event.subtype
|
|
135
|
+
if subtype:
|
|
136
|
+
return f"claude run failed ({subtype})"
|
|
137
|
+
return "claude run failed"
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _usage_payload(event: claude_schema.StreamResultMessage) -> dict[str, Any]:
|
|
142
|
+
usage: dict[str, Any] = {}
|
|
143
|
+
for key in (
|
|
144
|
+
"total_cost_usd",
|
|
145
|
+
"duration_ms",
|
|
146
|
+
"duration_api_ms",
|
|
147
|
+
"num_turns",
|
|
148
|
+
):
|
|
149
|
+
value = getattr(event, key, None)
|
|
150
|
+
if value is not None:
|
|
151
|
+
usage[key] = value
|
|
152
|
+
if event.usage is not None:
|
|
153
|
+
usage["usage"] = event.usage
|
|
154
|
+
return usage
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def translate_claude_event(
|
|
158
|
+
event: claude_schema.StreamJsonMessage,
|
|
159
|
+
*,
|
|
160
|
+
title: str,
|
|
161
|
+
state: ClaudeStreamState,
|
|
162
|
+
factory: EventFactory,
|
|
163
|
+
) -> list[TakopiEvent]:
|
|
164
|
+
match event:
|
|
165
|
+
case claude_schema.StreamSystemMessage(subtype=subtype):
|
|
166
|
+
if subtype != "init":
|
|
167
|
+
return []
|
|
168
|
+
session_id = event.session_id
|
|
169
|
+
if not session_id:
|
|
170
|
+
return []
|
|
171
|
+
meta: dict[str, Any] = {}
|
|
172
|
+
for key in (
|
|
173
|
+
"cwd",
|
|
174
|
+
"tools",
|
|
175
|
+
"permissionMode",
|
|
176
|
+
"output_style",
|
|
177
|
+
"apiKeySource",
|
|
178
|
+
"mcp_servers",
|
|
179
|
+
):
|
|
180
|
+
value = getattr(event, key, None)
|
|
181
|
+
if value is not None:
|
|
182
|
+
meta[key] = value
|
|
183
|
+
model = event.model
|
|
184
|
+
token = ResumeToken(engine=ENGINE, value=session_id)
|
|
185
|
+
event_title = str(model) if isinstance(model, str) and model else title
|
|
186
|
+
return [factory.started(token, title=event_title, meta=meta or None)]
|
|
187
|
+
case claude_schema.StreamAssistantMessage(
|
|
188
|
+
message=message, parent_tool_use_id=parent_tool_use_id
|
|
189
|
+
):
|
|
190
|
+
out: list[TakopiEvent] = []
|
|
191
|
+
for content in message.content:
|
|
192
|
+
match content:
|
|
193
|
+
case claude_schema.StreamToolUseBlock():
|
|
194
|
+
action = _tool_action(
|
|
195
|
+
content,
|
|
196
|
+
parent_tool_use_id=parent_tool_use_id,
|
|
197
|
+
)
|
|
198
|
+
state.pending_actions[action.id] = action
|
|
199
|
+
out.append(
|
|
200
|
+
factory.action_started(
|
|
201
|
+
action_id=action.id,
|
|
202
|
+
kind=action.kind,
|
|
203
|
+
title=action.title,
|
|
204
|
+
detail=action.detail,
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
case claude_schema.StreamThinkingBlock(
|
|
208
|
+
thinking=thinking, signature=signature
|
|
209
|
+
):
|
|
210
|
+
if not thinking:
|
|
211
|
+
continue
|
|
212
|
+
state.note_seq += 1
|
|
213
|
+
action_id = f"claude.thinking.{state.note_seq}"
|
|
214
|
+
detail: dict[str, Any] = {}
|
|
215
|
+
if parent_tool_use_id:
|
|
216
|
+
detail["parent_tool_use_id"] = parent_tool_use_id
|
|
217
|
+
if signature:
|
|
218
|
+
detail["signature"] = signature
|
|
219
|
+
out.append(
|
|
220
|
+
factory.action_completed(
|
|
221
|
+
action_id=action_id,
|
|
222
|
+
kind="note",
|
|
223
|
+
title=thinking,
|
|
224
|
+
ok=True,
|
|
225
|
+
detail=detail,
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
case claude_schema.StreamTextBlock(text=text):
|
|
229
|
+
if text:
|
|
230
|
+
state.last_assistant_text = text
|
|
231
|
+
case _:
|
|
232
|
+
continue
|
|
233
|
+
return out
|
|
234
|
+
case claude_schema.StreamUserMessage(message=message):
|
|
235
|
+
if not isinstance(message.content, list):
|
|
236
|
+
return []
|
|
237
|
+
out: list[TakopiEvent] = []
|
|
238
|
+
for content in message.content:
|
|
239
|
+
if not isinstance(content, claude_schema.StreamToolResultBlock):
|
|
240
|
+
continue
|
|
241
|
+
tool_use_id = content.tool_use_id
|
|
242
|
+
action = state.pending_actions.pop(tool_use_id, None)
|
|
243
|
+
if action is None:
|
|
244
|
+
action = Action(
|
|
245
|
+
id=tool_use_id,
|
|
246
|
+
kind="tool",
|
|
247
|
+
title="tool result",
|
|
248
|
+
detail={},
|
|
249
|
+
)
|
|
250
|
+
out.append(
|
|
251
|
+
_tool_result_event(
|
|
252
|
+
content,
|
|
253
|
+
action=action,
|
|
254
|
+
factory=factory,
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
return out
|
|
258
|
+
case claude_schema.StreamResultMessage():
|
|
259
|
+
ok = not event.is_error
|
|
260
|
+
result_text = event.result or ""
|
|
261
|
+
if ok and not result_text and state.last_assistant_text:
|
|
262
|
+
result_text = state.last_assistant_text
|
|
263
|
+
|
|
264
|
+
resume = ResumeToken(engine=ENGINE, value=event.session_id)
|
|
265
|
+
error = None if ok else _extract_error(event)
|
|
266
|
+
usage = _usage_payload(event)
|
|
267
|
+
|
|
268
|
+
return [
|
|
269
|
+
factory.completed(
|
|
270
|
+
ok=ok,
|
|
271
|
+
answer=result_text,
|
|
272
|
+
resume=resume,
|
|
273
|
+
error=error,
|
|
274
|
+
usage=usage or None,
|
|
275
|
+
)
|
|
276
|
+
]
|
|
277
|
+
case _:
|
|
278
|
+
return []
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@dataclass(slots=True)
|
|
282
|
+
class ClaudeRunner(ResumeTokenMixin, JsonlSubprocessRunner):
|
|
283
|
+
engine: EngineId = ENGINE
|
|
284
|
+
resume_re: re.Pattern[str] = _RESUME_RE
|
|
285
|
+
|
|
286
|
+
claude_cmd: str = "claude"
|
|
287
|
+
model: str | None = None
|
|
288
|
+
allowed_tools: list[str] | None = None
|
|
289
|
+
dangerously_skip_permissions: bool = False
|
|
290
|
+
use_api_billing: bool = False
|
|
291
|
+
session_title: str = "claude"
|
|
292
|
+
logger = logger
|
|
293
|
+
|
|
294
|
+
def format_resume(self, token: ResumeToken) -> str:
|
|
295
|
+
if token.engine != ENGINE:
|
|
296
|
+
raise RuntimeError(f"resume token is for engine {token.engine!r}")
|
|
297
|
+
return f"`claude --resume {token.value}`"
|
|
298
|
+
|
|
299
|
+
def _build_args(self, prompt: str, resume: ResumeToken | None) -> list[str]:
|
|
300
|
+
run_options = get_run_options()
|
|
301
|
+
args: list[str] = ["-p", "--output-format", "stream-json", "--verbose"]
|
|
302
|
+
if resume is not None:
|
|
303
|
+
args.extend(["--resume", resume.value])
|
|
304
|
+
model = self.model
|
|
305
|
+
if run_options is not None and run_options.model:
|
|
306
|
+
model = run_options.model
|
|
307
|
+
if model is not None:
|
|
308
|
+
args.extend(["--model", str(model)])
|
|
309
|
+
allowed_tools = _coerce_comma_list(self.allowed_tools)
|
|
310
|
+
if allowed_tools is not None:
|
|
311
|
+
args.extend(["--allowedTools", allowed_tools])
|
|
312
|
+
if self.dangerously_skip_permissions is True:
|
|
313
|
+
args.append("--dangerously-skip-permissions")
|
|
314
|
+
args.append("--")
|
|
315
|
+
args.append(prompt)
|
|
316
|
+
return args
|
|
317
|
+
|
|
318
|
+
def command(self) -> str:
|
|
319
|
+
return self.claude_cmd
|
|
320
|
+
|
|
321
|
+
def build_args(
|
|
322
|
+
self,
|
|
323
|
+
prompt: str,
|
|
324
|
+
resume: ResumeToken | None,
|
|
325
|
+
*,
|
|
326
|
+
state: Any,
|
|
327
|
+
) -> list[str]:
|
|
328
|
+
return self._build_args(prompt, resume)
|
|
329
|
+
|
|
330
|
+
def stdin_payload(
|
|
331
|
+
self,
|
|
332
|
+
prompt: str,
|
|
333
|
+
resume: ResumeToken | None,
|
|
334
|
+
*,
|
|
335
|
+
state: Any,
|
|
336
|
+
) -> bytes | None:
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
def env(self, *, state: Any) -> dict[str, str] | None:
|
|
340
|
+
if self.use_api_billing is not True:
|
|
341
|
+
env = dict(os.environ)
|
|
342
|
+
env.pop("ANTHROPIC_API_KEY", None)
|
|
343
|
+
return env
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
def new_state(self, prompt: str, resume: ResumeToken | None) -> ClaudeStreamState:
|
|
347
|
+
return ClaudeStreamState()
|
|
348
|
+
|
|
349
|
+
def start_run(
|
|
350
|
+
self,
|
|
351
|
+
prompt: str,
|
|
352
|
+
resume: ResumeToken | None,
|
|
353
|
+
*,
|
|
354
|
+
state: ClaudeStreamState,
|
|
355
|
+
) -> None:
|
|
356
|
+
pass
|
|
357
|
+
|
|
358
|
+
def decode_jsonl(
|
|
359
|
+
self,
|
|
360
|
+
*,
|
|
361
|
+
line: bytes,
|
|
362
|
+
) -> claude_schema.StreamJsonMessage:
|
|
363
|
+
return claude_schema.decode_stream_json_line(line)
|
|
364
|
+
|
|
365
|
+
def decode_error_events(
|
|
366
|
+
self,
|
|
367
|
+
*,
|
|
368
|
+
raw: str,
|
|
369
|
+
line: str,
|
|
370
|
+
error: Exception,
|
|
371
|
+
state: ClaudeStreamState,
|
|
372
|
+
) -> list[TakopiEvent]:
|
|
373
|
+
if isinstance(error, msgspec.DecodeError):
|
|
374
|
+
self.get_logger().warning(
|
|
375
|
+
"jsonl.msgspec.invalid",
|
|
376
|
+
tag=self.tag(),
|
|
377
|
+
error=str(error),
|
|
378
|
+
error_type=error.__class__.__name__,
|
|
379
|
+
)
|
|
380
|
+
return []
|
|
381
|
+
return super().decode_error_events(
|
|
382
|
+
raw=raw,
|
|
383
|
+
line=line,
|
|
384
|
+
error=error,
|
|
385
|
+
state=state,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
def invalid_json_events(
|
|
389
|
+
self,
|
|
390
|
+
*,
|
|
391
|
+
raw: str,
|
|
392
|
+
line: str,
|
|
393
|
+
state: ClaudeStreamState,
|
|
394
|
+
) -> list[TakopiEvent]:
|
|
395
|
+
return []
|
|
396
|
+
|
|
397
|
+
def translate(
|
|
398
|
+
self,
|
|
399
|
+
data: claude_schema.StreamJsonMessage,
|
|
400
|
+
*,
|
|
401
|
+
state: ClaudeStreamState,
|
|
402
|
+
resume: ResumeToken | None,
|
|
403
|
+
found_session: ResumeToken | None,
|
|
404
|
+
) -> list[TakopiEvent]:
|
|
405
|
+
return translate_claude_event(
|
|
406
|
+
data,
|
|
407
|
+
title=self.session_title,
|
|
408
|
+
state=state,
|
|
409
|
+
factory=state.factory,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
def process_error_events(
|
|
413
|
+
self,
|
|
414
|
+
rc: int,
|
|
415
|
+
*,
|
|
416
|
+
resume: ResumeToken | None,
|
|
417
|
+
found_session: ResumeToken | None,
|
|
418
|
+
state: ClaudeStreamState,
|
|
419
|
+
) -> list[TakopiEvent]:
|
|
420
|
+
message = f"claude failed (rc={rc})."
|
|
421
|
+
resume_for_completed = found_session or resume
|
|
422
|
+
return [
|
|
423
|
+
self.note_event(message, state=state, ok=False),
|
|
424
|
+
state.factory.completed_error(
|
|
425
|
+
error=message,
|
|
426
|
+
resume=resume_for_completed,
|
|
427
|
+
),
|
|
428
|
+
]
|
|
429
|
+
|
|
430
|
+
def stream_end_events(
|
|
431
|
+
self,
|
|
432
|
+
*,
|
|
433
|
+
resume: ResumeToken | None,
|
|
434
|
+
found_session: ResumeToken | None,
|
|
435
|
+
state: ClaudeStreamState,
|
|
436
|
+
) -> list[TakopiEvent]:
|
|
437
|
+
if not found_session:
|
|
438
|
+
message = "claude finished but no session_id was captured"
|
|
439
|
+
resume_for_completed = resume
|
|
440
|
+
return [
|
|
441
|
+
state.factory.completed_error(
|
|
442
|
+
error=message,
|
|
443
|
+
resume=resume_for_completed,
|
|
444
|
+
)
|
|
445
|
+
]
|
|
446
|
+
|
|
447
|
+
message = "claude finished without a result event"
|
|
448
|
+
return [
|
|
449
|
+
state.factory.completed_error(
|
|
450
|
+
error=message,
|
|
451
|
+
answer=state.last_assistant_text or "",
|
|
452
|
+
resume=found_session,
|
|
453
|
+
)
|
|
454
|
+
]
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def build_runner(config: EngineConfig, _config_path: Path) -> Runner:
|
|
458
|
+
claude_cmd = shutil.which("claude") or "claude"
|
|
459
|
+
|
|
460
|
+
model = config.get("model")
|
|
461
|
+
if "allowed_tools" in config:
|
|
462
|
+
allowed_tools = config.get("allowed_tools")
|
|
463
|
+
else:
|
|
464
|
+
allowed_tools = DEFAULT_ALLOWED_TOOLS
|
|
465
|
+
dangerously_skip_permissions = config.get("dangerously_skip_permissions") is True
|
|
466
|
+
use_api_billing = config.get("use_api_billing") is True
|
|
467
|
+
title = str(model) if model is not None else "claude"
|
|
468
|
+
|
|
469
|
+
return ClaudeRunner(
|
|
470
|
+
claude_cmd=claude_cmd,
|
|
471
|
+
model=model,
|
|
472
|
+
allowed_tools=allowed_tools,
|
|
473
|
+
dangerously_skip_permissions=dangerously_skip_permissions,
|
|
474
|
+
use_api_billing=use_api_billing,
|
|
475
|
+
session_title=title,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
BACKEND = EngineBackend(
|
|
480
|
+
id="claude",
|
|
481
|
+
build_runner=build_runner,
|
|
482
|
+
install_cmd="npm install -g @anthropic-ai/claude-code",
|
|
483
|
+
)
|