agentlings 0.2.2__tar.gz → 0.2.3__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.
- {agentlings-0.2.2 → agentlings-0.2.3}/PKG-INFO +1 -1
- {agentlings-0.2.2 → agentlings-0.2.3}/pyproject.toml +1 -1
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/protocol/a2a.py +16 -2
- agentlings-0.2.3/tests/unit/test_a2a_executor.py +292 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/.env.example +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/.github/workflows/ci.yml +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/.github/workflows/publish.yml +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/.gitignore +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/CLAUDE.md +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/DESIGN-memory-sleep.md +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/Dockerfile +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/LICENSE +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/README.md +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/agent.example.yaml +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/docker-compose.test.yml +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/logo.png +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/scripts/release.sh +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/sleep.png +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/__init__.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/__main__.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/cli/__init__.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/cli/_migrations.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/cli/_templates.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/cli/_version.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/cli/init.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/cli/upgrade.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/config.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/__init__.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/completion.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/llm.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/loop.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/memory_models.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/memory_store.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/models.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/prompt.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/scheduler.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/sleep.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/store.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/task.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/telemetry.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/log.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/migrations/__init__.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/migrations/m0001_seed.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/protocol/__init__.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/protocol/a2a_task_store.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/protocol/agent_card.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/protocol/mcp.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/server.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/templates/__init__.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/templates/default/.env.example +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/templates/default/agent.yaml +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/tools/__init__.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/tools/builtins.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/tools/memory.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/tools/registry.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/Dockerfile +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/__init__.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/agent.test.yaml +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/__init__.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/a2a_client.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/conftest.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/mcp_client.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/test_a2a.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/test_agent_card.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/test_mcp.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/test_ollama.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/test_task_flow.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/__init__.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/conftest.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_a2a_task_store.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_agent_card.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_cli_init.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_cli_upgrade.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_completion.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_config.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_live_api.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_llm.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_logging.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_loop.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_mcp_handler.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_memory_models.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_memory_store.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_memory_tool.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_models.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_prompt.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_scheduler.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_sleep.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_store.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_task.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_telemetry.py +0 -0
- {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentlings
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Lightweight A2A + MCP single-process agent framework
|
|
5
5
|
Project-URL: Homepage, https://github.com/andyjmorgan/DonkeyWork-Agentlings
|
|
6
6
|
Project-URL: Repository, https://github.com/andyjmorgan/DonkeyWork-Agentlings
|
|
@@ -7,6 +7,11 @@ A2A ``Task`` object (``status.state == working``) is enqueued so the caller
|
|
|
7
7
|
can poll via ``GetTask`` — those GetTask calls are routed back to our engine
|
|
8
8
|
by ``EngineTaskStore`` so the SDK's answers always reflect live state.
|
|
9
9
|
|
|
10
|
+
Clients can opt out of the await window per-request by setting
|
|
11
|
+
``configuration.return_immediately = true`` on ``message/send``; in that
|
|
12
|
+
case the executor passes ``await_seconds=0`` to the engine and a ``Task``
|
|
13
|
+
object is enqueued without blocking.
|
|
14
|
+
|
|
10
15
|
Cancellation (``CancelTask``) hits the engine's cancel path by task id.
|
|
11
16
|
"""
|
|
12
17
|
|
|
@@ -78,10 +83,19 @@ class AgentlingExecutor(AgentExecutor):
|
|
|
78
83
|
# through EngineTaskStore) and the Task object we enqueue all agree.
|
|
79
84
|
sdk_task_id = context.task_id
|
|
80
85
|
|
|
86
|
+
# A2A 1.0: clients can opt out of blocking by setting
|
|
87
|
+
# ``configuration.return_immediately``. When set, skip the await
|
|
88
|
+
# window so a Task handle is enqueued immediately.
|
|
89
|
+
return_immediately = bool(
|
|
90
|
+
getattr(context.configuration, "return_immediately", False)
|
|
91
|
+
)
|
|
92
|
+
await_seconds = 0.0 if return_immediately else self._await_seconds
|
|
93
|
+
|
|
81
94
|
logger.debug(
|
|
82
|
-
"a2a execute: context_id=%s task_id=%s text=%r",
|
|
95
|
+
"a2a execute: context_id=%s task_id=%s return_immediately=%s text=%r",
|
|
83
96
|
context_id,
|
|
84
97
|
sdk_task_id,
|
|
98
|
+
return_immediately,
|
|
85
99
|
(user_text or "")[:100],
|
|
86
100
|
)
|
|
87
101
|
|
|
@@ -90,7 +104,7 @@ class AgentlingExecutor(AgentExecutor):
|
|
|
90
104
|
message=user_text,
|
|
91
105
|
context_id=context_id,
|
|
92
106
|
via="a2a",
|
|
93
|
-
await_seconds=
|
|
107
|
+
await_seconds=await_seconds,
|
|
94
108
|
task_id=sdk_task_id,
|
|
95
109
|
)
|
|
96
110
|
except ContextBusyError as e:
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Tests for ``AgentlingExecutor`` covering the A2A SendMessage entrypoint.
|
|
2
|
+
|
|
3
|
+
Focus is on protocol-level wiring: that ``configuration.return_immediately``
|
|
4
|
+
is honored as a per-request opt-out of the await window, and that the
|
|
5
|
+
default behavior continues to block until ``AGENT_TASK_AWAIT_SECONDS``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from a2a.helpers.proto_helpers import new_text_message
|
|
15
|
+
from a2a.server.agent_execution.context import RequestContext
|
|
16
|
+
from a2a.server.context import ServerCallContext
|
|
17
|
+
from a2a.server.events import EventQueue
|
|
18
|
+
from a2a.types import (
|
|
19
|
+
Message,
|
|
20
|
+
Role,
|
|
21
|
+
SendMessageConfiguration,
|
|
22
|
+
SendMessageRequest,
|
|
23
|
+
Task,
|
|
24
|
+
)
|
|
25
|
+
from a2a.types import TaskState as A2ATaskState
|
|
26
|
+
|
|
27
|
+
from agentlings.config import AgentConfig
|
|
28
|
+
from agentlings.core.llm import LLMResponse
|
|
29
|
+
from agentlings.core.store import JournalStore
|
|
30
|
+
from agentlings.core.task import TaskEngine
|
|
31
|
+
from agentlings.protocol.a2a import AgentlingExecutor
|
|
32
|
+
from agentlings.tools.registry import ToolRegistry
|
|
33
|
+
from tests.unit.test_task import ControllableLLM
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _LoopShim:
|
|
37
|
+
"""Minimal stand-in for ``MessageLoop`` exposing ``.engine`` only.
|
|
38
|
+
|
|
39
|
+
The executor's constructor reads ``loop.engine`` once at init and never
|
|
40
|
+
touches the loop again. Building the real ``MessageLoop`` would force
|
|
41
|
+
it to spin up its own internal ``TaskEngine``, defeating the point of
|
|
42
|
+
sharing the test-controlled engine.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, engine: TaskEngine) -> None:
|
|
46
|
+
self.engine = engine
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _make_request_context(
|
|
50
|
+
*,
|
|
51
|
+
text: str,
|
|
52
|
+
context_id: str,
|
|
53
|
+
task_id: str,
|
|
54
|
+
return_immediately: bool | None = None,
|
|
55
|
+
) -> RequestContext:
|
|
56
|
+
msg = new_text_message(text, context_id=context_id, role=Role.ROLE_USER)
|
|
57
|
+
if return_immediately is None:
|
|
58
|
+
configuration = None
|
|
59
|
+
else:
|
|
60
|
+
configuration = SendMessageConfiguration(
|
|
61
|
+
return_immediately=return_immediately,
|
|
62
|
+
)
|
|
63
|
+
request = SendMessageRequest(message=msg, configuration=configuration)
|
|
64
|
+
return RequestContext(
|
|
65
|
+
call_context=ServerCallContext(),
|
|
66
|
+
request=request,
|
|
67
|
+
context_id=context_id,
|
|
68
|
+
task_id=task_id,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def _execute_and_drain(
|
|
73
|
+
executor: AgentlingExecutor,
|
|
74
|
+
ctx: RequestContext,
|
|
75
|
+
queue: EventQueue,
|
|
76
|
+
*,
|
|
77
|
+
timeout: float,
|
|
78
|
+
) -> tuple[list[object], float]:
|
|
79
|
+
"""Run ``executor.execute`` and drain enqueued events concurrently.
|
|
80
|
+
|
|
81
|
+
Returns the list of events enqueued and the wall-clock seconds taken.
|
|
82
|
+
A concurrent consumer is required because ``EventQueue.close()`` blocks
|
|
83
|
+
on ``queue.join()`` until every enqueued event has been ``task_done``'d.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
events: list[object] = []
|
|
87
|
+
|
|
88
|
+
async def consumer() -> None:
|
|
89
|
+
while True:
|
|
90
|
+
try:
|
|
91
|
+
event = await queue.dequeue_event()
|
|
92
|
+
except Exception: # queue closed
|
|
93
|
+
return
|
|
94
|
+
events.append(event)
|
|
95
|
+
queue.task_done()
|
|
96
|
+
if queue.is_closed() and queue.queue.empty():
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
start = asyncio.get_event_loop().time()
|
|
100
|
+
consumer_task = asyncio.create_task(consumer())
|
|
101
|
+
try:
|
|
102
|
+
await asyncio.wait_for(executor.execute(ctx, queue), timeout=timeout)
|
|
103
|
+
finally:
|
|
104
|
+
try:
|
|
105
|
+
await asyncio.wait_for(consumer_task, timeout=1.0)
|
|
106
|
+
except asyncio.TimeoutError:
|
|
107
|
+
consumer_task.cancel()
|
|
108
|
+
elapsed = asyncio.get_event_loop().time() - start
|
|
109
|
+
return events, elapsed
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@pytest.fixture
|
|
113
|
+
def slow_engine(
|
|
114
|
+
tmp_data_dir: Path, test_config: AgentConfig
|
|
115
|
+
) -> tuple[TaskEngine, ControllableLLM]:
|
|
116
|
+
"""Engine wired to a ControllableLLM that never replies until told to.
|
|
117
|
+
|
|
118
|
+
This guarantees that any call to ``engine.spawn`` with a non-zero
|
|
119
|
+
``await_seconds`` will burn the whole window — perfect for proving that
|
|
120
|
+
``return_immediately`` collapses it to zero.
|
|
121
|
+
"""
|
|
122
|
+
llm = ControllableLLM()
|
|
123
|
+
store = JournalStore(tmp_data_dir)
|
|
124
|
+
tools = ToolRegistry()
|
|
125
|
+
engine = TaskEngine(config=test_config, store=store, llm=llm, tools=tools)
|
|
126
|
+
return engine, llm
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TestReturnImmediately:
|
|
130
|
+
@pytest.mark.asyncio
|
|
131
|
+
async def test_return_immediately_yields_task_without_blocking(
|
|
132
|
+
self,
|
|
133
|
+
slow_engine: tuple[TaskEngine, ControllableLLM],
|
|
134
|
+
test_config: AgentConfig,
|
|
135
|
+
) -> None:
|
|
136
|
+
engine, llm = slow_engine
|
|
137
|
+
# Configure a long await window — if the executor honored it, the
|
|
138
|
+
# test would hang for ~30s. With return_immediately=True it should
|
|
139
|
+
# return ~immediately.
|
|
140
|
+
test_config.agent_task_await_seconds = 30
|
|
141
|
+
loop = _LoopShim(engine)
|
|
142
|
+
executor = AgentlingExecutor(loop, test_config)
|
|
143
|
+
|
|
144
|
+
ctx = _make_request_context(
|
|
145
|
+
text="hello",
|
|
146
|
+
context_id="ctx-immediate",
|
|
147
|
+
task_id="task-immediate",
|
|
148
|
+
return_immediately=True,
|
|
149
|
+
)
|
|
150
|
+
queue = EventQueue()
|
|
151
|
+
|
|
152
|
+
# If the executor blocked on await_seconds (30), the 2s wall-clock
|
|
153
|
+
# cap would expire and fail the test.
|
|
154
|
+
events, elapsed = await _execute_and_drain(
|
|
155
|
+
executor, ctx, queue, timeout=2.0,
|
|
156
|
+
)
|
|
157
|
+
assert elapsed < 1.0, f"executor did not return immediately ({elapsed:.3f}s)"
|
|
158
|
+
assert len(events) == 1
|
|
159
|
+
event = events[0]
|
|
160
|
+
assert isinstance(event, Task), f"expected Task, got {type(event).__name__}"
|
|
161
|
+
assert event.id == "task-immediate"
|
|
162
|
+
assert event.context_id == "ctx-immediate"
|
|
163
|
+
assert event.status.state in (
|
|
164
|
+
A2ATaskState.TASK_STATE_WORKING,
|
|
165
|
+
A2ATaskState.TASK_STATE_SUBMITTED,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Cleanup: let the still-running worker finish so the test doesn't
|
|
169
|
+
# leak a background task.
|
|
170
|
+
llm.push(LLMResponse(
|
|
171
|
+
content=[{"type": "text", "text": "done"}],
|
|
172
|
+
stop_reason="end_turn",
|
|
173
|
+
))
|
|
174
|
+
rec = engine.registry.get("task-immediate")
|
|
175
|
+
if rec is not None:
|
|
176
|
+
await asyncio.wait_for(rec.completion_event.wait(), timeout=3.0)
|
|
177
|
+
|
|
178
|
+
@pytest.mark.asyncio
|
|
179
|
+
async def test_default_blocking_uses_configured_await(
|
|
180
|
+
self,
|
|
181
|
+
slow_engine: tuple[TaskEngine, ControllableLLM],
|
|
182
|
+
test_config: AgentConfig,
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Without return_immediately, the executor blocks up to the
|
|
185
|
+
configured await window and surfaces a working Task when the worker
|
|
186
|
+
hasn't finished by then."""
|
|
187
|
+
engine, llm = slow_engine
|
|
188
|
+
test_config.agent_task_await_seconds = 0.2
|
|
189
|
+
loop = _LoopShim(engine)
|
|
190
|
+
executor = AgentlingExecutor(loop, test_config)
|
|
191
|
+
|
|
192
|
+
ctx = _make_request_context(
|
|
193
|
+
text="hello",
|
|
194
|
+
context_id="ctx-default",
|
|
195
|
+
task_id="task-default",
|
|
196
|
+
# configuration absent — represents a stock client that hasn't
|
|
197
|
+
# opted out of blocking.
|
|
198
|
+
return_immediately=None,
|
|
199
|
+
)
|
|
200
|
+
queue = EventQueue()
|
|
201
|
+
|
|
202
|
+
events, elapsed = await _execute_and_drain(
|
|
203
|
+
executor, ctx, queue, timeout=2.0,
|
|
204
|
+
)
|
|
205
|
+
# Must have waited approximately the configured window.
|
|
206
|
+
assert elapsed >= 0.15, f"executor returned too early ({elapsed:.3f}s)"
|
|
207
|
+
assert len(events) == 1
|
|
208
|
+
assert isinstance(events[0], Task)
|
|
209
|
+
|
|
210
|
+
# Cleanup.
|
|
211
|
+
llm.push(LLMResponse(
|
|
212
|
+
content=[{"type": "text", "text": "done"}],
|
|
213
|
+
stop_reason="end_turn",
|
|
214
|
+
))
|
|
215
|
+
rec = engine.registry.get("task-default")
|
|
216
|
+
if rec is not None:
|
|
217
|
+
await asyncio.wait_for(rec.completion_event.wait(), timeout=3.0)
|
|
218
|
+
|
|
219
|
+
@pytest.mark.asyncio
|
|
220
|
+
async def test_return_immediately_false_still_blocks(
|
|
221
|
+
self,
|
|
222
|
+
slow_engine: tuple[TaskEngine, ControllableLLM],
|
|
223
|
+
test_config: AgentConfig,
|
|
224
|
+
) -> None:
|
|
225
|
+
"""An explicit ``return_immediately=False`` must behave like the
|
|
226
|
+
default — i.e. honor the configured await window."""
|
|
227
|
+
engine, llm = slow_engine
|
|
228
|
+
test_config.agent_task_await_seconds = 0.2
|
|
229
|
+
loop = _LoopShim(engine)
|
|
230
|
+
executor = AgentlingExecutor(loop, test_config)
|
|
231
|
+
|
|
232
|
+
ctx = _make_request_context(
|
|
233
|
+
text="hello",
|
|
234
|
+
context_id="ctx-explicit-blocking",
|
|
235
|
+
task_id="task-explicit-blocking",
|
|
236
|
+
return_immediately=False,
|
|
237
|
+
)
|
|
238
|
+
queue = EventQueue()
|
|
239
|
+
|
|
240
|
+
events, elapsed = await _execute_and_drain(
|
|
241
|
+
executor, ctx, queue, timeout=2.0,
|
|
242
|
+
)
|
|
243
|
+
assert elapsed >= 0.15, (
|
|
244
|
+
f"executor returned too early with return_immediately=False "
|
|
245
|
+
f"({elapsed:.3f}s)"
|
|
246
|
+
)
|
|
247
|
+
assert isinstance(events[0], Task)
|
|
248
|
+
|
|
249
|
+
llm.push(LLMResponse(
|
|
250
|
+
content=[{"type": "text", "text": "done"}],
|
|
251
|
+
stop_reason="end_turn",
|
|
252
|
+
))
|
|
253
|
+
rec = engine.registry.get("task-explicit-blocking")
|
|
254
|
+
if rec is not None:
|
|
255
|
+
await asyncio.wait_for(rec.completion_event.wait(), timeout=3.0)
|
|
256
|
+
|
|
257
|
+
@pytest.mark.asyncio
|
|
258
|
+
async def test_fast_path_with_immediate_completion_returns_message(
|
|
259
|
+
self,
|
|
260
|
+
tmp_data_dir: Path,
|
|
261
|
+
test_config: AgentConfig,
|
|
262
|
+
) -> None:
|
|
263
|
+
"""If the LLM is fast (mock backend completes synchronously),
|
|
264
|
+
the default path still returns a Message, not a Task — proving
|
|
265
|
+
we haven't accidentally forced everything onto the slow path."""
|
|
266
|
+
from agentlings.core.llm import MockLLMClient
|
|
267
|
+
|
|
268
|
+
tools = ToolRegistry()
|
|
269
|
+
tools.register_tools(["bash", "filesystem"])
|
|
270
|
+
llm = MockLLMClient(tool_names=tools.tool_names())
|
|
271
|
+
store = JournalStore(tmp_data_dir)
|
|
272
|
+
engine = TaskEngine(
|
|
273
|
+
config=test_config, store=store, llm=llm, tools=tools,
|
|
274
|
+
)
|
|
275
|
+
loop = _LoopShim(engine)
|
|
276
|
+
executor = AgentlingExecutor(loop, test_config)
|
|
277
|
+
|
|
278
|
+
ctx = _make_request_context(
|
|
279
|
+
text="hello",
|
|
280
|
+
context_id="ctx-fast",
|
|
281
|
+
task_id="task-fast",
|
|
282
|
+
return_immediately=None,
|
|
283
|
+
)
|
|
284
|
+
queue = EventQueue()
|
|
285
|
+
events, _elapsed = await _execute_and_drain(
|
|
286
|
+
executor, ctx, queue, timeout=5.0,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
assert len(events) == 1
|
|
290
|
+
assert isinstance(events[0], Message), (
|
|
291
|
+
f"fast path should return Message, got {type(events[0]).__name__}"
|
|
292
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|