agent-runtime-core 0.8.0__py3-none-any.whl → 0.9.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.
@@ -34,7 +34,7 @@ Example usage:
34
34
  return RunResult(final_output={"message": "Hello!"})
35
35
  """
36
36
 
37
- __version__ = "0.8.0"
37
+ __version__ = "0.9.0"
38
38
 
39
39
  # Core interfaces
40
40
  from agent_runtime_core.interfaces import (
@@ -63,6 +63,7 @@ from agent_runtime_core.tool_calling_agent import ToolCallingAgent
63
63
  from agent_runtime_core.agentic_loop import (
64
64
  run_agentic_loop,
65
65
  AgenticLoopResult,
66
+ UsageStats,
66
67
  )
67
68
 
68
69
  # Configuration
@@ -187,17 +188,49 @@ from agent_runtime_core.tools import (
187
188
  schemas_to_openai_format,
188
189
  )
189
190
 
190
- # Multi-agent support (agent-as-tool pattern)
191
+ # Multi-agent support (agent-as-tool pattern, system context)
191
192
  from agent_runtime_core.multi_agent import (
193
+ # System context for shared knowledge
194
+ SystemContext,
195
+ SharedKnowledge,
196
+ SharedMemoryConfig,
197
+ InjectMode,
198
+ # Agent-as-tool pattern
192
199
  AgentTool,
193
200
  AgentInvocationResult,
194
201
  InvocationMode,
195
202
  ContextMode,
196
203
  SubAgentContext,
197
204
  invoke_agent,
205
+ invoke_agent_with_fallback,
198
206
  create_agent_tool_handler,
199
207
  register_agent_tools,
200
208
  build_sub_agent_messages,
209
+ # Structured Handback Protocol
210
+ HandbackStatus,
211
+ HandbackResult,
212
+ Learning,
213
+ # Stuck/Loop Detection
214
+ StuckCondition,
215
+ StuckDetectionResult,
216
+ StuckDetector,
217
+ # Journey Mode
218
+ JourneyState,
219
+ JourneyEndReason,
220
+ JourneyEndResult,
221
+ JourneyManager,
222
+ JOURNEY_STATE_KEY,
223
+ # Fallback Routing
224
+ FallbackConfig,
225
+ )
226
+
227
+ # Privacy and user isolation
228
+ from agent_runtime_core.privacy import (
229
+ PrivacyConfig,
230
+ UserContext,
231
+ MemoryScope,
232
+ DEFAULT_PRIVACY_CONFIG,
233
+ ANONYMOUS_USER,
201
234
  )
202
235
 
203
236
  # Cross-conversation memory
@@ -230,6 +263,7 @@ __all__ = [
230
263
  "ToolCallingAgent",
231
264
  "run_agentic_loop",
232
265
  "AgenticLoopResult",
266
+ "UsageStats",
233
267
  # Configuration
234
268
  "RuntimeConfig",
235
269
  "configure",
@@ -306,14 +340,42 @@ __all__ = [
306
340
  "ToolSchemaBuilder",
307
341
  "ToolParameter",
308
342
  "schemas_to_openai_format",
309
- # Multi-agent support
343
+ # Multi-agent support - System context
344
+ "SystemContext",
345
+ "SharedKnowledge",
346
+ "SharedMemoryConfig",
347
+ "InjectMode",
348
+ # Multi-agent support - Agent-as-tool
310
349
  "AgentTool",
311
350
  "AgentInvocationResult",
312
351
  "InvocationMode",
313
352
  "ContextMode",
314
353
  "SubAgentContext",
315
354
  "invoke_agent",
355
+ "invoke_agent_with_fallback",
316
356
  "create_agent_tool_handler",
317
357
  "register_agent_tools",
318
358
  "build_sub_agent_messages",
359
+ # Multi-agent support - Structured Handback Protocol
360
+ "HandbackStatus",
361
+ "HandbackResult",
362
+ "Learning",
363
+ # Multi-agent support - Stuck/Loop Detection
364
+ "StuckCondition",
365
+ "StuckDetectionResult",
366
+ "StuckDetector",
367
+ # Multi-agent support - Journey Mode
368
+ "JourneyState",
369
+ "JourneyEndReason",
370
+ "JourneyEndResult",
371
+ "JourneyManager",
372
+ "JOURNEY_STATE_KEY",
373
+ # Multi-agent support - Fallback Routing
374
+ "FallbackConfig",
375
+ # Privacy and user isolation
376
+ "PrivacyConfig",
377
+ "UserContext",
378
+ "MemoryScope",
379
+ "DEFAULT_PRIVACY_CONFIG",
380
+ "ANONYMOUS_USER",
319
381
  ]
@@ -12,7 +12,7 @@ This can be used by any agent implementation without requiring inheritance.
12
12
 
13
13
  import json
14
14
  import logging
15
- from dataclasses import dataclass
15
+ from dataclasses import dataclass, field
16
16
  from typing import Any, Callable, Optional, Awaitable, Union
17
17
 
18
18
  from agent_runtime_core.interfaces import (
@@ -21,9 +21,107 @@ from agent_runtime_core.interfaces import (
21
21
  LLMClient,
22
22
  LLMResponse,
23
23
  )
24
+ from agent_runtime_core.config import get_config
24
25
 
25
26
  logger = logging.getLogger(__name__)
26
27
 
28
+
29
+ # =============================================================================
30
+ # Cost Estimation Configuration
31
+ # =============================================================================
32
+
33
+ # Pricing per 1M tokens (input/output) - updated Jan 2026
34
+ # These are approximate and should be updated as pricing changes
35
+ MODEL_PRICING = {
36
+ # OpenAI
37
+ "gpt-4o": {"input": 2.50, "output": 10.00},
38
+ "gpt-4o-mini": {"input": 0.15, "output": 0.60},
39
+ "gpt-4-turbo": {"input": 10.00, "output": 30.00},
40
+ "gpt-4": {"input": 30.00, "output": 60.00},
41
+ "gpt-3.5-turbo": {"input": 0.50, "output": 1.50},
42
+ "o1": {"input": 15.00, "output": 60.00},
43
+ "o1-mini": {"input": 3.00, "output": 12.00},
44
+ "o1-preview": {"input": 15.00, "output": 60.00},
45
+ "o3-mini": {"input": 1.10, "output": 4.40},
46
+ # Anthropic
47
+ "claude-3-5-sonnet-20241022": {"input": 3.00, "output": 15.00},
48
+ "claude-3-5-haiku-20241022": {"input": 0.80, "output": 4.00},
49
+ "claude-3-opus-20240229": {"input": 15.00, "output": 75.00},
50
+ "claude-3-sonnet-20240229": {"input": 3.00, "output": 15.00},
51
+ "claude-3-haiku-20240307": {"input": 0.25, "output": 1.25},
52
+ # Google
53
+ "gemini-1.5-pro": {"input": 1.25, "output": 5.00},
54
+ "gemini-1.5-flash": {"input": 0.075, "output": 0.30},
55
+ "gemini-2.0-flash": {"input": 0.10, "output": 0.40},
56
+ # Default fallback
57
+ "default": {"input": 3.00, "output": 15.00},
58
+ }
59
+
60
+
61
+ def _get_model_pricing(model: str) -> dict:
62
+ """Get pricing for a model, with fallback to default."""
63
+ # Try exact match first
64
+ if model in MODEL_PRICING:
65
+ return MODEL_PRICING[model]
66
+ # Try prefix match (e.g., "gpt-4o-2024-08-06" -> "gpt-4o")
67
+ for key in MODEL_PRICING:
68
+ if model.startswith(key):
69
+ return MODEL_PRICING[key]
70
+ return MODEL_PRICING["default"]
71
+
72
+
73
+ def _estimate_cost(usage: dict, model: str) -> float:
74
+ """Estimate cost in USD from usage dict and model."""
75
+ pricing = _get_model_pricing(model)
76
+ prompt_tokens = usage.get("prompt_tokens", 0)
77
+ completion_tokens = usage.get("completion_tokens", 0)
78
+
79
+ input_cost = (prompt_tokens / 1_000_000) * pricing["input"]
80
+ output_cost = (completion_tokens / 1_000_000) * pricing["output"]
81
+
82
+ return input_cost + output_cost
83
+
84
+
85
+ def _format_cost(cost: float) -> str:
86
+ """Format cost for display."""
87
+ if cost < 0.01:
88
+ return f"${cost:.4f}"
89
+ return f"${cost:.3f}"
90
+
91
+
92
+ @dataclass
93
+ class UsageStats:
94
+ """Accumulated usage statistics for the agentic loop."""
95
+
96
+ total_prompt_tokens: int = 0
97
+ total_completion_tokens: int = 0
98
+ total_cost: float = 0.0
99
+ llm_calls: int = 0
100
+ tool_calls: int = 0
101
+
102
+ def add_llm_call(self, usage: dict, model: str):
103
+ """Add usage from an LLM call."""
104
+ self.llm_calls += 1
105
+ self.total_prompt_tokens += usage.get("prompt_tokens", 0)
106
+ self.total_completion_tokens += usage.get("completion_tokens", 0)
107
+ self.total_cost += _estimate_cost(usage, model)
108
+
109
+ def add_tool_call(self):
110
+ """Record a tool call."""
111
+ self.tool_calls += 1
112
+
113
+ def to_dict(self) -> dict:
114
+ """Convert to dictionary."""
115
+ return {
116
+ "total_prompt_tokens": self.total_prompt_tokens,
117
+ "total_completion_tokens": self.total_completion_tokens,
118
+ "total_tokens": self.total_prompt_tokens + self.total_completion_tokens,
119
+ "total_cost_usd": self.total_cost,
120
+ "llm_calls": self.llm_calls,
121
+ "tool_calls": self.tool_calls,
122
+ }
123
+
124
+
27
125
  # Type alias for tool executor function
28
126
  ToolExecutor = Callable[[str, dict], Awaitable[Any]]
29
127
 
@@ -31,19 +129,22 @@ ToolExecutor = Callable[[str, dict], Awaitable[Any]]
31
129
  @dataclass
32
130
  class AgenticLoopResult:
33
131
  """Result from running the agentic loop."""
34
-
132
+
35
133
  final_content: str
36
134
  """The final text response from the LLM."""
37
-
135
+
38
136
  messages: list[dict]
39
137
  """All messages including tool calls and results."""
40
-
138
+
41
139
  iterations: int
42
140
  """Number of iterations the loop ran."""
43
-
141
+
44
142
  usage: dict
45
143
  """Token usage from the final LLM call."""
46
144
 
145
+ usage_stats: Optional[UsageStats] = None
146
+ """Accumulated usage statistics across all LLM calls (if debug mode enabled)."""
147
+
47
148
 
48
149
  async def run_agentic_loop(
49
150
  llm: LLMClient,
@@ -53,19 +154,20 @@ async def run_agentic_loop(
53
154
  ctx: RunContext,
54
155
  *,
55
156
  model: Optional[str] = None,
56
- max_iterations: int = 15,
157
+ max_iterations: Optional[int] = None,
57
158
  emit_events: bool = True,
159
+ ensure_final_response: bool = False,
58
160
  **llm_kwargs,
59
161
  ) -> AgenticLoopResult:
60
162
  """
