mainsequence 9.0.2.dev32__tar.gz → 9.0.3.dev33__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 (101) hide show
  1. {mainsequence-9.0.2.dev32/mainsequence.egg-info → mainsequence-9.0.3.dev33}/PKG-INFO +1 -1
  2. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/AGENTS.md +3 -0
  3. mainsequence-9.0.3.dev33/agent_scaffold/skills/mainsequence-inference/SKILL.md +198 -0
  4. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/skills/sdk_code_repository_execution/SKILL.md +3 -0
  5. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/api.py +1 -1
  6. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/cli.py +1 -1
  7. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/__init__.py +12 -0
  8. mainsequence-9.0.3.dev33/mainsequence/client/inference.py +184 -0
  9. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33/mainsequence.egg-info}/PKG-INFO +1 -1
  10. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence.egg-info/SOURCES.txt +3 -0
  11. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/pyproject.toml +1 -1
  12. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_cli.py +148 -16
  13. mainsequence-9.0.3.dev33/tests/test_inference_client.py +167 -0
  14. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_scaffold_skills.py +1 -0
  15. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/LICENSE +0 -0
  16. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/README.md +0 -0
  17. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
  18. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  19. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  20. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md +0 -0
  21. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/skills/maintenance/metatables_transition/SKILL.md +0 -0
  22. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  23. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  24. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/__init__.py +0 -0
  25. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/__main__.py +0 -0
  26. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/_request_identity.py +0 -0
  27. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/bootstrap.py +0 -0
  28. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/__init__.py +0 -0
  29. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/browser_auth.py +0 -0
  30. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/code_repository_status.py +0 -0
  31. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/config.py +0 -0
  32. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/docker_utils.py +0 -0
  33. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/doctor.py +0 -0
  34. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/local_ops.py +0 -0
  35. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/model_filters.py +0 -0
  36. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/pydantic_cli.py +0 -0
  37. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/sdk_utils.py +0 -0
  38. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/ssh_utils.py +0 -0
  39. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/ui.py +0 -0
  40. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/agent_runtime_models.py +0 -0
  41. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/base.py +0 -0
  42. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/client.py +0 -0
  43. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/compute_validation.py +0 -0
  44. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/exceptions.py +0 -0
  45. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/github_issues.py +0 -0
  46. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/models_foundry.py +0 -0
  47. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/models_helpers.py +0 -0
  48. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/models_user.py +0 -0
  49. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/observability.py +0 -0
  50. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/utils.py +0 -0
  51. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/value_sets.py +0 -0
  52. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/code_repository_context.py +0 -0
  53. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/code_repository_skills.py +0 -0
  54. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/defaults.py +0 -0
  55. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/instrumentation/__init__.py +0 -0
  56. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/instrumentation/utils.py +0 -0
  57. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/logconf.py +0 -0
  58. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/repository_identity_security.py +0 -0
  59. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/runtime_flags.py +0 -0
  60. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/scaffold_skills.py +0 -0
  61. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/server/__init__.py +0 -0
  62. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/server/caller_assertions.py +0 -0
  63. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/server/fastapi.py +0 -0
  64. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence.egg-info/dependency_links.txt +0 -0
  65. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence.egg-info/entry_points.txt +0 -0
  66. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence.egg-info/requires.txt +0 -0
  67. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence.egg-info/top_level.txt +0 -0
  68. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/setup.cfg +0 -0
  69. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_auth_bootstrap.py +0 -0
  70. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_auth_precedence.py +0 -0
  71. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_caller_assertions.py +0 -0
  72. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_canonical_api_route_contract.py +0 -0
  73. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_cli_auth_contract.py +0 -0
  74. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_cli_browser_auth.py +0 -0
  75. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_cli_secure_storage.py +0 -0
  76. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_client.py +0 -0
  77. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_code_repository_branch_contract.py +0 -0
  78. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_code_repository_documentation_contract.py +0 -0
  79. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_code_repository_runtime_context.py +0 -0
  80. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_code_repository_skills.py +0 -0
  81. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_dependency_extras.py +0 -0
  82. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_dev_release_version.py +0 -0
  83. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_environment_resource_context.py +0 -0
  84. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_environment_response_contracts.py +0 -0
  85. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_fastapi_request_identity.py +0 -0
  86. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_filter_normalization.py +0 -0
  87. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_github_issues_client.py +0 -0
  88. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_independent_context.py +0 -0
  89. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_instrumentation.py +0 -0
  90. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_job_client.py +0 -0
  91. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_local_ops.py +0 -0
  92. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_logconf.py +0 -0
  93. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_models_user_request_bound_auth.py +0 -0
  94. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_notification_uid_contract.py +0 -0
  95. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_observability_client.py +0 -0
  96. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_secret_client_model.py +0 -0
  97. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_source_ownership.py +0 -0
  98. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_ssh_utils.py +0 -0
  99. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_tolerant_response_reading.py +0 -0
  100. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_tolerant_value_sets.py +0 -0
  101. {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_user_application_facts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 9.0.2.dev32
3
+ Version: 9.0.3.dev33
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: MIT
@@ -83,6 +83,9 @@ bundle. Platform skills are installed separately under
83
83
  `.agents/skills/mainsequence/platform_operations/orchestration_and_releases/SKILL.md`
84
84
  - RBAC, sharing, constants, secrets, and access verification:
85
85
  `.agents/skills/mainsequence/platform_operations/access_control_and_sharing/SKILL.md`
86
+ - Direct recorded model calls, structured extraction, thinking/provider options,
87
+ inference history and idempotent replay:
88
+ `.agents/skills/mainsequence/mainsequence-inference/SKILL.md`
86
89
  - A2A session discovery, messages, files, and SDK response handling:
87
90
  `.agents/skills/mainsequence/a2a_sdk_execution/SKILL.md`
88
91
  - Turning a CodeRepository into a platform coding agent or selecting other
@@ -0,0 +1,198 @@
1
+ ---
2
+ name: mainsequence-inference
3
+ description: Use the Main Sequence Python SDK for direct recorded model completions and structured extraction, with thinking levels, provider-native options, conversation history and safe replay. Use for model calls that do not need an Agent or A2A session.
4
+ ---
5
+
6
+ # Main Sequence Recorded Inference
7
+
8
+ Use `mainsequence.client.InferenceClient` to call the recorded inference endpoint.
9
+ Main Sequence authenticates the caller, uses existing stored provider credentials,
10
+ and records input/output. No Agent, AgentSession, Tau runtime or application-side
11
+ provider key is required. This skill teaches the SDK client; the platform owns
12
+ provider availability, permissions and execution behavior.
13
+
14
+ ## Establish the client and model
15
+
16
+ Use the installed `mainsequence.client.inference` implementation and its matching
17
+ [inference documentation](https://mainsequence-sdk.github.io/mainsequence-sdk/knowledge/infrastructure/inference/).
18
+ If the installed SDK lacks `InferenceClient`, report the missing capability. Do
19
+ not silently upgrade the SDK, restore retired `Agent.respond` calls, or replace
20
+ recorded inference with direct provider HTTP calls.
21
+
22
+ Reuse existing Main Sequence authentication and the current provider catalog.
23
+ Select an exact available provider/model with configured credentials; examples
24
+ below illustrate request shapes, not a guarantee that an account can use a model.
25
+ There is no inference CLI command; use the Python client.
26
+
27
+ For local CodeRepository code using human credentials, resolve the Environment
28
+ from the current Git checkout through the existing SDK context:
29
+
30
+ ```python
31
+ from mainsequence.client import InferenceClient
32
+ from mainsequence.code_repository_context import get_code_repository_context
33
+
34
+ context = get_code_repository_context()
35
+ if context.organization_environment_uid is None:
36
+ raise RuntimeError("This checkout has no resolved Organization Environment")
37
+ client = InferenceClient(
38
+ organization_environment_uid=context.organization_environment_uid,
39
+ )
40
+ ```
41
+
42
+ For an authenticated deployed runtime, use `InferenceClient()`; the server derives
43
+ its exact Environment. A contradictory selector fails. Outside a CodeRepository,
44
+ human callers provide an authorized Environment UID from their application
45
+ context. Never invent an Environment, use another branch as a fallback, or add
46
+ inference environment variables. Provider secrets remain in existing server-side
47
+ credential storage and must not appear in messages or `provider_options`.
48
+
49
+ ## Construct a completion envelope
50
+
51
+ Every call supplies complete ordered text messages: `system`, `user`, `assistant`.
52
+ Earlier requests are never automatically prepended. An omitted `conversation_uid`
53
+ creates a conversation; an existing UID groups another independent request with
54
+ its own full context. Each new logical call needs its own idempotency key.
55
+
56
+ ```python
57
+ def extract_weight(client, document, *, idempotency_key, conversation_uid=None):
58
+ return client.complete(
59
+ provider="openai",
60
+ model="gpt-5.4", # Replace with the exact available catalog selection.
61
+ messages=[
62
+ {"role": "system", "content": "Extract the fund weight as a fraction."},
63
+ {"role": "user", "content": document},
64
+ ],
65
+ thinking_level="high",
66
+ provider_options={"store": False, "text": {"verbosity": "low"}},
67
+ response_schema={
68
+ "type": "object",
69
+ "properties": {"weight": {"type": "number"}},
70
+ "required": ["weight"],
71
+ "additionalProperties": False,
72
+ },
73
+ metadata={"operation": "fund-weight-extraction"},
74
+ conversation_uid=conversation_uid,
75
+ idempotency_key=idempotency_key,
76
+ )
77
+ ```
78
+
79
+ `response_schema`, `thinking_level`, `provider_options`, `provider_storage`,
80
+ `metadata` and `conversation_uid` are optional. Metadata is recorded content,
81
+ not authorization or a credential selector. Keep sensitive payloads out of logs.
82
+ The current contract accepts bounded text/JSON; it does not execute tools, stream,
83
+ fetch document URLs, or accept file/image attachments. Extract document text first.
84
+
85
+ Only consume `result.output["content"]` when `result.status == "completed"`,
86
+ `result.content_available` is true, and `result.output` is present.
87
+ A validated common `response_schema` returns parsed JSON there; ordinary text
88
+ completions return a string. A replay can return a pending result without output.
89
+ Preserve `conversation_uid` and `request_uid` from `InferenceResult` for inspection.
90
+
91
+ ## Thinking and native provider options
92
+
93
+ `thinking_level` accepts the catalog vocabulary `off`, `minimal`, `low`, `medium`,
94
+ `high`, `xhigh`, but the exact model and adapter must support the requested level.
95
+ Unsupported mappings fail; do not silently lower the level to make a call work.
96
+ Omitting the field preserves native thinking options, or the provider default
97
+ when none are supplied. `off` is an explicit selection, not omission.
98
+
99
+ Use `provider_options` for native JSON body fields the caller knows. Objects,
100
+ arrays, numbers, false and null are preserved. Disjoint nested fields merge;
101
+ colliding explicit common/native controls fail even when their values agree.
102
+ Do not combine `thinking_level` with native thinking selection or
103
+ `provider_storage.store_response` with native `store`. Omit the corresponding
104
+ common convenience when choosing native configuration.
105
+
106
+ | Family | Native `provider_options` example | Meaning |
107
+ | --- | --- | --- |
108
+ | OpenAI Responses | `{"reasoning": {"effort": "high"}, "store": false, "text": {"verbosity": "low"}}` | Native reasoning and storage; Chat Completions uses `reasoning_effort` instead. The catalog chooses the API. |
109
+ | Anthropic Messages | `{"thinking": {"type": "adaptive"}, "output_config": {"effort": "high"}, "max_tokens": 4096}` | For an adaptive model; older models can use their native thinking budget. Do not send an invented OpenAI `store` flag. |
110
+ | Ollama compatible API | `{"reasoning_effort": "high"}` | Select the existing Organization custom-provider identifier and a compatible model/API. Native Ollama `/api/chat` fields are not automatically translated. |
111
+ | OpenRouter Chat | `{"reasoning": {"effort": "high"}, "provider": {"require_parameters": true, "zdr": true}}` | The nested `provider` object is upstream routing, distinct from the top-level catalog selector. ZDR routing is not proof of account-wide zero retention. |
112
+
113
+ These are independent native-only examples: omit `thinking_level` when using
114
+ them. Main Sequence forwards accepted native fields without inventing a normalized
115
+ thinking level or claiming a provider honored an unknown option. Native fields
116
+ cannot override the canonical model/input, credentials, endpoint, headers,
117
+ streaming/background lifecycle, provider continuation, tool execution or alternate
118
+ model selection. Common OpenRouter thinking/schema controls already add
119
+ `provider.require_parameters`; do not also set that path in native options.
120
+
121
+ ## Provider storage versus Main Sequence history
122
+
123
+ **Main Sequence keeps every admitted conversation exchange until explicitly
124
+ deleted.** Provider storage controls never disable this history. Do not add an
125
+ expiry setting, capture opt-in, `record_history=False`, or a new policy registry.
126
+
127
+ - OpenAI: `provider_storage={"store_response": False}` compiles to `store: false`.
128
+ Choose this convenience or native `provider_options={"store": False}`.
129
+ - Anthropic: `store_response=False` means the stateless Messages contract. It
130
+ does not promise that operational logs or caches do not exist.
131
+ - Ollama and OpenRouter: the common storage convenience is not supported as a
132
+ universal per-request guarantee. Use supported native/account controls; do not
133
+ add or drop flags silently. Ollama is configured through existing custom
134
+ providers; `keep_alive` governs model residency, not conversation retention.
135
+ - `provider_storage={"require_zero_data_retention": True}` currently rejects
136
+ before dispatch because verified account eligibility is unavailable. Neither
137
+ OpenAI `store: false` nor OpenRouter `provider.zdr` establishes that guarantee.
138
+
139
+ ## Handle outcomes and replay
140
+
141
+ Create and persist the key with the application's logical request **before** its
142
+ first submission. Keys contain 1–128 letters, digits, `.`, `_`, `:` or `-`. Do not
143
+ put key generation inside a retry loop. Preserve the exact envelope for replay,
144
+ including conversation creation/attachment, model, thinking, native options,
145
+ metadata and schema. Reuse the same User, Environment and authenticated workload;
146
+ idempotency is scoped to that context, not global across unrelated JobRuns.
147
+
148
+ - `InferenceExecutionError` exposes `result` and `status_code`. Retain the
149
+ returned IDs and inspect `client.request(error.result.request_uid)`. Invalid
150
+ structured output, provider failure and unknown outcomes are not successes.
151
+ - `InferenceTransportError` retains `idempotency_key`; the outcome may already
152
+ be recorded or executing. Inspect known request/history IDs, or explicitly
153
+ replay the identical envelope and key. Never substitute a new key after an
154
+ ambiguous result. Lower-level HTTP failures also do not justify a fresh key.
155
+ - `admitted`/`dispatching` means pending; `completed` means persisted success;
156
+ `failed`/`unknown` requires inspection. Replaying unknown work does not execute
157
+ again. A changed envelope with the same key conflicts (409); deleted content
158
+ yields a recorded 410. Do not hide these cases behind an automatic retry loop.
159
+
160
+ The client does not automatically retry network failures. It permits one refresh
161
+ following an explicit authentication 401, preserving body and key. A longer
162
+ client `timeout` does not extend the server's execution deadline. Neither a
163
+ transport timeout nor `unknown` proves the provider stopped processing.
164
+ Missing usage remains unknown; inspect `usage["complete"]` where present instead
165
+ of treating absent token counts as a free call. Native-only structured output
166
+ is not automatically validated by the common `response_schema` validator.
167
+
168
+ ## Read history and delete only when requested
169
+
170
+ ```python
171
+ conversations = client.conversations(limit=25, offset=0)
172
+ conversation = client.conversation(result.conversation_uid)
173
+ request = client.request(result.request_uid)
174
+ history = client.history(result.conversation_uid, limit=25, offset=0)
175
+ insights = client.insights(result.conversation_uid)
176
+ ```
177
+
178
+ Lists/history return `count`, `next`, `previous`, `results`; request retrieval
179
+ returns `InferenceResult`. History records each complete submitted context and its
180
+ output in admission order, not model-managed memory. Insights summarize outcomes,
181
+ submitted messages and known tokens with unknown-usage counts. Read methods never
182
+ invoke the model. The owner and authorized same-Organization administrators can
183
+ inspect content within the admitted Environment; only the owner can append.
184
+
185
+ For an explicit content-deletion request, use
186
+ `client.delete_content(conversation_uid)`. It removes protected inputs, outputs,
187
+ schemas and options, while keeping request identity and non-content outcome/usage
188
+ records. Replay never reconstructs deleted content or runs the provider again,
189
+ even if a response arrives after deletion. Do not schedule automatic deletion.
190
+
191
+ ## Verify an integration
192
+
193
+ Mock the provider-facing boundary or SDK transport for local tests. Check the
194
+ chosen thinking/native envelope, parsed structured result, stable retry identity,
195
+ pending/error handling and history retrieval. Make a live provider call only
196
+ when the task authorizes it and the existing account is ready; a successful call
197
+ does not verify the provider's retention practices. Report mocked versus live
198
+ verification clearly.
@@ -60,6 +60,9 @@ Do not let this skill become a domain manual.
60
60
  `.agents/skills/mainsequence/platform_operations/orchestration_and_releases/SKILL.md`
61
61
  - RBAC and sharing:
62
62
  `.agents/skills/mainsequence/platform_operations/access_control_and_sharing/SKILL.md`
63
+ - direct recorded inference, structured extraction, thinking/provider controls,
64
+ conversation history and safe replay:
65
+ `.agents/skills/mainsequence/mainsequence-inference/SKILL.md`
63
66
  - TAU-based Harness Agent repository integration, local development, project
64
67
  customization, and runtime A2A adaptation: use the version-matched skills in
65
68
  `.agents/skills/ms_tau_sdk/` after the platform-owned skill defines the
@@ -810,7 +810,7 @@ def get_logged_user_details() -> dict[str, Any]:
810
810
  organization.pop("id", None)
811
811
  return user
812
812
  if hasattr(user, "model_dump"):
813
- payload = user.model_dump()
813
+ payload = user.model_dump(mode="json")
814
814
  payload.pop("id", None)
815
815
  organization = payload.get("organization")
816
816
  if isinstance(organization, dict):
@@ -285,7 +285,7 @@ def _to_jsonable(value):
285
285
  return [_to_jsonable(item) for item in value]
286
286
  if isinstance(value, datetime.datetime | datetime.date | datetime.time):
287
287
  return value.isoformat()
288
- if isinstance(value, pathlib.Path):
288
+ if isinstance(value, pathlib.Path | uuid.UUID):
289
289
  return str(value)
290
290
  if isinstance(value, PyEnum):
291
291
  return value.value
@@ -1,6 +1,18 @@
1
1
  from mainsequence.logconf import logger as logger
2
2
 
3
3
  from .github_issues import * # noqa: F403
4
+ from .inference import (
5
+ InferenceClient as InferenceClient,
6
+ )
7
+ from .inference import (
8
+ InferenceExecutionError as InferenceExecutionError,
9
+ )
10
+ from .inference import (
11
+ InferenceResult as InferenceResult,
12
+ )
13
+ from .inference import (
14
+ InferenceTransportError as InferenceTransportError,
15
+ )
4
16
  from .models_foundry import * # noqa: F403
5
17
  from .models_helpers import * # noqa: F403
6
18
  from .models_user import * # noqa: F403
@@ -0,0 +1,184 @@
1
+ """Recorded model calls through Main Sequence's authenticated API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from copy import deepcopy
6
+ from typing import Any
7
+ from uuid import UUID
8
+
9
+ import requests
10
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
11
+
12
+ from . import utils
13
+ from .exceptions import raise_for_response
14
+
15
+
16
+ class InferenceResult(BaseModel):
17
+ model_config = ConfigDict(extra="allow")
18
+ conversation_uid: str
19
+ request_uid: str
20
+ sequence: int
21
+ provider: str
22
+ model: str
23
+ status: str
24
+ error_code: str
25
+ content_available: bool
26
+ usage: dict[str, Any] = Field(default_factory=dict)
27
+ input: dict[str, Any] | None = None
28
+ output: dict[str, Any] | None = None
29
+ effective_controls: dict[str, Any] = Field(default_factory=dict)
30
+ provider_error: dict[str, Any] | None = None
31
+
32
+
33
+ class InferenceExecutionError(RuntimeError):
34
+ """A recorded failure; inspect ``result.request_uid`` without re-executing."""
35
+
36
+ def __init__(self, result: InferenceResult, status_code: int):
37
+ self.result = result
38
+ self.status_code = status_code
39
+ super().__init__(result.error_code or result.status)
40
+
41
+
42
+ class InferenceTransportError(RuntimeError):
43
+ """Outcome is uncertain. Replay the same body and idempotency key only."""
44
+
45
+ def __init__(self, idempotency_key: str | None):
46
+ self.idempotency_key = idempotency_key
47
+ super().__init__(
48
+ "The request outcome is unknown. Inspect history or replay the identical request with the same idempotency key."
49
+ )
50
+
51
+
52
+ class InferenceClient:
53
+ """Full caller-supplied context, with durable history and no Agent dependency.
54
+
55
+ Runtime credentials derive their Environment on the server. Human callers
56
+ provide ``organization_environment_uid`` explicitly. Provider secrets never
57
+ pass through this client. Optional provider options retain native JSON values.
58
+ """
59
+
60
+ def __init__(self, *, organization_environment_uid: str | UUID | None = None):
61
+ self.organization_environment_uid = (
62
+ str(UUID(str(organization_environment_uid)))
63
+ if organization_environment_uid is not None
64
+ else None
65
+ )
66
+
67
+ def _request(self, method, path, *, body=None, key=None, params=None, timeout=100):
68
+ query = dict(params or {})
69
+ if self.organization_environment_uid is not None:
70
+ query["organization_environment_uid"] = self.organization_environment_uid
71
+ url = f"{utils.API_ENDPOINT.rstrip('/')}/{path}"
72
+ # The shared make_request loop retries network failures. Inference uses
73
+ # the same auth loader with a dedicated zero-retry transport instead.
74
+ with utils.build_session(loaders=utils.loaders, retries=0) as session:
75
+ headers = dict(utils.loaders.refresh_headers(force=False, session=session))
76
+ if key is not None:
77
+ headers["Idempotency-Key"] = key
78
+ try:
79
+ response = session.request(
80
+ method,
81
+ url,
82
+ headers=headers,
83
+ params=query,
84
+ json=body,
85
+ timeout=timeout,
86
+ allow_redirects=False,
87
+ )
88
+ if response.status_code == 401:
89
+ headers.update(utils.loaders.refresh_headers(force=True, session=session))
90
+ response = session.request(
91
+ method,
92
+ url,
93
+ headers=headers,
94
+ params=query,
95
+ json=body,
96
+ timeout=timeout,
97
+ allow_redirects=False,
98
+ )
99
+ except requests.RequestException as exc:
100
+ raise InferenceTransportError(key) from exc
101
+ if method == "POST":
102
+ try:
103
+ payload = response.json()
104
+ except ValueError as exc:
105
+ raise_for_response(response)
106
+ raise InferenceTransportError(key) from exc
107
+ if isinstance(payload, dict) and "request_uid" in payload:
108
+ try:
109
+ result = InferenceResult.model_validate(payload)
110
+ except ValidationError as exc:
111
+ raise InferenceTransportError(key) from exc
112
+ if response.status_code >= 400:
113
+ raise InferenceExecutionError(result, response.status_code)
114
+ return result
115
+ raise_for_response(response)
116
+ raise InferenceTransportError(key)
117
+ raise_for_response(response)
118
+ return response.json()
119
+
120
+ def complete(
121
+ self,
122
+ *,
123
+ provider: str,
124
+ model: str,
125
+ messages: list[dict[str, str]],
126
+ idempotency_key: str,
127
+ conversation_uid: str | UUID | None = None,
128
+ thinking_level: str | None = None,
129
+ provider_options: dict[str, Any] | None = None,
130
+ provider_storage: dict[str, Any] | None = None,
131
+ response_schema: dict[str, Any] | None = None,
132
+ metadata: dict[str, Any] | None = None,
133
+ timeout: float = 100,
134
+ ) -> InferenceResult:
135
+ """Execute once or replay an existing identity; never generate a retry key."""
136
+ import re
137
+
138
+ if not isinstance(idempotency_key, str) or not re.fullmatch(
139
+ r"[A-Za-z0-9._:-]{1,128}", idempotency_key
140
+ ):
141
+ raise ValueError(
142
+ "idempotency_key must contain 1–128 letters, digits, dots, underscores, colons or hyphens."
143
+ )
144
+ body = {"provider": provider, "model": model, "messages": deepcopy(messages)}
145
+ if conversation_uid is not None:
146
+ body["conversation_uid"] = str(UUID(str(conversation_uid)))
147
+ for name, value in (
148
+ ("thinking_level", thinking_level),
149
+ ("provider_options", provider_options),
150
+ ("provider_storage", provider_storage),
151
+ ("response_schema", response_schema),
152
+ ("metadata", metadata),
153
+ ):
154
+ if value is not None:
155
+ body[name] = deepcopy(value)
156
+ return self._request(
157
+ "POST", "inference/completions/", body=body, key=idempotency_key, timeout=timeout
158
+ )
159
+
160
+ def conversations(self, *, limit: int = 25, offset: int = 0) -> dict[str, Any]:
161
+ return self._request(
162
+ "GET", "inference/conversations/", params={"limit": limit, "offset": offset}
163
+ )
164
+
165
+ def conversation(self, uid: str | UUID) -> dict[str, Any]:
166
+ return self._request("GET", f"inference/conversations/{UUID(str(uid))}/")
167
+
168
+ def history(self, uid: str | UUID, *, limit: int = 25, offset: int = 0) -> dict[str, Any]:
169
+ return self._request(
170
+ "GET",
171
+ f"inference/conversations/{UUID(str(uid))}/history/",
172
+ params={"limit": limit, "offset": offset},
173
+ )
174
+
175
+ def request(self, uid: str | UUID) -> InferenceResult:
176
+ return InferenceResult.model_validate(
177
+ self._request("GET", f"inference/requests/{UUID(str(uid))}/")
178
+ )
179
+
180
+ def insights(self, uid: str | UUID) -> dict[str, Any]:
181
+ return self._request("GET", f"inference/conversations/{UUID(str(uid))}/insights/")
182
+
183
+ def delete_content(self, uid: str | UUID) -> dict[str, Any]:
184
+ return self._request("DELETE", f"inference/conversations/{UUID(str(uid))}/content/")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 9.0.2.dev32
3
+ Version: 9.0.3.dev33
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: MIT
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  agent_scaffold/AGENTS.md
5
5
  agent_scaffold/skills/a2a_sdk_execution/SKILL.md
6
6
  agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md
7
+ agent_scaffold/skills/mainsequence-inference/SKILL.md
7
8
  agent_scaffold/skills/maintenance/bug_auditor/SKILL.md
8
9
  agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md
9
10
  agent_scaffold/skills/maintenance/metatables_transition/SKILL.md
@@ -48,6 +49,7 @@ mainsequence/client/client.py
48
49
  mainsequence/client/compute_validation.py
49
50
  mainsequence/client/exceptions.py
50
51
  mainsequence/client/github_issues.py
52
+ mainsequence/client/inference.py
51
53
  mainsequence/client/models_foundry.py
52
54
  mainsequence/client/models_helpers.py
53
55
  mainsequence/client/models_user.py
@@ -80,6 +82,7 @@ tests/test_fastapi_request_identity.py
80
82
  tests/test_filter_normalization.py
81
83
  tests/test_github_issues_client.py
82
84
  tests/test_independent_context.py
85
+ tests/test_inference_client.py
83
86
  tests/test_instrumentation.py
84
87
  tests/test_job_client.py
85
88
  tests/test_local_ops.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mainsequence"
7
- version = "9.0.2.dev32"
7
+ version = "9.0.3.dev33"
8
8
  description = "Main Sequence SDK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import dataclasses
4
+ import datetime
3
5
  import hashlib
4
6
  import importlib
5
7
  import json
@@ -8,8 +10,11 @@ import pathlib
8
10
  import subprocess
9
11
  import sys
10
12
  import types
13
+ from enum import Enum
14
+ from uuid import UUID
11
15
 
12
16
  import pytest
17
+ from pydantic import BaseModel
13
18
  from typer.testing import CliRunner
14
19
 
15
20
  USER_UID = "8f5d6b54-2f5e-4a8b-bb10-0b17f3f4c123"
@@ -217,6 +222,102 @@ def test_user_show_json(cli_mod, runner, monkeypatch):
217
222
  assert payload["organization"]["name"] == "Main Sequence"
218
223
 
219
224
 
225
+ @pytest.mark.parametrize("json_output", [False, True])
226
+ def test_user_show_handles_nested_uuid_identity(cli_mod, runner, monkeypatch, json_output):
227
+ organization_uid = "00000000-0000-4000-8000-000000000002"
228
+ identity = {
229
+ "uid": UUID(USER_UID),
230
+ "username": "jose",
231
+ "organization": {"uid": UUID(organization_uid), "name": "Main Sequence"},
232
+ "active_team_uids": [UUID(TEAM_UID)],
233
+ }
234
+ monkeypatch.setattr(cli_mod, "get_logged_user_details", lambda: identity)
235
+
236
+ result = runner.invoke(cli_mod.app, ["user"] + (["--json"] if json_output else []))
237
+
238
+ assert result.exit_code == 0
239
+ if json_output:
240
+ assert json.loads(result.output) == {
241
+ "uid": USER_UID,
242
+ "username": "jose",
243
+ "organization": {"uid": organization_uid, "name": "Main Sequence"},
244
+ "active_team_uids": [TEAM_UID],
245
+ }
246
+ else:
247
+ assert USER_UID in result.output
248
+ assert "Main Sequence" in result.output
249
+
250
+
251
+ def test_to_jsonable_normalizes_uuid_in_nested_containers(cli_mod):
252
+ @dataclasses.dataclass
253
+ class Identity:
254
+ uid: UUID
255
+
256
+ class IdentityModel(BaseModel):
257
+ uid: UUID
258
+
259
+ class State(Enum):
260
+ ACTIVE = "active"
261
+
262
+ uid = UUID(USER_UID)
263
+ joined = datetime.datetime(2026, 1, 1, tzinfo=datetime.UTC)
264
+ payload = {
265
+ "uid": uid,
266
+ "nested": {"uid": uid, "orm_class": object()},
267
+ "list": [uid],
268
+ "tuple": (uid,),
269
+ "set": {uid},
270
+ "dataclass": Identity(uid),
271
+ "model": IdentityModel(uid=uid),
272
+ "indexed": {uid: uid},
273
+ "date_joined": joined,
274
+ "path": pathlib.Path("skills"),
275
+ "state": State.ACTIVE,
276
+ "orm_class": object(),
277
+ }
278
+
279
+ assert json.loads(json.dumps(cli_mod._to_jsonable(payload))) == {
280
+ "uid": USER_UID,
281
+ "nested": {"uid": USER_UID},
282
+ "list": [USER_UID],
283
+ "tuple": [USER_UID],
284
+ "set": [USER_UID],
285
+ "dataclass": {"uid": USER_UID},
286
+ "model": {"uid": USER_UID},
287
+ "indexed": {USER_UID: USER_UID},
288
+ "date_joined": joined.isoformat(),
289
+ "path": "skills",
290
+ "state": "active",
291
+ }
292
+
293
+
294
+ def test_to_jsonable_does_not_hide_unsupported_type_errors(cli_mod):
295
+ with pytest.raises(TypeError, match="not JSON serializable"):
296
+ json.dumps(cli_mod._to_jsonable(object()))
297
+
298
+
299
+ @pytest.mark.parametrize("json_output", [False, True])
300
+ @pytest.mark.parametrize(
301
+ ("error_type", "message", "expected_message"),
302
+ [
303
+ ("NotLoggedIn", "Invalid session", "Not logged in. Run: mainsequence login"),
304
+ ("ApiError", "Current user unavailable", "Current user unavailable"),
305
+ ],
306
+ )
307
+ def test_user_show_preserves_errors(
308
+ cli_mod, runner, monkeypatch, json_output, error_type, message, expected_message
309
+ ):
310
+ def rejected():
311
+ raise getattr(cli_mod, error_type)(message)
312
+
313
+ monkeypatch.setattr(cli_mod, "get_logged_user_details", rejected)
314
+
315
+ result = runner.invoke(cli_mod.app, ["user"] + (["--json"] if json_output else []))
316
+
317
+ assert result.exit_code == 1
318
+ assert expected_message in result.output
319
+
320
+
220
321
  def test_skills_list(cli_mod, runner, monkeypatch, tmp_path):
221
322
  bundle_dir = tmp_path / "agent_scaffold"
222
323
  (bundle_dir / "skills" / "code_repository_design").mkdir(parents=True)
@@ -4134,9 +4235,34 @@ def test_validate_code_repository_name_uses_client_model(cli_mod, monkeypatch):
4134
4235
  assert out["normalized"]["repository_library_name"] == "rates_platform"
4135
4236
 
4136
4237
 
4137
- def test_get_logged_user_details_uses_canonical_authenticated_user_method(cli_mod, monkeypatch):
4238
+ def test_get_logged_user_details_uses_canonical_authenticated_user_method(
4239
+ cli_mod, runner, monkeypatch
4240
+ ):
4241
+ from mainsequence.client.models_user import User
4242
+
4138
4243
  api_mod = importlib.import_module("mainsequence.cli.api")
4139
4244
  captured = {}
4245
+ user = User.model_validate(
4246
+ {
4247
+ "id": 7,
4248
+ "uid": USER_UID,
4249
+ "username": "jose",
4250
+ "email": "jose@main-sequence.io",
4251
+ "date_joined": "2026-01-01T00:00:00Z",
4252
+ "is_active": True,
4253
+ "api_request_limit": 10000,
4254
+ "mfa_enabled": False,
4255
+ "active_team_uids": [TEAM_UID],
4256
+ "organization": {
4257
+ "id": 2,
4258
+ "uid": "00000000-0000-4000-8000-000000000003",
4259
+ "name": "Main Sequence",
4260
+ "organization_domain": "main-sequence.io",
4261
+ "production_environment_uid": "00000000-0000-4000-8000-000000000002",
4262
+ },
4263
+ }
4264
+ )
4265
+ assert isinstance(user.model_dump()["active_team_uids"][0], UUID)
4140
4266
 
4141
4267
  monkeypatch.setattr(
4142
4268
  api_mod, "get_tokens", lambda: {"access": "acc", "refresh": "ref", "username": "u"}
@@ -4182,20 +4308,7 @@ def test_get_logged_user_details_uses_canonical_authenticated_user_method(cli_mo
4182
4308
  @classmethod
4183
4309
  def get_authenticated_user_details(cls):
4184
4310
  captured["current_user_url"] = f"{cls.ROOT_URL}/users/me/"
4185
- return types.SimpleNamespace(
4186
- model_dump=lambda: {
4187
- "id": 7,
4188
- "uid": "user-uid-7",
4189
- "username": "jose",
4190
- "email": "jose@main-sequence.io",
4191
- "organization": {
4192
- "id": 2,
4193
- "uid": "org-uid-2",
4194
- "name": "Main Sequence",
4195
- "production_environment_uid": ("00000000-0000-4000-8000-000000000002"),
4196
- },
4197
- }
4198
- )
4311
+ return user
4199
4312
 
4200
4313
  fake_base.BaseObjectOrm = FakeBaseObjectOrm
4201
4314
  fake_models_user.User = FakeUser
@@ -4215,12 +4328,31 @@ def test_get_logged_user_details_uses_canonical_authenticated_user_method(cli_mo
4215
4328
  assert captured["current_user_url"] == "https://backend.test/api/v1/users/me/"
4216
4329
  assert "id" not in out
4217
4330
  assert "id" not in out["organization"]
4218
- assert out["uid"] == "user-uid-7"
4331
+ assert out["uid"] == USER_UID
4219
4332
  assert out["username"] == "jose"
4333
+ assert out["active_team_uids"] == [TEAM_UID]
4334
+ assert json.loads(json.dumps(out))["uid"] == USER_UID
4220
4335
  assert (
4221
4336
  out["organization"]["production_environment_uid"] == "00000000-0000-4000-8000-000000000002"
4222
4337
  )
4223
4338
 
4339
+ normal = runner.invoke(cli_mod.app, ["user"])
4340
+ structured = runner.invoke(cli_mod.app, ["user", "--json"])
4341
+
4342
+ assert normal.exit_code == structured.exit_code == 0
4343
+ assert USER_UID in normal.output
4344
+ assert "Main Sequence" in normal.output
4345
+ payload = json.loads(structured.output)
4346
+ assert payload["uid"] == out["uid"]
4347
+ assert payload["organization"]["uid"] == out["organization"]["uid"]
4348
+ assert payload["active_team_uids"] == [TEAM_UID]
4349
+ assert "id" not in payload
4350
+ assert "id" not in payload["organization"]
4351
+ assert "user_permissions" not in payload
4352
+ assert "orm_class" not in payload
4353
+ assert '"acc"' not in structured.output
4354
+ assert '"ref"' not in structured.output
4355
+
4224
4356
 
4225
4357
  def test_search_code_repositories_uses_client_model(cli_mod, monkeypatch):
4226
4358
  api_mod = importlib.import_module("mainsequence.cli.api")
@@ -0,0 +1,167 @@
1
+ from contextlib import nullcontext
2
+ from unittest.mock import Mock
3
+
4
+ import pytest
5
+ import requests
6
+
7
+ from mainsequence.client import inference
8
+
9
+ ENVIRONMENT = "00000000-0000-4000-8000-000000000001"
10
+ CONVERSATION = "00000000-0000-4000-8000-000000000002"
11
+ REQUEST = "00000000-0000-4000-8000-000000000003"
12
+
13
+
14
+ def result(**extra):
15
+ return {
16
+ "conversation_uid": CONVERSATION,
17
+ "request_uid": REQUEST,
18
+ "sequence": 1,
19
+ "provider": "openai",
20
+ "model": "configured-model",
21
+ "status": "completed",
22
+ "error_code": "",
23
+ "content_available": True,
24
+ "usage": {"complete": False},
25
+ **extra,
26
+ }
27
+
28
+
29
+ @pytest.fixture
30
+ def transport(monkeypatch):
31
+ session = Mock()
32
+ response = Mock(status_code=200)
33
+ response.json.return_value = result()
34
+ session.request.return_value = response
35
+ factory = Mock(return_value=nullcontext(session))
36
+ monkeypatch.setattr(inference.utils, "build_session", factory)
37
+ monkeypatch.setattr(
38
+ inference.utils.loaders,
39
+ "refresh_headers",
40
+ Mock(return_value={"Authorization": "test-auth"}),
41
+ )
42
+ monkeypatch.setattr(inference.utils, "API_ENDPOINT", "https://mainsequence.test/api/v1")
43
+ return session, response, factory
44
+
45
+
46
+ @pytest.mark.parametrize(
47
+ "provider,options",
48
+ [
49
+ ("openai", {"store": False, "text": {"verbosity": "low"}}),
50
+ ("anthropic", {"thinking": {"type": "adaptive"}, "output_config": {"effort": "high"}}),
51
+ ("ollama-local", {"reasoning_effort": "high"}),
52
+ ("openrouter", {"provider": {"zdr": True}, "reasoning": {"effort": "high"}}),
53
+ ],
54
+ )
55
+ def test_native_options_survive_for_each_provider(transport, provider, options):
56
+ options["future_field"] = [None, False, {"new": "value"}]
57
+ session, response, factory = transport
58
+ client = inference.InferenceClient(organization_environment_uid=ENVIRONMENT)
59
+ answer = client.complete(
60
+ provider=provider,
61
+ model="configured-model",
62
+ messages=[{"role": "user", "content": "test"}],
63
+ idempotency_key="one",
64
+ provider_options=options,
65
+ )
66
+ assert answer.request_uid == REQUEST
67
+ kwargs = session.request.call_args.kwargs
68
+ assert kwargs["json"]["provider_options"] == options
69
+ assert "thinking_level" not in kwargs["json"]
70
+ assert kwargs["headers"]["Idempotency-Key"] == "one"
71
+ assert kwargs["params"]["organization_environment_uid"] == ENVIRONMENT
72
+ assert factory.call_args.kwargs["retries"] == 0
73
+
74
+
75
+ def test_common_controls_and_retry_identity(transport):
76
+ session, _, _ = transport
77
+ client = inference.InferenceClient()
78
+ kwargs = dict(
79
+ provider="openai",
80
+ model="configured-model",
81
+ messages=[],
82
+ idempotency_key="stable",
83
+ thinking_level="high",
84
+ provider_storage={"store_response": False},
85
+ provider_options={"future": None},
86
+ )
87
+ client.complete(**kwargs)
88
+ first = session.request.call_args
89
+ client.complete(**kwargs)
90
+ assert session.request.call_args == first
91
+ assert first.kwargs["json"]["thinking_level"] == "high"
92
+ assert first.kwargs["json"]["provider_storage"] == {"store_response": False}
93
+
94
+
95
+ def test_timeout_does_not_retry_or_create_a_new_key(transport):
96
+ session, _, _ = transport
97
+ session.request.side_effect = requests.Timeout()
98
+ with pytest.raises(inference.InferenceTransportError) as exc:
99
+ inference.InferenceClient().complete(
100
+ provider="openai", model="configured-model", messages=[], idempotency_key="stable"
101
+ )
102
+ assert exc.value.idempotency_key == "stable"
103
+ assert session.request.call_count == 1
104
+
105
+
106
+ def test_recorded_failure_preserves_identifiers(transport):
107
+ session, response, _ = transport
108
+ response.status_code = 504
109
+ response.json.return_value = result(status="unknown", error_code="provider_outcome_unknown")
110
+ with pytest.raises(inference.InferenceExecutionError) as exc:
111
+ inference.InferenceClient().complete(
112
+ provider="openai", model="configured-model", messages=[], idempotency_key="stable"
113
+ )
114
+ assert exc.value.result.request_uid == REQUEST
115
+ assert session.request.call_count == 1
116
+
117
+
118
+ @pytest.mark.parametrize(
119
+ "method,path",
120
+ [
121
+ ("conversation", ""),
122
+ ("history", "history/"),
123
+ ("insights", "insights/"),
124
+ ("delete_content", "content/"),
125
+ ],
126
+ )
127
+ def test_history_operations_use_mainsequence_and_exact_uid(transport, method, path):
128
+ session, _, _ = transport
129
+ getattr(inference.InferenceClient(organization_environment_uid=ENVIRONMENT), method)(
130
+ CONVERSATION
131
+ )
132
+ assert (
133
+ session.request.call_args.args[1]
134
+ == f"https://mainsequence.test/api/v1/inference/conversations/{CONVERSATION}/{path}"
135
+ )
136
+ assert session.request.call_args.args[0] == ("DELETE" if method == "delete_content" else "GET")
137
+
138
+
139
+ def test_authentication_refresh_keeps_body_and_key(transport):
140
+ session, response, _ = transport
141
+ refused = Mock(status_code=401)
142
+ session.request.side_effect = [refused, response]
143
+ inference.InferenceClient().complete(
144
+ provider="openai", model="configured-model", messages=[], idempotency_key="stable"
145
+ )
146
+ assert session.request.call_count == 2
147
+ assert (
148
+ session.request.call_args_list[0].kwargs["json"]
149
+ == session.request.call_args_list[1].kwargs["json"]
150
+ )
151
+ assert session.request.call_args_list[1].kwargs["headers"]["Idempotency-Key"] == "stable"
152
+
153
+
154
+ @pytest.mark.parametrize("invalid", ["html", {}, {"request_uid": REQUEST}])
155
+ def test_invalid_success_is_uncertain_and_never_retried(transport, invalid):
156
+ session, response, _ = transport
157
+ if invalid == "html":
158
+ response.json.side_effect = ValueError("Non-JSON reply")
159
+ else:
160
+ response.json.return_value = invalid
161
+ with pytest.raises(inference.InferenceTransportError) as exc:
162
+ inference.InferenceClient().complete(
163
+ provider="openai", model="configured-model", messages=[], idempotency_key="stable"
164
+ )
165
+ assert exc.value.idempotency_key == "stable"
166
+ assert session.request.call_count == 1
167
+ assert session.request.call_args.kwargs["allow_redirects"] is False
@@ -250,6 +250,7 @@ def test_packaged_scaffold_keeps_retained_sdk_skills():
250
250
 
251
251
  assert skill_paths == {
252
252
  "a2a_sdk_execution/SKILL.md",
253
+ "mainsequence-inference/SKILL.md",
253
254
  "application_surfaces/api_surfaces/SKILL.md",
254
255
  "maintenance/bug_auditor/SKILL.md",
255
256
  "maintenance/code_repository_maintenance/SKILL.md",