linch 1.0.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.
Files changed (328) hide show
  1. linch-1.0.0/.gitignore +17 -0
  2. linch-1.0.0/AGENTS.md +39 -0
  3. linch-1.0.0/CLAUDE.md +198 -0
  4. linch-1.0.0/LICENSE +21 -0
  5. linch-1.0.0/PKG-INFO +426 -0
  6. linch-1.0.0/README.md +398 -0
  7. linch-1.0.0/docs/ROADMAP.md +666 -0
  8. linch-1.0.0/docs/architecture/README.md +25 -0
  9. linch-1.0.0/docs/architecture/compaction.md +76 -0
  10. linch-1.0.0/docs/architecture/data-types.md +102 -0
  11. linch-1.0.0/docs/architecture/events.md +68 -0
  12. linch-1.0.0/docs/architecture/invariants.md +51 -0
  13. linch-1.0.0/docs/architecture/module-inventory.md +54 -0
  14. linch-1.0.0/docs/architecture/overview.md +96 -0
  15. linch-1.0.0/docs/architecture/provider-contract.md +53 -0
  16. linch-1.0.0/docs/architecture/skills-subagents.md +121 -0
  17. linch-1.0.0/docs/architecture/structured-output.md +48 -0
  18. linch-1.0.0/docs/architecture/subsystems.md +410 -0
  19. linch-1.0.0/docs/architecture/system-prompt.md +43 -0
  20. linch-1.0.0/docs/architecture/tool-protocol.md +97 -0
  21. linch-1.0.0/docs/architecture/turn-lifecycle.md +91 -0
  22. linch-1.0.0/docs/architecture.md +23 -0
  23. linch-1.0.0/docs/contributing.md +310 -0
  24. linch-1.0.0/docs/usage/README.md +99 -0
  25. linch-1.0.0/docs/usage/agent.md +231 -0
  26. linch-1.0.0/docs/usage/context-and-memory.md +223 -0
  27. linch-1.0.0/docs/usage/coordination.md +129 -0
  28. linch-1.0.0/docs/usage/deep-agent.md +138 -0
  29. linch-1.0.0/docs/usage/events.md +232 -0
  30. linch-1.0.0/docs/usage/examples.md +110 -0
  31. linch-1.0.0/docs/usage/extending.md +206 -0
  32. linch-1.0.0/docs/usage/filesystem.md +146 -0
  33. linch-1.0.0/docs/usage/hooks.md +195 -0
  34. linch-1.0.0/docs/usage/providers.md +348 -0
  35. linch-1.0.0/docs/usage/skills.md +41 -0
  36. linch-1.0.0/docs/usage/structured-output.md +173 -0
  37. linch-1.0.0/docs/usage/tools.md +325 -0
  38. linch-1.0.0/docs/usage/workflows.md +97 -0
  39. linch-1.0.0/docs/usage.md +25 -0
  40. linch-1.0.0/docs/versioning.md +75 -0
  41. linch-1.0.0/examples/context/context_injection.py +168 -0
  42. linch-1.0.0/examples/context/rag_context_builder.py +161 -0
  43. linch-1.0.0/examples/coordination/scheduling_agent.py +119 -0
  44. linch-1.0.0/examples/coordination/team_mailbox.py +146 -0
  45. linch-1.0.0/examples/core/aligning_agent.py +177 -0
  46. linch-1.0.0/examples/core/ask_user_agent.py +132 -0
  47. linch-1.0.0/examples/core/budget_capped_agent.py +74 -0
  48. linch-1.0.0/examples/core/chat_agent.py +178 -0
  49. linch-1.0.0/examples/core/coding_agent.py +116 -0
  50. linch-1.0.0/examples/core/compaction_ladder_agent.py +123 -0
  51. linch-1.0.0/examples/core/custom_permissions.py +340 -0
  52. linch-1.0.0/examples/core/deep_agent_resume.py +293 -0
  53. linch-1.0.0/examples/core/event_streaming.py +317 -0
  54. linch-1.0.0/examples/core/interactive_cli.py +27 -0
  55. linch-1.0.0/examples/core/loop_guard_agent.py +196 -0
  56. linch-1.0.0/examples/core/multi_session.py +379 -0
  57. linch-1.0.0/examples/core/policy_aware_execution.py +80 -0
  58. linch-1.0.0/examples/core/reading_agent.py +116 -0
  59. linch-1.0.0/examples/core/structured_output.py +432 -0
  60. linch-1.0.0/examples/core/system_prompts.py +302 -0
  61. linch-1.0.0/examples/core/tiny_agent.py +30 -0
  62. linch-1.0.0/examples/core/workflow_fleet.py +136 -0
  63. linch-1.0.0/examples/integrations/multi_agent_isolation.py +451 -0
  64. linch-1.0.0/examples/integrations/skills_usage.py +122 -0
  65. linch-1.0.0/examples/integrations/subagent_coordinator.py +169 -0
  66. linch-1.0.0/examples/memory/memory_agent.py +127 -0
  67. linch-1.0.0/examples/memory/pgvector_memory.py +295 -0
  68. linch-1.0.0/examples/memory/sqlite_memory_agent.py +171 -0
  69. linch-1.0.0/examples/observability/custom_observer.py +135 -0
  70. linch-1.0.0/examples/observability/observability_agent.py +102 -0
  71. linch-1.0.0/examples/providers/anthropic_agent.py +171 -0
  72. linch-1.0.0/examples/providers/deepseek_agent.py +356 -0
  73. linch-1.0.0/examples/providers/openai_agent.py +314 -0
  74. linch-1.0.0/examples/recipes/ralph_loop.py +166 -0
  75. linch-1.0.0/examples/recipes/research_desk.py +234 -0
  76. linch-1.0.0/examples/tools/custom_tools.py +322 -0
  77. linch-1.0.0/examples/tools/filesystem_offload.py +173 -0
  78. linch-1.0.0/examples/tools/parallel_search_agent.py +149 -0
  79. linch-1.0.0/examples/tools/rag_tools.py +465 -0
  80. linch-1.0.0/examples/tools/runtime_tools.py +132 -0
  81. linch-1.0.0/examples/tools/tool_middleware.py +174 -0
  82. linch-1.0.0/examples/tools/tool_reliability_agent.py +266 -0
  83. linch-1.0.0/pyproject.toml +62 -0
  84. linch-1.0.0/scripts/benchmark_runtime.py +130 -0
  85. linch-1.0.0/scripts/colab_sglang_cache_diag.py +205 -0
  86. linch-1.0.0/scripts/colab_sglang_provider_test.py +290 -0
  87. linch-1.0.0/scripts/colab_vllm_diagnostic.py +199 -0
  88. linch-1.0.0/scripts/colab_vllm_provider_test.py +307 -0
  89. linch-1.0.0/src/linch/__init__.py +487 -0
  90. linch-1.0.0/src/linch/_blocking.py +71 -0
  91. linch-1.0.0/src/linch/_http_errors.py +99 -0
  92. linch-1.0.0/src/linch/_prompt_cache.py +120 -0
  93. linch-1.0.0/src/linch/_version.py +26 -0
  94. linch-1.0.0/src/linch/abort.py +77 -0
  95. linch-1.0.0/src/linch/agent.py +1088 -0
  96. linch-1.0.0/src/linch/budget.py +93 -0
  97. linch-1.0.0/src/linch/compaction.py +521 -0
  98. linch-1.0.0/src/linch/config.py +97 -0
  99. linch-1.0.0/src/linch/context/__init__.py +21 -0
  100. linch-1.0.0/src/linch/context/builder.py +216 -0
  101. linch-1.0.0/src/linch/coordination/__init__.py +57 -0
  102. linch-1.0.0/src/linch/coordination/mailbox/__init__.py +13 -0
  103. linch-1.0.0/src/linch/coordination/mailbox/core.py +80 -0
  104. linch-1.0.0/src/linch/coordination/mailbox/correlation.py +51 -0
  105. linch-1.0.0/src/linch/coordination/scheduling/__init__.py +19 -0
  106. linch-1.0.0/src/linch/coordination/scheduling/cron.py +105 -0
  107. linch-1.0.0/src/linch/coordination/scheduling/loop.py +114 -0
  108. linch-1.0.0/src/linch/coordination/scheduling/schedule.py +71 -0
  109. linch-1.0.0/src/linch/coordination/scheduling/sqlite.py +142 -0
  110. linch-1.0.0/src/linch/coordination/scheduling/store.py +61 -0
  111. linch-1.0.0/src/linch/coordination/scheduling/tools.py +72 -0
  112. linch-1.0.0/src/linch/coordination/send_message.py +107 -0
  113. linch-1.0.0/src/linch/deep_agent/__init__.py +9 -0
  114. linch-1.0.0/src/linch/deep_agent/factory.py +194 -0
  115. linch-1.0.0/src/linch/deep_agent/prompts.py +126 -0
  116. linch-1.0.0/src/linch/deep_agent/subagents.py +127 -0
  117. linch-1.0.0/src/linch/errors.py +64 -0
  118. linch-1.0.0/src/linch/evals/__init__.py +46 -0
  119. linch-1.0.0/src/linch/evals/harness.py +226 -0
  120. linch-1.0.0/src/linch/evals/scorers.py +215 -0
  121. linch-1.0.0/src/linch/evals/scripted.py +96 -0
  122. linch-1.0.0/src/linch/events.py +887 -0
  123. linch-1.0.0/src/linch/filesystem/__init__.py +54 -0
  124. linch-1.0.0/src/linch/filesystem/backend.py +253 -0
  125. linch-1.0.0/src/linch/filesystem/disk.py +119 -0
  126. linch-1.0.0/src/linch/filesystem/offload.py +140 -0
  127. linch-1.0.0/src/linch/filesystem/postgres.py +180 -0
  128. linch-1.0.0/src/linch/filesystem/sqlite.py +157 -0
  129. linch-1.0.0/src/linch/filesystem/tools.py +287 -0
  130. linch-1.0.0/src/linch/hooks/__init__.py +71 -0
  131. linch-1.0.0/src/linch/hooks/adapters.py +378 -0
  132. linch-1.0.0/src/linch/hooks/contexts.py +166 -0
  133. linch-1.0.0/src/linch/hooks/dispatcher.py +212 -0
  134. linch-1.0.0/src/linch/hooks/memory.py +117 -0
  135. linch-1.0.0/src/linch/hooks/types.py +101 -0
  136. linch-1.0.0/src/linch/loop/__init__.py +30 -0
  137. linch-1.0.0/src/linch/loop/checkpoint.py +196 -0
  138. linch-1.0.0/src/linch/loop/dispatch.py +225 -0
  139. linch-1.0.0/src/linch/loop/finalize.py +433 -0
  140. linch-1.0.0/src/linch/loop/request.py +243 -0
  141. linch-1.0.0/src/linch/loop/runner.py +1412 -0
  142. linch-1.0.0/src/linch/loop/streaming.py +292 -0
  143. linch-1.0.0/src/linch/loop/terminals.py +369 -0
  144. linch-1.0.0/src/linch/loop_guard/__init__.py +17 -0
  145. linch-1.0.0/src/linch/loop_guard/guard.py +164 -0
  146. linch-1.0.0/src/linch/mcp/__init__.py +51 -0
  147. linch-1.0.0/src/linch/mcp/client.py +163 -0
  148. linch-1.0.0/src/linch/mcp/config.py +29 -0
  149. linch-1.0.0/src/linch/mcp/naming.py +13 -0
  150. linch-1.0.0/src/linch/mcp/permission_bridge.py +32 -0
  151. linch-1.0.0/src/linch/mcp/result.py +64 -0
  152. linch-1.0.0/src/linch/mcp/tool.py +91 -0
  153. linch-1.0.0/src/linch/memory/__init__.py +28 -0
  154. linch-1.0.0/src/linch/memory/builder.py +135 -0
  155. linch-1.0.0/src/linch/memory/keyword.py +94 -0
  156. linch-1.0.0/src/linch/memory/lifecycle.py +93 -0
  157. linch-1.0.0/src/linch/memory/postgres.py +188 -0
  158. linch-1.0.0/src/linch/memory/sqlite.py +169 -0
  159. linch-1.0.0/src/linch/memory/store.py +40 -0
  160. linch-1.0.0/src/linch/memory/tiered.py +183 -0
  161. linch-1.0.0/src/linch/memory/tools.py +188 -0
  162. linch-1.0.0/src/linch/memory/types.py +25 -0
  163. linch-1.0.0/src/linch/middleware.py +160 -0
  164. linch-1.0.0/src/linch/observability/__init__.py +32 -0
  165. linch-1.0.0/src/linch/observability/dispatcher.py +67 -0
  166. linch-1.0.0/src/linch/observability/otel.py +222 -0
  167. linch-1.0.0/src/linch/observability/protocol.py +155 -0
  168. linch-1.0.0/src/linch/observability/reference.py +243 -0
  169. linch-1.0.0/src/linch/openai_responses.py +357 -0
  170. linch-1.0.0/src/linch/permissions/__init__.py +25 -0
  171. linch-1.0.0/src/linch/permissions/engine.py +295 -0
  172. linch-1.0.0/src/linch/permissions/keys.py +39 -0
  173. linch-1.0.0/src/linch/permissions/rules.py +321 -0
  174. linch-1.0.0/src/linch/permissions/ruleset.py +45 -0
  175. linch-1.0.0/src/linch/pricing.py +95 -0
  176. linch-1.0.0/src/linch/providers/__init__.py +47 -0
  177. linch-1.0.0/src/linch/providers/anthropic.py +467 -0
  178. linch-1.0.0/src/linch/providers/base.py +87 -0
  179. linch-1.0.0/src/linch/providers/catalog.py +137 -0
  180. linch-1.0.0/src/linch/providers/gemini.py +296 -0
  181. linch-1.0.0/src/linch/providers/llamacpp.py +178 -0
  182. linch-1.0.0/src/linch/providers/openai_chat.py +317 -0
  183. linch-1.0.0/src/linch/providers/openai_responses.py +54 -0
  184. linch-1.0.0/src/linch/providers/retry.py +57 -0
  185. linch-1.0.0/src/linch/providers/sglang.py +76 -0
  186. linch-1.0.0/src/linch/providers/vllm.py +59 -0
  187. linch-1.0.0/src/linch/reports.py +400 -0
  188. linch-1.0.0/src/linch/run_store.py +520 -0
  189. linch-1.0.0/src/linch/scheduler.py +1112 -0
  190. linch-1.0.0/src/linch/session.py +260 -0
  191. linch-1.0.0/src/linch/sessions/__init__.py +16 -0
  192. linch-1.0.0/src/linch/sessions/memory.py +200 -0
  193. linch-1.0.0/src/linch/sessions/postgres.py +634 -0
  194. linch-1.0.0/src/linch/sessions/sqlite.py +554 -0
  195. linch-1.0.0/src/linch/sessions/store.py +74 -0
  196. linch-1.0.0/src/linch/sessions/tasks.py +44 -0
  197. linch-1.0.0/src/linch/skills/__init__.py +23 -0
  198. linch-1.0.0/src/linch/skills/builtins.py +62 -0
  199. linch-1.0.0/src/linch/skills/listing.py +65 -0
  200. linch-1.0.0/src/linch/skills/loader.py +222 -0
  201. linch-1.0.0/src/linch/skills/overlay.py +7 -0
  202. linch-1.0.0/src/linch/skills/shell_split.py +49 -0
  203. linch-1.0.0/src/linch/skills/substitute.py +25 -0
  204. linch-1.0.0/src/linch/skills/system_reminder.py +5 -0
  205. linch-1.0.0/src/linch/skills/types.py +31 -0
  206. linch-1.0.0/src/linch/storage/__init__.py +3 -0
  207. linch-1.0.0/src/linch/storage/_executor.py +133 -0
  208. linch-1.0.0/src/linch/storage/_pg.py +38 -0
  209. linch-1.0.0/src/linch/subagents/__init__.py +34 -0
  210. linch-1.0.0/src/linch/subagents/builtins.py +57 -0
  211. linch-1.0.0/src/linch/subagents/default_agent.py +24 -0
  212. linch-1.0.0/src/linch/subagents/generator.py +377 -0
  213. linch-1.0.0/src/linch/subagents/loader.py +181 -0
  214. linch-1.0.0/src/linch/subagents/registry.py +46 -0
  215. linch-1.0.0/src/linch/subagents/runner.py +399 -0
  216. linch-1.0.0/src/linch/subagents/types.py +38 -0
  217. linch-1.0.0/src/linch/subagents/workers.py +26 -0
  218. linch-1.0.0/src/linch/tools/__init__.py +47 -0
  219. linch-1.0.0/src/linch/tools/_worker_utils.py +18 -0
  220. linch-1.0.0/src/linch/tools/ask_user.py +237 -0
  221. linch-1.0.0/src/linch/tools/base.py +97 -0
  222. linch-1.0.0/src/linch/tools/builtin.py +833 -0
  223. linch-1.0.0/src/linch/tools/execution.py +294 -0
  224. linch-1.0.0/src/linch/tools/file_tracker.py +27 -0
  225. linch-1.0.0/src/linch/tools/function.py +239 -0
  226. linch-1.0.0/src/linch/tools/isolation.py +67 -0
  227. linch-1.0.0/src/linch/tools/registry.py +176 -0
  228. linch-1.0.0/src/linch/tools/skill.py +126 -0
  229. linch-1.0.0/src/linch/tools/subagent.py +320 -0
  230. linch-1.0.0/src/linch/tools/subagent_continue.py +152 -0
  231. linch-1.0.0/src/linch/tools/subagent_stop.py +103 -0
  232. linch-1.0.0/src/linch/tools/tasks.py +222 -0
  233. linch-1.0.0/src/linch/types.py +275 -0
  234. linch-1.0.0/src/linch/verification.py +133 -0
  235. linch-1.0.0/src/linch/workflow/__init__.py +29 -0
  236. linch-1.0.0/src/linch/workflow/context.py +199 -0
  237. linch-1.0.0/src/linch/workflow/engine.py +87 -0
  238. linch-1.0.0/src/linch/workflow/journal.py +66 -0
  239. linch-1.0.0/tests/context/test_context_builder.py +307 -0
  240. linch-1.0.0/tests/context/test_system_blocks.py +275 -0
  241. linch-1.0.0/tests/filesystem/test_backends.py +91 -0
  242. linch-1.0.0/tests/filesystem/test_offload.py +166 -0
  243. linch-1.0.0/tests/filesystem/test_tools.py +85 -0
  244. linch-1.0.0/tests/integration/test_live_api.py +508 -0
  245. linch-1.0.0/tests/integration/test_live_deepseek.py +353 -0
  246. linch-1.0.0/tests/loop/test_agent_loop.py +65 -0
  247. linch-1.0.0/tests/loop/test_alignment.py +268 -0
  248. linch-1.0.0/tests/loop/test_compaction_ladder.py +449 -0
  249. linch-1.0.0/tests/loop/test_compaction_provider_agnostic.py +202 -0
  250. linch-1.0.0/tests/loop/test_compaction_read_tracker.py +70 -0
  251. linch-1.0.0/tests/loop/test_compaction_response_chaining.py +96 -0
  252. linch-1.0.0/tests/loop/test_hardening.py +529 -0
  253. linch-1.0.0/tests/loop/test_loop_guard.py +478 -0
  254. linch-1.0.0/tests/loop/test_model_fallback.py +142 -0
  255. linch-1.0.0/tests/loop/test_run_resume.py +1028 -0
  256. linch-1.0.0/tests/loop/test_tool_failure_recovery.py +279 -0
  257. linch-1.0.0/tests/loop/test_verification.py +602 -0
  258. linch-1.0.0/tests/mailbox/__init__.py +0 -0
  259. linch-1.0.0/tests/mailbox/test_mailbox.py +132 -0
  260. linch-1.0.0/tests/mailbox/test_mailbox_tool.py +212 -0
  261. linch-1.0.0/tests/mcp/test_client.py +213 -0
  262. linch-1.0.0/tests/mcp/test_permission_bridge.py +170 -0
  263. linch-1.0.0/tests/memory/test_memory_lifecycle.py +203 -0
  264. linch-1.0.0/tests/observability/test_observability.py +682 -0
  265. linch-1.0.0/tests/permissions/test_rules.py +75 -0
  266. linch-1.0.0/tests/permissions/test_ruleset.py +94 -0
  267. linch-1.0.0/tests/providers/test_anthropic_provider.py +813 -0
  268. linch-1.0.0/tests/providers/test_gemini_provider.py +411 -0
  269. linch-1.0.0/tests/providers/test_llamacpp_provider.py +329 -0
  270. linch-1.0.0/tests/providers/test_openai_chat_stream.py +222 -0
  271. linch-1.0.0/tests/providers/test_openai_responses.py +128 -0
  272. linch-1.0.0/tests/providers/test_parity_surface.py +49 -0
  273. linch-1.0.0/tests/providers/test_provider_capabilities.py +318 -0
  274. linch-1.0.0/tests/providers/test_provider_catalog.py +104 -0
  275. linch-1.0.0/tests/providers/test_retry.py +86 -0
  276. linch-1.0.0/tests/providers/test_sglang_provider.py +159 -0
  277. linch-1.0.0/tests/providers/test_structured_output.py +478 -0
  278. linch-1.0.0/tests/providers/test_vllm_provider.py +164 -0
  279. linch-1.0.0/tests/scheduling/test_scheduling.py +232 -0
  280. linch-1.0.0/tests/skills/test_builtins.py +97 -0
  281. linch-1.0.0/tests/skills/test_substitute.py +34 -0
  282. linch-1.0.0/tests/storage/test_memory_primitives.py +267 -0
  283. linch-1.0.0/tests/storage/test_postgres.py +164 -0
  284. linch-1.0.0/tests/storage/test_run_store.py +107 -0
  285. linch-1.0.0/tests/storage/test_serialization_versioning.py +82 -0
  286. linch-1.0.0/tests/storage/test_sessions.py +27 -0
  287. linch-1.0.0/tests/storage/test_sqlite_concurrency.py +201 -0
  288. linch-1.0.0/tests/storage/test_task_coordination.py +137 -0
  289. linch-1.0.0/tests/storage/test_tiered_memory.py +697 -0
  290. linch-1.0.0/tests/subagents/test_budget_shared.py +130 -0
  291. linch-1.0.0/tests/subagents/test_fork_mode.py +118 -0
  292. linch-1.0.0/tests/subagents/test_generator.py +140 -0
  293. linch-1.0.0/tests/subagents/test_permission_bubbling.py +89 -0
  294. linch-1.0.0/tests/subagents/test_registry.py +57 -0
  295. linch-1.0.0/tests/subagents/test_runner_signals.py +63 -0
  296. linch-1.0.0/tests/subagents/test_subagent_isolation.py +201 -0
  297. linch-1.0.0/tests/test_abort.py +58 -0
  298. linch-1.0.0/tests/test_backpressure.py +75 -0
  299. linch-1.0.0/tests/test_budget.py +251 -0
  300. linch-1.0.0/tests/test_deep_agent.py +681 -0
  301. linch-1.0.0/tests/test_evals.py +529 -0
  302. linch-1.0.0/tests/test_example_coordination.py +116 -0
  303. linch-1.0.0/tests/test_example_interaction.py +181 -0
  304. linch-1.0.0/tests/test_example_ralph_loop.py +75 -0
  305. linch-1.0.0/tests/test_example_research_desk.py +78 -0
  306. linch-1.0.0/tests/test_hooks.py +689 -0
  307. linch-1.0.0/tests/test_input_aware_concurrency.py +79 -0
  308. linch-1.0.0/tests/test_multitenancy.py +118 -0
  309. linch-1.0.0/tests/test_pricing.py +279 -0
  310. linch-1.0.0/tests/test_public_api.py +43 -0
  311. linch-1.0.0/tests/test_reports.py +230 -0
  312. linch-1.0.0/tests/tools/test_ask_user.py +186 -0
  313. linch-1.0.0/tests/tools/test_background_tool.py +156 -0
  314. linch-1.0.0/tests/tools/test_builtin_search.py +214 -0
  315. linch-1.0.0/tests/tools/test_execution_backend.py +578 -0
  316. linch-1.0.0/tests/tools/test_function_tools.py +185 -0
  317. linch-1.0.0/tests/tools/test_isolation.py +97 -0
  318. linch-1.0.0/tests/tools/test_middleware.py +259 -0
  319. linch-1.0.0/tests/tools/test_registry_ext.py +165 -0
  320. linch-1.0.0/tests/tools/test_scheduler_v2.py +365 -0
  321. linch-1.0.0/tests/tools/test_subagent_tool.py +428 -0
  322. linch-1.0.0/tests/tools/test_tool_deps.py +149 -0
  323. linch-1.0.0/tests/tools/test_tool_reliability.py +519 -0
  324. linch-1.0.0/tests/tools/test_tool_result_v2.py +37 -0
  325. linch-1.0.0/tests/workflow/test_context.py +86 -0
  326. linch-1.0.0/tests/workflow/test_engine.py +245 -0
  327. linch-1.0.0/tests/workflow/test_journal.py +98 -0
  328. linch-1.0.0/uv.lock +1463 -0
