apodex-agent-core 0.3.0__py3-none-any.whl
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.
- agent_core/__init__.py +32 -0
- agent_core/components/__init__.py +1 -0
- agent_core/components/agent_bus/__init__.py +107 -0
- agent_core/components/agent_bus/agent_comm.py +278 -0
- agent_core/components/agent_bus/bus.py +2407 -0
- agent_core/components/agent_bus/fan_in.py +436 -0
- agent_core/components/agent_bus/models.py +246 -0
- agent_core/components/agent_bus/runtime.py +501 -0
- agent_core/components/agent_bus/shared_pool.py +43 -0
- agent_core/components/agent_bus/spawn_guard.py +341 -0
- agent_core/components/agent_bus/stop_signal.py +78 -0
- agent_core/components/finalization/__init__.py +53 -0
- agent_core/components/finalization/budget.py +267 -0
- agent_core/components/finalization/recovery.py +308 -0
- agent_core/components/middleware/__init__.py +1 -0
- agent_core/components/middleware/llm/__init__.py +1 -0
- agent_core/components/middleware/llm/base.py +233 -0
- agent_core/components/middleware/llm/proxy.py +276 -0
- agent_core/components/middleware/llm/skill_injection.py +232 -0
- agent_core/components/middleware/llm/stream_repetition.py +160 -0
- agent_core/components/middleware/llm/summarization.py +203 -0
- agent_core/components/observers/__init__.py +1 -0
- agent_core/components/observers/budget_observer.py +75 -0
- agent_core/components/observers/context_size_guard.py +103 -0
- agent_core/components/observers/finalization_reserve.py +57 -0
- agent_core/components/observers/last_turn_forcer.py +40 -0
- agent_core/components/observers/leaked_tool_call_retry.py +289 -0
- agent_core/components/observers/react_step_tracker.py +166 -0
- agent_core/components/observers/repetition_guard.py +175 -0
- agent_core/components/observers/sse_observer.py +170 -0
- agent_core/components/observers/stop_signal_observer.py +70 -0
- agent_core/components/observers/stuck_target_guard.py +499 -0
- agent_core/components/observers/task_board.py +52 -0
- agent_core/components/observers/text_repetition_guard.py +184 -0
- agent_core/components/observers/trajectory.py +650 -0
- agent_core/components/observers/wall_clock_guard.py +86 -0
- agent_core/components/observers/wall_clock_observer.py +161 -0
- agent_core/components/skills/__init__.py +13 -0
- agent_core/components/skills/allowlist_loader.py +55 -0
- agent_core/components/skills/config.py +49 -0
- agent_core/components/skills/extensions_config.py +134 -0
- agent_core/components/skills/file_system_loader.py +241 -0
- agent_core/context/__init__.py +16 -0
- agent_core/context/blob_store.py +63 -0
- agent_core/context/manager.py +81 -0
- agent_core/context/models.py +54 -0
- agent_core/context/notes.py +44 -0
- agent_core/context/projection.py +44 -0
- agent_core/context/protocols.py +40 -0
- agent_core/context/sqlite_store.py +209 -0
- agent_core/errors.py +164 -0
- agent_core/events.py +24 -0
- agent_core/execution_context.py +171 -0
- agent_core/llm.py +107 -0
- agent_core/llm_utils.py +27 -0
- agent_core/loop_types.py +709 -0
- agent_core/messages.py +267 -0
- agent_core/models/__init__.py +1 -0
- agent_core/models/agent_definition.py +51 -0
- agent_core/models/agent_message.py +27 -0
- agent_core/models/event.py +238 -0
- agent_core/models/node_context.py +82 -0
- agent_core/models/pipeline_spec.py +253 -0
- agent_core/models/task.py +57 -0
- agent_core/models/task_budget.py +133 -0
- agent_core/protocols.py +230 -0
- agent_core/providers/__init__.py +80 -0
- agent_core/providers/anthropic.py +724 -0
- agent_core/providers/aux_builder.py +202 -0
- agent_core/providers/fallback.py +794 -0
- agent_core/providers/finish_reason.py +65 -0
- agent_core/providers/nonblocking_stream.py +160 -0
- agent_core/providers/openai_chat.py +562 -0
- agent_core/providers/openai_responses.py +439 -0
- agent_core/providers/prompt_cache.py +189 -0
- agent_core/providers/protocol_client.py +261 -0
- agent_core/providers/summary.py +262 -0
- agent_core/py.typed +0 -0
- agent_core/retry_policy.py +29 -0
- agent_core/runtime/__init__.py +40 -0
- agent_core/runtime/dag/__init__.py +14 -0
- agent_core/runtime/dag/base_state.py +33 -0
- agent_core/runtime/dag/graph_builder.py +543 -0
- agent_core/runtime/dag/minidag.py +328 -0
- agent_core/runtime/env.py +35 -0
- agent_core/runtime/events.py +98 -0
- agent_core/runtime/llm_request_overrides.py +68 -0
- agent_core/runtime/loop/__init__.py +126 -0
- agent_core/runtime/loop/_bind.py +201 -0
- agent_core/runtime/loop/_call.py +1192 -0
- agent_core/runtime/loop/_response.py +468 -0
- agent_core/runtime/loop/_runaway.py +358 -0
- agent_core/runtime/loop/_streaming.py +584 -0
- agent_core/runtime/loop/agent_loop.py +1381 -0
- agent_core/runtime/loop/budget_consistency.py +128 -0
- agent_core/runtime/loop/compact.py +506 -0
- agent_core/runtime/loop/compact_llm.py +616 -0
- agent_core/runtime/loop/context_budget.py +113 -0
- agent_core/runtime/loop/guardrails.py +221 -0
- agent_core/runtime/loop/llm_client.py +69 -0
- agent_core/runtime/loop/message_trimmer.py +173 -0
- agent_core/runtime/loop/model_profile.py +672 -0
- agent_core/runtime/loop/summary_prompt.py +300 -0
- agent_core/runtime/loop/tiered_compact.py +626 -0
- agent_core/runtime/loop/tokenizer.py +81 -0
- agent_core/runtime/loop/tool_call_parser.py +726 -0
- agent_core/runtime/loop/tool_call_recovery.py +107 -0
- agent_core/runtime/loop/tool_call_repair.py +204 -0
- agent_core/runtime/loop/tool_exec.py +390 -0
- agent_core/runtime/pause_check.py +87 -0
- agent_core/runtime/registries/__init__.py +7 -0
- agent_core/runtime/registries/agents.py +66 -0
- agent_core/runtime/registries/scope.py +119 -0
- agent_core/runtime/registries/services.py +155 -0
- agent_core/runtime/registries/workflows.py +96 -0
- agent_core/runtime/resources/__init__.py +22 -0
- agent_core/runtime/resources/llm.py +54 -0
- agent_core/runtime/resources/manager.py +188 -0
- agent_core/runtime/resources/permissions.py +45 -0
- agent_core/runtime/retriable.py +553 -0
- agent_core/runtime/session_history.py +365 -0
- agent_core/runtime/spill.py +452 -0
- agent_core/runtime/tool_permission.py +151 -0
- agent_core/runtime/usage_meter.py +213 -0
- agent_core/runtime/wall_time_lease.py +243 -0
- agent_core/scheduling/__init__.py +27 -0
- agent_core/scheduling/pipeline_registry.py +32 -0
- agent_core/scheduling/scheduler.py +580 -0
- agent_core/scheduling/topology_registry.py +57 -0
- agent_core/scheduling/workflow_defaults.py +51 -0
- agent_core/tokens.py +87 -0
- agent_core/tool.py +297 -0
- agent_core/types.py +63 -0
- agent_core/utils/__init__.py +1 -0
- agent_core/utils/language.py +347 -0
- apodex_agent_core-0.3.0.dist-info/METADATA +267 -0
- apodex_agent_core-0.3.0.dist-info/RECORD +139 -0
- apodex_agent_core-0.3.0.dist-info/WHEEL +4 -0
- apodex_agent_core-0.3.0.dist-info/licenses/LICENSE +201 -0
agent_core/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Product-neutral building blocks for Apodex agent runtimes."""
|
|
2
|
+
|
|
3
|
+
from agent_core.errors import AgentCoreError
|
|
4
|
+
from agent_core.events import EventType
|
|
5
|
+
from agent_core.execution_context import ExecutionScope, build_execution_scope
|
|
6
|
+
from agent_core.llm import LLMClient, LLMResponse, StreamDelta
|
|
7
|
+
from agent_core.messages import (
|
|
8
|
+
Message,
|
|
9
|
+
ToolCall,
|
|
10
|
+
assistant_msg,
|
|
11
|
+
system_msg,
|
|
12
|
+
tool_msg,
|
|
13
|
+
user_msg,
|
|
14
|
+
)
|
|
15
|
+
from agent_core.types import TaskStatus
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AgentCoreError",
|
|
19
|
+
"EventType",
|
|
20
|
+
"ExecutionScope",
|
|
21
|
+
"LLMClient",
|
|
22
|
+
"LLMResponse",
|
|
23
|
+
"Message",
|
|
24
|
+
"StreamDelta",
|
|
25
|
+
"TaskStatus",
|
|
26
|
+
"ToolCall",
|
|
27
|
+
"assistant_msg",
|
|
28
|
+
"build_execution_scope",
|
|
29
|
+
"system_msg",
|
|
30
|
+
"tool_msg",
|
|
31
|
+
"user_msg",
|
|
32
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Portable agent runtime components."""
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""AgentBus — multi-agent dispatch, communication, and spawn control.
|
|
2
|
+
|
|
3
|
+
Also serves as the back-compat entry point for ``agent_core.components.agent_bus``
|
|
4
|
+
(the former flat-file module). Re-exports the public API from submodules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from agent_core.components.agent_bus.agent_comm import (
|
|
8
|
+
AgentComm,
|
|
9
|
+
DeliveryMode,
|
|
10
|
+
EventStoreContractError,
|
|
11
|
+
event_ordinal,
|
|
12
|
+
)
|
|
13
|
+
from agent_core.components.agent_bus.bus import (
|
|
14
|
+
AgentBus,
|
|
15
|
+
configure_default_event_sink_resolver,
|
|
16
|
+
configure_default_pause_check_factory,
|
|
17
|
+
configure_default_runtime_hooks,
|
|
18
|
+
configure_default_session_activity,
|
|
19
|
+
configure_default_tool_call_parser,
|
|
20
|
+
)
|
|
21
|
+
from agent_core.components.agent_bus.fan_in import (
|
|
22
|
+
COMPLETE_STOP_REASONS,
|
|
23
|
+
INCOMPLETE_STOP_REASONS,
|
|
24
|
+
ORCHESTRATOR_AGENT_NAME,
|
|
25
|
+
CompletionInfo,
|
|
26
|
+
CompletionPolicy,
|
|
27
|
+
FanInBatch,
|
|
28
|
+
classify_completion,
|
|
29
|
+
format_report_block,
|
|
30
|
+
format_status_line,
|
|
31
|
+
format_status_report_block,
|
|
32
|
+
process_collected,
|
|
33
|
+
session_state,
|
|
34
|
+
)
|
|
35
|
+
from agent_core.components.agent_bus.models import (
|
|
36
|
+
CollectResult,
|
|
37
|
+
DepthLimitExceeded,
|
|
38
|
+
JobEntry,
|
|
39
|
+
PendingSessionTask,
|
|
40
|
+
SessionWaitOutcome,
|
|
41
|
+
SubAgentResult,
|
|
42
|
+
SubAgentRuntimeSpec,
|
|
43
|
+
SubAgentSession,
|
|
44
|
+
SubTask,
|
|
45
|
+
)
|
|
46
|
+
from agent_core.components.agent_bus.runtime import (
|
|
47
|
+
adapt_default_session_result,
|
|
48
|
+
adapt_default_subagent_result,
|
|
49
|
+
build_default_subagent_loop_config,
|
|
50
|
+
build_default_subagent_observers,
|
|
51
|
+
build_session_loop_config,
|
|
52
|
+
close_session_boundary_aborted,
|
|
53
|
+
resolve_session_observers,
|
|
54
|
+
)
|
|
55
|
+
from agent_core.components.agent_bus.shared_pool import SharedArtifactPool
|
|
56
|
+
from agent_core.components.agent_bus.spawn_guard import (
|
|
57
|
+
BudgetExhausted,
|
|
58
|
+
SpawnDepthExceeded,
|
|
59
|
+
SpawnGuard,
|
|
60
|
+
SpawnReservation,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"COMPLETE_STOP_REASONS",
|
|
65
|
+
"INCOMPLETE_STOP_REASONS",
|
|
66
|
+
"ORCHESTRATOR_AGENT_NAME",
|
|
67
|
+
"AgentBus",
|
|
68
|
+
"AgentComm",
|
|
69
|
+
"BudgetExhausted",
|
|
70
|
+
"CollectResult",
|
|
71
|
+
"CompletionInfo",
|
|
72
|
+
"CompletionPolicy",
|
|
73
|
+
"DeliveryMode",
|
|
74
|
+
"DepthLimitExceeded",
|
|
75
|
+
"EventStoreContractError",
|
|
76
|
+
"FanInBatch",
|
|
77
|
+
"JobEntry",
|
|
78
|
+
"PendingSessionTask",
|
|
79
|
+
"SessionWaitOutcome",
|
|
80
|
+
"SharedArtifactPool",
|
|
81
|
+
"SpawnDepthExceeded",
|
|
82
|
+
"SpawnGuard",
|
|
83
|
+
"SpawnReservation",
|
|
84
|
+
"SubAgentResult",
|
|
85
|
+
"SubAgentRuntimeSpec",
|
|
86
|
+
"SubAgentSession",
|
|
87
|
+
"SubTask",
|
|
88
|
+
"adapt_default_session_result",
|
|
89
|
+
"adapt_default_subagent_result",
|
|
90
|
+
"build_default_subagent_loop_config",
|
|
91
|
+
"build_default_subagent_observers",
|
|
92
|
+
"build_session_loop_config",
|
|
93
|
+
"classify_completion",
|
|
94
|
+
"close_session_boundary_aborted",
|
|
95
|
+
"configure_default_event_sink_resolver",
|
|
96
|
+
"configure_default_pause_check_factory",
|
|
97
|
+
"configure_default_runtime_hooks",
|
|
98
|
+
"configure_default_session_activity",
|
|
99
|
+
"configure_default_tool_call_parser",
|
|
100
|
+
"event_ordinal",
|
|
101
|
+
"format_report_block",
|
|
102
|
+
"format_status_line",
|
|
103
|
+
"format_status_report_block",
|
|
104
|
+
"process_collected",
|
|
105
|
+
"resolve_session_observers",
|
|
106
|
+
"session_state",
|
|
107
|
+
]
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""AgentComm — inter-agent communication service.
|
|
2
|
+
|
|
3
|
+
Sends typed AgentMessages, persists them as KernelEvents,
|
|
4
|
+
and publishes to EventBus for real-time streaming.
|
|
5
|
+
|
|
6
|
+
Phase D (Issue #24): DeliveryMode (TRIGGER/QUEUE) for message routing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import itertools
|
|
13
|
+
import logging
|
|
14
|
+
from enum import StrEnum
|
|
15
|
+
from typing import Protocol, runtime_checkable
|
|
16
|
+
|
|
17
|
+
from agent_core.events import EventType
|
|
18
|
+
from agent_core.models.agent_message import AgentMessage
|
|
19
|
+
from agent_core.models.event import KernelEvent
|
|
20
|
+
from agent_core.protocols import EventReader
|
|
21
|
+
from agent_core.runtime.events import EventBus
|
|
22
|
+
from agent_core.types import TaskId
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EventStoreContractError(RuntimeError):
|
|
28
|
+
"""The event store violated the :class:`_AgentCommEventStore` contract.
|
|
29
|
+
|
|
30
|
+
Distinct from a transient read failure on purpose: a caller that treats
|
|
31
|
+
read errors as soft (``AgentBus`` result recovery does) must still
|
|
32
|
+
surface a misconfigured store loudly, because every future read will
|
|
33
|
+
fail the same way.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def event_ordinal(event: KernelEvent) -> int:
|
|
38
|
+
"""Return the store-assigned total-order ordinal of ``event``.
|
|
39
|
+
|
|
40
|
+
Prefers the explicit :attr:`KernelEvent.seq`. Falls back to a decimal
|
|
41
|
+
``id`` so a store that exposes its integer primary key as the event id
|
|
42
|
+
keeps working unchanged.
|
|
43
|
+
|
|
44
|
+
Anything else cannot carry a total order — an unset id, or the opaque
|
|
45
|
+
hex of :func:`agent_core.types.new_event_id`, whose values are neither
|
|
46
|
+
numeric nor monotonic. Those are rejected with an actionable
|
|
47
|
+
:class:`EventStoreContractError` rather than the bare ``ValueError``
|
|
48
|
+
``int()`` would raise, because the fix is a store change and the
|
|
49
|
+
message has to say so.
|
|
50
|
+
"""
|
|
51
|
+
if event.seq is not None:
|
|
52
|
+
return event.seq
|
|
53
|
+
raw = str(event.id)
|
|
54
|
+
if raw.isdecimal():
|
|
55
|
+
return int(raw)
|
|
56
|
+
raise EventStoreContractError(
|
|
57
|
+
"Event store must stamp every appended event with a total-order "
|
|
58
|
+
"ordinal: set KernelEvent.seq to a monotonically increasing integer "
|
|
59
|
+
"(a decimal KernelEvent.id is also accepted). Got "
|
|
60
|
+
f"id={raw!r}, seq=None — an opaque id such as new_event_id() cannot "
|
|
61
|
+
"order a durable cursor, so AgentComm.consume cannot advance safely."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@runtime_checkable
|
|
66
|
+
class _AgentCommEventStore(EventReader, Protocol):
|
|
67
|
+
"""AgentComm needs a whole-event append plus cursored reads
|
|
68
|
+
(:class:`~agent_core.protocols.EventReader`) — combined here because
|
|
69
|
+
Python lacks an intersection type. Public Protocols stay minimal in
|
|
70
|
+
``core.protocols``.
|
|
71
|
+
|
|
72
|
+
``append`` takes a :class:`KernelEvent`, *not* the flattened
|
|
73
|
+
``(task_id, event_type, payload, agent_role)`` of
|
|
74
|
+
:class:`~agent_core.protocols.EventSink`: an inter-agent message carries
|
|
75
|
+
``from_agent`` / ``to_agent`` / ``message_type`` / ``correlation_id`` as
|
|
76
|
+
first-class fields that the reader queries on, and ``EventSink`` cannot
|
|
77
|
+
express them. A host that implements only ``EventSink`` is a valid
|
|
78
|
+
telemetry sink for ``AgentBus`` but is *not* a valid AgentComm store.
|
|
79
|
+
|
|
80
|
+
The combination has a stronger semantic contract than either half:
|
|
81
|
+
``append`` retains the event and returns it stamped with a total-order
|
|
82
|
+
ordinal (see :func:`event_ordinal`), and sequential awaited appends
|
|
83
|
+
receive increasing ordinals. That ordinal — not ``timestamp`` — is the
|
|
84
|
+
total order, and ``EventReader``'s ``after_id`` cursor is expressed in
|
|
85
|
+
it. Concurrent appends have no defined relative order, only that every
|
|
86
|
+
reader observes the same resulting order.
|
|
87
|
+
|
|
88
|
+
Ordering is not guaranteed across live writer processes sharing one
|
|
89
|
+
JSONL path. Nor is losslessness guaranteed for a capacity-bounded
|
|
90
|
+
in-memory store, which evicts its oldest retained events.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
async def append(self, event: KernelEvent) -> KernelEvent: ...
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class DeliveryMode(StrEnum):
|
|
97
|
+
"""Message delivery mode.
|
|
98
|
+
|
|
99
|
+
TRIGGER: persist + hot queue + EventBus broadcast.
|
|
100
|
+
Use when receiver must act immediately (e.g., assertion → critic).
|
|
101
|
+
QUEUE: persist + hot queue only (no broadcast).
|
|
102
|
+
Use for status updates the receiver pulls when ready.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
TRIGGER = "trigger"
|
|
106
|
+
QUEUE = "queue"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AgentComm:
|
|
110
|
+
"""Sends inter-agent messages and logs them as events.
|
|
111
|
+
|
|
112
|
+
All messages are persisted to EventStore (truth source).
|
|
113
|
+
Hot queues are optional in-memory acceleration.
|
|
114
|
+
DeliveryMode controls whether EventBus broadcast fires.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
event_store: _AgentCommEventStore,
|
|
120
|
+
event_bus: EventBus,
|
|
121
|
+
) -> None:
|
|
122
|
+
self._event_store = event_store
|
|
123
|
+
self._event_bus = event_bus
|
|
124
|
+
# Each message type gets an independent cursor. A specialized
|
|
125
|
+
# consumer (for example AgentBus result recovery) must not advance
|
|
126
|
+
# the generic inbox cursor or consume status/control messages that
|
|
127
|
+
# belong to another component.
|
|
128
|
+
self._cursors: dict[tuple[str, str | None, str | None], int] = {}
|
|
129
|
+
self._hot_queues: dict[str, asyncio.Queue[KernelEvent]] = {}
|
|
130
|
+
|
|
131
|
+
async def send(
|
|
132
|
+
self,
|
|
133
|
+
msg: AgentMessage,
|
|
134
|
+
mode: DeliveryMode = DeliveryMode.QUEUE,
|
|
135
|
+
) -> KernelEvent:
|
|
136
|
+
"""Send an agent message.
|
|
137
|
+
|
|
138
|
+
Always persists to EventStore (truth source).
|
|
139
|
+
TRIGGER mode additionally broadcasts via EventBus for immediate wakeup.
|
|
140
|
+
"""
|
|
141
|
+
event = KernelEvent(
|
|
142
|
+
task_id=TaskId(msg.task_id),
|
|
143
|
+
event_type=EventType.AGENT_MESSAGE,
|
|
144
|
+
from_agent=msg.from_agent,
|
|
145
|
+
to_agent=msg.to_agent,
|
|
146
|
+
message_type=msg.message_type,
|
|
147
|
+
correlation_id=msg.content.get("correlation_id"),
|
|
148
|
+
payload={
|
|
149
|
+
"message_id": msg.id,
|
|
150
|
+
"from_agent": msg.from_agent,
|
|
151
|
+
"to_agent": msg.to_agent,
|
|
152
|
+
"message_type": msg.message_type,
|
|
153
|
+
"content": msg.content,
|
|
154
|
+
"parent_id": msg.parent_id,
|
|
155
|
+
"correlation_id": msg.content.get("correlation_id"),
|
|
156
|
+
"delivery_mode": mode.value,
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# 1. Persist (always — EventStore is truth source)
|
|
161
|
+
persisted = await self._event_store.append(event)
|
|
162
|
+
|
|
163
|
+
# 2. Hot queue (always — non-durable acceleration)
|
|
164
|
+
queue = self._hot_queues.get(msg.to_agent)
|
|
165
|
+
if queue is not None:
|
|
166
|
+
try:
|
|
167
|
+
queue.put_nowait(persisted)
|
|
168
|
+
except asyncio.QueueFull:
|
|
169
|
+
logger.debug(
|
|
170
|
+
"Hot queue for %s is full; consumer uses EventStore",
|
|
171
|
+
msg.to_agent,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# 3. Broadcast (TRIGGER only — immediate wakeup signal)
|
|
175
|
+
if mode == DeliveryMode.TRIGGER:
|
|
176
|
+
await self._event_bus.publish(
|
|
177
|
+
event.event_type, event.payload,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
logger.debug(
|
|
181
|
+
"Agent message %s: %s → %s [%s] mode=%s",
|
|
182
|
+
msg.id, msg.from_agent, msg.to_agent,
|
|
183
|
+
msg.message_type, mode.value,
|
|
184
|
+
)
|
|
185
|
+
return persisted
|
|
186
|
+
|
|
187
|
+
async def consume(
|
|
188
|
+
self,
|
|
189
|
+
agent_id: str,
|
|
190
|
+
*,
|
|
191
|
+
task_id: str | None = None,
|
|
192
|
+
limit: int = 50,
|
|
193
|
+
message_type: str | None = None,
|
|
194
|
+
) -> list[KernelEvent]:
|
|
195
|
+
"""Consume undelivered messages for an agent using an idempotent cursor.
|
|
196
|
+
|
|
197
|
+
Cursor is per ``(agent_id, task_id, message_type)``. Typed consumers
|
|
198
|
+
therefore cannot skip or discard unrelated inbox traffic. Restarting
|
|
199
|
+
from 0 replays all messages in the selected stream.
|
|
200
|
+
|
|
201
|
+
Ordering (L2-2): messages come back in send order within a task.
|
|
202
|
+
That rests entirely on the stronger ``_AgentCommEventStore``
|
|
203
|
+
contract above and the "Ordering" section of
|
|
204
|
+
``core.protocols.EventReader``. Three consequences callers rely on:
|
|
205
|
+
|
|
206
|
+
* a sender that awaits each ``send`` sees its own messages here
|
|
207
|
+
in exactly that order — never reordered, never with a later
|
|
208
|
+
message ahead of an earlier one;
|
|
209
|
+
* the cursor only moves forward, so each message reaches this
|
|
210
|
+
``(agent, task, message_type)`` reader exactly once even when
|
|
211
|
+
``limit`` truncates the batch — the remainder arrives on the
|
|
212
|
+
next call, still in order;
|
|
213
|
+
* ``message_type`` is part of the cursor key on purpose: a
|
|
214
|
+
shared cursor would let one consumer's read carry another
|
|
215
|
+
consumer's mail past unread.
|
|
216
|
+
|
|
217
|
+
``tests/test_agent_comm_store_contract.py`` pins all three, plus
|
|
218
|
+
the ordinal requirement itself, against a reference store.
|
|
219
|
+
"""
|
|
220
|
+
cursor_key = (agent_id, task_id, message_type)
|
|
221
|
+
after_id = self._cursors.get(cursor_key, 0)
|
|
222
|
+
# Preserve compatibility with third-party EventReader implementations
|
|
223
|
+
# written before typed inbox queries existed.
|
|
224
|
+
if message_type is None:
|
|
225
|
+
events = await self._event_store.get_events_for_agent(
|
|
226
|
+
agent_id,
|
|
227
|
+
after_id=after_id,
|
|
228
|
+
limit=limit,
|
|
229
|
+
task_id=task_id,
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
events = await self._event_store.get_events_for_agent(
|
|
233
|
+
agent_id,
|
|
234
|
+
after_id=after_id,
|
|
235
|
+
limit=limit,
|
|
236
|
+
task_id=task_id,
|
|
237
|
+
message_type=message_type,
|
|
238
|
+
)
|
|
239
|
+
if events:
|
|
240
|
+
ids = [event_ordinal(event) for event in events]
|
|
241
|
+
if any(current <= previous for previous, current in itertools.pairwise(ids)):
|
|
242
|
+
# Advancing to max(ids) can permanently skip lower ids that
|
|
243
|
+
# a misordered, limited query left outside this batch. Keep
|
|
244
|
+
# the cursor untouched and fail loudly instead of returning
|
|
245
|
+
# a partial stream with silent data loss.
|
|
246
|
+
logger.error(
|
|
247
|
+
"Event store returned %d events for %s outside strict "
|
|
248
|
+
"ascending id order (%s); cursor remains at %d",
|
|
249
|
+
len(ids), agent_id, ids, after_id,
|
|
250
|
+
)
|
|
251
|
+
raise RuntimeError(
|
|
252
|
+
"EventReader must return events in strictly ascending id order"
|
|
253
|
+
)
|
|
254
|
+
self._cursors[cursor_key] = ids[-1]
|
|
255
|
+
return events
|
|
256
|
+
|
|
257
|
+
def reset_cursor(
|
|
258
|
+
self,
|
|
259
|
+
agent_id: str,
|
|
260
|
+
task_id: str | None = None,
|
|
261
|
+
*,
|
|
262
|
+
message_type: str | None = None,
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Reset one typed cursor, or every cursor for an agent/task pair."""
|
|
265
|
+
if message_type is not None:
|
|
266
|
+
self._cursors.pop((agent_id, task_id, message_type), None)
|
|
267
|
+
return
|
|
268
|
+
for key in list(self._cursors):
|
|
269
|
+
if key[:2] == (agent_id, task_id):
|
|
270
|
+
self._cursors.pop(key, None)
|
|
271
|
+
|
|
272
|
+
def hot_queue_for(
|
|
273
|
+
self, agent_id: str,
|
|
274
|
+
) -> asyncio.Queue[KernelEvent]:
|
|
275
|
+
"""Return the non-durable hot queue for an agent."""
|
|
276
|
+
return self._hot_queues.setdefault(
|
|
277
|
+
agent_id, asyncio.Queue(maxsize=256),
|
|
278
|
+
)
|