bub 0.3.3__tar.gz → 0.3.4__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/.gitignore +147 -0
- {bub-0.3.3 → bub-0.3.4}/PKG-INFO +17 -16
- {bub-0.3.3 → bub-0.3.4}/pyproject.toml +34 -27
- {bub-0.3.3 → bub-0.3.4}/src/bub/__init__.py +1 -1
- {bub-0.3.3 → bub-0.3.4}/src/bub/builtin/agent.py +152 -47
- bub-0.3.4/src/bub/builtin/cli.py +201 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/builtin/hook_impl.py +6 -2
- {bub-0.3.3 → bub-0.3.4}/src/bub/builtin/tools.py +8 -2
- {bub-0.3.3 → bub-0.3.4}/src/bub/channels/base.py +12 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/channels/cli/__init__.py +37 -14
- bub-0.3.4/src/bub/channels/cli/renderer.py +82 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/channels/manager.py +18 -3
- {bub-0.3.3 → bub-0.3.4}/src/bub/channels/telegram.py +4 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/framework.py +32 -14
- {bub-0.3.3 → bub-0.3.4}/src/bub/hook_runtime.py +15 -4
- {bub-0.3.3 → bub-0.3.4}/src/bub/hookspecs.py +7 -2
- {bub-0.3.3 → bub-0.3.4}/src/bub/types.py +4 -1
- {bub-0.3.3 → bub-0.3.4}/src/skills/telegram/SKILL.md +15 -25
- {bub-0.3.3 → bub-0.3.4}/src/skills/telegram/scripts/telegram_send.py +5 -29
- {bub-0.3.3 → bub-0.3.4}/tests/test_builtin_agent.py +27 -10
- {bub-0.3.3 → bub-0.3.4}/tests/test_builtin_hook_impl.py +15 -5
- {bub-0.3.3 → bub-0.3.4}/tests/test_channels.py +27 -10
- {bub-0.3.3 → bub-0.3.4}/tests/test_subagent_tool.py +8 -1
- bub-0.3.3/src/bub/builtin/cli.py +0 -83
- bub-0.3.3/src/bub/channels/cli/renderer.py +0 -46
- {bub-0.3.3 → bub-0.3.4}/LICENSE +0 -0
- {bub-0.3.3 → bub-0.3.4}/README.md +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/__main__.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/builtin/__init__.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/builtin/auth.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/builtin/context.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/builtin/settings.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/builtin/shell_manager.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/builtin/store.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/builtin/tape.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/channels/__init__.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/channels/handler.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/channels/message.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/envelope.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/skills.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/tools.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/bub/utils.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/skills/README.md +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/skills/gh/SKILL.md +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/skills/skill-creator/SKILL.md +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/skills/skill-creator/license.txt +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/skills/skill-creator/scripts/init_skill.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/skills/skill-creator/scripts/quick_validate.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/src/skills/telegram/scripts/telegram_edit.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/tests/test_builtin_cli.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/tests/test_builtin_tools.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/tests/test_cli_help.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/tests/test_envelope.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/tests/test_file_tape_store_entry_ids.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/tests/test_fork_store_merge_back.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/tests/test_framework.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/tests/test_hook_runtime.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/tests/test_image_message.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/tests/test_settings.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/tests/test_skills.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/tests/test_tape_search_output.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/tests/test_tools.py +0 -0
- {bub-0.3.3 → bub-0.3.4}/tests/test_utils.py +0 -0
bub-0.3.4/.gitignore
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
docs/source
|
|
2
|
+
|
|
3
|
+
# From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore
|
|
4
|
+
|
|
5
|
+
# Byte-compiled / optimized / DLL files
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*$py.class
|
|
9
|
+
|
|
10
|
+
# C extensions
|
|
11
|
+
*.so
|
|
12
|
+
|
|
13
|
+
# Distribution / packaging
|
|
14
|
+
.Python
|
|
15
|
+
build/
|
|
16
|
+
develop-eggs/
|
|
17
|
+
dist/
|
|
18
|
+
downloads/
|
|
19
|
+
eggs/
|
|
20
|
+
.eggs/
|
|
21
|
+
lib/
|
|
22
|
+
lib64/
|
|
23
|
+
parts/
|
|
24
|
+
sdist/
|
|
25
|
+
var/
|
|
26
|
+
wheels/
|
|
27
|
+
share/python-wheels/
|
|
28
|
+
*.egg-info/
|
|
29
|
+
.installed.cfg
|
|
30
|
+
*.egg
|
|
31
|
+
MANIFEST
|
|
32
|
+
|
|
33
|
+
# PyInstaller
|
|
34
|
+
# Usually these files are written by a python script from a template
|
|
35
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
36
|
+
*.manifest
|
|
37
|
+
*.spec
|
|
38
|
+
|
|
39
|
+
# Installer logs
|
|
40
|
+
pip-log.txt
|
|
41
|
+
pip-delete-this-directory.txt
|
|
42
|
+
|
|
43
|
+
# Unit test / coverage reports
|
|
44
|
+
htmlcov/
|
|
45
|
+
.tox/
|
|
46
|
+
.nox/
|
|
47
|
+
.coverage
|
|
48
|
+
.coverage.*
|
|
49
|
+
.cache
|
|
50
|
+
nosetests.xml
|
|
51
|
+
coverage.xml
|
|
52
|
+
*.cover
|
|
53
|
+
*.py,cover
|
|
54
|
+
.hypothesis/
|
|
55
|
+
.pytest_cache/
|
|
56
|
+
cover/
|
|
57
|
+
|
|
58
|
+
# Translations
|
|
59
|
+
*.mo
|
|
60
|
+
*.pot
|
|
61
|
+
|
|
62
|
+
# Django stuff:
|
|
63
|
+
*.log
|
|
64
|
+
local_settings.py
|
|
65
|
+
db.sqlite3
|
|
66
|
+
db.sqlite3-journal
|
|
67
|
+
|
|
68
|
+
# Flask stuff:
|
|
69
|
+
instance/
|
|
70
|
+
.webassets-cache
|
|
71
|
+
|
|
72
|
+
# Scrapy stuff:
|
|
73
|
+
.scrapy
|
|
74
|
+
|
|
75
|
+
# Sphinx documentation
|
|
76
|
+
docs/_build/
|
|
77
|
+
|
|
78
|
+
# PyBuilder
|
|
79
|
+
.pybuilder/
|
|
80
|
+
target/
|
|
81
|
+
|
|
82
|
+
# Jupyter Notebook
|
|
83
|
+
.ipynb_checkpoints
|
|
84
|
+
|
|
85
|
+
# IPython
|
|
86
|
+
profile_default/
|
|
87
|
+
ipython_config.py
|
|
88
|
+
|
|
89
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
90
|
+
__pypackages__/
|
|
91
|
+
|
|
92
|
+
# Celery stuff
|
|
93
|
+
celerybeat-schedule
|
|
94
|
+
celerybeat.pid
|
|
95
|
+
|
|
96
|
+
# SageMath parsed files
|
|
97
|
+
*.sage.py
|
|
98
|
+
|
|
99
|
+
# Environments
|
|
100
|
+
.env
|
|
101
|
+
.venv
|
|
102
|
+
env/
|
|
103
|
+
venv/
|
|
104
|
+
ENV/
|
|
105
|
+
env.bak/
|
|
106
|
+
venv.bak/
|
|
107
|
+
.pdm-python
|
|
108
|
+
|
|
109
|
+
# Spyder project settings
|
|
110
|
+
.spyderproject
|
|
111
|
+
.spyproject
|
|
112
|
+
|
|
113
|
+
# Rope project settings
|
|
114
|
+
.ropeproject
|
|
115
|
+
|
|
116
|
+
# mkdocs documentation
|
|
117
|
+
/site
|
|
118
|
+
|
|
119
|
+
# mypy
|
|
120
|
+
.mypy_cache/
|
|
121
|
+
.dmypy.json
|
|
122
|
+
dmypy.json
|
|
123
|
+
|
|
124
|
+
# Pyre type checker
|
|
125
|
+
.pyre/
|
|
126
|
+
|
|
127
|
+
# pytype static type analyzer
|
|
128
|
+
.pytype/
|
|
129
|
+
|
|
130
|
+
# Cython debug symbols
|
|
131
|
+
cython_debug/
|
|
132
|
+
|
|
133
|
+
# Vscode config files
|
|
134
|
+
.vscode/
|
|
135
|
+
|
|
136
|
+
# PyCharm
|
|
137
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
138
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
139
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
140
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
141
|
+
#.idea/
|
|
142
|
+
|
|
143
|
+
# Reference directory - ignore all reference projects
|
|
144
|
+
reference/
|
|
145
|
+
|
|
146
|
+
# Local legacy backups created during framework migrations
|
|
147
|
+
backup/
|
{bub-0.3.3 → bub-0.3.4}/PKG-INFO
RENAMED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: bub
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: A common shape for agents that live alongside people.
|
|
5
|
-
|
|
5
|
+
Project-URL: Homepage, https://bub.build
|
|
6
|
+
Project-URL: Repository, https://github.com/bubbuild/bub
|
|
7
|
+
Project-URL: Documentation, https://bub.build
|
|
8
|
+
Author-email: Chojan Shang <psiace@apache.org>, Frost Ming <me@frostming.com>, Yihong <zouzou0208@gmail.com>
|
|
9
|
+
License-File: LICENSE
|
|
6
10
|
Classifier: Intended Audience :: Developers
|
|
7
11
|
Classifier: Programming Language :: Python
|
|
8
12
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -10,25 +14,22 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
10
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
11
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
12
16
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
-
Project-URL: Homepage, https://bub.build
|
|
14
|
-
Project-URL: Repository, https://github.com/bubbuild/bub
|
|
15
|
-
Project-URL: Documentation, https://bub.build
|
|
16
17
|
Requires-Python: <4.0,>=3.12
|
|
17
|
-
Requires-Dist:
|
|
18
|
-
Requires-Dist: pydantic-settings>=2.0.0
|
|
19
|
-
Requires-Dist: pyyaml>=6.0.0
|
|
20
|
-
Requires-Dist: pluggy>=1.6.0
|
|
21
|
-
Requires-Dist: typer>=0.9.0
|
|
22
|
-
Requires-Dist: republic>=0.5.4
|
|
18
|
+
Requires-Dist: aiohttp>=3.13.3
|
|
23
19
|
Requires-Dist: any-llm-sdk[anthropic]
|
|
24
|
-
Requires-Dist:
|
|
20
|
+
Requires-Dist: loguru>=0.7.2
|
|
21
|
+
Requires-Dist: pluggy>=1.6.0
|
|
25
22
|
Requires-Dist: prompt-toolkit>=3.0.0
|
|
23
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
24
|
+
Requires-Dist: pydantic>=2.0.0
|
|
26
25
|
Requires-Dist: python-telegram-bot>=21.0
|
|
27
|
-
Requires-Dist:
|
|
26
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
28
27
|
Requires-Dist: rapidfuzz>=3.14.3
|
|
29
|
-
Requires-Dist:
|
|
28
|
+
Requires-Dist: republic>=0.5.4
|
|
29
|
+
Requires-Dist: rich>=13.0.0
|
|
30
|
+
Requires-Dist: typer>=0.9.0
|
|
30
31
|
Provides-Extra: logfire
|
|
31
|
-
Requires-Dist: logfire>=4.31.0; extra ==
|
|
32
|
+
Requires-Dist: logfire>=4.31.0; extra == 'logfire'
|
|
32
33
|
Description-Content-Type: text/markdown
|
|
33
34
|
|
|
34
35
|
# Bub
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "bub"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.4"
|
|
4
4
|
description = "A common shape for agents that live alongside people."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Chojan Shang", email = "psiace@apache.org" },
|
|
@@ -18,6 +18,7 @@ classifiers = [
|
|
|
18
18
|
"Programming Language :: Python :: 3.14",
|
|
19
19
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
20
|
]
|
|
21
|
+
|
|
21
22
|
dependencies = [
|
|
22
23
|
"pydantic>=2.0.0",
|
|
23
24
|
"pydantic-settings>=2.0.0",
|
|
@@ -63,30 +64,22 @@ dev = [
|
|
|
63
64
|
]
|
|
64
65
|
|
|
65
66
|
[build-system]
|
|
66
|
-
requires = [
|
|
67
|
-
|
|
68
|
-
]
|
|
69
|
-
build-backend = "pdm.backend"
|
|
67
|
+
requires = ["hatchling"]
|
|
68
|
+
build-backend = "hatchling.build"
|
|
70
69
|
|
|
71
|
-
[tool.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
]
|
|
70
|
+
[tool.hatch.build.targets.sdist]
|
|
71
|
+
only-include = ["src/bub", "src/skills", "tests"]
|
|
72
|
+
|
|
73
|
+
[tool.hatch.build.targets.wheel]
|
|
74
|
+
sources = ["src"]
|
|
75
|
+
only-include = ["src/bub", "src/skills"]
|
|
76
76
|
|
|
77
77
|
[tool.vulture]
|
|
78
|
-
ignore_names = [
|
|
79
|
-
|
|
80
|
-
"Test*",
|
|
81
|
-
]
|
|
82
|
-
paths = [
|
|
83
|
-
"src",
|
|
84
|
-
]
|
|
78
|
+
ignore_names = ["test_*", "Test*"]
|
|
79
|
+
paths = ["src"]
|
|
85
80
|
|
|
86
81
|
[tool.mypy]
|
|
87
|
-
files = [
|
|
88
|
-
"src",
|
|
89
|
-
]
|
|
82
|
+
files = ["src"]
|
|
90
83
|
disallow_untyped_defs = false
|
|
91
84
|
disallow_any_unimported = false
|
|
92
85
|
no_implicit_optional = true
|
|
@@ -100,48 +93,62 @@ exclude = [
|
|
|
100
93
|
]
|
|
101
94
|
|
|
102
95
|
[tool.pytest.ini_options]
|
|
103
|
-
testpaths = [
|
|
104
|
-
"tests",
|
|
105
|
-
]
|
|
96
|
+
testpaths = ["tests"]
|
|
106
97
|
|
|
107
98
|
[tool.ruff]
|
|
108
99
|
target-version = "py312"
|
|
109
100
|
line-length = 120
|
|
110
101
|
fix = true
|
|
111
102
|
extend-exclude = [
|
|
112
|
-
"src/skills/**/scripts/*"
|
|
103
|
+
"src/skills/**/scripts/*"
|
|
113
104
|
]
|
|
114
105
|
|
|
115
106
|
[tool.ruff.lint]
|
|
116
107
|
select = [
|
|
108
|
+
# flake8-2020
|
|
117
109
|
"YTT",
|
|
110
|
+
# flake8-bandit
|
|
118
111
|
"S",
|
|
112
|
+
# flake8-bugbear
|
|
119
113
|
"B",
|
|
114
|
+
# flake8-builtins
|
|
120
115
|
"A",
|
|
116
|
+
# flake8-comprehensions
|
|
121
117
|
"C4",
|
|
118
|
+
# flake8-debugger
|
|
122
119
|
"T10",
|
|
120
|
+
# flake8-simplify
|
|
123
121
|
"SIM",
|
|
122
|
+
# isort
|
|
124
123
|
"I",
|
|
124
|
+
# mccabe
|
|
125
125
|
"C90",
|
|
126
|
+
# pycodestyle
|
|
126
127
|
"E",
|
|
127
128
|
"W",
|
|
129
|
+
# pyflakes
|
|
128
130
|
"F",
|
|
131
|
+
# pygrep-hooks
|
|
129
132
|
"PGH",
|
|
133
|
+
# pyupgrade
|
|
130
134
|
"UP",
|
|
135
|
+
# ruff
|
|
131
136
|
"RUF",
|
|
137
|
+
# tryceratops
|
|
132
138
|
"TRY",
|
|
133
139
|
]
|
|
134
140
|
ignore = [
|
|
141
|
+
# LineTooLong
|
|
135
142
|
"E501",
|
|
143
|
+
# DoNotAssignLambda
|
|
136
144
|
"E731",
|
|
145
|
+
# Avoid noisy exception-message rule for CLI/tool errors
|
|
137
146
|
"TRY003",
|
|
138
147
|
"S603",
|
|
139
148
|
]
|
|
140
149
|
|
|
141
150
|
[tool.ruff.lint.per-file-ignores]
|
|
142
|
-
"tests/*" = [
|
|
143
|
-
"S101",
|
|
144
|
-
]
|
|
151
|
+
"tests/*" = ["S101"]
|
|
145
152
|
|
|
146
153
|
[tool.ruff.format]
|
|
147
154
|
preview = true
|
|
@@ -7,7 +7,8 @@ import inspect
|
|
|
7
7
|
import re
|
|
8
8
|
import shlex
|
|
9
9
|
import time
|
|
10
|
-
from collections.abc import Collection
|
|
10
|
+
from collections.abc import AsyncGenerator, AsyncIterator, Callable, Collection, Coroutine, Iterable
|
|
11
|
+
from contextlib import AsyncExitStack
|
|
11
12
|
from dataclasses import dataclass, replace
|
|
12
13
|
from datetime import UTC, datetime
|
|
13
14
|
from functools import cached_property
|
|
@@ -15,7 +16,15 @@ from pathlib import Path
|
|
|
15
16
|
from typing import Any
|
|
16
17
|
|
|
17
18
|
from loguru import logger
|
|
18
|
-
from republic import
|
|
19
|
+
from republic import (
|
|
20
|
+
LLM,
|
|
21
|
+
AsyncStreamEvents,
|
|
22
|
+
AsyncTapeStore,
|
|
23
|
+
StreamEvent,
|
|
24
|
+
StreamState,
|
|
25
|
+
TapeContext,
|
|
26
|
+
ToolContext,
|
|
27
|
+
)
|
|
19
28
|
from republic.tape import InMemoryTapeStore, Tape
|
|
20
29
|
|
|
21
30
|
from bub.builtin.settings import AgentSettings, load_settings
|
|
@@ -29,6 +38,11 @@ from bub.utils import workspace_from_state
|
|
|
29
38
|
|
|
30
39
|
CONTINUE_PROMPT = "Continue the task."
|
|
31
40
|
HINT_RE = re.compile(r"\$([A-Za-z0-9_.-]+)")
|
|
41
|
+
_CONTEXT_LENGTH_PATTERNS = re.compile(
|
|
42
|
+
r"context.{0,20}length|maximum.{0,20}context|token.{0,10}limit|prompt.{0,10}too long|tokens? > \d+ maximum",
|
|
43
|
+
re.IGNORECASE,
|
|
44
|
+
)
|
|
45
|
+
MAX_AUTO_HANDOFF_RETRIES = 1
|
|
32
46
|
|
|
33
47
|
|
|
34
48
|
class Agent:
|
|
@@ -47,6 +61,25 @@ class Agent:
|
|
|
47
61
|
llm = _build_llm(self.settings, tape_store, self.framework.build_tape_context())
|
|
48
62
|
return TapeService(llm, self.settings.home / "tapes", tape_store)
|
|
49
63
|
|
|
64
|
+
@staticmethod
|
|
65
|
+
def _events_from_iterable(iterable: Iterable) -> AsyncStreamEvents:
|
|
66
|
+
async def generator() -> AsyncIterator:
|
|
67
|
+
for item in iterable:
|
|
68
|
+
yield item
|
|
69
|
+
|
|
70
|
+
return AsyncStreamEvents(generator())
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def _events_with_callback(
|
|
74
|
+
events: AsyncStreamEvents, callback: Callable[[], Coroutine[Any, Any, Any]]
|
|
75
|
+
) -> AsyncStreamEvents:
|
|
76
|
+
async def generator() -> AsyncIterator[StreamEvent]:
|
|
77
|
+
async for event in events:
|
|
78
|
+
yield event
|
|
79
|
+
await callback()
|
|
80
|
+
|
|
81
|
+
return AsyncStreamEvents(generator(), state=events._state)
|
|
82
|
+
|
|
50
83
|
async def run(
|
|
51
84
|
self,
|
|
52
85
|
*,
|
|
@@ -56,19 +89,33 @@ class Agent:
|
|
|
56
89
|
model: str | None = None,
|
|
57
90
|
allowed_skills: Collection[str] | None = None,
|
|
58
91
|
allowed_tools: Collection[str] | None = None,
|
|
59
|
-
) ->
|
|
92
|
+
) -> AsyncStreamEvents:
|
|
60
93
|
if not prompt:
|
|
61
|
-
|
|
94
|
+
events = [
|
|
95
|
+
StreamEvent("text", {"delta": "error: empty prompt"}),
|
|
96
|
+
StreamEvent("final", {"text": "error: empty prompt", "ok": False}),
|
|
97
|
+
]
|
|
98
|
+
return self._events_from_iterable(events)
|
|
99
|
+
|
|
62
100
|
tape = self.tapes.session_tape(session_id, workspace_from_state(state))
|
|
63
101
|
tape.context = replace(tape.context, state=state)
|
|
64
102
|
merge_back = not session_id.startswith("temp/")
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
103
|
+
stack = AsyncExitStack()
|
|
104
|
+
# the fork_tape context manager must not be exited until the last chunk of the stream is consumed.
|
|
105
|
+
# So we use an AsyncExitStack and inject a callback to the iterator.
|
|
106
|
+
await stack.enter_async_context(self.tapes.fork_tape(tape.name, merge_back=merge_back))
|
|
107
|
+
await self.tapes.ensure_bootstrap_anchor(tape.name)
|
|
108
|
+
if isinstance(prompt, str) and prompt.strip().startswith(","):
|
|
109
|
+
result = await self._run_command(tape=tape, line=prompt.strip())
|
|
110
|
+
events = self._events_from_iterable([
|
|
111
|
+
StreamEvent("text", {"delta": result}),
|
|
112
|
+
StreamEvent("final", {"text": result, "ok": True}),
|
|
113
|
+
])
|
|
114
|
+
else:
|
|
115
|
+
events = await self._agent_loop(
|
|
70
116
|
tape=tape, prompt=prompt, model=model, allowed_skills=allowed_skills, allowed_tools=allowed_tools
|
|
71
117
|
)
|
|
118
|
+
return self._events_with_callback(events, callback=stack.aclose)
|
|
72
119
|
|
|
73
120
|
async def _run_command(self, tape: Tape, *, line: str) -> str:
|
|
74
121
|
line = line[1:].strip()
|
|
@@ -118,7 +165,7 @@ class Agent:
|
|
|
118
165
|
model: str | None = None,
|
|
119
166
|
allowed_skills: Collection[str] | None = None,
|
|
120
167
|
allowed_tools: Collection[str] | None = None,
|
|
121
|
-
) ->
|
|
168
|
+
) -> AsyncStreamEvents:
|
|
122
169
|
next_prompt: str | list[dict] = prompt
|
|
123
170
|
display_model = model or self.settings.model
|
|
124
171
|
await self.tapes.append_event(
|
|
@@ -131,34 +178,61 @@ class Agent:
|
|
|
131
178
|
"allowed_tools": list(allowed_tools) if allowed_tools else None,
|
|
132
179
|
},
|
|
133
180
|
)
|
|
181
|
+
state = StreamState()
|
|
182
|
+
iterator = self._stream_events_with_auto_handoff(
|
|
183
|
+
tape=tape,
|
|
184
|
+
prompt=next_prompt,
|
|
185
|
+
state=state,
|
|
186
|
+
model=model,
|
|
187
|
+
allowed_skills=allowed_skills,
|
|
188
|
+
allowed_tools=allowed_tools,
|
|
189
|
+
)
|
|
190
|
+
return AsyncStreamEvents(iterator, state=state)
|
|
191
|
+
|
|
192
|
+
async def _stream_events_with_auto_handoff(
|
|
193
|
+
self,
|
|
194
|
+
tape: Tape,
|
|
195
|
+
prompt: str | list[dict],
|
|
196
|
+
state: StreamState,
|
|
197
|
+
model: str | None = None,
|
|
198
|
+
allowed_skills: Collection[str] | None = None,
|
|
199
|
+
allowed_tools: Collection[str] | None = None,
|
|
200
|
+
) -> AsyncGenerator[StreamEvent, None]:
|
|
201
|
+
auto_handoff_remaining = MAX_AUTO_HANDOFF_RETRIES
|
|
202
|
+
display_model = model or self.settings.model
|
|
203
|
+
next_prompt = prompt
|
|
134
204
|
for step in range(1, self.settings.max_steps + 1):
|
|
135
205
|
start = time.monotonic()
|
|
206
|
+
outcome = _ToolAutoOutcome(kind="text", text="", error="")
|
|
136
207
|
logger.info("loop.step step={} tape={} model={}", step, tape.name, display_model)
|
|
137
208
|
await self.tapes.append_event(tape.name, "loop.step.start", {"step": step, "prompt": next_prompt})
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
209
|
+
output = await self._run_once(
|
|
210
|
+
tape=tape,
|
|
211
|
+
prompt=next_prompt,
|
|
212
|
+
model=model,
|
|
213
|
+
allowed_skills=allowed_skills,
|
|
214
|
+
allowed_tools=allowed_tools,
|
|
215
|
+
)
|
|
216
|
+
async for event in output:
|
|
217
|
+
yield event
|
|
218
|
+
if event.kind == "error":
|
|
219
|
+
elapsed_ms = int((time.monotonic() - start) * 1000)
|
|
220
|
+
await self.tapes.append_event(
|
|
221
|
+
tape.name,
|
|
222
|
+
"loop.step",
|
|
223
|
+
{
|
|
224
|
+
"step": step,
|
|
225
|
+
"elapsed_ms": elapsed_ms,
|
|
226
|
+
"status": "error",
|
|
227
|
+
"error": event.data.get("message", ""),
|
|
228
|
+
"date": datetime.now(UTC).isoformat(),
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
elif event.kind == "final":
|
|
232
|
+
outcome = _resolve_tool_auto_result(event.data)
|
|
233
|
+
|
|
234
|
+
state.error = output.error
|
|
235
|
+
state.usage = output.usage
|
|
162
236
|
elapsed_ms = int((time.monotonic() - start) * 1000)
|
|
163
237
|
if outcome.kind == "text":
|
|
164
238
|
await self.tapes.append_event(
|
|
@@ -171,7 +245,7 @@ class Agent:
|
|
|
171
245
|
"date": datetime.now(UTC).isoformat(),
|
|
172
246
|
},
|
|
173
247
|
)
|
|
174
|
-
return
|
|
248
|
+
return
|
|
175
249
|
if outcome.kind == "continue":
|
|
176
250
|
if "context" in tape.context.state:
|
|
177
251
|
next_prompt = f"{CONTINUE_PROMPT} [context: {tape.context.state['context']}]"
|
|
@@ -188,6 +262,35 @@ class Agent:
|
|
|
188
262
|
},
|
|
189
263
|
)
|
|
190
264
|
continue
|
|
265
|
+
|
|
266
|
+
# Check if this is a context-length error that can be recovered via auto-handoff
|
|
267
|
+
if auto_handoff_remaining > 0 and _is_context_length_error(outcome.error):
|
|
268
|
+
auto_handoff_remaining -= 1
|
|
269
|
+
logger.warning(
|
|
270
|
+
"auto_handoff: context length exceeded, performing automatic handoff. tape={} step={}",
|
|
271
|
+
tape.name,
|
|
272
|
+
step,
|
|
273
|
+
)
|
|
274
|
+
await self.tapes.handoff(
|
|
275
|
+
tape.name,
|
|
276
|
+
name="auto_handoff/context_overflow",
|
|
277
|
+
state={"reason": "context_length_exceeded", "error": outcome.error},
|
|
278
|
+
)
|
|
279
|
+
await self.tapes.append_event(
|
|
280
|
+
tape.name,
|
|
281
|
+
"loop.step",
|
|
282
|
+
{
|
|
283
|
+
"step": step,
|
|
284
|
+
"elapsed_ms": elapsed_ms,
|
|
285
|
+
"status": "auto_handoff",
|
|
286
|
+
"error": outcome.error,
|
|
287
|
+
"date": datetime.now(UTC).isoformat(),
|
|
288
|
+
},
|
|
289
|
+
)
|
|
290
|
+
# Retry with original prompt — the handoff anchor will truncate history
|
|
291
|
+
next_prompt = prompt
|
|
292
|
+
continue
|
|
293
|
+
|
|
191
294
|
await self.tapes.append_event(
|
|
192
295
|
tape.name,
|
|
193
296
|
"loop.step",
|
|
@@ -212,7 +315,7 @@ class Agent:
|
|
|
212
315
|
expanded_skills = set(HINT_RE.findall(prompt)) & set(skill_index.keys())
|
|
213
316
|
return render_skills_prompt(list(skill_index.values()), expanded_skills=expanded_skills)
|
|
214
317
|
|
|
215
|
-
async def
|
|
318
|
+
async def _run_once(
|
|
216
319
|
self,
|
|
217
320
|
*,
|
|
218
321
|
tape: Tape,
|
|
@@ -220,7 +323,7 @@ class Agent:
|
|
|
220
323
|
model: str | None = None,
|
|
221
324
|
allowed_tools: Collection[str] | None = None,
|
|
222
325
|
allowed_skills: Collection[str] | None = None,
|
|
223
|
-
) ->
|
|
326
|
+
) -> AsyncStreamEvents:
|
|
224
327
|
prompt_text = prompt if isinstance(prompt, str) else _extract_text_from_parts(prompt)
|
|
225
328
|
if allowed_tools is not None:
|
|
226
329
|
allowed_tools = {name.casefold() for name in allowed_tools}
|
|
@@ -232,8 +335,8 @@ class Agent:
|
|
|
232
335
|
else:
|
|
233
336
|
tools = list(REGISTRY.values())
|
|
234
337
|
async with asyncio.timeout(self.settings.model_timeout_seconds):
|
|
235
|
-
return await tape.
|
|
236
|
-
prompt=prompt,
|
|
338
|
+
return await tape.stream_events_async(
|
|
339
|
+
prompt=prompt,
|
|
237
340
|
system_prompt=self._system_prompt(prompt_text, state=tape.context.state, allowed_skills=allowed_skills),
|
|
238
341
|
max_tokens=self.settings.max_tokens,
|
|
239
342
|
tools=model_tools(tools),
|
|
@@ -260,15 +363,12 @@ class _ToolAutoOutcome:
|
|
|
260
363
|
error: str = ""
|
|
261
364
|
|
|
262
365
|
|
|
263
|
-
def _resolve_tool_auto_result(
|
|
264
|
-
if
|
|
265
|
-
return _ToolAutoOutcome(kind="text", text=
|
|
266
|
-
if
|
|
366
|
+
def _resolve_tool_auto_result(final_data: dict[str, Any]) -> _ToolAutoOutcome:
|
|
367
|
+
if (text := final_data.get("text")) is not None:
|
|
368
|
+
return _ToolAutoOutcome(kind="text", text=text)
|
|
369
|
+
if final_data.get("tool_calls") or final_data.get("tool_results"):
|
|
267
370
|
return _ToolAutoOutcome(kind="continue")
|
|
268
|
-
|
|
269
|
-
return _ToolAutoOutcome(kind="error", error="tool_auto_error: unknown")
|
|
270
|
-
error_kind = getattr(output.error.kind, "value", str(output.error.kind))
|
|
271
|
-
return _ToolAutoOutcome(kind="error", error=f"{error_kind}: {output.error.message}")
|
|
371
|
+
return _ToolAutoOutcome(kind="error", error="unknown error")
|
|
272
372
|
|
|
273
373
|
|
|
274
374
|
def _build_llm(settings: AgentSettings, tape_store: AsyncTapeStore, tape_context: TapeContext) -> LLM:
|
|
@@ -318,6 +418,11 @@ def _parse_args(args_tokens: list[str]) -> Args:
|
|
|
318
418
|
return Args(positional=positional, kwargs=kwargs)
|
|
319
419
|
|
|
320
420
|
|
|
421
|
+
def _is_context_length_error(error_msg: str) -> bool:
|
|
422
|
+
"""Check whether an error message indicates a context-length / prompt-too-long failure."""
|
|
423
|
+
return bool(_CONTEXT_LENGTH_PATTERNS.search(error_msg))
|
|
424
|
+
|
|
425
|
+
|
|
321
426
|
def _extract_text_from_parts(parts: list[dict]) -> str:
|
|
322
427
|
"""Extract text content from multimodal content parts."""
|
|
323
428
|
return "\n".join(p.get("text", "") for p in parts if p.get("type") == "text")
|