mainsequence 6.0.45__tar.gz → 6.0.46__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 (131) hide show
  1. {mainsequence-6.0.45/mainsequence.egg-info → mainsequence-6.0.46}/PKG-INFO +1 -1
  2. {mainsequence-6.0.45 → mainsequence-6.0.46}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +48 -5
  3. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/cli/api.py +4 -4
  4. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/cli/cli.py +12 -9
  5. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/client/agent_runtime_models.py +255 -8
  6. {mainsequence-6.0.45 → mainsequence-6.0.46/mainsequence.egg-info}/PKG-INFO +1 -1
  7. {mainsequence-6.0.45 → mainsequence-6.0.46}/pyproject.toml +1 -1
  8. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_cli.py +1 -1
  9. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_filter_normalization.py +181 -3
  10. {mainsequence-6.0.45 → mainsequence-6.0.46}/LICENSE +0 -0
  11. {mainsequence-6.0.45 → mainsequence-6.0.46}/README.md +0 -0
  12. {mainsequence-6.0.45 → mainsequence-6.0.46}/agent_scaffold/AGENTS.md +0 -0
  13. {mainsequence-6.0.45 → mainsequence-6.0.46}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  14. {mainsequence-6.0.45 → mainsequence-6.0.46}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  15. {mainsequence-6.0.45 → mainsequence-6.0.46}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +0 -0
  16. {mainsequence-6.0.45 → mainsequence-6.0.46}/agent_scaffold/skills/data_publishing/meta_table_migrations/SKILL.md +0 -0
  17. {mainsequence-6.0.45 → mainsequence-6.0.46}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +0 -0
  18. {mainsequence-6.0.45 → mainsequence-6.0.46}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  19. {mainsequence-6.0.45 → mainsequence-6.0.46}/agent_scaffold/skills/maintenance/project-maintenance/SKILL.md +0 -0
  20. {mainsequence-6.0.45 → mainsequence-6.0.46}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  21. {mainsequence-6.0.45 → mainsequence-6.0.46}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  22. {mainsequence-6.0.45 → mainsequence-6.0.46}/agent_scaffold/skills/sdk_project_execution/SKILL.md +0 -0
  23. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/__init__.py +0 -0
  24. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/__main__.py +0 -0
  25. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/bootstrap.py +0 -0
  26. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/cli/__init__.py +0 -0
  27. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/cli/browser_auth.py +0 -0
  28. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/cli/config.py +0 -0
  29. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/cli/docker_utils.py +0 -0
  30. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/cli/doctor.py +0 -0
  31. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/cli/local_ops.py +0 -0
  32. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/cli/migrations.py +0 -0
  33. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/cli/model_filters.py +0 -0
  34. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/cli/project_status.py +0 -0
  35. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/cli/pydantic_cli.py +0 -0
  36. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/cli/sdk_utils.py +0 -0
  37. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/cli/ssh_utils.py +0 -0
  38. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/cli/ui.py +0 -0
  39. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/client/__init__.py +0 -0
  40. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/client/base.py +0 -0
  41. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/client/client.py +0 -0
  42. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/client/compute_validation.py +0 -0
  43. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  44. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  45. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
  46. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  47. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/client/dtype_codec.py +0 -0
  48. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/client/exceptions.py +0 -0
  49. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/client/metatables/__init__.py +0 -0
  50. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/client/metatables/core.py +0 -0
  51. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/client/models_foundry.py +0 -0
  52. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/client/models_helpers.py +0 -0
  53. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/client/models_user.py +0 -0
  54. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/client/utils.py +0 -0
  55. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/defaults.py +0 -0
  56. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/instrumentation/__init__.py +0 -0
  57. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/instrumentation/utils.py +0 -0
  58. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/logconf.py +0 -0
  59. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/__init__.py +0 -0
  60. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/__main__.py +0 -0
  61. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
  62. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/compiled_sql/v1.py +0 -0
  63. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  64. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/data_nodes/build_operations.py +0 -0
  65. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
  66. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  67. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  68. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/data_nodes/persist_managers.py +0 -0
  69. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
  70. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  71. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/future_registry.py +0 -0
  72. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/hashing.py +0 -0
  73. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/migrations/__init__.py +0 -0
  74. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/migrations/alembic.py +0 -0
  75. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/migrations/env.py +0 -0
  76. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/migrations/provider.py +0 -0
  77. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/migrations/registry.py +0 -0
  78. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/migrations/scaffold.py +0 -0
  79. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/migrations/templates/__init__.py +0 -0
  80. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/migrations/templates/env.py.mako +0 -0
  81. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/migrations/templates/script.py.mako +0 -0
  82. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  83. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/schema_names.py +0 -0
  84. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/meta_tables/sqlalchemy_contracts.py +0 -0
  85. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/project_context.py +0 -0
  86. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/project_skills.py +0 -0
  87. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/runtime_flags.py +0 -0
  88. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence/scaffold_skills.py +0 -0
  89. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence.egg-info/SOURCES.txt +0 -0
  90. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence.egg-info/dependency_links.txt +0 -0
  91. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence.egg-info/entry_points.txt +0 -0
  92. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence.egg-info/requires.txt +0 -0
  93. {mainsequence-6.0.45 → mainsequence-6.0.46}/mainsequence.egg-info/top_level.txt +0 -0
  94. {mainsequence-6.0.45 → mainsequence-6.0.46}/setup.cfg +0 -0
  95. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_auth_precedence.py +0 -0
  96. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_build_operations_hashing.py +0 -0
  97. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_canonical_api_route_contract.py +0 -0
  98. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_cli_browser_auth.py +0 -0
  99. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_cli_migrations.py +0 -0
  100. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_client.py +0 -0
  101. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  102. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_data_node_storage_dimension_queries.py +0 -0
  103. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_data_node_update_flow.py +0 -0
  104. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_dependency_extras.py +0 -0
  105. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_duckdb_interface_dimensions.py +0 -0
  106. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_environment_resource_context.py +0 -0
  107. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_environment_response_contracts.py +0 -0
  108. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_instrumentation.py +0 -0
  109. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_local_ops.py +0 -0
  110. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_logconf.py +0 -0
  111. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_meta_table_migrations.py +0 -0
  112. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_meta_tables_client_models.py +0 -0
  113. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_meta_tables_sqlalchemy_contracts.py +0 -0
  114. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_models_user_request_bound_auth.py +0 -0
  115. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_notification_uid_contract.py +0 -0
  116. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_pod_project_resolution.py +0 -0
  117. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_project_batch_jobs_from_file.py +0 -0
  118. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_project_branch_contract.py +0 -0
  119. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_project_runtime_context.py +0 -0
  120. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_project_skills.py +0 -0
  121. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_run_configuration.py +0 -0
  122. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_scaffold_skills.py +0 -0
  123. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_scheduler_client_model.py +0 -0
  124. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_schema_names.py +0 -0
  125. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_secret_client_model.py +0 -0
  126. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_source_table_configuration.py +0 -0
  127. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_sqlite_interface_dimensions.py +0 -0
  128. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_ssh_utils.py +0 -0
  129. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_update_runner_uid_runtime.py +0 -0
  130. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_update_statistics.py +0 -0
  131. {mainsequence-6.0.45 → mainsequence-6.0.46}/tests/test_update_uid_guards.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 6.0.45
