linch 1.0.0__py3-none-any.whl

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 (154) hide show
  1. linch/__init__.py +487 -0
  2. linch/_blocking.py +71 -0
  3. linch/_http_errors.py +99 -0
  4. linch/_prompt_cache.py +120 -0
  5. linch/_version.py +26 -0
  6. linch/abort.py +77 -0
  7. linch/agent.py +1088 -0
  8. linch/budget.py +93 -0
  9. linch/compaction.py +521 -0
  10. linch/config.py +97 -0
  11. linch/context/__init__.py +21 -0
  12. linch/context/builder.py +216 -0
  13. linch/coordination/__init__.py +57 -0
  14. linch/coordination/mailbox/__init__.py +13 -0
  15. linch/coordination/mailbox/core.py +80 -0
  16. linch/coordination/mailbox/correlation.py +51 -0
  17. linch/coordination/scheduling/__init__.py +19 -0
  18. linch/coordination/scheduling/cron.py +105 -0
  19. linch/coordination/scheduling/loop.py +114 -0
  20. linch/coordination/scheduling/schedule.py +71 -0
  21. linch/coordination/scheduling/sqlite.py +142 -0
  22. linch/coordination/scheduling/store.py +61 -0
  23. linch/coordination/scheduling/tools.py +72 -0
  24. linch/coordination/send_message.py +107 -0
  25. linch/deep_agent/__init__.py +9 -0
  26. linch/deep_agent/factory.py +194 -0
  27. linch/deep_agent/prompts.py +126 -0
  28. linch/deep_agent/subagents.py +127 -0
  29. linch/errors.py +64 -0
  30. linch/evals/__init__.py +46 -0
  31. linch/evals/harness.py +226 -0
  32. linch/evals/scorers.py +215 -0
  33. linch/evals/scripted.py +96 -0
  34. linch/events.py +887 -0
  35. linch/filesystem/__init__.py +54 -0
  36. linch/filesystem/backend.py +253 -0
  37. linch/filesystem/disk.py +119 -0
  38. linch/filesystem/offload.py +140 -0
  39. linch/filesystem/postgres.py +180 -0
  40. linch/filesystem/sqlite.py +157 -0
  41. linch/filesystem/tools.py +287 -0
  42. linch/hooks/__init__.py +71 -0
  43. linch/hooks/adapters.py +378 -0
  44. linch/hooks/contexts.py +166 -0
  45. linch/hooks/dispatcher.py +212 -0
  46. linch/hooks/memory.py +117 -0
  47. linch/hooks/types.py +101 -0
  48. linch/loop/__init__.py +30 -0
  49. linch/loop/checkpoint.py +196 -0
  50. linch/loop/dispatch.py +225 -0
  51. linch/loop/finalize.py +433 -0
  52. linch/loop/request.py +243 -0
  53. linch/loop/runner.py +1412 -0
  54. linch/loop/streaming.py +292 -0
  55. linch/loop/terminals.py +369 -0
  56. linch/loop_guard/__init__.py +17 -0
  57. linch/loop_guard/guard.py +164 -0
  58. linch/mcp/__init__.py +51 -0
  59. linch/mcp/client.py +163 -0
  60. linch/mcp/config.py +29 -0
  61. linch/mcp/naming.py +13 -0
  62. linch/mcp/permission_bridge.py +32 -0
  63. linch/mcp/result.py +64 -0
  64. linch/mcp/tool.py +91 -0
  65. linch/memory/__init__.py +28 -0
  66. linch/memory/builder.py +135 -0
  67. linch/memory/keyword.py +94 -0
  68. linch/memory/lifecycle.py +93 -0
  69. linch/memory/postgres.py +188 -0
  70. linch/memory/sqlite.py +169 -0
  71. linch/memory/store.py +40 -0
  72. linch/memory/tiered.py +183 -0
  73. linch/memory/tools.py +188 -0
  74. linch/memory/types.py +25 -0
  75. linch/middleware.py +160 -0
  76. linch/observability/__init__.py +32 -0
  77. linch/observability/dispatcher.py +67 -0
  78. linch/observability/otel.py +222 -0
  79. linch/observability/protocol.py +155 -0
  80. linch/observability/reference.py +243 -0
  81. linch/openai_responses.py +357 -0
  82. linch/permissions/__init__.py +25 -0
  83. linch/permissions/engine.py +295 -0
  84. linch/permissions/keys.py +39 -0
  85. linch/permissions/rules.py +321 -0
  86. linch/permissions/ruleset.py +45 -0
  87. linch/pricing.py +95 -0
  88. linch/providers/__init__.py +47 -0
  89. linch/providers/anthropic.py +467 -0
  90. linch/providers/base.py +87 -0
  91. linch/providers/catalog.py +137 -0
  92. linch/providers/gemini.py +296 -0
  93. linch/providers/llamacpp.py +178 -0
  94. linch/providers/openai_chat.py +317 -0
  95. linch/providers/openai_responses.py +54 -0
  96. linch/providers/retry.py +57 -0
  97. linch/providers/sglang.py +76 -0
  98. linch/providers/vllm.py +59 -0
  99. linch/reports.py +400 -0
  100. linch/run_store.py +520 -0
  101. linch/scheduler.py +1112 -0
  102. linch/session.py +260 -0
  103. linch/sessions/__init__.py +16 -0
  104. linch/sessions/memory.py +200 -0
  105. linch/sessions/postgres.py +634 -0
  106. linch/sessions/sqlite.py +554 -0
  107. linch/sessions/store.py +74 -0
  108. linch/sessions/tasks.py +44 -0
  109. linch/skills/__init__.py +23 -0
  110. linch/skills/builtins.py +62 -0
  111. linch/skills/listing.py +65 -0
  112. linch/skills/loader.py +222 -0
  113. linch/skills/overlay.py +7 -0
  114. linch/skills/shell_split.py +49 -0
  115. linch/skills/substitute.py +25 -0
  116. linch/skills/system_reminder.py +5 -0
  117. linch/skills/types.py +31 -0
  118. linch/storage/__init__.py +3 -0
  119. linch/storage/_executor.py +133 -0
  120. linch/storage/_pg.py +38 -0
  121. linch/subagents/__init__.py +34 -0
  122. linch/subagents/builtins.py +57 -0
  123. linch/subagents/default_agent.py +24 -0
  124. linch/subagents/generator.py +377 -0
  125. linch/subagents/loader.py +181 -0
  126. linch/subagents/registry.py +46 -0
  127. linch/subagents/runner.py +399 -0
  128. linch/subagents/types.py +38 -0
  129. linch/subagents/workers.py +26 -0
  130. linch/tools/__init__.py +47 -0
  131. linch/tools/_worker_utils.py +18 -0
  132. linch/tools/ask_user.py +237 -0
  133. linch/tools/base.py +97 -0
  134. linch/tools/builtin.py +833 -0
  135. linch/tools/execution.py +294 -0
  136. linch/tools/file_tracker.py +27 -0
  137. linch/tools/function.py +239 -0
  138. linch/tools/isolation.py +67 -0
  139. linch/tools/registry.py +176 -0
  140. linch/tools/skill.py +126 -0
  141. linch/tools/subagent.py +320 -0
  142. linch/tools/subagent_continue.py +152 -0
  143. linch/tools/subagent_stop.py +103 -0
  144. linch/tools/tasks.py +222 -0
  145. linch/types.py +275 -0
  146. linch/verification.py +133 -0
  147. linch/workflow/__init__.py +29 -0
  148. linch/workflow/context.py +199 -0
  149. linch/workflow/engine.py +87 -0
  150. linch/workflow/journal.py +66 -0
  151. linch-1.0.0.dist-info/METADATA +426 -0
  152. linch-1.0.0.dist-info/RECORD +154 -0
  153. linch-1.0.0.dist-info/WHEEL +4 -0
  154. linch-1.0.0.dist-info/licenses/LICENSE +21 -0
