fast-agent-mcp 0.3.15__py3-none-any.whl → 0.3.17__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.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

Files changed (47) hide show
  1. fast_agent/__init__.py +2 -0
  2. fast_agent/agents/agent_types.py +5 -0
  3. fast_agent/agents/llm_agent.py +7 -0
  4. fast_agent/agents/llm_decorator.py +6 -0
  5. fast_agent/agents/mcp_agent.py +134 -10
  6. fast_agent/cli/__main__.py +35 -0
  7. fast_agent/cli/commands/check_config.py +85 -0
  8. fast_agent/cli/commands/go.py +100 -36
  9. fast_agent/cli/constants.py +15 -1
  10. fast_agent/cli/main.py +2 -1
  11. fast_agent/config.py +39 -10
  12. fast_agent/constants.py +8 -0
  13. fast_agent/context.py +24 -15
  14. fast_agent/core/direct_decorators.py +9 -0
  15. fast_agent/core/fastagent.py +101 -1
  16. fast_agent/core/logging/listeners.py +8 -0
  17. fast_agent/interfaces.py +12 -0
  18. fast_agent/llm/fastagent_llm.py +45 -0
  19. fast_agent/llm/memory.py +26 -1
  20. fast_agent/llm/model_database.py +4 -1
  21. fast_agent/llm/model_factory.py +4 -2
  22. fast_agent/llm/model_info.py +19 -43
  23. fast_agent/llm/provider/anthropic/llm_anthropic.py +112 -0
  24. fast_agent/llm/provider/google/llm_google_native.py +238 -7
  25. fast_agent/llm/provider/openai/llm_openai.py +382 -19
  26. fast_agent/llm/provider/openai/responses.py +133 -0
  27. fast_agent/resources/setup/agent.py +2 -0
  28. fast_agent/resources/setup/fastagent.config.yaml +6 -0
  29. fast_agent/skills/__init__.py +9 -0
  30. fast_agent/skills/registry.py +208 -0
  31. fast_agent/tools/shell_runtime.py +404 -0
  32. fast_agent/ui/console_display.py +47 -996
  33. fast_agent/ui/elicitation_form.py +76 -24
  34. fast_agent/ui/elicitation_style.py +2 -2
  35. fast_agent/ui/enhanced_prompt.py +107 -37
  36. fast_agent/ui/history_display.py +20 -5
  37. fast_agent/ui/interactive_prompt.py +108 -3
  38. fast_agent/ui/markdown_helpers.py +104 -0
  39. fast_agent/ui/markdown_truncator.py +103 -45
  40. fast_agent/ui/message_primitives.py +50 -0
  41. fast_agent/ui/streaming.py +638 -0
  42. fast_agent/ui/tool_display.py +417 -0
  43. {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/METADATA +8 -7
  44. {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/RECORD +47 -39
  45. {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/WHEEL +0 -0
  46. {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/entry_points.txt +0 -0
  47. {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/licenses/LICENSE +0 -0
@@ -7,7 +7,7 @@ from mcp.types import (
7
7
  ContentBlock,
8
8
  TextContent,
9
9
  )
10
- from openai import APIError, AsyncOpenAI, AuthenticationError
10
+ from openai import APIError, AsyncOpenAI, AuthenticationError, DefaultAioHttpClient
11
11
  from openai.lib.streaming.chat import ChatCompletionStreamState
12
12
 
13
13
  # from openai.types.beta.chat import
@@ -95,9 +95,19 @@ class OpenAILLM(FastAgentLLM[ChatCompletionMessageParam, ChatCompletionMessage])
95
95
  return self.context.config.openai.base_url if self.context.config.openai else None
96
96
 
97
97
  def _openai_client(self) -> AsyncOpenAI:
98
- try:
99
- return AsyncOpenAI(api_key=self._api_key(), base_url=self._base_url())
98
+ """
99
+ Create an OpenAI client instance.
100
+ Subclasses can override this to provide different client types (e.g., AzureOpenAI).
100
101
 
102
+ Note: The returned client should be used within an async context manager
103
+ to ensure proper cleanup of aiohttp sessions.
104
+ """
105
+ try:
106
+ return AsyncOpenAI(
107
+ api_key=self._api_key(),
108
+ base_url=self._base_url(),
109
+ http_client=DefaultAioHttpClient(),
110
+ )
101
111
  except AuthenticationError as e:
102
112
  raise ProviderKeyError(
103
113
  "Invalid OpenAI API key",
@@ -105,6 +115,96 @@ class OpenAILLM(FastAgentLLM[ChatCompletionMessageParam, ChatCompletionMessage])
105
115
  "Please check that your API key is valid and not expired.",
106
116
  ) from e
107
117
 
118
+ def _streams_tool_arguments(self) -> bool:
119
+ """
120
+ Determine whether the current provider streams tool call arguments incrementally.
121
+
122
+ Official OpenAI and Azure OpenAI endpoints stream arguments. Most third-party
123
+ OpenAI-compatible gateways (e.g. OpenRouter, Moonshot) deliver the full arguments
124
+ once, so we should treat them as non-streaming to restore the legacy \"Calling Tool\"
125
+ display experience.
126
+ """
127
+ if self.provider == Provider.AZURE:
128
+ return True
129
+
130
+ if self.provider == Provider.OPENAI:
131
+ base_url = self._base_url()
132
+ if not base_url:
133
+ return True
134
+ lowered = base_url.lower()
135
+ return "api.openai" in lowered or "openai.azure" in lowered or "azure.com" in lowered
136
+
137
+ return False
138
+
139
+ def _emit_tool_notification_fallback(
140
+ self,
141
+ tool_calls: Any,
142
+ notified_indices: set[int],
143
+ *,
144
+ streams_arguments: bool,
145
+ model: str,
146
+ ) -> None:
147
+ """Emit start/stop notifications when streaming metadata was missing."""
148
+ if not tool_calls:
149
+ return
150
+
151
+ for index, tool_call in enumerate(tool_calls):
152
+ if index in notified_indices:
153
+ continue
154
+
155
+ tool_name = None
156
+ tool_use_id = None
157
+
158
+ try:
159
+ tool_use_id = getattr(tool_call, "id", None)
160
+ function = getattr(tool_call, "function", None)
161
+ if function:
162
+ tool_name = getattr(function, "name", None)
163
+ except Exception:
164
+ tool_use_id = None
165
+ tool_name = None
166
+
167
+ if not tool_name:
168
+ tool_name = "tool"
169
+ if not tool_use_id:
170
+ tool_use_id = f"tool-{index}"
171
+
172
+ payload = {
173
+ "tool_name": tool_name,
174
+ "tool_use_id": tool_use_id,
175
+ "index": index,
176
+ "streams_arguments": streams_arguments,
177
+ }
178
+
179
+ self._notify_tool_stream_listeners("start", payload)
180
+ self.logger.info(
181
+ "Model emitted fallback tool notification",
182
+ data={
183
+ "progress_action": ProgressAction.CALLING_TOOL,
184
+ "agent_name": self.name,
185
+ "model": model,
186
+ "tool_name": tool_name,
187
+ "tool_use_id": tool_use_id,
188
+ "tool_event": "start",
189
+ "streams_arguments": streams_arguments,
190
+ "fallback": True,
191
+ },
192
+ )
193
+ self._notify_tool_stream_listeners("stop", payload)
194
+ self.logger.info(
195
+ "Model emitted fallback tool notification",
196
+ data={
197
+ "progress_action": ProgressAction.CALLING_TOOL,
198
+ "agent_name": self.name,
199
+ "model": model,
200
+ "tool_name": tool_name,
201
+ "tool_use_id": tool_use_id,
202
+ "tool_event": "stop",
203
+ "streams_arguments": streams_arguments,
204
+ "fallback": True,
205
+ },
206
+ )
207
+
108
208
  async def _process_stream(self, stream, model: str):
109
209
  """Process the streaming response and display real-time token usage."""
110
210
  # Track estimated output tokens by counting text chunks
@@ -113,22 +213,145 @@ class OpenAILLM(FastAgentLLM[ChatCompletionMessageParam, ChatCompletionMessage])
113
213
  # For non-OpenAI providers (like Ollama), ChatCompletionStreamState might not work correctly
114
214
  # Fall back to manual accumulation if needed
115
215
  # TODO -- consider this and whether to subclass instead
116
- if self.provider in [Provider.GENERIC, Provider.OPENROUTER, Provider.GOOGLE_OAI]:
216
+ if self.provider in [
217
+ Provider.GENERIC,
218
+ Provider.OPENROUTER,
219
+ Provider.GOOGLE_OAI,
220
+ ]:
117
221
  return await self._process_stream_manual(stream, model)
118
222
 
119
223
  # Use ChatCompletionStreamState helper for accumulation (OpenAI only)
120
224
  state = ChatCompletionStreamState()
121
225
 
226
+ # Track tool call state for stream events
227
+ tool_call_started: dict[int, dict[str, Any]] = {}
228
+ streams_arguments = self._streams_tool_arguments()
229
+ notified_tool_indices: set[int] = set()
230
+
122
231
  # Process the stream chunks
123
232
  async for chunk in stream:
124
233
  # Handle chunk accumulation
125
234
  state.handle_chunk(chunk)
235
+ # Process streaming events for tool calls
236
+ if chunk.choices:
237
+ choice = chunk.choices[0]
238
+ delta = choice.delta
239
+
240
+ # Handle tool call streaming
241
+ if delta.tool_calls:
242
+ for tool_call in delta.tool_calls:
243
+ index = tool_call.index
244
+
245
+ # Fire "start" event on first chunk for this tool call
246
+ if index is None:
247
+ continue
248
+
249
+ existing_info = tool_call_started.get(index)
250
+ tool_use_id = tool_call.id or (
251
+ existing_info.get("tool_use_id") if existing_info else None
252
+ )
253
+ function_name = (
254
+ tool_call.function.name
255
+ if tool_call.function and tool_call.function.name
256
+ else (existing_info.get("tool_name") if existing_info else None)
257
+ )
126
258
 
127
- # Count tokens in real-time from content deltas
128
- if chunk.choices and chunk.choices[0].delta.content:
129
- content = chunk.choices[0].delta.content
130
- # Use base class method for token estimation and progress emission
131
- estimated_tokens = self._update_streaming_progress(content, model, estimated_tokens)
259
+ if existing_info is None and tool_use_id and function_name:
260
+ tool_call_started[index] = {
261
+ "tool_name": function_name,
262
+ "tool_use_id": tool_use_id,
263
+ "streams_arguments": streams_arguments,
264
+ }
265
+ self._notify_tool_stream_listeners(
266
+ "start",
267
+ {
268
+ "tool_name": function_name,
269
+ "tool_use_id": tool_use_id,
270
+ "index": index,
271
+ "streams_arguments": streams_arguments,
272
+ },
273
+ )
274
+ self.logger.info(
275
+ "Model started streaming tool call",
276
+ data={
277
+ "progress_action": ProgressAction.CALLING_TOOL,
278
+ "agent_name": self.name,
279
+ "model": model,
280
+ "tool_name": function_name,
281
+ "tool_use_id": tool_use_id,
282
+ "tool_event": "start",
283
+ "streams_arguments": streams_arguments,
284
+ },
285
+ )
286
+ notified_tool_indices.add(index)
287
+ elif existing_info:
288
+ if tool_use_id:
289
+ existing_info["tool_use_id"] = tool_use_id
290
+ if function_name:
291
+ existing_info["tool_name"] = function_name
292
+
293
+ # Fire "delta" event for argument chunks
294
+ if tool_call.function and tool_call.function.arguments:
295
+ info = tool_call_started.setdefault(
296
+ index,
297
+ {
298
+ "tool_name": function_name,
299
+ "tool_use_id": tool_use_id,
300
+ "streams_arguments": streams_arguments,
301
+ },
302
+ )
303
+ self._notify_tool_stream_listeners(
304
+ "delta",
305
+ {
306
+ "tool_name": info.get("tool_name"),
307
+ "tool_use_id": info.get("tool_use_id"),
308
+ "index": index,
309
+ "chunk": tool_call.function.arguments,
310
+ "streams_arguments": info.get("streams_arguments", False),
311
+ },
312
+ )
313
+
314
+ # Handle text content streaming
315
+ if delta.content:
316
+ content = delta.content
317
+ # Use base class method for token estimation and progress emission
318
+ estimated_tokens = self._update_streaming_progress(
319
+ content, model, estimated_tokens
320
+ )
321
+ self._notify_tool_stream_listeners(
322
+ "text",
323
+ {
324
+ "chunk": content,
325
+ "streams_arguments": streams_arguments,
326
+ },
327
+ )
328
+
329
+ # Fire "stop" event when tool calls complete
330
+ if choice.finish_reason == "tool_calls":
331
+ for index, info in list(tool_call_started.items()):
332
+ self._notify_tool_stream_listeners(
333
+ "stop",
334
+ {
335
+ "tool_name": info.get("tool_name"),
336
+ "tool_use_id": info.get("tool_use_id"),
337
+ "index": index,
338
+ "streams_arguments": info.get("streams_arguments", False),
339
+ },
340
+ )
341
+ self.logger.info(
342
+ "Model finished streaming tool call",
343
+ data={
344
+ "progress_action": ProgressAction.CALLING_TOOL,
345
+ "agent_name": self.name,
346
+ "model": model,
347
+ "tool_name": info.get("tool_name"),
348
+ "tool_use_id": info.get("tool_use_id"),
349
+ "tool_event": "stop",
350
+ "streams_arguments": info.get("streams_arguments", False),
351
+ },
352
+ )
353
+ notified_tool_indices.add(index)
354
+ tool_call_started.clear()
132
355
 
133
356
  # Check if we hit the length limit to avoid LengthFinishReasonError
134
357
  current_snapshot = state.current_completion_snapshot
@@ -157,12 +380,24 @@ class OpenAILLM(FastAgentLLM[ChatCompletionMessageParam, ChatCompletionMessage])
157
380
  f"Streaming complete - Model: {model}, Input tokens: {final_completion.usage.prompt_tokens}, Output tokens: {final_completion.usage.completion_tokens}"
158
381
  )
159
382
 
383
+ final_message = None
384
+ if hasattr(final_completion, "choices") and final_completion.choices:
385
+ final_message = getattr(final_completion.choices[0], "message", None)
386
+ tool_calls = getattr(final_message, "tool_calls", None) if final_message else None
387
+ self._emit_tool_notification_fallback(
388
+ tool_calls,
389
+ notified_tool_indices,
390
+ streams_arguments=streams_arguments,
391
+ model=model,
392
+ )
393
+
160
394
  return final_completion
161
395
 
162
396
  # TODO - as per other comment this needs to go in another class. There are a number of "special" cases dealt with
163
397
  # here to deal with OpenRouter idiosyncrasies between e.g. Anthropic and Gemini models.
164
398
  async def _process_stream_manual(self, stream, model: str):
165
399
  """Manual stream processing for providers like Ollama that may not work with ChatCompletionStreamState."""
400
+
166
401
  from openai.types.chat import ChatCompletionMessageToolCall
167
402
 
168
403
  # Track estimated output tokens by counting text chunks
@@ -176,14 +411,132 @@ class OpenAILLM(FastAgentLLM[ChatCompletionMessageParam, ChatCompletionMessage])
176
411
  finish_reason = None
177
412
  usage_data = None
178
413
 
414
+ # Track tool call state for stream events
415
+ tool_call_started: dict[int, dict[str, Any]] = {}
416
+ streams_arguments = self._streams_tool_arguments()
417
+ notified_tool_indices: set[int] = set()
418
+
179
419
  # Process the stream chunks manually
180
420
  async for chunk in stream:
181
- # Count tokens in real-time from content deltas
182
- if chunk.choices and chunk.choices[0].delta.content:
183
- content = chunk.choices[0].delta.content
184
- accumulated_content += content
185
- # Use base class method for token estimation and progress emission
186
- estimated_tokens = self._update_streaming_progress(content, model, estimated_tokens)
421
+ # Process streaming events for tool calls
422
+ if chunk.choices:
423
+ choice = chunk.choices[0]
424
+ delta = choice.delta
425
+
426
+ # Handle tool call streaming
427
+ if delta.tool_calls:
428
+ for tool_call in delta.tool_calls:
429
+ if tool_call.index is not None:
430
+ index = tool_call.index
431
+
432
+ existing_info = tool_call_started.get(index)
433
+ tool_use_id = tool_call.id or (
434
+ existing_info.get("tool_use_id") if existing_info else None
435
+ )
436
+ function_name = (
437
+ tool_call.function.name
438
+ if tool_call.function and tool_call.function.name
439
+ else (existing_info.get("tool_name") if existing_info else None)
440
+ )
441
+
442
+ # Fire "start" event on first chunk for this tool call
443
+ if index not in tool_call_started and tool_use_id and function_name:
444
+ tool_call_started[index] = {
445
+ "tool_name": function_name,
446
+ "tool_use_id": tool_use_id,
447
+ "streams_arguments": streams_arguments,
448
+ }
449
+ self._notify_tool_stream_listeners(
450
+ "start",
451
+ {
452
+ "tool_name": function_name,
453
+ "tool_use_id": tool_use_id,
454
+ "index": index,
455
+ "streams_arguments": streams_arguments,
456
+ },
457
+ )
458
+ self.logger.info(
459
+ "Model started streaming tool call",
460
+ data={
461
+ "progress_action": ProgressAction.CALLING_TOOL,
462
+ "agent_name": self.name,
463
+ "model": model,
464
+ "tool_name": function_name,
465
+ "tool_use_id": tool_use_id,
466
+ "tool_event": "start",
467
+ "streams_arguments": streams_arguments,
468
+ },
469
+ )
470
+ notified_tool_indices.add(index)
471
+ elif existing_info:
472
+ if tool_use_id:
473
+ existing_info["tool_use_id"] = tool_use_id
474
+ if function_name:
475
+ existing_info["tool_name"] = function_name
476
+
477
+ # Fire "delta" event for argument chunks
478
+ if tool_call.function and tool_call.function.arguments:
479
+ info = tool_call_started.setdefault(
480
+ index,
481
+ {
482
+ "tool_name": function_name,
483
+ "tool_use_id": tool_use_id,
484
+ "streams_arguments": streams_arguments,
485
+ },
486
+ )
487
+ self._notify_tool_stream_listeners(
488
+ "delta",
489
+ {
490
+ "tool_name": info.get("tool_name"),
491
+ "tool_use_id": info.get("tool_use_id"),
492
+ "index": index,
493
+ "chunk": tool_call.function.arguments,
494
+ "streams_arguments": info.get("streams_arguments", False),
495
+ },
496
+ )
497
+
498
+ # Handle text content streaming
499
+ if delta.content:
500
+ content = delta.content
501
+ accumulated_content += content
502
+ # Use base class method for token estimation and progress emission
503
+ estimated_tokens = self._update_streaming_progress(
504
+ content, model, estimated_tokens
505
+ )
506
+ self._notify_tool_stream_listeners(
507
+ "text",
508
+ {
509
+ "chunk": content,
510
+ "streams_arguments": streams_arguments,
511
+ },
512
+ )
513
+
514
+ # Fire "stop" event when tool calls complete
515
+ if choice.finish_reason == "tool_calls":
516
+ for index, info in list(tool_call_started.items()):
517
+ self._notify_tool_stream_listeners(
518
+ "stop",
519
+ {
520
+ "tool_name": info.get("tool_name"),
521
+ "tool_use_id": info.get("tool_use_id"),
522
+ "index": index,
523
+ "streams_arguments": info.get("streams_arguments", False),
524
+ },
525
+ )
526
+ self.logger.info(
527
+ "Model finished streaming tool call",
528
+ data={
529
+ "progress_action": ProgressAction.CALLING_TOOL,
530
+ "agent_name": self.name,
531
+ "model": model,
532
+ "tool_name": info.get("tool_name"),
533
+ "tool_use_id": info.get("tool_use_id"),
534
+ "tool_event": "stop",
535
+ "streams_arguments": info.get("streams_arguments", False),
536
+ },
537
+ )
538
+ notified_tool_indices.add(index)
539
+ tool_call_started.clear()
187
540
 
188
541
  # Extract other fields from the chunk
189
542
  if chunk.choices:
@@ -284,6 +637,15 @@ class OpenAILLM(FastAgentLLM[ChatCompletionMessageParam, ChatCompletionMessage])
284
637
  f"Streaming complete - Model: {model}, Input tokens: {getattr(usage_data, 'prompt_tokens', 0)}, Output tokens: {actual_tokens}"
285
638
  )
286
639
 
640
+ final_message = final_completion.choices[0].message if final_completion.choices else None
641
+ tool_calls = getattr(final_message, "tool_calls", None) if final_message else None
642
+ self._emit_tool_notification_fallback(
643
+ tool_calls,
644
+ notified_tool_indices,
645
+ streams_arguments=streams_arguments,
646
+ model=model,
647
+ )
648
+
287
649
  return final_completion
288
650
 
289
651
  async def _openai_completion(
@@ -343,11 +705,12 @@ class OpenAILLM(FastAgentLLM[ChatCompletionMessageParam, ChatCompletionMessage])
343
705
  self._log_chat_progress(self.chat_turn(), model=self.default_request_params.model)
344
706
  model_name = self.default_request_params.model or DEFAULT_OPENAI_MODEL
345
707
 
346
- # Use basic streaming API
708
+ # Use basic streaming API with context manager to properly close aiohttp session
347
709
  try:
348
- stream = await self._openai_client().chat.completions.create(**arguments)
349
- # Process the stream
350
- response = await self._process_stream(stream, model_name)
710
+ async with self._openai_client() as client:
711
+ stream = await client.chat.completions.create(**arguments)
712
+ # Process the stream
713
+ response = await self._process_stream(stream, model_name)
351
714
  except APIError as error:
352
715
  self.logger.error("APIError during OpenAI completion", exc_info=error)
353
716
  return self._stream_failure_response(error, model_name)
@@ -0,0 +1,133 @@
1
+ # from openai.types.beta.chat import
2
+ from typing import List
3
+
4
+ from mcp import Tool
5
+ from mcp.types import ContentBlock, TextContent
6
+ from openai import AsyncOpenAI
7
+ from openai.types.chat import (
8
+ ChatCompletionMessage,
9
+ ChatCompletionMessageParam,
10
+ )
11
+ from openai.types.responses import (
12
+ ResponseReasoningItem,
13
+ ResponseReasoningSummaryTextDeltaEvent,
14
+ ResponseTextDeltaEvent,
15
+ )
16
+
17
+ from fast_agent.constants import REASONING
18
+ from fast_agent.core.logging.logger import get_logger
19
+ from fast_agent.event_progress import ProgressAction
20
+ from fast_agent.llm.fastagent_llm import FastAgentLLM
21
+ from fast_agent.llm.provider_types import Provider
22
+ from fast_agent.llm.request_params import RequestParams
23
+ from fast_agent.mcp.prompt_message_extended import PromptMessageExtended
24
+ from fast_agent.types.llm_stop_reason import LlmStopReason
25
+
26
+ _logger = get_logger(__name__)
27
+
28
+ DEFAULT_RESPONSES_MODEL = "gpt-5-mini"
29
+ DEFAULT_REASONING_EFFORT = "medium"
30
+
31
+
32
+ # model selection
33
+ # system prompt
34
+ # usage info
35
+ # reasoning/thinking display and summary
36
+ # encrypted tokens
37
+
38
+
39
+ class ResponsesLLM(FastAgentLLM[ChatCompletionMessageParam, ChatCompletionMessage]):
40
+ """LLM implementation for OpenAI's Responses models."""
41
+
42
+ # OpenAI-specific parameter exclusions
43
+
44
+ def __init__(self, provider=Provider.RESPONSES, *args, **kwargs):
45
+ super().__init__(*args, provider=provider, **kwargs)
46
+
47
+ async def _responses_client(self) -> AsyncOpenAI:
48
+ return AsyncOpenAI(api_key=self._api_key())
49
+
50
+ async def _apply_prompt_provider_specific(
51
+ self,
52
+ multipart_messages: List[PromptMessageExtended],
53
+ request_params: RequestParams | None = None,
54
+ tools: List[Tool] | None = None,
55
+ is_template: bool = False,
56
+ ) -> PromptMessageExtended:
57
+ responses_client = await self._responses_client()
58
+
59
+ async with responses_client.responses.stream(
60
+ model="gpt-5-mini",
61
+ instructions="You are a helpful assistant.",
62
+ input=multipart_messages[-1].all_text(),
63
+ reasoning={"summary": "auto", "effort": DEFAULT_REASONING_EFFORT},
64
+ ) as stream:
65
+ reasoning_chars: int = 0
66
+ text_chars: int = 0
67
+
68
+ async for event in stream:
69
+ if isinstance(event, ResponseReasoningSummaryTextDeltaEvent):
70
+ reasoning_chars += len(event.delta)
71
+ await self._emit_streaming_progress(
72
+ model="gpt-5-mini (thinking)",
73
+ new_total=reasoning_chars,
74
+ type=ProgressAction.THINKING,
75
+ )
76
+ if isinstance(event, ResponseTextDeltaEvent):
77
+ # Notify stream listeners with the delta text
78
+ self._notify_stream_listeners(event.delta)
79
+ text_chars += len(event.delta)
80
+ await self._emit_streaming_progress(
81
+ model="gpt-5-mini",
82
+ new_total=text_chars,
83
+ )
84
+
85
+ final_response = await stream.get_final_response()
86
+ reasoning_content: List[ContentBlock] = []
87
+ for output_item in final_response.output:
88
+ if isinstance(output_item, ResponseReasoningItem):
89
+ summary_text = "\n".join(part.text for part in output_item.summary if part.text)
90
+ # reasoning text is not supplied by openai - leaving for future use with other providers
91
+ reasoning_text = "".join(
92
+ chunk.text
93
+ for chunk in (output_item.content or [])
94
+ if chunk.type == "reasoning_text"
95
+ )
96
+ if summary_text.strip():
97
+ reasoning_content.append(TextContent(type="text", text=summary_text.strip()))
98
+ if reasoning_text.strip():
99
+ reasoning_content.append(
100
+ TextContent(type="text", text=reasoning_text.strip())
101
+ )
102
+ channels = {REASONING: reasoning_content} if reasoning_content else None
103
+
104
+ return PromptMessageExtended(
105
+ role="assistant",
106
+ channels=channels,
107
+ content=[TextContent(type="text", text=final_response.output_text)],
108
+ stop_reason=LlmStopReason.END_TURN,
109
+ )
110
+
111
+ async def _emit_streaming_progress(
112
+ self,
113
+ model: str,
114
+ new_total: int,
115
+ type: ProgressAction = ProgressAction.STREAMING,
116
+ ) -> None:
117
+ """Emit a streaming progress event.
118
+
119
+ Args:
120
+ model: The model being used.
121
+ new_total: The new total token count.
122
+ """
123
+ token_str = str(new_total).rjust(5)
124
+
125
+ # Emit progress event
126
+ data = {
127
+ "progress_action": type,
128
+ "model": model,
129
+ "agent_name": self.name,
130
+ "chat_turn": self.chat_turn(),
131
+ "details": token_str.strip(), # Token count goes in details for STREAMING action
132
+ }
133
+ self.logger.info("Streaming progress", data=data)
@@ -10,6 +10,8 @@ default_instruction = """You are a helpful AI Agent.
10
10
 
11
11
  {{serverInstructions}}
12
12
 
13
+ {{agentSkills}}
14
+
13
15
  The current date is {{currentDate}}."""
14
16
 
15
17
 
@@ -20,6 +20,12 @@ mcp_timeline:
20
20
  steps: 20 # number of timeline buckets to render
21
21
  step_seconds: 15 # seconds per bucket (accepts values like "45s", "2m")
22
22
 
23
+ #shell_execution:
24
+ # length of time before terminating subprocess
25
+ # timeout_seconds: 20
26
+ # warning interval if no output seen
27
+ # warning_seconds: 5
28
+
23
29
  # Logging and Console Configuration:
24
30
  logger:
25
31
  # level: "debug" | "info" | "warning" | "error"
@@ -0,0 +1,9 @@
1
+ """Skill discovery utilities."""
2
+
3
+ from .registry import SkillManifest, SkillRegistry, format_skills_for_prompt
4
+
5
+ __all__ = [
6
+ "SkillManifest",
7
+ "SkillRegistry",
8
+ "format_skills_for_prompt",
9
+ ]