bazaar-compute-node 0.1.18__tar.gz → 0.1.20__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 (105) hide show
  1. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/PKG-INFO +1 -1
  2. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/pyproject.toml +1 -1
  3. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/pyproject.toml.orig +1 -1
  4. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/app/command.py +1 -0
  5. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/app/wrapper.py +17 -3
  6. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/bcc.py +4 -1
  7. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/lark/channel.py +33 -1
  8. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/lark/transport.py +34 -9
  9. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/telegram/channel.py +14 -0
  10. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/models/__init__.py +2 -0
  11. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/models/entities.py +13 -0
  12. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/models/states.py +6 -0
  13. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/orchestration/turn.py +49 -25
  14. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/LICENSE +0 -0
  15. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/README.md +0 -0
  16. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/__init__.py +0 -0
  17. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/app/__init__.py +0 -0
  18. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/app/agent.py +0 -0
  19. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/app/agent_management.py +0 -0
  20. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/app/application.py +0 -0
  21. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/app/attachments.py +0 -0
  22. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/app/config.py +0 -0
  23. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/app/registry.py +0 -0
  24. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/app/reminder_dispatch.py +0 -0
  25. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/app/system_service.py +0 -0
  26. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/app/transport.py +0 -0
  27. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/app/windows_pipe.py +0 -0
  28. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/cli.py +0 -0
  29. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/__init__.py +0 -0
  30. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/codex/__init__.py +0 -0
  31. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/codex/approval.py +0 -0
  32. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/codex/client.py +0 -0
  33. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/codex/events.py +0 -0
  34. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/codex/plugin.py +0 -0
  35. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/codex/process.py +0 -0
  36. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/codex/protocol.py +0 -0
  37. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/codex/runtime.py +0 -0
  38. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/lark/__init__.py +0 -0
  39. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/lark/api.py +0 -0
  40. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/lark/approval.py +0 -0
  41. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/lark/attachments.py +0 -0
  42. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/lark/frame.py +0 -0
  43. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/lark/identity.py +0 -0
  44. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/lark/outbound.py +0 -0
  45. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/lark/plugin.py +0 -0
  46. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/logging/__init__.py +0 -0
  47. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/logging/audit.py +0 -0
  48. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/logging/plugin.py +0 -0
  49. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/sqlite/__init__.py +0 -0
  50. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/sqlite/agent_migration.py +0 -0
  51. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/sqlite/codec.py +0 -0
  52. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/sqlite/database.py +0 -0
  53. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/sqlite/migrations.py +0 -0
  54. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/sqlite/plugin.py +0 -0
  55. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/sqlite/reminder_codec.py +0 -0
  56. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/sqlite/reminder_database.py +0 -0
  57. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/sqlite/reminder_migration.py +0 -0
  58. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/sqlite/reminder_repository.py +0 -0
  59. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/sqlite/repository.py +0 -0
  60. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/sqlite/scoped_repository.py +0 -0
  61. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/telegram/__init__.py +0 -0
  62. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/telegram/api.py +0 -0
  63. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/telegram/approval.py +0 -0
  64. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/telegram/attachments.py +0 -0
  65. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/telegram/identity.py +0 -0
  66. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/telegram/markdown.py +0 -0
  67. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/telegram/outbound.py +0 -0
  68. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/telegram/plugin.py +0 -0
  69. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/wecom/__init__.py +0 -0
  70. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/wecom/channel.py +0 -0
  71. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/wecom/markdown.py +0 -0
  72. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/wecom/outbound.py +0 -0
  73. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/contrib/wecom/plugin.py +0 -0
  74. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/__init__.py +0 -0
  75. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/approval.py +0 -0
  76. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/audit.py +0 -0
  77. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/channel.py +0 -0
  78. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/client.py +0 -0
  79. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/command.py +0 -0
  80. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/concurrency.py +0 -0
  81. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/correlation.py +0 -0
  82. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/instruction.py +0 -0
  83. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/lifecycle.py +0 -0
  84. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/models/reminder.py +0 -0
  85. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/models/reminder_owner.py +0 -0
  86. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/observability.py +0 -0
  87. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/orchestration/__init__.py +0 -0
  88. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/orchestration/command.py +0 -0
  89. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/orchestration/delivery.py +0 -0
  90. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/orchestration/error_feedback.py +0 -0
  91. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/orchestration/reminder.py +0 -0
  92. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/orchestration/reminder_command.py +0 -0
  93. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/orchestration/services.py +0 -0
  94. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/orchestration/session.py +0 -0
  95. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/outcomes.py +0 -0
  96. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/paths.py +0 -0
  97. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/reminder.py +0 -0
  98. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/reminder_schedule.py +0 -0
  99. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/runtime.py +0 -0
  100. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/storage.py +0 -0
  101. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/core/timerwheel.py +0 -0
  102. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/i18n/__init__.py +0 -0
  103. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/i18n/catalog.py +0 -0
  104. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/i18n/english.py +0 -0
  105. {bazaar_compute_node-0.1.18 → bazaar_compute_node-0.1.20}/src/bazaar_compute_node/i18n/schinese.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bazaar-compute-node
