@steerable/runtime-win32-x64 0.9.2 → 0.10.0
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.
- package/package.json +1 -1
- package/python-runtime/manifest.json +1 -1
- package/python-runtime/python/Lib/site-packages/cffi-2.1.1.dist-info/RECORD +1 -1
- package/python-runtime/python/Lib/site-packages/httpx-0.28.1.dist-info/RECORD +1 -1
- package/python-runtime/python/Lib/site-packages/idna-3.20.dist-info/RECORD +1 -1
- package/python-runtime/python/Lib/site-packages/jsonschema-4.26.0.dist-info/RECORD +1 -1
- package/python-runtime/python/Lib/site-packages/mcp-1.30.0.dist-info/RECORD +1 -1
- package/python-runtime/python/Lib/site-packages/python_dotenv-1.2.4.dist-info/RECORD +1 -1
- package/python-runtime/python/Lib/site-packages/pywin32-312.dist-info/RECORD +2 -2
- package/python-runtime/python/Lib/site-packages/{steerable_agent_harness-0.9.2.dist-info → steerable_agent_harness-0.10.0.dist-info}/METADATA +1 -1
- package/python-runtime/python/Lib/site-packages/{steerable_agent_harness-0.9.2.dist-info → steerable_agent_harness-0.10.0.dist-info}/RECORD +7 -7
- package/python-runtime/python/Lib/site-packages/{steerable_agent_protocol-0.9.2.dist-info → steerable_agent_protocol-0.10.0.dist-info}/METADATA +1 -1
- package/python-runtime/python/Lib/site-packages/steerable_agent_protocol-0.10.0.dist-info/RECORD +11 -0
- package/python-runtime/python/Lib/site-packages/steerable_agent_runtime/__init__.py +48 -0
- package/python-runtime/python/Lib/site-packages/steerable_agent_runtime/budget.py +124 -0
- package/python-runtime/python/Lib/site-packages/steerable_agent_runtime/child_notice.py +84 -0
- package/python-runtime/python/Lib/site-packages/steerable_agent_runtime/child_record.py +174 -0
- package/python-runtime/python/Lib/site-packages/steerable_agent_runtime/engines.py +499 -0
- package/python-runtime/python/Lib/site-packages/steerable_agent_runtime/history.py +114 -1
- package/python-runtime/python/Lib/site-packages/steerable_agent_runtime/orchestration.py +496 -33
- package/python-runtime/python/Lib/site-packages/steerable_agent_runtime/pool.py +662 -16
- package/python-runtime/python/Lib/site-packages/steerable_agent_runtime/registry.py +345 -0
- package/python-runtime/python/Lib/site-packages/steerable_agent_runtime/storage/__init__.py +52 -0
- package/python-runtime/python/Lib/site-packages/steerable_agent_runtime/storage/in_memory.py +74 -0
- package/python-runtime/python/Lib/site-packages/steerable_agent_runtime/storage/sqlalchemy_store.py +153 -0
- package/python-runtime/python/Lib/site-packages/steerable_agent_runtime/storage/sqlite_store.py +117 -1
- package/python-runtime/python/Lib/site-packages/steerable_agent_runtime/subagent.py +157 -6
- package/python-runtime/python/Lib/site-packages/{steerable_agent_runtime-0.9.2.dist-info → steerable_agent_runtime-0.10.0.dist-info}/METADATA +38 -3
- package/python-runtime/python/Lib/site-packages/{steerable_agent_runtime-0.9.2.dist-info → steerable_agent_runtime-0.10.0.dist-info}/RECORD +26 -16
- package/python-runtime/python/Lib/site-packages/steerable_sidecar/sidecar.py +545 -6
- package/python-runtime/python/Lib/site-packages/steerable_sidecar-0.10.0.dist-info/METADATA +97 -0
- package/python-runtime/python/Lib/site-packages/{steerable_sidecar-0.9.2.dist-info → steerable_sidecar-0.10.0.dist-info}/RECORD +12 -12
- package/python-runtime/python/Lib/site-packages/uvicorn-0.54.0.dist-info/RECORD +1 -1
- package/python-runtime/python/Scripts/cffi-gen-src.exe +0 -0
- package/python-runtime/python/Scripts/dotenv.exe +0 -0
- package/python-runtime/python/Scripts/httpx.exe +0 -0
- package/python-runtime/python/Scripts/idna.exe +0 -0
- package/python-runtime/python/Scripts/jsonschema.exe +0 -0
- package/python-runtime/python/Scripts/mcp.exe +0 -0
- package/python-runtime/python/Scripts/pip.exe +0 -0
- package/python-runtime/python/Scripts/pip3.12.exe +0 -0
- package/python-runtime/python/Scripts/pip3.exe +0 -0
- package/python-runtime/python/Scripts/pywin32_postinstall.exe +0 -0
- package/python-runtime/python/Scripts/pywin32_testall.exe +0 -0
- package/python-runtime/python/Scripts/steerable-sidecar-acp.exe +0 -0
- package/python-runtime/python/Scripts/steerable-sidecar-headless.exe +0 -0
- package/python-runtime/python/Scripts/steerable-sidecar.exe +0 -0
- package/python-runtime/python/Scripts/uvicorn.exe +0 -0
- package/runtime.json +80 -60
- package/python-runtime/python/Lib/site-packages/steerable_agent_protocol-0.9.2.dist-info/RECORD +0 -11
- package/python-runtime/python/Lib/site-packages/steerable_sidecar-0.9.2.dist-info/METADATA +0 -58
- /package/python-runtime/python/Lib/site-packages/{steerable_agent_harness-0.9.2.dist-info → steerable_agent_harness-0.10.0.dist-info}/INSTALLER +0 -0
- /package/python-runtime/python/Lib/site-packages/{steerable_agent_harness-0.9.2.dist-info → steerable_agent_harness-0.10.0.dist-info}/WHEEL +0 -0
- /package/python-runtime/python/Lib/site-packages/{steerable_agent_protocol-0.9.2.dist-info → steerable_agent_protocol-0.10.0.dist-info}/INSTALLER +0 -0
- /package/python-runtime/python/Lib/site-packages/{steerable_agent_protocol-0.9.2.dist-info → steerable_agent_protocol-0.10.0.dist-info}/WHEEL +0 -0
- /package/python-runtime/python/Lib/site-packages/{steerable_agent_runtime-0.9.2.dist-info → steerable_agent_runtime-0.10.0.dist-info}/INSTALLER +0 -0
- /package/python-runtime/python/Lib/site-packages/{steerable_agent_runtime-0.9.2.dist-info → steerable_agent_runtime-0.10.0.dist-info}/WHEEL +0 -0
- /package/python-runtime/python/Lib/site-packages/{steerable_sidecar-0.9.2.dist-info → steerable_sidecar-0.10.0.dist-info}/INSTALLER +0 -0
- /package/python-runtime/python/Lib/site-packages/{steerable_sidecar-0.9.2.dist-info → steerable_sidecar-0.10.0.dist-info}/WHEEL +0 -0
- /package/python-runtime/python/Lib/site-packages/{steerable_sidecar-0.9.2.dist-info → steerable_sidecar-0.10.0.dist-info}/entry_points.txt +0 -0
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
../../Scripts/cffi-gen-src.exe,sha256=
|
|
1
|
+
../../Scripts/cffi-gen-src.exe,sha256=Zivsl8ko9fk244xL-CEH27FnFkFdvIPOeL-Z0KEgla0,108388
|
|
2
2
|
_cffi_backend.cp312-win_amd64.pyd,sha256=mvXk9opB8mtyby6UJPnVik9WZhifaki-lfQGslcQKuw,181248
|
|
3
3
|
cffi-2.1.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
4
4
|
cffi-2.1.1.dist-info/METADATA,sha256=Z7anWV337eIZUBagXShfqn02Ko9XwjiYTtuLtnS1dw0,2594
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
../../Scripts/httpx.exe,sha256=
|
|
1
|
+
../../Scripts/httpx.exe,sha256=w2krSROCLDam4SIizZ8h6560s0GWydAOPZQW4BJ-Q3c,108377
|
|
2
2
|
httpx-0.28.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
3
3
|
httpx-0.28.1.dist-info/METADATA,sha256=_rubD48-gNV8gZnDBPNcQzboWB0dGNeYPJJ2a4J5OyU,7052
|
|
4
4
|
httpx-0.28.1.dist-info/RECORD,,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
../../Scripts/idna.exe,sha256=
|
|
1
|
+
../../Scripts/idna.exe,sha256=dQJXpAPBYX4AFcNaAmS8VSAnP2mIgB924WkoxxEJx2g,108380
|
|
2
2
|
idna-3.20.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
3
3
|
idna-3.20.dist-info/METADATA,sha256=29jBTB5MoeDJgkptvHy794iE8461LZEUjNMCX2ceS4U,7207
|
|
4
4
|
idna-3.20.dist-info/RECORD,,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
../../Scripts/jsonschema.exe,sha256=
|
|
1
|
+
../../Scripts/jsonschema.exe,sha256=4oCz5sBzF1xpGTjZmHd21IMH4hSNCs25w70qV2G4w5c,108386
|
|
2
2
|
jsonschema-4.26.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
3
3
|
jsonschema-4.26.0.dist-info/METADATA,sha256=BbGJhmypipbaOTS_a4NofO0_-FMlJzt34WtwX22LJtI,7592
|
|
4
4
|
jsonschema-4.26.0.dist-info/RECORD,,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
../../Scripts/mcp.exe,sha256=
|
|
1
|
+
../../Scripts/mcp.exe,sha256=xGljkG9hOH-0gBy8jSzFrq6vzIWwDcddahCaLEpcB5g,108377
|
|
2
2
|
mcp-1.30.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
3
3
|
mcp-1.30.0.dist-info/METADATA,sha256=OIDCmJYUmGKXGvV5QuPlwK0qkJaWpmuYEcYvbkfmURE,9289
|
|
4
4
|
mcp-1.30.0.dist-info/RECORD,,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
../../Scripts/dotenv.exe,sha256=
|
|
1
|
+
../../Scripts/dotenv.exe,sha256=Icvj6FrDFpujkrW7N_n1ga_vW45dYpJ_RdMfWzIs_fo,108385
|
|
2
2
|
dotenv/__init__.py,sha256=bhY-iK6wwHZamWII6eSCuCAWXUNdn2TQFmEOuXSU7SM,1230
|
|
3
3
|
dotenv/__main__.py,sha256=N0RhLG7nHIqtlJHwwepIo-zbJPNx9sewCCRGY528h_4,129
|
|
4
4
|
dotenv/__pycache__/__init__.cpython-312.pyc,,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
../../Scripts/__pycache__/pywin32_postinstall.cpython-312.pyc,,
|
|
2
2
|
../../Scripts/__pycache__/pywin32_testall.cpython-312.pyc,,
|
|
3
|
-
../../Scripts/pywin32_postinstall.exe,sha256=
|
|
3
|
+
../../Scripts/pywin32_postinstall.exe,sha256=DXGtSVHBKZkR8yGUbR0l8brfflJp4tABr3oic5np7JU,108405
|
|
4
4
|
../../Scripts/pywin32_postinstall.py,sha256=RT3RogIBArMUhRCFaivWkl2NsVZwr9cNNd2hcsXUBU4,25576
|
|
5
|
-
../../Scripts/pywin32_testall.exe,sha256=
|
|
5
|
+
../../Scripts/pywin32_testall.exe,sha256=cm5gX-G_W9N1YD_wIaSCpl7G2wFteflaJSH4q6tF6uI,108401
|
|
6
6
|
../../Scripts/pywin32_testall.py,sha256=FYvjx43thdvbkWaYiLMTX39RxsFL0c-1o-xY2udlBqA,3847
|
|
7
7
|
PyWin32.chm,sha256=X8MAsKaFhJRAzKBn0QR-GEXRoShgndE6NQg6LqhzTVI,2642799
|
|
8
8
|
__pycache__/pythoncom.cpython-312.pyc,,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
steerable_agent_harness-0.
|
|
2
|
-
steerable_agent_harness-0.
|
|
3
|
-
steerable_agent_harness-0.
|
|
4
|
-
steerable_agent_harness-0.
|
|
5
|
-
steerable_agent_harness-0.
|
|
6
|
-
steerable_agent_harness-0.
|
|
7
|
-
steerable_agent_harness-0.
|
|
1
|
+
steerable_agent_harness-0.10.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
2
|
+
steerable_agent_harness-0.10.0.dist-info/METADATA,sha256=hezhQUaNKGROfqHgUMQfwBLzvMkqiqhACLvEBXS-liw,385
|
|
3
|
+
steerable_agent_harness-0.10.0.dist-info/RECORD,,
|
|
4
|
+
steerable_agent_harness-0.10.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
steerable_agent_harness-0.10.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
steerable_agent_harness-0.10.0.dist-info/direct_url.json,sha256=UXvoylUbK4Lkfuzi3TRjKqrGCVAmNd-ebjMbkos5cAk,290
|
|
7
|
+
steerable_agent_harness-0.10.0.dist-info/top_level.txt,sha256=L99GRcOHaeL_gakhZZnmpEbK5yV4fOeIxXzWKW_2lDA,24
|
|
8
8
|
steerable_agent_harness/__init__.py,sha256=hZK9IhKIFS66jIYH9LKMNuGmdJkLEeE5LsPeRPcLl6E,1253
|
|
9
9
|
steerable_agent_harness/__pycache__/__init__.cpython-312.pyc,,
|
|
10
10
|
steerable_agent_harness/__pycache__/budget.cpython-312.pyc,,
|
package/python-runtime/python/Lib/site-packages/steerable_agent_protocol-0.10.0.dist-info/RECORD
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
steerable_agent_protocol-0.10.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
2
|
+
steerable_agent_protocol-0.10.0.dist-info/METADATA,sha256=IgT9xICSjhelATS_pyfkbK_Ou1UlVBlhn2kPJbItGYg,328
|
|
3
|
+
steerable_agent_protocol-0.10.0.dist-info/RECORD,,
|
|
4
|
+
steerable_agent_protocol-0.10.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
steerable_agent_protocol-0.10.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
steerable_agent_protocol-0.10.0.dist-info/direct_url.json,sha256=e_xB1q2-B5msly9-DBUbHPG6yqDTLnFuKp6JrevO-rA,291
|
|
7
|
+
steerable_agent_protocol-0.10.0.dist-info/top_level.txt,sha256=K8P6-3hLZwuhUEWM2CS2wjFpBzJ83fyzZbdS2Txh4VU,25
|
|
8
|
+
steerable_agent_protocol/__init__.py,sha256=adci5VbbNw9seedRNj8OsibxVH3B1M531_adq3fIU1c,25
|
|
9
|
+
steerable_agent_protocol/__pycache__/__init__.cpython-312.pyc,,
|
|
10
|
+
steerable_agent_protocol/__pycache__/generated.cpython-312.pyc,,
|
|
11
|
+
steerable_agent_protocol/generated.py,sha256=jeynKncOP0HWEpXcFs7A7y3z4zloR0njCar2gMcBfbo,6005
|
|
@@ -83,6 +83,7 @@ from .branch import (
|
|
|
83
83
|
lineage,
|
|
84
84
|
resolve_fork_seq,
|
|
85
85
|
)
|
|
86
|
+
from .budget import SessionBudgetExceeded, TreeBudget, TreeBudgetReport
|
|
86
87
|
from .cache_control import (
|
|
87
88
|
CacheControlProvider,
|
|
88
89
|
CacheDriftMonitor,
|
|
@@ -91,6 +92,22 @@ from .cache_control import (
|
|
|
91
92
|
system_blocks_with_cache,
|
|
92
93
|
)
|
|
93
94
|
from .calibration import CalibratingProvider, ModelCalibration, UsageCalibration
|
|
95
|
+
from .child_record import (
|
|
96
|
+
INTERRUPTED_CHILD_STATUS,
|
|
97
|
+
RESUMABLE_CHILD_STATUSES,
|
|
98
|
+
TERMINAL_CHILD_STATUSES,
|
|
99
|
+
ChildRecord,
|
|
100
|
+
)
|
|
101
|
+
from .child_notice import CHILD_NOTICE_KIND, ChildNoticeHooks, render_child_notice
|
|
102
|
+
from .engines import (
|
|
103
|
+
AcpEngineSpec,
|
|
104
|
+
AcpOutcome,
|
|
105
|
+
AcpPermissionPolicy,
|
|
106
|
+
AcpToolBridge,
|
|
107
|
+
AcpUnavailable,
|
|
108
|
+
PermissionDiagnostic,
|
|
109
|
+
run_acp_child,
|
|
110
|
+
)
|
|
94
111
|
from .compaction_policy import (
|
|
95
112
|
CompactionPolicy,
|
|
96
113
|
is_large_window,
|
|
@@ -121,6 +138,7 @@ from .history import (
|
|
|
121
138
|
RecordEntry,
|
|
122
139
|
RecordFormatError,
|
|
123
140
|
SystemPromptFragment,
|
|
141
|
+
completed_turn_prefix,
|
|
124
142
|
entry_from_dict,
|
|
125
143
|
render_fragment_capped,
|
|
126
144
|
entry_to_dict,
|
|
@@ -209,6 +227,13 @@ from .pricing import (
|
|
|
209
227
|
register_model_price,
|
|
210
228
|
)
|
|
211
229
|
from .pseudo import extract_inline_tool_calls
|
|
230
|
+
from .registry import (
|
|
231
|
+
ChildRegistry,
|
|
232
|
+
ChildStatusView,
|
|
233
|
+
SpawnedChild,
|
|
234
|
+
default_record_id,
|
|
235
|
+
registry_status,
|
|
236
|
+
)
|
|
212
237
|
from .recording import (
|
|
213
238
|
DEFAULT_MAX_ITEM_TOKENS,
|
|
214
239
|
InMemoryRequestSink,
|
|
@@ -292,6 +317,13 @@ from .world_state import (
|
|
|
292
317
|
)
|
|
293
318
|
|
|
294
319
|
__all__ = [
|
|
320
|
+
"AcpEngineSpec",
|
|
321
|
+
"AcpOutcome",
|
|
322
|
+
"AcpPermissionPolicy",
|
|
323
|
+
"AcpToolBridge",
|
|
324
|
+
"AcpUnavailable",
|
|
325
|
+
"PermissionDiagnostic",
|
|
326
|
+
"run_acp_child",
|
|
295
327
|
"DEFAULT_MAX_ITEM_TOKENS",
|
|
296
328
|
"DEFAULT_MAX_SERVER_TOOLS",
|
|
297
329
|
"DEFAULT_SHELL_TOOLS",
|
|
@@ -338,6 +370,21 @@ __all__ = [
|
|
|
338
370
|
"CacheControlProvider",
|
|
339
371
|
"CacheDriftMonitor",
|
|
340
372
|
"CacheRetention",
|
|
373
|
+
"ChildRecord",
|
|
374
|
+
"ChildRegistry",
|
|
375
|
+
"ChildNoticeHooks",
|
|
376
|
+
"CHILD_NOTICE_KIND",
|
|
377
|
+
"SessionBudgetExceeded",
|
|
378
|
+
"TreeBudget",
|
|
379
|
+
"TreeBudgetReport",
|
|
380
|
+
"render_child_notice",
|
|
381
|
+
"ChildStatusView",
|
|
382
|
+
"SpawnedChild",
|
|
383
|
+
"default_record_id",
|
|
384
|
+
"registry_status",
|
|
385
|
+
"INTERRUPTED_CHILD_STATUS",
|
|
386
|
+
"RESUMABLE_CHILD_STATUSES",
|
|
387
|
+
"TERMINAL_CHILD_STATUSES",
|
|
341
388
|
"CalibratingProvider",
|
|
342
389
|
"ChainHooks",
|
|
343
390
|
"ChildOutcome",
|
|
@@ -354,6 +401,7 @@ __all__ = [
|
|
|
354
401
|
"ContentPart",
|
|
355
402
|
"ContextFragment",
|
|
356
403
|
"ContextManager",
|
|
404
|
+
"completed_turn_prefix",
|
|
357
405
|
"CoreLoop",
|
|
358
406
|
"DecisionNote",
|
|
359
407
|
"ExecutionBudget",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""One token budget for a whole delegating turn tree.
|
|
2
|
+
|
|
3
|
+
``steerable_agent_harness.budget`` budgets **one loop run**: it charges that
|
|
4
|
+
run's own requests and reports exhaustion to that run. That is the wrong scope
|
|
5
|
+
once a turn can delegate: a parent plus three children would each get the full
|
|
6
|
+
allowance, so a turn's real spend could be four times what the host asked for
|
|
7
|
+
and no single loop would ever notice.
|
|
8
|
+
|
|
9
|
+
This module is the other scope — one counter shared by every agent in the tree.
|
|
10
|
+
Each request, whoever made it, charges the same state, and the first one to
|
|
11
|
+
cross the limit fails there. The alternative (handing each child a slice of
|
|
12
|
+
the remainder) was considered and rejected: a serial child given 1/(n+1) of
|
|
13
|
+
the budget hits its own wall while the tree still has room, which turns a
|
|
14
|
+
generous budget into a tight one.
|
|
15
|
+
|
|
16
|
+
Attribution is kept alongside the total because "the turn spent 40k" is not
|
|
17
|
+
actionable when a host needs to know which child spent it.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import threading
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
|
|
25
|
+
from .llm import LLMUsage
|
|
26
|
+
|
|
27
|
+
__all__ = ["SessionBudgetExceeded", "TreeBudget", "TreeBudgetReport"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SessionBudgetExceeded(Exception):
|
|
31
|
+
"""Raised when a request would take the tree past its token limit.
|
|
32
|
+
|
|
33
|
+
Named for the session, not for a child: the limit belongs to the whole
|
|
34
|
+
turn tree, and the agent that happens to cross it is not the one at fault.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class TreeBudgetReport:
|
|
40
|
+
"""The tree's spend at one point in time.
|
|
41
|
+
|
|
42
|
+
``by_agent`` keys are the caller's own agent labels (``"parent"``,
|
|
43
|
+
``"0.1"`` …) — the budget does not know about lineage and must not guess
|
|
44
|
+
it, so it stores whatever key the caller passes.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
limit: int
|
|
48
|
+
spent: int
|
|
49
|
+
by_agent: dict[str, int]
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def remaining(self) -> int:
|
|
53
|
+
return max(self.limit - self.spent, 0)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def exhausted(self) -> bool:
|
|
57
|
+
return self.spent >= self.limit
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class TreeBudget:
|
|
62
|
+
"""A token limit shared by every agent in one turn tree.
|
|
63
|
+
|
|
64
|
+
Thread-safe: the loop hands usage in from both the parent's event loop and
|
|
65
|
+
the worker thread that drives the native engine, so plain attribute
|
|
66
|
+
updates would race.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
limit: int
|
|
70
|
+
_spent: int = field(default=0, init=False)
|
|
71
|
+
_by_agent: dict[str, int] = field(default_factory=dict, init=False)
|
|
72
|
+
_lock: threading.Lock = field(default_factory=threading.Lock, init=False)
|
|
73
|
+
|
|
74
|
+
def charge(self, usage: LLMUsage | None, *, agent: str) -> int:
|
|
75
|
+
"""Charge one request's usage; returns the tree's spend afterwards.
|
|
76
|
+
|
|
77
|
+
``usage=None`` charges nothing but still records the agent, so a host
|
|
78
|
+
can tell "this child ran and reported nothing" apart from "this child
|
|
79
|
+
never ran" — the distinction matters when the bill does not add up.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
SessionBudgetExceeded: when the charge takes the tree past
|
|
83
|
+
``limit``. The spend is recorded **before** raising, so the
|
|
84
|
+
report a caller shows still includes the request that broke it.
|
|
85
|
+
"""
|
|
86
|
+
if usage is None:
|
|
87
|
+
with self._lock:
|
|
88
|
+
self._by_agent.setdefault(agent, 0)
|
|
89
|
+
return self._spent
|
|
90
|
+
# Total, not prompt+completion: providers disagree about whether the
|
|
91
|
+
# total includes cache reads, and ``usage.total_tokens`` is the one
|
|
92
|
+
# number every provider reports consistently.
|
|
93
|
+
billed = max(usage.total_tokens, 0)
|
|
94
|
+
with self._lock:
|
|
95
|
+
self._spent += billed
|
|
96
|
+
self._by_agent[agent] = self._by_agent.get(agent, 0) + billed
|
|
97
|
+
spent = self._spent
|
|
98
|
+
if spent > self.limit:
|
|
99
|
+
raise SessionBudgetExceeded(
|
|
100
|
+
f"turn tree spent {spent} tokens against a {self.limit}-token "
|
|
101
|
+
f"budget (agent {agent!r} crossed it)"
|
|
102
|
+
)
|
|
103
|
+
return spent
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def remaining(self) -> int:
|
|
107
|
+
"""Tokens left, floored at zero."""
|
|
108
|
+
with self._lock:
|
|
109
|
+
return max(self.limit - self._spent, 0)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def exhausted(self) -> bool:
|
|
113
|
+
"""True once the tree has spent its whole limit."""
|
|
114
|
+
with self._lock:
|
|
115
|
+
return self._spent >= self.limit
|
|
116
|
+
|
|
117
|
+
def report(self) -> TreeBudgetReport:
|
|
118
|
+
"""A snapshot, safe to hand to a host mid-turn."""
|
|
119
|
+
with self._lock:
|
|
120
|
+
return TreeBudgetReport(
|
|
121
|
+
limit=self.limit,
|
|
122
|
+
spent=self._spent,
|
|
123
|
+
by_agent=dict(self._by_agent),
|
|
124
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Parent-side delivery of child-agent terminal notices.
|
|
2
|
+
|
|
3
|
+
A parent that spawned children has two ways to learn one finished: poll
|
|
4
|
+
``agent_wait``, or be told. Polling costs a whole model round per check and,
|
|
5
|
+
worse, the model has to *remember* to look. This hook closes that gap: when a
|
|
6
|
+
child of this pool settles, the notice is appended to the parent's transcript
|
|
7
|
+
at its next round boundary.
|
|
8
|
+
|
|
9
|
+
Design notes that matter for correctness:
|
|
10
|
+
|
|
11
|
+
- The notice is a **system-role** message with its own record kind
|
|
12
|
+
(``subagent.notice``). It is not a user message: nothing about it came from
|
|
13
|
+
the user, and a model that treats it as an instruction from the user would
|
|
14
|
+
be misreading the conversation.
|
|
15
|
+
- The notice carries **no child text**. The answer still comes from
|
|
16
|
+
``agent_wait``; putting the answer here would duplicate the child's output
|
|
17
|
+
into the parent's context on every finish, and the parent may never read it.
|
|
18
|
+
- Delivery is idempotent per child: each notice is drained exactly once, so a
|
|
19
|
+
settled child cannot re-notify on every later step.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
from .hooks import NoopHooks, PreStepAction, TranscriptAppend
|
|
27
|
+
from .llm import LLMMessage
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
30
|
+
from .loop import LoopContext
|
|
31
|
+
from .pool import AgentPool
|
|
32
|
+
|
|
33
|
+
__all__ = ["CHILD_NOTICE_KIND", "ChildNoticeHooks"]
|
|
34
|
+
|
|
35
|
+
#: Record kind for the injected notice. ``<feature>.<name>`` like the other
|
|
36
|
+
#: injected fragments (``skills.catalog``, ``world_state.patch``), so a replay
|
|
37
|
+
#: can tell it apart from anything the user or the model said.
|
|
38
|
+
CHILD_NOTICE_KIND = "subagent.notice"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ChildNoticeHooks(NoopHooks):
|
|
42
|
+
"""``pre_step``: append notices for children that settled since last step.
|
|
43
|
+
|
|
44
|
+
One instance per parent loop. Wrap it in a ``ChainHooks`` with whatever
|
|
45
|
+
else the host installs — this hook only ever appends; it never rewrites,
|
|
46
|
+
never rejects, and never changes tool choice.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, pool: AgentPool) -> None:
|
|
50
|
+
self._pool = pool
|
|
51
|
+
|
|
52
|
+
async def pre_step(
|
|
53
|
+
self, transcript: list[LLMMessage], ctx: "LoopContext"
|
|
54
|
+
) -> PreStepAction:
|
|
55
|
+
notices = self._pool.drain_notices()
|
|
56
|
+
if not notices:
|
|
57
|
+
return PreStepAction(kind="proceed")
|
|
58
|
+
appends = [
|
|
59
|
+
TranscriptAppend(
|
|
60
|
+
message=LLMMessage.text_of("system", render_child_notice(*notice)),
|
|
61
|
+
kind=CHILD_NOTICE_KIND,
|
|
62
|
+
)
|
|
63
|
+
for notice in notices
|
|
64
|
+
]
|
|
65
|
+
return PreStepAction(
|
|
66
|
+
kind="proceed",
|
|
67
|
+
appends=appends,
|
|
68
|
+
append_action="child_notice",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def render_child_notice(child_id: str, status: str, answer: str) -> str:
|
|
73
|
+
"""The notice text.
|
|
74
|
+
|
|
75
|
+
A system message, so it reads as harness bookkeeping rather than as either
|
|
76
|
+
party speaking. The one line of guidance matters: without it a model that
|
|
77
|
+
sees "child 0.2 completed" tends to narrate the completion instead of
|
|
78
|
+
collecting the answer.
|
|
79
|
+
"""
|
|
80
|
+
settled = "finished" if status == "completed" else f"ended with status {status}"
|
|
81
|
+
return (
|
|
82
|
+
f"[sub-agent notice] Sub-agent {child_id} {settled}. "
|
|
83
|
+
"Its answer is not included here; call agent_wait to read it."
|
|
84
|
+
)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""The child-agent registry record: one shape, shared by every store.
|
|
2
|
+
|
|
3
|
+
A record answers only "whose child is this, how deep, what built it, and
|
|
4
|
+
where it ended". Task text and answers are deliberately absent — they live
|
|
5
|
+
in the child's own ``history`` record, so each fact has one authoritative
|
|
6
|
+
copy.
|
|
7
|
+
|
|
8
|
+
This module is a leaf: no storage and no agent imports. Both the registry
|
|
9
|
+
(``registry.py``) and the adapters (``storage/sqlite_store.py``) read these
|
|
10
|
+
definitions, which is what keeps their column mapping from drifting.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, replace
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
#: Statuses that will never change again. ``running`` is absent on purpose:
|
|
19
|
+
#: it is derived from a live in-process handle, never written, so a killed
|
|
20
|
+
#: process cannot leave a permanently "running" row behind.
|
|
21
|
+
TERMINAL_CHILD_STATUSES: frozenset[str] = frozenset(
|
|
22
|
+
{
|
|
23
|
+
"completed",
|
|
24
|
+
"error",
|
|
25
|
+
"cancelled",
|
|
26
|
+
"budget_exhausted",
|
|
27
|
+
"closed",
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
#: Derived, never written: the child has no terminal row and no live handle.
|
|
32
|
+
#: The parent turn ended or the process died mid-run; ``agent_send`` resumes it.
|
|
33
|
+
INTERRUPTED_CHILD_STATUS = "interrupted"
|
|
34
|
+
|
|
35
|
+
#: Statuses that accept a further message. ``closed`` is excluded: it is the
|
|
36
|
+
#: caller's explicit "never again" and must stay that way.
|
|
37
|
+
RESUMABLE_CHILD_STATUSES: frozenset[str] = frozenset(
|
|
38
|
+
{
|
|
39
|
+
"completed",
|
|
40
|
+
"interrupted",
|
|
41
|
+
"cancelled",
|
|
42
|
+
"budget_exhausted",
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class ChildRecord:
|
|
49
|
+
"""One registry row.
|
|
50
|
+
|
|
51
|
+
``record_id`` is the child's own durable record (``<parent>:child:<lineage
|
|
52
|
+
id>``); ``child_id`` is the lineage id inside the parent's pool. ``depth``
|
|
53
|
+
and ``profile`` are duplicated from creation time on purpose: resume must
|
|
54
|
+
rebuild the child's tool domain and nesting from what it was created with,
|
|
55
|
+
never from current settings — otherwise a depth-1 child that was never
|
|
56
|
+
allowed to spawn would gain orchestration tools by being resumed.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
record_id: str
|
|
60
|
+
parent_record_id: str
|
|
61
|
+
child_id: str
|
|
62
|
+
depth: int
|
|
63
|
+
profile: str | None
|
|
64
|
+
engine: str
|
|
65
|
+
status: str
|
|
66
|
+
resumable: bool
|
|
67
|
+
prompt_tokens: int | None
|
|
68
|
+
completion_tokens: int | None
|
|
69
|
+
usage_known: bool
|
|
70
|
+
created_at: str
|
|
71
|
+
updated_at: str
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def terminal(self) -> bool:
|
|
75
|
+
"""True when this status will never change again."""
|
|
76
|
+
return self.status in TERMINAL_CHILD_STATUSES
|
|
77
|
+
|
|
78
|
+
def with_terminal_status(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
status: str,
|
|
82
|
+
prompt_tokens: int | None,
|
|
83
|
+
completion_tokens: int | None,
|
|
84
|
+
usage_known: bool,
|
|
85
|
+
resumable: bool,
|
|
86
|
+
updated_at: str,
|
|
87
|
+
) -> ChildRecord:
|
|
88
|
+
"""This record after a run settled."""
|
|
89
|
+
return replace(
|
|
90
|
+
self,
|
|
91
|
+
status=status,
|
|
92
|
+
prompt_tokens=prompt_tokens,
|
|
93
|
+
completion_tokens=completion_tokens,
|
|
94
|
+
usage_known=usage_known,
|
|
95
|
+
resumable=resumable,
|
|
96
|
+
updated_at=updated_at,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def to_wire(self) -> dict[str, Any]:
|
|
100
|
+
"""Camel-case payload shape for ``agent_list`` and the ``children`` CLI.
|
|
101
|
+
|
|
102
|
+
The row is already model-facing (it says nothing about storage), so
|
|
103
|
+
this is the only translation step between disk and the protocol.
|
|
104
|
+
"""
|
|
105
|
+
return {
|
|
106
|
+
"recordId": self.record_id,
|
|
107
|
+
"parentRecordId": self.parent_record_id,
|
|
108
|
+
"childId": self.child_id,
|
|
109
|
+
"depth": self.depth,
|
|
110
|
+
"profile": self.profile,
|
|
111
|
+
"engine": self.engine,
|
|
112
|
+
"status": self.status,
|
|
113
|
+
"resumable": self.resumable,
|
|
114
|
+
"promptTokens": self.prompt_tokens,
|
|
115
|
+
"completionTokens": self.completion_tokens,
|
|
116
|
+
"usageKnown": self.usage_known,
|
|
117
|
+
"createdAt": self.created_at,
|
|
118
|
+
"updatedAt": self.updated_at,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
#: Record attribute → SQL column, in insert order. Adapters use this to bind
|
|
123
|
+
#: one statement; ``child_row.to_wire`` uses the attribute names for the wire.
|
|
124
|
+
CHILD_COLUMNS: tuple[tuple[str, str], ...] = (
|
|
125
|
+
("record_id", "record_id"),
|
|
126
|
+
("parent_record_id", "parent_record_id"),
|
|
127
|
+
("child_id", "child_id"),
|
|
128
|
+
("depth", "depth"),
|
|
129
|
+
("profile", "profile"),
|
|
130
|
+
("engine", "engine"),
|
|
131
|
+
("status", "status"),
|
|
132
|
+
("resumable", "resumable"),
|
|
133
|
+
("prompt_tokens", "prompt_tokens"),
|
|
134
|
+
("completion_tokens", "completion_tokens"),
|
|
135
|
+
("usage_known", "usage_known"),
|
|
136
|
+
("created_at", "created_at"),
|
|
137
|
+
("updated_at", "updated_at"),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
_INT_FIELDS = frozenset({"depth"})
|
|
141
|
+
_BOOL_FIELDS = frozenset({"resumable", "usage_known"})
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def record_to_values(record: ChildRecord) -> tuple[Any, ...]:
|
|
145
|
+
"""Positional values for the insert whose columns are ``CHILD_COLUMNS``.
|
|
146
|
+
|
|
147
|
+
SQLite has no boolean type, so flags bind as 0/1 here rather than at each
|
|
148
|
+
call site.
|
|
149
|
+
"""
|
|
150
|
+
values: list[Any] = []
|
|
151
|
+
for attribute, _column in CHILD_COLUMNS:
|
|
152
|
+
value = getattr(record, attribute)
|
|
153
|
+
if attribute in _BOOL_FIELDS:
|
|
154
|
+
values.append(int(bool(value)))
|
|
155
|
+
else:
|
|
156
|
+
values.append(value)
|
|
157
|
+
return tuple(values)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def record_from_columns(columns: dict[str, Any]) -> ChildRecord:
|
|
161
|
+
"""Rebuild a record from a store's column mapping.
|
|
162
|
+
|
|
163
|
+
Flags arrive as 0/1 from SQLite and as ``bool`` from SQLAlchemy; both are
|
|
164
|
+
normalised here so callers never branch on the storage engine.
|
|
165
|
+
"""
|
|
166
|
+
values: dict[str, Any] = {}
|
|
167
|
+
for attribute, column in CHILD_COLUMNS:
|
|
168
|
+
value = columns[column]
|
|
169
|
+
if attribute in _INT_FIELDS:
|
|
170
|
+
value = int(value)
|
|
171
|
+
elif attribute in _BOOL_FIELDS:
|
|
172
|
+
value = bool(value)
|
|
173
|
+
values[attribute] = value
|
|
174
|
+
return ChildRecord(**values)
|