mainsequence 9.0.20.dev62__tar.gz → 9.0.21.dev64__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.20.dev62 → mainsequence-9.0.21.dev64}/PKG-INFO +1 -1
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +20 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/cli/api.py +56 -22
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/cli/cli.py +20 -2
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/client/_transport.py +75 -1
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/client/agent_runtime_models.py +224 -91
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence.egg-info/PKG-INFO +1 -1
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence.egg-info/SOURCES.txt +1 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/pyproject.toml +1 -1
- mainsequence-9.0.21.dev64/tests/test_cli_request_budget.py +232 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_code_repository_skills.py +53 -2
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_http_retry_budget.py +66 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/LICENSE +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/README.md +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/agent_scaffold/AGENTS.md +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/agent_scaffold/skills/mainsequence-inference/SKILL.md +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/agent_scaffold/skills/maintenance/metatables_transition/SKILL.md +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/agent_scaffold/skills/sdk_code_repository_execution/SKILL.md +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/__init__.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/__main__.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/_request_identity.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/bootstrap.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/cli/__init__.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/cli/browser_auth.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/cli/code_repository_status.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/cli/config.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/cli/docker_utils.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/cli/doctor.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/cli/local_ops.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/cli/model_filters.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/cli/pydantic_cli.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/cli/sdk_utils.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/cli/ssh_utils.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/cli/ui.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/client/__init__.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/client/base.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/client/client.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/client/compute_validation.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/client/exceptions.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/client/github_issues.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/client/inference.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/client/models_foundry.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/client/models_helpers.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/client/models_user.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/client/observability.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/client/utils.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/client/value_sets.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/code_repository_context.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/code_repository_skills.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/defaults.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/instrumentation/__init__.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/instrumentation/utils.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/logconf.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/repository_identity_security.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/runtime_credential_exchange.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/runtime_flags.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/scaffold_skills.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/server/__init__.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/server/caller_assertions.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence/server/fastapi.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence.egg-info/dependency_links.txt +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence.egg-info/entry_points.txt +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence.egg-info/requires.txt +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/mainsequence.egg-info/top_level.txt +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/setup.cfg +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_auth_bootstrap.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_auth_precedence.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_caller_assertions.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_canonical_api_route_contract.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_cli_auth_contract.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_cli_browser_auth.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_cli_secure_storage.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_client.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_code_repository_branch_contract.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_code_repository_documentation_contract.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_code_repository_runtime_context.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_credential_repr.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_dependency_extras.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_dev_release_version.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_environment_resource_context.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_environment_response_contracts.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_fastapi_mcp.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_fastapi_request_identity.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_github_issues_client.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_independent_context.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_inference_client.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_instrumentation.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_job_client.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_local_ops.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_logconf.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_models_user_request_bound_auth.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_notification_uid_contract.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_observability_client.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_runtime_credential_exchange.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_scaffold_skills.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_secret_client_model.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_source_ownership.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_ssh_utils.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_tolerant_response_reading.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_tolerant_value_sets.py +0 -0
- {mainsequence-9.0.20.dev62 → mainsequence-9.0.21.dev64}/tests/test_user_application_facts.py +0 -0
|
@@ -33,6 +33,9 @@ internally and sends a standard A2A message.
|
|
|
33
33
|
- Read `a2a_profile` from discovery, default to `message`, and never infer Task
|
|
34
34
|
support.
|
|
35
35
|
- Use `--response-kind task` only when the selected Agent advertises `task`.
|
|
36
|
+
- The platform answers a direct `message` send within its Message limit, 90
|
|
37
|
+
seconds, or the send fails with `A2AMessageLimitExceededError`. No client
|
|
38
|
+
setting extends that limit; send work that can take longer as a Task.
|
|
36
39
|
- Do not send or request reasoning/thinking traces; standard A2A responses expose only message parts.
|
|
37
40
|
|
|
38
41
|
## Discovery Flow
|
|
@@ -244,6 +247,12 @@ result = AgentSession.send_a2a_message(
|
|
|
244
247
|
task = AgentSession.wait_for_a2a_task(session, task_id=result.task.id)
|
|
245
248
|
```
|
|
246
249
|
|
|
250
|
+
`wait_for_a2a_task` reads the Task every `poll_interval_seconds` (default 1)
|
|
251
|
+
until it reaches a final state. Once `wait_timeout_seconds` (default 900) have
|
|
252
|
+
passed it raises `TimeoutError`; the Task keeps running, so read it again with
|
|
253
|
+
`AgentSession.get_a2a_task` or stop it with `AgentSession.cancel_a2a_task`. Its
|
|
254
|
+
`timeout` is the total budget of each read, as for the other A2A calls.
|
|
255
|
+
|
|
247
256
|
For another message in the same conversation, reuse the same `session.uid`:
|
|
248
257
|
|
|
249
258
|
```python
|
|
@@ -312,7 +321,18 @@ handle for a different task or conversation.
|
|
|
312
321
|
task` for an Agent whose discovery profile advertises it. Preserve its ID and
|
|
313
322
|
follow it with the SDK task get/wait/cancel helpers.
|
|
314
323
|
- Treat a result kind different from the requested kind as a protocol error.
|
|
324
|
+
- `timeout` (CLI `--timeout`) is one total budget for the send, including the
|
|
325
|
+
wait for a target runtime that is still starting or waking; it can only
|
|
326
|
+
shorten waits. If the runtime does not become ready within it, the send fails
|
|
327
|
+
with `AgentRuntimeNotReadyError` ("did not become ready") and nothing was
|
|
328
|
+
sent, so sending again later is safe.
|
|
315
329
|
- Do not depend on reasoning, tool traces, runtime paths, or transport metadata.
|
|
330
|
+
- If a direct `message` send fails with `A2AMessageLimitExceededError` (the
|
|
331
|
+
error says the Agent did not answer within the platform's limit), the Agent
|
|
332
|
+
stopped the turn after the platform's 90-second Message limit, and work it
|
|
333
|
+
already did is not undone. Do not resend it as a Message; when the Agent
|
|
334
|
+
advertises `task`, send the work with `--response-kind task`
|
|
335
|
+
(`response_kind=A2AResponseKind.TASK`) and follow the Task.
|
|
316
336
|
- If a send times out or disconnects and must be retried, reuse the same
|
|
317
337
|
`--message-id` value to preserve request identity. Direct `message` sends do
|
|
318
338
|
not have durable replay protection, so do not retry automatically: the first
|
|
@@ -63,7 +63,27 @@ MCP_PROTOCOL_VERSION = "2025-11-25"
|
|
|
63
63
|
MCP_CLIENT_NAME = "mainsequence-cli"
|
|
64
64
|
MCP_MAX_RESOURCE_PAGES = 100
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
|
|
67
|
+
class _CliSession(requests.Session):
|
|
68
|
+
"""The CLI's HTTP session.
|
|
69
|
+
|
|
70
|
+
A request sent without its own timeout gets the SDK's default request
|
|
71
|
+
budget: 5 s to connect, 120 s to read and 125 s in total, inside any budget
|
|
72
|
+
that is already running.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def request(self, method, url, **kwargs):
|
|
76
|
+
if kwargs.get("timeout") is not None:
|
|
77
|
+
return super().request(method, url, **kwargs)
|
|
78
|
+
# The CLI imports the SDK client on its first request, not when it starts.
|
|
79
|
+
from mainsequence.client._transport import DEFAULT_TIMEOUT, request_budget
|
|
80
|
+
|
|
81
|
+
with request_budget(DEFAULT_TIMEOUT) as budget:
|
|
82
|
+
kwargs["timeout"] = budget.timeout()
|
|
83
|
+
return super().request(method, url, **kwargs)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
S = _CliSession()
|
|
67
87
|
S.headers.update({"Content-Type": "application/json"})
|
|
68
88
|
|
|
69
89
|
|
|
@@ -488,11 +508,13 @@ def _mcp_json_rpc(
|
|
|
488
508
|
request_id: int,
|
|
489
509
|
protocol_version: str | None = None,
|
|
490
510
|
) -> dict[str, Any]:
|
|
491
|
-
"""Send one authenticated stateless MCP JSON-RPC request.
|
|
511
|
+
"""Send one authenticated stateless MCP JSON-RPC request.
|
|
492
512
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
513
|
+
The request, a token renewal after ``401`` and the resend share the SDK's
|
|
514
|
+
default request budget: 5 s to connect, 120 s to read, 125 s in total.
|
|
515
|
+
"""
|
|
516
|
+
# The CLI imports the SDK client only where it needs it.
|
|
517
|
+
from mainsequence.client._transport import DEFAULT_TIMEOUT, request_budget
|
|
496
518
|
|
|
497
519
|
payload = {
|
|
498
520
|
"jsonrpc": "2.0",
|
|
@@ -501,24 +523,36 @@ def _mcp_json_rpc(
|
|
|
501
523
|
"params": params,
|
|
502
524
|
}
|
|
503
525
|
|
|
504
|
-
|
|
505
|
-
headers = {
|
|
506
|
-
"Authorization": f"Bearer {token}",
|
|
507
|
-
"Content-Type": "application/json",
|
|
508
|
-
"Accept": "application/json",
|
|
509
|
-
}
|
|
510
|
-
if protocol_version is not None:
|
|
511
|
-
headers["MCP-Protocol-Version"] = protocol_version
|
|
512
|
-
return S.request(
|
|
513
|
-
"POST",
|
|
514
|
-
_full(MCP_RESOURCE_PATH),
|
|
515
|
-
headers=headers,
|
|
516
|
-
data=json.dumps(payload),
|
|
517
|
-
)
|
|
526
|
+
with request_budget(DEFAULT_TIMEOUT) as budget:
|
|
518
527
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
528
|
+
def _send(token: str) -> requests.Response:
|
|
529
|
+
headers = {
|
|
530
|
+
"Authorization": f"Bearer {token}",
|
|
531
|
+
"Content-Type": "application/json",
|
|
532
|
+
"Accept": "application/json",
|
|
533
|
+
}
|
|
534
|
+
if protocol_version is not None:
|
|
535
|
+
headers["MCP-Protocol-Version"] = protocol_version
|
|
536
|
+
return S.request(
|
|
537
|
+
"POST",
|
|
538
|
+
_full(MCP_RESOURCE_PATH),
|
|
539
|
+
headers=headers,
|
|
540
|
+
data=json.dumps(payload),
|
|
541
|
+
timeout=budget.timeout(),
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
try:
|
|
545
|
+
access = _access_token()
|
|
546
|
+
if not access:
|
|
547
|
+
access = refresh_access()
|
|
548
|
+
response = _send(access)
|
|
549
|
+
if response.status_code == 401:
|
|
550
|
+
response = _send(refresh_access())
|
|
551
|
+
except requests.Timeout as exc:
|
|
552
|
+
raise ApiError(
|
|
553
|
+
f"MCP {method} request timed out: the platform did not answer within "
|
|
554
|
+
"the request's timeout budget."
|
|
555
|
+
) from exc
|
|
522
556
|
if response.status_code == 401:
|
|
523
557
|
raise NotLoggedIn("Not logged in.")
|
|
524
558
|
if not response.ok:
|
|
@@ -1946,6 +1946,10 @@ def login(
|
|
|
1946
1946
|
except ApiError as e:
|
|
1947
1947
|
error(f"Login failed: {e}")
|
|
1948
1948
|
raise typer.Exit(1) from e
|
|
1949
|
+
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
|
|
1950
|
+
# Never echo the exception: a transport error can carry request details.
|
|
1951
|
+
error(f"The backend at {normalized_backend} could not be reached ({type(e).__name__}).")
|
|
1952
|
+
raise typer.Exit(1) from e
|
|
1949
1953
|
finally:
|
|
1950
1954
|
if previous_backend_override is None:
|
|
1951
1955
|
os.environ.pop("MAINSEQUENCE_ENDPOINT", None)
|
|
@@ -4386,9 +4390,23 @@ def agent_session_a2a_send_cmd(
|
|
|
4386
4390
|
response_kind: str = typer.Option(
|
|
4387
4391
|
"message",
|
|
4388
4392
|
"--response-kind",
|
|
4389
|
-
help=
|
|
4393
|
+
help=(
|
|
4394
|
+
"Select the A2A result contract. message: wait for the Agent's answer "
|
|
4395
|
+
"within the platform's Message limit; a Message that reaches the limit "
|
|
4396
|
+
"fails and is not retried. task: get a Task back for longer work, when "
|
|
4397
|
+
"the Agent advertises it."
|
|
4398
|
+
),
|
|
4399
|
+
),
|
|
4400
|
+
timeout: int | None = typer.Option(
|
|
4401
|
+
None,
|
|
4402
|
+
"--timeout",
|
|
4403
|
+
help=(
|
|
4404
|
+
"Total time budget in seconds for the A2A send: waiting for the Agent "
|
|
4405
|
+
"runtime to become ready, the request and the response. "
|
|
4406
|
+
"Resolving a handle first is a separate request with the same budget. "
|
|
4407
|
+
"It can only shorten waits; it cannot extend the platform's limits."
|
|
4408
|
+
),
|
|
4390
4409
|
),
|
|
4391
|
-
timeout: int | None = typer.Option(None, "--timeout", help="Request timeout in seconds"),
|
|
4392
4410
|
):
|
|
4393
4411
|
"""
|
|
4394
4412
|
Send one standard A2A message to an existing session UID or cached handle.
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""One bounded retry policy for SDK HTTP sessions."""
|
|
2
2
|
|
|
3
|
+
import contextvars
|
|
3
4
|
import math
|
|
5
|
+
import threading
|
|
4
6
|
import time
|
|
5
|
-
from contextlib import contextmanager
|
|
7
|
+
from contextlib import contextmanager, suppress
|
|
6
8
|
from contextvars import ContextVar
|
|
7
9
|
|
|
8
10
|
import requests
|
|
@@ -80,6 +82,78 @@ def remaining_timeout(timeout):
|
|
|
80
82
|
return Timeout(total=remaining, connect=min(connect, remaining), read=min(read, remaining))
|
|
81
83
|
|
|
82
84
|
|
|
85
|
+
def request_within_budget(method, url, *, timeout, **kwargs):
|
|
86
|
+
"""Send one request, never retried, whose whole exchange ends by one deadline.
|
|
87
|
+
|
|
88
|
+
``timeout`` has the shared transport's meaning: a number is one total budget
|
|
89
|
+
in seconds, and a ``(connect, read)`` tuple keeps those phase limits with a
|
|
90
|
+
total equal to their sum. An enclosing :func:`request_budget` can only
|
|
91
|
+
shorten it. The deadline covers connecting, sending, the response headers
|
|
92
|
+
and the whole body, so a server that keeps sending bytes slowly cannot
|
|
93
|
+
extend it.
|
|
94
|
+
|
|
95
|
+
A socket read that keeps receiving bytes never times out on its own, so the
|
|
96
|
+
exchange runs on a helper thread and the caller stops waiting at the
|
|
97
|
+
deadline. Then the response, when it has started, is shut down and
|
|
98
|
+
``requests.Timeout`` is raised. The request may already have reached the
|
|
99
|
+
server.
|
|
100
|
+
"""
|
|
101
|
+
with request_budget(timeout) as budget:
|
|
102
|
+
attempt_timeout = budget.timeout()
|
|
103
|
+
lock = threading.Lock()
|
|
104
|
+
finished = threading.Event()
|
|
105
|
+
state = {"abandoned": False, "response": None, "outcome": None}
|
|
106
|
+
|
|
107
|
+
def exchange():
|
|
108
|
+
response = None
|
|
109
|
+
try:
|
|
110
|
+
with requests.Session() as session:
|
|
111
|
+
response = session.request(
|
|
112
|
+
method, url, timeout=attempt_timeout, stream=True, **kwargs
|
|
113
|
+
)
|
|
114
|
+
with lock:
|
|
115
|
+
state["response"] = response
|
|
116
|
+
abandoned = state["abandoned"]
|
|
117
|
+
if not abandoned:
|
|
118
|
+
# Read the body here, so the caller's deadline bounds it too.
|
|
119
|
+
_ = response.content
|
|
120
|
+
outcome = response
|
|
121
|
+
except BaseException as exc: # handed to the waiting caller
|
|
122
|
+
outcome = exc
|
|
123
|
+
with lock:
|
|
124
|
+
state["outcome"] = outcome
|
|
125
|
+
abandoned = state["abandoned"]
|
|
126
|
+
if abandoned and response is not None:
|
|
127
|
+
response.close()
|
|
128
|
+
finished.set()
|
|
129
|
+
|
|
130
|
+
threading.Thread(
|
|
131
|
+
target=contextvars.copy_context().run,
|
|
132
|
+
args=(exchange,),
|
|
133
|
+
name="mainsequence-request-within-budget",
|
|
134
|
+
daemon=True,
|
|
135
|
+
).start()
|
|
136
|
+
try:
|
|
137
|
+
finished.wait(budget.remaining)
|
|
138
|
+
finally:
|
|
139
|
+
with lock:
|
|
140
|
+
outcome = state["outcome"]
|
|
141
|
+
if outcome is None:
|
|
142
|
+
state["abandoned"] = True
|
|
143
|
+
started = state["response"]
|
|
144
|
+
if outcome is None and started is not None:
|
|
145
|
+
# Wakes a read that is still waiting for bytes.
|
|
146
|
+
with suppress(OSError, RuntimeError, ValueError):
|
|
147
|
+
started.raw.shutdown()
|
|
148
|
+
if outcome is None:
|
|
149
|
+
raise requests.Timeout(
|
|
150
|
+
"The SDK request did not finish within its total timeout budget."
|
|
151
|
+
)
|
|
152
|
+
if isinstance(outcome, BaseException):
|
|
153
|
+
raise outcome
|
|
154
|
+
return outcome
|
|
155
|
+
|
|
156
|
+
|
|
83
157
|
class DeadlineHTTPAdapter(HTTPAdapter):
|
|
84
158
|
def __init__(self, *, retries, backoff_factor):
|
|
85
159
|
if not isinstance(retries, int) or retries < 0:
|