timbal 2.4.2__tar.gz → 2.6.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 (290) hide show
  1. {timbal-2.4.2 → timbal-2.6.0}/.gitignore +4 -0
  2. {timbal-2.4.2 → timbal-2.6.0}/PKG-INFO +6 -2
  3. {timbal-2.4.2 → timbal-2.6.0}/pyproject.toml +5 -0
  4. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/_version.py +2 -2
  5. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/__main__.py +2 -0
  6. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/flow.py +35 -7
  7. timbal-2.6.0/python/timbal/codegen/guardrail_specs.py +161 -0
  8. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/transformers/__init__.py +27 -16
  9. timbal-2.6.0/python/timbal/codegen/transformers/add_guardrail.py +113 -0
  10. timbal-2.6.0/python/timbal/codegen/transformers/remove_guardrail.py +91 -0
  11. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/transformers/set_config.py +26 -12
  12. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/agent.py +389 -5
  13. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/fallback_model.py +7 -1
  14. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/llm/retry.py +21 -2
  15. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/llm/router.py +4 -1
  16. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/runnable.py +180 -3
  17. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/workflow.py +118 -35
  18. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/errors.py +26 -0
  19. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/__init__.py +2 -0
  20. timbal-2.6.0/python/timbal/evals/validators/rubric.py +105 -0
  21. timbal-2.6.0/python/timbal/guardrails/__init__.py +89 -0
  22. timbal-2.6.0/python/timbal/guardrails/apply.py +114 -0
  23. timbal-2.6.0/python/timbal/guardrails/builtins/__init__.py +20 -0
  24. timbal-2.6.0/python/timbal/guardrails/builtins/injection.py +99 -0
  25. timbal-2.6.0/python/timbal/guardrails/builtins/judge.py +150 -0
  26. timbal-2.6.0/python/timbal/guardrails/builtins/keywords.py +44 -0
  27. timbal-2.6.0/python/timbal/guardrails/builtins/length.py +44 -0
  28. timbal-2.6.0/python/timbal/guardrails/builtins/moderate.py +120 -0
  29. timbal-2.6.0/python/timbal/guardrails/builtins/pii.py +90 -0
  30. timbal-2.6.0/python/timbal/guardrails/builtins/secrets.py +53 -0
  31. timbal-2.6.0/python/timbal/guardrails/builtins/topic.py +69 -0
  32. timbal-2.6.0/python/timbal/guardrails/judge_llm.py +56 -0
  33. timbal-2.6.0/python/timbal/guardrails/presets.py +106 -0
  34. timbal-2.6.0/python/timbal/guardrails/rubric.py +228 -0
  35. timbal-2.6.0/python/timbal/guardrails/runner.py +342 -0
  36. timbal-2.6.0/python/timbal/guardrails/testing.py +74 -0
  37. timbal-2.6.0/python/timbal/guardrails/trace.py +75 -0
  38. timbal-2.6.0/python/timbal/guardrails/types.py +388 -0
  39. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/models.yaml +4 -4
  40. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/platform/utils.py +105 -7
  41. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/server/http.py +12 -0
  42. timbal-2.6.0/python/timbal/server/livekit_session.py +367 -0
  43. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/server/playground.py +176 -4
  44. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/server/single_session.py +110 -10
  45. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/server/voice.html +243 -16
  46. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/server/voice.py +73 -17
  47. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/state/context.py +7 -1
  48. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/state/tracing/providers/base.py +66 -1
  49. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/events/__init__.py +3 -1
  50. timbal-2.6.0/python/timbal/types/events/guardrail.py +57 -0
  51. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/run_status.py +1 -1
  52. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/__init__.py +10 -0
  53. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/config.py +3 -0
  54. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/elevenlabs.py +19 -1
  55. timbal-2.6.0/python/timbal/voice/fish_audio.py +294 -0
  56. timbal-2.6.0/python/timbal/voice/livekit.py +237 -0
  57. timbal-2.6.0/python/timbal/voice/munsit.py +166 -0
  58. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/providers.py +36 -1
  59. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/telephony.py +16 -0
  60. {timbal-2.4.2 → timbal-2.6.0}/LICENSE +0 -0
  61. {timbal-2.4.2 → timbal-2.6.0}/README.md +0 -0
  62. {timbal-2.4.2 → timbal-2.6.0}/docs/api-reference/ace/chat-completions.mdx +0 -0
  63. {timbal-2.4.2 → timbal-2.6.0}/docs/api-reference/ace/create-from-workforce.mdx +0 -0
  64. {timbal-2.4.2 → timbal-2.6.0}/docs/api-reference/ace/jobs/cancel.mdx +0 -0
  65. {timbal-2.4.2 → timbal-2.6.0}/docs/api-reference/ace/jobs/create.mdx +0 -0
  66. {timbal-2.4.2 → timbal-2.6.0}/docs/api-reference/ace/jobs/get.mdx +0 -0
  67. {timbal-2.4.2 → timbal-2.6.0}/docs/api-reference/ace/jobs/list.mdx +0 -0
  68. {timbal-2.4.2 → timbal-2.6.0}/docs/api-reference/ace/jobs/retry.mdx +0 -0
  69. {timbal-2.4.2 → timbal-2.6.0}/docs/api-reference/ace/link.mdx +0 -0
  70. {timbal-2.4.2 → timbal-2.6.0}/docs/api-reference/ace/list-context-vars.mdx +0 -0
  71. {timbal-2.4.2 → timbal-2.6.0}/docs/api-reference/ace/list-policies.mdx +0 -0
  72. {timbal-2.4.2 → timbal-2.6.0}/docs/api-reference/ace/unlink.mdx +0 -0
  73. {timbal-2.4.2 → timbal-2.6.0}/pyrightconfig.json +0 -0
  74. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/__init__.py +0 -0
  75. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/_slots.py +0 -0
  76. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/__init__.py +0 -0
  77. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/cli_utils.py +0 -0
  78. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/cst_utils.py +0 -0
  79. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/format.py +0 -0
  80. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/library.py +0 -0
  81. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/model_discovery.py +0 -0
  82. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/test.py +0 -0
  83. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/tool_discovery.py +0 -0
  84. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/transformers/add_edge.py +0 -0
  85. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/transformers/add_mcp.py +0 -0
  86. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/transformers/add_step.py +0 -0
  87. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/transformers/add_tool.py +0 -0
  88. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/transformers/remove_edge.py +0 -0
  89. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/transformers/remove_step.py +0 -0
  90. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/transformers/remove_tool.py +0 -0
  91. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/transformers/set_param.py +0 -0
  92. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/codegen/transformers/set_position.py +0 -0
  93. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/collectors/__init__.py +0 -0
  94. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/collectors/base.py +0 -0
  95. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/collectors/impl/__init__.py +0 -0
  96. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/collectors/impl/anthropic.py +0 -0
  97. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/collectors/impl/default.py +0 -0
  98. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/collectors/impl/message.py +0 -0
  99. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/collectors/impl/openai.py +0 -0
  100. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/collectors/impl/string.py +0 -0
  101. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/collectors/impl/timbal.py +0 -0
  102. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/collectors/registry.py +0 -0
  103. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/__init__.py +0 -0
  104. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/llm/__init__.py +0 -0
  105. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/llm/chat_completions.py +0 -0
  106. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/llm/clients.py +0 -0
  107. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/llm/messages.py +0 -0
  108. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/llm/registry.py +0 -0
  109. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/llm/responses.py +0 -0
  110. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/mcp.py +0 -0
  111. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/memory_compaction.py +0 -0
  112. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/models.py +0 -0
  113. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/provider_errors.py +0 -0
  114. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/skill.py +0 -0
  115. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/test_model.py +0 -0
  116. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/tool.py +0 -0
  117. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/tool_result_offload.py +0 -0
  118. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/core/tool_set.py +0 -0
  119. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/__init__.py +0 -0
  120. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/cli.py +0 -0
  121. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/display.py +0 -0
  122. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/fixtures/agent.py +0 -0
  123. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/fixtures/eval_examples.yaml +0 -0
  124. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/fixtures/eval_flow_validators.yaml +0 -0
  125. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/fixtures/eval_negations.yaml +0 -0
  126. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/fixtures/eval_parallel.yaml +0 -0
  127. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/fixtures/eval_subagent.yaml +0 -0
  128. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/fixtures/eval_transforms.yaml +0 -0
  129. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/fixtures/test_invalid_validator.yaml +0 -0
  130. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/models.py +0 -0
  131. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/reporters.py +0 -0
  132. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/runner.py +0 -0
  133. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/utils.py +0 -0
  134. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/_spans.py +0 -0
  135. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/base.py +0 -0
  136. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/comparison_base.py +0 -0
  137. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/contains.py +0 -0
  138. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/contains_all.py +0 -0
  139. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/contains_any.py +0 -0
  140. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/context.py +0 -0
  141. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/email.py +0 -0
  142. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/ends_with.py +0 -0
  143. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/eq.py +0 -0
  144. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/gt.py +0 -0
  145. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/gte.py +0 -0
  146. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/json.py +0 -0
  147. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/language.py +0 -0
  148. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/length.py +0 -0
  149. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/llm_base.py +0 -0
  150. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/lt.py +0 -0
  151. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/lte.py +0 -0
  152. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/max_length.py +0 -0
  153. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/min_length.py +0 -0
  154. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/not_null.py +0 -0
  155. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/parallel.py +0 -0
  156. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/pattern.py +0 -0
  157. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/prompt.py +0 -0
  158. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/semantic.py +0 -0
  159. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/seq.py +0 -0
  160. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/starts_with.py +0 -0
  161. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/evals/validators/type.py +0 -0
  162. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/logs.py +0 -0
  163. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/platform/__init__.py +0 -0
  164. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/platform/integrations.py +0 -0
  165. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/platform/knowledge_bases/__init__.py +0 -0
  166. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/platform/knowledge_bases/query.py +0 -0
  167. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/platform/tool_proxy.py +0 -0
  168. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/platform/types.py +0 -0
  169. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/server/__init__.py +0 -0
  170. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/server/__main__.py +0 -0
  171. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/server/jobs.py +0 -0
  172. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/server/recording_upload.py +0 -0
  173. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/server/rtc.py +0 -0
  174. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/server/telephony.py +0 -0
  175. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/state/__init__.py +0 -0
  176. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/state/config.py +0 -0
  177. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/state/config_loader.py +0 -0
  178. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/state/dependency_analyzer.py +0 -0
  179. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/state/tracing/__init__.py +0 -0
  180. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/state/tracing/exporters/__init__.py +0 -0
  181. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/state/tracing/exporters/otel.py +0 -0
  182. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/state/tracing/providers/__init__.py +0 -0
  183. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/state/tracing/providers/in_memory.py +0 -0
  184. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/state/tracing/providers/jsonl.py +0 -0
  185. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/state/tracing/providers/platform.py +0 -0
  186. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/state/tracing/providers/sqlite.py +0 -0
  187. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/state/tracing/span.py +0 -0
  188. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/state/tracing/trace.py +0 -0
  189. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/__init__.py +87 -87
  190. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/_creds.py +0 -0
  191. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/aircall.py +0 -0
  192. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/asana.py +0 -0
  193. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/bash.py +0 -0
  194. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/cala.py +0 -0
  195. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/cloudflare.py +0 -0
  196. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/connectif.py +0 -0
  197. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/covermanager.py +0 -0
  198. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/dynamics_business_central.py +0 -0
  199. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/dynamics_sales.py +0 -0
  200. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/edit.py +0 -0
  201. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/elasticsearch.py +0 -0
  202. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/elevenlabs.py +0 -0
  203. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/excel.py +0 -0
  204. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/fal.py +0 -0
  205. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/fathom.py +0 -0
  206. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/firecrawl.py +0 -0
  207. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/gemini_images.py +0 -0
  208. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/gmail.py +0 -0
  209. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/google_analytics.py +0 -0
  210. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/google_business.py +0 -0
  211. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/google_calendar.py +0 -0
  212. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/google_docs.py +0 -0
  213. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/google_drive.py +0 -0
  214. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/google_maps.py +0 -0
  215. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/google_merchant_center.py +0 -0
  216. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/google_search_console.py +0 -0
  217. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/google_sheets.py +0 -0
  218. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/happy_scribe.py +0 -0
  219. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/higgsfield.py +0 -0
  220. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/hubspot.py +0 -0
  221. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/interaction.py +0 -0
  222. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/jira.py +0 -0
  223. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/klaviyo.py +0 -0
  224. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/knowledge_base.py +0 -0
  225. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/krea.py +0 -0
  226. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/lancedb.py +0 -0
  227. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/linkedin.py +0 -0
  228. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/mongodb.py +0 -0
  229. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/netsuite.py +0 -0
  230. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/onedrive.py +0 -0
  231. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/outlook.py +0 -0
  232. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/parallel.py +0 -0
  233. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/pinecone.py +0 -0
  234. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/powerbi.py +0 -0
  235. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/quiver_ai.py +0 -0
  236. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/read.py +0 -0
  237. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/replicate.py +0 -0
  238. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/resend.py +0 -0
  239. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/salesforce.py +0 -0
  240. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/scraperapi.py +0 -0
  241. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/sharepoint.py +0 -0
  242. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/shopify.py +0 -0
  243. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/slack.py +0 -0
  244. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/stripe.py +0 -0
  245. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/tavily.py +0 -0
  246. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/web_search.py +0 -0
  247. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/write.py +0 -0
  248. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/xai.py +0 -0
  249. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/zendesk.py +0 -0
  250. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/tools/zoho_crm.py +0 -0
  251. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/__init__.py +0 -0
  252. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/approval.py +0 -0
  253. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/content/__init__.py +0 -0
  254. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/content/base.py +0 -0
  255. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/content/custom.py +0 -0
  256. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/content/file.py +0 -0
  257. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/content/text.py +0 -0
  258. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/content/thinking.py +0 -0
  259. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/content/tool_result.py +0 -0
  260. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/content/tool_use.py +0 -0
  261. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/events/approval.py +0 -0
  262. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/events/base.py +0 -0
  263. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/events/delta.py +0 -0
  264. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/events/interaction.py +0 -0
  265. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/events/output.py +0 -0
  266. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/events/start.py +0 -0
  267. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/file.py +0 -0
  268. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/types/message.py +0 -0
  269. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/utils/__init__.py +0 -0
  270. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/utils/import_spec.py +0 -0
  271. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/utils/model.py +0 -0
  272. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/utils/net.py +0 -0
  273. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/utils/schema.py +0 -0
  274. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/utils/serialization.py +0 -0
  275. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/_whisper_features.py +0 -0
  276. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/ambience.py +0 -0
  277. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/deepgram.py +0 -0
  278. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/endpointing.py +0 -0
  279. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/eou.py +0 -0
  280. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/events.py +0 -0
  281. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/metrics.py +0 -0
  282. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/namo.py +0 -0
  283. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/playback.py +0 -0
  284. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/realtime.py +0 -0
  285. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/recording.py +0 -0
  286. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/session.py +0 -0
  287. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/smart_turn.py +0 -0
  288. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/turn_detection.py +0 -0
  289. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/vad.py +0 -0
  290. {timbal-2.4.2 → timbal-2.6.0}/python/timbal/voice/webrtc.py +0 -0
