revenium-python-sdk 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/PKG-INFO +108 -4
  2. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/README.md +107 -3
  3. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/pyproject.toml +1 -1
  4. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/__init__.py +19 -0
  5. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_core/config.py +9 -0
  6. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_core/context.py +44 -0
  7. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_core/exceptions.py +42 -0
  8. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_core/fields.py +24 -1
  9. revenium_python_sdk-0.4.0/revenium_middleware/_core/outcomes.py +424 -0
  10. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_core/trace_fields.py +58 -0
  11. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/resources/ai.py +8 -0
  12. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/types/ai_create_completion_params.py +3 -0
  13. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/agentic_outcomes.py +58 -104
  14. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/anthropic/bedrock_transport.py +1 -0
  15. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/anthropic/middleware.py +9 -2
  16. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/anthropic/trace_fields.py +3 -0
  17. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/fal/_metering.py +2 -0
  18. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/fal/trace_fields.py +6 -0
  19. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/google/common/trace_fields.py +4 -0
  20. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/google/common/utils.py +5 -0
  21. revenium_python_sdk-0.4.0/revenium_middleware/job_context.py +508 -0
  22. revenium_python_sdk-0.4.0/revenium_middleware/job_history.py +127 -0
  23. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/litellm/client/__init__.py +2 -0
  24. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/litellm/client/decorators.py +110 -1
  25. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/litellm/client/integrations/crewai.py +151 -24
  26. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/litellm/client/middleware.py +5 -0
  27. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/litellm/client/trace_fields.py +3 -0
  28. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/ollama/middleware.py +5 -0
  29. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/ollama/trace_fields.py +3 -0
  30. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/openai/middleware.py +14 -3
  31. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/openai/trace_fields.py +3 -0
  32. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/perplexity/middleware.py +4 -1
  33. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/perplexity/perplexity_sdk.py +4 -1
  34. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/perplexity/trace_fields.py +3 -0
  35. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_python_sdk.egg-info/PKG-INFO +108 -4
  36. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_python_sdk.egg-info/SOURCES.txt +3 -0
  37. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/tests/test_metering.py +30 -0
  38. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/LICENSE +0 -0
  39. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_core/__init__.py +0 -0
  40. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_core/decorators.py +0 -0
  41. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_core/enforcement.py +0 -0
  42. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_core/log_sanitize.py +0 -0
  43. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_core/metering.py +0 -0
  44. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_core/metering_buffer.py +0 -0
  45. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_core/metering_submission.py +0 -0
  46. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_core/patch_registry.py +0 -0
  47. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_core/prompt_extraction.py +0 -0
  48. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_core/subscriber.py +0 -0
  49. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/LICENSE +0 -0
  50. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/__init__.py +0 -0
  51. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_base_client.py +0 -0
  52. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_client.py +0 -0
  53. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_compat.py +0 -0
  54. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_constants.py +0 -0
  55. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_exceptions.py +0 -0
  56. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_files.py +0 -0
  57. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_models.py +0 -0
  58. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_qs.py +0 -0
  59. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_resource.py +0 -0
  60. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_response.py +0 -0
  61. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_streaming.py +0 -0
  62. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_types.py +0 -0
  63. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_utils/__init__.py +0 -0
  64. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_utils/_logs.py +0 -0
  65. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_utils/_proxy.py +0 -0
  66. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_utils/_reflection.py +0 -0
  67. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_utils/_resources_proxy.py +0 -0
  68. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_utils/_streams.py +0 -0
  69. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_utils/_sync.py +0 -0
  70. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_utils/_transform.py +0 -0
  71. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_utils/_typing.py +0 -0
  72. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_utils/_utils.py +0 -0
  73. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/_version.py +0 -0
  74. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/context.py +0 -0
  75. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/decorator.py +0 -0
  76. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/py.typed +0 -0
  77. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/resources/__init__.py +0 -0
  78. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/resources/apis.py +0 -0
  79. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/resources/events.py +0 -0
  80. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/types/__init__.py +0 -0
  81. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/types/ai_create_audio_params.py +0 -0
  82. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/types/ai_create_image_params.py +0 -0
  83. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/types/ai_create_video_params.py +0 -0
  84. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/types/api_meter_request_params.py +0 -0
  85. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/types/api_meter_response_params.py +0 -0
  86. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/types/event_create_params.py +0 -0
  87. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/_metering/types/metering_response_resource.py +0 -0
  88. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/anthropic/__init__.py +0 -0
  89. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/anthropic/bedrock_adapter.py +0 -0
  90. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/anthropic/config.py +0 -0
  91. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/anthropic/prompt_extractor.py +0 -0
  92. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/anthropic/provider.py +0 -0
  93. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/anthropic/stream_create.py +0 -0
  94. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/fal/__init__.py +0 -0
  95. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/fal/config.py +0 -0
  96. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/fal/middleware.py +0 -0
  97. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/google/__init__.py +0 -0
  98. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/google/common/__init__.py +0 -0
  99. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/google/common/exceptions.py +0 -0
  100. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/google/common/protocols.py +0 -0
  101. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/google/common/types.py +0 -0
  102. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/google/config.py +0 -0
  103. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/google/google_ai/__init__.py +0 -0
  104. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/google/google_ai/middleware.py +0 -0
  105. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/google/google_ai/provider.py +0 -0
  106. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/google/prompt_extractor.py +0 -0
  107. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/google/vertex_ai/__init__.py +0 -0
  108. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/google/vertex_ai/middleware.py +0 -0
  109. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/google/vertex_ai/provider.py +0 -0
  110. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/litellm/__init__.py +0 -0
  111. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/litellm/client/config.py +0 -0
  112. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/litellm/client/context.py +0 -0
  113. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/litellm/client/hooks.py +0 -0
  114. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/litellm/client/integrations/__init__.py +0 -0
  115. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/litellm/client/validation.py +0 -0
  116. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/litellm/proxy/__init__.py +0 -0
  117. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/litellm/proxy/middleware.py +0 -0
  118. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/ollama/__init__.py +0 -0
  119. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/openai/__init__.py +0 -0
  120. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/openai/azure_config.py +0 -0
  121. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/openai/azure_model_resolver.py +0 -0
  122. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/openai/config.py +0 -0
  123. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/openai/exceptions.py +0 -0
  124. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/openai/langchain/__init__.py +0 -0
  125. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/openai/langchain/_utils.py +0 -0
  126. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/openai/langchain/unified_handler.py +0 -0
  127. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/openai/prompt_extractor.py +0 -0
  128. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/openai/provider.py +0 -0
  129. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/perplexity/__init__.py +0 -0
  130. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/perplexity/provider.py +0 -0
  131. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/webhooks/__init__.py +0 -0
  132. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_middleware/webhooks/_verify.py +0 -0
  133. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_python_sdk.egg-info/dependency_links.txt +0 -0
  134. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_python_sdk.egg-info/requires.txt +0 -0
  135. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/revenium_python_sdk.egg-info/top_level.txt +0 -0
  136. {revenium_python_sdk-0.3.0 → revenium_python_sdk-0.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: revenium-python-sdk
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: The official Revenium Python SDK — unified AI metering middleware for OpenAI, Anthropic, Google, Ollama, LiteLLM, Perplexity, and fal.ai.
5
5
  Author-email: Revenium <support@revenium.io>
6
6
  License: MIT
@@ -212,6 +212,70 @@ print(response.choices[0].message.content)
212
212
 
213
213
  Emit per-agent terminal outcomes (`CONVERTED`, `DEFLECTED`, `ESCALATED`) alongside completion and tool-event records, so dashboards show business value next to AI cost.
214
214
 
215
+ > **You need a write-scope key (`rev_sk_`) to use the agentic outcomes API.** Metering keys (`rev_mk_`) can only meter completions and tool events — they cannot report, amend, or read job outcomes, and the SDK rejects them client-side before any HTTP request is made. Key resolution: explicit `api_key=` > `REVENIUM_OUTCOME_API_KEY` > `REVENIUM_METERING_API_KEY`.
216
+
217
+ ### JobContext
218
+
219
+ `JobContext` is the recommended high-level API: every AI call made inside the block is automatically metered against the job (all provider middlewares pick the job fields up from context), and the job's business outcome is reported when the work is done.
220
+
221
+ ```python
222
+ from revenium_middleware import JobContext
223
+
224
+ with JobContext("loan-app-12345", type="loan_processing", version="2.1") as job:
225
+ response = client.chat.completions.create(...) # metered against the job automatically
226
+ job.report_outcome(
227
+ execution_status="SUCCESS", # SUCCESS | FAILED | CANCELLED
228
+ outcome_type="CONVERTED",
229
+ outcome_value=500.0,
230
+ outcome_currency="USD",
231
+ )
232
+ ```
233
+
234
+ - **Async:** `async with JobContext(...) as job:` works identically.
235
+ - **Auto-FAILED:** if an unhandled exception escapes the block before an outcome was reported, the context automatically reports `execution_status="FAILED"` (error message and class in metadata) and always re-raises the original exception.
236
+ - **Blocking:** outcome calls are synchronous HTTP requests with retries; tune how long they may block with the `retry_attempts`, `retry_initial_seconds`, and `retry_max_seconds` arguments, accepted by the `JobContext` constructor, `JobContext.attach()`, `get_outcome_history()`, and the CrewAI wrapper's `report_job_outcome`/`amend_job_outcome`.
237
+ - **Team resolution:** explicit `team_id=` > `REVENIUM_TEAM_ID` > automatic resolution from the API key; `OutcomeReportingError` is raised if none of these yields a team.
238
+ - **Nesting:** a nested `JobContext` is a different job (replace, not merge); exiting the inner context restores the outer job's fields.
239
+
240
+ To tag AI calls with job fields without a context manager, use per-call `usage_metadata={"agentic_job_id": ...}`, the `@track_job` decorator (LiteLLM), or the process-wide `REVENIUM_AGENTIC_JOB_*` environment variables — see [Optional Environment Variables](#optional-environment-variables).
241
+
242
+ ### Amending an Outcome
243
+
244
+ Outcomes are amendable: when the business result changes after the fact, amend the recorded outcome instead of re-reporting it. `JobContext.attach()` returns a lightweight handle to an existing job — it is not entered as a context manager and does not touch AI-call scoping — so amendments work from a different process than the one that ran the job.
245
+
246
+ ```python
247
+ from revenium_middleware import JobContext, get_outcome_history
248
+
249
+ # Two weeks after the agent converted the lead at $500,
250
+ # the customer expands to the annual plan.
251
+ job = JobContext.attach("sales-lead-8842")
252
+ job.amend_outcome(
253
+ reason="Customer expanded to the annual plan after the initial conversion",
254
+ outcome_value=750.0,
255
+ )
256
+ job.close()
257
+
258
+ history = get_outcome_history("sales-lead-8842")
259
+ # List[JobOutcomeAmendment], ordered by amendment_sequence (1 = the initial report)
260
+ ```
261
+
262
+ `amend_outcome()` takes a mandatory non-blank `reason` plus the same optional fields as `report_outcome()` (`execution_status`, `outcome_type`, `outcome_value`, `outcome_currency`, `metadata`, `reported_by`), and returns the updated job as a dict.
263
+
264
+ ### Outcome Exceptions
265
+
266
+ All outcome exceptions are importable from `revenium_middleware` and share the `OutcomeReportingError` base, so `except OutcomeReportingError:` catches the whole family:
267
+
268
+ | Exception | Raised when | What to do |
269
+ |-----------|-------------|------------|
270
+ | `OutcomeReportingError` | Base class — configuration failures (no API key available, unresolvable `team_id`) | Fix the key / team configuration |
271
+ | `OutcomeAlreadyReportedError` | Re-reporting a job that already has an outcome (backend 409) | Amend with `amend_outcome()` instead; the exception carries `reported_at` and `amendment_count` |
272
+ | `OutcomeNotReportedError` | Amending a job that has no outcome yet (backend 422) | Call `report_outcome()` first |
273
+ | `OutcomeAmendConflictError` | A concurrent amendment changed the outcome (backend 409, optimistic lock) | Refetch with `get_outcome_history()` and retry — the SDK does not auto-retry |
274
+
275
+ ### Low-Level Client
276
+
277
+ For manual control over every metric (one `emit_completion` per LLM call, one `emit_tool_event` per tool/step), use `AgenticOutcomeClient` directly:
278
+
215
279
  ```python
216
280
  from revenium_middleware.agentic_outcomes import AgenticOutcomeClient, AgenticOutcomeSettings
217
281
 
@@ -678,8 +742,9 @@ LiteLLM provides additional tracking decorators beyond the standard `@revenium_m
678
742
  | `@track_product()` | Track product-specific usage |
679
743
  | `@track_subscriber()` | Identify end users |
680
744
  | `@track_quality()` | Track response quality scores |
745
+ | `@track_job()` | Inject agentic job fields for cost/ROI correlation, e.g. `@track_job(job_id="loan-app-12345", type="loan_processing")` |
681
746
 
682
- All decorators support static values, extraction from function arguments (`name_from_arg`), or extraction from object attributes (`name_from_attr`).
747
+ The tracking decorators above support static values, extraction from function arguments (`name_from_arg`), or extraction from object attributes (`name_from_attr`); `@track_job` supports static values and argument extraction (`job_id_from_arg`, `type_from_arg`) but has no attribute variant.
683
748
 
684
749
  #### CrewAI Integration
685
750
 
@@ -689,6 +754,35 @@ pip install "revenium-python-sdk[litellm]" crewai
689
754
 
690
755
  Pre-built wrapper for tracking CrewAI agent executions. **Note:** CrewAI requires Python 3.12 or earlier.
691
756
 
757
+ **Job outcome tracking:** pass the `agentic_job_*` kwargs to tie every LLM call in the crew to one agentic job, then report (or later amend) the job's business outcome. Requires a write-scope key (`rev_sk_`) — see [Agentic Outcomes](#agentic-outcomes-outcome-based-metering).
758
+
759
+ ```python
760
+ from revenium_middleware.litellm.client.integrations.crewai import ReveniumCrewWrapper
761
+
762
+ crew = ReveniumCrewWrapper(
763
+ agents=[support_agent],
764
+ tasks=[triage_task],
765
+ organization_id="AcmeCorp",
766
+ subscription_id="82764738",
767
+ product_id="Platinum",
768
+ agentic_job_id="support-ticket-456",
769
+ agentic_job_name="Support Ticket Triage",
770
+ agentic_job_type="customer_support",
771
+ agentic_job_version="2.0",
772
+ )
773
+ result = crew.kickoff()
774
+
775
+ crew.report_job_outcome(
776
+ execution_status="SUCCESS",
777
+ outcome_type="DEFLECTED",
778
+ outcome_value=25.0,
779
+ )
780
+
781
+ # Later, if the business result changes:
782
+ # crew.amend_job_outcome(reason="Ticket reopened and escalated to a human agent",
783
+ # outcome_type="ESCALATED", outcome_value=0.0)
784
+ ```
785
+
692
786
  **LiteLLM environment variables:**
693
787
  - `LITELLM_PROXY_URL` - Your LiteLLM proxy URL
694
788
  - `LITELLM_API_KEY` - Your LiteLLM proxy API key
@@ -931,6 +1025,7 @@ Enhanced observability fields for tracking AI operations across environments, re
931
1025
  | `parent_transaction_id` | `REVENIUM_PARENT_TRANSACTION_ID` | Parent transaction ID | Link child operations to parents across microservices |
932
1026
  | `transaction_name` | `REVENIUM_TRANSACTION_NAME` | Human-friendly operation name | Label operations (e.g., `"Generate Response"`, `"Analyze Sentiment"`) |
933
1027
  | `retry_number` | `REVENIUM_RETRY_NUMBER` | Retry attempt number (0 = first attempt) | Track retry attempts for failed operations |
1028
+ | `ticket_id` | `REVENIUM_TICKET_ID` | External ticket or issue ID (e.g., Jira, Linear) (max 256 chars) | Attribute AI costs to individual tickets or issues |
934
1029
 
935
1030
  **Note:** `operation_type` (e.g., `CHAT`, `EMBED`, `TOOL_CALL`) and `operation_subtype` (e.g., `function_call`, `streaming`) are automatically detected by the middleware and cannot be overridden.
936
1031
 
@@ -958,7 +1053,8 @@ response = client.chat.completions.create(
958
1053
  "trace_type": "customer-support",
959
1054
  "trace_name": "Support Chat Session",
960
1055
  "transaction_name": "Generate Response",
961
- "parent_transaction_id": "parent-txn-123"
1056
+ "parent_transaction_id": "parent-txn-123",
1057
+ "ticket_id": "JIRA-123"
962
1058
  }
963
1059
  )
964
1060
  ```
@@ -1362,7 +1458,7 @@ print(get_buffer_stats())
1362
1458
  | `REVENIUM_LOG_LEVEL` | `INFO` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
1363
1459
  | `REVENIUM_CAPTURE_PROMPTS` | `false` | Enable prompt capture |
1364
1460
  | `REVENIUM_SELECTIVE_METERING` | `false` | Only meter `@revenium_meter` decorated functions |
1365
- | `REVENIUM_TEAM_ID` | - | Team ID for cost lookups |
1461
+ | `REVENIUM_TEAM_ID` | - | Team ID for cost lookups and outcome reporting (JobContext team resolution) |
1366
1462
  | `REVENIUM_ENVIRONMENT` | - | Deployment environment (auto-detects from `ENVIRONMENT`, `DEPLOYMENT_ENV`) |
1367
1463
  | `REVENIUM_REGION` | - | Cloud region (auto-detects from `AWS_REGION`, `AZURE_REGION`, `GCP_REGION`) |
1368
1464
  | `REVENIUM_CREDENTIAL_ALIAS` | - | Human-readable API key name |
@@ -1371,10 +1467,18 @@ print(get_buffer_stats())
1371
1467
  | `REVENIUM_PARENT_TRANSACTION_ID` | - | Parent transaction ID for distributed tracing |
1372
1468
  | `REVENIUM_TRANSACTION_NAME` | - | Human-friendly operation name |
1373
1469
  | `REVENIUM_RETRY_NUMBER` | - | Retry attempt number |
1470
+ | `REVENIUM_AGENTIC_JOB_ID` | - | Agentic job instance ID attached to all completions in the process (triggers backend job auto-creation) |
1471
+ | `REVENIUM_AGENTIC_JOB_NAME` | - | Human-readable agentic job name |
1472
+ | `REVENIUM_AGENTIC_JOB_TYPE` | - | Agentic job type category |
1473
+ | `REVENIUM_AGENTIC_JOB_VERSION` | - | Agentic job version |
1474
+ | `REVENIUM_OUTCOME_API_KEY` | - | Write-scope key (`rev_sk_`) for the agentic outcomes API (report/amend/history); falls back to `REVENIUM_METERING_API_KEY` |
1475
+ | `REVENIUM_PROFITSTREAM_BASE_URL` | `https://api.revenium.io` | Agentic outcomes API base URL |
1374
1476
  | `REVENIUM_BEDROCK_DISABLE` | - | Set to `1` to disable Bedrock auto-detection |
1375
1477
  | `REVENIUM_BUFFER_MAX_SIZE` | `1000` | Store-and-forward buffer capacity (oldest events evicted when full) |
1376
1478
  | `REVENIUM_BUFFER_FLUSH_INTERVAL` | `30` | Seconds between automatic replay attempts for buffered events |
1377
1479
 
1480
+ Per-call `usage_metadata` values take precedence over the `REVENIUM_AGENTIC_JOB_*` environment variables, and the LiteLLM proxy path sources job fields from `x-revenium-*` headers only — these process-level env fallbacks do not apply to proxied traffic.
1481
+
1378
1482
  ### Provider-Specific Environment Variables
1379
1483
 
1380
1484
  | Variable | Provider | Description |
@@ -119,6 +119,70 @@ print(response.choices[0].message.content)
119
119
 
120
120
  Emit per-agent terminal outcomes (`CONVERTED`, `DEFLECTED`, `ESCALATED`) alongside completion and tool-event records, so dashboards show business value next to AI cost.
121
121
 
122
+ > **You need a write-scope key (`rev_sk_`) to use the agentic outcomes API.** Metering keys (`rev_mk_`) can only meter completions and tool events — they cannot report, amend, or read job outcomes, and the SDK rejects them client-side before any HTTP request is made. Key resolution: explicit `api_key=` > `REVENIUM_OUTCOME_API_KEY` > `REVENIUM_METERING_API_KEY`.
123
+
124
+ ### JobContext
125
+
126
+ `JobContext` is the recommended high-level API: every AI call made inside the block is automatically metered against the job (all provider middlewares pick the job fields up from context), and the job's business outcome is reported when the work is done.
127
+
128
+ ```python
129
+ from revenium_middleware import JobContext
130
+
131
+ with JobContext("loan-app-12345", type="loan_processing", version="2.1") as job:
132
+ response = client.chat.completions.create(...) # metered against the job automatically
133
+ job.report_outcome(
134
+ execution_status="SUCCESS", # SUCCESS | FAILED | CANCELLED
135
+ outcome_type="CONVERTED",
136
+ outcome_value=500.0,
137
+ outcome_currency="USD",
138
+ )
139
+ ```
140
+
141
+ - **Async:** `async with JobContext(...) as job:` works identically.
142
+ - **Auto-FAILED:** if an unhandled exception escapes the block before an outcome was reported, the context automatically reports `execution_status="FAILED"` (error message and class in metadata) and always re-raises the original exception.
143
+ - **Blocking:** outcome calls are synchronous HTTP requests with retries; tune how long they may block with the `retry_attempts`, `retry_initial_seconds`, and `retry_max_seconds` arguments, accepted by the `JobContext` constructor, `JobContext.attach()`, `get_outcome_history()`, and the CrewAI wrapper's `report_job_outcome`/`amend_job_outcome`.
144
+ - **Team resolution:** explicit `team_id=` > `REVENIUM_TEAM_ID` > automatic resolution from the API key; `OutcomeReportingError` is raised if none of these yields a team.
145
+ - **Nesting:** a nested `JobContext` is a different job (replace, not merge); exiting the inner context restores the outer job's fields.
146
+
147
+ To tag AI calls with job fields without a context manager, use per-call `usage_metadata={"agentic_job_id": ...}`, the `@track_job` decorator (LiteLLM), or the process-wide `REVENIUM_AGENTIC_JOB_*` environment variables — see [Optional Environment Variables](#optional-environment-variables).
148
+
149
+ ### Amending an Outcome
150
+
151
+ Outcomes are amendable: when the business result changes after the fact, amend the recorded outcome instead of re-reporting it. `JobContext.attach()` returns a lightweight handle to an existing job — it is not entered as a context manager and does not touch AI-call scoping — so amendments work from a different process than the one that ran the job.
152
+
153
+ ```python
154
+ from revenium_middleware import JobContext, get_outcome_history
155
+
156
+ # Two weeks after the agent converted the lead at $500,
157
+ # the customer expands to the annual plan.
158
+ job = JobContext.attach("sales-lead-8842")
159
+ job.amend_outcome(
160
+ reason="Customer expanded to the annual plan after the initial conversion",
161
+ outcome_value=750.0,
162
+ )
163
+ job.close()
164
+
165
+ history = get_outcome_history("sales-lead-8842")
166
+ # List[JobOutcomeAmendment], ordered by amendment_sequence (1 = the initial report)
167
+ ```
168
+
169
+ `amend_outcome()` takes a mandatory non-blank `reason` plus the same optional fields as `report_outcome()` (`execution_status`, `outcome_type`, `outcome_value`, `outcome_currency`, `metadata`, `reported_by`), and returns the updated job as a dict.
170
+
171
+ ### Outcome Exceptions
172
+
173
+ All outcome exceptions are importable from `revenium_middleware` and share the `OutcomeReportingError` base, so `except OutcomeReportingError:` catches the whole family:
174
+
175
+ | Exception | Raised when | What to do |
176
+ |-----------|-------------|------------|
177
+ | `OutcomeReportingError` | Base class — configuration failures (no API key available, unresolvable `team_id`) | Fix the key / team configuration |
178
+ | `OutcomeAlreadyReportedError` | Re-reporting a job that already has an outcome (backend 409) | Amend with `amend_outcome()` instead; the exception carries `reported_at` and `amendment_count` |
179
+ | `OutcomeNotReportedError` | Amending a job that has no outcome yet (backend 422) | Call `report_outcome()` first |
180
+ | `OutcomeAmendConflictError` | A concurrent amendment changed the outcome (backend 409, optimistic lock) | Refetch with `get_outcome_history()` and retry — the SDK does not auto-retry |
181
+
182
+ ### Low-Level Client
183
+
184
+ For manual control over every metric (one `emit_completion` per LLM call, one `emit_tool_event` per tool/step), use `AgenticOutcomeClient` directly:
185
+
122
186
  ```python
123
187
  from revenium_middleware.agentic_outcomes import AgenticOutcomeClient, AgenticOutcomeSettings
124
188
 
@@ -585,8 +649,9 @@ LiteLLM provides additional tracking decorators beyond the standard `@revenium_m
585
649
  | `@track_product()` | Track product-specific usage |
586
650
  | `@track_subscriber()` | Identify end users |
587
651
  | `@track_quality()` | Track response quality scores |
652
+ | `@track_job()` | Inject agentic job fields for cost/ROI correlation, e.g. `@track_job(job_id="loan-app-12345", type="loan_processing")` |
588
653
 
589
- All decorators support static values, extraction from function arguments (`name_from_arg`), or extraction from object attributes (`name_from_attr`).
654
+ The tracking decorators above support static values, extraction from function arguments (`name_from_arg`), or extraction from object attributes (`name_from_attr`); `@track_job` supports static values and argument extraction (`job_id_from_arg`, `type_from_arg`) but has no attribute variant.
590
655
 
591
656
  #### CrewAI Integration
592
657
 
@@ -596,6 +661,35 @@ pip install "revenium-python-sdk[litellm]" crewai
596
661
 
597
662
  Pre-built wrapper for tracking CrewAI agent executions. **Note:** CrewAI requires Python 3.12 or earlier.
598
663
 
664
+ **Job outcome tracking:** pass the `agentic_job_*` kwargs to tie every LLM call in the crew to one agentic job, then report (or later amend) the job's business outcome. Requires a write-scope key (`rev_sk_`) — see [Agentic Outcomes](#agentic-outcomes-outcome-based-metering).
665
+
666
+ ```python
667
+ from revenium_middleware.litellm.client.integrations.crewai import ReveniumCrewWrapper
668
+
669
+ crew = ReveniumCrewWrapper(
670
+ agents=[support_agent],
671
+ tasks=[triage_task],
672
+ organization_id="AcmeCorp",
673
+ subscription_id="82764738",
674
+ product_id="Platinum",
675
+ agentic_job_id="support-ticket-456",
676
+ agentic_job_name="Support Ticket Triage",
677
+ agentic_job_type="customer_support",
678
+ agentic_job_version="2.0",
679
+ )
680
+ result = crew.kickoff()
681
+
682
+ crew.report_job_outcome(
683
+ execution_status="SUCCESS",
684
+ outcome_type="DEFLECTED",
685
+ outcome_value=25.0,
686
+ )
687
+
688
+ # Later, if the business result changes:
689
+ # crew.amend_job_outcome(reason="Ticket reopened and escalated to a human agent",
690
+ # outcome_type="ESCALATED", outcome_value=0.0)
691
+ ```
692
+
599
693
  **LiteLLM environment variables:**
600
694
  - `LITELLM_PROXY_URL` - Your LiteLLM proxy URL
601
695
  - `LITELLM_API_KEY` - Your LiteLLM proxy API key
@@ -838,6 +932,7 @@ Enhanced observability fields for tracking AI operations across environments, re
838
932
  | `parent_transaction_id` | `REVENIUM_PARENT_TRANSACTION_ID` | Parent transaction ID | Link child operations to parents across microservices |
839
933
  | `transaction_name` | `REVENIUM_TRANSACTION_NAME` | Human-friendly operation name | Label operations (e.g., `"Generate Response"`, `"Analyze Sentiment"`) |
840
934
  | `retry_number` | `REVENIUM_RETRY_NUMBER` | Retry attempt number (0 = first attempt) | Track retry attempts for failed operations |
935
+ | `ticket_id` | `REVENIUM_TICKET_ID` | External ticket or issue ID (e.g., Jira, Linear) (max 256 chars) | Attribute AI costs to individual tickets or issues |
841
936
 
842
937
  **Note:** `operation_type` (e.g., `CHAT`, `EMBED`, `TOOL_CALL`) and `operation_subtype` (e.g., `function_call`, `streaming`) are automatically detected by the middleware and cannot be overridden.
843
938
 
@@ -865,7 +960,8 @@ response = client.chat.completions.create(
865
960
  "trace_type": "customer-support",
866
961
  "trace_name": "Support Chat Session",
867
962
  "transaction_name": "Generate Response",
868
- "parent_transaction_id": "parent-txn-123"
963
+ "parent_transaction_id": "parent-txn-123",
964
+ "ticket_id": "JIRA-123"
869
965
  }
870
966
  )
871
967
  ```
@@ -1269,7 +1365,7 @@ print(get_buffer_stats())
1269
1365
  | `REVENIUM_LOG_LEVEL` | `INFO` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
1270
1366
  | `REVENIUM_CAPTURE_PROMPTS` | `false` | Enable prompt capture |
1271
1367
  | `REVENIUM_SELECTIVE_METERING` | `false` | Only meter `@revenium_meter` decorated functions |
1272
- | `REVENIUM_TEAM_ID` | - | Team ID for cost lookups |
1368
+ | `REVENIUM_TEAM_ID` | - | Team ID for cost lookups and outcome reporting (JobContext team resolution) |
1273
1369
  | `REVENIUM_ENVIRONMENT` | - | Deployment environment (auto-detects from `ENVIRONMENT`, `DEPLOYMENT_ENV`) |
1274
1370
  | `REVENIUM_REGION` | - | Cloud region (auto-detects from `AWS_REGION`, `AZURE_REGION`, `GCP_REGION`) |
1275
1371
  | `REVENIUM_CREDENTIAL_ALIAS` | - | Human-readable API key name |
@@ -1278,10 +1374,18 @@ print(get_buffer_stats())
1278
1374
  | `REVENIUM_PARENT_TRANSACTION_ID` | - | Parent transaction ID for distributed tracing |
1279
1375
  | `REVENIUM_TRANSACTION_NAME` | - | Human-friendly operation name |
1280
1376
  | `REVENIUM_RETRY_NUMBER` | - | Retry attempt number |
1377
+ | `REVENIUM_AGENTIC_JOB_ID` | - | Agentic job instance ID attached to all completions in the process (triggers backend job auto-creation) |
1378
+ | `REVENIUM_AGENTIC_JOB_NAME` | - | Human-readable agentic job name |
1379
+ | `REVENIUM_AGENTIC_JOB_TYPE` | - | Agentic job type category |
1380
+ | `REVENIUM_AGENTIC_JOB_VERSION` | - | Agentic job version |
1381
+ | `REVENIUM_OUTCOME_API_KEY` | - | Write-scope key (`rev_sk_`) for the agentic outcomes API (report/amend/history); falls back to `REVENIUM_METERING_API_KEY` |
1382
+ | `REVENIUM_PROFITSTREAM_BASE_URL` | `https://api.revenium.io` | Agentic outcomes API base URL |
1281
1383
  | `REVENIUM_BEDROCK_DISABLE` | - | Set to `1` to disable Bedrock auto-detection |
1282
1384
  | `REVENIUM_BUFFER_MAX_SIZE` | `1000` | Store-and-forward buffer capacity (oldest events evicted when full) |
1283
1385
  | `REVENIUM_BUFFER_FLUSH_INTERVAL` | `30` | Seconds between automatic replay attempts for buffered events |
1284
1386
 
1387
+ Per-call `usage_metadata` values take precedence over the `REVENIUM_AGENTIC_JOB_*` environment variables, and the LiteLLM proxy path sources job fields from `x-revenium-*` headers only — these process-level env fallbacks do not apply to proxied traffic.
1388
+
1285
1389
  ### Provider-Specific Environment Variables
1286
1390
 
1287
1391
  | Variable | Provider | Description |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "revenium-python-sdk"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "The official Revenium Python SDK — unified AI metering middleware for OpenAI, Anthropic, Google, Ollama, LiteLLM, Perplexity, and fal.ai."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -166,6 +166,16 @@ from ._metering import meter_tool, report_tool_call, configure # noqa: E402
166
166
  # Agentic-outcome client (used by examples/agentic_outcomes/ pack)
167
167
  from .agentic_outcomes import AgenticOutcomeClient, AgenticOutcomeSettings # noqa: E402
168
168
 
169
+ # Public job-context surface (BACK-777)
170
+ from .job_context import JobContext # noqa: E402
171
+ from .job_history import JobOutcomeAmendment, get_outcome_history # noqa: E402
172
+ from ._core.exceptions import ( # noqa: E402
173
+ OutcomeAlreadyReportedError,
174
+ OutcomeAmendConflictError,
175
+ OutcomeNotReportedError,
176
+ OutcomeReportingError,
177
+ )
178
+
169
179
  __all__ = [
170
180
  # Metering exports
171
181
  "client",
@@ -199,4 +209,13 @@ __all__ = [
199
209
  # Agentic-outcome exports
200
210
  "AgenticOutcomeClient",
201
211
  "AgenticOutcomeSettings",
212
+ # Job-context exports (BACK-777)
213
+ "JobContext",
214
+ "OutcomeReportingError",
215
+ "OutcomeAlreadyReportedError",
216
+ # Outcome amendment / history exports (BACK-777 Phase 3)
217
+ "get_outcome_history",
218
+ "JobOutcomeAmendment",
219
+ "OutcomeNotReportedError",
220
+ "OutcomeAmendConflictError",
202
221
  ]
@@ -56,6 +56,15 @@ class Config:
56
56
  ENV_REVENIUM_PARENT_TRANSACTION_ID: str = "REVENIUM_PARENT_TRANSACTION_ID"
57
57
  ENV_REVENIUM_TRANSACTION_NAME: str = "REVENIUM_TRANSACTION_NAME"
58
58
  ENV_REVENIUM_RETRY_NUMBER: str = "REVENIUM_RETRY_NUMBER"
59
+ ENV_REVENIUM_TICKET_ID: str = "REVENIUM_TICKET_ID"
60
+
61
+ # Agentic job field environment variables (BACK-777 fallbacks)
62
+ ENV_REVENIUM_AGENTIC_JOB_ID: str = "REVENIUM_AGENTIC_JOB_ID"
63
+ ENV_REVENIUM_AGENTIC_JOB_NAME: str = "REVENIUM_AGENTIC_JOB_NAME"
64
+ ENV_REVENIUM_AGENTIC_JOB_TYPE: str = "REVENIUM_AGENTIC_JOB_TYPE"
65
+ ENV_REVENIUM_AGENTIC_JOB_VERSION: str = "REVENIUM_AGENTIC_JOB_VERSION"
66
+ ENV_REVENIUM_OUTCOME_API_KEY: str = "REVENIUM_OUTCOME_API_KEY"
67
+ ENV_REVENIUM_PROFITSTREAM_BASE_URL: str = "REVENIUM_PROFITSTREAM_BASE_URL"
59
68
 
60
69
  # Prompt capture settings
61
70
  ENV_REVENIUM_CAPTURE_PROMPTS: str = "REVENIUM_CAPTURE_PROMPTS"
@@ -30,6 +30,12 @@ _idempotency_key_context: contextvars.ContextVar[Optional[str]] = contextvars.Co
30
30
  'revenium_idempotency_key', default=None
31
31
  )
32
32
 
33
+ # Context variable holding agentic job fields (wire-name keys) for the current scope.
34
+ # This is the seam the public JobContext (BACK-777 Phase 2) builds on.
35
+ _agentic_job_context: contextvars.ContextVar[Optional[Dict[str, Any]]] = contextvars.ContextVar(
36
+ 'revenium_agentic_job', default=None
37
+ )
38
+
33
39
 
34
40
  def is_inside_decorated_function() -> bool:
35
41
  """
@@ -172,3 +178,41 @@ def idempotency_key(key: str) -> Iterator[None]:
172
178
  finally:
173
179
  _idempotency_key_context.reset(token)
174
180
 
181
+
182
+ def get_agentic_job_fields() -> Optional[Dict[str, Any]]:
183
+ """Return the agentic job fields set on the current context, or None.
184
+
185
+ Keys are wire names (``agenticJobId``, ``agenticJobName``, ``agenticJobType``,
186
+ ``agenticJobVersion``) — the same keys ``extract_agentic_job_fields`` emits,
187
+ so the fallback merge in ``_core/fields.py`` is key-aligned.
188
+ """
189
+ return _agentic_job_context.get()
190
+
191
+
192
+ def set_agentic_job_fields(
193
+ job_id: Optional[str] = None,
194
+ name: Optional[str] = None,
195
+ type: Optional[str] = None,
196
+ version: Optional[str] = None,
197
+ ) -> contextvars.Token:
198
+ """Set agentic job fields on the current context. Returns a Token for reset().
199
+
200
+ Maps snake-case arguments to wire-name keys, omitting Nones. Callers own the
201
+ token and must reset it (``_agentic_job_context.reset(token)``) when the scope
202
+ ends — the JobContext context manager in Phase 2 does this on exit.
203
+
204
+ Raises:
205
+ ValueError: if no field is provided.
206
+ """
207
+ fields: Dict[str, Any] = {}
208
+ if job_id is not None:
209
+ fields["agenticJobId"] = job_id
210
+ if name is not None:
211
+ fields["agenticJobName"] = name
212
+ if type is not None:
213
+ fields["agenticJobType"] = type
214
+ if version is not None:
215
+ fields["agenticJobVersion"] = version
216
+ if not fields:
217
+ raise ValueError("set_agentic_job_fields requires at least one field")
218
+ return _agentic_job_context.set(fields)
@@ -41,3 +41,45 @@ class BudgetExceededError(Exception):
41
41
  # `except ReveniumCostLimitExceeded:` continues to catch the new exception
42
42
  # unchanged. Plan to remove in a future major release.
43
43
  ReveniumCostLimitExceeded = BudgetExceededError
44
+
45
+
46
+ class OutcomeReportingError(Exception):
47
+ """Raised when an agentic job outcome cannot be reported.
48
+
49
+ Base of the outcome exception family (BACK-777). Configuration failures
50
+ (unresolvable team_id, missing API key) raise this class directly;
51
+ backend-state conditions raise the subclasses.
52
+ """
53
+
54
+
55
+ class OutcomeAlreadyReportedError(OutcomeReportingError):
56
+ """The job already has an outcome (backend 409 with amendment guidance).
57
+
58
+ Callers can inspect ``reported_at`` / ``amendment_count`` and decide to
59
+ amend (``amend_outcome``, BACK-777 Phase 3).
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ message: str,
65
+ reported_at: Optional[str] = None,
66
+ amendment_count: Optional[int] = None,
67
+ ):
68
+ super().__init__(message)
69
+ self.reported_at = reported_at
70
+ self.amendment_count = amendment_count
71
+
72
+
73
+ class OutcomeNotReportedError(OutcomeReportingError):
74
+ """Amendment attempted on a job with no outcome yet (backend 422).
75
+
76
+ Report the initial outcome with ``report_outcome`` first.
77
+ """
78
+
79
+
80
+ class OutcomeAmendConflictError(OutcomeReportingError):
81
+ """Concurrent amendment changed the outcome row (backend 409, optimistic lock).
82
+
83
+ Retryable by the caller: refetch the current state (``get_outcome_history``)
84
+ and re-issue the amendment. The SDK does not auto-retry.
85
+ """
@@ -1,8 +1,12 @@
1
1
  import logging
