steerable-agent-runtime 0.3.0__tar.gz → 0.4.0__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.
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/PKG-INFO +3 -3
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/pyproject.toml +13 -3
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/__init__.py +25 -1
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/approval.py +33 -1
- steerable_agent_runtime-0.4.0/src/steerable_agent_runtime/approval_policy.py +243 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/compaction.py +29 -9
- steerable_agent_runtime-0.4.0/src/steerable_agent_runtime/default.harness.json +30 -0
- steerable_agent_runtime-0.4.0/src/steerable_agent_runtime/default.harness.yaml +28 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/errors.py +11 -0
- steerable_agent_runtime-0.4.0/src/steerable_agent_runtime/handoff.py +147 -0
- steerable_agent_runtime-0.4.0/src/steerable_agent_runtime/harness.py +674 -0
- steerable_agent_runtime-0.4.0/src/steerable_agent_runtime/harness_spec.py +339 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/history.py +124 -20
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/hooks.py +86 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/llm/__init__.py +14 -0
- steerable_agent_runtime-0.4.0/src/steerable_agent_runtime/llm/compat.py +259 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/llm/errors.py +23 -2
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/llm/openai_compat.py +115 -11
- steerable_agent_runtime-0.4.0/src/steerable_agent_runtime/llm/system_proxy.py +177 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/loop.py +660 -96
- steerable_agent_runtime-0.4.0/src/steerable_agent_runtime/maintenance.py +238 -0
- steerable_agent_runtime-0.4.0/src/steerable_agent_runtime/mcp_server.py +319 -0
- steerable_agent_runtime-0.4.0/src/steerable_agent_runtime/model_catalog.py +7619 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/model_info.py +101 -9
- steerable_agent_runtime-0.4.0/src/steerable_agent_runtime/model_resolve.py +151 -0
- steerable_agent_runtime-0.4.0/src/steerable_agent_runtime/observation_aging.py +196 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/orchestration.py +205 -29
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/otel.py +15 -3
- steerable_agent_runtime-0.4.0/src/steerable_agent_runtime/reminders.py +279 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/resume.py +16 -3
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/retry.py +5 -1
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/sandboxed.py +28 -8
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/storage/__init__.py +32 -7
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/storage/in_memory.py +20 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +91 -22
- steerable_agent_runtime-0.4.0/src/steerable_agent_runtime/storage/sqlite_store.py +414 -0
- steerable_agent_runtime-0.4.0/src/steerable_agent_runtime/storage/write_lease.py +134 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/tokens.py +23 -4
- steerable_agent_runtime-0.4.0/src/steerable_agent_runtime/tool_search.py +177 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/tracing.py +112 -13
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime.egg-info/PKG-INFO +3 -3
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime.egg-info/SOURCES.txt +32 -1
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime.egg-info/requires.txt +2 -2
- steerable_agent_runtime-0.4.0/tests/test_approval_policy.py +185 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_compaction.py +52 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_error_taxonomy.py +11 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_fragment_bounds.py +26 -0
- steerable_agent_runtime-0.4.0/tests/test_handoff.py +113 -0
- steerable_agent_runtime-0.4.0/tests/test_harness.py +244 -0
- steerable_agent_runtime-0.4.0/tests/test_harness_spec.py +251 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_history_persistence.py +126 -6
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_llm_wire_helpers.py +174 -0
- steerable_agent_runtime-0.4.0/tests/test_long_session.py +163 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_loop.py +38 -0
- steerable_agent_runtime-0.4.0/tests/test_maintenance.py +136 -0
- steerable_agent_runtime-0.4.0/tests/test_mcp_server.py +209 -0
- steerable_agent_runtime-0.4.0/tests/test_model_catalog.py +183 -0
- steerable_agent_runtime-0.4.0/tests/test_model_equivalence.py +185 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_model_info.py +64 -1
- steerable_agent_runtime-0.4.0/tests/test_model_resolve.py +86 -0
- steerable_agent_runtime-0.4.0/tests/test_observation_aging.py +169 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_orchestration.py +312 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_otel.py +12 -5
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_provider_compat.py +54 -0
- steerable_agent_runtime-0.4.0/tests/test_reminders.py +136 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_retry_hooks.py +13 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_sandboxed.py +32 -2
- steerable_agent_runtime-0.4.0/tests/test_soft_timeout.py +1174 -0
- steerable_agent_runtime-0.4.0/tests/test_sqlite_storage.py +238 -0
- steerable_agent_runtime-0.4.0/tests/test_steer.py +281 -0
- steerable_agent_runtime-0.4.0/tests/test_storage_contract.py +135 -0
- steerable_agent_runtime-0.4.0/tests/test_system_proxy.py +211 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_tool_exposure.py +188 -8
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_tool_timeout.py +73 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_trace_recorder.py +133 -7
- steerable_agent_runtime-0.4.0/tests/test_write_lease.py +99 -0
- steerable_agent_runtime-0.3.0/src/steerable_agent_runtime/llm/compat.py +0 -130
- steerable_agent_runtime-0.3.0/src/steerable_agent_runtime/tool_search.py +0 -126
- steerable_agent_runtime-0.3.0/tests/test_soft_timeout.py +0 -165
- steerable_agent_runtime-0.3.0/tests/test_steer.py +0 -127
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/README.md +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/setup.cfg +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/antihallucination.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/branch.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/cache_control.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/calibration.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/llm/parts.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/mcp.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/pricing.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/pseudo.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/recording.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/replay.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/skills.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/spill.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/subagent.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/tool_schema.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/tools.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/transport/__init__.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime/world_state.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_antihallucination.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_approval.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_branch.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_cache_control.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_cache_instrumentation.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_calibration.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_content_parts.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_golden.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_history.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_hooks.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_in_memory_storage.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_loop_cancellation.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_loop_replay.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_loop_sandbox_event.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_mcp.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_parallel_tools.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_pseudo.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_recording.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_replay_crosslang.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_resume.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_safety_gate.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_skills.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_spill.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_stream_strip.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_subagent.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_tokens.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_tool_hygiene.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_tool_router.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_tool_schema.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_transport_jsonrpc.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_transport_sse.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_usage_attribution.py +0 -0
- {steerable_agent_runtime-0.3.0 → steerable_agent_runtime-0.4.0}/tests/test_world_state.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: steerable-agent-runtime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -13,14 +13,14 @@ Provides-Extra: fastapi
|
|
|
13
13
|
Requires-Dist: fastapi>=0.110; extra == "fastapi"
|
|
14
14
|
Requires-Dist: starlette>=0.37; extra == "fastapi"
|
|
15
15
|
Provides-Extra: openai
|
|
16
|
-
Requires-Dist: httpx>=0.27; extra == "openai"
|
|
16
|
+
Requires-Dist: httpx[socks]>=0.27; extra == "openai"
|
|
17
17
|
Provides-Extra: anthropic
|
|
18
18
|
Requires-Dist: anthropic>=0.40; extra == "anthropic"
|
|
19
19
|
Provides-Extra: all
|
|
20
20
|
Requires-Dist: sqlalchemy>=2.0; extra == "all"
|
|
21
21
|
Requires-Dist: fastapi>=0.110; extra == "all"
|
|
22
22
|
Requires-Dist: starlette>=0.37; extra == "all"
|
|
23
|
-
Requires-Dist: httpx>=0.27; extra == "all"
|
|
23
|
+
Requires-Dist: httpx[socks]>=0.27; extra == "all"
|
|
24
24
|
Requires-Dist: anthropic>=0.40; extra == "all"
|
|
25
25
|
|
|
26
26
|
# steerable-agent-runtime
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "steerable-agent-runtime"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Steerable agent runtime: LLM, tool, storage, and transport adapters."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -18,13 +18,17 @@ dependencies = [
|
|
|
18
18
|
[project.optional-dependencies]
|
|
19
19
|
sqlalchemy = ["sqlalchemy>=2.0"]
|
|
20
20
|
fastapi = ["fastapi>=0.110", "starlette>=0.37"]
|
|
21
|
-
|
|
21
|
+
# The `socks` extra is not optional in practice: httpx builds a transport for
|
|
22
|
+
# every proxy the environment declares while the client is constructed, so a
|
|
23
|
+
# host exporting `all_proxy=socks5://…` — the common shell proxy snippet — makes
|
|
24
|
+
# construction raise without it, failing provider calls that never reach a URL.
|
|
25
|
+
openai = ["httpx[socks]>=0.27"]
|
|
22
26
|
anthropic = ["anthropic>=0.40"]
|
|
23
27
|
all = [
|
|
24
28
|
"sqlalchemy>=2.0",
|
|
25
29
|
"fastapi>=0.110",
|
|
26
30
|
"starlette>=0.37",
|
|
27
|
-
"httpx>=0.27",
|
|
31
|
+
"httpx[socks]>=0.27",
|
|
28
32
|
"anthropic>=0.40",
|
|
29
33
|
]
|
|
30
34
|
|
|
@@ -38,6 +42,12 @@ package-dir = {"" = "src"}
|
|
|
38
42
|
[tool.setuptools.packages.find]
|
|
39
43
|
where = ["src"]
|
|
40
44
|
|
|
45
|
+
# default.harness.yaml/.json ship inside the wheel: the sidecar resolves
|
|
46
|
+
# the spec relative to the installed package (Harbor trial containers
|
|
47
|
+
# pip-install the wheels, so a repo-relative path would not exist there).
|
|
48
|
+
[tool.setuptools.package-data]
|
|
49
|
+
steerable_agent_runtime = ["*.yaml", "*.json"]
|
|
50
|
+
|
|
41
51
|
[tool.uv.sources]
|
|
42
52
|
steerable-agent-protocol = { workspace = true }
|
|
43
53
|
steerable-agent-harness = { workspace = true }
|
|
@@ -22,6 +22,13 @@ from .approval import (
|
|
|
22
22
|
JsonApprovalStore,
|
|
23
23
|
SessionApprovalCache,
|
|
24
24
|
)
|
|
25
|
+
from .approval_policy import (
|
|
26
|
+
ApprovalPolicy,
|
|
27
|
+
ApprovalRule,
|
|
28
|
+
JsonApprovalPolicyStore,
|
|
29
|
+
PolicyApprover,
|
|
30
|
+
rule_from_amendment,
|
|
31
|
+
)
|
|
25
32
|
from .branch import (
|
|
26
33
|
BranchPoint,
|
|
27
34
|
ForkResult,
|
|
@@ -59,10 +66,13 @@ from .history import (
|
|
|
59
66
|
HistoryStore,
|
|
60
67
|
RecordEntry,
|
|
61
68
|
RecordFormatError,
|
|
69
|
+
SystemPromptFragment,
|
|
62
70
|
entry_from_dict,
|
|
71
|
+
render_fragment_capped,
|
|
63
72
|
entry_to_dict,
|
|
64
73
|
message_from_dict,
|
|
65
74
|
message_to_dict,
|
|
75
|
+
upgrade_entry_dict,
|
|
66
76
|
)
|
|
67
77
|
from .hooks import (
|
|
68
78
|
ChainHooks,
|
|
@@ -150,7 +160,12 @@ from .replay import (
|
|
|
150
160
|
build_step_decision_event,
|
|
151
161
|
reduce_execution_state,
|
|
152
162
|
)
|
|
153
|
-
from .resume import
|
|
163
|
+
from .resume import (
|
|
164
|
+
close_dangling_tool_calls,
|
|
165
|
+
load_history_transcript,
|
|
166
|
+
load_transcript,
|
|
167
|
+
project_transcript,
|
|
168
|
+
)
|
|
154
169
|
from .retry import RetryHooks
|
|
155
170
|
from .sandboxed import (
|
|
156
171
|
DEFAULT_SHELL_TOOLS,
|
|
@@ -221,7 +236,9 @@ __all__ = [
|
|
|
221
236
|
"ApprovalAborted",
|
|
222
237
|
"ApprovalDecision",
|
|
223
238
|
"ApprovalExecutor",
|
|
239
|
+
"ApprovalPolicy",
|
|
224
240
|
"ApprovalRequest",
|
|
241
|
+
"ApprovalRule",
|
|
225
242
|
"ApprovalStore",
|
|
226
243
|
"Approver",
|
|
227
244
|
"AutoApprover",
|
|
@@ -255,6 +272,7 @@ __all__ = [
|
|
|
255
272
|
"InMemoryApprovalStore",
|
|
256
273
|
"InMemoryRequestSink",
|
|
257
274
|
"InMemorySpillStore",
|
|
275
|
+
"JsonApprovalPolicyStore",
|
|
258
276
|
"JsonApprovalStore",
|
|
259
277
|
"JsonlRequestSink",
|
|
260
278
|
"LLMMessage",
|
|
@@ -277,6 +295,7 @@ __all__ = [
|
|
|
277
295
|
"OrchestrationBudgetExceeded",
|
|
278
296
|
"OrchestrationConfig",
|
|
279
297
|
"OrchestrationExecutor",
|
|
298
|
+
"PolicyApprover",
|
|
280
299
|
"PolicyDeniedError",
|
|
281
300
|
"PreStepAction",
|
|
282
301
|
"PrivacyMode",
|
|
@@ -296,6 +315,7 @@ __all__ = [
|
|
|
296
315
|
"SessionApprovalCache",
|
|
297
316
|
"SkillConfig",
|
|
298
317
|
"SkillDefinition",
|
|
318
|
+
"SystemPromptFragment",
|
|
299
319
|
"SkillExecutor",
|
|
300
320
|
"SkillHooks",
|
|
301
321
|
"SkillProvider",
|
|
@@ -329,6 +349,7 @@ __all__ = [
|
|
|
329
349
|
"branch_label",
|
|
330
350
|
"build_step_decision_event",
|
|
331
351
|
"clamp_reasoning_effort",
|
|
352
|
+
"close_dangling_tool_calls",
|
|
332
353
|
"detect_claimed_execution",
|
|
333
354
|
"detect_deferred_execution",
|
|
334
355
|
"detect_deferred_execution_eager",
|
|
@@ -367,9 +388,11 @@ __all__ = [
|
|
|
367
388
|
"register_model_info",
|
|
368
389
|
"register_model_price",
|
|
369
390
|
"register_tool_search",
|
|
391
|
+
"render_fragment_capped",
|
|
370
392
|
"render_skill_catalog",
|
|
371
393
|
"resolve_fork_seq",
|
|
372
394
|
"resolve_model_info",
|
|
395
|
+
"rule_from_amendment",
|
|
373
396
|
"select_catalog",
|
|
374
397
|
"select_skills",
|
|
375
398
|
"should_run_grounding_judge",
|
|
@@ -381,6 +404,7 @@ __all__ = [
|
|
|
381
404
|
"to_otlp_json",
|
|
382
405
|
"tool",
|
|
383
406
|
"tool_search_descriptor",
|
|
407
|
+
"upgrade_entry_dict",
|
|
384
408
|
]
|
|
385
409
|
|
|
386
410
|
__version__ = "0.1.0"
|
|
@@ -28,6 +28,7 @@ import json
|
|
|
28
28
|
import logging
|
|
29
29
|
import os
|
|
30
30
|
import tempfile
|
|
31
|
+
import time
|
|
31
32
|
from dataclasses import dataclass
|
|
32
33
|
from pathlib import Path
|
|
33
34
|
from typing import TYPE_CHECKING, Any, Literal, Protocol, runtime_checkable
|
|
@@ -101,6 +102,11 @@ class ApprovalRequest:
|
|
|
101
102
|
calls sharing a category share a cached decision. It defaults to the tool
|
|
102
103
|
name; hosts with parameterized risk (e.g. a shell tool) can pass a
|
|
103
104
|
resolver that categorizes by command class instead.
|
|
105
|
+
|
|
106
|
+
``call_id`` is the originating tool call's id, for approver channels that
|
|
107
|
+
correlate the prompt with the tool call in the UI (ACP
|
|
108
|
+
``session/request_permission`` requires it). Empty for synthesized
|
|
109
|
+
requests.
|
|
104
110
|
"""
|
|
105
111
|
|
|
106
112
|
tool_name: str
|
|
@@ -108,6 +114,7 @@ class ApprovalRequest:
|
|
|
108
114
|
mode: ToolMode
|
|
109
115
|
category: str
|
|
110
116
|
round_index: int = 0
|
|
117
|
+
call_id: str = ""
|
|
111
118
|
|
|
112
119
|
|
|
113
120
|
@runtime_checkable
|
|
@@ -238,6 +245,7 @@ def _default_resolver(call: ToolCall, ctx: LoopContext) -> ApprovalRequest:
|
|
|
238
245
|
mode=decide_tool_mode(call.name),
|
|
239
246
|
category=call.name,
|
|
240
247
|
round_index=ctx.round_index,
|
|
248
|
+
call_id=call.id,
|
|
241
249
|
)
|
|
242
250
|
|
|
243
251
|
|
|
@@ -282,10 +290,28 @@ class ApprovalExecutor:
|
|
|
282
290
|
async def execute(self, call: ToolCall, ctx: LoopContext) -> ToolResult:
|
|
283
291
|
request = self._resolve(call, ctx)
|
|
284
292
|
decision = self._stored_decision(request)
|
|
293
|
+
# W2.7.2: when the decision required asking (cache miss), record the
|
|
294
|
+
# wait on the result so the trace can show approval latency as its
|
|
295
|
+
# own span. Cache hits carry no marker — no wait happened.
|
|
296
|
+
waited_ms: int | None = None
|
|
285
297
|
if decision is None:
|
|
298
|
+
ask_started = time.monotonic()
|
|
286
299
|
decision = await self._ask(request)
|
|
300
|
+
waited_ms = int((time.monotonic() - ask_started) * 1000)
|
|
287
301
|
self._persist(request, decision)
|
|
288
302
|
|
|
303
|
+
approval_marker = (
|
|
304
|
+
{
|
|
305
|
+
"_approval": {
|
|
306
|
+
"kind": decision.kind,
|
|
307
|
+
"category": request.category,
|
|
308
|
+
"waitMs": waited_ms,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if waited_ms is not None
|
|
312
|
+
else None
|
|
313
|
+
)
|
|
314
|
+
|
|
289
315
|
if decision.kind == "abort":
|
|
290
316
|
raise ApprovalAborted(
|
|
291
317
|
decision.reason or f"tool call '{request.tool_name}' aborted by approval"
|
|
@@ -300,6 +326,7 @@ class ApprovalExecutor:
|
|
|
300
326
|
"approval": decision.kind,
|
|
301
327
|
"category": request.category,
|
|
302
328
|
"reason": reason,
|
|
329
|
+
**(approval_marker or {}),
|
|
303
330
|
"message": (
|
|
304
331
|
f"Tool call '{request.tool_name}' was denied "
|
|
305
332
|
f"({decision.kind}): {reason}"
|
|
@@ -307,7 +334,12 @@ class ApprovalExecutor:
|
|
|
307
334
|
},
|
|
308
335
|
)
|
|
309
336
|
ctx.consent_granted = True
|
|
310
|
-
|
|
337
|
+
result = await self._inner.execute(call, ctx)
|
|
338
|
+
if approval_marker is not None:
|
|
339
|
+
result = result.model_copy(
|
|
340
|
+
update={"data": {**(result.data or {}), **approval_marker}}
|
|
341
|
+
)
|
|
342
|
+
return result
|
|
311
343
|
|
|
312
344
|
def _stored_decision(self, request: ApprovalRequest) -> ApprovalDecision | None:
|
|
313
345
|
"""Durable wins over session: it is the stronger commitment."""
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Approval policy rules: pattern-matched auto-decisions orthogonal to the
|
|
2
|
+
8-variant lattice (W2.4).
|
|
3
|
+
|
|
4
|
+
Reference: codex execpolicy — durable rules mapping command patterns to
|
|
5
|
+
allow/deny verdicts, consulted before the interactive prompt. The lattice's
|
|
6
|
+
category caches answer "this KIND of call, decided before"; policy rules
|
|
7
|
+
answer "calls matching this PATTERN, decided in advance" — a shell category
|
|
8
|
+
covers every command, while a rule can carve out ``git status`` precisely.
|
|
9
|
+
|
|
10
|
+
Resolution order inside ``ApprovalExecutor`` is unchanged (durable store →
|
|
11
|
+
session cache → approver); ``PolicyApprover`` sits at the approver seam, so
|
|
12
|
+
rules are consulted after the lattice's own caches and before the host
|
|
13
|
+
prompt. Rule hits return request-scoped variants (``allow_once`` /
|
|
14
|
+
``deny_once``): the rule itself is the durable grant, so the lattice must
|
|
15
|
+
not double-record it into a category cache.
|
|
16
|
+
|
|
17
|
+
Amendments (W2.4.2): a host approval reply may carry an ``amendment``
|
|
18
|
+
payload — "allow, and keep allowing commands like this". The sidecar turns
|
|
19
|
+
it into a rule in the durable policy store; the user is not re-asked.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import logging
|
|
26
|
+
import os
|
|
27
|
+
import shlex
|
|
28
|
+
import tempfile
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any, Literal
|
|
32
|
+
|
|
33
|
+
from .approval import ApprovalDecision, ApprovalRequest, Approver
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"ApprovalPolicy",
|
|
39
|
+
"ApprovalRule",
|
|
40
|
+
"JsonApprovalPolicyStore",
|
|
41
|
+
"PolicyApprover",
|
|
42
|
+
"rule_from_amendment",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
RuleDecision = Literal["allow", "deny"]
|
|
46
|
+
_RULE_DECISIONS = frozenset({"allow", "deny"})
|
|
47
|
+
|
|
48
|
+
#: Conventional arguments key holding the shell command string (mirrors the
|
|
49
|
+
#: sidecar's default ``commandArg``).
|
|
50
|
+
_DEFAULT_COMMAND_ARG = "command"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class ApprovalRule:
|
|
55
|
+
"""One pattern → verdict rule.
|
|
56
|
+
|
|
57
|
+
``tool`` is an exact tool name. ``command_prefix`` is an argv token
|
|
58
|
+
prefix matched against the call's command string (shlex-split); empty
|
|
59
|
+
means "every call of this tool". A rule with a prefix never matches a
|
|
60
|
+
call whose command is missing or unparseable — fail closed, the inner
|
|
61
|
+
approver decides instead.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
tool: str
|
|
65
|
+
decision: RuleDecision
|
|
66
|
+
command_prefix: tuple[str, ...] = ()
|
|
67
|
+
command_arg: str = _DEFAULT_COMMAND_ARG
|
|
68
|
+
|
|
69
|
+
def __post_init__(self) -> None:
|
|
70
|
+
if not self.tool:
|
|
71
|
+
raise ValueError("rule tool must be non-empty")
|
|
72
|
+
if self.decision not in _RULE_DECISIONS:
|
|
73
|
+
raise ValueError(f"rule decision must be allow|deny, got {self.decision!r}")
|
|
74
|
+
|
|
75
|
+
def matches(self, request: ApprovalRequest) -> bool:
|
|
76
|
+
if request.tool_name != self.tool:
|
|
77
|
+
return False
|
|
78
|
+
if not self.command_prefix:
|
|
79
|
+
return True
|
|
80
|
+
raw = request.arguments.get(self.command_arg)
|
|
81
|
+
if not isinstance(raw, str) or not raw.strip():
|
|
82
|
+
return False
|
|
83
|
+
try:
|
|
84
|
+
argv = shlex.split(raw)
|
|
85
|
+
except ValueError: # unbalanced quotes etc. — no match, fail closed
|
|
86
|
+
return False
|
|
87
|
+
prefix = list(self.command_prefix)
|
|
88
|
+
return argv[: len(prefix)] == prefix
|
|
89
|
+
|
|
90
|
+
def to_dict(self) -> dict[str, Any]:
|
|
91
|
+
return {
|
|
92
|
+
"tool": self.tool,
|
|
93
|
+
"decision": self.decision,
|
|
94
|
+
"commandPrefix": list(self.command_prefix),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def from_dict(cls, data: dict[str, Any]) -> ApprovalRule:
|
|
99
|
+
tool = data.get("tool")
|
|
100
|
+
decision = data.get("decision")
|
|
101
|
+
prefix = data.get("commandPrefix") or []
|
|
102
|
+
if not isinstance(tool, str) or not isinstance(prefix, list):
|
|
103
|
+
raise ValueError(f"invalid rule entry: {data!r}")
|
|
104
|
+
return cls(
|
|
105
|
+
tool=tool,
|
|
106
|
+
decision=decision, # validated in __post_init__
|
|
107
|
+
command_prefix=tuple(str(token) for token in prefix),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(slots=True)
|
|
112
|
+
class ApprovalPolicy:
|
|
113
|
+
"""Ordered rule list; the first matching rule decides."""
|
|
114
|
+
|
|
115
|
+
rules: list[ApprovalRule] = field(default_factory=list)
|
|
116
|
+
|
|
117
|
+
def decide(self, request: ApprovalRequest) -> ApprovalDecision | None:
|
|
118
|
+
for rule in self.rules:
|
|
119
|
+
if rule.matches(request):
|
|
120
|
+
pattern = (
|
|
121
|
+
" ".join(rule.command_prefix) if rule.command_prefix else "*"
|
|
122
|
+
)
|
|
123
|
+
reason = f"policy rule: {rule.tool} {pattern} → {rule.decision}"
|
|
124
|
+
return ApprovalDecision(
|
|
125
|
+
"allow_once" if rule.decision == "allow" else "deny_once",
|
|
126
|
+
reason,
|
|
127
|
+
)
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
def add(self, rule: ApprovalRule) -> None:
|
|
131
|
+
"""Append a rule. An identical existing rule is a no-op (amendments
|
|
132
|
+
are idempotent — re-approving the same pattern must not grow the
|
|
133
|
+
file)."""
|
|
134
|
+
if rule not in self.rules:
|
|
135
|
+
self.rules.append(rule)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class JsonApprovalPolicyStore:
|
|
139
|
+
"""File-backed durable policy: one JSON object ``{"version", "rules"}``.
|
|
140
|
+
|
|
141
|
+
Writes are atomic (tmp file + rename), mirroring ``JsonApprovalStore`` —
|
|
142
|
+
a crash mid-write must not corrupt the policy every future run loads.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(self, path: str | Path) -> None:
|
|
146
|
+
self._path = Path(path)
|
|
147
|
+
|
|
148
|
+
def load(self) -> ApprovalPolicy:
|
|
149
|
+
if not self._path.exists():
|
|
150
|
+
return ApprovalPolicy()
|
|
151
|
+
try:
|
|
152
|
+
data = json.loads(self._path.read_text(encoding="utf-8"))
|
|
153
|
+
rules = [ApprovalRule.from_dict(r) for r in data.get("rules") or []]
|
|
154
|
+
except (ValueError, KeyError, AttributeError) as exc:
|
|
155
|
+
# A corrupt policy fails closed: no rules, so every call falls
|
|
156
|
+
# through to the interactive approver — never an auto-allow.
|
|
157
|
+
logger.warning("ignoring unreadable approval policy %s: %s", self._path, exc)
|
|
158
|
+
return ApprovalPolicy()
|
|
159
|
+
return ApprovalPolicy(rules)
|
|
160
|
+
|
|
161
|
+
def save(self, policy: ApprovalPolicy) -> None:
|
|
162
|
+
payload = {
|
|
163
|
+
"version": 1,
|
|
164
|
+
"rules": [rule.to_dict() for rule in policy.rules],
|
|
165
|
+
}
|
|
166
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
fd, tmp = tempfile.mkstemp(
|
|
168
|
+
dir=str(self._path.parent), prefix=self._path.name, suffix=".tmp"
|
|
169
|
+
)
|
|
170
|
+
try:
|
|
171
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
172
|
+
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
|
173
|
+
handle.write("\n")
|
|
174
|
+
os.replace(tmp, self._path)
|
|
175
|
+
except BaseException:
|
|
176
|
+
try:
|
|
177
|
+
os.unlink(tmp)
|
|
178
|
+
except OSError:
|
|
179
|
+
pass
|
|
180
|
+
raise
|
|
181
|
+
|
|
182
|
+
def add_rule(self, rule: ApprovalRule) -> None:
|
|
183
|
+
"""Load → add → save. The policy is small (user-authored rules), so
|
|
184
|
+
read-modify-write per amendment is fine and keeps the file the single
|
|
185
|
+
source of truth across processes."""
|
|
186
|
+
policy = self.load()
|
|
187
|
+
policy.add(rule)
|
|
188
|
+
self.save(policy)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class PolicyApprover:
|
|
192
|
+
"""``Approver`` decorator consulting the policy before the inner approver.
|
|
193
|
+
|
|
194
|
+
A rule hit short-circuits (no host round-trip); a miss delegates. The
|
|
195
|
+
inner approver is typically ``HostApprover`` (interactive) or
|
|
196
|
+
``AutoApprover`` (headless) — the policy composes with either.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def __init__(self, inner: Approver, policy: ApprovalPolicy) -> None:
|
|
200
|
+
self._inner = inner
|
|
201
|
+
self._policy = policy
|
|
202
|
+
|
|
203
|
+
async def approve(self, request: ApprovalRequest) -> ApprovalDecision:
|
|
204
|
+
decision = self._policy.decide(request)
|
|
205
|
+
if decision is not None:
|
|
206
|
+
return decision
|
|
207
|
+
return await self._inner.approve(request)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def rule_from_amendment(
|
|
211
|
+
request: ApprovalRequest, amendment: Any
|
|
212
|
+
) -> ApprovalRule | None:
|
|
213
|
+
"""Decode a host reply's ``amendment`` payload into a rule (W2.4.2).
|
|
214
|
+
|
|
215
|
+
Wire shape::
|
|
216
|
+
|
|
217
|
+
{"decision": "allow" | "deny", "commandPrefix": ["git", "status"]?}
|
|
218
|
+
|
|
219
|
+
The rule's tool is the approved call's tool — an amendment can only
|
|
220
|
+
widen/narrow the pattern, never retarget another tool. Invalid payloads
|
|
221
|
+
return ``None`` (the decision itself still stands; only the persistence
|
|
222
|
+
is dropped), never raise into the approval path.
|
|
223
|
+
"""
|
|
224
|
+
if not isinstance(amendment, dict):
|
|
225
|
+
return None
|
|
226
|
+
decision = amendment.get("decision")
|
|
227
|
+
if decision not in _RULE_DECISIONS:
|
|
228
|
+
logger.warning("ignoring approval amendment with bad decision: %r", amendment)
|
|
229
|
+
return None
|
|
230
|
+
prefix_raw = amendment.get("commandPrefix") or []
|
|
231
|
+
if not isinstance(prefix_raw, list) or not all(
|
|
232
|
+
isinstance(t, str) for t in prefix_raw
|
|
233
|
+
):
|
|
234
|
+
logger.warning("ignoring approval amendment with bad commandPrefix: %r", amendment)
|
|
235
|
+
return None
|
|
236
|
+
try:
|
|
237
|
+
return ApprovalRule(
|
|
238
|
+
tool=request.tool_name,
|
|
239
|
+
decision=decision,
|
|
240
|
+
command_prefix=tuple(prefix_raw),
|
|
241
|
+
)
|
|
242
|
+
except ValueError:
|
|
243
|
+
return None
|
|
@@ -47,8 +47,15 @@ _FOLD_EXCERPT_CHARS = 160
|
|
|
47
47
|
__all__ = ["CompactionHooks", "estimate_tokens"]
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
def _fold_content(content: str | None) -> str:
|
|
51
|
-
|
|
50
|
+
def _fold_content(content: str | None, excerpt_chars: int = _FOLD_EXCERPT_CHARS) -> str:
|
|
51
|
+
text = content or ""
|
|
52
|
+
if len(text) <= excerpt_chars:
|
|
53
|
+
excerpt = text
|
|
54
|
+
else:
|
|
55
|
+
head = max(excerpt_chars // 5, 1)
|
|
56
|
+
tail = excerpt_chars - head
|
|
57
|
+
omitted = len(text) - excerpt_chars
|
|
58
|
+
excerpt = f"{text[:head]}\n...[{omitted} chars truncated]...\n{text[-tail:]}"
|
|
52
59
|
if excerpt:
|
|
53
60
|
return f"{_FOLDED_TOOL_MARKER} excerpt: {excerpt}"
|
|
54
61
|
return _FOLDED_TOOL_MARKER
|
|
@@ -67,6 +74,7 @@ class CompactionHooks(NoopHooks):
|
|
|
67
74
|
summarizer: LLMProvider | None = None,
|
|
68
75
|
model: str | None = None,
|
|
69
76
|
recompact_margin_ratio: float = 0.1,
|
|
77
|
+
fold_excerpt_chars: int = _FOLD_EXCERPT_CHARS,
|
|
70
78
|
) -> None:
|
|
71
79
|
if not 0 < threshold_ratio <= 1:
|
|
72
80
|
raise ValueError("threshold_ratio must be in (0, 1]")
|
|
@@ -76,6 +84,7 @@ class CompactionHooks(NoopHooks):
|
|
|
76
84
|
self._threshold = threshold_ratio
|
|
77
85
|
self._keep_last = keep_last_messages
|
|
78
86
|
self._keep_last_tools = keep_last_tool_results
|
|
87
|
+
self._fold_excerpt_chars = fold_excerpt_chars
|
|
79
88
|
self._summarizer = summarizer
|
|
80
89
|
#: Model name used for calibrated token estimates (see tokens.py).
|
|
81
90
|
self._model = model
|
|
@@ -219,7 +228,7 @@ class CompactionHooks(NoopHooks):
|
|
|
219
228
|
return [
|
|
220
229
|
LLMMessage.text_of(
|
|
221
230
|
"tool",
|
|
222
|
-
_fold_content(m.content_text),
|
|
231
|
+
_fold_content(m.content_text, self._fold_excerpt_chars),
|
|
223
232
|
name=m.name,
|
|
224
233
|
tool_call_id=m.tool_call_id,
|
|
225
234
|
)
|
|
@@ -266,12 +275,23 @@ class CompactionHooks(NoopHooks):
|
|
|
266
275
|
# discarded: never write it into the prompt cache (pi's
|
|
267
276
|
# retention=none rule). The kwarg is consumed by
|
|
268
277
|
# CacheControlProvider; providers without it ignore the key.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
278
|
+
try:
|
|
279
|
+
message, _usage = await self._summarizer.complete(
|
|
280
|
+
prompt, cache_retention="none"
|
|
281
|
+
)
|
|
282
|
+
except Exception:
|
|
283
|
+
# Transport/protocol errors here run outside the stream
|
|
284
|
+
# retry loop. Falling back keeps wrap-up running so Harbor
|
|
285
|
+
# still scores files instead of a NonZeroAgentExit.
|
|
286
|
+
return self._excerpt_summary(middle)
|
|
287
|
+
text = (message.content_text or "").strip()
|
|
288
|
+
if text:
|
|
289
|
+
return text
|
|
290
|
+
return self._excerpt_summary(middle)
|
|
291
|
+
|
|
292
|
+
def _excerpt_summary(self, middle: list[LLMMessage]) -> str:
|
|
293
|
+
# No summarizer, empty model reply, or a failed complete(): keep
|
|
294
|
+
# role + a short excerpt per message so the thread of actions survives.
|
|
275
295
|
lines = []
|
|
276
296
|
for m in middle:
|
|
277
297
|
excerpt = m.content_text.replace("\n", " ")[:200]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"context": [
|
|
3
|
+
{
|
|
4
|
+
"impl": "pressure_compaction"
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
"impl": "spill"
|
|
8
|
+
}
|
|
9
|
+
],
|
|
10
|
+
"retry": [
|
|
11
|
+
{
|
|
12
|
+
"impl": "informed_backtrack"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"impl": "simple",
|
|
16
|
+
"params": {
|
|
17
|
+
"max_attempts": 3
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"validator": "null",
|
|
22
|
+
"tools": "full",
|
|
23
|
+
"memory": "stateless",
|
|
24
|
+
"orchestration": "single",
|
|
25
|
+
"loop": {
|
|
26
|
+
"max_rounds": 80,
|
|
27
|
+
"max_tool_errors": 16,
|
|
28
|
+
"tool_dedup": false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# The framework's default harness (W1.2.4): a declaration of what the
|
|
2
|
+
# sidecar chat path has always assembled by hand —
|
|
3
|
+
# ChainHooks(CompactionHooks, SpillHooks, RetryHooks) + InMemoryStorage
|
|
4
|
+
# + full tool surface + no delegation.
|
|
5
|
+
# `max_context_tokens` is intentionally absent: the host resolves it from
|
|
6
|
+
# the model catalog at assembly time (runtime_params), per-model.
|
|
7
|
+
context:
|
|
8
|
+
- impl: pressure_compaction
|
|
9
|
+
- impl: spill
|
|
10
|
+
retry:
|
|
11
|
+
# Order matters: overflow backtrack first (it declines non-overflow
|
|
12
|
+
# errors), then taxonomy-routed backoff. Mirrors ChainHooks ordering.
|
|
13
|
+
- impl: informed_backtrack
|
|
14
|
+
# max_attempts 3 pins the production default (RetryHooks()); the strategy
|
|
15
|
+
# default of 4 would silently diverge the spec from the sidecar chat path.
|
|
16
|
+
- impl: simple
|
|
17
|
+
params: {max_attempts: 3}
|
|
18
|
+
# Quoted: bare `null` is YAML's null literal, not the implementation name.
|
|
19
|
+
validator: "null"
|
|
20
|
+
tools: full
|
|
21
|
+
memory: stateless
|
|
22
|
+
orchestration: single
|
|
23
|
+
# Loop limits the harness pins (W3.4.2.4): entrypoints read these instead of
|
|
24
|
+
# hardcoding knobs. Absent fields fall back to the entrypoint's baseline.
|
|
25
|
+
loop:
|
|
26
|
+
max_rounds: 80
|
|
27
|
+
max_tool_errors: 16
|
|
28
|
+
tool_dedup: false
|
|
@@ -18,6 +18,17 @@ class StorageError(RuntimeError):
|
|
|
18
18
|
"""Persistence layer failure."""
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
class StoreAlreadyOwnedError(StorageError):
|
|
22
|
+
"""Another process already holds the write lease for this database."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, path: str) -> None:
|
|
25
|
+
super().__init__(
|
|
26
|
+
f"store already owned: {path} "
|
|
27
|
+
"(another process has this sqlite database open for write)"
|
|
28
|
+
)
|
|
29
|
+
self.path = path
|
|
30
|
+
|
|
31
|
+
|
|
21
32
|
class ToolDispatchError(RuntimeError):
|
|
22
33
|
"""Tool router could not satisfy a ToolCall."""
|
|
23
34
|
|