3
- Version: 0.1.18
3
+ Version: 0.1.20
4
4
  Summary: Runtime-agnostic computer node daemon for agents and channels.
5
5
  License-Expression: AGPL-3.0-only
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bazaar-compute-node"
3
- version = "0.1.18"
3
+ version = "0.1.20"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bazaar-compute-node"
3
- version = "0.1.18"
3
+ version = "0.1.20"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -27,6 +27,7 @@ def serialize_inbound(message: InboundMessage) -> dict[str, object]:
27
27
  if message.sender is None
28
28
  else {"id": message.sender.id, "name": message.sender.name}
29
29
  ),
30
+ "sender_kind": message.sender_kind.value,
30
31
  "message_type": message.message_type,
31
32
  "canonical_target": message.canonical_target,
32
33
  "target_kind": message.target_kind.value,
@@ -32,16 +32,30 @@ def install_bcc_wrapper(bin_dir: Path, *, agent_id: str) -> Path:
32
32
  command_path.write_text(
33
33
  "@echo off\n"
34
34
  f'set "BCN_AGENT_ID={agent_id}"\n'
35
- 'set "PYTHONUTF8=1"\n'
36
35
  'set "PYTHONIOENCODING=utf-8"\n'
36
+ 'set "PYTHONUTF8=1"\n'
37
+ 'set "LANG=C.UTF-8"\n'
38
+ 'set "LC_ALL=C.UTF-8"\n'
39
+ "chcp 65001 >NUL 2>NUL\n"
37
40
  f'"{python_executable}" -m bazaar_compute_node.bcc %*\n',
38
41
  encoding="utf-8",
39
42
  )
40
43
  (bin_dir / "bcc.ps1").write_text(
44
+ "$ErrorActionPreference = 'Stop'\n"
41
45
  f"$env:BCN_AGENT_ID = '{agent_id}'\n"
42
- "$env:PYTHONUTF8 = '1'\n"
46
+ "$utf8NoBom = [System.Text.UTF8Encoding]::new($false)\n"
47
+ "[Console]::OutputEncoding = $utf8NoBom\n"
48
+ "$OutputEncoding = $utf8NoBom\n"
43
49
  "$env:PYTHONIOENCODING = 'utf-8'\n"
44
- f'& "{python_executable}" -m bazaar_compute_node.bcc @args\n'
50
+ "$env:PYTHONUTF8 = '1'\n"
51
+ "$env:LANG = 'C.UTF-8'\n"
52
+ "$env:LC_ALL = 'C.UTF-8'\n"
53
+ f'$python = "{python_executable}"\n'
54
+ "if ($MyInvocation.ExpectingInput) {\n"
55
+ " $input | & $python -m bazaar_compute_node.bcc @args\n"
56
+ "} else {\n"
57
+ " & $python -m bazaar_compute_node.bcc @args\n"
58
+ "}\n"
45
59
  "exit $LASTEXITCODE\n",
46
60
  encoding="utf-8",
47
61
  )