2
+ import os
2
3
  import threading
3
4
  import warnings
4
5
  from typing import Any, Dict, Mapping, Optional, Set, Tuple
5
6
 
7
+ from .config import Config
8
+ from .context import get_agentic_job_fields
9
+
6
10
  logger = logging.getLogger(__name__)
7
11
 
8
12
  # Dedup set for deprecated-alias logger.warning so high-volume callers don't get a flood
@@ -19,6 +23,13 @@ AGENTIC_JOB_FIELD_MAP = {
19
23
  "agenticJobVersion": ("agentic_job_version", "agenticJobVersion"),
20
24
  }
21
25
 
26
+ _AGENTIC_JOB_ENV_MAP = {
27
+ "agenticJobId": Config.ENV_REVENIUM_AGENTIC_JOB_ID,
28
+ "agenticJobName": Config.ENV_REVENIUM_AGENTIC_JOB_NAME,
29
+ "agenticJobType": Config.ENV_REVENIUM_AGENTIC_JOB_TYPE,
30
+ "agenticJobVersion": Config.ENV_REVENIUM_AGENTIC_JOB_VERSION,
31
+ }
32
+
22
33
 
23
34
  def extract_field_with_fallback(
24
35
  source: Mapping[str, Any],
@@ -96,13 +107,25 @@ def extract_common_metadata(source: Mapping[str, Any]) -> Dict[str, Any]:
96
107
 
97
108
 
98
109
  def extract_agentic_job_fields(source: Mapping[str, Any]) -> Dict[str, Any]:
110
+ """Resolve agentic job fields with per-field precedence:
111
+
112
+ explicit source metadata (snake then camel alias) > job contextvar >
113
+ ``REVENIUM_AGENTIC_JOB_*`` env var. Each field resolves independently.
114
+ """
115
+ context_fields = get_agentic_job_fields() or {}
99
116
  result = {}
100
117
  for wire_name, aliases in AGENTIC_JOB_FIELD_MAP.items():
118
+ value = None
101
119
  for alias in aliases:
102
120
  value = source.get(alias)
103
121
  if value is not None:
104
- result[wire_name] = value
105
122
  break
123
+ if value is None:
124
+ value = context_fields.get(wire_name)
125
+ if value is None:
126
+ value = os.getenv(_AGENTIC_JOB_ENV_MAP[wire_name]) or None
127
+ if value is not None:
128
+ result[wire_name] = value
106
129
  return result
107
130
 
108
131