fluxos-agent 2.0.0__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.
- fluxos_agent-2.0.0/PKG-INFO +116 -0
- fluxos_agent-2.0.0/README.md +96 -0
- fluxos_agent-2.0.0/flux_agent/__init__.py +57 -0
- fluxos_agent-2.0.0/flux_agent/agent.py +433 -0
- fluxos_agent-2.0.0/flux_agent/agent_loop.py +458 -0
- fluxos_agent-2.0.0/flux_agent/event_stream.py +56 -0
- fluxos_agent-2.0.0/flux_agent/proxy.py +294 -0
- fluxos_agent-2.0.0/flux_agent/types.py +460 -0
- fluxos_agent-2.0.0/flux_agent/version.py +1 -0
- fluxos_agent-2.0.0/fluxos_agent.egg-info/PKG-INFO +116 -0
- fluxos_agent-2.0.0/fluxos_agent.egg-info/SOURCES.txt +14 -0
- fluxos_agent-2.0.0/fluxos_agent.egg-info/dependency_links.txt +1 -0
- fluxos_agent-2.0.0/fluxos_agent.egg-info/requires.txt +8 -0
- fluxos_agent-2.0.0/fluxos_agent.egg-info/top_level.txt +1 -0
- fluxos_agent-2.0.0/pyproject.toml +58 -0
- fluxos_agent-2.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fluxos-agent
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Stateful agent runtime with tool execution and event streaming
|
|
5
|
+
Author-email: fluxos contributors <dev@fluxtopus.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: httpx>=0.27.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
18
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
19
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
20
|
+
|
|
21
|
+
# fluxos-agent
|
|
22
|
+
|
|
23
|
+
Python port of `pi-mono/packages/agent` [https://github.com/badlogic/pi-mono/tree/main/packages/agent].
|
|
24
|
+
|
|
25
|
+
`fluxos-agent` provides:
|
|
26
|
+
- Stateful agent runtime (`Agent`) with prompt/continue flows
|
|
27
|
+
- Agent loop primitives (`agent_loop`, `agent_loop_continue`)
|
|
28
|
+
- Tool execution with streaming update events
|
|
29
|
+
- Steering and follow-up queues
|
|
30
|
+
- Optional proxy stream adapter (`stream_proxy`)
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install -e packages/fluxos-agent
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import asyncio
|
|
42
|
+
|
|
43
|
+
from flux_agent import Agent, AgentOptions
|
|
44
|
+
from flux_agent.types import (
|
|
45
|
+
AssistantDoneEvent,
|
|
46
|
+
AssistantMessage,
|
|
47
|
+
Context,
|
|
48
|
+
Model,
|
|
49
|
+
TextContent,
|
|
50
|
+
Usage,
|
|
51
|
+
UsageCost,
|
|
52
|
+
)
|
|
53
|
+
from flux_agent.event_stream import EventStream
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class MockAssistantStream(EventStream):
|
|
57
|
+
def __init__(self):
|
|
58
|
+
super().__init__(
|
|
59
|
+
lambda event: event.type in {"done", "error"},
|
|
60
|
+
lambda event: event.message if event.type == "done" else event.error,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def stream_fn(model: Model, context: Context, options):
|
|
65
|
+
stream = MockAssistantStream()
|
|
66
|
+
message = AssistantMessage(
|
|
67
|
+
content=[TextContent(text="Hello from Python agent")],
|
|
68
|
+
api=model.api,
|
|
69
|
+
provider=model.provider,
|
|
70
|
+
model=model.id,
|
|
71
|
+
usage=Usage(cost=UsageCost()),
|
|
72
|
+
stop_reason="stop",
|
|
73
|
+
)
|
|
74
|
+
stream.push(AssistantDoneEvent(reason="stop", message=message))
|
|
75
|
+
return stream
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def main():
|
|
79
|
+
agent = Agent(
|
|
80
|
+
AgentOptions(
|
|
81
|
+
stream_fn=stream_fn,
|
|
82
|
+
initial_state={
|
|
83
|
+
"system_prompt": "You are helpful.",
|
|
84
|
+
"model": Model(id="mock", name="mock", api="mock", provider="mock"),
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
agent.subscribe(lambda event: print(event.type))
|
|
90
|
+
await agent.prompt("Hello")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
asyncio.run(main())
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Testing
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
cd packages/fluxos-agent
|
|
100
|
+
pip install -e ".[dev]"
|
|
101
|
+
pytest tests/unit -v
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Run full suite (unit + e2e scaffolding):
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pytest tests -v
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Release Tracking
|
|
111
|
+
|
|
112
|
+
- Keep package changes tracked in `CHANGELOG.md`.
|
|
113
|
+
- For every release:
|
|
114
|
+
1. Update `pyproject.toml` version.
|
|
115
|
+
2. Add a new dated section in `CHANGELOG.md`.
|
|
116
|
+
3. Publish and tag the release.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# fluxos-agent
|
|
2
|
+
|
|
3
|
+
Python port of `pi-mono/packages/agent` [https://github.com/badlogic/pi-mono/tree/main/packages/agent].
|
|
4
|
+
|
|
5
|
+
`fluxos-agent` provides:
|
|
6
|
+
- Stateful agent runtime (`Agent`) with prompt/continue flows
|
|
7
|
+
- Agent loop primitives (`agent_loop`, `agent_loop_continue`)
|
|
8
|
+
- Tool execution with streaming update events
|
|
9
|
+
- Steering and follow-up queues
|
|
10
|
+
- Optional proxy stream adapter (`stream_proxy`)
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install -e packages/fluxos-agent
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import asyncio
|
|
22
|
+
|
|
23
|
+
from flux_agent import Agent, AgentOptions
|
|
24
|
+
from flux_agent.types import (
|
|
25
|
+
AssistantDoneEvent,
|
|
26
|
+
AssistantMessage,
|
|
27
|
+
Context,
|
|
28
|
+
Model,
|
|
29
|
+
TextContent,
|
|
30
|
+
Usage,
|
|
31
|
+
UsageCost,
|
|
32
|
+
)
|
|
33
|
+
from flux_agent.event_stream import EventStream
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MockAssistantStream(EventStream):
|
|
37
|
+
def __init__(self):
|
|
38
|
+
super().__init__(
|
|
39
|
+
lambda event: event.type in {"done", "error"},
|
|
40
|
+
lambda event: event.message if event.type == "done" else event.error,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def stream_fn(model: Model, context: Context, options):
|
|
45
|
+
stream = MockAssistantStream()
|
|
46
|
+
message = AssistantMessage(
|
|
47
|
+
content=[TextContent(text="Hello from Python agent")],
|
|
48
|
+
api=model.api,
|
|
49
|
+
provider=model.provider,
|
|
50
|
+
model=model.id,
|
|
51
|
+
usage=Usage(cost=UsageCost()),
|
|
52
|
+
stop_reason="stop",
|
|
53
|
+
)
|
|
54
|
+
stream.push(AssistantDoneEvent(reason="stop", message=message))
|
|
55
|
+
return stream
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def main():
|
|
59
|
+
agent = Agent(
|
|
60
|
+
AgentOptions(
|
|
61
|
+
stream_fn=stream_fn,
|
|
62
|
+
initial_state={
|
|
63
|
+
"system_prompt": "You are helpful.",
|
|
64
|
+
"model": Model(id="mock", name="mock", api="mock", provider="mock"),
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
agent.subscribe(lambda event: print(event.type))
|
|
70
|
+
await agent.prompt("Hello")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
asyncio.run(main())
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Testing
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
cd packages/fluxos-agent
|
|
80
|
+
pip install -e ".[dev]"
|
|
81
|
+
pytest tests/unit -v
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Run full suite (unit + e2e scaffolding):
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pytest tests -v
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Release Tracking
|
|
91
|
+
|
|
92
|
+
- Keep package changes tracked in `CHANGELOG.md`.
|
|
93
|
+
- For every release:
|
|
94
|
+
1. Update `pyproject.toml` version.
|
|
95
|
+
2. Add a new dated section in `CHANGELOG.md`.
|
|
96
|
+
3. Publish and tag the release.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""fluxos-agent - Stateful agent loop and runtime for Python."""
|
|
2
|
+
|
|
3
|
+
from .agent import Agent, AgentOptions
|
|
4
|
+
from .agent_loop import agent_loop, agent_loop_continue
|
|
5
|
+
from .event_stream import EventStream
|
|
6
|
+
from .proxy import ProxyMessageEventStream, ProxyStreamOptions, stream_proxy
|
|
7
|
+
from .types import (
|
|
8
|
+
AgentContext,
|
|
9
|
+
AgentLoopConfig,
|
|
10
|
+
AgentMessage,
|
|
11
|
+
AgentState,
|
|
12
|
+
AgentTool,
|
|
13
|
+
AgentToolResult,
|
|
14
|
+
AssistantMessage,
|
|
15
|
+
AssistantMessageEvent,
|
|
16
|
+
Context,
|
|
17
|
+
ImageContent,
|
|
18
|
+
Message,
|
|
19
|
+
Model,
|
|
20
|
+
TextContent,
|
|
21
|
+
ThinkingBudgets,
|
|
22
|
+
ThinkingContent,
|
|
23
|
+
ThinkingLevel,
|
|
24
|
+
ToolCallContent,
|
|
25
|
+
ToolResultMessage,
|
|
26
|
+
)
|
|
27
|
+
from .version import __version__
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Agent",
|
|
31
|
+
"AgentOptions",
|
|
32
|
+
"EventStream",
|
|
33
|
+
"agent_loop",
|
|
34
|
+
"agent_loop_continue",
|
|
35
|
+
"stream_proxy",
|
|
36
|
+
"ProxyMessageEventStream",
|
|
37
|
+
"ProxyStreamOptions",
|
|
38
|
+
"Model",
|
|
39
|
+
"Context",
|
|
40
|
+
"Message",
|
|
41
|
+
"AgentMessage",
|
|
42
|
+
"AssistantMessage",
|
|
43
|
+
"ToolResultMessage",
|
|
44
|
+
"AgentContext",
|
|
45
|
+
"AgentLoopConfig",
|
|
46
|
+
"AgentState",
|
|
47
|
+
"AgentTool",
|
|
48
|
+
"AgentToolResult",
|
|
49
|
+
"TextContent",
|
|
50
|
+
"ImageContent",
|
|
51
|
+
"ThinkingContent",
|
|
52
|
+
"ToolCallContent",
|
|
53
|
+
"AssistantMessageEvent",
|
|
54
|
+
"ThinkingLevel",
|
|
55
|
+
"ThinkingBudgets",
|
|
56
|
+
"__version__",
|
|
57
|
+
]
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
from .agent_loop import agent_loop, agent_loop_continue, build_error_message
|
|
8
|
+
from .types import (
|
|
9
|
+
AgentContext,
|
|
10
|
+
AgentEndEvent,
|
|
11
|
+
AgentEvent,
|
|
12
|
+
AgentLoopConfig,
|
|
13
|
+
AgentMessage,
|
|
14
|
+
AgentState,
|
|
15
|
+
AgentTool,
|
|
16
|
+
ImageContent,
|
|
17
|
+
Message,
|
|
18
|
+
Model,
|
|
19
|
+
TextContent,
|
|
20
|
+
ThinkingBudgets,
|
|
21
|
+
ThinkingLevel,
|
|
22
|
+
Transport,
|
|
23
|
+
message_role,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _AbortSignal:
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self._event = asyncio.Event()
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def aborted(self) -> bool:
|
|
33
|
+
return self._event.is_set()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AbortController:
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
self.signal = _AbortSignal()
|
|
39
|
+
|
|
40
|
+
def abort(self) -> None:
|
|
41
|
+
self.signal._event.set()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def default_convert_to_llm(messages: list[AgentMessage]) -> list[Message]:
|
|
45
|
+
return [
|
|
46
|
+
message
|
|
47
|
+
for message in messages
|
|
48
|
+
if message_role(message) in {"user", "assistant", "toolResult"}
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def default_stream_fn(_model: Model, _context: Any, _options: Any):
|
|
53
|
+
raise RuntimeError("No stream function configured. Provide Agent(stream_fn=...) with a provider implementation.")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class AgentOptions:
|
|
58
|
+
initial_state: AgentState | dict[str, Any] | None = None
|
|
59
|
+
convert_to_llm: Callable[[list[AgentMessage]], list[Message] | Any] | None = None
|
|
60
|
+
transform_context: Callable[[list[AgentMessage], Any | None], Any] | None = None
|
|
61
|
+
steering_mode: str = "one-at-a-time"
|
|
62
|
+
follow_up_mode: str = "one-at-a-time"
|
|
63
|
+
stream_fn: Callable[..., Any] | None = None
|
|
64
|
+
session_id: str | None = None
|
|
65
|
+
get_api_key: Callable[[str], Any] | None = None
|
|
66
|
+
thinking_budgets: ThinkingBudgets | None = None
|
|
67
|
+
transport: Transport = "sse"
|
|
68
|
+
max_retry_delay_ms: int | None = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Agent:
|
|
72
|
+
def __init__(self, opts: AgentOptions | None = None, **kwargs: Any) -> None:
|
|
73
|
+
options = opts or AgentOptions(**kwargs)
|
|
74
|
+
|
|
75
|
+
self._state = AgentState()
|
|
76
|
+
if isinstance(options.initial_state, AgentState):
|
|
77
|
+
self._state = options.initial_state
|
|
78
|
+
elif isinstance(options.initial_state, dict):
|
|
79
|
+
for key, value in options.initial_state.items():
|
|
80
|
+
if hasattr(self._state, key):
|
|
81
|
+
setattr(self._state, key, value)
|
|
82
|
+
|
|
83
|
+
self._listeners: set[Callable[[AgentEvent], None]] = set()
|
|
84
|
+
self._abort_controller: AbortController | None = None
|
|
85
|
+
self._convert_to_llm = options.convert_to_llm or default_convert_to_llm
|
|
86
|
+
self._transform_context = options.transform_context
|
|
87
|
+
self._steering_queue: list[AgentMessage] = []
|
|
88
|
+
self._follow_up_queue: list[AgentMessage] = []
|
|
89
|
+
self._steering_mode = options.steering_mode
|
|
90
|
+
self._follow_up_mode = options.follow_up_mode
|
|
91
|
+
self.stream_fn = options.stream_fn or default_stream_fn
|
|
92
|
+
self._session_id = options.session_id
|
|
93
|
+
self.get_api_key = options.get_api_key
|
|
94
|
+
self._thinking_budgets = options.thinking_budgets
|
|
95
|
+
self._transport: Transport = options.transport
|
|
96
|
+
self._max_retry_delay_ms = options.max_retry_delay_ms
|
|
97
|
+
self._running_prompt: asyncio.Task[None] | None = None
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def state(self) -> AgentState:
|
|
101
|
+
return self._state
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def session_id(self) -> str | None:
|
|
105
|
+
return self._session_id
|
|
106
|
+
|
|
107
|
+
@session_id.setter
|
|
108
|
+
def session_id(self, value: str | None) -> None:
|
|
109
|
+
self._session_id = value
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def thinking_budgets(self) -> ThinkingBudgets | None:
|
|
113
|
+
return self._thinking_budgets
|
|
114
|
+
|
|
115
|
+
@thinking_budgets.setter
|
|
116
|
+
def thinking_budgets(self, value: ThinkingBudgets | None) -> None:
|
|
117
|
+
self._thinking_budgets = value
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def transport(self) -> Transport:
|
|
121
|
+
return self._transport
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def max_retry_delay_ms(self) -> int | None:
|
|
125
|
+
return self._max_retry_delay_ms
|
|
126
|
+
|
|
127
|
+
@max_retry_delay_ms.setter
|
|
128
|
+
def max_retry_delay_ms(self, value: int | None) -> None:
|
|
129
|
+
self._max_retry_delay_ms = value
|
|
130
|
+
|
|
131
|
+
def subscribe(self, fn: Callable[[AgentEvent], None]) -> Callable[[], None]:
|
|
132
|
+
self._listeners.add(fn)
|
|
133
|
+
|
|
134
|
+
def _unsubscribe() -> None:
|
|
135
|
+
self._listeners.discard(fn)
|
|
136
|
+
|
|
137
|
+
return _unsubscribe
|
|
138
|
+
|
|
139
|
+
def set_system_prompt(self, value: str) -> None:
|
|
140
|
+
self._state.system_prompt = value
|
|
141
|
+
|
|
142
|
+
def set_model(self, model: Model) -> None:
|
|
143
|
+
self._state.model = model
|
|
144
|
+
|
|
145
|
+
def set_thinking_level(self, level: ThinkingLevel) -> None:
|
|
146
|
+
self._state.thinking_level = level
|
|
147
|
+
|
|
148
|
+
def set_steering_mode(self, mode: str) -> None:
|
|
149
|
+
self._steering_mode = mode
|
|
150
|
+
|
|
151
|
+
def get_steering_mode(self) -> str:
|
|
152
|
+
return self._steering_mode
|
|
153
|
+
|
|
154
|
+
def set_follow_up_mode(self, mode: str) -> None:
|
|
155
|
+
self._follow_up_mode = mode
|
|
156
|
+
|
|
157
|
+
def get_follow_up_mode(self) -> str:
|
|
158
|
+
return self._follow_up_mode
|
|
159
|
+
|
|
160
|
+
def set_tools(self, tools: list[AgentTool]) -> None:
|
|
161
|
+
self._state.tools = tools
|
|
162
|
+
|
|
163
|
+
def set_transport(self, value: Transport) -> None:
|
|
164
|
+
self._transport = value
|
|
165
|
+
|
|
166
|
+
def replace_messages(self, messages: list[AgentMessage]) -> None:
|
|
167
|
+
self._state.messages = list(messages)
|
|
168
|
+
|
|
169
|
+
def append_message(self, message: AgentMessage) -> None:
|
|
170
|
+
self._state.messages = [*self._state.messages, message]
|
|
171
|
+
|
|
172
|
+
def steer(self, message: AgentMessage) -> None:
|
|
173
|
+
self._steering_queue.append(message)
|
|
174
|
+
|
|
175
|
+
def follow_up(self, message: AgentMessage) -> None:
|
|
176
|
+
self._follow_up_queue.append(message)
|
|
177
|
+
|
|
178
|
+
def clear_steering_queue(self) -> None:
|
|
179
|
+
self._steering_queue = []
|
|
180
|
+
|
|
181
|
+
def clear_follow_up_queue(self) -> None:
|
|
182
|
+
self._follow_up_queue = []
|
|
183
|
+
|
|
184
|
+
def clear_all_queues(self) -> None:
|
|
185
|
+
self._steering_queue = []
|
|
186
|
+
self._follow_up_queue = []
|
|
187
|
+
|
|
188
|
+
def has_queued_messages(self) -> bool:
|
|
189
|
+
return bool(self._steering_queue or self._follow_up_queue)
|
|
190
|
+
|
|
191
|
+
def clear_messages(self) -> None:
|
|
192
|
+
self._state.messages = []
|
|
193
|
+
|
|
194
|
+
def abort(self) -> None:
|
|
195
|
+
if self._abort_controller:
|
|
196
|
+
self._abort_controller.abort()
|
|
197
|
+
|
|
198
|
+
async def wait_for_idle(self) -> None:
|
|
199
|
+
if self._running_prompt:
|
|
200
|
+
await self._running_prompt
|
|
201
|
+
|
|
202
|
+
def reset(self) -> None:
|
|
203
|
+
self._state.messages = []
|
|
204
|
+
self._state.is_streaming = False
|
|
205
|
+
self._state.stream_message = None
|
|
206
|
+
self._state.pending_tool_calls = set()
|
|
207
|
+
self._state.error = None
|
|
208
|
+
self.clear_all_queues()
|
|
209
|
+
|
|
210
|
+
async def prompt(self, input_message: str | AgentMessage | list[AgentMessage], images: list[ImageContent] | None = None) -> None:
|
|
211
|
+
if self._state.is_streaming:
|
|
212
|
+
raise RuntimeError(
|
|
213
|
+
"Agent is already processing a prompt. Use steer() or follow_up() to queue messages, or wait for completion."
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if isinstance(input_message, list):
|
|
217
|
+
messages = input_message
|
|
218
|
+
elif isinstance(input_message, str):
|
|
219
|
+
content: list[TextContent | ImageContent] = [TextContent(text=input_message)]
|
|
220
|
+
if images:
|
|
221
|
+
content.extend(images)
|
|
222
|
+
messages = [{"role": "user", "content": content, "timestamp": _now_ms()}]
|
|
223
|
+
else:
|
|
224
|
+
messages = [input_message]
|
|
225
|
+
|
|
226
|
+
await self._run_loop(messages)
|
|
227
|
+
|
|
228
|
+
async def continue_(self) -> None:
|
|
229
|
+
if self._state.is_streaming:
|
|
230
|
+
raise RuntimeError("Agent is already processing. Wait for completion before continuing.")
|
|
231
|
+
|
|
232
|
+
messages = self._state.messages
|
|
233
|
+
if not messages:
|
|
234
|
+
raise RuntimeError("No messages to continue from")
|
|
235
|
+
|
|
236
|
+
if message_role(messages[-1]) == "assistant":
|
|
237
|
+
queued_steering = self._dequeue_steering_messages()
|
|
238
|
+
if queued_steering:
|
|
239
|
+
await self._run_loop(queued_steering, skip_initial_steering_poll=True)
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
queued_follow_up = self._dequeue_follow_up_messages()
|
|
243
|
+
if queued_follow_up:
|
|
244
|
+
await self._run_loop(queued_follow_up)
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
raise RuntimeError("Cannot continue from message role: assistant")
|
|
248
|
+
|
|
249
|
+
await self._run_loop(None)
|
|
250
|
+
|
|
251
|
+
async def _run_loop(self, messages: list[AgentMessage] | None, skip_initial_steering_poll: bool = False) -> None:
|
|
252
|
+
model = self._state.model
|
|
253
|
+
|
|
254
|
+
async def _runner() -> None:
|
|
255
|
+
self._abort_controller = AbortController()
|
|
256
|
+
self._state.is_streaming = True
|
|
257
|
+
self._state.stream_message = None
|
|
258
|
+
self._state.error = None
|
|
259
|
+
|
|
260
|
+
reasoning = None if self._state.thinking_level == "off" else self._state.thinking_level
|
|
261
|
+
context = AgentContext(
|
|
262
|
+
system_prompt=self._state.system_prompt,
|
|
263
|
+
messages=list(self._state.messages),
|
|
264
|
+
tools=self._state.tools,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
local_skip = skip_initial_steering_poll
|
|
268
|
+
|
|
269
|
+
async def _get_steering_messages() -> list[AgentMessage]:
|
|
270
|
+
nonlocal local_skip
|
|
271
|
+
if local_skip:
|
|
272
|
+
local_skip = False
|
|
273
|
+
return []
|
|
274
|
+
return self._dequeue_steering_messages()
|
|
275
|
+
|
|
276
|
+
config = AgentLoopConfig(
|
|
277
|
+
model=model,
|
|
278
|
+
reasoning=reasoning,
|
|
279
|
+
session_id=self._session_id,
|
|
280
|
+
transport=self._transport,
|
|
281
|
+
thinking_budgets=self._thinking_budgets,
|
|
282
|
+
max_retry_delay_ms=self._max_retry_delay_ms,
|
|
283
|
+
convert_to_llm=self._convert_to_llm,
|
|
284
|
+
transform_context=self._transform_context,
|
|
285
|
+
get_api_key=self.get_api_key,
|
|
286
|
+
get_steering_messages=_get_steering_messages,
|
|
287
|
+
get_follow_up_messages=self._dequeue_follow_up_messages,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
partial: AgentMessage | None = None
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
stream = (
|
|
294
|
+
agent_loop(messages, context, config, self._abort_controller.signal, self.stream_fn)
|
|
295
|
+
if messages is not None
|
|
296
|
+
else agent_loop_continue(context, config, self._abort_controller.signal, self.stream_fn)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
async for event in stream:
|
|
300
|
+
event_type = getattr(event, "type", "")
|
|
301
|
+
|
|
302
|
+
if event_type in {"message_start", "message_update"}:
|
|
303
|
+
partial = event.message
|
|
304
|
+
self._state.stream_message = event.message
|
|
305
|
+
elif event_type == "message_end":
|
|
306
|
+
partial = None
|
|
307
|
+
self._state.stream_message = None
|
|
308
|
+
self.append_message(event.message)
|
|
309
|
+
elif event_type == "tool_execution_start":
|
|
310
|
+
pending = set(self._state.pending_tool_calls)
|
|
311
|
+
pending.add(event.tool_call_id)
|
|
312
|
+
self._state.pending_tool_calls = pending
|
|
313
|
+
elif event_type == "tool_execution_end":
|
|
314
|
+
pending = set(self._state.pending_tool_calls)
|
|
315
|
+
pending.discard(event.tool_call_id)
|
|
316
|
+
self._state.pending_tool_calls = pending
|
|
317
|
+
elif event_type == "turn_end":
|
|
318
|
+
if message_role(event.message) == "assistant" and getattr(event.message, "error_message", None):
|
|
319
|
+
self._state.error = str(getattr(event.message, "error_message"))
|
|
320
|
+
elif event_type == "agent_end":
|
|
321
|
+
self._state.is_streaming = False
|
|
322
|
+
self._state.stream_message = None
|
|
323
|
+
|
|
324
|
+
self._emit(event)
|
|
325
|
+
|
|
326
|
+
if partial and message_role(partial) == "assistant":
|
|
327
|
+
content = getattr(partial, "content", [])
|
|
328
|
+
only_empty = not any(_has_non_empty_content(part) for part in content)
|
|
329
|
+
if not only_empty:
|
|
330
|
+
self.append_message(partial)
|
|
331
|
+
elif self._abort_controller and self._abort_controller.signal.aborted:
|
|
332
|
+
raise RuntimeError("Request was aborted")
|
|
333
|
+
|
|
334
|
+
except Exception as exc:
|
|
335
|
+
error_message = build_error_message(
|
|
336
|
+
model,
|
|
337
|
+
exc,
|
|
338
|
+
aborted=bool(self._abort_controller and self._abort_controller.signal.aborted),
|
|
339
|
+
)
|
|
340
|
+
self.append_message(error_message)
|
|
341
|
+
self._state.error = str(exc)
|
|
342
|
+
self._emit(AgentEndEvent(messages=[error_message]))
|
|
343
|
+
finally:
|
|
344
|
+
self._state.is_streaming = False
|
|
345
|
+
self._state.stream_message = None
|
|
346
|
+
self._state.pending_tool_calls = set()
|
|
347
|
+
self._abort_controller = None
|
|
348
|
+
|
|
349
|
+
task = asyncio.create_task(_runner())
|
|
350
|
+
self._running_prompt = task
|
|
351
|
+
try:
|
|
352
|
+
await task
|
|
353
|
+
finally:
|
|
354
|
+
self._running_prompt = None
|
|
355
|
+
|
|
356
|
+
def _dequeue_steering_messages(self) -> list[AgentMessage]:
|
|
357
|
+
if self._steering_mode == "one-at-a-time":
|
|
358
|
+
if self._steering_queue:
|
|
359
|
+
first = self._steering_queue[0]
|
|
360
|
+
self._steering_queue = self._steering_queue[1:]
|
|
361
|
+
return [first]
|
|
362
|
+
return []
|
|
363
|
+
|
|
364
|
+
queued = list(self._steering_queue)
|
|
365
|
+
self._steering_queue = []
|
|
366
|
+
return queued
|
|
367
|
+
|
|
368
|
+
def _dequeue_follow_up_messages(self) -> list[AgentMessage]:
|
|
369
|
+
if self._follow_up_mode == "one-at-a-time":
|
|
370
|
+
if self._follow_up_queue:
|
|
371
|
+
first = self._follow_up_queue[0]
|
|
372
|
+
self._follow_up_queue = self._follow_up_queue[1:]
|
|
373
|
+
return [first]
|
|
374
|
+
return []
|
|
375
|
+
|
|
376
|
+
queued = list(self._follow_up_queue)
|
|
377
|
+
self._follow_up_queue = []
|
|
378
|
+
return queued
|
|
379
|
+
|
|
380
|
+
def _emit(self, event: AgentEvent) -> None:
|
|
381
|
+
for listener in list(self._listeners):
|
|
382
|
+
listener(event)
|
|
383
|
+
|
|
384
|
+
# TypeScript API aliases for easier migration.
|
|
385
|
+
setSystemPrompt = set_system_prompt
|
|
386
|
+
setModel = set_model
|
|
387
|
+
setThinkingLevel = set_thinking_level
|
|
388
|
+
setSteeringMode = set_steering_mode
|
|
389
|
+
getSteeringMode = get_steering_mode
|
|
390
|
+
setFollowUpMode = set_follow_up_mode
|
|
391
|
+
getFollowUpMode = get_follow_up_mode
|
|
392
|
+
setTools = set_tools
|
|
393
|
+
setTransport = set_transport
|
|
394
|
+
replaceMessages = replace_messages
|
|
395
|
+
appendMessage = append_message
|
|
396
|
+
followUp = follow_up
|
|
397
|
+
clearSteeringQueue = clear_steering_queue
|
|
398
|
+
clearFollowUpQueue = clear_follow_up_queue
|
|
399
|
+
clearAllQueues = clear_all_queues
|
|
400
|
+
hasQueuedMessages = has_queued_messages
|
|
401
|
+
clearMessages = clear_messages
|
|
402
|
+
waitForIdle = wait_for_idle
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
continue_conversation = Agent.continue_
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _has_non_empty_content(part: Any) -> bool:
|
|
409
|
+
if isinstance(part, dict):
|
|
410
|
+
part_type = part.get("type")
|
|
411
|
+
if part_type == "thinking":
|
|
412
|
+
return bool(str(part.get("thinking", "")).strip())
|
|
413
|
+
if part_type == "text":
|
|
414
|
+
return bool(str(part.get("text", "")).strip())
|
|
415
|
+
if part_type == "toolCall":
|
|
416
|
+
return bool(str(part.get("name", "")).strip())
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
part_type = getattr(part, "type", None)
|
|
420
|
+
if part_type == "thinking":
|
|
421
|
+
return bool(str(getattr(part, "thinking", "")).strip())
|
|
422
|
+
if part_type == "text":
|
|
423
|
+
return bool(str(getattr(part, "text", "")).strip())
|
|
424
|
+
if part_type == "toolCall":
|
|
425
|
+
return bool(str(getattr(part, "name", "")).strip())
|
|
426
|
+
return False
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _now_ms() -> int:
|
|
430
|
+
import time
|
|
431
|
+
|
|
432
|
+
return int(time.time() * 1000)
|
|
433
|
+
|