ms-tau-sdk 2.0.22.dev73__tar.gz → 2.0.23.dev75__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 (87) hide show
  1. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/CHANGELOG.md +17 -0
  2. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/PKG-INFO +1 -1
  3. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/pyproject.toml +1 -1
  4. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/api/a2a.py +190 -128
  5. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/.gitignore +0 -0
  6. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/README.md +0 -0
  7. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/packages/tau-board/src/ms_tau_board/__init__.py +0 -0
  8. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/packages/tau-board/src/ms_tau_board/app.py +0 -0
  9. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/packages/tau-board/src/ms_tau_board/cli.py +0 -0
  10. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/packages/tau-board/src/ms_tau_board/config.py +0 -0
  11. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/packages/tau-board/src/ms_tau_board/env_file.py +0 -0
  12. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/packages/tau-board/src/ms_tau_board/logs.py +0 -0
  13. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/packages/tau-board/src/ms_tau_board/proxy.py +0 -0
  14. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/packages/tau-board/src/ms_tau_board/state.py +0 -0
  15. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/packages/tau-board/src/ms_tau_board/static/BULMA-LICENSE.txt +0 -0
  16. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/packages/tau-board/src/ms_tau_board/static/app.js +0 -0
  17. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/packages/tau-board/src/ms_tau_board/static/board.css +0 -0
  18. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/packages/tau-board/src/ms_tau_board/static/bulma.min.css +0 -0
  19. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/packages/tau-board/src/ms_tau_board/static/index.html +0 -0
  20. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/__init__.py +0 -0
  21. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/agent_skills/tau_a2a_runtime_adapter/SKILL.md +0 -0
  22. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/agent_skills/tau_local_development/SKILL.md +0 -0
  23. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/agent_skills/tau_project_customization/SKILL.md +0 -0
  24. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/agent_skills/tau_repository_integration/SKILL.md +0 -0
  25. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/agent_skills/tau_security_and_access/SKILL.md +0 -0
  26. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/api/__init__.py +0 -0
  27. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/api/chat.py +0 -0
  28. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/api/conversations.py +0 -0
  29. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/api/dependencies.py +0 -0
  30. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/api/health.py +0 -0
  31. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/api/inspection.py +0 -0
  32. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/api/local_chat.py +0 -0
  33. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/api/models.py +0 -0
  34. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/api/request_identity.py +0 -0
  35. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/api/sessions.py +0 -0
  36. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/app.py +0 -0
  37. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/application.py +0 -0
  38. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/backend/__init__.py +0 -0
  39. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/backend/assertions.py +0 -0
  40. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/backend/auth.py +0 -0
  41. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/backend/client.py +0 -0
  42. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/backend/local.py +0 -0
  43. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/backend/mcp.py +0 -0
  44. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/backend/models.py +0 -0
  45. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/backend/routes.py +0 -0
  46. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/backend/team_memberships.py +0 -0
  47. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/cli.py +0 -0
  48. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/errors.py +0 -0
  49. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/logging.py +0 -0
  50. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/protocols/__init__.py +0 -0
  51. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/protocols/a2a_failure.py +0 -0
  52. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/protocols/a2a_message.py +0 -0
  53. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/protocols/a2a_roles.py +0 -0
  54. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/protocols/assistant_ui.py +0 -0
  55. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/protocols/chat_history.py +0 -0
  56. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/protocols/strict_json.py +0 -0
  57. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/providers/__init__.py +0 -0
  58. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/providers/definitions.py +0 -0
  59. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/providers/factory.py +0 -0
  60. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/providers/tau_compat.py +0 -0
  61. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/resources/SYSTEM.md +0 -0
  62. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/resources/__init__.py +0 -0
  63. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/resources/loader.py +0 -0
  64. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/resources/prompts/review-code-repository.md +0 -0
  65. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/runtime/__init__.py +0 -0
  66. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/runtime/deployment_health.py +0 -0
  67. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/runtime/events.py +0 -0
  68. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/runtime/extensions.py +0 -0
  69. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/runtime/failures.py +0 -0
  70. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/runtime/live_turns.py +0 -0
  71. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/runtime/manager.py +0 -0
  72. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/runtime/observability.py +0 -0
  73. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/runtime/provenance.py +0 -0
  74. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/runtime/requester.py +0 -0
  75. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/runtime/session.py +0 -0
  76. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/runtime/snapshots.py +0 -0
  77. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/runtime/task_context.py +0 -0
  78. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/sessions/__init__.py +0 -0
  79. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/sessions/storage.py +0 -0
  80. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/settings.py +0 -0
  81. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/skills.py +0 -0
  82. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/tools/__init__.py +0 -0
  83. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/tools/mainsequence_mcp.py +0 -0
  84. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/tools/mcp_applications.py +0 -0
  85. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/tools/mcp_connection.py +0 -0
  86. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/tools/skill_read.py +0 -0
  87. {ms_tau_sdk-2.0.22.dev73 → ms_tau_sdk-2.0.23.dev75}/src/ms_tau_sdk/tools/task_control.py +0 -0
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.23 — 2026-10-21
4
+
5
+ - **A hosted Task's answer reaches the platform in a few writes, and none of it is lost
6
+ ([#116](https://github.com/mainsequence-sdk/ms-tau-sdk/issues/116)).** While a Task attempt
7
+ checked for cancellation or wrote its answer, it stopped reading the model. With platform calls
8
+ slower than the 200-millisecond write interval, every small piece of text became its own write,
9
+ each preceded by another read of the Task, so an answer took minutes to reach the platform. The
10
+ check and the writes now run in the background, one of each at a time, while the turn keeps
11
+ reading the model; text that arrives meanwhile joins the next write. In a test with 60-millisecond
12
+ platform calls, a 40-piece answer takes 4 writes and 9 reads instead of 41 and 86. Cancellation,
13
+ the lease check on each write, the order of the text and settlement are unchanged, and nothing the
14
+ writer started keeps running once the attempt ends.
15
+ - When an answer was written in more than one batch, the text still waiting at the end was
16
+ dropped: the output was finalized without writing it. It is now written first.
17
+ - The poll interval now runs from when the previous cancellation check ended, not when it
18
+ started.
19
+
3
20
  ## 2.0.22 — 2026-10-21
4
21
 
5
22
  - **A hosted Task's answer reaches the caller of GetTask
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ms-tau-sdk
3
- Version: 2.0.22.dev73
3
+ Version: 2.0.23.dev75
4
4
  Summary: Workspace-bound Tau application primitives for Main Sequence projects
5
5
  Project-URL: Changelog, https://github.com/mainsequence-sdk/ms-tau-sdk/blob/development/CHANGELOG.md
6
6
  Project-URL: Documentation, https://github.com/mainsequence-sdk/ms-tau-sdk/tree/development/docs
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ms-tau-sdk"
7
- version = "2.0.22.dev73"
7
+ version = "2.0.23.dev75"
8
8
  description = "Workspace-bound Tau application primitives for Main Sequence projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -1289,6 +1289,15 @@ async def _settle_execution_failure(
1289
1289
 
1290
1290
  @dataclass(slots=True)
1291
1291
  class _DurableArtifactWriter:
1292
+ """Write a Task attempt's text answer as one durable output.
1293
+
1294
+ The routine cancellation read and each batch's write run in the background, one of each at a
1295
+ time, so the turn keeps reading the model while the platform answers; text that arrives
1296
+ meanwhile joins the next batch. A batch that reaches ``flush_bytes`` waits for the write before
1297
+ it, which bounds the text held. A failure or cancellation found in the background is raised by
1298
+ the writer's next call. ``close()`` stops background work before the attempt settles.
1299
+ """
1300
+
1292
1301
  client: MainSequenceClient
1293
1302
  manager: SessionRuntimeManager
1294
1303
  task: AgentTask
@@ -1303,16 +1312,35 @@ class _DurableArtifactWriter:
1303
1312
  pending_bytes: int = 0
1304
1313
  last_flush_at: float = field(default_factory=time.monotonic)
1305
1314
  last_cancellation_check_at: float = 0.0
1315
+ writing: asyncio.Task[None] | None = None
1316
+ polling: asyncio.Task[None] | None = None
1306
1317
 
1307
1318
  async def ensure_not_canceled(self, *, force: bool = False) -> None:
1308
- now = time.monotonic()
1309
- if (
1310
- not force
1311
- and now - self.last_cancellation_check_at < self.cancellation_poll_interval_seconds
1319
+ """Raise when the Task was canceled; ``force`` reads the Task now and waits for it.
1320
+
1321
+ Otherwise the read starts in the background once the poll interval has passed, and what
1322
+ it finds is raised by a later call.
1323
+ """
1324
+
1325
+ self._observe_cancellation_read()
1326
+ if force:
1327
+ await self._read_cancellation()
1328
+ elif (
1329
+ self.polling is None
1330
+ and time.monotonic() - self.last_cancellation_check_at
1331
+ >= self.cancellation_poll_interval_seconds
1312
1332
  ):
1313
- return
1333
+ self.polling = asyncio.create_task(self._read_cancellation())
1334
+
1335
+ def _observe_cancellation_read(self) -> None:
1336
+ polling = self.polling
1337
+ if polling is not None and polling.done():
1338
+ self.polling = None
1339
+ polling.result()
1340
+
1341
+ async def _read_cancellation(self) -> None:
1314
1342
  current = await self.client.get_task(self.task.uid)
1315
- self.last_cancellation_check_at = now
1343
+ self.last_cancellation_check_at = time.monotonic()
1316
1344
  if current.cancellation_requested or current.status == "canceled":
1317
1345
  raise _TaskCancellationRequested(current)
1318
1346
 
@@ -1321,15 +1349,39 @@ class _DurableArtifactWriter:
1321
1349
  return
1322
1350
  self.pending.append(value)
1323
1351
  self.pending_bytes += len(value.encode("utf-8"))
1352
+ if self.writing is not None:
1353
+ if not self.writing.done() and self.pending_bytes < self.flush_bytes:
1354
+ return
1355
+ await self._finish_write()
1324
1356
  elapsed_ms = (time.monotonic() - self.last_flush_at) * 1000
1325
1357
  if self.pending_bytes >= self.flush_bytes or elapsed_ms >= self.flush_interval_ms:
1326
- await self.flush()
1358
+ self.writing = asyncio.create_task(self._write())
1359
+
1360
+ async def _finish_write(self) -> None:
1361
+ writing, self.writing = self.writing, None
1362
+ if writing is not None:
1363
+ await writing
1327
1364
 
1328
1365
  async def flush(self, *, final: bool = False) -> None:
1366
+ await self._finish_write()
1367
+ await self._write(final=final)
1368
+
1369
+ async def close(self) -> None:
1370
+ """Stop the background read and write, keeping none of their results."""
1371
+
1372
+ running = [work for work in (self.writing, self.polling) if work is not None]
1373
+ self.writing = self.polling = None
1374
+ for work in running:
1375
+ work.cancel()
1376
+ await asyncio.gather(*running, return_exceptions=True)
1377
+
1378
+ async def _write(self, *, final: bool = False) -> None:
1329
1379
  text = "".join(self.pending)
1330
1380
  if not text and not final:
1331
1381
  return
1332
- await self.ensure_not_canceled(force=True)
1382
+ self.pending.clear()
1383
+ self.pending_bytes = 0
1384
+ await self._read_cancellation()
1333
1385
  fence = await _current_task_fence(self.manager, self.task, self.claim)
1334
1386
  created = self.revision == 0
1335
1387
  if created:
@@ -1343,35 +1395,34 @@ class _DurableArtifactWriter:
1343
1395
  name="Agent response",
1344
1396
  )
1345
1397
  self.output_uid = str(result["uid"])
1346
- elif final:
1398
+ elif text:
1347
1399
  if self.output_uid is None:
1348
1400
  raise RuntimeError("Backend Task output UID is unavailable")
1349
- result = await self.client.finalize_task_output(
1401
+ result = await self.client.append_task_output(
1350
1402
  self.task.uid,
1351
1403
  attempt_uid=self.claim.attempt_uid,
1352
1404
  holder_id=fence.holder_id,
1353
1405
  lease_token=fence.lease_token,
1354
1406
  output_uid=self.output_uid,
1355
1407
  expected_revision=self.revision,
1408
+ parts=[{"text": text}],
1356
1409
  )
1357
1410
  else:
1358
1411
  if self.output_uid is None:
1359
1412
  raise RuntimeError("Backend Task output UID is unavailable")
1360
- result = await self.client.append_task_output(
1413
+ result = await self.client.finalize_task_output(
1361
1414
  self.task.uid,
1362
1415
  attempt_uid=self.claim.attempt_uid,
1363
1416
  holder_id=fence.holder_id,
1364
1417
  lease_token=fence.lease_token,
1365
1418
  output_uid=self.output_uid,
1366
1419
  expected_revision=self.revision,
1367
- parts=[{"text": text}] if text else [],
1368
1420
  )
1369
1421
  self.revision = int(result.get("revision") or self.revision + 1)
1370
- self.pending.clear()
1371
- self.pending_bytes = 0
1372
1422
  self.last_flush_at = time.monotonic()
1373
- if final and created:
1374
- await self.flush(final=True)
1423
+ if final and (created or text):
1424
+ # The output is finalized after the text still pending is written, never instead.
1425
+ await self._write(final=True)
1375
1426
 
1376
1427
  async def write_strict_json(self, text: str) -> None:
1377
1428
  await self.ensure_not_canceled(force=True)
@@ -1419,26 +1470,32 @@ async def _collect_task_turn(
1419
1470
  return text
1420
1471
 
1421
1472
  accumulator = _TurnAccumulator(max_output_bytes=max_output_bytes)
1422
- async for event in manager.prompt(
1423
- task.context_id,
1424
- prompt,
1425
- provenance=provenance,
1426
- turn_uid=writer.claim.turn_uid,
1427
- ):
1428
- await writer.ensure_not_canceled()
1429
- accumulator.consume(event)
1430
- value = _event_text(event.type, event.data, has_chunks=bool(accumulator.delta_chunks[:-1]))
1431
- if event.type in {"text_delta", "text-delta"} and value:
1432
- await writer.append_text(value)
1433
- context = active_task_execution()
1434
- if context is not None and context.interruption_status is not None:
1435
- await writer.flush()
1436
- return ""
1437
- text = accumulator.result()
1438
- if not accumulator.delta_chunks:
1439
- await writer.append_text(text)
1440
- await writer.flush(final=True)
1441
- return text
1473
+ try:
1474
+ async for event in manager.prompt(
1475
+ task.context_id,
1476
+ prompt,
1477
+ provenance=provenance,
1478
+ turn_uid=writer.claim.turn_uid,
1479
+ ):
1480
+ await writer.ensure_not_canceled()
1481
+ accumulator.consume(event)
1482
+ value = _event_text(
1483
+ event.type, event.data, has_chunks=bool(accumulator.delta_chunks[:-1])
1484
+ )
1485
+ if event.type in {"text_delta", "text-delta"} and value:
1486
+ await writer.append_text(value)
1487
+ context = active_task_execution()
1488
+ if context is not None and context.interruption_status is not None:
1489
+ await writer.flush()
1490
+ return ""
1491
+ text = accumulator.result()
1492
+ if not accumulator.delta_chunks:
1493
+ await writer.append_text(text)
1494
+ await writer.flush(final=True)
1495
+ return text
1496
+ finally:
1497
+ # Nothing the writer started in the background outlives the turn or reaches settlement.
1498
+ await writer.close()
1442
1499
 
1443
1500
 
1444
1501
  async def _execute_task(
@@ -1884,111 +1941,116 @@ async def _stream_task_events(
1884
1941
  text = ""
1885
1942
  emitted = False
1886
1943
  try:
1887
- await writer.ensure_not_canceled(force=True)
1888
- if output_contract.enabled:
1889
- with task_execution_scope(task_context):
1890
- text = await _collect_validated_turn(
1891
- manager,
1892
- task.context_id,
1893
- prompt,
1894
- output_contract,
1895
- max_output_bytes=max_output_bytes,
1896
- provenance=provenance,
1897
- turn_uid=claim.turn_uid,
1898
- )
1899
- if text and task_context.interruption_status is None:
1900
- await writer.write_strict_json(text)
1901
- yield _artifact_update(
1902
- task,
1903
- artifact_id=artifact_id,
1904
- part={
1905
- "data": json.loads(text),
1906
- "mediaType": "application/json",
1907
- },
1908
- append=False,
1909
- last_chunk=True,
1910
- )
1911
- emitted = True
1912
- else:
1913
- accumulator = _TurnAccumulator(max_output_bytes=max_output_bytes)
1914
- with task_execution_scope(task_context):
1915
- async for event in manager.prompt(
1916
- task.context_id,
1917
- prompt,
1918
- provenance=provenance,
1919
- turn_uid=claim.turn_uid,
1920
- ):
1921
- await writer.ensure_not_canceled()
1922
- accumulator.consume(event)
1923
- if event.type not in {"text_delta", "text-delta"}:
1924
- continue
1925
- value = _event_text(
1926
- event.type,
1927
- event.data,
1928
- has_chunks=bool(accumulator.delta_chunks[:-1]),
1929
- )
1930
- if not value:
1931
- continue
1932
- await writer.append_text(value)
1933
- yield _artifact_update(
1934
- task,
1935
- artifact_id=artifact_id,
1936
- part={"text": value},
1937
- append=emitted,
1938
- last_chunk=False,
1944
+ try:
1945
+ await writer.ensure_not_canceled(force=True)
1946
+ if output_contract.enabled:
1947
+ with task_execution_scope(task_context):
1948
+ text = await _collect_validated_turn(
1949
+ manager,
1950
+ task.context_id,
1951
+ prompt,
1952
+ output_contract,
1953
+ max_output_bytes=max_output_bytes,
1954
+ provenance=provenance,
1955
+ turn_uid=claim.turn_uid,
1939
1956
  )
1940
- emitted = True
1941
- if task_context.interruption_status is None:
1942
- text = accumulator.result()
1943
- if not accumulator.delta_chunks:
1944
- await writer.append_text(text)
1957
+ if text and task_context.interruption_status is None:
1958
+ await writer.write_strict_json(text)
1945
1959
  yield _artifact_update(
1946
1960
  task,
1947
1961
  artifact_id=artifact_id,
1948
- part={"text": text},
1962
+ part={
1963
+ "data": json.loads(text),
1964
+ "mediaType": "application/json",
1965
+ },
1949
1966
  append=False,
1950
- last_chunk=False,
1967
+ last_chunk=True,
1951
1968
  )
1952
1969
  emitted = True
1953
- if task_context.interruption_status is not None:
1954
- await writer.flush()
1970
+ else:
1971
+ accumulator = _TurnAccumulator(max_output_bytes=max_output_bytes)
1972
+ with task_execution_scope(task_context):
1973
+ async for event in manager.prompt(
1974
+ task.context_id,
1975
+ prompt,
1976
+ provenance=provenance,
1977
+ turn_uid=claim.turn_uid,
1978
+ ):
1979
+ await writer.ensure_not_canceled()
1980
+ accumulator.consume(event)
1981
+ if event.type not in {"text_delta", "text-delta"}:
1982
+ continue
1983
+ value = _event_text(
1984
+ event.type,
1985
+ event.data,
1986
+ has_chunks=bool(accumulator.delta_chunks[:-1]),
1987
+ )
1988
+ if not value:
1989
+ continue
1990
+ await writer.append_text(value)
1991
+ yield _artifact_update(
1992
+ task,
1993
+ artifact_id=artifact_id,
1994
+ part={"text": value},
1995
+ append=emitted,
1996
+ last_chunk=False,
1997
+ )
1998
+ emitted = True
1999
+ if task_context.interruption_status is None:
2000
+ text = accumulator.result()
2001
+ if not accumulator.delta_chunks:
2002
+ await writer.append_text(text)
2003
+ yield _artifact_update(
2004
+ task,
2005
+ artifact_id=artifact_id,
2006
+ part={"text": text},
2007
+ append=False,
2008
+ last_chunk=False,
2009
+ )
2010
+ emitted = True
2011
+ if task_context.interruption_status is not None:
2012
+ await writer.flush()
2013
+ await writer.ensure_not_canceled(force=True)
2014
+ interrupted = await _settle_claimed_task(
2015
+ client,
2016
+ manager,
2017
+ task,
2018
+ claim,
2019
+ status=task_context.interruption_status,
2020
+ status_message=task_context.interruption_message,
2021
+ )
2022
+ interrupted = await _task_with_history(
2023
+ client, interrupted, history_length=history_length
2024
+ )
2025
+ yield {"task": _task_payload(interrupted, history_length=history_length)}
2026
+ return
2027
+ if not output_contract.enabled:
2028
+ await writer.flush(final=True)
1955
2029
  await writer.ensure_not_canceled(force=True)
1956
- interrupted = await _settle_claimed_task(
2030
+ completed = await _settle_claimed_task(
1957
2031
  client,
1958
2032
  manager,
1959
2033
  task,
1960
2034
  claim,
1961
- status=task_context.interruption_status,
1962
- status_message=task_context.interruption_message,
1963
- )
1964
- interrupted = await _task_with_history(
1965
- client, interrupted, history_length=history_length
2035
+ status="completed",
1966
2036
  )
1967
- yield {"task": _task_payload(interrupted, history_length=history_length)}
1968
- return
1969
- if not output_contract.enabled:
1970
- await writer.flush(final=True)
1971
- await writer.ensure_not_canceled(force=True)
1972
- completed = await _settle_claimed_task(
1973
- client,
1974
- manager,
1975
- task,
1976
- claim,
1977
- status="completed",
1978
- )
1979
- if emitted and not output_contract.enabled:
1980
- yield _artifact_update(
1981
- task,
1982
- artifact_id=artifact_id,
1983
- part={"text": ""},
1984
- append=True,
1985
- last_chunk=True,
1986
- )
1987
- completed = await _task_with_history(client, completed, history_length=history_length)
1988
- yield {
1989
- "task": _task_payload(completed, history_length=history_length),
1990
- "final": True,
1991
- }
2037
+ if emitted and not output_contract.enabled:
2038
+ yield _artifact_update(
2039
+ task,
2040
+ artifact_id=artifact_id,
2041
+ part={"text": ""},
2042
+ append=True,
2043
+ last_chunk=True,
2044
+ )
2045
+ completed = await _task_with_history(client, completed, history_length=history_length)
2046
+ yield {
2047
+ "task": _task_payload(completed, history_length=history_length),
2048
+ "final": True,
2049
+ }
2050
+ finally:
2051
+ # Nothing the writer started in the background outlives the turn or reaches
2052
+ # settlement.
2053
+ await writer.close()
1992
2054
  except _TaskCancellationRequested as cancellation:
1993
2055
  canceled = cancellation.task
1994
2056
  if canceled.status != "canceled":