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.
- {mainsequence-9.0.2.dev32/mainsequence.egg-info → mainsequence-9.0.3.dev33}/PKG-INFO +1 -1
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/AGENTS.md +3 -0
- mainsequence-9.0.3.dev33/agent_scaffold/skills/mainsequence-inference/SKILL.md +198 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/skills/sdk_code_repository_execution/SKILL.md +3 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/api.py +1 -1
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/cli.py +1 -1
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/__init__.py +12 -0
- mainsequence-9.0.3.dev33/mainsequence/client/inference.py +184 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33/mainsequence.egg-info}/PKG-INFO +1 -1
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence.egg-info/SOURCES.txt +3 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/pyproject.toml +1 -1
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_cli.py +148 -16
- mainsequence-9.0.3.dev33/tests/test_inference_client.py +167 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_scaffold_skills.py +1 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/LICENSE +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/README.md +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/skills/maintenance/metatables_transition/SKILL.md +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/__init__.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/__main__.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/_request_identity.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/bootstrap.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/__init__.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/browser_auth.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/code_repository_status.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/config.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/docker_utils.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/doctor.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/local_ops.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/model_filters.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/pydantic_cli.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/sdk_utils.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/ssh_utils.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/ui.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/agent_runtime_models.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/base.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/client.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/compute_validation.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/exceptions.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/github_issues.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/models_foundry.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/models_helpers.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/models_user.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/observability.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/utils.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/value_sets.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/code_repository_context.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/code_repository_skills.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/defaults.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/instrumentation/__init__.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/instrumentation/utils.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/logconf.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/repository_identity_security.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/runtime_flags.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/scaffold_skills.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/server/__init__.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/server/caller_assertions.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/server/fastapi.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence.egg-info/dependency_links.txt +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence.egg-info/entry_points.txt +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence.egg-info/requires.txt +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence.egg-info/top_level.txt +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/setup.cfg +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_auth_bootstrap.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_auth_precedence.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_caller_assertions.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_canonical_api_route_contract.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_cli_auth_contract.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_cli_browser_auth.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_cli_secure_storage.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_client.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_code_repository_branch_contract.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_code_repository_documentation_contract.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_code_repository_runtime_context.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_code_repository_skills.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_dependency_extras.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_dev_release_version.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_environment_resource_context.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_environment_response_contracts.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_fastapi_request_identity.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_filter_normalization.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_github_issues_client.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_independent_context.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_instrumentation.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_job_client.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_local_ops.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_logconf.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_models_user_request_bound_auth.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_notification_uid_contract.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_observability_client.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_secret_client_model.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_source_ownership.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_ssh_utils.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_tolerant_response_reading.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_tolerant_value_sets.py +0 -0
- {mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_user_application_facts.py +0 -0
|
@@ -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/")
|
|
@@ -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
|
|
@@ -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(
|
|
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
|
|
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"] ==
|
|
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",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/cli/code_repository_status.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/agent_runtime_models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/client/compute_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/code_repository_context.py
RENAMED
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/code_repository_skills.py
RENAMED
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/instrumentation/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/repository_identity_security.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence/server/caller_assertions.py
RENAMED
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/mainsequence.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_canonical_api_route_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_code_repository_branch_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_code_repository_runtime_context.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_environment_resource_context.py
RENAMED
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_environment_response_contracts.py
RENAMED
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_fastapi_request_identity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_models_user_request_bound_auth.py
RENAMED
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_notification_uid_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.2.dev32 → mainsequence-9.0.3.dev33}/tests/test_tolerant_response_reading.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|