@@ -54,6 +54,10 @@ node_modules/
54
54
  # Internal planning docs
55
55
  planning/
56
56
 
57
+ # Local rust WIP — do not ship in the Python package
58
+ /rust/
59
+ /benchmarks/rust/
60
+
57
61
  .claude/
58
62
  .cursor/
59
63
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: timbal
3
- Version: 2.4.2
3
+ Version: 2.6.0
4
4
  Summary: Simple, performant, battle-tested framework for building reliable AI applications
5
5
  Project-URL: Source, https://github.com/timbal-ai/timbal
6
6
  Author-email: Timbal <team@timbal.ai>
@@ -238,6 +238,7 @@ Requires-Dist: huggingface-hub>=0.28.0; extra == 'all'
238
238
  Requires-Dist: libcst>=1.8.6; extra == 'all'
239
239
  Requires-Dist: onnxruntime>=1.20.0; extra == 'all'
240
240
  Requires-Dist: openpyxl>=3.1.5; extra == 'all'
241
+ Requires-Dist: ormsgpack>=1.5.0; extra == 'all'
241
242
  Requires-Dist: pymupdf>=1.27.1; extra == 'all'
242
243
  Requires-Dist: python-docx>=1.1.2; extra == 'all'
243
244
  Requires-Dist: rich>=14.2.0; extra == 'all'