@@ -480,6 +480,9 @@ def _message_header_fields(
480
480
  ) -> tuple[str, str, str, str, str | None, str]:
481
481
  target = _require_text(message, "canonical_target")
482
482
  message_id = _require_text(message, "message_id")
483
+ sender_kind = _require_text(message, "sender_kind")
484
+ if sender_kind not in {"human", "agent", "unknown"}:
485
+ _invalid_response("command response contains an invalid sender_kind")
483
486
  sender_value = message.get("sender")
484
487
  sender: str | None = None
485
488
  if sender_value is not None:
@@ -493,7 +496,7 @@ def _message_header_fields(
493
496
  target,
494
497
  message_id,
495
498
  _format_message_timestamp(message),
496
- _require_text(message, "message_type"),
499
+ sender_kind,
497
500
  sender,
498
501
  _require_text(message, "body", allow_empty=True),
499
502
  )
@@ -27,6 +27,7 @@ from ...core.models import (
27
27
  RuntimeEvent,
28
28
  RuntimeEventState,
29
29
  SenderIdentity,
30
+ SenderKind,
30
31
  StreamEvent,
31
32
  )
32
33
  from ...core.outcomes import ProviderCallResult, ProviderCallStatus
@@ -481,6 +482,18 @@ class LarkChannel(IChannel):
481
482
  tenant_key=tenant_key,
482
483
  )
483
484
  metadata = dict(provider_payload_metadata or {})
485
+ sender_type = (
486
+ _provider_text(sender_payload.get("sender_type"))
487
+ if isinstance(sender_payload, Mapping)
488
+ else None
489
+ )
490
+ if sender_type == "user":
491
+ sender_kind = SenderKind.HUMAN
492
+ elif sender_type in {"app", "bot", "system"}:
493
+ sender_kind = SenderKind.AGENT
494
+ else:
495
+ sender_kind = SenderKind.UNKNOWN
496
+ metadata.setdefault("sender_kind", sender_kind.value)
484
497
  if projection.content_error:
485
498
  metadata["content_parse_failed"] = True
486
499
  return InboundMessage(
@@ -758,7 +771,14 @@ class LarkChannel(IChannel):
758
771
  )
759
772
  api = self._api
760
773
  identity = self._identity
761
- if api is None or identity is None or self._state not in {"connected", "ready"}:
774
+ transport = self._transport
775
+ if (
776
+ api is None
777
+ or identity is None
778
+ or transport is None
779
+ or self._state not in {"connected", "ready"}
780
+ or transport.state != "connected"
781
+ ):
762
782
  return ProviderCallResult(
763
783
  status=ProviderCallStatus.FAILED,
764
784
  error_kind="channel_unavailable",
@@ -779,6 +799,18 @@ class LarkChannel(IChannel):
779
799
  error_message="Lark delivery timed out waiting for the send lock",
780
800
  )
781
801
  try:
