coderio 0.4.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 (97) hide show
  1. coderio/__init__.py +14 -0
  2. coderio/agent/__init__.py +5 -0
  3. coderio/agent/_deepagents_compat.py +139 -0
  4. coderio/agent/command_review.py +140 -0
  5. coderio/agent/deep_loop.py +758 -0
  6. coderio/agent/harness.py +768 -0
  7. coderio/agent/harness_middleware.py +293 -0
  8. coderio/agent/permission_middleware.py +52 -0
  9. coderio/agent/prompts.py +304 -0
  10. coderio/agent/skill_tool.py +70 -0
  11. coderio/agent/state.py +144 -0
  12. coderio/agent/stream.py +93 -0
  13. coderio/cli/__init__.py +0 -0
  14. coderio/cli/app.py +257 -0
  15. coderio/cli/commands.py +339 -0
  16. coderio/cli/credentials.py +70 -0
  17. coderio/cli/mcp_cmd.py +227 -0
  18. coderio/cli/multimodal.py +94 -0
  19. coderio/cli/onboarding.py +270 -0
  20. coderio/cli/providers.py +113 -0
  21. coderio/cli/render.py +28 -0
  22. coderio/cli/repl.py +189 -0
  23. coderio/cli/run_cmd.py +171 -0
  24. coderio/cli/skills_cmd.py +77 -0
  25. coderio/cli/stream.py +224 -0
  26. coderio/cli/tui.py +1584 -0
  27. coderio/cli/tui_onboarding.py +465 -0
  28. coderio/cli/tui_screens.py +334 -0
  29. coderio/cli/tui_widgets.py +453 -0
  30. coderio/config/__init__.py +21 -0
  31. coderio/config/bootstrap.py +38 -0
  32. coderio/config/loader.py +273 -0
  33. coderio/config/models.py +193 -0
  34. coderio/config/trust.py +136 -0
  35. coderio/llm/__init__.py +3 -0
  36. coderio/llm/factory.py +164 -0
  37. coderio/llm/probe.py +153 -0
  38. coderio/mcp_loader.py +274 -0
  39. coderio/session/__init__.py +3 -0
  40. coderio/session/message.py +79 -0
  41. coderio/session/store.py +244 -0
  42. coderio/skills/__init__.py +4 -0
  43. coderio/skills/bundled/executing-plans/SKILL.md +15 -0
  44. coderio/skills/lion-skills/LICENSE +21 -0
  45. coderio/skills/lion-skills/README.md +114 -0
  46. coderio/skills/lion-skills/skills/clarifying-questions/SKILL.md +119 -0
  47. coderio/skills/lion-skills/skills/clarifying-questions/evals/evals.json +40 -0
  48. coderio/skills/lion-skills/skills/code-review/SKILL.md +102 -0
  49. coderio/skills/lion-skills/skills/code-review/evals/evals.json +40 -0
  50. coderio/skills/lion-skills/skills/commit-message/SKILL.md +97 -0
  51. coderio/skills/lion-skills/skills/commit-message/evals/evals.json +20 -0
  52. coderio/skills/lion-skills/skills/debugging/SKILL.md +116 -0
  53. coderio/skills/lion-skills/skills/debugging/evals/evals.json +40 -0
  54. coderio/skills/lion-skills/skills/error-handling/SKILL.md +74 -0
  55. coderio/skills/lion-skills/skills/error-handling/evals/evals.json +20 -0
  56. coderio/skills/lion-skills/skills/lion-writing-skills/SKILL.md +85 -0
  57. coderio/skills/lion-skills/skills/lion-writing-skills/evals/evals.json +20 -0
  58. coderio/skills/lion-skills/skills/naming/SKILL.md +85 -0
  59. coderio/skills/lion-skills/skills/naming/evals/evals.json +20 -0
  60. coderio/skills/lion-skills/skills/onboarding-unknown-codebase/SKILL.md +95 -0
  61. coderio/skills/lion-skills/skills/onboarding-unknown-codebase/evals/evals.json +20 -0
  62. coderio/skills/lion-skills/skills/spec-writing/SKILL.md +154 -0
  63. coderio/skills/lion-skills/skills/spec-writing/evals/evals.json +40 -0
  64. coderio/skills/lion-skills/skills/task-breakdown/SKILL.md +134 -0
  65. coderio/skills/lion-skills/skills/task-breakdown/evals/evals.json +46 -0
  66. coderio/skills/lion-skills/skills/testing/SKILL.md +134 -0
  67. coderio/skills/lion-skills/skills/testing/evals/evals.json +40 -0
  68. coderio/skills/lion-skills/skills/verify-and-fix/SKILL.md +132 -0
  69. coderio/skills/lion-skills/skills/verify-and-fix/evals/evals.json +40 -0
  70. coderio/skills/models.py +66 -0
  71. coderio/skills/parser.py +50 -0
  72. coderio/skills/store.py +102 -0
  73. coderio/tools/__init__.py +94 -0
  74. coderio/tools/base.py +55 -0
  75. coderio/tools/bash.py +168 -0
  76. coderio/tools/command_policy.py +364 -0
  77. coderio/tools/edit_file.py +55 -0
  78. coderio/tools/glob_tool.py +21 -0
  79. coderio/tools/grep_tool.py +90 -0
  80. coderio/tools/linux_sandbox.py +196 -0
  81. coderio/tools/list_dir.py +58 -0
  82. coderio/tools/multi_edit.py +62 -0
  83. coderio/tools/note.py +72 -0
  84. coderio/tools/permission.py +214 -0
  85. coderio/tools/read_file.py +51 -0
  86. coderio/tools/sandbox_runner.py +113 -0
  87. coderio/tools/todo.py +71 -0
  88. coderio/tools/web_fetch.py +188 -0
  89. coderio/tools/web_search.py +49 -0
  90. coderio/tools/win_job.py +214 -0
  91. coderio/tools/win_sandbox.py +639 -0
  92. coderio/tools/write_file.py +25 -0
  93. coderio-0.4.0.dist-info/METADATA +490 -0
  94. coderio-0.4.0.dist-info/RECORD +97 -0
  95. coderio-0.4.0.dist-info/WHEEL +4 -0
  96. coderio-0.4.0.dist-info/entry_points.txt +2 -0
  97. coderio-0.4.0.dist-info/licenses/LICENSE +21 -0