@@ -256,6 +257,7 @@ Provides-Extra: evals
256
257
  Requires-Dist: rich>=14.2.0; extra == 'evals'
257
258
  Provides-Extra: server
258
259
  Requires-Dist: fastapi>=0.115.8; extra == 'server'
260
+ Requires-Dist: ormsgpack>=1.5.0; extra == 'server'
259
261
  Requires-Dist: uvicorn>=0.34.0; extra == 'server'
260
262
  Requires-Dist: websockets>=15.0.1; extra == 'server'
261
263
  Provides-Extra: voice
@@ -263,6 +265,8 @@ Requires-Dist: aiortc>=1.9.0; extra == 'voice'
263
265
  Requires-Dist: huggingface-hub>=0.28.0; extra == 'voice'
264
266
  Requires-Dist: onnxruntime>=1.20.0; extra == 'voice'
265
267
  Requires-Dist: transformers>=4.40.0; extra == 'voice'
268
+ Provides-Extra: voice-livekit
269
+ Requires-Dist: livekit>=1.1.14; extra == 'voice-livekit'
266
270
  Description-Content-Type: text/markdown
267
271
 
268
272
  # Timbal
@@ -57,6 +57,7 @@ evals = [
57
57
  ]
58
58
  server = [
59
59
  "fastapi>=0.115.8",
60
+ "ormsgpack>=1.5.0",
60
61
  "uvicorn>=0.34.0",
61
62
  "websockets>=15.0.1",
62
63
  ]
