steerable-agent-runtime 0.6.2__tar.gz → 0.6.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.
Files changed (146) hide show
  1. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/PKG-INFO +1 -1
  2. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/pyproject.toml +1 -1
  3. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/__init__.py +32 -0
  4. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/ask_user.py +101 -2
  5. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/branch.py +125 -0
  6. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/compaction.py +4 -2
  7. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/harness.py +39 -5
  8. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/hooks.py +26 -0
  9. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/llm/__init__.py +14 -0
  10. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/llm/openai_compat.py +39 -6
  11. steerable_agent_runtime-0.6.4/src/steerable_agent_runtime/llm/presets.py +259 -0
  12. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/loop.py +61 -1
  13. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/orchestration.py +26 -309
  14. steerable_agent_runtime-0.6.4/src/steerable_agent_runtime/plugins.py +487 -0
  15. steerable_agent_runtime-0.6.4/src/steerable_agent_runtime/pool.py +384 -0
  16. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/subagent.py +130 -36
  17. steerable_agent_runtime-0.6.4/src/steerable_agent_runtime/todo.py +213 -0
  18. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime.egg-info/PKG-INFO +1 -1
  19. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime.egg-info/SOURCES.txt +5 -0
  20. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_ask_user.py +144 -0
  21. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_branch.py +100 -2
  22. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_compaction.py +72 -0
  23. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_harness.py +19 -2
  24. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_harness_spec.py +2 -2
  25. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_hooks.py +44 -0
  26. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_loop.py +31 -0
  27. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_orchestration.py +52 -0
  28. steerable_agent_runtime-0.6.4/tests/test_plugins.py +400 -0
  29. steerable_agent_runtime-0.6.4/tests/test_provider_presets.py +254 -0
  30. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_subagent.py +326 -1
  31. steerable_agent_runtime-0.6.4/tests/test_todo.py +156 -0
  32. steerable_agent_runtime-0.6.2/src/steerable_agent_runtime/plugins.py +0 -67
  33. steerable_agent_runtime-0.6.2/tests/test_plugins.py +0 -84
  34. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/README.md +0 -0
  35. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/setup.cfg +0 -0
  36. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/antihallucination.py +0 -0
  37. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/approval.py +0 -0
  38. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/approval_policy.py +0 -0
  39. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/cache_control.py +0 -0
  40. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/calibration.py +0 -0
  41. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/config.py +0 -0
  42. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/default.harness.json +0 -0
  43. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/default.harness.yaml +0 -0
  44. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/errors.py +0 -0
  45. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/handoff.py +0 -0
  46. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/harness_spec.py +0 -0
  47. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/history.py +0 -0
  48. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
  49. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/llm/compat.py +0 -0
  50. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/llm/errors.py +0 -0
  51. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/llm/parts.py +0 -0
  52. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
  53. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/maintenance.py +0 -0
  54. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/mcp.py +0 -0
  55. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/mcp_server.py +0 -0
  56. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/model_catalog.py +0 -0
  57. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/model_info.py +0 -0
  58. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/model_resolve.py +0 -0
  59. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/observation_aging.py +0 -0
  60. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/otel.py +0 -0
  61. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/pricing.py +0 -0
  62. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/pseudo.py +0 -0
  63. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/recording.py +0 -0
  64. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/reminders.py +0 -0
  65. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/replay.py +0 -0
  66. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/resume.py +0 -0
  67. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/retry.py +0 -0
  68. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/sandboxed.py +0 -0
  69. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/skills.py +0 -0
  70. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/spill.py +0 -0
  71. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/storage/__init__.py +0 -0
  72. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
  73. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
  74. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
  75. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
  76. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/tokens.py +0 -0
  77. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/tool_schema.py +0 -0
  78. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/tool_search.py +0 -0
  79. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/tools.py +0 -0
  80. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/tracing.py +0 -0
  81. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/transport/__init__.py +0 -0
  82. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
  83. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
  84. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime/world_state.py +0 -0
  85. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
  86. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime.egg-info/requires.txt +0 -0
  87. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
  88. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_antihallucination.py +0 -0
  89. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_approval.py +0 -0
  90. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_approval_policy.py +0 -0
  91. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_cache_control.py +0 -0
  92. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_cache_instrumentation.py +0 -0
  93. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_calibration.py +0 -0
  94. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_config.py +0 -0
  95. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_content_parts.py +0 -0
  96. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_error_taxonomy.py +0 -0
  97. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_fragment_bounds.py +0 -0
  98. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_golden.py +0 -0
  99. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_handoff.py +0 -0
  100. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_history.py +0 -0
  101. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_history_persistence.py +0 -0
  102. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_in_memory_storage.py +0 -0
  103. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_llm_wire_helpers.py +0 -0
  104. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_long_session.py +0 -0
  105. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_loop_cancellation.py +0 -0
  106. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_loop_replay.py +0 -0
  107. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_loop_sandbox_event.py +0 -0
  108. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_maintenance.py +0 -0
  109. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_mcp.py +0 -0
  110. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_mcp_server.py +0 -0
  111. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_model_catalog.py +0 -0
  112. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_model_equivalence.py +0 -0
  113. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_model_info.py +0 -0
  114. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_model_resolve.py +0 -0
  115. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_observation_aging.py +0 -0
  116. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_otel.py +0 -0
  117. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_parallel_tools.py +0 -0
  118. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_provider_compat.py +0 -0
  119. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_pseudo.py +0 -0
  120. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_recording.py +0 -0
  121. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_reminders.py +0 -0
  122. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_replay_crosslang.py +0 -0
  123. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_resume.py +0 -0
  124. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_retry_hooks.py +0 -0
  125. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_safety_gate.py +0 -0
  126. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_sandboxed.py +0 -0
  127. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_skills.py +0 -0
  128. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_soft_timeout.py +0 -0
  129. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_spill.py +0 -0
  130. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_sqlite_storage.py +0 -0
  131. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_steer.py +0 -0
  132. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_storage_contract.py +0 -0
  133. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_stream_strip.py +0 -0
  134. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_system_proxy.py +0 -0
  135. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_tokens.py +0 -0
  136. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_tool_exposure.py +0 -0
  137. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_tool_hygiene.py +0 -0
  138. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_tool_router.py +0 -0
  139. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_tool_schema.py +0 -0
  140. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_tool_timeout.py +0 -0
  141. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_trace_recorder.py +0 -0
  142. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_transport_jsonrpc.py +0 -0
  143. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_transport_sse.py +0 -0
  144. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_usage_attribution.py +0 -0
  145. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_world_state.py +0 -0
  146. {steerable_agent_runtime-0.6.2 → steerable_agent_runtime-0.6.4}/tests/test_write_lease.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerable-agent-runtime
