bub 0.4.0__tar.gz → 0.4.2__tar.gz
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.
- {bub-0.4.0 → bub-0.4.2}/PKG-INFO +2 -1
- {bub-0.4.0 → bub-0.4.2}/README.md +1 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/_version.py +2 -2
- {bub-0.4.0 → bub-0.4.2}/src/bub/builtin/agent.py +1 -2
- {bub-0.4.0 → bub-0.4.2}/src/bub/builtin/cli.py +7 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/builtin/codex_provider.py +8 -2
- {bub-0.4.0 → bub-0.4.2}/src/bub/builtin/hook_impl.py +14 -4
- {bub-0.4.0 → bub-0.4.2}/src/bub/builtin/model_runner.py +29 -23
- {bub-0.4.0 → bub-0.4.2}/src/bub/builtin/settings.py +3 -2
- {bub-0.4.0 → bub-0.4.2}/src/bub/builtin/tape.py +24 -6
- {bub-0.4.0 → bub-0.4.2}/src/bub/builtin/tools.py +27 -31
- {bub-0.4.0 → bub-0.4.2}/src/bub/channels/cli/__init__.py +177 -109
- bub-0.4.2/src/bub/channels/cli/ansi_bridge.py +23 -0
- bub-0.4.2/src/bub/channels/cli/terminal_output.py +140 -0
- bub-0.4.2/src/bub/channels/cli/writers.py +42 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/tools.py +42 -8
- {bub-0.4.0 → bub-0.4.2}/tests/test_agent_hooks.py +1 -1
- {bub-0.4.0 → bub-0.4.2}/tests/test_builtin_agent.py +35 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_builtin_cli.py +45 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_builtin_codex.py +8 -1
- {bub-0.4.0 → bub-0.4.2}/tests/test_builtin_hook_impl.py +13 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_builtin_model_runner.py +102 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_builtin_tape.py +38 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_builtin_tools.py +57 -38
- {bub-0.4.0 → bub-0.4.2}/tests/test_channels.py +405 -44
- {bub-0.4.0 → bub-0.4.2}/tests/test_settings.py +8 -1
- {bub-0.4.0 → bub-0.4.2}/tests/test_tools.py +64 -1
- {bub-0.4.0 → bub-0.4.2}/.gitignore +0 -0
- {bub-0.4.0 → bub-0.4.2}/LICENSE +0 -0
- {bub-0.4.0 → bub-0.4.2}/pyproject.toml +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/__init__.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/__main__.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/builtin/__init__.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/builtin/auth.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/builtin/context.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/builtin/shell_manager.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/builtin/steering.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/builtin/store.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/channels/__init__.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/channels/admission.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/channels/base.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/channels/cli/renderer.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/channels/contracts.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/channels/handler.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/channels/manager.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/channels/message.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/channels/telegram.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/configure.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/envelope.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/errors.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/framework.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/hooks/__init__.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/hooks/interception.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/hooks/runtime.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/hooks/specs.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/inquirer.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/model_selection.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/skills.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/streaming.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/tape.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/turn.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/types.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/bub/utils.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/skills/README.md +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/skills/gh/SKILL.md +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/skills/skill-creator/SKILL.md +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/skills/skill-creator/license.txt +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/skills/skill-creator/scripts/init_skill.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/skills/skill-creator/scripts/quick_validate.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/skills/telegram/SKILL.md +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/skills/telegram/scripts/telegram_edit.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/skills/telegram/scripts/telegram_rich.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/src/skills/telegram/scripts/telegram_send.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/tests/conftest.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_cli_help.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_cli_renderer.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_configure.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_envelope.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_file_tape_store_entry_ids.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_fork_store_merge_back.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_framework.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_hook_runtime.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_image_message.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_skills.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_subagent_tool.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_tape_search_output.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_types.py +0 -0
- {bub-0.4.0 → bub-0.4.2}/tests/test_utils.py +0 -0
{bub-0.4.0 → bub-0.4.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bub
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: A common shape for agents that live alongside people.
|
|
5
5
|
Project-URL: Homepage, https://bub.build
|
|
6
6
|
Project-URL: Repository, https://github.com/bubbuild/bub
|
|
@@ -153,6 +153,7 @@ Lines starting with `,` enter internal command mode (`,help`, `,skill name=my-sk
|
|
|
153
153
|
| `BUB_API_KEY` | — | Provider key (optional with `bub login openai`) |
|
|
154
154
|
| `BUB_API_BASE` | — | Custom provider endpoint |
|
|
155
155
|
| `BUB_CLIENT_ARGS` | — | JSON object forwarded to the underlying model client |
|
|
156
|
+
| `BUB_COMPLETION_ARGS` | — | JSON object forwarded to each completion call |
|
|
156
157
|
| `BUB_MAX_STEPS` | `50` | Max tool-use loop iterations |
|
|
157
158
|
| `BUB_MAX_TOKENS` | `16384` | Max tokens per model call |
|
|
158
159
|
| `BUB_MODEL_TIMEOUT_SECONDS` | — | Model call timeout (seconds) |
|
|
@@ -116,6 +116,7 @@ Lines starting with `,` enter internal command mode (`,help`, `,skill name=my-sk
|
|
|
116
116
|
| `BUB_API_KEY` | — | Provider key (optional with `bub login openai`) |
|
|
117
117
|
| `BUB_API_BASE` | — | Custom provider endpoint |
|
|
118
118
|
| `BUB_CLIENT_ARGS` | — | JSON object forwarded to the underlying model client |
|
|
119
|
+
| `BUB_COMPLETION_ARGS` | — | JSON object forwarded to each completion call |
|
|
119
120
|
| `BUB_MAX_STEPS` | `50` | Max tool-use loop iterations |
|
|
120
121
|
| `BUB_MAX_TOKENS` | `16384` | Max tokens per model call |
|
|
121
122
|
| `BUB_MODEL_TIMEOUT_SECONDS` | — | Model call timeout (seconds) |
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.4.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 4,
|
|
21
|
+
__version__ = version = '0.4.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 4, 2)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -32,6 +32,7 @@ from bub.tools import (
|
|
|
32
32
|
REGISTRY,
|
|
33
33
|
Tool,
|
|
34
34
|
ToolContext,
|
|
35
|
+
model_tools,
|
|
35
36
|
)
|
|
36
37
|
from bub.turn import TurnState
|
|
37
38
|
from bub.utils import workspace_from_state
|
|
@@ -352,8 +353,6 @@ class Agent:
|
|
|
352
353
|
allowed_skills: set[str] | None,
|
|
353
354
|
tools: list[Tool],
|
|
354
355
|
) -> AsyncStreamEvents:
|
|
355
|
-
from bub.builtin.tools import model_tools
|
|
356
|
-
|
|
357
356
|
system_prompt = self._system_prompt(
|
|
358
357
|
prompt_text, state=tape.context.state, allowed_skills=allowed_skills, tools=tools
|
|
359
358
|
)
|
|
@@ -87,6 +87,13 @@ def gateway(
|
|
|
87
87
|
framework = ctx.ensure_object(BubFramework)
|
|
88
88
|
|
|
89
89
|
manager = ChannelManager(framework, enabled_channels=enable_channels or None)
|
|
90
|
+
if not manager.enabled_channels():
|
|
91
|
+
typer.secho(
|
|
92
|
+
"No channels are enabled. Configure at least one channel with 'bub onboard' or '--enable-channel'.",
|
|
93
|
+
err=True,
|
|
94
|
+
fg="red",
|
|
95
|
+
)
|
|
96
|
+
raise typer.Exit(1)
|
|
90
97
|
asyncio.run(manager.listen_and_run())
|
|
91
98
|
|
|
92
99
|
|
|
@@ -365,22 +365,28 @@ class CodexCompletionChunkMapper:
|
|
|
365
365
|
})
|
|
366
366
|
|
|
367
367
|
|
|
368
|
-
def _completion_usage_from_response_usage(usage: Any) -> dict[str,
|
|
368
|
+
def _completion_usage_from_response_usage(usage: Any) -> dict[str, Any]:
|
|
369
369
|
payload = usage.model_dump(exclude_none=True) if hasattr(usage, "model_dump") else usage
|
|
370
370
|
if not isinstance(payload, dict):
|
|
371
371
|
payload = {
|
|
372
372
|
"input_tokens": getattr(usage, "input_tokens", 0),
|
|
373
373
|
"output_tokens": getattr(usage, "output_tokens", 0),
|
|
374
374
|
"total_tokens": getattr(usage, "total_tokens", 0),
|
|
375
|
+
"input_tokens_details": getattr(usage, "input_tokens_details", None),
|
|
375
376
|
}
|
|
376
377
|
prompt_tokens = int(payload.get("input_tokens") or payload.get("prompt_tokens") or 0)
|
|
377
378
|
completion_tokens = int(payload.get("output_tokens") or payload.get("completion_tokens") or 0)
|
|
378
379
|
total_tokens = int(payload.get("total_tokens") or prompt_tokens + completion_tokens)
|
|
379
|
-
|
|
380
|
+
completion_usage: dict[str, Any] = {
|
|
380
381
|
"prompt_tokens": prompt_tokens,
|
|
381
382
|
"completion_tokens": completion_tokens,
|
|
382
383
|
"total_tokens": total_tokens,
|
|
383
384
|
}
|
|
385
|
+
input_details = _mapping_from_value(payload.get("input_tokens_details"))
|
|
386
|
+
cached_tokens = input_details.get("cached_tokens")
|
|
387
|
+
if isinstance(cached_tokens, int) and not isinstance(cached_tokens, bool):
|
|
388
|
+
completion_usage["prompt_tokens_details"] = {"cached_tokens": cached_tokens}
|
|
389
|
+
return completion_usage
|
|
384
390
|
|
|
385
391
|
|
|
386
392
|
def _string_attr(obj: Any, name: str) -> str:
|
|
@@ -90,6 +90,16 @@ class BuiltinImpl:
|
|
|
90
90
|
return str(model) if model else None
|
|
91
91
|
return None
|
|
92
92
|
|
|
93
|
+
async def _recover_session_reasoning_effort(self, session_id: str) -> str | None:
|
|
94
|
+
"""Recover the latest per-session reasoning effort override."""
|
|
95
|
+
session = self._get_agent().tape.session_tape(session_id, self.framework.workspace)
|
|
96
|
+
entries = list(await session.store.fetch_all(session.query().kinds("event")))
|
|
97
|
+
for entry in reversed(entries):
|
|
98
|
+
if entry.kind == "event" and entry.payload.get("name") == "reasoning_effort_switch":
|
|
99
|
+
reasoning_effort = (entry.payload.get("data") or {}).get("reasoning_effort")
|
|
100
|
+
return str(reasoning_effort) if reasoning_effort else None
|
|
101
|
+
return None
|
|
102
|
+
|
|
93
103
|
@staticmethod
|
|
94
104
|
async def _discard_message(_: ChannelMessage) -> None:
|
|
95
105
|
return
|
|
@@ -149,6 +159,8 @@ class BuiltinImpl:
|
|
|
149
159
|
# fresh/unknown session never inherits another session's model.
|
|
150
160
|
if model := await self._recover_session_model(session_id):
|
|
151
161
|
state["model"] = model
|
|
162
|
+
if reasoning_effort := await self._recover_session_reasoning_effort(session_id):
|
|
163
|
+
state["reasoning_effort"] = reasoning_effort
|
|
152
164
|
if model := field_of(message, "context", {}).get("model"):
|
|
153
165
|
state["model"] = model
|
|
154
166
|
if thread_id := field_of(message, "context", {}).get("thread_id"):
|
|
@@ -161,9 +173,8 @@ class BuiltinImpl:
|
|
|
161
173
|
lifespan = field_of(message, "lifespan")
|
|
162
174
|
if lifespan is not None:
|
|
163
175
|
await lifespan.__aexit__(tp, value, traceback)
|
|
164
|
-
#
|
|
165
|
-
#
|
|
166
|
-
# turn), so nothing to write here — this hook only closes the lifespan.
|
|
176
|
+
# Per-session completion overrides are persisted by their tools as tape
|
|
177
|
+
# events, so nothing to write here — this hook only closes the lifespan.
|
|
167
178
|
|
|
168
179
|
@hookimpl
|
|
169
180
|
async def build_prompt(self, message: ChannelMessage, session_id: str, state: TurnState) -> str | list[dict]:
|
|
@@ -248,7 +259,6 @@ class BuiltinImpl:
|
|
|
248
259
|
"Channels",
|
|
249
260
|
choices=available_channels,
|
|
250
261
|
enabled=default_channels,
|
|
251
|
-
validate=lambda values: True if values else "Select at least one channel.",
|
|
252
262
|
)
|
|
253
263
|
|
|
254
264
|
stream_output = bub_inquirer.ask_confirm("Stream output", default=bool(current_config.get("stream_output")))
|
|
@@ -11,6 +11,7 @@ from typing import Any, Literal, cast
|
|
|
11
11
|
|
|
12
12
|
from any_llm import AnyLLM
|
|
13
13
|
from any_llm.constants import LLMProvider
|
|
14
|
+
from any_llm.providers.anthropic.base import BaseAnthropicProvider
|
|
14
15
|
from any_llm.providers.openai.base import BaseOpenAIProvider
|
|
15
16
|
from any_llm.types.completion import (
|
|
16
17
|
ChatCompletion,
|
|
@@ -46,17 +47,13 @@ TOOL_ARGUMENTS_ADAPTER = TypeAdapter(dict[str, Any])
|
|
|
46
47
|
CompletionResult = ChatCompletion | ParsedChatCompletion[Any] | AsyncIterator[ChatCompletionChunk]
|
|
47
48
|
|
|
48
49
|
|
|
49
|
-
def
|
|
50
|
-
"""
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"""
|
|
57
|
-
if stream and isinstance(llm, BaseOpenAIProvider):
|
|
58
|
-
return {"include_usage": True}
|
|
59
|
-
return None
|
|
50
|
+
def _extra_options(llm: AnyLLM, *, stream: bool) -> dict[str, Any]:
|
|
51
|
+
"""Return provider-specific extra completion options."""
|
|
52
|
+
if isinstance(llm, BaseAnthropicProvider):
|
|
53
|
+
return {"cache_control": {"type": "ephemeral"}}
|
|
54
|
+
elif stream and isinstance(llm, BaseOpenAIProvider):
|
|
55
|
+
return {"stream_options": {"include_usage": True}}
|
|
56
|
+
return {}
|
|
60
57
|
|
|
61
58
|
|
|
62
59
|
class ModelRunner:
|
|
@@ -84,25 +81,33 @@ class ModelRunner:
|
|
|
84
81
|
return AnyLLM.create(candidate.provider, **client_kwargs)
|
|
85
82
|
|
|
86
83
|
async def completion_response(
|
|
87
|
-
self,
|
|
84
|
+
self,
|
|
85
|
+
*,
|
|
86
|
+
model: str,
|
|
87
|
+
messages: list[dict[str, Any]],
|
|
88
|
+
tools: list[Tool],
|
|
89
|
+
max_tokens: int | None = None,
|
|
90
|
+
reasoning_effort: str | None = None,
|
|
88
91
|
) -> CompletionResult:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
tool_payloads = completion_tools(tools) or None
|
|
92
|
+
tool_payloads = [tool.to_schema() for tool in tools] or None
|
|
92
93
|
completion_messages: list[dict[str, Any] | ChatCompletionMessage] = list(messages)
|
|
93
94
|
clients = list(self.iter_llm_clients(model))
|
|
94
95
|
completion_error: Exception | None = None
|
|
95
96
|
for index, (candidate, llm) in enumerate(clients):
|
|
96
97
|
try:
|
|
97
98
|
streaming = llm.SUPPORTS_COMPLETION_STREAMING
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
99
|
+
completion_kwargs = {
|
|
100
|
+
**self.settings.completion_args,
|
|
101
|
+
**_extra_options(llm, stream=streaming),
|
|
102
|
+
"model": candidate.model_id,
|
|
103
|
+
"messages": completion_messages,
|
|
104
|
+
"tools": tool_payloads,
|
|
105
|
+
"max_tokens": max_tokens if max_tokens is not None else self.settings.max_tokens,
|
|
106
|
+
"stream": streaming,
|
|
107
|
+
}
|
|
108
|
+
if reasoning_effort is not None:
|
|
109
|
+
completion_kwargs["reasoning_effort"] = reasoning_effort
|
|
110
|
+
return cast("CompletionResult", await llm.acompletion(**completion_kwargs))
|
|
106
111
|
except Exception as exc:
|
|
107
112
|
if completion_error is None:
|
|
108
113
|
completion_error = exc
|
|
@@ -176,6 +181,7 @@ class ModelRunner:
|
|
|
176
181
|
messages=list(request.messages),
|
|
177
182
|
tools=tools,
|
|
178
183
|
max_tokens=request.max_tokens,
|
|
184
|
+
reasoning_effort=tape.context.state.get("reasoning_effort"),
|
|
179
185
|
)
|
|
180
186
|
async for event in self._completion_events(completion, state, output):
|
|
181
187
|
yield event
|
|
@@ -60,6 +60,7 @@ class AgentSettings(Settings):
|
|
|
60
60
|
max_tokens: int = DEFAULT_MAX_TOKENS
|
|
61
61
|
model_timeout_seconds: int | None = None
|
|
62
62
|
client_args: dict[str, Any] = Field(default_factory=dict)
|
|
63
|
+
completion_args: dict[str, Any] = Field(default_factory=dict)
|
|
63
64
|
verbose: int = Field(default=0, description="Verbosity level for logging. Higher means more verbose.", ge=0, le=2)
|
|
64
65
|
|
|
65
66
|
@classmethod
|
|
@@ -79,9 +80,9 @@ class AgentSettings(Settings):
|
|
|
79
80
|
file_secret_settings,
|
|
80
81
|
)
|
|
81
82
|
|
|
82
|
-
@field_validator("client_args", mode="before")
|
|
83
|
+
@field_validator("client_args", "completion_args", mode="before")
|
|
83
84
|
@classmethod
|
|
84
|
-
def
|
|
85
|
+
def default_dict_args(cls, value: Any) -> Any:
|
|
85
86
|
return {} if value is None else value
|
|
86
87
|
|
|
87
88
|
def model_candidates(self, model: str) -> list[ModelCandidate]:
|
|
@@ -4,7 +4,7 @@ import contextlib
|
|
|
4
4
|
import hashlib
|
|
5
5
|
import inspect
|
|
6
6
|
import json
|
|
7
|
-
from collections.abc import AsyncGenerator
|
|
7
|
+
from collections.abc import AsyncGenerator, Mapping
|
|
8
8
|
from dataclasses import asdict, dataclass, field, replace
|
|
9
9
|
from datetime import UTC, datetime
|
|
10
10
|
from pathlib import Path
|
|
@@ -33,6 +33,7 @@ class TapeInfo:
|
|
|
33
33
|
last_anchor: str | None
|
|
34
34
|
entries_since_last_anchor: int
|
|
35
35
|
last_token_usage: int | None
|
|
36
|
+
last_token_cache_hit_rate: float | None
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
@dataclass(frozen=True)
|
|
@@ -77,13 +78,29 @@ class Tape:
|
|
|
77
78
|
last_anchor = None
|
|
78
79
|
entries_since_last_anchor = len(entries)
|
|
79
80
|
last_token_usage: int | None = None
|
|
81
|
+
last_token_cache_hit_rate: float | None = None
|
|
80
82
|
for entry in reversed(entries):
|
|
81
83
|
if entry.kind == "event" and entry.payload.get("name") == "run":
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
data = entry.payload.get("data")
|
|
85
|
+
usage = data.get("usage") if isinstance(data, Mapping) else None
|
|
86
|
+
if not isinstance(usage, Mapping):
|
|
87
|
+
continue
|
|
88
|
+
token_usage = usage.get("total_tokens")
|
|
89
|
+
if not isinstance(token_usage, int) or isinstance(token_usage, bool):
|
|
90
|
+
continue
|
|
91
|
+
last_token_usage = token_usage
|
|
92
|
+
prompt_tokens = usage.get("prompt_tokens")
|
|
93
|
+
prompt_details = usage.get("prompt_tokens_details")
|
|
94
|
+
cached_tokens = prompt_details.get("cached_tokens") if isinstance(prompt_details, Mapping) else None
|
|
95
|
+
if (
|
|
96
|
+
isinstance(prompt_tokens, int)
|
|
97
|
+
and not isinstance(prompt_tokens, bool)
|
|
98
|
+
and prompt_tokens > 0
|
|
99
|
+
and isinstance(cached_tokens, int)
|
|
100
|
+
and not isinstance(cached_tokens, bool)
|
|
101
|
+
):
|
|
102
|
+
last_token_cache_hit_rate = cached_tokens / prompt_tokens
|
|
103
|
+
break
|
|
87
104
|
return TapeInfo(
|
|
88
105
|
name=self.name,
|
|
89
106
|
entries=len(entries),
|
|
@@ -91,6 +108,7 @@ class Tape:
|
|
|
91
108
|
last_anchor=str(last_anchor) if last_anchor else None,
|
|
92
109
|
entries_since_last_anchor=entries_since_last_anchor,
|
|
93
110
|
last_token_usage=last_token_usage,
|
|
111
|
+
last_token_cache_hit_rate=last_token_cache_hit_rate,
|
|
94
112
|
)
|
|
95
113
|
|
|
96
114
|
async def ensure_bootstrap_anchor(self) -> None:
|
|
@@ -4,11 +4,9 @@ import asyncio
|
|
|
4
4
|
import json
|
|
5
5
|
import uuid
|
|
6
6
|
from collections.abc import Iterable
|
|
7
|
-
from dataclasses import replace
|
|
8
7
|
from pathlib import Path
|
|
9
8
|
from typing import TYPE_CHECKING, cast
|
|
10
9
|
|
|
11
|
-
from openai.types.chat import ChatCompletionToolParam
|
|
12
10
|
from pydantic import BaseModel, Field
|
|
13
11
|
|
|
14
12
|
from bub.builtin.shell_manager import shell_manager
|
|
@@ -72,11 +70,6 @@ def resolve_tool_names(names: Iterable[str] | None = None, *, exclude: Iterable[
|
|
|
72
70
|
return resolved - excluded
|
|
73
71
|
|
|
74
72
|
|
|
75
|
-
def model_tools(tools: Iterable[Tool]) -> list[Tool]:
|
|
76
|
-
"""Convert runtime tool names into model-safe aliases."""
|
|
77
|
-
return [replace(tool_item, name=_to_model_name(tool_item.name)) for tool_item in tools]
|
|
78
|
-
|
|
79
|
-
|
|
80
73
|
def _tool_signature(tool_item: Tool) -> str:
|
|
81
74
|
properties = tool_item.parameters.get("properties", {})
|
|
82
75
|
if not isinstance(properties, dict) or not properties:
|
|
@@ -90,10 +83,11 @@ def _tool_signature(tool_item: Tool) -> str:
|
|
|
90
83
|
|
|
91
84
|
def render_tools_prompt(tools: Iterable[Tool]) -> str:
|
|
92
85
|
"""Render a human-readable description of tools for builtin agent prompts."""
|
|
93
|
-
|
|
86
|
+
agent_tools = [tool_item for tool_item in tools if tool_item.agent_use]
|
|
87
|
+
if not agent_tools:
|
|
94
88
|
return ""
|
|
95
89
|
lines = []
|
|
96
|
-
for tool_item in
|
|
90
|
+
for tool_item in agent_tools:
|
|
97
91
|
line = f"- {_tool_signature(tool_item)}"
|
|
98
92
|
if tool_item.description:
|
|
99
93
|
line += f": {tool_item.description}"
|
|
@@ -101,21 +95,6 @@ def render_tools_prompt(tools: Iterable[Tool]) -> str:
|
|
|
101
95
|
return f"<available_tools>\n{'\n'.join(lines)}\n</available_tools>"
|
|
102
96
|
|
|
103
97
|
|
|
104
|
-
def completion_tools(tools: Iterable[Tool]) -> list[ChatCompletionToolParam]:
|
|
105
|
-
"""Build any-llm completion tool payloads from Bub tools."""
|
|
106
|
-
return [
|
|
107
|
-
{
|
|
108
|
-
"type": "function",
|
|
109
|
-
"function": {
|
|
110
|
-
"name": tool_item.name,
|
|
111
|
-
"description": tool_item.description,
|
|
112
|
-
"parameters": tool_item.parameters,
|
|
113
|
-
},
|
|
114
|
-
}
|
|
115
|
-
for tool_item in tools
|
|
116
|
-
]
|
|
117
|
-
|
|
118
|
-
|
|
119
98
|
def _raise_for_failed_shell(returncode: int | None, output: str) -> None:
|
|
120
99
|
if returncode in (None, 0):
|
|
121
100
|
return
|
|
@@ -253,16 +232,20 @@ def fs_edit(path: str, old: str, new: str, start: int = 0, *, context: ToolConte
|
|
|
253
232
|
|
|
254
233
|
|
|
255
234
|
@tool(context=True, name="skill")
|
|
256
|
-
def skill_describe(name: str, *, context: ToolContext) -> str:
|
|
257
|
-
"""Load the skill content by name. Return the location and skill content.
|
|
235
|
+
def skill_describe(name: str | None = None, *, context: ToolContext) -> str:
|
|
236
|
+
"""Load the skill content by name. Return the location and skill content.
|
|
237
|
+
If name is not provided, list all available skills in the current workspace.
|
|
238
|
+
"""
|
|
258
239
|
from bub.utils import workspace_from_state
|
|
259
240
|
|
|
260
241
|
allowed_skills = context.state.get("allowed_skills")
|
|
261
|
-
if allowed_skills is not None and name.casefold() not in allowed_skills:
|
|
242
|
+
if allowed_skills is not None and name and name.casefold() not in allowed_skills:
|
|
262
243
|
return f"(skill '{name}' is not allowed in this context)"
|
|
263
244
|
|
|
264
245
|
workspace = workspace_from_state(context.state)
|
|
265
246
|
skill_index = {skill.name: skill for skill in discover_skills(workspace)}
|
|
247
|
+
if name is None:
|
|
248
|
+
return "Available skills:\n" + "\n".join(f"- {skill.name}" for skill in skill_index.values())
|
|
266
249
|
if name.casefold() not in skill_index:
|
|
267
250
|
return "(no such skill)"
|
|
268
251
|
skill = skill_index[name.casefold()]
|
|
@@ -273,13 +256,15 @@ def skill_describe(name: str, *, context: ToolContext) -> str:
|
|
|
273
256
|
async def tape_info(context: ToolContext) -> str:
|
|
274
257
|
"""Get information about the current tape, such as number of entries and anchors."""
|
|
275
258
|
info = await context.tape.info()
|
|
259
|
+
cache_hit_rate = f"{info.last_token_cache_hit_rate:.2%}" if info.last_token_cache_hit_rate is not None else "None"
|
|
276
260
|
return (
|
|
277
261
|
f"name: {info.name}\n"
|
|
278
262
|
f"entries: {info.entries}\n"
|
|
279
263
|
f"anchors: {info.anchors}\n"
|
|
280
264
|
f"last_anchor: {info.last_anchor}\n"
|
|
281
265
|
f"entries_since_last_anchor: {info.entries_since_last_anchor}\n"
|
|
282
|
-
f"last_token_usage: {info.last_token_usage}"
|
|
266
|
+
f"last_token_usage: {info.last_token_usage}\n"
|
|
267
|
+
f"last_token_cache_hit_rate: {cache_hit_rate}"
|
|
283
268
|
)
|
|
284
269
|
|
|
285
270
|
|
|
@@ -370,7 +355,7 @@ async def run_subagent(param: SubAgentInput, *, context: ToolContext) -> str:
|
|
|
370
355
|
return output
|
|
371
356
|
|
|
372
357
|
|
|
373
|
-
@tool(name="help")
|
|
358
|
+
@tool(name="help", agent_use=False)
|
|
374
359
|
def show_help() -> str:
|
|
375
360
|
"""Show a help message."""
|
|
376
361
|
return (
|
|
@@ -393,7 +378,7 @@ def show_help() -> str:
|
|
|
393
378
|
)
|
|
394
379
|
|
|
395
380
|
|
|
396
|
-
@tool(name="quit", context=True)
|
|
381
|
+
@tool(name="quit", context=True, agent_use=False)
|
|
397
382
|
async def quit_tool(*, context: ToolContext) -> str:
|
|
398
383
|
"""Abort the tasks of the current session. DO NOT use it in a normal workflow."""
|
|
399
384
|
agent = _get_agent(context)
|
|
@@ -403,7 +388,7 @@ async def quit_tool(*, context: ToolContext) -> str:
|
|
|
403
388
|
return "Session tasks stopped."
|
|
404
389
|
|
|
405
390
|
|
|
406
|
-
@tool(name="model", context=True)
|
|
391
|
+
@tool(name="model", context=True, agent_use=False)
|
|
407
392
|
async def set_model(model_id: str, *, context: ToolContext) -> str:
|
|
408
393
|
"""Switch the model for THIS session. Invoke as the `,model <model_id>` command.
|
|
409
394
|
|
|
@@ -419,6 +404,17 @@ async def set_model(model_id: str, *, context: ToolContext) -> str:
|
|
|
419
404
|
return f"Session model set to {model_id} (applies from the next turn)."
|
|
420
405
|
|
|
421
406
|
|
|
407
|
+
@tool(name="reasoning_effort", context=True, agent_use=False)
|
|
408
|
+
async def set_reasoning_effort(reasoning_effort: str, *, context: ToolContext) -> str:
|
|
409
|
+
"""Set the reasoning effort for this session starting from the next turn."""
|
|
410
|
+
reasoning_effort = reasoning_effort.strip()
|
|
411
|
+
if not reasoning_effort:
|
|
412
|
+
raise ValueError("reasoning_effort must not be empty")
|
|
413
|
+
context.state["reasoning_effort"] = reasoning_effort
|
|
414
|
+
await context.tape.append_event("reasoning_effort_switch", {"reasoning_effort": reasoning_effort})
|
|
415
|
+
return f"Session reasoning effort set to {reasoning_effort} (applies from the next turn)."
|
|
416
|
+
|
|
417
|
+
|
|
422
418
|
def _resolve_path(context: ToolContext, raw_path: str) -> Path:
|
|
423
419
|
workspace = context.state.get("_runtime_workspace")
|
|
424
420
|
path = Path(raw_path).expanduser()
|