@@ -66,6 +67,9 @@ voice = [
66
67
  "onnxruntime>=1.20.0",
67
68
  "transformers>=4.40.0",
68
69
  ]
70
+ "voice-livekit" = [
71
+ "livekit>=1.1.14",
72
+ ]
69
73
  all = [
70
74
  "timbal[codegen]",
71
75
  "timbal[documents]",
@@ -108,6 +112,7 @@ exclude = [
108
112
  "/sandbox",
109
113
  "/scripts",
110
114
  "/benchmarks",
115
+ "/rust",
111
116
  ]
112
117
 
113
118
  [tool.uv]
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.4.2'
22
- __version_tuple__ = version_tuple = (2, 4, 2)
21
+ __version__ = version = '2.6.0'
22
+ __version_tuple__ = version_tuple = (2, 6, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -10,10 +10,12 @@ from pathlib import Path
10
10
  # A test asserts this table stays in sync with the modules on disk.
11
11
  _TRANSFORMER_OPS: dict[str, tuple[str, str]] = {
12
12
  "add-edge": ("add_edge", "Add an ordering or conditional edge between two workflow steps."),
13
+ "add-guardrail": ("add_guardrail", "Add a guardrail shorthand to the agent's guardrails list."),
13
14
  "add-mcp": ("add_mcp", "Add an MCP server to the agent's tools list."),
14
15
  "add-step": ("add_step", "Add a step to the workflow."),
15
16
  "add-tool": ("add_tool", "Add a tool to the agent's tools list."),
16
17
  "remove-edge": ("remove_edge", "Remove an edge between two workflow steps."),
18
+ "remove-guardrail": ("remove_guardrail", "Remove a guardrail from the agent's guardrails list by name."),
17
19
  "remove-step": ("remove_step", "Remove a step from the workflow by name."),
18
20
  "remove-tool": ("remove_tool", "Remove a tool from the agent's tools list by name."),
19
21
  "set-config": ("set_config", "Set configuration on the agent/step or on a specific tool."),
@@ -39,6 +39,20 @@ def _get_when_source(step: Any) -> str | None:
39
39
  return _get_callable_source(step.when["callable"])
40
40
 
41
41
 
42
+ def _get_while_info(step: Any) -> dict[str, Any] | None:
43
+ """Describe a step's while_ loop config for JSON output.
44
+
45
+ Returns ``{"count": N}`` for count loops, ``{"expr": "<source>"}`` for
46
+ callable conditions, or None when the step doesn't loop.
47
+ """
48
+ while_ = getattr(step, "while_", None)
49
+ if not while_:
50
+ return None
51
+ if "count" in while_:
52
+ return {"count": while_["count"]}
53
+ return {"expr": _get_callable_source(while_["callable"]) or "<callable>"}
54
+
55
+
42
56
  def _extract_key_from_lambda(fn: Any, source_step: str) -> str | None:
43
57
  """Extract the dot-notation key path from a runtime lambda's source.
44
58
 
@@ -208,9 +222,9 @@ def _edge_kind(kinds: set[str] | None) -> str:
208
222
 
209
223
  A dependency can arise from several sources at once (e.g. an explicit
210
224
  ``depends_on`` that also reads the step's output via a param map). Precedence
211
- is ``ordering > when > param`` so explicit sequencing is never masked by
212
- param wiring. Unknown/missing (e.g. edges from a direct ``_link``) default to
213
- ``ordering`` — the safe choice, since ordering edges are never reduced away.
225
+ is ``ordering > when > while > param`` so explicit sequencing is never masked
226
+ by param wiring. Unknown/missing (e.g. edges from a direct ``_link``) default
227
+ to ``ordering`` — the safe choice, since ordering edges are never reduced away.
214
228
  """
215
229
  if not kinds:
216
230
  return "ordering"
@@ -218,6 +232,8 @@ def _edge_kind(kinds: set[str] | None) -> str:
218
232
  return "ordering"
219
233
  if "when" in kinds:
220
234
  return "when"
235
+ if "while" in kinds:
236
+ return "while"
221
237
  return "param"
222
238
 
223
239
 
@@ -253,7 +269,11 @@ def get_flow(workspace_path: str | Path) -> dict[str, Any]:
253
269
 
254
270
  if isinstance(runnable, Workflow):
255
271
  for step in runnable._steps.values():
256
- nodes.append(_build_node(step, include_tools=isinstance(step, Agent)))
272
+ node = _build_node(step, include_tools=isinstance(step, Agent))
273
+ while_info = _get_while_info(step)
274
+ if while_info is not None:
275
+ node["data"]["while"] = while_info
276
+ nodes.append(node)
257
277
 
258
278
  for _step_name, step in runnable._steps.items():
259
279
  when_source = _get_when_source(step) if step.when else None
@@ -407,6 +427,13 @@ def _fmt_node_lines(node: dict, indent: str) -> list[str]:
407
427
  elif ntype == "workflow":
408
428
  lines.append(f"{indent}workflow {name}")
409
429
 
430
+ while_info = node["data"].get("while")
431
+ if while_info:
432
+ if "count" in while_info:
433
+ lines.append(f"{indent} while: count={while_info['count']}")
434
+ else:
435
+ lines.append(f"{indent} while: {while_info.get('expr', '<callable>')}")
436
+
410
437
  return lines
411
438
 
412
439
 
@@ -420,9 +447,10 @@ def _reduce_param_edges(edges: list[dict]) -> list[dict]:
420
447
  first step.
421
448
 
422
449
  Only edges whose single ``kind`` is ``param`` are eligible for removal —
423
- explicit ordering (``depends_on`` / hooks) and ``when`` edges are always
424
- kept so user-added structure never silently disappears from the compact
425
- view. The full edge set (including these) is retained in the JSON output.
450
+ explicit ordering (``depends_on`` / hooks), ``when``, and ``while`` edges
451
+ are always kept so user-added structure never silently disappears from the
452
+ compact view. The full edge set (including these) is retained in the JSON
453
+ output.
426
454
  """
427
455
  adj: dict[str, list[str]] = defaultdict(list)
428
456
  for e in edges:
@@ -0,0 +1,161 @@
1
+ """Shared CST helpers for the add-guardrail / remove-guardrail transformers.
2
+
3
+ Lives outside ``transformers/`` on purpose. That package is scanned by ``pkgutil`` and
4
+ every module in it is imported as an operation, so a helper module placed there would be
5
+ registered as a bogus operation — and, worse, having one transformer import another's
6
+ privates couples unrelated operations together at import time.
7
+ """
8
+
9
+ import libcst as cst
10
+
11
+ from .cst_utils import (
12
+ collect_assignments,
13
+ collect_step_names,
14
+ is_bare_function_step,
15
+ resolve_entry_point_type,
16
+ )
17
+
18
+ __all__ = [
19
+ "guardrails_kwarg_index",
20
+ "literal_shorthands",
21
+ "rail_name",
22
+ "string_element",
23
+ "string_value",
24
+ "validate_guardrail_target",
25
+ "validate_guardrails_value",
26
+ "validate_shorthand",
27
+ ]
28
+
29
+
30
+ def validate_shorthand(spec: str) -> None:
31
+ """Validate one ``name[:action]`` shorthand at the CLI boundary.
32
+
33
+ Unknown names/actions re-raise the presets error (it lists the valid options).
34
+ Rails that are valid but have required constructor params (topic, judge, keywords,
35
+ length) get a pointer to configure them in code instead of a pydantic dump.
36
+ """
37
+ from timbal.guardrails.presets import coerce_rail
38
+
39
+ try:
40
+ coerce_rail(spec)
41
+ except ValueError as e:
42
+ if "Unknown guardrail" in str(e):
43
+ raise
44
+ reason = e.errors()[0]["msg"].removeprefix("Value error, ") if hasattr(e, "errors") else str(e)
45
+ raise ValueError(
46
+ f"Guardrail {spec.partition(':')[0].strip()!r} needs configuration that a shorthand cannot "
47
+ f"express: {reason} Configure it in code instead — e.g. "
48
+ "Agent(guardrails=[TopicGuard(allow=[...])]) from timbal.guardrails."
49
+ ) from e
50
+
51
+
52
+ def validate_guardrails_value(rails: object) -> None:
53
+ """Validate a JSON ``guardrails`` value (agent- or tool-level) at the CLI.
54
+
55
+ Accepts the whole-value string ``"default"``, a single shorthand string, or a list
56
+ of shorthand strings. ``"default"`` inside a *list* is rejected: at runtime the
57
+ preset is only recognized as the whole value, so emitting it as a list entry would
58
+ fail at construction with a misleading "unknown shorthand" error.
59
+ """
60
+ from timbal.guardrails.presets import DEFAULT_SHORTHANDS
61
+
62
+ if isinstance(rails, str):
63
+ if rails.strip().lower() != "default":
64
+ validate_shorthand(rails)
65
+ return
66
+ if not isinstance(rails, list) or not all(isinstance(s, str) for s in rails):
67
+ raise ValueError(
68
+ 'guardrails must be a JSON list of shorthand strings (e.g. ["pii:redact", "injection:block"]) '
69
+ 'or the string "default".'
70
+ )
71
+ for spec in rails:
72
+ if spec.strip().lower() == "default":
73
+ raise ValueError(
74
+ '"default" is the whole-value preset, not a list entry. Use '
75
+ '{"guardrails": "default"}, or list its shorthands explicitly: '
76
+ f"{list(DEFAULT_SHORTHANDS)}."
77
+ )
78
+ validate_shorthand(spec)
79
+
80
+
81
+ def validate_guardrail_target(
82
+ tree: cst.Module | None,
83
+ entry_point: str,
84
+ step: str | None,
85
+ operation: str,
86
+ ) -> tuple[str, dict]:
87
+ """Resolve and validate the Agent the operation targets. Mirrors remove-tool."""
88
+ if tree is not None:
89
+ ep_type = resolve_entry_point_type(tree, entry_point)
90
+ if step:
91
+ if ep_type is not None and ep_type != "Workflow":
92
+ raise ValueError(f"--step requires a Workflow entry point, but '{entry_point}' is a {ep_type}.")
93
+ else:
94
+ if ep_type is not None and ep_type != "Agent":
95
+ raise ValueError(f"{operation} requires an Agent entry point, but '{entry_point}' is a {ep_type}.")
96
+
97
+ target = step if step else entry_point
98
+ assignments = collect_assignments(tree) if tree else {}
99
+
100
+ if tree is not None:
101
+ if step:
102
+ step_names = collect_step_names(tree, entry_point, assignments)
103
+ if (
104
+ step not in step_names
105
+ and step not in assignments
106
+ and not is_bare_function_step(tree, entry_point, step, assignments)
107
+ ):
108
+ raise ValueError(
109
+ f"Workflow step '{step}' not found. "
110
+ "Use the step variable name from .step(...), not the runtime name."
111
+ )
112
+ elif entry_point not in assignments:
113
+ raise ValueError(
114
+ f"Entry point variable '{entry_point}' not found in source. "
115
+ "Ensure timbal.yaml fqn matches the Agent/Workflow variable name."
116
+ )
117
+ return target, assignments
118
+
119
+
120
+ def string_value(node: cst.BaseExpression) -> str | None:
121
+ if isinstance(node, cst.SimpleString):
122
+ return node.evaluated_value if isinstance(node.evaluated_value, str) else None
123
+ return None
124
+
125
+
126
+ def string_element(value: str) -> cst.Element:
127
+ return cst.Element(value=cst.SimpleString(f'"{value}"'))
128
+
129
+
130
+ def rail_name(spec: str) -> str:
131
+ """The rail identity of a shorthand — ``"pii:redact"`` and ``"pii"`` are the same rail."""
132
+ return spec.partition(":")[0].strip().lower()
133
+
134
+
135
+ def guardrails_kwarg_index(call: cst.Call) -> int | None:
136
+ for i, arg in enumerate(call.args):
137
+ if isinstance(arg.keyword, cst.Name) and arg.keyword.value == "guardrails":
138
+ return i
139
+ return None
140
+
141
+
142
+ def literal_shorthands(value: cst.BaseExpression) -> list[str] | None:
143
+ """The kwarg's current shorthand list, or None when it isn't literal strings.
144
+
145
+ A ``"default"`` string expands to its preset shorthands so list edits compose.
146
+ """
147
+ if (s := string_value(value)) is not None:
148
+ if s.strip().lower() == "default":
149
+ from timbal.guardrails.presets import DEFAULT_SHORTHANDS
150
+
151
+ return list(DEFAULT_SHORTHANDS)
152
+ return [s]
153
+ if isinstance(value, cst.List):
154
+ out: list[str] = []
155
+ for el in value.elements:
156
+ s = string_value(el.value)
157
+ if s is None:
158
+ return None
159
+ out.append(s)
160
+ return out
161
+ return None
@@ -16,22 +16,36 @@ from timbal.codegen import parse_fqn
16
16
  from timbal.codegen.cst_utils import collect_assignments, resolve_runnable_name
17
17
  from timbal.codegen.format import format_code
18
18
 
19
- _transformer_modules = None
20
-
21
19
 
22
20
  def load_modules() -> dict:
23
- modules = {}
24
- for info in pkgutil.iter_modules([str(Path(__file__).parent)]):
25
- mod = importlib.import_module(f"timbal.codegen.transformers.{info.name}")
26
- modules[info.name] = mod
27
- return modules
21
+ """Import every transformer module. Used for discovery/introspection, not dispatch —
22
+ see :func:`_load_operation` for why running an operation must not import them all."""
23
+ return {
24
+ info.name: importlib.import_module(f"timbal.codegen.transformers.{info.name}")
25
+ for info in pkgutil.iter_modules([str(Path(__file__).parent)])
26
+ }
27
+
28
28
 
29
+ def _module_names() -> set[str]:
30
+ """Available operation names, discovered without importing anything."""
31
+ return {info.name for info in pkgutil.iter_modules([str(Path(__file__).parent)])}
29
32
 
30
- def _get_transformer_modules() -> dict:
31
- global _transformer_modules
32
- if _transformer_modules is None:
33
- _transformer_modules = load_modules()
34
- return _transformer_modules
33
+
34
+ def _load_operation(operation: str):
35
+ """Import the single transformer an operation needs.
36
+
37
+ Deliberately not ``load_modules()``: importing every transformer to run one means a
38
+ failure in any of them (a bad import, a syntax error mid-edit) breaks every unrelated
39
+ operation too.
40
+ """
41
+ if operation not in _module_names():
42
+ raise ValueError(f"unknown operation: {operation}")
43
+ try:
44
+ return importlib.import_module(f"timbal.codegen.transformers.{operation}")
45
+ except Exception as e:
46
+ raise ValueError(
47
+ f"operation '{operation.replace('_', '-')}' failed to load: {type(e).__name__}: {e}"
48
+ ) from e
35
49
 
36
50
 
37
51
  class _UsageAnalyzer(cst.CSTVisitor):
@@ -301,10 +315,7 @@ def apply_operation(workspace_path: str | Path, operation: str, **kwargs) -> str
301
315
  except cst.ParserSyntaxError as e:
302
316
  raise ValueError(f"Cannot parse {spec.path}: {e}") from e
303
317
 
304
- modules = _get_transformer_modules()
305
- mod = modules.get(operation)
306
- if mod is None:
307
- raise ValueError(f"unknown operation: {operation}")
318
+ mod = _load_operation(operation)
308
319
 
309
320
  args = SimpleNamespace(**kwargs)
310
321
  result = mod.run(spec.target, args, tree=tree)
@@ -0,0 +1,113 @@
1
+ """add-guardrail: wire a guardrail shorthand into the Agent's guardrails list.
2
+
3
+ ```bash
4
+ python -m timbal.codegen add-guardrail --spec "pii:redact"
5
+ python -m timbal.codegen add-guardrail --spec default # set guardrails="default"
6
+ python -m timbal.codegen add-guardrail --spec "moderation:warn" --step agent_a
7
+ ```
8
+
9
+ Semantics on the existing ``guardrails=`` kwarg:
10
+
11
+ - absent → ``guardrails=["<spec>"]`` (or ``guardrails="default"`` for the default preset)
12
+ - string ``"default"`` → expanded to its shorthand list, then the spec is merged in
13
+ - string shorthand → converted to a two-element list
14
+ - list of string literals → the spec is appended; an entry with the same rail *name*
15
+ (the part before ``:``) is replaced, since duplicate rail names are invalid at runtime
16
+ - anything non-literal (a variable, rail instances) → loud error; edit the code directly
17
+ """
18
+
19
+ import argparse
20
+
21
+ import libcst as cst
22
+
23
+ from ..guardrail_specs import (
24
+ guardrails_kwarg_index,
25
+ literal_shorthands,
26
+ rail_name,
27
+ string_element,
28
+ validate_guardrail_target,
29
+ validate_shorthand,
30
+ )
31
+
32
+
33
+ def register(subparsers: argparse._SubParsersAction) -> None:
34
+ sp = subparsers.add_parser(
35
+ "add-guardrail",
36
+ help="Add a guardrail shorthand to the agent's guardrails list.",
37
+ )
38
+ sp.add_argument(
39
+ "--spec",
40
+ required=True,
41
+ help='Guardrail shorthand: "default", or "<name>[:action]" (e.g. "pii:redact", "injection:block").',
42
+ )
43
+ sp.add_argument(
44
+ "--step",
45
+ default=None,
46
+ help="Target step name within a Workflow. When provided, the guardrail is added to that step's Agent.",
47
+ )
48
+
49
+
50
+ def _validate_spec(spec: str) -> None:
51
+ """Reject unknown shorthands loudly at the CLI boundary."""
52
+ if spec.strip().lower() == "default":
53
+ return
54
+ validate_shorthand(spec)
55
+
56
+
57
+ def run(entry_point: str, args: argparse.Namespace, *, tree: cst.Module | None = None) -> cst.CSTTransformer:
58
+ _validate_spec(args.spec)
59
+ target, _assignments = validate_guardrail_target(
60
+ tree, entry_point, getattr(args, "step", None), "add-guardrail"
61
+ )
62
+ return GuardrailAdder(target, args.spec.strip())
63
+
64
+
65
+ class GuardrailAdder(cst.CSTTransformer):
66
+ def __init__(self, target: str, spec: str) -> None:
67
+ self.target = target
68
+ self.spec = spec
69
+ self.matched = False
70
+
71
+ def _edit_call(self, call: cst.Call) -> cst.Call:
72
+ self.matched = True
73
+ idx = guardrails_kwarg_index(call)
74
+
75
+ if self.spec.lower() == "default":
76
+ new_value: cst.BaseExpression = cst.SimpleString('"default"')
77
+ else:
78
+ current = [] if idx is None else literal_shorthands(call.args[idx].value)
79
+ if current is None:
80
+ raise ValueError(
81
+ "The existing guardrails= value is not a literal string/list of shorthands "
82
+ "(it may hold rail instances or a variable). Edit the source directly."
83
+ )
84
+ name = rail_name(self.spec)
85
+ merged = [s for s in current if rail_name(s) != name]
86
+ merged.append(self.spec)
87
+ if merged == current:
88
+ return call # already present — idempotent
89
+ new_value = cst.List(elements=[string_element(s) for s in merged])
90
+
91
+ new_arg = cst.Arg(keyword=cst.Name("guardrails"), value=new_value)
92
+ if idx is None:
93
+ return call.with_changes(args=[*call.args, new_arg])
94
+ return call.with_changes(args=[*call.args[:idx], new_arg, *call.args[idx + 1 :]])
95
+
96
+ def leave_Assign(self, original_node: cst.Assign, updated_node: cst.Assign) -> cst.Assign: # noqa: ARG002
97
+ for assign_target in updated_node.targets:
98
+ if (
99
+ isinstance(assign_target.target, cst.Name)
100
+ and assign_target.target.value == self.target
101
+ and isinstance(updated_node.value, cst.Call)
102
+ ):
103
+ return updated_node.with_changes(value=self._edit_call(updated_node.value))
104
+ return updated_node
105
+
106
+ def leave_AnnAssign(self, original_node: cst.AnnAssign, updated_node: cst.AnnAssign) -> cst.AnnAssign: # noqa: ARG002
107
+ if (
108
+ isinstance(updated_node.target, cst.Name)
109
+ and updated_node.target.value == self.target
110
+ and isinstance(updated_node.value, cst.Call)
111
+ ):
112
+ return updated_node.with_changes(value=self._edit_call(updated_node.value))
113
+ return updated_node
@@ -0,0 +1,91 @@
1
+ """remove-guardrail: remove a guardrail from the Agent's guardrails list by rail name.
2
+
3
+ ```bash
4
+ python -m timbal.codegen remove-guardrail --name pii
5
+ python -m timbal.codegen remove-guardrail --name injection --step agent_a
6
+ ```
7
+
8
+ Matches list entries by rail *name* (the part before ``:``), so ``--name pii`` removes
9
+ ``"pii"``, ``"pii:redact"``, or ``"pii:block"``. A ``guardrails="default"`` string is
10
+ expanded to its shorthand list first. Removing the last rail drops the kwarg entirely.
11
+ Removing an already-absent rail is an idempotent success.
12
+ """
13
+
14
+ import argparse
15
+
16
+ import libcst as cst
17
+
18
+ from ..guardrail_specs import (
19
+ guardrails_kwarg_index,
20
+ literal_shorthands,
21
+ rail_name,
22
+ string_element,
23
+ validate_guardrail_target,
24
+ )
25
+
26
+
27
+ def register(subparsers: argparse._SubParsersAction) -> None:
28
+ sp = subparsers.add_parser(
29
+ "remove-guardrail",
30
+ help="Remove a guardrail from the agent's guardrails list by rail name.",
31
+ )
32
+ sp.add_argument("--name", required=True, help='The rail name to remove (e.g. "pii", "injection").')
33
+ sp.add_argument(
34
+ "--step",
35
+ default=None,
36
+ help="Target step name within a Workflow. When provided, the guardrail is removed from that step's Agent.",
37
+ )
38
+
39
+
40
+ def run(entry_point: str, args: argparse.Namespace, *, tree: cst.Module | None = None) -> cst.CSTTransformer:
41
+ target, _assignments = validate_guardrail_target(
42
+ tree, entry_point, getattr(args, "step", None), "remove-guardrail"
43
+ )
44
+ return GuardrailRemover(target, args.name.strip().lower())
45
+
46
+
47
+ class GuardrailRemover(cst.CSTTransformer):
48
+ # Removing an absent rail is an idempotent success.
49
+ allow_noop = True
50
+
51
+ def __init__(self, target: str, name: str) -> None:
52
+ self.target = target
53
+ self.name = name
54
+
55
+ def _edit_call(self, call: cst.Call) -> cst.Call:
56
+ idx = guardrails_kwarg_index(call)
57
+ if idx is None:
58
+ return call # nothing configured — idempotent
59
+ current = literal_shorthands(call.args[idx].value)
60
+ if current is None:
61
+ raise ValueError(
62
+ "The existing guardrails= value is not a literal string/list of shorthands "
63
+ "(it may hold rail instances or a variable). Edit the source directly."
64
+ )
65
+ remaining = [s for s in current if rail_name(s) != self.name]
66
+ if remaining == current:
67
+ return call # not present — idempotent (still normalizes "default" only on a hit)
68
+ if not remaining:
69
+ return call.with_changes(args=[*call.args[:idx], *call.args[idx + 1 :]])
70
+ new_value = cst.List(elements=[string_element(s) for s in remaining])
71
+ new_arg = cst.Arg(keyword=cst.Name("guardrails"), value=new_value)
72
+ return call.with_changes(args=[*call.args[:idx], new_arg, *call.args[idx + 1 :]])
73
+
74
+ def leave_Assign(self, original_node: cst.Assign, updated_node: cst.Assign) -> cst.Assign: # noqa: ARG002
75
+ for assign_target in updated_node.targets:
76
+ if (
77
+ isinstance(assign_target.target, cst.Name)
78
+ and assign_target.target.value == self.target
79
+ and isinstance(updated_node.value, cst.Call)
80
+ ):
81
+ return updated_node.with_changes(value=self._edit_call(updated_node.value))
82
+ return updated_node
83
+
84
+ def leave_AnnAssign(self, original_node: cst.AnnAssign, updated_node: cst.AnnAssign) -> cst.AnnAssign: # noqa: ARG002
85
+ if (
86
+ isinstance(updated_node.target, cst.Name)
87
+ and updated_node.target.value == self.target
88
+ and isinstance(updated_node.value, cst.Call)
89
+ ):
90
+ return updated_node.with_changes(value=self._edit_call(updated_node.value))
91
+ return updated_node