3
- Version: 0.6.2
3
+ Version: 0.6.4
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "steerable-agent-runtime"
3
- version = "0.6.2"
3
+ version = "0.6.4"
4
4
  description = "Steerable agent runtime: LLM, tool, storage, and transport adapters."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -35,6 +35,13 @@ from .ask_user import (
35
35
  AskUserHandler,
36
36
  make_ask_user_tool,
37
37
  )
38
+ from .todo import (
39
+ TODO_SCHEMA,
40
+ TODO_TOOL_NAME,
41
+ TodoStore,
42
+ make_todo_write_tool,
43
+ todo_write_tool_descriptor,
44
+ )
38
45
  from .config import (
39
46
  DEFAULT_CONFIG_PATH,
40
47
  ConfigError,
@@ -44,13 +51,23 @@ from .config import (
44
51
  )
45
52
  from .plugins import (
46
53
  STEERABLE_TOOLS_ENTRY_POINT_GROUP,
54
+ DirectorySource,
55
+ EntryPointSource,
47
56
  PluginLoadError,
57
+ PluginRecord,
58
+ PluginRegistry,
59
+ PluginSource,
60
+ PluginSpec,
61
+ PluginStateError,
48
62
  load_tool_entry_points,
49
63
  )
50
64
  from .branch import (
51
65
  BranchPoint,
66
+ FamilyTree,
67
+ FamilyTreeNode,
52
68
  ForkResult,
53
69
  branch_label,
70
+ family_tree,
54
71
  fork_record,
55
72
  lineage,
56
73
  resolve_fork_seq,
@@ -252,11 +269,20 @@ __all__ = [
252
269
  "TOOL_SEARCH_NAME",
253
270
  "ASK_USER_SCHEMA",
254
271
  "ASK_USER_TOOL_NAME",
272
+ "TODO_SCHEMA",
273
+ "TODO_TOOL_NAME",
255
274
  "STEERABLE_TOOLS_ENTRY_POINT_GROUP",
256
275
  "AgentPool",
257
276
  "AntiHallucinationConfig",
258
277
  "AskUserHandler",
278
+ "DirectorySource",
279
+ "EntryPointSource",
259
280
  "PluginLoadError",
281
+ "PluginRecord",
282
+ "PluginRegistry",
283
+ "PluginSource",
284
+ "PluginSpec",
285
+ "PluginStateError",
260
286
  "AntiHallucinationHooks",
261
287
  "ApprovalAborted",
262
288
  "ApprovalDecision",
@@ -288,6 +314,8 @@ __all__ = [
288
314
  "ContextManager",
289
315
  "CoreLoop",
290
316
  "ExecutionBudget",
317
+ "FamilyTree",
318
+ "FamilyTreeNode",
291
319
  "FilesystemSkillProvider",
292
320
  "FilesystemSpillStore",
293
321
  "FilteredToolsExecutor",
@@ -363,6 +391,7 @@ __all__ = [
363
391
  "ToolExecutor",
364
392
  "ToolExposure",
365
393
  "ToolRouter",
394
+ "TodoStore",
366
395
  "TraceRecorder",
367
396
  "TranscriptAppend",
368
397
  "TransportAdapter",
@@ -393,6 +422,7 @@ __all__ = [
393
422
  "export_trace",
394
423
  "extract_inline_tool_calls",
395
424
  "factor_for_model",
425
+ "family_tree",
396
426
  "fork_record",
397
427
  "last_world_state_snapshot",
398
428
  "lineage",
@@ -403,6 +433,7 @@ __all__ = [
403
433
  "load_recorded_requests",
404
434
  "load_transcript",
405
435
  "make_ask_user_tool",
436
+ "make_todo_write_tool",
406
437
  "matches_conditions",
407
438
  "mcp_invoker",
408
439
  "merge_patch",
@@ -436,6 +467,7 @@ __all__ = [
436
467
  "system_blocks_with_cache",
437
468
  "text_parts",
438
469
  "to_otlp_json",
470
+ "todo_write_tool_descriptor",
439
471
  "tool",
440
472
  "tool_search_descriptor",
441
473
  "upgrade_entry_dict",
@@ -39,16 +39,37 @@ ASK_USER_SCHEMA: dict[str, Any] = {
39
39
  },
40
40
  "questions": {
41
41
  "type": "array",
42
+ "minItems": 1,
43
+ "maxItems": 4,
42
44
  "items": {
43
45
  "type": "object",
44
46
  "properties": {
45
47
  "id": {"type": "string"},
46
48
  "text": {"type": "string"},
49
+ "header": {
50
+ "type": "string",
51
+ "maxLength": 12,
52
+ "description": (
53
+ "Short chip label shown above the question "
54
+ "(<=12 chars). Optional; the host derives one "
55
+ "from `text` when absent."
56
+ ),
57
+ },
47
58
  "type": {
48
59
  "type": "string",
49
60
  "enum": ["select", "text", "password"],
50
61
  },
51
- "options": {"type": "array", "items": {"type": "string"}},
62
+ "options": {
63
+ "type": "array",
64
+ "items": {"type": "string"},
65
+ "minItems": 2,
66
+ "maxItems": 4,
67
+ "description": (
68
+ "Choices for a select question (2-4). The host "
69
+ "auto-appends an 'Other' free-text escape; do "
70
+ "not list it yourself."
71
+ ),
72
+ },
52
73
  "placeholder": {"type": "string"},
53
74
  "multiSelect": {"type": "boolean"},
54
75
  },
@@ -67,8 +88,40 @@ ASK_USER_SCHEMA: dict[str, Any] = {
67
88
  _QUESTION_ALIASES = {"id": "name", "text": "message", "options": "choices"}
68
89
 
69
90
 
91
+ #: Claude Code ``AskUserQuestion`` parity bounds. The card renders a batched
92
+ #: decision, not an interview — cap the question count so the model cannot
93
+ #: bombard the user, and cap select options so it pre-categorizes instead of
94
+ #: dumping a long list. The host auto-appends an "Other" free-text escape, so
95
+ #: the model never lists it (the option space the model thought of is not the
96
+ #: complete space).
97
+ _MIN_QUESTIONS = 1
98
+ _MAX_QUESTIONS = 4
99
+ _MIN_OPTIONS = 2
100
+ _MAX_OPTIONS = 4
101
+ #: ``header`` is the short chip label shown above the question (CC parity).
102
+ #: Optional on the wire; derived from ``text`` when absent.
103
+ _MAX_HEADER_LEN = 12
104
+
105
+
106
+ def _derive_header(text: str) -> str:
107
+ """Derive a <=12-char chip label from the question text when the model did
108
+ not supply ``header``. Strips trailing punctuation and truncates on a word
109
+ boundary so the chip stays readable."""
110
+ cleaned = text.strip().rstrip("?.!。")
111
+ if len(cleaned) <= _MAX_HEADER_LEN:
112
+ return cleaned
113
+ truncated = cleaned[:_MAX_HEADER_LEN]
114
+ # Prefer cutting on the last space so the chip does not end mid-word.
115
+ space = truncated.rfind(" ")
116
+ if space > 0:
117
+ truncated = truncated[:space]
118
+ return truncated
119
+
120
+
70
121
  def _normalize_questions(questions: list[dict[str, Any]]) -> list[dict[str, Any]]:
71
- """Map known alias keys onto the canonical payload fields and require the
122
+ """Map known alias keys onto the canonical payload fields, enforce the
123
+ Claude Code ``AskUserQuestion`` bounds (1-4 questions, 2-4 options per
124
+ select, ``header`` <=12 chars, ``multiSelect`` explicit), and require the
72
125
  two fields the host card cannot render without (``id`` to key the answer,
73
126
  ``text`` to label the control). A violation raises ``ToolDispatchError`` —
74
127
  the router wraps it as the tool result, so the model sees exactly what to
@@ -77,6 +130,11 @@ def _normalize_questions(questions: list[dict[str, Any]]) -> list[dict[str, Any]
77
130
  raise ToolDispatchError(
78
131
  f"ask_user: questions must be an array, got {type(questions).__name__}"
79
132
  )
133
+ if not (_MIN_QUESTIONS <= len(questions) <= _MAX_QUESTIONS):
134
+ raise ToolDispatchError(
135
+ f"ask_user: questions must contain {_MIN_QUESTIONS}-{_MAX_QUESTIONS} "
136
+ f"items, got {len(questions)}. Batch a small decision, not an interview."
137
+ )
80
138
  normalized: list[dict[str, Any]] = []
81
139
  for index, question in enumerate(questions):
82
140
  if not isinstance(question, dict):
@@ -98,6 +156,47 @@ def _normalize_questions(questions: list[dict[str, Any]]) -> list[dict[str, Any]
98
156
  f"ask_user: questions[{index}] is missing a non-empty string "
99
157
  '"text" (the question label shown to the user).'
100
158
  )
159
+ # header: optional on the wire; validate length when present, derive
160
+ # from text when absent so the card always has a chip label.
161
+ header = q.get("header")
162
+ if header is None:
163
+ q["header"] = _derive_header(q["text"])
164
+ elif not isinstance(header, str) or not header:
165
+ raise ToolDispatchError(
166
+ f"ask_user: questions[{index}].header must be a non-empty string."
167
+ )
168
+ elif len(header) > _MAX_HEADER_LEN:
169
+ raise ToolDispatchError(
170
+ f"ask_user: questions[{index}].header is {len(header)} chars; "
171
+ f"the chip label must be <= {_MAX_HEADER_LEN}."
172
+ )
173
+ # multiSelect: CC requires the model to commit to single vs multi.
174
+ # Default to False when absent so existing single-select calls keep
175
+ # working, but stamp it so hosts always read an explicit boolean.
176
+ if "multiSelect" not in q or q["multiSelect"] is None:
177
+ q["multiSelect"] = False
178
+ elif not isinstance(q["multiSelect"], bool):
179
+ raise ToolDispatchError(
180
+ f"ask_user: questions[{index}].multiSelect must be a boolean, "
181
+ f"got {type(q['multiSelect']).__name__}."
182
+ )
183
+ # options: only a select question carries choices; enforce the 2-4
184
+ # bound so the model pre-categorizes instead of dumping a long list.
185
+ qtype = q.get("type", "select")
186
+ options = q.get("options")
187
+ if qtype == "select":
188
+ if options is not None:
189
+ if not isinstance(options, list):
190
+ raise ToolDispatchError(
191
+ f"ask_user: questions[{index}].options must be an array, "
192
+ f"got {type(options).__name__}."
193
+ )
194
+ if not (_MIN_OPTIONS <= len(options) <= _MAX_OPTIONS):
195
+ raise ToolDispatchError(
196
+ f"ask_user: questions[{index}] has {len(options)} options; "
197
+ f"a select question needs {_MIN_OPTIONS}-{_MAX_OPTIONS}. "
198
+ "The host auto-appends an 'Other' escape — do not list it."
199
+ )
101
200
  normalized.append(q)
102
201
  return normalized
103
202
 
@@ -19,6 +19,8 @@ This module ties the existing primitives (``load_history_items`` +
19
19
  messages, not record seqs (regenerate = fork keeping the last user
20
20
  turn, dropping the assistant reply after it).
21
21
  - ``lineage`` — walk the seed-provenance chain upwards, cycle-guarded.
22
+ - ``family_tree`` — the full family containing a record: lineage to the
23
+ root, then every descendant expanded from one storage-wide scan.
22
24
 
23
25
  Children discovery is deliberately host-side: ``StorageAdapter`` has no
24
26
  record enumeration, so stores that can list records implement
@@ -49,8 +51,11 @@ if TYPE_CHECKING:
49
51
 
50
52
  __all__ = [
51
53
  "BranchPoint",
54
+ "FamilyTree",
55
+ "FamilyTreeNode",
52
56
  "ForkResult",
53
57
  "branch_label",
58
+ "family_tree",
54
59
  "fork_record",
55
60
  "lineage",
56
61
  "resolve_fork_seq",
@@ -60,6 +65,11 @@ __all__ = [
60
65
  #: corruption, not a deep tree; fail loud past this depth.
61
66
  _MAX_LINEAGE_DEPTH = 32
62
67
 
68
+ #: Bound on ``family_tree`` expansion — record enumeration is
69
+ #: storage-wide, so a runaway family is cut off and reported
70
+ #: (``FamilyTree.truncated``) rather than streamed unbounded to the host.
71
+ _MAX_TREE_NODES = 500
72
+
63
73
  #: Reverse-scan page for ``resolve_fork_seq`` — the regen fork point is
64
74
  #: almost always in the tail page.
65
75
  _FORK_SCAN_PAGE = 128
@@ -95,6 +105,32 @@ class ForkResult:
95
105
  messages: list["LLMMessage"]
96
106
 
97
107
 
108
+ @dataclass(frozen=True, slots=True)
109
+ class FamilyTreeNode:
110
+ """One node in a full branch-family tree — a ``BranchPoint`` plus its
111
+ children, recursively. ``depth`` is 0 on the family root, +1 per fork
112
+ hop, matching the depths ``lineage`` assigns along the chain."""
113
+
114
+ record_id: str
115
+ source_record_id: str | None
116
+ source_until_seq: int | None
117
+ label: str
118
+ depth: int
119
+ children: tuple["FamilyTreeNode", ...] = ()
120
+
121
+
122
+ @dataclass(frozen=True, slots=True)
123
+ class FamilyTree:
124
+ """The full branch family containing one record, expanded from the
125
+ family root. ``truncated`` reports that a safety bound (depth or node
126
+ count) cut the expansion — the returned tree is still valid, just
127
+ incomplete below the cut."""
128
+
129
+ root: FamilyTreeNode
130
+ node_count: int
131
+ truncated: bool
132
+
133
+
98
134
  def branch_label(messages: list["LLMMessage"], *, max_chars: int = 60) -> str:
99
135
  """Derive a branch summary from the forked prefix — deterministic, no
100
136
  LLM call: the last user message, whitespace-collapsed and truncated.
@@ -292,3 +328,92 @@ async def lineage(
292
328
  )
293
329
  for index, point in enumerate(chain)
294
330
  ]
331
+
332
+
333
+ async def family_tree(
334
+ storage: "StorageAdapter",
335
+ record_id: str,
336
+ *,
337
+ max_depth: int = _MAX_LINEAGE_DEPTH,
338
+ max_nodes: int = _MAX_TREE_NODES,
339
+ ) -> FamilyTree:
340
+ """Full branch family containing ``record_id``, expanded from the root.
341
+
342
+ Walks seed provenance to the family root (``lineage``), then expands
343
+ every descendant from ONE storage-wide scan: parent edges live in each
344
+ record's first-entry seed, so reading that entry per enumerated record
345
+ (``list_history_records``) yields the children of every family member
346
+ at once — the same discovery ``agent.session.branches`` does for one
347
+ level, applied recursively without re-scanning per node.
348
+
349
+ Raises ``KeyError`` when the record has no entries (an empty record is
350
+ indistinguishable from a missing one — records are created by append)
351
+ and ``ValueError`` on lineage corruption (cycle / over-deep chain),
352
+ matching ``fork_record`` / ``lineage``. Expansion is bounded twice:
353
+ ``max_depth`` cuts descendants below that depth from the root (the
354
+ default matches the lineage corruption bound, so by default only
355
+ corrupt families are cut) and ``max_nodes`` caps the total — either
356
+ cut is reported via ``FamilyTree.truncated`` instead of streaming
357
+ unbounded data. ``max_depth`` does NOT relax the lineage walk itself:
358
+ a queried record whose own chain exceeds the corruption bound still
359
+ raises ``ValueError``.
360
+ """
361
+
362
+ if not await storage.list_history(record_id, limit=1):
363
+ raise KeyError(f"record not found: {record_id}")
364
+ chain = await lineage(storage, record_id)
365
+ root_point = chain[0]
366
+
367
+ children_of: dict[str, list[BranchPoint]] = {}
368
+ for candidate in await storage.list_history_records():
369
+ if candidate == root_point.record_id:
370
+ continue
371
+ first = await storage.list_history(candidate, limit=1)
372
+ if not first:
373
+ continue
374
+ entry = entry_from_dict(first[0])
375
+ if isinstance(entry, HistorySeed) and entry.source_record_id is not None:
376
+ children_of.setdefault(entry.source_record_id, []).append(
377
+ BranchPoint(
378
+ record_id=candidate,
379
+ source_record_id=entry.source_record_id,
380
+ source_until_seq=entry.source_until_seq,
381
+ label=branch_label(list(entry.messages)),
382
+ )
383
+ )
384
+
385
+ total = 0
386
+ truncated = False
387
+
388
+ def build(point: BranchPoint, depth: int) -> FamilyTreeNode:
389
+ nonlocal total, truncated
390
+ total += 1
391
+ children: list[FamilyTreeNode] = []
392
+ if depth >= max_depth:
393
+ if children_of.get(point.record_id):
394
+ truncated = True
395
+ else:
396
+ for child in children_of.get(point.record_id, []):
397
+ if total >= max_nodes:
398
+ truncated = True
399
+ break
400
+ children.append(build(child, depth + 1))
401
+ return FamilyTreeNode(
402
+ record_id=point.record_id,
403
+ source_record_id=point.source_record_id,
404
+ source_until_seq=point.source_until_seq,
405
+ label=point.label,
406
+ depth=depth,
407
+ children=tuple(children),
408
+ )
409
+
410
+ root = build(
411
+ BranchPoint(
412
+ record_id=root_point.record_id,
413
+ source_record_id=root_point.source_record_id,
414
+ source_until_seq=root_point.source_until_seq,
415
+ label=root_point.label,
416
+ ),
417
+ 0,
418
+ )
419
+ return FamilyTree(root=root, node_count=total, truncated=truncated)
@@ -38,8 +38,10 @@ Three trigger paths share the fold/summarize machinery:
38
38
  prompt-cache prefix, so the interval trades cache hits for a bounded
39
39
  transcript;
40
40
  - **manual** (``compact_now``) — the host-command path (CC ``/compact``
41
- parity): fold + summarize on demand, bypassing threshold, hysteresis, and
42
- the circuit breaker.
41
+ parity): the host calls the sidecar's ``agent.chat.compact`` RPC, which
42
+ sets ``CoreLoop.request_compact()``; the loop runs ``compact_now`` at the
43
+ next pre_step boundary, folding + summarizing on demand, bypassing
44
+ threshold, hysteresis, and the circuit breaker.
43
45
 
44
46
  Two safety rails share the machinery:
45
47
 
@@ -128,12 +128,27 @@ class OrchestrationStrategy(Protocol):
128
128
 
129
129
 
130
130
  class _PreStepOnly(NoopHooks):
131
- def __init__(self, inner: LoopHooks) -> None:
131
+ """Project a hooks impl onto the pre_step slice.
132
+
133
+ ``forward`` names optional off-protocol capabilities (probed with
134
+ ``getattr``, e.g. ``compact_now``) that must stay reachable through the
135
+ projection — without it the wrapper would silently sever the sidecar's
136
+ ``agent.chat.compact`` path, which probes the assembled chain for the
137
+ one hook implementing manual compaction.
138
+ """
139
+
140
+ def __init__(self, inner: LoopHooks, *, forward: tuple[str, ...] = ()) -> None:
132
141
  self._inner = inner
142
+ self._forward = forward
133
143
 
134
144
  async def pre_step(self, transcript: Any, ctx: Any) -> Any:
135
145
  return await self._inner.pre_step(transcript, ctx)
136
146
 
147
+ def __getattr__(self, name: str) -> Any:
148
+ if name in self._forward:
149
+ return getattr(self._inner, name)
150
+ raise AttributeError(name)
151
+
137
152
 
138
153
  class _OnRequestErrorOnly(NoopHooks):
139
154
  def __init__(self, inner: LoopHooks) -> None:
@@ -188,6 +203,12 @@ class PressureCompaction:
188
203
  keep_last_tool_results: int = 2
189
204
  fold_excerpt_chars: int | None = None
190
205
  model: str | None = None
206
+ # Proactive micro-compaction (CC time-based microcompact parity): fold old
207
+ # tool results every N rounds regardless of pressure. 0 (default) is off —
208
+ # each fold invalidates the provider prompt-cache prefix, so the interval
209
+ # trades cache hits for a bounded transcript. R16 found compaction does not
210
+ # move the eval score, so this stays opt-in capability, not a default.
211
+ micro_compact_interval_rounds: int = 0
191
212
  name: str = "pressure_compaction"
192
213
  assumes: str = (
193
214
  "long trajectories exceed the window; older detail is expendable "
@@ -209,8 +230,10 @@ class PressureCompaction:
209
230
  keep_last_tool_results=self.keep_last_tool_results,
210
231
  summarizer=provider,
211
232
  model=self.model,
233
+ micro_compact_interval_rounds=self.micro_compact_interval_rounds,
212
234
  **extra,
213
- )
235
+ ),
236
+ forward=("compact_now",),
214
237
  )
215
238
 
216
239
 
@@ -805,8 +828,16 @@ class SingleAgent:
805
828
 
806
829
 
807
830
  @dataclass(frozen=True, slots=True)
808
- class SubAgentDelegation:
809
- """The existing AgentPool six-tool delegation (OrchestrationExecutor)."""
831
+ class PoolOrchestration:
832
+ """The six-tool orchestration family over AgentPool (OrchestrationExecutor).
833
+
834
+ This is the advanced, model-driven orchestration arm: the parent gets
835
+ ``agent_spawn``/``agent_send``/``agent_wait``/``agent_close``/
836
+ ``agent_list``/``agent_interrupt`` and drives parallel children
837
+ explicitly. (The single-tool ``delegate_subagent`` seam —
838
+ ``SubagentExecutor`` — is the default multi-agent surface on the
839
+ sidecar chat path; both run on the same AgentPool engine.)
840
+ """
810
841
 
811
842
  name: str = "subagent"
812
843
  assumes: str = (
@@ -852,5 +883,8 @@ STRATEGY_REGISTRY: dict[str, dict[str, type]] = {
852
883
  "progressive": ProgressiveDisclosure,
853
884
  },
854
885
  "memory": {"stateless": Stateless, "filesystem": FilesystemState},
855
- "orchestration": {"single": SingleAgent, "subagent": SubAgentDelegation},
886
+ # The "subagent" key names the eval arm (harness specs predate the
887
+ # delegate-on-pool unification); the implementation is the six-tool
888
+ # pool orchestration family.
889
+ "orchestration": {"single": SingleAgent, "subagent": PoolOrchestration},
856
890
  }
@@ -309,6 +309,10 @@ class ChainHooks:
309
309
  so a trailing delivery gate still forces writes when an earlier
310
310
  validator would only ask for a no-tools summary.
311
311
  - ``wrap_up_may_drop_tools``: False if any hook forbids dropping tools.
312
+ - ``compact_now``: optional capability (not part of the ``LoopHooks``
313
+ protocol), probed with ``getattr`` like ``on_stream_chunk`` —
314
+ delegated to the first hook that implements it (CompactionHooks);
315
+ a chain without one returns a no-op proceed.
312
316
 
313
317
  This is how a product stacks e.g. compaction + spill + retry without the
314
318
  loop knowing about any of them.
@@ -363,6 +367,28 @@ class ChainHooks:
363
367
  append_action=append_action,
364
368
  )
365
369
 
370
+ async def compact_now(
371
+ self, transcript: list[LLMMessage], ctx: LoopContext
372
+ ) -> PreStepAction:
373
+ """Manual compaction (CC ``/compact`` parity): delegate to the first
374
+ hook in the chain that implements ``compact_now`` (CompactionHooks).
375
+
376
+ ``compact_now`` is an optional capability, not a ``LoopHooks``
377
+ protocol method, so membership is probed with ``getattr`` — the same
378
+ pattern the loop uses for ``on_stream_chunk``. ``NoopHooks`` defines
379
+ no ``compact_now``, so the probe skips it (and any other hook that
380
+ only has the protocol surface) without a special case. A chain with
381
+ no compaction hook returns a no-op proceed: a manual compact request
382
+ must never fail the turn.
383
+ """
384
+ for hook in self._hooks:
385
+ callback = getattr(hook, "compact_now", None)
386
+ if callable(callback):
387
+ return await callback(transcript, ctx)
388
+ return PreStepAction(
389
+ kind="proceed", reason="compact: no compaction hook in chain"
390
+ )
391
+
366
392
  async def post_tool_result(
367
393
  self, result: ToolResult, call: ToolCall, ctx: LoopContext
368
394
  ) -> ToolResult:
@@ -159,9 +159,18 @@ from .errors import (
159
159
  is_retryable,
160
160
  )
161
161
  from .openai_compat import OpenAICompatProvider
162
+ from .presets import (
163
+ PROVIDER_PRESETS,
164
+ PresetEntry,
165
+ ProviderPreset,
166
+ describe_provider_presets,
167
+ preset_for,
168
+ register_provider_preset,
169
+ )
162
170
 
163
171
  __all__ = [
164
172
  "PROVIDER_COMPAT_HOSTS",
173
+ "PROVIDER_PRESETS",
165
174
  "RETRYABLE_KINDS",
166
175
  "AnthropicProvider",
167
176
  "ContentPart",
@@ -174,7 +183,10 @@ __all__ = [
174
183
  "LLMStreamChunk",
175
184
  "LLMUsage",
176
185
  "OpenAICompatFlags",
186
+ "PresetEntry",
187
+ "ProviderPreset",
177
188
  "describe_compat_flags",
189
+ "describe_provider_presets",
178
190
  "OpenAICompatProvider",
179
191
  "TextPart",
180
192
  "classify_error",
@@ -182,5 +194,7 @@ __all__ = [
182
194
  "compat_for_base_url",
183
195
  "content_text",
184
196
  "is_retryable",
197
+ "preset_for",
198
+ "register_provider_preset",
185
199
  "text_parts",
186
200
  ]