coderio/cli/run_cmd.py ADDED
@@ -0,0 +1,171 @@
1
+ """``coderio run`` — headless one-shot agent execution.
2
+
3
+ Non-interactive counterpart to the TUI: same runtime (config, model, tools,
4
+ skills, session), no Textual app. Purpose-built for CI, scripting, and
5
+ benchmark harnesses (Terminal-Bench / Harbor agents call exactly this shape:
6
+ give a task string, get the final result).
7
+
8
+ Design rules:
9
+ - NEVER block on interactive prompts. Onboarding missing → error exit.
10
+ Untrusted repo config → error exit (run interactive ``coderio`` once to
11
+ confirm). Permission mode defaults to ``full`` because there is no TTY to
12
+ answer confirm prompts; users who want a stricter tier pass
13
+ ``--permission plan`` (plan never prompts — it just denies).
14
+ - Default permission is FULL but the command blacklist still applies
15
+ (CommandReviewMiddleware is independent of the permission gate —
16
+ ``rm -rf /`` stays blocked), and the harness four gates stay active.
17
+ - Streams tokens to stdout so long tasks show progress; ``--quiet`` silences
18
+ everything except the final result.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import sys
24
+ from typing import Any
25
+
26
+ from coderio.agent.stream import NullStream
27
+
28
+
29
+ class HeadlessStream(NullStream):
30
+ """Stream for non-interactive runs: tokens to stdout, progress to stderr.
31
+
32
+ NullStream base keeps every other hook a no-op. ``quiet=True`` disables
33
+ all streaming — the caller only prints the final result.
34
+ """
35
+
36
+ def __init__(self, quiet: bool = False) -> None:
37
+ super().__init__()
38
+ self.quiet = quiet
39
+
40
+ def on_token(self, text: str) -> None:
41
+ if not self.quiet:
42
+ sys.stdout.write(text)
43
+ sys.stdout.flush()
44
+
45
+ def on_tool_start(
46
+ self,
47
+ name: str,
48
+ args: dict[str, Any],
49
+ step: int = 1,
50
+ tool_index: int = 0,
51
+ tool_total: int = 0,
52
+ ) -> None:
53
+ if not self.quiet:
54
+ brief = args.get("command") or args.get("path") or args.get("pattern") or ""
55
+ brief = str(brief)[:80]
56
+ print(f"[tool] {name} {brief}", file=sys.stderr)
57
+
58
+ def on_finish(self) -> None:
59
+ if not self.quiet:
60
+ sys.stdout.write("\n")
61
+ sys.stdout.flush()
62
+
63
+
64
+ def run_headless(
65
+ task: str,
66
+ *,
67
+ provider: str | None = None,
68
+ model: str | None = None,
69
+ permission: str = "full",
70
+ session_id: str | None = None,
71
+ quiet: bool = False,
72
+ ) -> None:
73
+ """Entry point behind ``coderio run``. Prints the final agent reply to
74
+ stdout (or errors to stderr + exit 1). Always returns None — failures
75
+ raise SystemExit via typer, handled by the CLI wrapper."""
76
+ from pathlib import Path
77
+
78
+ import typer
79
+
80
+ from coderio.config.bootstrap import ensure_user_dirs
81
+ from coderio.config.loader import _find_project_dir
82
+ from coderio.config.trust import existing_repo_configs, is_repo_trusted
83
+
84
+ ensure_user_dirs()
85
+ creds_path = Path.home() / ".coderio" / "credentials"
86
+
87
+ # Interactive onboarding would hang forever without a TTY — fail loudly.
88
+ from coderio.cli.repl import _needs_onboarding
89
+
90
+ if _needs_onboarding(creds_path):
91
+ typer.secho(
92
+ "No credentials configured. Run `coderio` once interactively to "
93
+ "complete onboarding, or set ANTHROPIC_API_KEY / OPENAI_API_KEY / "
94
+ "Z_API_KEY in the environment.",
95
+ err=True,
96
+ fg=typer.colors.RED,
97
+ )
98
+ raise typer.Exit(1)
99
+
100
+ # Repo-config trust: same gate as the TUI, but non-interactive — an
101
+ # untrusted repo config is a hard error (prompting would hang headless
102
+ # runs). The user confirms once via interactive `coderio`, then this
103
+ # passes forever (until the repo config content changes).
104
+ user_dir = Path.home() / ".coderio"
105
+ project_dir = _find_project_dir(Path(".").resolve())
106
+ if existing_repo_configs(project_dir) and not is_repo_trusted(project_dir, user_dir):
107
+ typer.secho(
108
+ f"This repository has untrusted coderio config ({project_dir}).\n"
109
+ "Run interactive `coderio` once in this directory and confirm the "
110
+ "config, then retry `coderio run`.",
111
+ err=True,
112
+ fg=typer.colors.RED,
113
+ )
114
+ raise typer.Exit(1)
115
+
116
+ try:
117
+ from coderio.cli.repl import build_runtime
118
+ from coderio.config import load_config
119
+
120
+ cfg_early = load_config()
121
+ session = None
122
+ if session_id:
123
+ from coderio.session.store import Session
124
+
125
+ try:
126
+ session = Session.load_by_id(cfg_early.session.save_dir, session_id)
127
+ except Exception as e: # noqa: BLE001 — surface as a clean CLI error
128
+ typer.secho(f"Could not load session {session_id!r}: {e}", err=True, fg=typer.colors.RED)
129
+ raise typer.Exit(1)
130
+
131
+ cfg, store, chat_model, tools, gate, session, active, _rich_stream = build_runtime(
132
+ console=None,
133
+ mode_override=permission,
134
+ model_override=model,
135
+ provider_override=provider,
136
+ session=session,
137
+ )
138
+ except SystemExit:
139
+ raise
140
+ except Exception as e: # noqa: BLE001 — config/model errors → clean exit
141
+ typer.secho(f"Runtime setup failed: {e}", err=True, fg=typer.colors.RED)
142
+ raise typer.Exit(1)
143
+
144
+ from coderio.agent.deep_loop import run_deep_agent
145
+ from coderio.tools.command_policy import CommandPolicy
146
+
147
+ cmd_policy = CommandPolicy(
148
+ extra_blocked=cfg.tools.blocked_commands,
149
+ network_allowed=cfg.tools.network_allowed,
150
+ whitelist_mode=cfg.tools.whitelist_mode,
151
+ allowed_commands=cfg.tools.allowed_commands,
152
+ )
153
+ final = run_deep_agent(
154
+ user_input=task,
155
+ model=chat_model,
156
+ session=session,
157
+ stream=HeadlessStream(quiet=quiet),
158
+ gate=gate,
159
+ skill_store=store,
160
+ active_skills=active,
161
+ tools=tools,
162
+ workdir=cfg.tools.workspace_root or None,
163
+ harness_enabled=cfg.skills.harness,
164
+ command_policy=cmd_policy,
165
+ sandbox_mode=cfg.tools.sandbox_mode,
166
+ network_allowed=cfg.tools.network_allowed,
167
+ fs_config=cfg.tools.sandbox_fs,
168
+ bash_shell=cfg.tools.bash_shell,
169
+ )
170
+ if quiet:
171
+ print(final)
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+
8
+ @dataclass
9
+ class InstallResult:
10
+ success: bool
11
+ action: str = ""
12
+ skills: list[str] = field(default_factory=list)
13
+ message: str = ""
14
+
15
+
16
+ def _list_skills(target: Path) -> list[str]:
17
+ return sorted(p.parent.name for p in target.glob("*/SKILL.md"))
18
+
19
+
20
+ def install_skills(repo_url: str, target_dir: Path | str, force: bool = False) -> InstallResult:
21
+ """Clone repo_url into target_dir, or git-pull if it already exists as a repo.
22
+
23
+ - target missing OR empty -> clone (normal first install)
24
+ - target is a git repo -> git pull (update)
25
+ - target non-empty and not a git repo -> error unless --force
26
+ """
27
+ target = Path(target_dir)
28
+ target.parent.mkdir(parents=True, exist_ok=True)
29
+
30
+ def _is_empty(d: Path) -> bool:
31
+ return not d.is_dir() or not any(d.iterdir())
32
+
33
+ if not target.exists() or _is_empty(target):
34
+ if target.exists():
35
+ target.rmdir()
36
+ try:
37
+ subprocess.run(
38
+ ["git", "clone", "--quiet", repo_url, str(target)],
39
+ check=True,
40
+ capture_output=True,
41
+ text=True,
42
+ )
43
+ except subprocess.CalledProcessError as e:
44
+ return InstallResult(success=False, message=f"git clone failed: {e.stderr.strip() or e}")
45
+ return InstallResult(
46
+ success=True,
47
+ action="cloned",
48
+ skills=_list_skills(target),
49
+ message=f"Cloned {repo_url} -> {target}",
50
+ )
51
+
52
+ if not (target / ".git").is_dir():
53
+ if force:
54
+ import shutil
55
+
56
+ shutil.rmtree(target)
57
+ return install_skills(repo_url, target, force=False)
58
+ return InstallResult(
59
+ success=False,
60
+ message=f"{target} exists, is not empty, and is not a git repo (use --force to overwrite).",
61
+ )
62
+
63
+ try:
64
+ subprocess.run(
65
+ ["git", "-C", str(target), "pull", "--quiet"],
66
+ check=True,
67
+ capture_output=True,
68
+ text=True,
69
+ )
70
+ except subprocess.CalledProcessError as e:
71
+ return InstallResult(success=False, message=f"git pull failed: {e.stderr.strip() or e}")
72
+ return InstallResult(
73
+ success=True,
74
+ action="updated",
75
+ skills=_list_skills(target),
76
+ message=f"Updated {target}",
77
+ )
coderio/cli/stream.py ADDED
@@ -0,0 +1,224 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Any
5
+
6
+ from rich.console import Console
7
+ from rich.live import Live
8
+ from rich.markdown import Markdown
9
+ from rich.panel import Panel
10
+ from rich.spinner import Spinner
11
+ from rich.text import Text
12
+
13
+ # Human-readable labels for AgentState values (shared with the TUI StatusBar).
14
+ _PHASE_LABELS: dict[str, str] = {
15
+ "explore": "探索",
16
+ "plan": "规划",
17
+ "implement": "实现",
18
+ "verify": "验证",
19
+ "complete": "完成",
20
+ }
21
+
22
+
23
+ class RichStream:
24
+ """StreamHandler impl: live token stream + final markdown re-render.
25
+
26
+ Implements coderio.agent.stream.StreamHandler (duck-typed; no inherit needed).
27
+ A single ALWAYS-ON busy indicator covers the whole model wait (thinking,
28
+ generation, and the gaps between a tool result and the next model call) with
29
+ an elapsed-time timer, so the UI never looks frozen.
30
+
31
+ Thinking is captured per-round and rendered COLLAPSED by default (a one-line
32
+ summary), with the full text available via show_last_thinking() (the /think
33
+ command). This keeps the screen uncluttered while preserving auditability.
34
+ """
35
+
36
+ name = "rich_stream"
37
+
38
+ def __init__(self, console: Console, show_tool_output: bool = True) -> None:
39
+ self.console = console
40
+ self.buffer = ""
41
+ self._live = None
42
+
43
+ self._busy_live = None
44
+ self._busy_buf = ""
45
+ self._busy_start = 0.0
46
+ self.usage = {"input_tokens": 0, "output_tokens": 0}
47
+ self.show_tool_output = show_tool_output
48
+
49
+ # Per-round thinking capture (for collapsed display + /think expand).
50
+ self._round_thinking = "" # accumulates the current round's thinking
51
+ self._round_think_start = 0.0 # when thinking started this round
52
+ self._last_thinking = "" # the most recent completed round's full text
53
+ self._last_think_secs = 0.0 # duration of that round's thinking
54
+
55
+ # --------------------------------------------------------------- busy indicator
56
+ def _start_busy(self) -> None:
57
+ self._busy_buf = ""
58
+ self._busy_start = time.monotonic()
59
+ if self._busy_live is None:
60
+ self._busy_live = Live(
61
+ get_renderable=self._busy_renderable,
62
+ console=self.console,
63
+ refresh_per_second=10,
64
+ transient=True,
65
+ )
66
+ self._busy_live.start()
67
+
68
+ def _refresh_busy(self) -> None:
69
+ if self._busy_live is not None:
70
+ self._busy_live.refresh()
71
+
72
+ def _busy_renderable(self) -> Spinner:
73
+ elapsed = time.monotonic() - self._busy_start
74
+ preview = self._busy_buf[-50:].replace("\n", " ")
75
+ suffix = f" 正在思考… {elapsed:4.1f}s"
76
+ if preview:
77
+ suffix += f" {preview}"
78
+ return Spinner("dots", text=Text(suffix, style="dim"))
79
+
80
+ def _stop_busy(self) -> None:
81
+ if self._busy_live is not None:
82
+ self._busy_live.stop()
83
+ self._busy_live = None
84
+ self._busy_buf = ""
85
+
86
+ def _flush_round_thinking(self) -> None:
87
+ """If the current round accumulated thinking text, render a collapsed
88
+ summary and stash the full text for /think. Called when the thinking
89
+ phase ends (first token, tool call, or finish)."""
90
+ if not self._round_thinking.strip():
91
+ return
92
+ secs = time.monotonic() - self._round_think_start if self._round_think_start else 0.0
93
+ chars = len(self._round_thinking)
94
+ self._last_thinking = self._round_thinking
95
+ self._last_think_secs = secs
96
+ # Collapsed summary line (full text via show_last_thinking()).
97
+ self.console.print(Text(f"💭 思考 · {secs:.1f}s · {chars} 字 · /think 展开", style="dim italic"))
98
+ self._round_thinking = ""
99
+ self._round_think_start = 0.0
100
+
101
+ def show_last_thinking(self) -> bool:
102
+ """Expand the most recent round's full thinking text. Returns True if
103
+ there was something to show (used by the /think command)."""
104
+ if not self._last_thinking.strip():
105
+ self.console.print("[dim]最近一轮没有思考内容。[/dim]")
106
+ return False
107
+ secs = self._last_think_secs
108
+ self.console.print(
109
+ Panel(
110
+ Text(self._last_thinking),
111
+ title=f"[dim]💭 思考 · {secs:.1f}s[/dim]",
112
+ border_style="dim",
113
+ )
114
+ )
115
+ return True
116
+
117
+ # -------------------------------------------------------- StreamHandler protocol
118
+ def on_step_start(self, step: int = 1) -> None:
119
+ self._stop_live()
120
+ self._start_busy()
121
+
122
+ def on_token(self, text: str) -> None:
123
+ # First visible token: the thinking phase is over — flush its summary.
124
+ self._flush_round_thinking()
125
+ self._stop_busy()
126
+ self.buffer += text
127
+ if self._live is None:
128
+ self._live = Live(Text(self.buffer), console=self.console, refresh_per_second=15)
129
+ self._live.start()
130
+ else:
131
+ self._live.update(Text(self.buffer))
132
+
133
+ def on_thinking(self, text: str) -> None:
134
+ """Accumulate thinking into the round buffer (for later collapsed display)
135
+ AND into the busy preview (live feedback during the wait)."""
136
+ if not self._round_thinking:
137
+ self._round_think_start = time.monotonic()
138
+ self._round_thinking += text
139
+ self._busy_buf += text
140
+ if self._busy_live is None:
141
+ self._start_busy()
142
+ else:
143
+ self._refresh_busy()
144
+
145
+ def on_tool_start(
146
+ self,
147
+ name: str,
148
+ args: dict[str, Any],
149
+ step: int = 1,
150
+ tool_index: int = 0,
151
+ tool_total: int = 0,
152
+ ) -> None:
153
+ # A tool call also ends the thinking phase.
154
+ self._flush_round_thinking()
155
+ self._stop_live()
156
+ self._stop_busy()
157
+ args_str = ", ".join(f"{k}={v!r}" for k, v in args.items())
158
+ if len(args_str) > 100:
159
+ args_str = args_str[:100] + "…"
160
+ self.console.print(f"[bold green]⏺[/bold green] [green]{name}[/green]({args_str})")
161
+
162
+ def on_tool_end(self, name: str, result: str) -> None:
163
+ if not self.show_tool_output:
164
+ first = result.splitlines()[0][:60] if result.splitlines() else ""
165
+ self.console.print(Text(f" → {first}{'…' if len(result) > 60 else ''}", style="dim"))
166
+ return
167
+ lines = result.splitlines()
168
+ shown = "\n".join(lines[:3])
169
+ if len(lines) > 3:
170
+ shown += f"\n…({len(lines) - 3} more lines)"
171
+ self.console.print(Text(shown, style="dim"))
172
+
173
+ def on_truncated(self, stop_reason: str) -> None:
174
+ self._flush_round_thinking()
175
+ self._stop_live()
176
+ self._stop_busy()
177
+ self.console.print(
178
+ Panel(
179
+ f"⚠ 输出被截断 (stop_reason: {stop_reason})。内容可能不完整 —— 可能需要分多次完成或调大 max_tokens。",
180
+ title="截断警告",
181
+ border_style="yellow",
182
+ )
183
+ )
184
+
185
+ def on_harness_warn(self, message: str) -> None:
186
+ self._flush_round_thinking()
187
+ self._stop_live()
188
+ self._stop_busy()
189
+ self.console.print(
190
+ Panel(
191
+ f"⚠ {message}\n\n[dim]产出可能未经验证,请人工复核。[/dim]",
192
+ title="[bold red]⚠ harness 警告[/bold red]",
193
+ border_style="red",
194
+ )
195
+ )
196
+
197
+ def on_phase_change(self, state: str, step: int, hint: str) -> None:
198
+ """Print a dim phase-change marker (useful in non-TUI / debug mode)."""
199
+ label = _PHASE_LABELS.get(state, state)
200
+ self.console.print(f"[dim]· [{label}][/dim] {hint}", highlight=False)
201
+
202
+ def on_finish(self) -> None:
203
+ self._flush_round_thinking()
204
+ self._stop_live()
205
+ self._stop_busy()
206
+ if self.buffer.strip():
207
+ self.console.print(
208
+ Panel(
209
+ Markdown(self.buffer),
210
+ border_style="blue",
211
+ title="[dim]coderio[/dim]",
212
+ )
213
+ )
214
+
215
+ def add_usage(self, meta: dict[str, int]) -> None:
216
+ for k in ("input_tokens", "output_tokens"):
217
+ if k in meta:
218
+ self.usage[k] += meta[k]
219
+
220
+ def _stop_live(self) -> None:
221
+ if self._live is not None:
222
+ self._live.stop()
223
+ self._live = None
224
+ self.buffer = ""