travis234 2.3.1__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.
Files changed (214) hide show
  1. travis/__init__.py +8 -0
  2. travis/agent/__init__.py +69 -0
  3. travis/agent/agent.py +443 -0
  4. travis/agent/agent_loop.py +870 -0
  5. travis/agent/async_utils.py +33 -0
  6. travis/agent/run_lease.py +62 -0
  7. travis/agent/tool_coordinator.py +55 -0
  8. travis/agent/types.py +290 -0
  9. travis/ai/__init__.py +85 -0
  10. travis/ai/auth/__init__.py +35 -0
  11. travis/ai/auth/context.py +24 -0
  12. travis/ai/auth/credential_store.py +62 -0
  13. travis/ai/auth/helpers.py +58 -0
  14. travis/ai/auth/resolve.py +205 -0
  15. travis/ai/auth/types.py +90 -0
  16. travis/ai/builtin_models.json +1 -0
  17. travis/ai/builtin_models.py +56 -0
  18. travis/ai/catalog_generation.py +76 -0
  19. travis/ai/context_estimate.py +226 -0
  20. travis/ai/env_config.py +295 -0
  21. travis/ai/event_stream.py +102 -0
  22. travis/ai/image_types.py +69 -0
  23. travis/ai/images.py +266 -0
  24. travis/ai/lazy_stream.py +48 -0
  25. travis/ai/model_cost.py +60 -0
  26. travis/ai/model_resolver.py +470 -0
  27. travis/ai/models.py +546 -0
  28. travis/ai/oauth.py +16 -0
  29. travis/ai/overflow.py +124 -0
  30. travis/ai/provider_metadata.py +150 -0
  31. travis/ai/providers/__init__.py +45 -0
  32. travis/ai/providers/_shared.py +33 -0
  33. travis/ai/providers/all.py +169 -0
  34. travis/ai/providers/anthropic_stream.py +290 -0
  35. travis/ai/providers/base.py +199 -0
  36. travis/ai/providers/bedrock_stream.py +200 -0
  37. travis/ai/providers/capabilities.py +243 -0
  38. travis/ai/providers/catalog.py +349 -0
  39. travis/ai/providers/chat_stream.py +407 -0
  40. travis/ai/providers/codex_auth.py +57 -0
  41. travis/ai/providers/codex_runtime.py +582 -0
  42. travis/ai/providers/copilot_headers.py +40 -0
  43. travis/ai/providers/faux.py +116 -0
  44. travis/ai/providers/github_copilot_oauth.py +450 -0
  45. travis/ai/providers/google_stream.py +207 -0
  46. travis/ai/providers/message_translation.py +433 -0
  47. travis/ai/providers/mistral_stream.py +229 -0
  48. travis/ai/providers/openai_compat.py +136 -0
  49. travis/ai/providers/params.py +321 -0
  50. travis/ai/providers/provider_auth.py +49 -0
  51. travis/ai/providers/provider_errors.py +66 -0
  52. travis/ai/providers/provider_request.py +236 -0
  53. travis/ai/providers/responses_stream.py +396 -0
  54. travis/ai/providers/responses_translation.py +325 -0
  55. travis/ai/providers/sse_common.py +57 -0
  56. travis/ai/providers/streaming_json.py +266 -0
  57. travis/ai/providers/subscription_oauth.py +469 -0
  58. travis/ai/providers/transports.py +2269 -0
  59. travis/ai/providers/travis_env.py +200 -0
  60. travis/ai/stream_proxy.py +72 -0
  61. travis/ai/types.py +307 -0
  62. travis/ai/validation.py +343 -0
  63. travis/app.py +891 -0
  64. travis/cli.py +1022 -0
  65. travis/coding_agent/__init__.py +302 -0
  66. travis/coding_agent/agent_harness.py +257 -0
  67. travis/coding_agent/agent_session.py +416 -0
  68. travis/coding_agent/agent_session_runtime.py +542 -0
  69. travis/coding_agent/agent_session_services.py +554 -0
  70. travis/coding_agent/artifacts.py +107 -0
  71. travis/coding_agent/auth_storage.py +309 -0
  72. travis/coding_agent/automation.py +169 -0
  73. travis/coding_agent/bash_executor.py +118 -0
  74. travis/coding_agent/branch_summarization.py +345 -0
  75. travis/coding_agent/capabilities.py +51 -0
  76. travis/coding_agent/compaction_adapter.py +536 -0
  77. travis/coding_agent/compaction_coordinator.py +761 -0
  78. travis/coding_agent/config.py +92 -0
  79. travis/coding_agent/deep_compaction_command.py +491 -0
  80. travis/coding_agent/eval_trace.py +122 -0
  81. travis/coding_agent/event_bus.py +81 -0
  82. travis/coding_agent/execution_backend.py +69 -0
  83. travis/coding_agent/experimental.py +9 -0
  84. travis/coding_agent/export_html.py +2890 -0
  85. travis/coding_agent/export_html_assets/vendor/highlight.min.js +1213 -0
  86. travis/coding_agent/export_html_assets/vendor/marked.min.js +6 -0
  87. travis/coding_agent/extension_cli.py +83 -0
  88. travis/coding_agent/extensions.py +1401 -0
  89. travis/coding_agent/input_expansion.py +169 -0
  90. travis/coding_agent/mailbox.py +115 -0
  91. travis/coding_agent/message_utils.py +43 -0
  92. travis/coding_agent/model_registry.py +817 -0
  93. travis/coding_agent/object_utils.py +32 -0
  94. travis/coding_agent/output_utils.py +9 -0
  95. travis/coding_agent/package_cli.py +192 -0
  96. travis/coding_agent/package_manager.py +655 -0
  97. travis/coding_agent/process_context.py +195 -0
  98. travis/coding_agent/processes/__init__.py +53 -0
  99. travis/coding_agent/processes/completions.py +693 -0
  100. travis/coding_agent/processes/containment.py +77 -0
  101. travis/coding_agent/processes/local.py +267 -0
  102. travis/coding_agent/processes/output.py +344 -0
  103. travis/coding_agent/processes/service.py +1087 -0
  104. travis/coding_agent/processes/transport.py +38 -0
  105. travis/coding_agent/processes/types.py +195 -0
  106. travis/coding_agent/project_trust.py +233 -0
  107. travis/coding_agent/prompt_templates.py +148 -0
  108. travis/coding_agent/resolve_config_value.py +181 -0
  109. travis/coding_agent/resource_discovery.py +118 -0
  110. travis/coding_agent/resource_loader.py +712 -0
  111. travis/coding_agent/rpc.py +258 -0
  112. travis/coding_agent/session_bash.py +182 -0
  113. travis/coding_agent/session_catalog.py +177 -0
  114. travis/coding_agent/session_commands.py +94 -0
  115. travis/coding_agent/session_events.py +280 -0
  116. travis/coding_agent/session_extensions.py +750 -0
  117. travis/coding_agent/session_index.py +397 -0
  118. travis/coding_agent/session_lock.py +42 -0
  119. travis/coding_agent/session_models.py +354 -0
  120. travis/coding_agent/session_persistence.py +557 -0
  121. travis/coding_agent/session_policy_controller.py +78 -0
  122. travis/coding_agent/session_store.py +967 -0
  123. travis/coding_agent/session_subagents.py +505 -0
  124. travis/coding_agent/session_tooling.py +309 -0
  125. travis/coding_agent/session_turns.py +710 -0
  126. travis/coding_agent/session_types.py +595 -0
  127. travis/coding_agent/settings_manager.py +639 -0
  128. travis/coding_agent/skills.py +242 -0
  129. travis/coding_agent/source_info.py +57 -0
  130. travis/coding_agent/sqlite_utils.py +21 -0
  131. travis/coding_agent/subagent_trace.py +662 -0
  132. travis/coding_agent/subagents.py +922 -0
  133. travis/coding_agent/subprocess_environment.py +52 -0
  134. travis/coding_agent/system_prompt.py +144 -0
  135. travis/coding_agent/themes.py +62 -0
  136. travis/coding_agent/tools/__init__.py +150 -0
  137. travis/coding_agent/tools/atomic_file.py +41 -0
  138. travis/coding_agent/tools/bash.py +672 -0
  139. travis/coding_agent/tools/common.py +31 -0
  140. travis/coding_agent/tools/edit.py +191 -0
  141. travis/coding_agent/tools/edit_diff.py +199 -0
  142. travis/coding_agent/tools/file_mutation_queue.py +39 -0
  143. travis/coding_agent/tools/find.py +175 -0
  144. travis/coding_agent/tools/grep.py +248 -0
  145. travis/coding_agent/tools/ls.py +139 -0
  146. travis/coding_agent/tools/output_spool.py +190 -0
  147. travis/coding_agent/tools/path_utils.py +188 -0
  148. travis/coding_agent/tools/process.py +534 -0
  149. travis/coding_agent/tools/read.py +442 -0
  150. travis/coding_agent/tools/truncate.py +188 -0
  151. travis/coding_agent/tools/types.py +78 -0
  152. travis/coding_agent/tools/write.py +127 -0
  153. travis/compaction/__init__.py +41 -0
  154. travis/compaction/compressor.py +1898 -0
  155. travis/compaction/policy.py +113 -0
  156. travis/compaction/strategy.py +101 -0
  157. travis/compaction/timing.py +895 -0
  158. travis/extensions/__init__.py +1 -0
  159. travis/resources/README.md +5 -0
  160. travis/resources/docs/README.md +3 -0
  161. travis/resources/docs/extensions.md +37 -0
  162. travis/resources/examples/README.md +3 -0
  163. travis/resources/extensions/hypa/__init__.py +301 -0
  164. travis/resources/extensions/hypa/hypa_tools.py +342 -0
  165. travis/runtime_facade.py +20 -0
  166. travis/tui/__init__.py +236 -0
  167. travis/tui/builtin_themes.py +362 -0
  168. travis/tui/component.py +2 -0
  169. travis/tui/components/__init__.py +35 -0
  170. travis/tui/components/autocomplete.py +481 -0
  171. travis/tui/components/base.py +253 -0
  172. travis/tui/components/editor.py +651 -0
  173. travis/tui/components/footer.py +286 -0
  174. travis/tui/components/image.py +108 -0
  175. travis/tui/components/loaders.py +141 -0
  176. travis/tui/components/markdown.py +269 -0
  177. travis/tui/components/multiline_editor.py +210 -0
  178. travis/tui/components/pickers.py +436 -0
  179. travis/tui/dispatcher.py +110 -0
  180. travis/tui/footer_data.py +342 -0
  181. travis/tui/fuzzy.py +106 -0
  182. travis/tui/interactive.py +759 -0
  183. travis/tui/interactive_command_dispatcher.py +378 -0
  184. travis/tui/interactive_custom_dialog.py +75 -0
  185. travis/tui/interactive_extensions.py +565 -0
  186. travis/tui/interactive_mode.py +272 -0
  187. travis/tui/interactive_model_auth.py +503 -0
  188. travis/tui/interactive_process_commands.py +407 -0
  189. travis/tui/interactive_session_commands.py +543 -0
  190. travis/tui/interactive_shutdown.py +146 -0
  191. travis/tui/interactive_turn_controller.py +270 -0
  192. travis/tui/interactive_view.py +742 -0
  193. travis/tui/keybindings.py +157 -0
  194. travis/tui/keys.py +300 -0
  195. travis/tui/kill_ring.py +34 -0
  196. travis/tui/native_modifiers.py +10 -0
  197. travis/tui/stdin_buffer.py +257 -0
  198. travis/tui/terminal.py +433 -0
  199. travis/tui/terminal_colors.py +57 -0
  200. travis/tui/terminal_image.py +416 -0
  201. travis/tui/theme.py +268 -0
  202. travis/tui/theme_controller.py +184 -0
  203. travis/tui/tui.py +944 -0
  204. travis/tui/undo_stack.py +27 -0
  205. travis/tui/user_commands.py +353 -0
  206. travis/tui/utils.py +479 -0
  207. travis/tui/word_navigation.py +148 -0
  208. travis234-2.3.1.dist-info/METADATA +442 -0
  209. travis234-2.3.1.dist-info/RECORD +214 -0
  210. travis234-2.3.1.dist-info/WHEEL +5 -0
  211. travis234-2.3.1.dist-info/entry_points.txt +2 -0
  212. travis234-2.3.1.dist-info/licenses/LICENSE +23 -0
  213. travis234-2.3.1.dist-info/licenses/NOTICE.md +13 -0
  214. travis234-2.3.1.dist-info/top_level.txt +1 -0
