bub 0.3.4__tar.gz → 0.3.6__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.3.4 → bub-0.3.6}/.gitignore +1 -0
- {bub-0.3.4 → bub-0.3.6}/PKG-INFO +5 -2
- {bub-0.3.4 → bub-0.3.6}/README.md +4 -1
- {bub-0.3.4 → bub-0.3.6}/pyproject.toml +9 -2
- bub-0.3.6/src/bub/__init__.py +19 -0
- bub-0.3.6/src/bub/_version.py +24 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/builtin/agent.py +252 -25
- {bub-0.3.4 → bub-0.3.6}/src/bub/builtin/cli.py +61 -6
- {bub-0.3.4 → bub-0.3.6}/src/bub/builtin/hook_impl.py +6 -3
- {bub-0.3.4 → bub-0.3.6}/src/bub/builtin/shell_manager.py +4 -1
- {bub-0.3.4 → bub-0.3.6}/src/bub/builtin/tools.py +3 -3
- {bub-0.3.4 → bub-0.3.6}/src/bub/channels/base.py +4 -4
- {bub-0.3.4 → bub-0.3.6}/src/bub/channels/cli/__init__.py +27 -33
- {bub-0.3.4 → bub-0.3.6}/src/bub/channels/manager.py +14 -7
- {bub-0.3.4 → bub-0.3.6}/src/bub/framework.py +27 -11
- {bub-0.3.4 → bub-0.3.6}/src/bub/hook_runtime.py +16 -1
- {bub-0.3.4 → bub-0.3.6}/src/bub/types.py +2 -2
- {bub-0.3.4 → bub-0.3.6}/src/skills/telegram/SKILL.md +17 -29
- {bub-0.3.4 → bub-0.3.6}/src/skills/telegram/scripts/telegram_edit.py +2 -1
- {bub-0.3.4 → bub-0.3.6}/src/skills/telegram/scripts/telegram_send.py +1 -1
- {bub-0.3.4 → bub-0.3.6}/tests/test_builtin_agent.py +5 -5
- bub-0.3.6/tests/test_builtin_cli.py +150 -0
- {bub-0.3.4 → bub-0.3.6}/tests/test_builtin_hook_impl.py +22 -4
- {bub-0.3.4 → bub-0.3.6}/tests/test_channels.py +81 -5
- {bub-0.3.4 → bub-0.3.6}/tests/test_framework.py +112 -0
- {bub-0.3.4 → bub-0.3.6}/tests/test_hook_runtime.py +35 -0
- {bub-0.3.4 → bub-0.3.6}/tests/test_subagent_tool.py +12 -12
- bub-0.3.4/src/bub/__init__.py +0 -8
- bub-0.3.4/tests/test_builtin_cli.py +0 -71
- {bub-0.3.4 → bub-0.3.6}/LICENSE +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/__main__.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/builtin/__init__.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/builtin/auth.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/builtin/context.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/builtin/settings.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/builtin/store.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/builtin/tape.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/channels/__init__.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/channels/cli/renderer.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/channels/handler.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/channels/message.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/channels/telegram.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/envelope.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/hookspecs.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/skills.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/tools.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/bub/utils.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/skills/README.md +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/skills/gh/SKILL.md +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/skills/skill-creator/SKILL.md +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/skills/skill-creator/license.txt +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/skills/skill-creator/scripts/init_skill.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/src/skills/skill-creator/scripts/quick_validate.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/tests/test_builtin_tools.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/tests/test_cli_help.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/tests/test_envelope.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/tests/test_file_tape_store_entry_ids.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/tests/test_fork_store_merge_back.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/tests/test_image_message.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/tests/test_settings.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/tests/test_skills.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/tests/test_tape_search_output.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/tests/test_tools.py +0 -0
- {bub-0.3.4 → bub-0.3.6}/tests/test_utils.py +0 -0
{bub-0.3.4 → bub-0.3.6}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bub
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
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
|
|
@@ -130,11 +130,14 @@ See the [Extension Guide](https://bub.build/extension-guide/) for hook semantics
|
|
|
130
130
|
| `bub chat` | Interactive REPL |
|
|
131
131
|
| `bub run MESSAGE` | One-shot turn |
|
|
132
132
|
| `bub gateway` | Channel listener (Telegram, etc.) |
|
|
133
|
+
| `bub install` | Install or sync Bub plugin deps |
|
|
134
|
+
| `bub update` | Upgrade Bub plugin deps |
|
|
133
135
|
| `bub login openai` | OpenAI Codex OAuth |
|
|
134
|
-
| `bub hooks` | Print hook-to-plugin bindings |
|
|
135
136
|
|
|
136
137
|
Lines starting with `,` enter internal command mode (`,help`, `,skill name=my-skill`, `,fs.read path=README.md`).
|
|
137
138
|
|
|
139
|
+
`bub hooks` still exists for diagnostics, but it is hidden from top-level help. `bub install` and `bub update` manage a separate uv project for Bub plugins, defaulting to `~/.bub/bub-project` or `BUB_PROJECT`.
|
|
140
|
+
|
|
138
141
|
## Configuration
|
|
139
142
|
|
|
140
143
|
| Variable | Default | Description |
|
|
@@ -96,11 +96,14 @@ See the [Extension Guide](https://bub.build/extension-guide/) for hook semantics
|
|
|
96
96
|
| `bub chat` | Interactive REPL |
|
|
97
97
|
| `bub run MESSAGE` | One-shot turn |
|
|
98
98
|
| `bub gateway` | Channel listener (Telegram, etc.) |
|
|
99
|
+
| `bub install` | Install or sync Bub plugin deps |
|
|
100
|
+
| `bub update` | Upgrade Bub plugin deps |
|
|
99
101
|
| `bub login openai` | OpenAI Codex OAuth |
|
|
100
|
-
| `bub hooks` | Print hook-to-plugin bindings |
|
|
101
102
|
|
|
102
103
|
Lines starting with `,` enter internal command mode (`,help`, `,skill name=my-skill`, `,fs.read path=README.md`).
|
|
103
104
|
|
|
105
|
+
`bub hooks` still exists for diagnostics, but it is hidden from top-level help. `bub install` and `bub update` manage a separate uv project for Bub plugins, defaulting to `~/.bub/bub-project` or `BUB_PROJECT`.
|
|
106
|
+
|
|
104
107
|
## Configuration
|
|
105
108
|
|
|
106
109
|
| Variable | Default | Description |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "bub"
|
|
3
|
-
|
|
3
|
+
dynamic = ["version"]
|
|
4
4
|
description = "A common shape for agents that live alongside people."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Chojan Shang", email = "psiace@apache.org" },
|
|
@@ -64,9 +64,16 @@ dev = [
|
|
|
64
64
|
]
|
|
65
65
|
|
|
66
66
|
[build-system]
|
|
67
|
-
requires = ["hatchling"]
|
|
67
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
68
68
|
build-backend = "hatchling.build"
|
|
69
69
|
|
|
70
|
+
[tool.hatch.version]
|
|
71
|
+
source = "vcs"
|
|
72
|
+
fallback-version = "0.3.0"
|
|
73
|
+
|
|
74
|
+
[tool.hatch.build.hooks.vcs]
|
|
75
|
+
version-file = "src/bub/_version.py"
|
|
76
|
+
|
|
70
77
|
[tool.hatch.build.targets.sdist]
|
|
71
78
|
only-include = ["src/bub", "src/skills", "tests"]
|
|
72
79
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Bub framework package."""
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from importlib.metadata import PackageNotFoundError
|
|
5
|
+
from importlib.metadata import version as metadata_version
|
|
6
|
+
|
|
7
|
+
from bub.framework import BubFramework
|
|
8
|
+
from bub.hookspecs import hookimpl
|
|
9
|
+
from bub.tools import tool
|
|
10
|
+
|
|
11
|
+
__all__ = ["BubFramework", "hookimpl", "tool"]
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
__version__ = import_module("bub._version").version
|
|
15
|
+
except ModuleNotFoundError:
|
|
16
|
+
try:
|
|
17
|
+
__version__ = metadata_version("bub")
|
|
18
|
+
except PackageNotFoundError:
|
|
19
|
+
__version__ = "0.0.0"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.3.6'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 6)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -13,16 +13,18 @@ from dataclasses import dataclass, replace
|
|
|
13
13
|
from datetime import UTC, datetime
|
|
14
14
|
from functools import cached_property
|
|
15
15
|
from pathlib import Path
|
|
16
|
-
from typing import Any
|
|
16
|
+
from typing import Any, Literal, overload
|
|
17
17
|
|
|
18
18
|
from loguru import logger
|
|
19
19
|
from republic import (
|
|
20
20
|
LLM,
|
|
21
21
|
AsyncStreamEvents,
|
|
22
22
|
AsyncTapeStore,
|
|
23
|
+
RepublicError,
|
|
23
24
|
StreamEvent,
|
|
24
25
|
StreamState,
|
|
25
26
|
TapeContext,
|
|
27
|
+
ToolAutoResult,
|
|
26
28
|
ToolContext,
|
|
27
29
|
)
|
|
28
30
|
from republic.tape import InMemoryTapeStore, Tape
|
|
@@ -89,6 +91,29 @@ class Agent:
|
|
|
89
91
|
model: str | None = None,
|
|
90
92
|
allowed_skills: Collection[str] | None = None,
|
|
91
93
|
allowed_tools: Collection[str] | None = None,
|
|
94
|
+
) -> str:
|
|
95
|
+
if not prompt:
|
|
96
|
+
return "error: empty prompt"
|
|
97
|
+
tape = self.tapes.session_tape(session_id, workspace_from_state(state))
|
|
98
|
+
tape.context = replace(tape.context, state=state)
|
|
99
|
+
merge_back = not session_id.startswith("temp/")
|
|
100
|
+
async with self.tapes.fork_tape(tape.name, merge_back=merge_back):
|
|
101
|
+
await self.tapes.ensure_bootstrap_anchor(tape.name)
|
|
102
|
+
if isinstance(prompt, str) and prompt.strip().startswith(","):
|
|
103
|
+
return await self._run_command(tape=tape, line=prompt.strip())
|
|
104
|
+
return await self._agent_loop(
|
|
105
|
+
tape=tape, prompt=prompt, model=model, allowed_skills=allowed_skills, allowed_tools=allowed_tools
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
async def run_stream(
|
|
109
|
+
self,
|
|
110
|
+
*,
|
|
111
|
+
session_id: str,
|
|
112
|
+
prompt: str | list[dict],
|
|
113
|
+
state: State,
|
|
114
|
+
model: str | None = None,
|
|
115
|
+
allowed_skills: Collection[str] | None = None,
|
|
116
|
+
allowed_tools: Collection[str] | None = None,
|
|
92
117
|
) -> AsyncStreamEvents:
|
|
93
118
|
if not prompt:
|
|
94
119
|
events = [
|
|
@@ -113,7 +138,12 @@ class Agent:
|
|
|
113
138
|
])
|
|
114
139
|
else:
|
|
115
140
|
events = await self._agent_loop(
|
|
116
|
-
tape=tape,
|
|
141
|
+
tape=tape,
|
|
142
|
+
prompt=prompt,
|
|
143
|
+
model=model,
|
|
144
|
+
allowed_skills=allowed_skills,
|
|
145
|
+
allowed_tools=allowed_tools,
|
|
146
|
+
stream_output=True,
|
|
117
147
|
)
|
|
118
148
|
return self._events_with_callback(events, callback=stack.aclose)
|
|
119
149
|
|
|
@@ -157,6 +187,30 @@ class Agent:
|
|
|
157
187
|
}
|
|
158
188
|
await self.tapes.append_event(tape.name, "command", event_payload)
|
|
159
189
|
|
|
190
|
+
@overload
|
|
191
|
+
async def _agent_loop(
|
|
192
|
+
self,
|
|
193
|
+
*,
|
|
194
|
+
tape: Tape,
|
|
195
|
+
prompt: str | list[dict],
|
|
196
|
+
model: str | None = ...,
|
|
197
|
+
allowed_skills: Collection[str] | None = ...,
|
|
198
|
+
allowed_tools: Collection[str] | None = ...,
|
|
199
|
+
stream_output: Literal[False] = ...,
|
|
200
|
+
) -> str: ...
|
|
201
|
+
|
|
202
|
+
@overload
|
|
203
|
+
async def _agent_loop(
|
|
204
|
+
self,
|
|
205
|
+
*,
|
|
206
|
+
tape: Tape,
|
|
207
|
+
prompt: str | list[dict],
|
|
208
|
+
model: str | None = ...,
|
|
209
|
+
allowed_skills: Collection[str] | None = ...,
|
|
210
|
+
allowed_tools: Collection[str] | None = ...,
|
|
211
|
+
stream_output: Literal[True] = ...,
|
|
212
|
+
) -> AsyncStreamEvents: ...
|
|
213
|
+
|
|
160
214
|
async def _agent_loop(
|
|
161
215
|
self,
|
|
162
216
|
*,
|
|
@@ -165,7 +219,8 @@ class Agent:
|
|
|
165
219
|
model: str | None = None,
|
|
166
220
|
allowed_skills: Collection[str] | None = None,
|
|
167
221
|
allowed_tools: Collection[str] | None = None,
|
|
168
|
-
|
|
222
|
+
stream_output: bool = False,
|
|
223
|
+
) -> AsyncStreamEvents | str:
|
|
169
224
|
next_prompt: str | list[dict] = prompt
|
|
170
225
|
display_model = model or self.settings.model
|
|
171
226
|
await self.tapes.append_event(
|
|
@@ -178,16 +233,137 @@ class Agent:
|
|
|
178
233
|
"allowed_tools": list(allowed_tools) if allowed_tools else None,
|
|
179
234
|
},
|
|
180
235
|
)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
236
|
+
if stream_output:
|
|
237
|
+
state = StreamState()
|
|
238
|
+
iterator = self._stream_events_with_auto_handoff(
|
|
239
|
+
tape=tape,
|
|
240
|
+
prompt=next_prompt,
|
|
241
|
+
state=state,
|
|
242
|
+
model=model,
|
|
243
|
+
allowed_skills=allowed_skills,
|
|
244
|
+
allowed_tools=allowed_tools,
|
|
245
|
+
)
|
|
246
|
+
return AsyncStreamEvents(iterator, state=state)
|
|
247
|
+
else:
|
|
248
|
+
return await self._run_tools_with_auto_handoff(
|
|
249
|
+
tape=tape,
|
|
250
|
+
prompt=next_prompt,
|
|
251
|
+
model=model,
|
|
252
|
+
allowed_skills=allowed_skills,
|
|
253
|
+
allowed_tools=allowed_tools,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
async def _run_tools_with_auto_handoff(
|
|
257
|
+
self,
|
|
258
|
+
tape: Tape,
|
|
259
|
+
prompt: str | list[dict],
|
|
260
|
+
model: str | None = None,
|
|
261
|
+
allowed_skills: Collection[str] | None = None,
|
|
262
|
+
allowed_tools: Collection[str] | None = None,
|
|
263
|
+
) -> str:
|
|
264
|
+
auto_handoff_remaining = MAX_AUTO_HANDOFF_RETRIES
|
|
265
|
+
display_model = model or self.settings.model
|
|
266
|
+
next_prompt = prompt
|
|
267
|
+
for step in range(1, self.settings.max_steps + 1):
|
|
268
|
+
start = time.monotonic()
|
|
269
|
+
logger.info("loop.step step={} tape={} model={}", step, tape.name, display_model)
|
|
270
|
+
await self.tapes.append_event(tape.name, "loop.step.start", {"step": step, "prompt": next_prompt})
|
|
271
|
+
try:
|
|
272
|
+
output = await self._run_once(
|
|
273
|
+
tape=tape,
|
|
274
|
+
prompt=next_prompt,
|
|
275
|
+
model=model,
|
|
276
|
+
allowed_skills=allowed_skills,
|
|
277
|
+
allowed_tools=allowed_tools,
|
|
278
|
+
)
|
|
279
|
+
except Exception as exc:
|
|
280
|
+
elapsed_ms = int((time.monotonic() - start) * 1000)
|
|
281
|
+
await self.tapes.append_event(
|
|
282
|
+
tape.name,
|
|
283
|
+
"loop.step",
|
|
284
|
+
{
|
|
285
|
+
"step": step,
|
|
286
|
+
"elapsed_ms": elapsed_ms,
|
|
287
|
+
"status": "error",
|
|
288
|
+
"error": f"{exc!s}",
|
|
289
|
+
"date": datetime.now(UTC).isoformat(),
|
|
290
|
+
},
|
|
291
|
+
)
|
|
292
|
+
raise
|
|
293
|
+
|
|
294
|
+
outcome = _resolve_tool_auto_result(output)
|
|
295
|
+
elapsed_ms = int((time.monotonic() - start) * 1000)
|
|
296
|
+
if outcome.kind == "text":
|
|
297
|
+
await self.tapes.append_event(
|
|
298
|
+
tape.name,
|
|
299
|
+
"loop.step",
|
|
300
|
+
{
|
|
301
|
+
"step": step,
|
|
302
|
+
"elapsed_ms": elapsed_ms,
|
|
303
|
+
"status": "ok",
|
|
304
|
+
"date": datetime.now(UTC).isoformat(),
|
|
305
|
+
},
|
|
306
|
+
)
|
|
307
|
+
return outcome.text
|
|
308
|
+
if outcome.kind == "continue":
|
|
309
|
+
if "context" in tape.context.state:
|
|
310
|
+
next_prompt = f"{CONTINUE_PROMPT} [context: {tape.context.state['context']}]"
|
|
311
|
+
else:
|
|
312
|
+
next_prompt = CONTINUE_PROMPT
|
|
313
|
+
await self.tapes.append_event(
|
|
314
|
+
tape.name,
|
|
315
|
+
"loop.step",
|
|
316
|
+
{
|
|
317
|
+
"step": step,
|
|
318
|
+
"elapsed_ms": elapsed_ms,
|
|
319
|
+
"status": "continue",
|
|
320
|
+
"date": datetime.now(UTC).isoformat(),
|
|
321
|
+
},
|
|
322
|
+
)
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
# Check if this is a context-length error that can be recovered via auto-handoff
|
|
326
|
+
if auto_handoff_remaining > 0 and _is_context_length_error(outcome.error):
|
|
327
|
+
auto_handoff_remaining -= 1
|
|
328
|
+
logger.warning(
|
|
329
|
+
"auto_handoff: context length exceeded, performing automatic handoff. tape={} step={}",
|
|
330
|
+
tape.name,
|
|
331
|
+
step,
|
|
332
|
+
)
|
|
333
|
+
await self.tapes.handoff(
|
|
334
|
+
tape.name,
|
|
335
|
+
name="auto_handoff/context_overflow",
|
|
336
|
+
state={"reason": "context_length_exceeded", "error": outcome.error},
|
|
337
|
+
)
|
|
338
|
+
await self.tapes.append_event(
|
|
339
|
+
tape.name,
|
|
340
|
+
"loop.step",
|
|
341
|
+
{
|
|
342
|
+
"step": step,
|
|
343
|
+
"elapsed_ms": elapsed_ms,
|
|
344
|
+
"status": "auto_handoff",
|
|
345
|
+
"error": outcome.error,
|
|
346
|
+
"date": datetime.now(UTC).isoformat(),
|
|
347
|
+
},
|
|
348
|
+
)
|
|
349
|
+
# Retry with original prompt — the handoff anchor will truncate history
|
|
350
|
+
next_prompt = prompt
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
await self.tapes.append_event(
|
|
354
|
+
tape.name,
|
|
355
|
+
"loop.step",
|
|
356
|
+
{
|
|
357
|
+
"step": step,
|
|
358
|
+
"elapsed_ms": elapsed_ms,
|
|
359
|
+
"status": "error",
|
|
360
|
+
"error": outcome.error,
|
|
361
|
+
"date": datetime.now(UTC).isoformat(),
|
|
362
|
+
},
|
|
363
|
+
)
|
|
364
|
+
raise RuntimeError(outcome.error)
|
|
365
|
+
|
|
366
|
+
raise RuntimeError(f"max_steps_reached={self.settings.max_steps}")
|
|
191
367
|
|
|
192
368
|
async def _stream_events_with_auto_handoff(
|
|
193
369
|
self,
|
|
@@ -212,6 +388,7 @@ class Agent:
|
|
|
212
388
|
model=model,
|
|
213
389
|
allowed_skills=allowed_skills,
|
|
214
390
|
allowed_tools=allowed_tools,
|
|
391
|
+
stream_output=True,
|
|
215
392
|
)
|
|
216
393
|
async for event in output:
|
|
217
394
|
yield event
|
|
@@ -229,7 +406,7 @@ class Agent:
|
|
|
229
406
|
},
|
|
230
407
|
)
|
|
231
408
|
elif event.kind == "final":
|
|
232
|
-
outcome =
|
|
409
|
+
outcome = _resolve_final_data(event.data, output.error)
|
|
233
410
|
|
|
234
411
|
state.error = output.error
|
|
235
412
|
state.usage = output.usage
|
|
@@ -315,6 +492,30 @@ class Agent:
|
|
|
315
492
|
expanded_skills = set(HINT_RE.findall(prompt)) & set(skill_index.keys())
|
|
316
493
|
return render_skills_prompt(list(skill_index.values()), expanded_skills=expanded_skills)
|
|
317
494
|
|
|
495
|
+
@overload
|
|
496
|
+
async def _run_once(
|
|
497
|
+
self,
|
|
498
|
+
*,
|
|
499
|
+
tape: Tape,
|
|
500
|
+
prompt: str | list[dict],
|
|
501
|
+
model: str | None = ...,
|
|
502
|
+
allowed_skills: Collection[str] | None = ...,
|
|
503
|
+
allowed_tools: Collection[str] | None = ...,
|
|
504
|
+
stream_output: Literal[False] = ...,
|
|
505
|
+
) -> ToolAutoResult: ...
|
|
506
|
+
|
|
507
|
+
@overload
|
|
508
|
+
async def _run_once(
|
|
509
|
+
self,
|
|
510
|
+
*,
|
|
511
|
+
tape: Tape,
|
|
512
|
+
prompt: str | list[dict],
|
|
513
|
+
model: str | None = ...,
|
|
514
|
+
allowed_skills: Collection[str] | None = ...,
|
|
515
|
+
allowed_tools: Collection[str] | None = ...,
|
|
516
|
+
stream_output: Literal[True] = ...,
|
|
517
|
+
) -> AsyncStreamEvents: ...
|
|
518
|
+
|
|
318
519
|
async def _run_once(
|
|
319
520
|
self,
|
|
320
521
|
*,
|
|
@@ -323,7 +524,8 @@ class Agent:
|
|
|
323
524
|
model: str | None = None,
|
|
324
525
|
allowed_tools: Collection[str] | None = None,
|
|
325
526
|
allowed_skills: Collection[str] | None = None,
|
|
326
|
-
|
|
527
|
+
stream_output: bool = False,
|
|
528
|
+
) -> AsyncStreamEvents | ToolAutoResult:
|
|
327
529
|
prompt_text = prompt if isinstance(prompt, str) else _extract_text_from_parts(prompt)
|
|
328
530
|
if allowed_tools is not None:
|
|
329
531
|
allowed_tools = {name.casefold() for name in allowed_tools}
|
|
@@ -335,13 +537,26 @@ class Agent:
|
|
|
335
537
|
else:
|
|
336
538
|
tools = list(REGISTRY.values())
|
|
337
539
|
async with asyncio.timeout(self.settings.model_timeout_seconds):
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
540
|
+
if stream_output:
|
|
541
|
+
return await tape.stream_events_async(
|
|
542
|
+
prompt=prompt,
|
|
543
|
+
system_prompt=self._system_prompt(
|
|
544
|
+
prompt_text, state=tape.context.state, allowed_skills=allowed_skills
|
|
545
|
+
),
|
|
546
|
+
max_tokens=self.settings.max_tokens,
|
|
547
|
+
tools=model_tools(tools),
|
|
548
|
+
model=model,
|
|
549
|
+
)
|
|
550
|
+
else:
|
|
551
|
+
return await tape.run_tools_async(
|
|
552
|
+
prompt=prompt,
|
|
553
|
+
system_prompt=self._system_prompt(
|
|
554
|
+
prompt_text, state=tape.context.state, allowed_skills=allowed_skills
|
|
555
|
+
),
|
|
556
|
+
max_tokens=self.settings.max_tokens,
|
|
557
|
+
tools=model_tools(tools),
|
|
558
|
+
model=model,
|
|
559
|
+
)
|
|
345
560
|
|
|
346
561
|
def _system_prompt(self, prompt: str, state: State, allowed_skills: set[str] | None = None) -> str:
|
|
347
562
|
blocks: list[str] = []
|
|
@@ -363,12 +578,24 @@ class _ToolAutoOutcome:
|
|
|
363
578
|
error: str = ""
|
|
364
579
|
|
|
365
580
|
|
|
366
|
-
def
|
|
581
|
+
def _resolve_final_data(final_data: dict[str, Any], error: RepublicError | None) -> _ToolAutoOutcome:
|
|
582
|
+
if final_data.get("tool_calls") or final_data.get("tool_results"):
|
|
583
|
+
return _ToolAutoOutcome(kind="continue")
|
|
367
584
|
if (text := final_data.get("text")) is not None:
|
|
368
585
|
return _ToolAutoOutcome(kind="text", text=text)
|
|
369
|
-
|
|
586
|
+
error_message = error.message if error else ""
|
|
587
|
+
return _ToolAutoOutcome(kind="error", error=error_message or "unknown error")
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _resolve_tool_auto_result(output: ToolAutoResult) -> _ToolAutoOutcome:
|
|
591
|
+
if output.kind == "text":
|
|
592
|
+
return _ToolAutoOutcome(kind="text", text=output.text or "")
|
|
593
|
+
if output.kind == "tools" or output.tool_calls or output.tool_results:
|
|
370
594
|
return _ToolAutoOutcome(kind="continue")
|
|
371
|
-
|
|
595
|
+
if output.error is None:
|
|
596
|
+
return _ToolAutoOutcome(kind="error", error="tool_auto_error: unknown")
|
|
597
|
+
error_kind = getattr(output.error.kind, "value", str(output.error.kind))
|
|
598
|
+
return _ToolAutoOutcome(kind="error", error=f"{error_kind}: {output.error.message}")
|
|
372
599
|
|
|
373
600
|
|
|
374
601
|
def _build_llm(settings: AgentSettings, tape_store: AsyncTapeStore, tape_context: TapeContext) -> LLM:
|
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import asyncio
|
|
7
|
+
import json
|
|
7
8
|
import os
|
|
8
9
|
import subprocess
|
|
9
10
|
import sys
|
|
10
11
|
from functools import lru_cache
|
|
12
|
+
from importlib import metadata
|
|
11
13
|
from pathlib import Path
|
|
14
|
+
from urllib.parse import unquote, urlsplit
|
|
15
|
+
from urllib.request import url2pathname
|
|
12
16
|
|
|
13
17
|
import typer
|
|
14
18
|
|
|
@@ -79,7 +83,7 @@ def chat(
|
|
|
79
83
|
|
|
80
84
|
framework = ctx.ensure_object(BubFramework)
|
|
81
85
|
|
|
82
|
-
manager = ChannelManager(framework, enabled_channels=["cli"])
|
|
86
|
+
manager = ChannelManager(framework, enabled_channels=["cli"], stream_output=True)
|
|
83
87
|
channel = manager.get_channel("cli")
|
|
84
88
|
if channel is None:
|
|
85
89
|
typer.echo("CLI channel not found. Please check your hook implementations.")
|
|
@@ -148,15 +152,67 @@ def _build_requirement(spec: str) -> str:
|
|
|
148
152
|
return f"git+https://github.com/{repo}.git{ref}"
|
|
149
153
|
else:
|
|
150
154
|
# Assume it's a package name in bub-contrib
|
|
151
|
-
name,
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
name, has_ref, ref = spec.partition("@")
|
|
156
|
+
if has_ref:
|
|
157
|
+
ref = f"@{ref}"
|
|
158
|
+
return f"git+{BUB_CONTRIB_REPO}{ref}#subdirectory=packages/{name}"
|
|
159
|
+
else: # PyPI package name
|
|
160
|
+
return name
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _build_local_requirement_path(url: str, subdirectory: str | None = None) -> str | None:
|
|
164
|
+
parsed = urlsplit(url)
|
|
165
|
+
if parsed.scheme != "file":
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
path = parsed.path
|
|
169
|
+
if parsed.netloc and parsed.netloc != "localhost":
|
|
170
|
+
path = f"//{parsed.netloc}{path}"
|
|
171
|
+
local_path = Path(url2pathname(unquote(path)))
|
|
172
|
+
if subdirectory:
|
|
173
|
+
local_path /= subdirectory
|
|
174
|
+
return os.fspath(local_path)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _build_bub_requirement() -> list[str]:
|
|
178
|
+
dist = metadata.distribution("bub")
|
|
179
|
+
dist_name = dist.name
|
|
180
|
+
direct_url_text = dist.read_text("direct_url.json")
|
|
181
|
+
if not direct_url_text:
|
|
182
|
+
return [dist_name]
|
|
183
|
+
|
|
184
|
+
direct_url = json.loads(direct_url_text)
|
|
185
|
+
requirement_url = str(direct_url["url"])
|
|
186
|
+
subdirectory = direct_url.get("subdirectory")
|
|
187
|
+
normalized_subdirectory = subdirectory if isinstance(subdirectory, str) and subdirectory else None
|
|
188
|
+
|
|
189
|
+
local_path = _build_local_requirement_path(requirement_url, normalized_subdirectory)
|
|
190
|
+
if local_path is not None:
|
|
191
|
+
dir_info = direct_url.get("dir_info")
|
|
192
|
+
editable = isinstance(dir_info, dict) and bool(dir_info.get("editable"))
|
|
193
|
+
return ["--editable", local_path] if editable else [local_path]
|
|
194
|
+
|
|
195
|
+
vcs_info = direct_url.get("vcs_info")
|
|
196
|
+
if isinstance(vcs_info, dict):
|
|
197
|
+
vcs = vcs_info.get("vcs")
|
|
198
|
+
requested_revision = vcs_info.get("requested_revision")
|
|
199
|
+
if isinstance(vcs, str) and vcs:
|
|
200
|
+
requirement_url = f"{vcs}+{requirement_url}"
|
|
201
|
+
if isinstance(requested_revision, str) and requested_revision:
|
|
202
|
+
requirement_url = f"{requirement_url}@{requested_revision}"
|
|
203
|
+
|
|
204
|
+
if normalized_subdirectory:
|
|
205
|
+
requirement_url = f"{requirement_url}#subdirectory={normalized_subdirectory}"
|
|
206
|
+
|
|
207
|
+
return [requirement_url]
|
|
154
208
|
|
|
155
209
|
|
|
156
210
|
def _ensure_project(project: Path) -> None:
|
|
157
211
|
if (project / "pyproject.toml").is_file():
|
|
158
212
|
return
|
|
159
213
|
_uv("init", "--bare", "--name", "bub-project", "--app", cwd=project)
|
|
214
|
+
bub_requirement = _build_bub_requirement()
|
|
215
|
+
_uv("add", "--active", "--no-sync", *bub_requirement, cwd=project)
|
|
160
216
|
|
|
161
217
|
|
|
162
218
|
def install(
|
|
@@ -180,8 +236,7 @@ def uninstall(
|
|
|
180
236
|
) -> None:
|
|
181
237
|
"""Uninstall a plugin from Bub's environment."""
|
|
182
238
|
_ensure_project(project)
|
|
183
|
-
_uv("remove", "--active",
|
|
184
|
-
_uv("sync", "--active", "--frozen", "--inexact", cwd=project)
|
|
239
|
+
_uv("remove", "--active", *packages, cwd=project)
|
|
185
240
|
|
|
186
241
|
|
|
187
242
|
def update(
|
|
@@ -106,9 +106,13 @@ class BuiltinImpl:
|
|
|
106
106
|
return text
|
|
107
107
|
|
|
108
108
|
@hookimpl
|
|
109
|
-
async def
|
|
109
|
+
async def run_model(self, prompt: str | list[dict], session_id: str, state: State) -> str:
|
|
110
110
|
return await self.agent.run(session_id=session_id, prompt=prompt, state=state)
|
|
111
111
|
|
|
112
|
+
@hookimpl
|
|
113
|
+
async def run_model_stream(self, prompt: str | list[dict], session_id: str, state: State) -> AsyncStreamEvents:
|
|
114
|
+
return await self.agent.run_stream(session_id=session_id, prompt=prompt, state=state)
|
|
115
|
+
|
|
112
116
|
@hookimpl
|
|
113
117
|
def register_cli_commands(self, app: typer.Typer) -> None:
|
|
114
118
|
from bub.builtin import cli
|
|
@@ -119,8 +123,7 @@ class BuiltinImpl:
|
|
|
119
123
|
app.command("hooks", hidden=True)(cli.list_hooks)
|
|
120
124
|
app.command("gateway")(cli.gateway)
|
|
121
125
|
app.command("install")(cli.install)
|
|
122
|
-
|
|
123
|
-
# app.command("uninstall")(cli.uninstall)
|
|
126
|
+
app.command("uninstall")(cli.uninstall)
|
|
124
127
|
app.command("update")(cli.update)
|
|
125
128
|
|
|
126
129
|
def _read_agents_file(self, state: State) -> str:
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import contextlib
|
|
5
5
|
import os
|
|
6
|
+
import shutil
|
|
6
7
|
import uuid
|
|
7
8
|
from dataclasses import dataclass, field
|
|
8
9
|
|
|
@@ -30,6 +31,8 @@ class ManagedShell:
|
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
class ShellManager:
|
|
34
|
+
SHELL = shutil.which("bash") or shutil.which("sh") if os.name != "nt" else None
|
|
35
|
+
|
|
33
36
|
def __init__(self) -> None:
|
|
34
37
|
self._shells: dict[str, ManagedShell] = {}
|
|
35
38
|
|
|
@@ -39,7 +42,7 @@ class ShellManager:
|
|
|
39
42
|
cwd=cwd,
|
|
40
43
|
stdout=asyncio.subprocess.PIPE,
|
|
41
44
|
stderr=asyncio.subprocess.PIPE,
|
|
42
|
-
executable=
|
|
45
|
+
executable=self.SHELL,
|
|
43
46
|
)
|
|
44
47
|
shell = ManagedShell(shell_id=f"bash-{uuid.uuid4().hex[:8]}", cmd=cmd, cwd=cwd, process=process)
|
|
45
48
|
shell.read_tasks.extend([
|
|
@@ -42,9 +42,9 @@ class SearchInput(BaseModel):
|
|
|
42
42
|
limit: int = Field(20, description="Maximum number of search results to return.")
|
|
43
43
|
start: str | None = Field(None, description="Optional start date to filter entries (ISO format).")
|
|
44
44
|
end: str | None = Field(None, description="Optional end date to filter entries (ISO format).")
|
|
45
|
-
kinds: list[
|
|
45
|
+
kinds: list[str] = Field(
|
|
46
46
|
default=["message", "tool_result"],
|
|
47
|
-
description="Optional list of entry kinds to filter search results.",
|
|
47
|
+
description="Optional list of entry kinds to filter search results. Can include 'event', 'anchor', 'system', 'message', 'tool_call', 'tool_result'.",
|
|
48
48
|
)
|
|
49
49
|
|
|
50
50
|
|
|
@@ -267,7 +267,7 @@ async def run_subagent(param: SubAgentInput, *, context: ToolContext) -> str:
|
|
|
267
267
|
state = {**context.state, "session_id": subagent_session}
|
|
268
268
|
allowed_tools = resolve_tool_names(param.allowed_tools or None, exclude={"subagent"})
|
|
269
269
|
output = ""
|
|
270
|
-
async for event in await agent.
|
|
270
|
+
async for event in await agent.run_stream(
|
|
271
271
|
session_id=subagent_session,
|
|
272
272
|
prompt=param.prompt,
|
|
273
273
|
state=state,
|