3
+ Version: 6.0.46
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: LicenseRef-MainSequence-Proprietary
@@ -30,6 +30,9 @@ internally and sends a standard A2A message.
30
30
  - Do not expose runtime credentials in prompts, logs, or user-facing answers.
31
31
  - Do not use commands that require both target agent UID and target session UID for chat.
32
32
  - Use `--strict-dictionary` when the target answer must be machine-parseable JSON.
33
+ - Read `a2a_profile` from discovery, default to `message`, and never infer Task
34
+ support.
35
+ - Use `--response-kind task` only when the selected Agent advertises `task`.
33
36
  - Do not send or request reasoning/thinking traces; standard A2A responses expose only message parts.
34
37
 
35
38
  ## Discovery Flow
@@ -53,6 +56,7 @@ Agent's environment.
53
56
  5. Prefer the highest `combined_score` when present.
54
57
  6. If the user asked only which agents are available, summarize the candidates and stop.
55
58
  Include agent name, agent UID, and a short capability summary.
59
+ 7. Preserve the selected Agent's `a2a_profile` for request validation.
56
60
 
57
61
  ## Target Session
58
62
 
@@ -106,6 +110,15 @@ mainsequence agent session a2a send \
106
110
  --message "Summarize the current portfolio drift."
107
111
  ```
108
112
 
113
+ For advertised asynchronous work, explicitly request a Task:
114
+
115
+ ```bash
116
+ mainsequence agent session a2a send \
117
+ <target_agent_session_uid> \
118
+ --message "Run the full analysis asynchronously." \
119
+ --response-kind task
120
+ ```
121
+
109
122
  For a second message to the same target session, send another chat command
110
123
  with the same target `AgentSession` UID:
111
124
 
@@ -189,7 +202,11 @@ handle_unique_id -> AgentSession.uid -> A2A message.contextId
189
202
  Direct SDK flow:
190
203
 
191
204
  ```python