linch/__init__.py ADDED
@@ -0,0 +1,487 @@
1
+ """Public API for linch."""
2
+
3
+ from ._version import get_version
4
+ from .agent import Agent, AgentOptions
5
+ from .budget import RunBudget
6
+ from .compaction import CompactionLadder, DefaultCompaction, DetailedCompaction
7
+ from .config import FeatureFlags, SystemPromptConfig, SystemPromptSection
8
+ from .context import (
9
+ ContextBudget,
10
+ ContextBuilder,
11
+ ContextBuilderChain,
12
+ ContextBuildResult,
13
+ ContextBuildTurn,
14
+ )
15
+ from .coordination.mailbox import Correlator, InMemoryMailbox, Mailbox, MailboxMessage
16
+ from .coordination.scheduling import (
17
+ InMemoryScheduleStore,
18
+ Schedule,
19
+ SchedulerLoop,
20
+ ScheduleStore,
21
+ SqliteScheduleStore,
22
+ cron_matches,
23
+ next_cron_time,
24
+ schedule_tools,
25
+ validate_cron,
26
+ )
27
+ from .deep_agent import DEEP_AGENT_SYSTEM_PROMPT, create_deep_agent
28
+ from .errors import (
29
+ AbortError,
30
+ AuthError,
31
+ ConfigError,
32
+ ContextLengthError,
33
+ LinchError,
34
+ PermissionDeniedError,
35
+ ProviderError,
36
+ RateLimitError,
37
+ SkillError,
38
+ ToolExecutionError,
39
+ ToolTimeoutError,
40
+ )
41
+ from .events import (
42
+ AssistantEvent,
43
+ BudgetEvent,
44
+ CompactionEvent,
45
+ ContextBuildEvent,
46
+ ErrorEvent,
47
+ Event,
48
+ HookEventRecord,
49
+ LoopGuardEvent,
50
+ ModelFallbackEvent,
51
+ PartialAssistantEvent,
52
+ PermissionRequestEvent,
53
+ ResultEvent,
54
+ ScheduleEvent,
55
+ SkillCompletedEvent,
56
+ SkillInvokedEvent,
57
+ SkillsLoadedEvent,
58
+ SubagentEvent,
59
+ SystemEvent,
60
+ ToolCallEndEvent,
61
+ ToolCallStartEvent,
62
+ UsageEvent,
63
+ UserEvent,
64
+ VerificationEvent,
65
+ WorkflowEvent,
66
+ is_budget_event,
67
+ is_context_build_event,
68
+ is_hook_event,
69
+ is_loop_guard_event,
70
+ is_subagent_event,
71
+ is_verification_event,
72
+ is_workflow_event,
73
+ )
74
+ from .filesystem import (
75
+ CompositeFileBackend,
76
+ DiskFileBackend,
77
+ FileBackend,
78
+ OffloadConfig,
79
+ SqliteFileBackend,
80
+ StateFileBackend,
81
+ filesystem_tools,
82
+ )
83
+ from .hooks import (
84
+ AfterProviderCallContext,
85
+ BeforeFinalAnswerContext,
86
+ BeforeProviderCallContext,
87
+ ContextInjectionHook,
88
+ FinalAnswerVerifierHook,
89
+ HookContext,
90
+ HookDispatcher,
91
+ HookDispatchResult,
92
+ HookEvent,
93
+ HookResult,
94
+ MemoryExtractionHook,
95
+ PostCompactContext,
96
+ PostToolUseContext,
97
+ PostToolUseFailureContext,
98
+ PreCompactContext,
99
+ PreToolUseContext,
100
+ RunTelemetryHook,
101
+ StopContext,
102
+ StopPredicateHook,
103
+ SubagentStartContext,
104
+ SubagentStopContext,
105
+ ToolMiddlewareHook,
106
+ UserPromptSubmitContext,
107
+ normalize_hooks,
108
+ )
109
+ from .loop import apply_provider_capabilities
110
+ from .loop_guard import (
111
+ LoopGuard,
112
+ LoopGuardDecision,
113
+ LoopGuardState,
114
+ evaluate_loop_guard,
115
+ normalize_loop_guard,
116
+ )
117
+ from .mcp import (
118
+ McpHttpServerConfig,
119
+ McpServerConfig,
120
+ McpStdioServerConfig,
121
+ connect_mcp_servers,
122
+ )
123
+ from .memory import (
124
+ ConsolidationGate,
125
+ InMemoryKeywordMemoryStore,
126
+ MemoryContextBuilder,
127
+ MemoryExtractionContext,
128
+ MemoryExtractor,
129
+ MemoryItem,
130
+ MemorySearchResult,
131
+ MemorySearchTool,
132
+ MemoryStore,
133
+ MemoryUpsertTool,
134
+ PostgresMemoryStore,
135
+ SqliteMemoryStore,
136
+ TieredMemoryStore,
137
+ )
138
+ from .middleware import (
139
+ AgentMiddleware,
140
+ MiddlewareContext,
141
+ ToolCallMiddlewareInput,
142
+ ToolCallMiddlewareResult,
143
+ )
144
+ from .observability import (
145
+ BaseObserver,
146
+ LoggingObserver,
147
+ ObserverDispatcher,
148
+ OpenTelemetryObserver,
149
+ ProviderCallInfo,
150
+ ProviderCallResult,
151
+ RunInfo,
152
+ RunObserver,
153
+ RunResultInfo,
154
+ Span,
155
+ SpanCollector,
156
+ ToolInfo,
157
+ ToolResultInfo,
158
+ TurnInfo,
159
+ normalize_observers,
160
+ )
161
+ from .openai_responses import OpenAIOptions, OpenAIReasoning
162
+ from .providers import (
163
+ AnthropicProvider,
164
+ AnthropicProviderOptions,
165
+ BaseProvider,
166
+ GeminiProvider,
167
+ GeminiProviderOptions,
168
+ LlamaCppProvider,
169
+ LlamaCppProviderOptions,
170
+ OpenAIChatCompletionsProvider,
171
+ OpenAIChatProviderOptions,
172
+ OpenAIResponsesProvider,
173
+ OpenAIResponsesProviderOptions,
174
+ ProviderCapabilities,
175
+ ProviderModelInfo,
176
+ SGLangProvider,
177
+ SGLangProviderOptions,
178
+ VLLMProvider,
179
+ VLLMProviderOptions,
180
+ get_provider_model_info,
181
+ list_provider_models,
182
+ )
183
+ from .providers.retry import RetryOptions
184
+ from .reports import RunReport, build_run_report, load_run_report
185
+ from .run_store import (
186
+ SCHEMA_VERSION as RUN_SCHEMA_VERSION,
187
+ )
188
+ from .run_store import (
189
+ InMemoryRunStore,
190
+ RunCheckpoint,
191
+ RunRecord,
192
+ RunStore,
193
+ SqliteRunStore,
194
+ StoredRunEvent,
195
+ )
196
+ from .session import RunOptions, Session
197
+ from .subagents import (
198
+ CreatedSubagentDefinition,
199
+ GeneratedSubagentDefinition,
200
+ create_subagent_definition,
201
+ generate_subagent_definition,
202
+ render_subagent_markdown,
203
+ write_subagent_definition,
204
+ )
205
+ from .tools import (
206
+ AskUserHandler,
207
+ AskUserOption,
208
+ AskUserQuestion,
209
+ AskUserRequest,
210
+ AskUserResponse,
211
+ AskUserTool,
212
+ Citation,
213
+ FileReadTracker,
214
+ FunctionTool,
215
+ ResourceAccess,
216
+ ResourceMode,
217
+ Tool,
218
+ ToolContext,
219
+ ToolRegistry,
220
+ ToolResult,
221
+ default_tools,
222
+ tool,
223
+ )
224
+ from .tools.isolation import IsolationBackend, TempDirIsolation
225
+ from .tools.registry import empty_tools, tools_from_defaults
226
+ from .types import (
227
+ ContentBlock,
228
+ ImageBlock,
229
+ Message,
230
+ ModelId,
231
+ OutputSchema,
232
+ PermissionMode,
233
+ RedactedThinkingBlock,
234
+ StopReason,
235
+ TextBlock,
236
+ ThinkingBlock,
237
+ ToolChoice,
238
+ ToolResultBlock,
239
+ ToolUseBlock,
240
+ Usage,
241
+ )
242
+ from .verification import (
243
+ ScorerVerifier,
244
+ Verdict,
245
+ VerificationContext,
246
+ Verifier,
247
+ evaluate_verifiers,
248
+ normalize_verifiers,
249
+ )
250
+ from .workflow import WorkflowContext, WorkflowError, WorkflowJournal
251
+
252
+ defaultTools = default_tools
253
+ __version__ = get_version()
254
+
255
+ __all__ = [
256
+ "AbortError",
257
+ "AskUserHandler",
258
+ "AskUserOption",
259
+ "AskUserQuestion",
260
+ "AskUserRequest",
261
+ "AskUserResponse",
262
+ "AskUserTool",
263
+ "Agent",
264
+ "LinchError",
265
+ "AgentOptions",
266
+ "RunBudget",
267
+ "BudgetEvent",
268
+ "is_budget_event",
269
+ "WorkflowContext",
270
+ "WorkflowError",
271
+ "WorkflowEvent",
272
+ "WorkflowJournal",
273
+ "is_workflow_event",
274
+ "ScheduleEvent",
275
+ "Schedule",
276
+ "ScheduleStore",
277
+ "InMemoryScheduleStore",
278
+ "SqliteScheduleStore",
279
+ "SchedulerLoop",
280
+ "schedule_tools",
281
+ "cron_matches",
282
+ "next_cron_time",
283
+ "validate_cron",
284
+ "AgentMiddleware",
285
+ "ContextBudget",
286
+ "ContextBuilder",
287
+ "ContextBuilderChain",
288
+ "ContextBuildEvent",
289
+ "ContextBuildResult",
290
+ "ContextBuildTurn",
291
+ "CompactionLadder",
292
+ "DefaultCompaction",
293
+ "DetailedCompaction",
294
+ "DEEP_AGENT_SYSTEM_PROMPT",
295
+ "FeatureFlags",
296
+ "CompositeFileBackend",
297
+ "DiskFileBackend",
298
+ "FileBackend",
299
+ "OffloadConfig",
300
+ "SqliteFileBackend",
301
+ "StateFileBackend",
302
+ "filesystem_tools",
303
+ "InMemoryKeywordMemoryStore",
304
+ "InMemoryRunStore",
305
+ "SystemPromptConfig",
306
+ "SystemPromptSection",
307
+ "OutputSchema",
308
+ "ToolChoice",
309
+ "empty_tools",
310
+ "tools_from_defaults",
311
+ "AssistantEvent",
312
+ "CompactionEvent",
313
+ "ContentBlock",
314
+ "ErrorEvent",
315
+ "Event",
316
+ "HookEvent",
317
+ "HookEventRecord",
318
+ "HookResult",
319
+ "HookContext",
320
+ "HookDispatchResult",
321
+ "HookDispatcher",
322
+ "UserPromptSubmitContext",
323
+ "BeforeProviderCallContext",
324
+ "AfterProviderCallContext",
325
+ "PreToolUseContext",
326
+ "PostToolUseContext",
327
+ "PostToolUseFailureContext",
328
+ "PreCompactContext",
329
+ "PostCompactContext",
330
+ "BeforeFinalAnswerContext",
331
+ "StopContext",
332
+ "SubagentStartContext",
333
+ "SubagentStopContext",
334
+ "ContextInjectionHook",
335
+ "ToolMiddlewareHook",
336
+ "FinalAnswerVerifierHook",
337
+ "StopPredicateHook",
338
+ "RunTelemetryHook",
339
+ "MemoryExtractionHook",
340
+ "normalize_hooks",
341
+ "is_hook_event",
342
+ "ImageBlock",
343
+ "AuthError",
344
+ "ConfigError",
345
+ "ContextLengthError",
346
+ "CreatedSubagentDefinition",
347
+ "Citation",
348
+ "FileReadTracker",
349
+ "FunctionTool",
350
+ "GeneratedSubagentDefinition",
351
+ "McpHttpServerConfig",
352
+ "McpServerConfig",
353
+ "McpStdioServerConfig",
354
+ "ConsolidationGate",
355
+ "MemoryContextBuilder",
356
+ "MemoryExtractionContext",
357
+ "MemoryExtractor",
358
+ "MemoryItem",
359
+ "MemorySearchResult",
360
+ "MemorySearchTool",
361
+ "MemoryStore",
362
+ "MemoryUpsertTool",
363
+ "MiddlewareContext",
364
+ "Message",
365
+ "ModelId",
366
+ "OpenAIOptions",
367
+ "OpenAIReasoning",
368
+ "PartialAssistantEvent",
369
+ "PermissionMode",
370
+ "PermissionDeniedError",
371
+ "PermissionRequestEvent",
372
+ "ProviderError",
373
+ "PostgresMemoryStore",
374
+ "RateLimitError",
375
+ "RedactedThinkingBlock",
376
+ "ResultEvent",
377
+ "RunOptions",
378
+ "ResourceAccess",
379
+ "ResourceMode",
380
+ "SkillCompletedEvent",
381
+ "SkillError",
382
+ "ThinkingBlock",
383
+ "SkillInvokedEvent",
384
+ "SkillsLoadedEvent",
385
+ "StopReason",
386
+ "SubagentEvent",
387
+ "SystemEvent",
388
+ "Session",
389
+ "SqliteMemoryStore",
390
+ "TieredMemoryStore",
391
+ "TextBlock",
392
+ "Tool",
393
+ "ToolCallEndEvent",
394
+ "ToolCallMiddlewareInput",
395
+ "ToolCallMiddlewareResult",
396
+ "ToolCallStartEvent",
397
+ "ToolContext",
398
+ "ToolExecutionError",
399
+ "ToolTimeoutError",
400
+ "ToolRegistry",
401
+ "ToolResultBlock",
402
+ "ToolResult",
403
+ "ToolUseBlock",
404
+ "tool",
405
+ "Usage",
406
+ "UsageEvent",
407
+ "UserEvent",
408
+ "AnthropicProvider",
409
+ "AnthropicProviderOptions",
410
+ "BaseProvider",
411
+ "GeminiProvider",
412
+ "GeminiProviderOptions",
413
+ "LlamaCppProvider",
414
+ "LlamaCppProviderOptions",
415
+ "OpenAIChatCompletionsProvider",
416
+ "OpenAIChatProviderOptions",
417
+ "OpenAIResponsesProvider",
418
+ "OpenAIResponsesProviderOptions",
419
+ "ProviderCapabilities",
420
+ "ProviderModelInfo",
421
+ "RetryOptions",
422
+ "SGLangProvider",
423
+ "SGLangProviderOptions",
424
+ "RunReport",
425
+ "RUN_SCHEMA_VERSION",
426
+ "RunCheckpoint",
427
+ "RunRecord",
428
+ "RunStore",
429
+ "SqliteRunStore",
430
+ "StoredRunEvent",
431
+ "VLLMProvider",
432
+ "VLLMProviderOptions",
433
+ "apply_provider_capabilities",
434
+ "build_run_report",
435
+ "create_subagent_definition",
436
+ "LoopGuard",
437
+ "LoopGuardDecision",
438
+ "LoopGuardEvent",
439
+ "Correlator",
440
+ "InMemoryMailbox",
441
+ "Mailbox",
442
+ "MailboxMessage",
443
+ "IsolationBackend",
444
+ "TempDirIsolation",
445
+ "ModelFallbackEvent",
446
+ "LoopGuardState",
447
+ "evaluate_loop_guard",
448
+ "normalize_loop_guard",
449
+ "ScorerVerifier",
450
+ "Verdict",
451
+ "VerificationContext",
452
+ "VerificationEvent",
453
+ "Verifier",
454
+ "evaluate_verifiers",
455
+ "is_verification_event",
456
+ "normalize_verifiers",
457
+ "connect_mcp_servers",
458
+ "create_deep_agent",
459
+ "defaultTools",
460
+ "default_tools",
461
+ "get_version",
462
+ "__version__",
463
+ "is_context_build_event",
464
+ "get_provider_model_info",
465
+ "generate_subagent_definition",
466
+ "is_loop_guard_event",
467
+ "is_subagent_event",
468
+ "list_provider_models",
469
+ "load_run_report",
470
+ "BaseObserver",
471
+ "LoggingObserver",
472
+ "ObserverDispatcher",
473
+ "OpenTelemetryObserver",
474
+ "ProviderCallInfo",
475
+ "ProviderCallResult",
476
+ "RunInfo",
477
+ "RunObserver",
478
+ "RunResultInfo",
479
+ "Span",
480
+ "SpanCollector",
481
+ "ToolInfo",
482
+ "ToolResultInfo",
483
+ "TurnInfo",
484
+ "normalize_observers",
485
+ "render_subagent_markdown",
486
+ "write_subagent_definition",
487
+ ]
linch/_blocking.py ADDED
@@ -0,0 +1,71 @@
1
+ """Bounded, daemon-thread offload for blocking work.
2
+
3
+ The core loop must never run blocking disk/DB/CPU work directly on the event
4
+ loop thread. ``asyncio.to_thread`` dispatches onto the default executor whose
5
+ *non-daemon* worker threads can keep the interpreter (and the managed test
6
+ sandbox) alive at teardown, and an unbounded ``threading.Thread`` per call has
7
+ no backpressure.
8
+
9
+ ``run_blocking`` threads the needle: each call runs on a fresh *daemon* thread
10
+ (never blocks teardown) and a per-loop semaphore caps how many run at once
11
+ (backpressure). Wakeup is via ``loop.call_soon_threadsafe`` so the awaiting
12
+ coroutine resumes reliably.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import threading
19
+ from collections.abc import Callable
20
+ from typing import Any, TypeVar
21
+
22
+ T = TypeVar("T")
23
+
24
+ # Cap on concurrently-offloaded blocking calls per event loop. Mirrors the
25
+ # default thread-pool sizing intent without sharing a global pool across loops.
26
+ _MAX_CONCURRENCY = 32
27
+
28
+
29
+ def _loop_semaphore(loop: asyncio.AbstractEventLoop) -> asyncio.Semaphore:
30
+ sem = getattr(loop, "_linch_blocking_sem", None)
31
+ if sem is None:
32
+ sem = asyncio.Semaphore(_MAX_CONCURRENCY)
33
+ try:
34
+ loop._linch_blocking_sem = sem # type: ignore[attr-defined]
35
+ except Exception:
36
+ pass
37
+ return sem
38
+
39
+
40
+ async def run_blocking(fn: Callable[..., T], *args: Any, **kwargs: Any) -> T:
41
+ """Run ``fn(*args, **kwargs)`` on a bounded daemon thread; return its result.
42
+
43
+ Propagates any exception ``fn`` raises to the awaiter. If the awaiting
44
+ coroutine is cancelled the daemon thread still runs to completion (the same
45
+ contract as ``asyncio.to_thread``), but it never blocks interpreter exit.
46
+ """
47
+ loop = asyncio.get_running_loop()
48
+ sem = _loop_semaphore(loop)
49
+ async with sem:
50
+ fut: asyncio.Future[T] = loop.create_future()
51
+
52
+ def _target() -> None:
53
+ try:
54
+ value = fn(*args, **kwargs)
55
+ except BaseException as exc: # noqa: BLE001 - propagated to awaiter
56
+ loop.call_soon_threadsafe(_safe_set_exception, fut, exc)
57
+ return
58
+ loop.call_soon_threadsafe(_safe_set_result, fut, value)
59
+
60
+ threading.Thread(target=_target, name="linch-blocking", daemon=True).start()
61
+ return await fut
62
+
63
+
64
+ def _safe_set_result(fut: asyncio.Future[Any], value: Any) -> None:
65
+ if not fut.done():
66
+ fut.set_result(value)
67
+
68
+
69
+ def _safe_set_exception(fut: asyncio.Future[Any], exc: BaseException) -> None:
70
+ if not fut.done():
71
+ fut.set_exception(exc)
linch/_http_errors.py ADDED
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from email.utils import parsedate_to_datetime
5
+ from typing import Any
6
+
7
+
8
+ def error_status(err: Exception) -> int | None:
9
+ value = getattr(err, "status_code", None) or getattr(err, "status", None)
10
+ return int(value) if isinstance(value, int) else None
11
+
12
+
13
+ def error_body(err: Exception) -> Any:
14
+ body = getattr(err, "body", None)
15
+ if body is not None:
16
+ return body
17
+ response = getattr(err, "response", None)
18
+ if response is not None:
19
+ body = getattr(response, "body", None)
20
+ if body is not None:
21
+ return body
22
+ try:
23
+ return response.json()
24
+ except Exception:
25
+ return None
26
+ return None
27
+
28
+
29
+ def nested_error(err: Exception) -> dict[str, Any]:
30
+ body = error_body(err)
31
+ if isinstance(body, dict):
32
+ raw = body.get("error", body)
33
+ if isinstance(raw, dict):
34
+ return raw
35
+ raw_error = getattr(err, "error", None)
36
+ if isinstance(raw_error, dict):
37
+ return raw_error
38
+ return {}
39
+
40
+
41
+ def error_code(err: Exception) -> str | None:
42
+ raw = nested_error(err).get("code") or nested_error(err).get("type")
43
+ return str(raw) if isinstance(raw, str) and raw else None
44
+
45
+
46
+ def error_message(err: Exception) -> str:
47
+ raw = nested_error(err).get("message")
48
+ if isinstance(raw, str) and raw:
49
+ return raw
50
+ return str(err)
51
+
52
+
53
+ def retry_after_seconds(err: Exception) -> float | None:
54
+ response = getattr(err, "response", None)
55
+ headers = getattr(response, "headers", None)
56
+ if not headers:
57
+ headers = getattr(err, "headers", None)
58
+ if not headers:
59
+ return None
60
+ raw = None
61
+ for key in ("retry-after", "Retry-After", "retry_after"):
62
+ try:
63
+ raw = headers.get(key)
64
+ except Exception:
65
+ raw = None
66
+ if raw is not None:
67
+ break
68
+ if raw is None:
69
+ return None
70
+ if isinstance(raw, (int, float)):
71
+ return max(0.0, float(raw))
72
+ text = str(raw).strip()
73
+ try:
74
+ return max(0.0, float(text))
75
+ except ValueError:
76
+ pass
77
+ try:
78
+ dt = parsedate_to_datetime(text)
79
+ except (TypeError, ValueError, IndexError, OverflowError):
80
+ return None
81
+ if dt.tzinfo is None:
82
+ dt = dt.replace(tzinfo=timezone.utc)
83
+ return max(0.0, (dt - datetime.now(timezone.utc)).total_seconds())
84
+
85
+
86
+ def is_prompt_length_error(err: Exception) -> bool:
87
+ code = error_code(err)
88
+ if code == "context_length_exceeded":
89
+ return True
90
+ message = error_message(err).lower()
91
+ clear_phrases = (
92
+ "prompt is too long",
93
+ "maximum context length",
94
+ "context length exceeded",
95
+ "input is too long",
96
+ )
97
+ if any(phrase in message for phrase in clear_phrases):
98
+ return True
99
+ return "tokens" in message and "maximum" in message and "prompt" in message