travis/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ from travis.app import CodingApp
2
+ from travis.coding_agent.agent_harness import AgentHarness, AgentHarnessConfig
3
+
4
+ __all__ = [
5
+ "AgentHarness",
6
+ "AgentHarnessConfig",
7
+ "CodingApp",
8
+ ]
@@ -0,0 +1,69 @@
1
+ """Core stateful-agent and bounded-loop interfaces."""
2
+
3
+ from travis.agent.agent import Agent, AgentState
4
+ from travis.agent.agent_loop import (
5
+ AgentEventSink,
6
+ AgentEventStream,
7
+ agent_loop,
8
+ agent_loop_continue,
9
+ run_agent_loop,
10
+ run_agent_loop_async,
11
+ run_agent_loop_continue,
12
+ run_agent_loop_continue_async,
13
+ )
14
+ from travis.agent.async_utils import MaybeAwaitable, resolve, run_sync
15
+ from travis.agent.run_lease import RunLease, RunLeaseToken
16
+ from travis.agent.tool_coordinator import ToolCoordinator
17
+ from travis.agent.types import (
18
+ AbortSignal,
19
+ AfterToolCallContext,
20
+ AfterToolCallResult,
21
+ AgentContext,
22
+ AgentEvent,
23
+ AgentLoopConfig,
24
+ AgentLoopTurnUpdate,
25
+ AgentMessage,
26
+ AgentTool,
27
+ AgentToolResult,
28
+ BeforeToolCallContext,
29
+ BeforeToolCallResult,
30
+ ImmediateToolOutcome,
31
+ PreparedToolCall,
32
+ PrepareNextTurnContext,
33
+ ShouldStopAfterTurnContext,
34
+ )
35
+
36
+ __all__ = [
37
+ "AbortSignal",
38
+ "AfterToolCallContext",
39
+ "AfterToolCallResult",
40
+ "Agent",
41
+ "AgentContext",
42
+ "AgentEvent",
43
+ "AgentEventSink",
44
+ "AgentEventStream",
45
+ "AgentLoopConfig",
46
+ "AgentLoopTurnUpdate",
47
+ "AgentMessage",
48
+ "AgentState",
49
+ "AgentTool",
50
+ "AgentToolResult",
51
+ "BeforeToolCallContext",
52
+ "BeforeToolCallResult",
53
+ "ImmediateToolOutcome",
54
+ "MaybeAwaitable",
55
+ "PreparedToolCall",
56
+ "RunLease",
57
+ "RunLeaseToken",
58
+ "ToolCoordinator",
59
+ "PrepareNextTurnContext",
60
+ "ShouldStopAfterTurnContext",
61
+ "agent_loop",
62
+ "agent_loop_continue",
63
+ "run_agent_loop",
64
+ "run_agent_loop_async",
65
+ "run_agent_loop_continue",
66
+ "run_agent_loop_continue_async",
67
+ "resolve",
68
+ "run_sync",
69
+ ]
travis/agent/agent.py ADDED
@@ -0,0 +1,443 @@
1
+ """Stateful Agent wrapper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import threading
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Callable, Optional, Union
9
+
10
+ from travis.ai.types import AssistantMessage, ImageContent, Message, Model, TextContent, UserMessage, empty_usage, now_ms
11
+ from travis.agent.agent_loop import (
12
+ AgentEventSink,
13
+ run_agent_loop_async,
14
+ run_agent_loop_continue_async,
15
+ )
16
+ from travis.agent.async_utils import resolve, run_sync
17
+ from travis.agent.run_lease import RunLease, RunLeaseToken
18
+ from travis.agent.types import (
19
+ AbortSignal,
20
+ AgentEndEvent,
21
+ AgentContext,
22
+ AgentEvent,
23
+ AgentLoopConfig,
24
+ AgentMessage,
25
+ AgentTool,
26
+ MessageEndEvent,
27
+ MessageStartEvent,
28
+ QueueMode,
29
+ ThinkingLevel,
30
+ TurnEndEvent,
31
+ )
32
+
33
+ Listener = Callable[..., None]
34
+
35
+
36
+ class PendingMessageQueue:
37
+ def __init__(self, mode: QueueMode = "one-at-a-time") -> None:
38
+ self.messages: list[AgentMessage] = []
39
+ self.mode = mode
40
+
41
+ def enqueue(self, message: AgentMessage) -> None:
42
+ self.messages.append(message)
43
+
44
+ def has_items(self) -> bool:
45
+ return bool(self.messages)
46
+
47
+ def drain(self) -> list[AgentMessage]:
48
+ if self.mode == "all":
49
+ drained = list(self.messages)
50
+ self.messages = []
51
+ return drained
52
+ if not self.messages:
53
+ return []
54
+ first = self.messages[0]
55
+ self.messages = self.messages[1:]
56
+ return [first]
57
+
58
+ def clear(self) -> None:
59
+ self.messages = []
60
+
61
+
62
+ @dataclass
63
+ class AgentState:
64
+ system_prompt: str
65
+ model: Model
66
+ thinking_level: ThinkingLevel = "off"
67
+ tools: list[AgentTool] = field(default_factory=list)
68
+ messages: list[AgentMessage] = field(default_factory=list)
69
+ is_streaming: bool = False
70
+ streaming_message: AgentMessage | None = None
71
+ pending_tool_calls: set[str] = field(default_factory=set)
72
+ error_message: str | None = None
73
+
74
+
75
+ class Agent:
76
+ """Owns conversation state and drives the functional agent loop."""
77
+
78
+ def __init__(
79
+ self,
80
+ *,
81
+ system_prompt: str,
82
+ model: Model,
83
+ convert_to_llm: Callable[[list[AgentMessage]], list[Message]],
84
+ tools: Optional[list[AgentTool]] = None,
85
+ thinking_level: ThinkingLevel = "off",
86
+ tool_execution: str = "parallel",
87
+ max_parallel_tools: int = 8,
88
+ before_tool_call=None,
89
+ after_tool_call=None,
90
+ should_stop_after_turn=None,
91
+ prepare_next_turn=None,
92
+ prepare_next_turn_with_context=None,
93
+ transform_context=None,
94
+ steering_mode: QueueMode = "one-at-a-time",
95
+ follow_up_mode: QueueMode = "one-at-a-time",
96
+ session_id: str | None = None,
97
+ thinking_budgets: dict[str, int] | None = None,
98
+ transport: str = "auto",
99
+ max_retry_delay_ms: int | None = None,
100
+ on_payload=None,
101
+ on_headers=None,
102
+ on_response=None,
103
+ stream_fn=None,
104
+ ) -> None:
105
+ self._state = AgentState(
106
+ system_prompt=system_prompt,
107
+ model=model,
108
+ thinking_level=thinking_level,
109
+ tools=list(tools or []),
110
+ )
111
+ self._convert_to_llm = convert_to_llm
112
+ self._tool_execution = tool_execution
113
+ self._max_parallel_tools = max(1, int(max_parallel_tools))
114
+ self._before_tool_call = before_tool_call
115
+ self._after_tool_call = after_tool_call
116
+ self._should_stop_after_turn = should_stop_after_turn
117
+ self._prepare_next_turn = prepare_next_turn
118
+ self._prepare_next_turn_with_context = prepare_next_turn_with_context
119
+ self._transform_context = transform_context
120
+ self.session_id = session_id
121
+ self.thinking_budgets = thinking_budgets
122
+ self.transport = transport
123
+ self.max_retry_delay_ms = max_retry_delay_ms
124
+ self.on_payload = on_payload
125
+ self.on_headers = on_headers
126
+ self.on_response = on_response
127
+ self._stream_fn = stream_fn
128
+ self._listeners: list[Listener] = []
129
+ self._signal = AbortSignal()
130
+ self._run_state_lock = threading.Lock()
131
+ self._run_lease = RunLease()
132
+ self._active_run_token: RunLeaseToken | None = None
133
+ self._idle_event = threading.Event()
134
+ self._idle_event.set()
135
+ self._steering = PendingMessageQueue(steering_mode)
136
+ self._follow_up = PendingMessageQueue(follow_up_mode)
137
+
138
+ @property
139
+ def state(self) -> AgentState:
140
+ return self._state
141
+
142
+ @property
143
+ def signal(self) -> AbortSignal:
144
+ return self._signal
145
+
146
+ @property
147
+ def run_lease(self) -> RunLease:
148
+ return self._run_lease
149
+
150
+ @property
151
+ def steering_mode(self) -> QueueMode:
152
+ return self._steering.mode
153
+
154
+ @steering_mode.setter
155
+ def steering_mode(self, mode: QueueMode) -> None:
156
+ self._steering.mode = mode
157
+
158
+ @property
159
+ def follow_up_mode(self) -> QueueMode:
160
+ return self._follow_up.mode
161
+
162
+ @follow_up_mode.setter
163
+ def follow_up_mode(self, mode: QueueMode) -> None:
164
+ self._follow_up.mode = mode
165
+
166
+ def subscribe(self, listener: Listener) -> Callable[[], None]:
167
+ self._listeners.append(listener)
168
+
169
+ def _unsubscribe() -> None:
170
+ if listener in self._listeners:
171
+ self._listeners.remove(listener)
172
+
173
+ return _unsubscribe
174
+
175
+ def steer(self, message: AgentMessage) -> None:
176
+ self._steering.enqueue(message)
177
+
178
+ def follow_up(self, message: AgentMessage) -> None:
179
+ self._follow_up.enqueue(message)
180
+
181
+ def clear_steering_queue(self) -> None:
182
+ self._steering.clear()
183
+
184
+ def clear_follow_up_queue(self) -> None:
185
+ self._follow_up.clear()
186
+
187
+ def clear_all_queues(self) -> None:
188
+ self.clear_steering_queue()
189
+ self.clear_follow_up_queue()
190
+
191
+ def has_queued_messages(self) -> bool:
192
+ return self._steering.has_items() or self._follow_up.has_items()
193
+
194
+ def abort(self) -> None:
195
+ self._signal.abort()
196
+
197
+ def reset_abort_signal(self) -> AbortSignal:
198
+ if self._state.is_streaming:
199
+ return self._signal
200
+ if self._signal.aborted:
201
+ self._signal = AbortSignal()
202
+ return self._signal
203
+
204
+
205
+ def wait_for_idle(self, timeout: float | None = None) -> bool:
206
+ return self._idle_event.wait(timeout)
207
+
208
+ def reset(self) -> None:
209
+ if self._run_lease.active:
210
+ raise RuntimeError("Cannot reset Agent while an active run is in progress; abort and wait for idle first")
211
+ self._state.messages = []
212
+ self._state.is_streaming = False
213
+ self._state.error_message = None
214
+ self._state.streaming_message = None
215
+ self._state.pending_tool_calls = set()
216
+ self.clear_all_queues()
217
+
218
+ def _build_config(self, *, skip_initial_steering_poll: bool = False) -> AgentLoopConfig:
219
+ skip_steering_poll = {"value": skip_initial_steering_poll}
220
+
221
+ def get_steering_messages() -> list[AgentMessage]:
222
+ if skip_steering_poll["value"]:
223
+ skip_steering_poll["value"] = False
224
+ return []
225
+ return self._drain_steering()
226
+
227
+ def prepare_next_turn_adapter(context):
228
+ if self._prepare_next_turn_with_context:
229
+ return self._prepare_next_turn_with_context(context, self._signal)
230
+ if self._prepare_next_turn:
231
+ return self._prepare_next_turn(self._signal)
232
+ return None
233
+
234
+ return AgentLoopConfig(
235
+ model=self._state.model,
236
+ convert_to_llm=self._convert_to_llm,
237
+ get_steering_messages=get_steering_messages,
238
+ get_follow_up_messages=self._drain_follow_up,
239
+ prepare_next_turn=prepare_next_turn_adapter
240
+ if self._prepare_next_turn_with_context or self._prepare_next_turn
241
+ else None,
242
+ tool_execution=self._tool_execution,
243
+ max_parallel_tools=self._max_parallel_tools,
244
+ before_tool_call=self._before_tool_call,
245
+ after_tool_call=self._after_tool_call,
246
+ should_stop_after_turn=self._should_stop_after_turn,
247
+ transform_context=self._transform_context,
248
+ reasoning=None if self._state.thinking_level == "off" else self._state.thinking_level,
249
+ session_id=self.session_id,
250
+ transport=self.transport,
251
+ thinking_budgets=self.thinking_budgets,
252
+ max_retry_delay_ms=self.max_retry_delay_ms,
253
+ on_payload=self.on_payload,
254
+ on_headers=self.on_headers,
255
+ on_response=self.on_response,
256
+ max_tokens=self._state.model.max_tokens or None,
257
+ )
258
+
259
+ def _drain_steering(self) -> list[AgentMessage]:
260
+ return self._steering.drain()
261
+
262
+ def _drain_follow_up(self) -> list[AgentMessage]:
263
+ return self._follow_up.drain()
264
+
265
+ def _context(self) -> AgentContext:
266
+ return AgentContext(
267
+ system_prompt=self._state.system_prompt,
268
+ messages=list(self._state.messages),
269
+ tools=list(self._state.tools),
270
+ )
271
+
272
+ def prompt(
273
+ self,
274
+ prompt: Union[str, AgentMessage, list[AgentMessage]],
275
+ stream_fn=None,
276
+ images: list[ImageContent] | None = None,
277
+ ) -> list[AgentMessage]:
278
+ return run_sync(self.async_prompt(prompt, stream_fn=stream_fn, images=images))
279
+
280
+ async def async_prompt(
281
+ self,
282
+ prompt: Union[str, AgentMessage, list[AgentMessage]],
283
+ stream_fn=None,
284
+ images: list[ImageContent] | None = None,
285
+ ) -> list[AgentMessage]:
286
+ self._begin_run(
287
+ "Agent is already processing a prompt. Use steer() or follow_up() to queue messages, or wait for completion."
288
+ )
289
+ if isinstance(prompt, str):
290
+ content = [TextContent(text=prompt)]
291
+ if images:
292
+ content.extend(images)
293
+ messages: list[AgentMessage] = [UserMessage(content=content, timestamp=now_ms())]
294
+ elif isinstance(prompt, list):
295
+ messages = list(prompt)
296
+ else:
297
+ messages = [prompt]
298
+ try:
299
+ new_messages = await run_agent_loop_async(
300
+ messages,
301
+ self._context(),
302
+ self._build_config(),
303
+ self._make_sink(),
304
+ self._signal,
305
+ stream_fn or self._stream_fn,
306
+ )
307
+ except Exception as error: # noqa: BLE001
308
+ new_messages = await self._handle_run_failure(error, self._signal.aborted)
309
+ finally:
310
+ self._finish_run()
311
+ return new_messages
312
+
313
+ def continue_(self, stream_fn=None) -> list[AgentMessage]:
314
+ return run_sync(self.async_continue(stream_fn=stream_fn))
315
+
316
+ async def async_continue(self, stream_fn=None) -> list[AgentMessage]:
317
+ self._begin_run("Agent is already processing. Wait for completion before continuing.")
318
+ try:
319
+ context = self._context()
320
+ last_message = context.messages[-1] if context.messages else None
321
+ if last_message is None:
322
+ raise ValueError("No messages to continue from")
323
+ if getattr(last_message, "role", None) == "assistant":
324
+ queued_steering = self._drain_steering()
325
+ if queued_steering:
326
+ run_messages = queued_steering
327
+ run_config = self._build_config(skip_initial_steering_poll=True)
328
+ continue_existing = False
329
+ else:
330
+ queued_follow_up = self._drain_follow_up()
331
+ if not queued_follow_up:
332
+ raise ValueError("Cannot continue from message role: assistant")
333
+ run_messages = queued_follow_up
334
+ run_config = self._build_config()
335
+ continue_existing = False
336
+ else:
337
+ run_messages = []
338
+ run_config = self._build_config()
339
+ continue_existing = True
340
+
341
+ try:
342
+ if continue_existing:
343
+ return await run_agent_loop_continue_async(
344
+ context,
345
+ run_config,
346
+ self._make_sink(),
347
+ self._signal,
348
+ stream_fn or self._stream_fn,
349
+ )
350
+ return await run_agent_loop_async(
351
+ run_messages,
352
+ context,
353
+ run_config,
354
+ self._make_sink(),
355
+ self._signal,
356
+ stream_fn or self._stream_fn,
357
+ )
358
+ except Exception as error: # noqa: BLE001
359
+ return await self._handle_run_failure(error, self._signal.aborted)
360
+ finally:
361
+ self._finish_run()
362
+
363
+ async def _handle_run_failure(self, error: BaseException, aborted: bool) -> list[AgentMessage]:
364
+ failure_message = AssistantMessage(
365
+ content=[TextContent(text="")],
366
+ api=self._state.model.api,
367
+ provider=self._state.model.provider,
368
+ model=self._state.model.id,
369
+ usage=empty_usage(),
370
+ stop_reason="aborted" if aborted else "error",
371
+ error_message=str(error),
372
+ timestamp=now_ms(),
373
+ )
374
+ sink = self._make_sink()
375
+ await sink(MessageStartEvent(message=failure_message))
376
+ await sink(MessageEndEvent(message=failure_message))
377
+ await sink(TurnEndEvent(message=failure_message, tool_results=[]))
378
+ await sink(AgentEndEvent(messages=[failure_message]))
379
+ return [failure_message]
380
+
381
+ def _begin_run(self, active_error: str) -> None:
382
+ with self._run_state_lock:
383
+ token = self._run_lease.acquire(active_error)
384
+ self._active_run_token = token
385
+ self._signal = AbortSignal()
386
+ self._idle_event.clear()
387
+ self._state.is_streaming = True
388
+ self._state.streaming_message = None
389
+ self._state.error_message = None
390
+
391
+ def _finish_run(self) -> None:
392
+ with self._run_state_lock:
393
+ self._state.is_streaming = False
394
+ self._state.streaming_message = None
395
+ self._state.pending_tool_calls = set()
396
+ self._idle_event.set()
397
+ token = self._active_run_token
398
+ self._active_run_token = None
399
+ if token is not None:
400
+ token.release()
401
+
402
+ def _make_sink(self) -> AgentEventSink:
403
+ async def _sink(event: AgentEvent) -> None:
404
+ self._process_event(event)
405
+ for listener in list(self._listeners):
406
+ if _listener_accepts_signal(listener):
407
+ await resolve(listener(event, self._signal))
408
+ else:
409
+ await resolve(listener(event))
410
+
411
+ return _sink
412
+
413
+ def _process_event(self, event: AgentEvent) -> None:
414
+ etype = event.type
415
+ if etype == "message_start":
416
+ if getattr(event.message, "role", None) == "assistant":
417
+ self._state.streaming_message = event.message
418
+ elif etype == "message_update":
419
+ self._state.streaming_message = event.message
420
+ elif etype == "message_end":
421
+ self._state.messages.append(event.message)
422
+ if getattr(event.message, "role", None) == "assistant":
423
+ self._state.streaming_message = None
424
+ if getattr(event.message, "stop_reason", None) in ("error", "aborted"):
425
+ self._state.error_message = getattr(event.message, "error_message", None)
426
+ elif etype == "tool_execution_start":
427
+ self._state.pending_tool_calls.add(event.tool_call_id)
428
+ elif etype == "tool_execution_end":
429
+ self._state.pending_tool_calls.discard(event.tool_call_id)
430
+
431
+
432
+ def _listener_accepts_signal(listener: Listener) -> bool:
433
+ try:
434
+ signature = inspect.signature(listener)
435
+ except (TypeError, ValueError):
436
+ return False
437
+ positional_count = 0
438
+ for parameter in signature.parameters.values():
439
+ if parameter.kind == inspect.Parameter.VAR_POSITIONAL:
440
+ return True
441
+ if parameter.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
442
+ positional_count += 1
443
+ return positional_count >= 2