192
- from mainsequence.client.agent_runtime_models import Agent, AgentSession
205
+ from mainsequence.client.agent_runtime_models import (
206
+ A2AResponseKind,
207
+ Agent,
208
+ AgentSession,
209
+ )
193
210
 
194
211
 
195
212
  agent = Agent.get_by_uid(target_agent_uid)
@@ -202,7 +219,24 @@ response = AgentSession.send_a2a_message(
202
219
  session.uid,
203
220
  message="Return a JSON object with keys: summary, risk_level, next_action.",
204
221
  strict_dictionary=True,
222
+ response_kind=A2AResponseKind.MESSAGE,
223
+ a2a_profile=agent.a2a_profile,
224
+ )
225
+ ```
226
+
227
+ Use asynchronous Task mode only when discovery advertises it:
228
+
229
+ ```python
230
+ if A2AResponseKind.TASK not in agent.a2a_profile.supported_response_kinds:
231
+ raise ValueError("The selected agent does not advertise A2A Task responses")
232
+
233
+ result = AgentSession.send_a2a_message(
234
+ session,
235
+ message="Run the full analysis asynchronously.",
236
+ response_kind=A2AResponseKind.TASK,
237
+ a2a_profile=agent.a2a_profile,
205
238
  )
239
+ task = AgentSession.wait_for_a2a_task(session, task_id=result.task.id)
206
240
  ```
207
241
 
208
242
  For another message in the same conversation, reuse the same `session.uid`:
@@ -267,12 +301,19 @@ handle for a different task or conversation.
267
301
  ## Response Handling
268
302
 
269
303
  - Parse the CLI output as the standard A2A JSON response.
270
- - Consume only `message.parts` from the A2A response.
304
+ - The default `message` result is direct execution and does not create an A2A
305
+ Task. Consume only its documented `message.parts`.
306
+ - A `task` result is valid only after explicitly selecting `--response-kind
307
+ task` for an Agent whose discovery profile advertises it. Preserve its ID and
308
+ follow it with the SDK task get/wait/cancel helpers.
309
+ - Treat a result kind different from the requested kind as a protocol error.
271
310
  - Do not depend on reasoning, tool traces, runtime paths, or transport metadata.
272
311
  - If a send times out or disconnects and must be retried, reuse the same
273
- `--message-id` value so the target can treat it as the same logical message.
312
+ `--message-id` value to preserve request identity. Direct `message` sends do
313
+ not have durable replay protection, so do not retry automatically: the first
314
+ turn may already have executed. Use `task` when durable recovery is required.
274
315
  - If the CLI generated the message id and the send failed, reuse the
275
- `A2A message id for exact retry` value printed with the error.
316
+ `A2A message id for request retry` value printed with the error.
276
317
 
277
318
  ## Deterministic Execution Path
278
319
 
@@ -282,7 +323,9 @@ handle for a different task or conversation.
282
323
  4. Create or resolve the target session with `mainsequence agent session get_or_create`.
283
324
  5. Use the returned session `uid`.
284
325
  6. Run `mainsequence agent session a2a send <target_agent_session_uid> ...`.
285
- 7. Parse `message.parts` from the CLI output.
326
+ 7. Inspect the selected Agent's `a2a_profile` and default to `message`.
327
+ 8. Parse `message.parts`, or follow the returned Task only when Task mode was
328
+ explicitly selected and advertised.
286
329
 
287
330
  ## Role-Specific Behavior
288
331
 
@@ -1417,7 +1417,7 @@ def send_agent_session_a2a_message(
1417
1417
  message_id: str | None = None,
1418
1418
  strict_dictionary: bool = False,
1419
1419
  json_repair_attempts: int = 3,
1420
- return_immediately: bool = False,
1420
+ response_kind: str = "message",
1421
1421
  timeout: int | None = None,
1422
1422
  ) -> dict[str, Any]:
1423
1423
  """
@@ -1437,7 +1437,7 @@ def send_agent_session_a2a_message(
1437
1437
  cached_runtime_access=cached_runtime_access,
1438
1438
  strict_dictionary=strict_dictionary,
1439
1439
  json_repair_attempts=json_repair_attempts,
1440
- return_immediately=return_immediately,
1440
+ response_kind=response_kind,
1441
1441
  timeout=timeout,
1442
1442
  ),
1443
1443
  )
@@ -1463,7 +1463,7 @@ def _send_agent_session_a2a_message_with_cache(
1463
1463
  cached_runtime_access: dict[str, Any] | None,
1464
1464
  strict_dictionary: bool,
1465
1465
  json_repair_attempts: int,
1466
- return_immediately: bool,
1466
+ response_kind: str,
1467
1467
  timeout: int | None,
1468
1468
  ):
1469
1469
  if cached_runtime_access is not None:
@@ -1476,7 +1476,7 @@ def _send_agent_session_a2a_message_with_cache(
1476
1476
  message_id=message_id,
1477
1477
  strict_dictionary=strict_dictionary,
1478
1478
  json_repair_attempts=json_repair_attempts,
1479
- return_immediately=return_immediately,
1479
+ response_kind=response_kind,
1480
1480
  timeout=timeout,
1481
1481
  )
1482
1482
 
@@ -3788,7 +3788,7 @@ def _agent_session_a2a_send_impl(
3788
3788
  strict_dictionary: bool,
3789
3789
  json_repair_attempts: int,
3790
3790
  message_id: str | None,
3791
- return_immediately: bool,
3791
+ response_kind: str,
3792
3792
  timeout: int | None,
3793
3793
  ) -> None:
3794
3794
  try:
@@ -3806,6 +3806,9 @@ def _agent_session_a2a_send_impl(
3806
3806
  if json_repair_attempts < 1:
3807
3807
  error("--json-repair-attempts must be greater than 0.")
3808
3808
  raise typer.Exit(1)
3809
+ if response_kind not in {"message", "task"}:
3810
+ error("--response-kind must be 'message' or 'task'.")
3811
+ raise typer.Exit(1)
3809
3812
  attachment_paths = list(files or [])
3810
3813
  attachment_media_types = list(media_types or [])
3811
3814
  if attachment_media_types and len(attachment_media_types) != len(attachment_paths):
@@ -3841,12 +3844,12 @@ def _agent_session_a2a_send_impl(
3841
3844
  message_id=effective_message_id,
3842
3845
  strict_dictionary=strict_dictionary,
3843
3846
  json_repair_attempts=json_repair_attempts,
3844
- return_immediately=return_immediately,
3847
+ response_kind=response_kind,
3845
3848
  timeout=timeout,
3846
3849
  )
3847
3850
  except ApiError as e:
3848
3851
  error(str(e))
3849
- error(f"A2A message id for exact retry: {effective_message_id}")
3852
+ error(f"A2A message id for request retry: {effective_message_id}")
3850
3853
  raise typer.Exit(1) from e
3851
3854
 
3852
3855
  typer.echo(json.dumps(response_payload, indent=2))
@@ -4991,12 +4994,12 @@ def agent_session_a2a_send_cmd(
4991
4994
  message_id: str | None = typer.Option(
4992
4995
  None,
4993
4996
  "--message-id",
4994
- help="Stable A2A message.messageId to reuse for an exact retry.",
4997
+ help="Stable A2A message.messageId to preserve request identity across a retry.",
4995
4998
  ),
4996
- return_immediately: bool = typer.Option(
4997
- False,
4998
- "--return-immediately",
4999
- help="Ask the runtime to return immediately instead of waiting for final output.",
4999
+ response_kind: str = typer.Option(
5000
+ "message",
5001
+ "--response-kind",
5002
+ help="Select the A2A result contract: message or task.",
5000
5003
  ),
5001
5004
  timeout: int | None = typer.Option(None, "--timeout", help="Request timeout in seconds"),
5002
5005
  ):
@@ -5015,7 +5018,7 @@ def agent_session_a2a_send_cmd(
5015
5018
  strict_dictionary=strict_dictionary,
5016
5019
  json_repair_attempts=json_repair_attempts,
5017
5020
  message_id=message_id,
5018
- return_immediately=return_immediately,
5021
+ response_kind=response_kind,
5019
5022
  timeout=timeout,
5020
5023
  )
5021
5024
 
@@ -20,7 +20,11 @@ DEFAULT_AGENT_SESSION_LONG_REQUEST_TIMEOUT = (5.0, 900.0)
20
20
  DEFAULT_AGENT_RUNTIME_ACCESS_CACHE_TTL_SECONDS = 60.0
21
21
  DEFAULT_AGENT_RUNTIME_ACCESS_CACHE_EXPIRY_SKEW_SECONDS = 30.0
22
22
  STANDARD_A2A_MESSAGE_SEND_PATH = "/api/a2a/v1/message:send"
23
+ STANDARD_A2A_TASKS_PATH = "/api/a2a/v1/tasks"
23
24
  STANDARD_A2A_CONTENT_TYPE = "application/a2a+json"
25
+ STANDARD_A2A_RESPONSE_KIND_EXTENSION_URI = (
26
+ "https://mainsequence.ai/a2a/extensions/response-kind/v1"
27
+ )
24
28
  STANDARD_A2A_OUTPUT_CONTRACT_METADATA_KEY = (
25
29
  "https://mainsequence.ai/a2a/extensions/output-contract/v1"
26
30
  )
@@ -35,6 +39,67 @@ class AgentSessionStatus(str, Enum):
35
39
  CANCELED = "canceled"
36
40
 
37
41
 
42
+ class A2AResponseKind(str, Enum):
43
+ MESSAGE = "message"
44
+ TASK = "task"
45
+
46
+
47
+ class AgentA2AProfile(BasePydanticModel):
48
+ response_kind_extension_uri: str = STANDARD_A2A_RESPONSE_KIND_EXTENSION_URI
49
+ supported_response_kinds: list[A2AResponseKind] = Field(
50
+ default_factory=lambda: [A2AResponseKind.MESSAGE]
51
+ )
52
+ default_response_kind: A2AResponseKind = A2AResponseKind.MESSAGE
53
+
54
+ @model_validator(mode="after")
55
+ def _validate_default(self) -> AgentA2AProfile:
56
+ if self.response_kind_extension_uri != STANDARD_A2A_RESPONSE_KIND_EXTENSION_URI:
57
+ raise ValueError("response_kind_extension_uri is not supported")
58
+ self.supported_response_kinds = list(dict.fromkeys(self.supported_response_kinds))
59
+ if not self.supported_response_kinds:
60
+ raise ValueError("supported_response_kinds must not be empty")
61
+ if A2AResponseKind.MESSAGE not in self.supported_response_kinds:
62
+ raise ValueError("supported_response_kinds must include 'message'")
63
+ if self.default_response_kind is not A2AResponseKind.MESSAGE:
64
+ raise ValueError("default_response_kind must be 'message'")
65
+ return self
66
+
67
+
68
+ class A2ATaskStatus(BasePydanticModel):
69
+ model_config = ConfigDict(extra="allow")
70
+
71
+ state: str
72
+ timestamp: str | None = None
73
+ message: Any | None = None
74
+
75
+
76
+ class A2ATask(BasePydanticModel):
77
+ model_config = ConfigDict(extra="allow", populate_by_name=True, serialize_by_alias=True)
78
+
79
+ kind: Literal["task"] = "task"
80
+ id: str
81
+ context_id: str = Field(alias="contextId")
82
+ status: A2ATaskStatus
83
+ artifacts: list[dict[str, Any]] = Field(default_factory=list)
84
+
85
+
86
+ class A2AMessageSendResult(BasePydanticModel):
87
+ model_config = ConfigDict(serialize_by_alias=True)
88
+
89
+ message: dict[str, Any] | None = None
90
+ task: A2ATask | None = None
91
+
92
+ @model_validator(mode="after")
93
+ def _require_exact_result(self) -> A2AMessageSendResult:
94
+ if (self.message is None) == (self.task is None):
95
+ raise ValueError("A2A message-send result must contain exactly one of message or task")
96
+ return self
97
+
98
+ @property
99
+ def response_kind(self) -> A2AResponseKind:
100
+ return A2AResponseKind.TASK if self.task is not None else A2AResponseKind.MESSAGE
101
+
102
+
38
103
  class AgentHarnessKind(str, Enum):
39
104
  PI = "pi"
40
105
  TAU = "tau"
@@ -56,6 +121,7 @@ class AgentSemanticSearchResult(BasePydanticModel):
56
121
  "",
57
122
  description="Short description returned by semantic search for the matched agent.",
58
123
  )
124
+ a2a_profile: AgentA2AProfile = Field(default_factory=AgentA2AProfile)
59
125
  project_branch_uid: str = Field(
60
126
  ...,
61
127
  description="Public UID of the ProjectBranch that owns the matched Project Coding Agent.",
@@ -278,6 +344,7 @@ class Agent(ShareableObjectMixin, BaseObjectOrm, BasePydanticModel):
278
344
  None,
279
345
  description="Optional structured agent card payload.",
280
346
  )
347
+ a2a_profile: AgentA2AProfile = Field(default_factory=AgentA2AProfile)
281
348
 
282
349
  llm_thinking: str
283
350
  llm_provider: str = Field(
@@ -958,7 +1025,7 @@ class AgentSession(BaseObjectOrm, BasePydanticModel):
958
1025
  message_id: str | None = None,
959
1026
  strict_dictionary: bool = False,
960
1027
  json_repair_attempts: int = 3,
961
- return_immediately: bool = False,
1028
+ response_kind: A2AResponseKind = A2AResponseKind.MESSAGE,
962
1029
  ) -> dict[str, Any]:
963
1030
  normalized_message = str(message)
964
1031
  if not normalized_message.strip():
@@ -970,6 +1037,10 @@ class AgentSession(BaseObjectOrm, BasePydanticModel):
970
1037
  normalized_message_id = f"msg-{uuid.uuid4()}"
971
1038
 
972
1039
  accepted_output_modes = ["application/json"] if strict_dictionary else ["text/plain"]
1040
+ try:
1041
+ normalized_response_kind = A2AResponseKind(response_kind)
1042
+ except ValueError as exc:
1043
+ raise ValueError("response_kind must be 'message' or 'task'") from exc
973
1044
  parts: list[dict[str, Any]] = [{"text": normalized_message}]
974
1045
  for file_spec in files or []:
975
1046
  parts.append(AgentSession._build_standard_a2a_raw_file_part(file_spec))
@@ -983,7 +1054,7 @@ class AgentSession(BaseObjectOrm, BasePydanticModel):
983
1054
  },
984
1055
  "configuration": {
985
1056
  "acceptedOutputModes": accepted_output_modes,
986
- "returnImmediately": bool(return_immediately),
1057
+ "responseKind": normalized_response_kind.value,
987
1058
  },
988
1059
  }
989
1060
  if strict_dictionary:
@@ -1060,6 +1131,7 @@ class AgentSession(BaseObjectOrm, BasePydanticModel):
1060
1131
  "Authorization": f"Bearer {access.token.strip()}",
1061
1132
  "Content-Type": STANDARD_A2A_CONTENT_TYPE,
1062
1133
  "Accept": STANDARD_A2A_CONTENT_TYPE,
1134
+ "A2A-Extensions": STANDARD_A2A_RESPONSE_KIND_EXTENSION_URI,
1063
1135
  }
1064
1136
  return requests.post(
1065
1137
  url,
@@ -1069,8 +1141,10 @@ class AgentSession(BaseObjectOrm, BasePydanticModel):
1069
1141
  )
1070
1142
 
1071
1143
  @staticmethod
1072
- def extract_a2a_message_text(payload: dict[str, Any]) -> str:
1073
- message = payload.get("message")
1144
+ def extract_a2a_message_text(
1145
+ payload: dict[str, Any] | A2AMessageSendResult,
1146
+ ) -> str:
1147
+ message = payload.message if isinstance(payload, A2AMessageSendResult) else payload.get("message")
1074
1148
  if not isinstance(message, dict):
1075
1149
  return ""
1076
1150
  parts = message.get("parts")
@@ -1092,13 +1166,38 @@ class AgentSession(BaseObjectOrm, BasePydanticModel):
1092
1166
  message_id: str | None = None,
1093
1167
  strict_dictionary: bool = False,
1094
1168
  json_repair_attempts: int = 3,
1095
- return_immediately: bool = False,
1169
+ response_kind: A2AResponseKind = A2AResponseKind.MESSAGE,
1170
+ a2a_profile: AgentA2AProfile | dict[str, Any] | None = None,
1096
1171
  timeout=None,
1097
- ) -> dict[str, Any]:
1172
+ ) -> A2AMessageSendResult:
1098
1173
  """
1099
1174
  Send one standard A2A message to the runtime for this agent session.
1100
1175
  """
1101
1176
  session_uid = cls._resolve_agent_session_uid(agent_session)
1177
+ normalized_response_kind = A2AResponseKind(response_kind)
1178
+ resolved_profile: AgentA2AProfile | None = None
1179
+ if a2a_profile is not None:
1180
+ resolved_profile = (
1181
+ a2a_profile
1182
+ if isinstance(a2a_profile, AgentA2AProfile)
1183
+ else AgentA2AProfile.model_validate(a2a_profile)
1184
+ )
1185
+ elif normalized_response_kind is A2AResponseKind.TASK:
1186
+ session = (
1187
+ agent_session
1188
+ if isinstance(agent_session, AgentSession)
1189
+ else cls.get(pk=session_uid, timeout=timeout)
1190
+ )
1191
+ if not session.agent_uid:
1192
+ raise ValueError("Task response_kind requires a session with an agent_uid")
1193
+ resolved_profile = Agent.get(pk=session.agent_uid, timeout=timeout).a2a_profile
1194
+ if (
1195
+ resolved_profile is not None
1196
+ and normalized_response_kind not in resolved_profile.supported_response_kinds
1197
+ ):
1198
+ raise ValueError(
1199
+ f"response_kind {normalized_response_kind.value!r} is not advertised by the agent"
1200
+ )
1102
1201
  body = cls._build_standard_a2a_message_send_body(
1103
1202
  agent_session_uid=session_uid,
1104
1203
  message=message,
@@ -1106,7 +1205,7 @@ class AgentSession(BaseObjectOrm, BasePydanticModel):
1106
1205
  message_id=message_id,
1107
1206
  strict_dictionary=strict_dictionary,
1108
1207
  json_repair_attempts=json_repair_attempts,
1109
- return_immediately=return_immediately,
1208
+ response_kind=normalized_response_kind,
1110
1209
  )
1111
1210
  access = cls._resolve_runtime_access_for_message_send(session_uid, timeout=timeout)
1112
1211
  response = cls._post_standard_a2a_message(
@@ -1140,7 +1239,150 @@ class AgentSession(BaseObjectOrm, BasePydanticModel):
1140
1239
  ) from exc
1141
1240
  if not isinstance(payload, dict):
1142
1241
  raise TypeError("Standard A2A response must be a JSON object")
1143
- return payload
1242
+ try:
1243
+ result = A2AMessageSendResult.model_validate(payload)
1244
+ except ValueError as exc:
1245
+ raise ApiError(
1246
+ "Standard A2A response must contain exactly one valid message or task result.",
1247
+ response=response,
1248
+ payload=body,
1249
+ ) from exc
1250
+ if result.response_kind is not normalized_response_kind:
1251
+ raise ApiError(
1252
+ "Standard A2A response kind does not match the requested response_kind.",
1253
+ response=response,
1254
+ payload=body,
1255
+ )
1256
+ return result
1257
+
1258
+ @classmethod
1259
+ def _request_a2a_task(
1260
+ cls,
1261
+ agent_session: str | AgentSession,
1262
+ *,
1263
+ task_id: str,
1264
+ cancel: bool,
1265
+ timeout=None,
1266
+ ) -> A2ATask:
1267
+ session_uid = cls._resolve_agent_session_uid(agent_session)
1268
+ normalized_task_id = str(task_id or "").strip()
1269
+ if not normalized_task_id or "/" in normalized_task_id:
1270
+ raise ValueError("task_id must be a non-empty A2A task identifier")
1271
+
1272
+ def request(access: AgentSessionRuntimeAccess) -> requests.Response:
1273
+ if access.mode != "token":
1274
+ raise ApiError("Coding-agent runtime access is unavailable.")
1275
+ if not isinstance(access.token, str) or not access.token.strip():
1276
+ raise ApiError("Runtime access response is missing token.")
1277
+ path = f"{STANDARD_A2A_TASKS_PATH}/{normalized_task_id}"
1278
+ method = "GET"
1279
+ if cancel:
1280
+ path = f"{path}:cancel"
1281
+ method = "POST"
1282
+ request_timeout = (
1283
+ DEFAULT_AGENT_SESSION_LONG_REQUEST_TIMEOUT if timeout is None else timeout
1284
+ )
1285
+ return requests.request(
1286
+ method,
1287
+ _join_runtime_url(access.rpc_url, path),
1288
+ headers={
1289
+ "Authorization": f"Bearer {access.token.strip()}",
1290
+ "Accept": STANDARD_A2A_CONTENT_TYPE,
1291
+ },
1292
+ timeout=request_timeout,
1293
+ )
1294
+
1295
+ access = cls._resolve_runtime_access_for_message_send(session_uid, timeout=timeout)
1296
+ response = request(access)
1297
+ if response.status_code in (401, 403):
1298
+ cls.clear_cached_runtime_access(session_uid)
1299
+ access = cls.resolve_runtime_access(session_uid, cache=True, timeout=timeout)
1300
+ response = request(access)
1301
+ if response.status_code in (401, 403):
1302
+ cls.clear_cached_runtime_access(session_uid)
1303
+ if not (200 <= response.status_code < 300):
1304
+ raise_for_response(response)
1305
+ try:
1306
+ payload = response.json()
1307
+ except Exception as exc:
1308
+ raise ApiError(
1309
+ "Standard A2A task response must be a JSON object.",
1310
+ response=response,
1311
+ ) from exc
1312
+ task_payload = payload.get("task") if isinstance(payload, dict) else None
1313
+ if not isinstance(task_payload, dict):
1314
+ raise ApiError(
1315
+ "Standard A2A task response must contain a task object.",
1316
+ response=response,
1317
+ )
1318
+ return A2ATask.model_validate(task_payload)
1319
+
1320
+ @classmethod
1321
+ def get_a2a_task(
1322
+ cls,
1323
+ agent_session: str | AgentSession,
1324
+ *,
1325
+ task_id: str,
1326
+ timeout=None,
1327
+ ) -> A2ATask:
1328
+ return cls._request_a2a_task(
1329
+ agent_session,
1330
+ task_id=task_id,
1331
+ cancel=False,
1332
+ timeout=timeout,
1333
+ )
1334
+
1335
+ @classmethod
1336
+ def cancel_a2a_task(
1337
+ cls,
1338
+ agent_session: str | AgentSession,
1339
+ *,
1340
+ task_id: str,
1341
+ timeout=None,
1342
+ ) -> A2ATask:
1343
+ return cls._request_a2a_task(
1344
+ agent_session,
1345
+ task_id=task_id,
1346
+ cancel=True,
1347
+ timeout=timeout,
1348
+ )
1349
+
1350
+ @classmethod
1351
+ def wait_for_a2a_task(
1352
+ cls,
1353
+ agent_session: str | AgentSession,
1354
+ *,
1355
+ task_id: str,
1356
+ poll_interval_seconds: float = 1.0,
1357
+ wait_timeout_seconds: float = 900.0,
1358
+ timeout=None,
1359
+ ) -> A2ATask:
1360
+ if poll_interval_seconds <= 0:
1361
+ raise ValueError("poll_interval_seconds must be greater than 0")
1362
+ if wait_timeout_seconds <= 0:
1363
+ raise ValueError("wait_timeout_seconds must be greater than 0")
1364
+ deadline = time.monotonic() + wait_timeout_seconds
1365
+ terminal_states = {
1366
+ "completed",
1367
+ "failed",
1368
+ "canceled",
1369
+ "rejected",
1370
+ "TASK_STATE_COMPLETED",
1371
+ "TASK_STATE_FAILED",
1372
+ "TASK_STATE_CANCELED",
1373
+ "TASK_STATE_REJECTED",
1374
+ }
1375
+ while True:
1376
+ task = cls.get_a2a_task(
1377
+ agent_session,
1378
+ task_id=task_id,
1379
+ timeout=timeout,
1380
+ )
1381
+ if task.status.state in terminal_states:
1382
+ return task
1383
+ if time.monotonic() >= deadline:
1384
+ raise TimeoutError(f"Timed out waiting for A2A task {task_id}")
1385
+ time.sleep(poll_interval_seconds)
1144
1386
 
1145
1387
  @classmethod
1146
1388
  def resolve_runtime_access(
@@ -1415,7 +1657,12 @@ class AgentSession(BaseObjectOrm, BasePydanticModel):
1415
1657
 
1416
1658
 
1417
1659
  __all__ = [
1660
+ "A2AMessageSendResult",
1661
+ "A2AResponseKind",
1662
+ "A2ATask",
1663
+ "A2ATaskStatus",
1418
1664
  "Agent",
1665
+ "AgentA2AProfile",
1419
1666
  "AgentRuntimeImageDrift",
1420
1667
  "AgentRuntimeImageDriftCheck",
1421
1668
  "AgentHarnessKind",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 6.0.45
3
+ Version: 6.0.46
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: LicenseRef-MainSequence-Proprietary
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mainsequence"
7
- version = "6.0.45"
7
+ version = "6.0.46"
8
8
  description = "Main Sequence SDK "
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -6867,7 +6867,7 @@ def test_agent_session_a2a_send_always_returns_json(cli_mod, runner, monkeypatch
6867
6867
  assert captured["kwargs"]["message_id"] == "msg-client-1"
6868
6868
  assert captured["kwargs"]["strict_dictionary"] is True
6869
6869
  assert captured["kwargs"]["json_repair_attempts"] == 3
6870
- assert captured["kwargs"]["return_immediately"] is False
6870
+ assert captured["kwargs"]["response_kind"] == "message"
6871
6871
  assert "omit_reasoning" not in captured["kwargs"]
6872
6872
 
6873
6873