802
+ if (
803
+ self._api is not api
804
+ or self._identity is not identity
805
+ or self._transport is not transport
806
+ or self._state not in {"connected", "ready"}
807
+ or transport.state != "connected"
808
+ ):
809
+ return ProviderCallResult(
810
+ status=ProviderCallStatus.FAILED,
811
+ error_kind="channel_unavailable",
812
+ error_message="Lark channel is not available",
813
+ )
782
814
  return await send_outbound(
783
815
  api,
784
816
  identity=identity,
@@ -421,6 +421,7 @@ class LarkTransport:
421
421
  started_at = monotonic()
422
422
  self._events_received += 1
423
423
  self._last_event_at_ms = time_ns() // 1_000_000
424
+ ack_sent = False
424
425
  try:
425
426
  handler = self._on_message
426
427
  if handler is None:
@@ -429,6 +430,11 @@ class LarkTransport:
429
430
  self._last_message_filter_reason = "handler_unavailable"
430
431
  await self._send_ack(frame, code=200, started_at=started_at)
431
432
  return
433
+
434
+ direct_response = _is_direct_response(message_type, decoded)
435
+ if not direct_response:
436
+ await self._send_ack(frame, code=200, started_at=started_at)
437
+ ack_sent = True
432
438
  result = handler(message_type, decoded, frame)
433
439
  if asyncio.iscoroutine(result) or isinstance(result, Awaitable):
434
440
  result = await result
@@ -436,8 +442,9 @@ class LarkTransport:
436
442
  ack_payload: bytes | None = None
437
443
  if isinstance(result, LarkAck):
438
444
  accepted = result.accepted
439
- ack_code = result.code
440
- ack_payload = result.payload
445
+ if direct_response:
446
+ ack_code = result.code
447
+ ack_payload = result.payload
441
448
  else:
442
449
  accepted = result is not False
443
450
  if not accepted:
@@ -448,12 +455,14 @@ class LarkTransport:
448
455
  self._messages_queued += 1
449
456
  self._last_message_disposition = "queued"
450
457
  self._last_message_filter_reason = None
451
- await self._send_ack(
452
- frame,
453
- code=ack_code,
454
- payload=ack_payload,
455
- started_at=started_at,
456
- )
458
+ if direct_response:
459
+ await self._send_ack(
460
+ frame,
461
+ code=ack_code,
462
+ payload=ack_payload,
463
+ started_at=started_at,
464
+ )
465
+ ack_sent = True
457
466
  if isinstance(result, LarkAck) and result.post_ack is not None:
458
467
  post_ack_task = asyncio.create_task(
459
468
  _run_post_ack(result.post_ack),
@@ -464,7 +473,8 @@ class LarkTransport:
464
473
  except Exception: # noqa: BLE001
465
474
  self._message_mapping_failures += 1
466
475
  self._last_message_disposition = "failed"
467
- await self._send_ack(frame, code=500, started_at=started_at)
476
+ if not ack_sent:
477
+ await self._send_ack(frame, code=500, started_at=started_at)
468
478
 
469
479
  async def _send_ack(
470
480
  self,
@@ -574,6 +584,21 @@ class LarkTransport:
574
584
  await timer.wait()
575
585
 
576
586
 
587
+ def _is_direct_response(
588
+ message_type: str,
589
+ payload: Mapping[str, object],
590
+ ) -> bool:
591
+ if message_type == MESSAGE_CARD:
592
+ return True
593
+ header = payload.get("header")
594
+ if not isinstance(header, Mapping):
595
+ return False
596
+ return header.get("event_type") in {
597
+ "card.action.trigger",
598
+ "p2.card.action.trigger",
599
+ }
600
+
601
+
577
602
  def _delay_ms(seconds: float) -> int:
578
603
  return math.ceil(max(0.0, seconds) * 1_000)
579
604
 
@@ -24,6 +24,7 @@ from ...core.models import (
24
24
  RuntimeEvent,
25
25
  RuntimeEventState,
26
26
  SenderIdentity,
27
+ SenderKind,
27
28
  )
28
29
  from ...core.outcomes import ProviderCallResult, ProviderCallStatus
29
30
  from ...core.runtime import RuntimeStreamItem
@@ -613,6 +614,7 @@ class TelegramChannel(IChannel):
613
614
  "telegram_chat_id": chat_id,
614
615
  "telegram_message_thread_id": topic_id,
615
616
  "telegram_chat_type": chat_type,
617
+ "sender_kind": self._sender_kind(message).value,
616
618
  "historical": historical,
617
619
  "activation_reason": activation_reason,
618
620
  "rich_message": content.rich_message,
@@ -702,6 +704,7 @@ class TelegramChannel(IChannel):
702
704
  "telegram_chat_id": identity.chat_id,
703
705
  "telegram_message_thread_id": identity.topic_id,
704
706
  "telegram_chat_type": chat_type,
707
+ "sender_kind": self._sender_kind(reply).value,
705
708
  "historical": historical,
706
709
  "activation_reason": "none",
707
710
  "quoted_backfill": True,
@@ -797,6 +800,17 @@ class TelegramChannel(IChannel):
797
800
  )
798
801
  return None
799
802
 
803
+ @staticmethod
804
+ def _sender_kind(message: Mapping[str, object]) -> SenderKind:
805
+ provider_sender = message.get("from")
806
+ if isinstance(provider_sender, Mapping):
807
+ is_bot = provider_sender.get("is_bot")
808
+ if is_bot is True:
809
+ return SenderKind.AGENT
810
+ if is_bot is False:
811
+ return SenderKind.HUMAN
812
+ return SenderKind.UNKNOWN
813
+
800
814
  @staticmethod
801
815
  def _text_projection(
802
816
  message: Mapping[str, object],
@@ -28,6 +28,7 @@ from .states import (
28
28
  ReminderState,
29
29
  RuntimeEventState,
30
30
  RuntimeTurnState,
31
+ SenderKind,
31
32
  SessionRuntimeObservation,
32
33
  SessionRuntimeObservationSource,
33
34
  SessionRuntimeSignal,
@@ -67,6 +68,7 @@ __all__ = [
67
68
  "RuntimeTurn",
68
69
  "RuntimeTurnState",
69
70
  "SenderIdentity",
71
+ "SenderKind",
70
72
  "SessionRuntimeObservation",
71
73
  "SessionRuntimeObservationSource",
72
74
  "SessionRuntimeSignal",
@@ -16,6 +16,7 @@ from .states import (
16
16
  OutboundDeliveryState,
17
17
  RuntimeEventState,
18
18
  RuntimeTurnState,
19
+ SenderKind,
19
20
  StreamEventKind,
20
21
  ensure_transition,
21
22
  )
@@ -306,6 +307,18 @@ class InboundMessage:
306
307
  if self.provider_time_ms is not None:
307
308
  _validate_non_negative(self.provider_time_ms, "provider_time_ms")
308
309
 
310
+ @property
311
+ def sender_kind(self) -> SenderKind:
312
+ value = self.metadata.get("sender_kind", SenderKind.UNKNOWN.value)
313
+ if not isinstance(value, str):
314
+ raise TypeError("metadata sender_kind must be a string")
315
+ try:
316
+ return SenderKind(value)
317
+ except ValueError as error:
318
+ raise ValueError(
319
+ "metadata sender_kind must be human, agent, or unknown"
320
+ ) from error
321
+
309
322
 
310
323
  @dataclass(frozen=True, slots=True)
311
324
  class OutboundMessage:
@@ -276,6 +276,12 @@ class ApprovalDecision(StrEnum):
276
276
  REJECTED = "rejected"
277
277
 
278
278
 
279
+ class SenderKind(StrEnum):
280
+ HUMAN = "human"
281
+ AGENT = "agent"
282
+ UNKNOWN = "unknown"
283
+
284
+
279
285
  class ReminderState(StrEnum):
280
286
  SCHEDULED = "scheduled"
281
287
  FIRED = "fired"
@@ -12,6 +12,7 @@ from ..concurrency import ISessionConcurrency
12
12
  from ..correlation import CorrelationContext
13
13
  from ..lifecycle import TimeoutBudget
14
14
  from ..models import (
15
+ ApprovalDecision,
15
16
  ApprovalRequest,
16
17
  ApprovalResult,
17
18
  BcnSession,
@@ -22,6 +23,7 @@ from ..models import (
22
23
  RuntimeSession,
23
24
  RuntimeTurn,
24
25
  RuntimeTurnState,
26
+ SenderKind,
25
27
  SessionRuntimeObservation,
26
28
  SessionRuntimeObservationSource,
27
29
  SessionRuntimeSignal,
@@ -179,38 +181,58 @@ class SessionTurnCoordinator:
179
181
  request_id=request_id,
180
182
  inbound_seq=message.seq,
181
183
  )
184
+ sender_kind = message.sender_kind
182
185
  await self._audit.append(
183
186
  event_name="approval.requested",
184
187
  state=RuntimeEventState.STARTED,
185
188
  correlation=approval_correlation,
186
- metadata={"action": request.action},
189
+ metadata={
190
+ "action": request.action,
191
+ "sender_kind": sender_kind.value,
192
+ },
187
193
  )
188
- try:
189
- channel_request = ChannelApprovalRequest(
190
- approval=request,
191
- target_kind=context.channel_session.target_kind,
192
- provider_thread_id=context.channel_session.provider_thread_id,
193
- provider_reply_to_message_id=message.provider_message_id,
194
- provider_sender_id=(
195
- message.sender.id if message.sender is not None else None
196
- ),
194
+ if sender_kind is not SenderKind.HUMAN:
195
+ rejection_reason = (
196
+ "agent_cannot_approve"
197
+ if sender_kind is SenderKind.AGENT
198
+ else "unknown_sender_cannot_approve"
197
199
  )
198
- result = await self._channel.request_approval(
199
- channel_request,
200
- timeout=timeout,
200
+ result = ApprovalResult(
201
+ request_id=request_id,
202
+ decision=ApprovalDecision.REJECTED,
203
+ decided_at_ms=self._clock(),
204
+ reason=rejection_reason,
201
205
  )
202
- if result.request_id != request_id:
203
- raise ValueError("channel approval result correlation mismatch")
204
- except Exception as error:
205
- await self._audit.append(
206
- event_name="approval.failed",
207
- state=RuntimeEventState.FAILED,
208
- correlation=approval_correlation,
209
- error_kind=ErrorKind.PROVIDER_FAILED,
210
- error_message=f"approval failed: {type(error).__name__}",
211
- metadata={"action": request.action},
212
- )
213
- raise
206
+ else:
207
+ try:
208
+ channel_request = ChannelApprovalRequest(
209
+ approval=request,
210
+ target_kind=context.channel_session.target_kind,
211
+ provider_thread_id=context.channel_session.provider_thread_id,
212
+ provider_reply_to_message_id=message.provider_message_id,
213
+ provider_sender_id=(
214
+ message.sender.id if message.sender is not None else None
215
+ ),
216
+ )
217
+ result = await self._channel.request_approval(
218
+ channel_request,
219
+ timeout=timeout,
220
+ )
221
+ if result.request_id != request_id:
222
+ raise ValueError("channel approval result correlation mismatch")
223
+ except Exception as error:
224
+ await self._audit.append(
225
+ event_name="approval.failed",
226
+ state=RuntimeEventState.FAILED,
227
+ correlation=approval_correlation,
228
+ error_kind=ErrorKind.PROVIDER_FAILED,
229
+ error_message=f"approval failed: {type(error).__name__}",
230
+ metadata={
231
+ "action": request.action,
232
+ "sender_kind": sender_kind.value,
233
+ },
234
+ )
235
+ raise
214
236
  await self._audit.append(
215
237
  event_name="approval.decided",
216
238
  state=RuntimeEventState.COMPLETED,
@@ -218,6 +240,8 @@ class SessionTurnCoordinator:
218
240
  metadata={
219
241
  "action": request.action,
220
242
  "decision": result.decision.value,
243
+ "sender_kind": sender_kind.value,
244
+ **({"reason": result.reason} if result.reason is not None else {}),
221
245
  },
222
246
  )
223
247
  return result