linch-1.0.0/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .pyright/
6
+ .venv/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ .agent_kit/
11
+ .env
12
+ .claude/
13
+ .codex/
14
+ .linch/
15
+
16
+ # Vendored reference material (has its own git repo)
17
+ learn-claude-code/
linch-1.0.0/AGENTS.md ADDED
@@ -0,0 +1,39 @@
1
+ # Repository Guidelines
2
+
3
+ ## Project Structure & Module Organization
4
+
5
+ `linch` is a Python SDK packaged from `src/linch`. Core runtime code lives in modules such as `agent.py`, `loop.py`, `scheduler.py`, `types.py`, and feature packages like `providers/`, `tools/`, `filesystem/`, `memory/`, `sessions/`, `skills/`, `subagents/`, and `observability/`. Tests mirror these areas under `tests/`, with focused subdirectories such as `tests/providers/`, `tests/tools/`, `tests/storage/`, and `tests/integration/`. Runnable examples are grouped by topic under `examples/`, docs live in `docs/`, and utility scripts live in `scripts/`.
6
+
7
+ ## Build, Test, and Development Commands
8
+
9
+ Install for local development:
10
+
11
+ ```bash
12
+ pip install -e '.[dev,mcp,anthropic,gemini]'
13
+ ```
14
+
15
+ Run the main checks before opening a PR:
16
+
17
+ ```bash
18
+ pytest
19
+ ruff check . && ruff format --check .
20
+ pyright
21
+ ```
22
+
23
+ Use targeted tests while iterating, for example `pytest tests/tools/test_function_tools.py` or `pytest -k context`. Auto-fix style issues with `ruff check --fix . && ruff format .`. Live API tests require relevant credentials, such as `OPENAI_API_KEY`; unit tests must not depend on live services.
24
+
25
+ ## Coding Style & Naming Conventions
26
+
27
+ Target Python 3.10+. Ruff enforces imports and lint rules (`E`, `F`, `I`, `UP`, `B`) with a 100-character line length. Use 4-space indentation, type annotations for public surfaces, and `slots=True` on new dataclasses following existing primitives. Keep runtime/provider paths async; avoid blocking I/O in `loop.py`, `scheduler.py`, `compaction.py`, and provider modules. Tool classes are duck-typed; do not introduce base-class inheritance where protocols are expected.
28
+
29
+ ## Testing Guidelines
30
+
31
+ Tests use `pytest` with `pytest-asyncio` in auto mode. Name files `test_*.py` and keep assertions focused on one behavior. Prefer fake providers and `InMemorySessionStore` for loop tests. Live provider coverage belongs in integration tests and must skip cleanly without credentials. Some hardening tests reload `linch`; in affected tests, import `Agent`, `Session`, and related content types inside test functions rather than at module scope.
32
+
33
+ ## Commit & Pull Request Guidelines
34
+
35
+ Recent history uses short imperative commit subjects, sometimes with a conventional prefix such as `chore:`. Examples: `Harden SDK from whole-codebase review` and `chore: exclude .claude and .codex from version control`. PRs should include a concise description, linked issue or motivation, behavioral notes, and the checks run. Add screenshots or logs only when they clarify UI, CLI, or observability changes.
36
+
37
+ ## Security & Configuration Tips
38
+
39
+ Never commit `.env`, API keys, local caches, or generated private state. Keep provider-specific wire formats inside `src/linch/providers/`; shared loop code should consume normalized provider events only.
linch-1.0.0/CLAUDE.md ADDED
@@ -0,0 +1,198 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ # Install for development (all extras)
9
+ pip install -e '.[dev,mcp,anthropic]'
10
+
11
+ # Run all tests
12
+ pytest
13
+
14
+ # Run a single test file
15
+ pytest tests/test_agent_loop.py
16
+
17
+ # Run a single test by name
18
+ pytest tests/test_agent_loop.py::test_function_name
19
+
20
+ # Lint
21
+ ruff check .
22
+
23
+ # Format check
24
+ ruff format --check .
25
+
26
+ # Auto-fix lint/format
27
+ ruff check --fix . && ruff format .
28
+
29
+ # Type check
30
+ pyright
31
+ ```
32
+
33
+ ## Architecture
34
+
35
+ Linch is a Python SDK for embedding a software engineering agent loop in applications. It is async-first, event-driven, and provider-agnostic.
36
+
37
+ ### Core flow
38
+
39
+ ```
40
+ Agent (config) → Session (state) → run_loop() → Events → caller
41
+ ```
42
+
43
+ 1. **Agent** (`agent.py`) — holds immutable config: model, provider, tools, permissions, session_store, system prompt, compaction strategy.
44
+ 2. **Session** (`session.py`) — per-conversation state: `provider_view` (trimmed for LLM context), `full_history` (complete record), `workers` (live `WorkerHandle` objects keyed by worker_id), `pending_notifications` (background worker `<task-notification>` Messages drained at top of each turn). Call `session.run(prompt)` to get an `AsyncIterator[Event]`.
45
+ 3. **run_loop / stream_turn** (`loop/`) — the main agent loop, split by responsibility: `loop/runner.py` (`run_loop`, `resume_loop`, the turn loop), `loop/streaming.py` (`stream_turn` + ContextLengthError recovery), `loop/request.py` (user message / context / `ProviderRequest` assembly), `loop/terminals.py` (terminal event tails + closed-loop gates), `loop/checkpoint.py` (event persistence + checkpoint serialization), `loop/dispatch.py` (per-lifecycle `HookDispatcher.dispatch` wrappers, bound into the runner via `functools.partial`), `loop/finalize.py` (terminal-answer finalization — the final-tool/final-text gate+hook+tail logic, driven through a `FinalizeCtx`/`TerminalOutcome` pair). Each turn: build user message → run `ContextBuilder` → call `provider.stream()` → collect text/tool-use blocks → check permissions → execute tools (via `scheduler.py`) → emit events → repeat if more tool calls; stop on text-only response. The public surface is re-exported from `loop/__init__.py`, so `from linch.loop import ...` is unchanged.
46
+ 4. **Events** (`events.py`) — all communication from the loop is through events: `UserEvent`, `ContextBuildEvent`, `AssistantEvent`, `ToolCallStartEvent`, `ToolCallEndEvent`, `PermissionRequestEvent`, `UsageEvent`, `ResultEvent`, `ErrorEvent`, `CompactionEvent`, `LoopGuardEvent`, skill/subagent events, and `BackgroundWorkerEvent` (`worker_id`, `status`, `display_name`; emitted when a background worker completes).
47
+
48
+ ### Providers (`providers/`)
49
+
50
+ Abstract interface (`BaseProvider`) with three methods: `context_window(model)`, `stream(req) → AsyncIterator[StreamEvent]`, and `capabilities(model) → ProviderCapabilities`. Implementations:
51
+ - `OpenAIChatCompletionsProvider` — standard OpenAI Chat API; also drives OpenAI-compatible endpoints (e.g. DeepSeek) via `base_url`
52
+ - `OpenAIResponsesProvider` — OpenAI o1/o3 Responses API (with reasoning tokens)
53
+ - `AnthropicProvider` — Anthropic Claude; full streaming with tool use, thinking blocks, and prompt caching (`prompt_cache=True`, `structured_output=False`)
54
+ - `GeminiProvider` — Google Gemini (optional `[gemini]` extra); cross-chunk tool-call dedup
55
+ - `LlamaCppProvider` — local llama.cpp server; inherits the Chat-Completions streaming path
56
+ - `with_retry` — wraps any provider callable with exponential-backoff retry
57
+
58
+ `providers/catalog.py` exposes static model metadata for the built-in direct providers: `list_provider_models(provider_id=None)`, `get_provider_model_info(model, provider_id=None)`, and the `ProviderModelInfo` record (`context_window`, `capabilities`, `pricing`). It intentionally excludes OpenAI-compatible/local models whose model lists depend on external config.
59
+
60
+ `ProviderCapabilities` declares per-provider feature support (`parallel_tool_calls`, `structured_output`, `tool_choice`, `prompt_cache`, `context_window`). `apply_provider_capabilities(req, caps)` in `loop/request.py` downgrades a `ProviderRequest` before each call — clears `cache_prompt`/`cache_ttl` for non-caching providers, strips `output_schema` when native structured output is unsupported. Duck-typed test providers that don't implement `capabilities()` are safely skipped via `hasattr` guard.
61
+
62
+ ### Loop Guard (`loop_guard/`)
63
+
64
+ `LoopGuard` detects obvious agentic loops without extra LLM calls:
65
+ - **Repeated identical tool calls** — same name+input called ≥ `max_identical_tool_calls` times (default 3).
66
+ - **Consecutive failure streaks** — every tool in a batch fails for ≥ `max_consecutive_failures` turns (default 3).
67
+
68
+ The guard is **on by default** at `Agent()` construction. Disable with `Agent(loop_guard=None)`. When tripped it emits a `LoopGuardEvent` and either stops immediately (default) or runs one final tools-disabled turn (`force_final_answer=True`). Max-turns exhaustion also emits a `LoopGuardEvent(reason="max_turns")` for clarity alongside the existing `ErrorEvent(TurnLimitError)`.
69
+
70
+ ### Verification (`verification.py`)
71
+
72
+ Opt-in closed-loop gates evaluated when the loop is about to return a final answer (text-only response). All default-off; with defaults the loop is byte-identical to before.
73
+
74
+ - **Structured-output repair**: `Agent(structured_output_retries=N)` — when the final text fails `output_schema` parsing/validation, the error is injected back as a system-reminder user message and the loop runs another turn (up to N times) instead of terminating with `structured_error`. Default `0` = legacy single-attempt.
75
+ - **Verifier protocol**: `Agent(verifiers=[...], max_verification_retries=2)` — a `Verifier` is duck-typed (`name` + `verify(ctx) -> Verdict`, sync or async). `Verdict.action` is `"pass"` (accept), `"retry"` (inject `Verdict.feedback`, run another turn), or `"stop"` (fail the run with an error `ResultEvent`). First non-pass verdict wins; a verifier that raises is treated as passing (mirrors the observer contract). Each gate action emits a `VerificationEvent` (`verifier`, `action` ∈ retry/stop/exhausted, `feedback`, `attempt`). When retries are exhausted, the answer is accepted as-is with an `action="exhausted"` event.
76
+ - **`ScorerVerifier`**: lifts an output-based evals scorer (`text_contains`, `schema_valid`) into a live verifier (`ScorerVerifier(scorer, feedback=..., on_fail="retry"|"stop")`). Event-based scorers get no events in a live run — write a custom verifier for those.
77
+ - **`RunOptions.stop_when`**: a `(session) -> bool` predicate checked before each provider turn; `True` ends the run gracefully (success result, final text = last assistant text). A raising predicate counts as `False`.
78
+
79
+ Gates are skipped on a loop-guard `force_final` turn so a guard-tripped run is never bounced back into the loop. Verification retries count toward `max_turns` and the run budget, so a strict verifier cannot loop unboundedly. Retry counters are per-run and not checkpointed — a resumed run starts fresh. The schema-repair gate runs before custom verifiers; gate evaluation lives in `_evaluate_terminal_gates` (`loop/terminals.py`), wired at the same chokepoint as the loop guard.
80
+
81
+ ### Tools (`tools/`)
82
+
83
+ Tools are **protocols** (duck-typed), not subclasses. Each tool has: `name`, `description`, `input_schema`, `scope`, `parallel_safe`, and methods `validate()`, `execute()`, `summarize()`. V2 tools may also expose `parallel`, `resources(input)`, `tags`, `capabilities`, and `cost_hint`. Built-ins: `Read`, `Write`, `Edit`, `Bash`, `Glob`, `Grep`. The `ToolRegistry` holds available tools; `default_tools()` returns the standard set.
84
+
85
+ **Function tools (`tools/function.py`)**: the `@tool` decorator wraps a plain (sync or async) Python function into a protocol-compatible `FunctionTool` — it infers a minimal JSON schema from the signature/annotations, injects `ToolContext` when the function declares a `ctx` parameter, and accepts `scope`, `parallel`, `tags`, `summary`, `resources`, `retryable`, and `execution_timeout_ms` overrides. No base-class inheritance; the result drops straight into a `ToolRegistry`.
86
+
87
+ **Execution backends (`tools/execution.py`)**: `ExecutionBackend` is a duck-typed protocol (`run(command, *, cwd, timeout_s, signal) → ExecResult`). `LocalBackend` (default) runs a subprocess shell with process-group kill and abort-aware communicate; `DockerBackend` runs inside `docker run --rm` (guarded by `shutil.which`, no Docker SDK), with configurable network/mount/read-only/user. Inject via `Agent(execution_backend=...)`, which replaces only the `Bash` tool (never adds one to a registry that excludes it). Both backends route timeout/abort through `_communicate_with_timeout_and_abort` so `session.abort()` interrupts in-flight commands.
88
+
89
+ `ToolContext` is passed to every `execute()` call and provides: `cwd`, `session_id`, `run_id`, `session_store`, `signal` (abort), `file_read_tracker`, `filesystem` (virtual `FileBackend` when the filesystem subsystem is enabled).
90
+
91
+ Read tools with `parallel=True` may run concurrently. Write and exec tools serialize by default. Optional `ResourceAccess` declarations prevent overlapping read/write conflicts on the same resource. Concurrency limit: `Agent(max_tool_concurrency=...)` or `AGENTKIT_MAX_TOOL_CONCURRENCY` env var (default: CPU count).
92
+
93
+ **Timeouts**: `Agent(tool_timeout_ms=N)` sets an agent-wide execution deadline (env `AGENTKIT_TOOL_TIMEOUT_MS`). A tool may override with a class-level `execution_timeout_ms` attribute; `0` opts out. Default `None` = no timeout (zero-overhead, backward compatible). A timed-out tool returns `is_error=True` with an actionable message; the run continues. Implemented via `asyncio.wait_for` catching `asyncio.TimeoutError` (Python 3.10 safe). `ToolTimeoutError` (`kind="tool_timeout"`, `retryable=True`) is the typed exception for observer/policy use.
94
+
95
+ **Retry**: `Agent(tool_retry=RetryOptions(max_attempts=..., base_delay_ms=...))` enables opt-in exponential-backoff retry. Gated by scope: read-scope tools retry any exception (idempotent); write/exec tools only retry when the tool sets `retryable = True`. `AbortError` is never retried.
96
+
97
+ `ToolResult` supports plain `content` plus `summary`, `metadata`, `citations`, `attachments`, `duration_ms`, and `truncated`. The model receives the compact `content`; host apps can use the richer fields for RAG provenance and UI rendering.
98
+
99
+ ### Context (`context/`)
100
+
101
+ Use `ContextBuilder.build(turn) -> ContextBuildResult` for RAG, memory recall, ephemeral system blocks, budget trimming, and request-scoped tool selection. Builder output is appended only to the provider request and does not mutate `session.provider_view`. The legacy `context_hooks.py` API has been removed.
102
+
103
+ ### Memory (`memory/`)
104
+
105
+ `MemoryStore` is a protocol for app-owned memory backends. Core includes `MemoryItem`, `MemorySearchResult`, `InMemoryKeywordMemoryStore`, `SqliteMemoryStore`, `MemoryContextBuilder`, `MemorySearchTool`, and `MemoryUpsertTool`. Do not add vector DB or embedding dependencies to core; adapters should implement the protocol or live in examples/recipes.
106
+
107
+ `TieredMemoryStore` is itself a `MemoryStore` — a deterministic router/merge over three sub-stores (`working`/`episodic`/`semantic`), routing writes by `item.metadata["tier"]` (default `working`) and fanning `search()` across all tiers, then merging by the canonical `(score, item.id)` key and slicing to the global `limit`. Optional `tier_limits` is a **hard per-tier cap** applied before the merge; leave it unset for a pure global top-N. `MemoryContextBuilder(group_by_tier=True)` renders results under tier subheadings (default output is byte-identical). An optional `PostgresMemoryStore` adapter (`[postgres]` extra) mirrors `SqliteMemoryStore`'s full-scan keyword search.
108
+
109
+ ### Virtual Filesystem (`filesystem/`)
110
+
111
+ `FileBackend` is a duck-typed protocol for a virtual, session-scoped filesystem that is separate from the real `cwd` on disk. Four implementations ship: `StateFileBackend` (in-memory, per-session default), `DiskFileBackend` (real files sandboxed under a root, default `.linch/offload`), `SqliteFileBackend` (persistent across sessions), and `CompositeFileBackend` (routes paths by prefix, e.g. `/memories/` → `SqliteFileBackend`).
112
+
113
+ **Auto-offload**: `Agent(result_offload=OffloadConfig())` enables automatic offloading of tool results that exceed `threshold_tokens` (default 20,000). The scheduler calls `maybe_offload()` at the single result chokepoint in `_execute_one` (`scheduler.py`). The full payload is written to the backend; `ToolResult.content` is replaced with a preview + path hint before the `ToolResultBlock` enters `provider_view`. The full `ToolResult` still travels on `ToolCallEndEvent.tool_result` for observers. `maybe_offload` never raises — a backend write failure silently returns the original result.
114
+
115
+ **Four tools** register automatically when a backend is configured (`Agent(filesystem=...)` or `Agent(result_offload=...)`): `ls`, `read_file` (supports `offset`/`limit` line windowing), `write_file`, `edit_file`. These are in `OffloadConfig.skip_tools` by default so reading back a large file cannot trigger a recursive re-offload. A filesystem-aware system-prompt block is also added automatically.
116
+
117
+ `FeatureFlags(filesystem=False)` disables the subsystem even when a backend is passed.
118
+
119
+ ### Permissions (`permissions/`)
120
+
121
+ `PermissionEngine` evaluates each tool call against configured rules before execution. Modes: `"default"` (prompt user), `"acceptEdits"` (auto-allow file edits), `"skip-dangerous"` (allow all). Rules: `ToolRule`, `BashRule`, `PathRule`. `BashRule` matches via fnmatch-glob and token-prefix (no substring matching); `PathRule` translates globs to anchored regexes where `*`/`?` do **not** cross `/` and `**` does (`permissions/rules.py`). When a tool call is not auto-approved, a `PermissionRequestEvent` is emitted and the loop pauses until the caller responds.
122
+
123
+ **Durable HITL**: resolved decisions are persisted into the run checkpoint (`RunCheckpoint.permission_decisions`, keyed by `permission_decision_key(tool_name, input)` in `permissions/keys.py` — stable across provider calls, unlike `tool_use_id`). On resume the scheduler replays a stored allow/deny instead of re-invoking the callback (Seam A); only explicit allow/deny are persisted (Seam B) — abort/callback-failure denials are not. A corrupt stored decision falls through to a fresh prompt.
124
+
125
+ ### Sessions & Storage (`sessions/`)
126
+
127
+ `SessionStore` is a protocol. Implementations: `InMemorySessionStore` (ephemeral) and `SqliteSessionStore` (persistent). The store also handles task management (`Task`, `TaskPatch`, status tracking) used by the `TaskCreate/List/Get/Update` tools.
128
+
129
+ `run_store.py` provides `SqliteRunStore` and `RunCheckpoint` for durable run checkpointing, enabling deep-agent runs to resume across process restarts. The persisted wire format is versioned: `SCHEMA_VERSION` (exported as `linch.RUN_SCHEMA_VERSION`) stamps every serialized checkpoint (`checkpoint_to_dict` → `"schema_version"`); `checkpoint_from_dict` reads field-by-field with defaults so a checkpoint from a newer binary round-trips its known fields, and `load_events` skips an event row it cannot decode (a future event type) instead of aborting the resume. See `docs/versioning.md` for the semver contract.
130
+
131
+ ### Deep agent preset (`deep_agent/`)
132
+
133
+ `create_deep_agent(*, model, durable, coordinator, cwd, ...)` is a factory in `deep_agent/factory.py` that assembles a fully-configured `Agent` with task tools, a built-in subagent roster (researcher, planner, implementer defined in `deep_agent/subagents.py` as `DEEP_AGENT_SUBAGENTS`), durable SQLite stores, a `/memories` filesystem partition, and a deepened system prompt.
134
+
135
+ - **`coordinator=True`** — strips heavy tools (`Edit`, `Write`, `Bash`, `Grep`, `Glob`, `Read`) from the parent agent, injects `COORDINATOR_SYSTEM_PROMPT` (from `deep_agent/prompts.py`), and registers `TaskStopTool`. Workers still receive full tool access.
136
+ - **`durable=True`** — sets `SqliteSessionStore`, `SqliteRunStore`, and `CompositeFileBackend` with a persistent `/memories` partition backed by `SqliteFileBackend`, enabling cross-process resume.
137
+ - `DEEP_AGENT_SYSTEM_PROMPT` and `COORDINATOR_SYSTEM_PROMPT` live in `deep_agent/prompts.py`.
138
+ - `DEEP_AGENT_SUBAGENTS` in `deep_agent/subagents.py` defines researcher, planner, and implementer subagent definitions used by default.
139
+
140
+ ### MCP Integration (`mcp/`)
141
+
142
+ `connect_mcp_servers()` connects to external MCP servers (stdio or HTTP) and returns an `McpConnection` that exposes MCP tools as Linch tools. MCP tool names are normalized via `mcp/naming.py`.
143
+
144
+ ### Skills & Subagents (`skills/`, `subagents/`)
145
+
146
+ **Skills** are slash-commands defined as `SKILL.md` files (YAML frontmatter + markdown body) loaded from `.linch/skills/*/SKILL.md`. The skill system supports argument substitution and system-reminder injection.
147
+
148
+ **Subagents** are specialized agent roles defined in `.linch/agents.yaml`. The subagent registry resolves agent definitions; `runner.py` executes them with their own tool overlays and prompts.
149
+
150
+ **Worker lifecycle and fork/continue**: `SubagentTool` passes `retain=True` in `RunSubagentArgs` so child sessions remain alive in `agent._sessions` after the initial run. `_drive_child` is a shared helper used by both `run_subagent` and `continue_subagent` to drive a child session. `SubagentContinueTool` (schema: `{to, message}`) re-engages a retained worker by worker_id or display_name, resuming with its full prior `provider_view`. `TaskStopTool` (schema: `{task_id, reason}`) cancels a running background worker task; the handle remains continuable. `WorkerHandle` (`subagents/workers.py`) is a dataclass tracking `worker_id`, `child_session_id`, `display_name`, `definition`, `status`, `task`, and `last_result_text` for each live worker. Parent session exposes `session.workers: dict[str, WorkerHandle]`.
151
+
152
+ **Background workers**: `SubagentTool` with `run_in_background=True` spawns `asyncio.create_task` and returns an ack immediately without blocking the turn. On completion the worker appends a `<task-notification>` XML `Message` to `session.pending_notifications`. The next `session.run()` call drains all pending notifications as `UserEvent`s at the top of the turn via `_drain_pending_notifications` in `loop/runner.py`. `session.abort()` cancels all running background worker tasks; `agent.close()` also cancels them and clears `_sessions`.
153
+
154
+ ### Compaction (`compaction.py`)
155
+
156
+ When the provider's context window approaches its limit, the compaction strategy summarizes old messages to free space. This is transparent to the caller; a `CompactionEvent` is emitted.
157
+
158
+ **Compaction ladder (opt-in)**: `Agent(compaction_ladder=CompactionLadder())` adds LLM-free recovery rungs. Rung 1 — `micro_compact`: copy-on-write elision of `ToolResultBlock` contents older than `keep_recent_turns` (blocks are shared with `full_history`, so changed messages are rebuilt, never mutated; `tool_use_id` pairing preserved). Runs proactively in `maybe_compact` and reactively on `ContextLengthError` (once per turn). Rung 2 — forced compaction capped at `max_forced_compactions` per run (circuit breaker), then the error surfaces. With the default `compaction_ladder=None`, behavior is byte-identical to the legacy single-retry path (`_stream_turn_with_compaction_retry` vs `_stream_turn_with_ladder` in `loop/streaming.py`).
159
+
160
+ ### Budgets (`budget.py`)
161
+
162
+ `RunBudget(max_tokens=..., max_cost_usd=...)` caps spending for a run **and its whole subagent tree** — children inherit the parent's budget object by reference (`run_subagent` copies `parent_session.active_budget` into `child_session.inherited_budget`). Resolution in `run_loop`: `RunOptions.budget` > `inherited_budget` > `Agent(budget=...)`. The loop charges after every provider turn and checks before each one; exhaustion emits `BudgetEvent(kind="exceeded")` → `ErrorEvent(BudgetExceededError)` → `ResultEvent(subtype="error")` and stops gracefully. `BudgetEvent(kind="warning")` fires once per budget object at `warn_ratio` (default 0.9). Unknown-model turns charge $0 — only the token limit binds for unpriced models.
163
+
164
+ ### Workflows (`workflow/`)
165
+
166
+ `agent.run_workflow(fn, *, budget=None, run_id=None, max_concurrency=4, on_event=None)` runs a deterministic "fleet loop": *fn* is a plain async function receiving a `WorkflowContext` (`wf`) with `await wf.agent(prompt, name=..., label=..., tools=...) -> str` (runs a subagent via `run_subagent`; failure raises `WorkflowError`), `await wf.parallel(thunks)` (semaphore-capped, order-preserving), `await wf.pipeline(items, *stages)` (per-item chaining, no barrier), `await wf.phase(title)`, and `wf.budget` (the shared `RunBudget`). With `Agent(run_store=...)` + `run_id`, each `wf.agent` result is journaled as a persisted `WorkflowEvent(kind="agent_end")`; re-running the same `run_id` replays the unchanged prefix from `WorkflowJournal.from_stored_events` (`kind="agent_replayed"`, no provider call). Calls are keyed by `sha256(subagent_type, prompt)` + occurrence counter (`workflow/journal.py`). Workflow functions must be deterministic for resume to be correct.
167
+
168
+ ### Observability (`observability/`)
169
+
170
+ `RunObserver` is a vendor-neutral protocol with nine span-hook methods (`on_run_start/end`, `on_turn_start/end`, `on_provider_call_start/end`, `on_tool_start/end`) plus an `on_event` catch-all. Methods may be sync or async.
171
+
172
+ `ObserverDispatcher` fans out hook calls to a list of observers, awaits async results, and swallows exceptions — a faulty observer never crashes a run. Zero-overhead when no observers are attached.
173
+
174
+ Stdlib reference observers: `LoggingObserver` (one log line per span) and `SpanCollector` (in-memory span list for tests). `OpenTelemetryObserver` is the production integration point, behind the optional `[otel]` extra (lazily imported, `pip install 'linch[otel]'`). Langfuse, LangSmith, Honeycomb, and Datadog are all reached via the OTel adapter — no vendor-specific code in core.
175
+
176
+ Observers attach through the unified hooks layer, not a dedicated `observers=` parameter: wrap them in the adapter hook — `Agent(hooks=[RunTelemetryHook([...])])`. The same pattern bridges the other legacy protocols into the single `HookDispatcher` runtime: `ToolMiddlewareHook(middleware)`, `FinalAnswerVerifierHook(verifiers)`, `StopPredicateHook(predicate)`, and `ContextInjectionHook(builder)` (all in `hooks/adapters.py`). `normalize_hooks` does **not** auto-detect a raw observer/verifier/middleware — always wrap via the matching adapter. Passing one unwrapped is unsupported and misbehaves: a few method names overlap (e.g. `RunObserver.on_turn_start`/`on_provider_call_start` collide with the hook dispatch methods), so those fire with the wrong context type while the rest are ignored. The legacy `RunObserver`/`Verifier`/middleware protocols remain exported for implementing your own; the loop dispatches everything through hooks, and `RunTelemetryHook` drives the wrapped observers via `ObserverDispatcher` (the single fan-out implementation — the adapter does not re-roll its own).
177
+
178
+ ### Evals (`evals/`)
179
+
180
+ A lightweight, deterministic harness for grading agent behavior offline. `ScriptedProvider` (with `TextTurn`/`ToolUseTurn`) replays a fixed turn sequence without a live model. `run_eval(agent_factory, cases, scorers)` runs an agent over a list of `EvalCase`s and returns `EvalResult`/`CaseResult` with per-scorer pass/fail/None. Built-in scorers (`evals/scorers.py`) each return `True`/`False`/`None`: `text_contains`, `tool_called`, `schema_valid`, `cost_under`, plus long-run scorers `context_selected_tool`, `context_not_trimmed`, `context_metadata_contains`, `memory_recalled`, `recovery_succeeded`, and `run_completed`. Each case session is popped from `agent._sessions` and aborted in a `finally`, so a long eval run does not leak sessions.
181
+
182
+ ### Run reports (`reports.py`)
183
+
184
+ `build_run_report(events, run=None)` folds an event stream (live `Event`s or persisted `StoredRunEvent`s) into a `RunReport` dataclass: tool calls, permission requests, context builds, loop guards, errors, usage, final result, checkpoint, and a `long_run` summary (selected-tool counts, peak context tokens, memory tier/namespace/citation rollups) plus a flat `timeline`. `load_run_report(store, run_id)` reconstructs one from a `RunStore`. Pure read model — it never mutates session state.
185
+
186
+ ## Key design constraints
187
+
188
+ - All async — no blocking I/O anywhere in the core loop.
189
+ - `provider_view` vs `full_history` are kept separately; only `provider_view` is sent to the LLM. Compaction modifies `provider_view` only.
190
+ - Tool protocol is duck-typed — avoid inheriting from a base class when adding tools; implement the protocol attributes directly.
191
+ - The loop continues as long as the response contains tool calls; it stops when the model returns a text-only response (or hits a stop condition).
192
+ - No vendor observability stack in core — observability backends (Langfuse, LangSmith, etc.) are reached via the OpenTelemetry seam, never as direct dependencies.
193
+ - Tool timeouts default to `None` (off) and use `asyncio.wait_for` + `asyncio.TimeoutError` (not the 3.11+ unified builtin) to stay Python 3.10 compatible. Timeouts convert to `is_error=True` results; they never raise out of `_execute_one` so parallel-lane siblings are unaffected.
194
+ - Tool retry is side-effect gated: read-scope tools only, or tools that explicitly set `retryable = True`. Write/exec tools are never retried by default.
195
+ - Virtual filesystem is opt-in (`Agent(filesystem=...)` or `Agent(result_offload=...)`); zero overhead when unset. `DiskFileBackend` defaults to `.linch/offload` (gitignored). `maybe_offload` is a no-op on error results and filesystem-tool results. A backend write failure silently returns the original result — storage errors never crash a run.
196
+ - Background workers use `asyncio.create_task` detached from the current turn; `session.abort()` cancels them and `agent.close()` also cancels and clears `_sessions`. `_cancel_background_workers` is called in both `except AbortError` and `except Exception` branches so orphaned tasks never write into a dead session. Both `session.abort()` and `agent.close()` also cancel detached background-*tool* tasks (`session.background_tasks`), so closing an agent never orphans an in-flight background tool.
197
+ - Multi-tenant safe: no process-global mutable state — each `Agent` builds its own tool registry, sessions dict, permission engine, and extension state (the one module-level singleton, `default_compaction`, is immutable). N agents run concurrently in one process without cross-talk.
198
+ - Public API contract: the supported surface is exactly `linch.__all__` (import from the top-level package; submodule paths and underscore names are private). `tests/test_public_api.py` guards it; `docs/versioning.md` is the semver policy. The event stream (`session.run()`) is a plain async-generator chain — it applies natural backpressure (a slow consumer throttles the producer); see `docs/usage/events.md`.
linch-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 skawld
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.