61
163
  Run the standard agentic tool-calling loop.
62
-
164
+
63
165
  This handles the common pattern of:
64
166
  1. Call LLM with available tools
65
167
  2. If LLM returns tool calls, execute them
66
168
  3. Add tool results to messages and loop back to step 1
67
169
  4. If LLM returns a text response (no tool calls), return it
68
-
170
+
69
171
  Args:
70
172
  llm: The LLM client to use for generation
71
173
  messages: Initial messages (should include system prompt)
@@ -73,19 +175,23 @@ async def run_agentic_loop(
73
175
  execute_tool: Async function that executes a tool: (name, args) -> result
74
176
  ctx: Run context for emitting events
75
177
  model: Model to use (passed to LLM client)
76
- max_iterations: Maximum loop iterations to prevent infinite loops
178
+ max_iterations: Maximum loop iterations to prevent infinite loops.
179
+ If None, uses the value from config (default: 50).
77
180
  emit_events: Whether to emit TOOL_CALL and TOOL_RESULT events
181
+ ensure_final_response: If True, ensures a summary is generated when tools
182
+ were used but the final response is empty or very short. This is useful
183
+ for agents that should always provide a summary of what was accomplished.
78
184
  **llm_kwargs: Additional kwargs passed to llm.generate()
79
-
185
+
80
186
  Returns:
81
187
  AgenticLoopResult with final content, messages, and metadata
82
-
188
+
83
189
  Example:
84
190
  async def my_tool_executor(name: str, args: dict) -> Any:
85
191
  if name == "get_weather":
86
192
  return {"temp": 72, "conditions": "sunny"}
87
193
  raise ValueError(f"Unknown tool: {name}")
88
-
194
+
89
195
  result = await run_agentic_loop(
90
196
  llm=my_llm_client,
91
197
  messages=[{"role": "system", "content": "You are helpful."}],
@@ -93,6 +199,7 @@ async def run_agentic_loop(
93
199
  execute_tool=my_tool_executor,
94
200
  ctx=ctx,
95
201
  model="gpt-4o",
202
+ ensure_final_response=True, # Guarantees a summary
96
203
  )
97
204
  """
98
205
  iteration = 0
@@ -101,10 +208,19 @@ async def run_agentic_loop(
101
208
  consecutive_errors = 0
102
209
  max_consecutive_errors = 3 # Bail out if tool keeps failing
103
210
 
104
- while iteration < max_iterations:
211
+ # Initialize usage tracking (enabled in debug mode)
212
+ config = get_config()
213
+ debug_mode = config.debug
214
+ usage_stats = UsageStats() if debug_mode else None
215
+ effective_model = model or "unknown"
216
+
217
+ # Use config default if max_iterations not specified
218
+ effective_max_iterations = max_iterations if max_iterations is not None else config.max_iterations
219
+
220
+ while iteration < effective_max_iterations:
105
221
  iteration += 1
106
- print(f"[agentic-loop] Iteration {iteration}/{max_iterations}, messages={len(messages)}", flush=True)
107
- logger.debug(f"Agentic loop iteration {iteration}/{max_iterations}")
222
+ print(f"[agentic-loop] Iteration {iteration}/{effective_max_iterations}, messages={len(messages)}", flush=True)
223
+ logger.debug(f"Agentic loop iteration {iteration}/{effective_max_iterations}")
108
224
 
109
225
  # Call LLM
110
226
  if tools:
@@ -120,8 +236,25 @@ async def run_agentic_loop(
120
236
  model=model,
121
237
  **llm_kwargs,
122
238
  )
123
-
239
+
124
240
  last_response = response
241
+
242
+ # Track usage in debug mode
243
+ if debug_mode and usage_stats:
244
+ # Get model from response if available, otherwise use effective_model
245
+ resp_model = response.model or effective_model
246
+ usage_stats.add_llm_call(response.usage, resp_model)
247
+
248
+ # Print debug info
249
+ prompt_tokens = response.usage.get("prompt_tokens", 0)
250
+ completion_tokens = response.usage.get("completion_tokens", 0)
251
+ call_cost = _estimate_cost(response.usage, resp_model)
252
+
253
+ print(f"[agentic-loop] 💰 LLM Call #{usage_stats.llm_calls}:", flush=True)
254
+ print(f"[agentic-loop] Model: {resp_model}", flush=True)
255
+ print(f"[agentic-loop] Tokens: {prompt_tokens:,} in / {completion_tokens:,} out", flush=True)
256
+ print(f"[agentic-loop] Cost: {_format_cost(call_cost)}", flush=True)
257
+ print(f"[agentic-loop] Running total: {usage_stats.total_prompt_tokens:,} in / {usage_stats.total_completion_tokens:,} out = {_format_cost(usage_stats.total_cost)}", flush=True)
125
258
 
126
259
  # Check for tool calls
127
260
  if response.tool_calls:
@@ -158,6 +291,11 @@ async def run_agentic_loop(
158
291
  logger.warning(f"Failed to parse tool args: {tool_args_str}")
159
292
  tool_args = {}
160
293
 
294
+ # Track tool call in debug mode
295
+ if debug_mode and usage_stats:
296
+ usage_stats.add_tool_call()
297
+ print(f"[agentic-loop] 🔧 Tool #{usage_stats.tool_calls}: {tool_name}", flush=True)
298
+
161
299
  # Emit tool call event
162
300
  if emit_events:
163
301
  await ctx.emit(EventType.TOOL_CALL, {
@@ -165,7 +303,7 @@ async def run_agentic_loop(
165
303
  "name": tool_name,
166
304
  "arguments": tool_args,
167
305
  })
168
-
306
+
169
307
  # Execute the tool
170
308
  try:
171
309
  result = await execute_tool(tool_name, tool_args)
@@ -209,8 +347,18 @@ async def run_agentic_loop(
209
347
  "iterations": iteration,
210
348
  })
211
349
 
212
- # Add error to messages for conversation history
213
- final_content = f"I encountered repeated errors while trying to complete this task. The last error was: {error_msg}"
350
+ # Generate a summary if ensure_final_response is enabled
351
+ if ensure_final_response:
352
+ logger.info("Generating summary after error exit because ensure_final_response=True")
353
+ print("[agentic-loop] Generating summary after error exit", flush=True)
354
+ summary = await _generate_task_summary(llm, messages, model, **llm_kwargs)
355
+ if summary:
356
+ final_content = f"{summary}\n\n---\n\n⚠️ Note: The task ended early due to repeated errors. Last error: {error_msg}"
357
+ else:
358
+ final_content = f"I encountered repeated errors while trying to complete this task. The last error was: {error_msg}"
359
+ else:
360
+ final_content = f"I encountered repeated errors while trying to complete this task. The last error was: {error_msg}"
361
+
214
362
  messages.append({
215
363
  "role": "assistant",
216
364
  "content": final_content,
@@ -222,11 +370,22 @@ async def run_agentic_loop(
222
370
  "role": "assistant",
223
371
  })
224
372
 
373
+ # Print final summary in debug mode
374
+ if debug_mode and usage_stats:
375
+ print(f"[agentic-loop] ═══════════════════════════════════════════", flush=True)
376
+ print(f"[agentic-loop] 📊 FINAL USAGE SUMMARY (error exit)", flush=True)
377
+ print(f"[agentic-loop] LLM calls: {usage_stats.llm_calls}", flush=True)
378
+ print(f"[agentic-loop] Tool calls: {usage_stats.tool_calls}", flush=True)
379
+ print(f"[agentic-loop] Total tokens: {usage_stats.total_prompt_tokens:,} in / {usage_stats.total_completion_tokens:,} out", flush=True)
380
+ print(f"[agentic-loop] Estimated cost: {_format_cost(usage_stats.total_cost)}", flush=True)
381
+ print(f"[agentic-loop] ═══════════════════════════════════════════", flush=True)
382
+
225
383
  return AgenticLoopResult(
226
384
  final_content=final_content,
227
385
  messages=messages,
228
386
  iterations=iteration,
229
387
  usage=last_response.usage if last_response else {},
388
+ usage_stats=usage_stats,
230
389
  )
231
390
 
232
391
  # Continue the loop to get next response
@@ -244,11 +403,117 @@ async def run_agentic_loop(
244
403
  })
245
404
 
246
405
  break
247
-
406
+
407
+ # Check if we need to ensure a final response (summary)
408
+ if ensure_final_response:
409
+ # Check if tools were used during this run
410
+ tools_were_used = any(
411
+ msg.get("role") == "assistant" and msg.get("tool_calls")
412
+ for msg in messages
413
+ )
414
+
415
+ # If tools were used but final response is empty or very short, generate a summary
416
+ if tools_were_used and (not final_content or len(final_content.strip()) < 50):
417
+ logger.info("Generating summary because tools were used but final response was empty/short")
418
+ print("[agentic-loop] Generating summary - tools were used but no final response", flush=True)
419
+
420
+ summary = await _generate_task_summary(llm, messages, model, **llm_kwargs)
421
+ if summary:
422
+ final_content = summary
423
+ # Emit the summary as an assistant message
424
+ if emit_events:
425
+ await ctx.emit(EventType.ASSISTANT_MESSAGE, {
426
+ "content": summary,
427
+ "role": "assistant",
428
+ })
429
+ # Add to messages
430
+ messages.append({"role": "assistant", "content": summary})
431
+
432
+ # Print final summary in debug mode
433
+ if debug_mode and usage_stats:
434
+ print(f"[agentic-loop] ═══════════════════════════════════════════", flush=True)
435
+ print(f"[agentic-loop] 📊 FINAL USAGE SUMMARY", flush=True)
436
+ print(f"[agentic-loop] Iterations: {iteration}", flush=True)
437
+ print(f"[agentic-loop] LLM calls: {usage_stats.llm_calls}", flush=True)
438
+ print(f"[agentic-loop] Tool calls: {usage_stats.tool_calls}", flush=True)
439
+ print(f"[agentic-loop] Total tokens: {usage_stats.total_prompt_tokens:,} in / {usage_stats.total_completion_tokens:,} out", flush=True)
440
+ print(f"[agentic-loop] Estimated cost: {_format_cost(usage_stats.total_cost)}", flush=True)
441
+ print(f"[agentic-loop] ═══════════════════════════════════════════", flush=True)
442
+
248
443
  return AgenticLoopResult(
249
444
  final_content=final_content,
250
445
  messages=messages,
251
446
  iterations=iteration,
252
447
  usage=last_response.usage if last_response else {},
448
+ usage_stats=usage_stats,
253
449
  )
254
450
 
451
+
452
+ async def _generate_task_summary(
453
+ llm: LLMClient,
454
+ messages: list[dict],
455
+ model: Optional[str] = None,
456
+ **llm_kwargs,
457
+ ) -> str:
458
+ """
459
+ Generate a summary of what was accomplished based on the conversation history.
460
+
461
+ This is called when ensure_final_response=True and tools were used but
462
+ no meaningful final response was provided.
463
+
464
+ Args:
465
+ llm: The LLM client to use
466
+ messages: The conversation history including tool calls and results
467
+ model: Model to use
468
+ **llm_kwargs: Additional kwargs for the LLM
469
+
470
+ Returns:
471
+ A summary string of what was accomplished
472
+ """
473
+ # Build a summary of tool calls and their results
474
+ tool_summary_parts = []
475
+ for msg in messages:
476
+ if msg.get("role") == "assistant" and msg.get("tool_calls"):
477
+ for tc in msg.get("tool_calls", []):
478
+ if isinstance(tc, dict):
479
+ name = tc.get("function", {}).get("name", "unknown")
480
+ else:
481
+ name = getattr(tc, "name", "unknown")
482
+ tool_summary_parts.append(f"- Called: {name}")
483
+ elif msg.get("role") == "tool":
484
+ content = msg.get("content", "")
485
+ # Truncate long results
486
+ if len(content) > 200:
487
+ content = content[:200] + "..."
488
+ tool_summary_parts.append(f" Result: {content}")
489
+
490
+ tool_summary = "\n".join(tool_summary_parts[-20:]) # Last 20 entries to avoid token limits
491
+
492
+ summary_prompt = f"""Based on the conversation above, provide a brief summary of what was accomplished.
493
+
494
+ Here's a summary of the tools that were called:
495
+ {tool_summary}
496
+
497
+ Please provide a clear, concise summary (2-4 sentences) of:
498
+ 1. What actions were taken
499
+ 2. What was accomplished or changed
500
+ 3. Any important results or next steps
501
+
502
+ Start your response directly with the summary - do not include phrases like "Here's a summary" or "Based on the conversation"."""
503
+
504
+ # Create a simplified message list for the summary request
505
+ summary_messages = [
506
+ {"role": "system", "content": "You are a helpful assistant that provides clear, concise summaries of completed tasks."},
507
+ {"role": "user", "content": summary_prompt},
508
+ ]
509
+
510
+ try:
511
+ response = await llm.generate(
512
+ summary_messages,
513
+ model=model,
514
+ **llm_kwargs,
515
+ )
516
+ return response.message.get("content", "")
517
+ except Exception as e:
518
+ logger.exception("Failed to generate task summary")
519
+ return f"Task completed. (Summary generation failed: {e})"
@@ -88,6 +88,12 @@ class RuntimeConfig:
88
88
  vertex_deployed_index_id: Optional[str] = None
89
89
  vertex_index_id: Optional[str] = None
90
90
 
91
+ # Debug mode - enables verbose logging, cost tracking, etc.
92
+ debug: bool = False
93
+
94
+ # Agentic loop settings
95
+ max_iterations: int = 50 # Maximum iterations for tool-calling loops
96
+
91
97
  def get_openai_api_key(self) -> Optional[str]:
92
98
  """Get OpenAI API key from config or environment."""
93
99
  return self.openai_api_key or os.environ.get("OPENAI_API_KEY")
@@ -199,11 +205,13 @@ def _apply_env_vars(config: RuntimeConfig) -> None:
199
205
  "AGENT_RUNTIME_RETRY_BACKOFF_BASE": "retry_backoff_base",
200
206
  "AGENT_RUNTIME_RETRY_BACKOFF_MAX": "retry_backoff_max",
201
207
  "AGENT_RUNTIME_MAX_HISTORY_MESSAGES": "max_history_messages",
208
+ "AGENT_RUNTIME_MAX_ITERATIONS": "max_iterations",
202
209
  }
203
210
 
204
211
  bool_fields = {
205
212
  "AGENT_RUNTIME_INCLUDE_CONVERSATION_HISTORY": "include_conversation_history",
206
213
  "AGENT_RUNTIME_AUTO_PERSIST_MESSAGES": "auto_persist_messages",
214
+ "AGENT_RUNTIME_DEBUG": "debug",
207
215
  }
208
216
 
209
217
  for env_var, attr in env_mapping.items():