gobby 0.2.5__py3-none-any.whl → 0.2.6__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 (148) hide show
  1. gobby/adapters/claude_code.py +13 -4
  2. gobby/adapters/codex.py +43 -3
  3. gobby/agents/runner.py +8 -0
  4. gobby/cli/__init__.py +6 -0
  5. gobby/cli/clones.py +419 -0
  6. gobby/cli/conductor.py +266 -0
  7. gobby/cli/installers/antigravity.py +3 -9
  8. gobby/cli/installers/claude.py +9 -9
  9. gobby/cli/installers/codex.py +2 -8
  10. gobby/cli/installers/gemini.py +2 -8
  11. gobby/cli/installers/shared.py +71 -8
  12. gobby/cli/skills.py +858 -0
  13. gobby/cli/tasks/ai.py +0 -440
  14. gobby/cli/tasks/crud.py +44 -6
  15. gobby/cli/tasks/main.py +0 -4
  16. gobby/cli/tui.py +2 -2
  17. gobby/cli/utils.py +3 -3
  18. gobby/clones/__init__.py +13 -0
  19. gobby/clones/git.py +547 -0
  20. gobby/conductor/__init__.py +16 -0
  21. gobby/conductor/alerts.py +135 -0
  22. gobby/conductor/loop.py +164 -0
  23. gobby/conductor/monitors/__init__.py +11 -0
  24. gobby/conductor/monitors/agents.py +116 -0
  25. gobby/conductor/monitors/tasks.py +155 -0
  26. gobby/conductor/pricing.py +234 -0
  27. gobby/conductor/token_tracker.py +160 -0
  28. gobby/config/app.py +63 -1
  29. gobby/config/search.py +110 -0
  30. gobby/config/servers.py +1 -1
  31. gobby/config/skills.py +43 -0
  32. gobby/config/tasks.py +6 -14
  33. gobby/hooks/event_handlers.py +145 -2
  34. gobby/hooks/hook_manager.py +48 -2
  35. gobby/hooks/skill_manager.py +130 -0
  36. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  37. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  38. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  39. gobby/llm/claude.py +22 -34
  40. gobby/llm/claude_executor.py +46 -256
  41. gobby/llm/codex_executor.py +59 -291
  42. gobby/llm/executor.py +21 -0
  43. gobby/llm/gemini.py +134 -110
  44. gobby/llm/litellm_executor.py +143 -6
  45. gobby/llm/resolver.py +95 -33
  46. gobby/mcp_proxy/instructions.py +54 -0
  47. gobby/mcp_proxy/models.py +15 -0
  48. gobby/mcp_proxy/registries.py +68 -5
  49. gobby/mcp_proxy/server.py +33 -3
  50. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  51. gobby/mcp_proxy/stdio.py +2 -1
  52. gobby/mcp_proxy/tools/__init__.py +0 -2
  53. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  54. gobby/mcp_proxy/tools/clones.py +903 -0
  55. gobby/mcp_proxy/tools/memory.py +1 -24
  56. gobby/mcp_proxy/tools/metrics.py +65 -1
  57. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  58. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  59. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  60. gobby/mcp_proxy/tools/session_messages.py +1 -2
  61. gobby/mcp_proxy/tools/skills/__init__.py +631 -0
  62. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  63. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  64. gobby/mcp_proxy/tools/task_sync.py +1 -1
  65. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  66. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  67. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  68. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  69. gobby/mcp_proxy/tools/tasks/_lifecycle.py +60 -29
  70. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  71. gobby/mcp_proxy/tools/workflows.py +1 -1
  72. gobby/mcp_proxy/tools/worktrees.py +5 -0
  73. gobby/memory/backends/__init__.py +6 -1
  74. gobby/memory/backends/mem0.py +6 -1
  75. gobby/memory/extractor.py +477 -0
  76. gobby/memory/manager.py +11 -2
  77. gobby/prompts/defaults/handoff/compact.md +63 -0
  78. gobby/prompts/defaults/handoff/session_end.md +57 -0
  79. gobby/prompts/defaults/memory/extract.md +61 -0
  80. gobby/runner.py +37 -16
  81. gobby/search/__init__.py +48 -6
  82. gobby/search/backends/__init__.py +159 -0
  83. gobby/search/backends/embedding.py +225 -0
  84. gobby/search/embeddings.py +238 -0
  85. gobby/search/models.py +148 -0
  86. gobby/search/unified.py +496 -0
  87. gobby/servers/http.py +23 -8
  88. gobby/servers/routes/admin.py +280 -0
  89. gobby/servers/routes/mcp/tools.py +241 -52
  90. gobby/servers/websocket.py +2 -2
  91. gobby/sessions/analyzer.py +2 -0
  92. gobby/sessions/transcripts/base.py +1 -0
  93. gobby/sessions/transcripts/claude.py +64 -5
  94. gobby/skills/__init__.py +91 -0
  95. gobby/skills/loader.py +685 -0
  96. gobby/skills/manager.py +384 -0
  97. gobby/skills/parser.py +258 -0
  98. gobby/skills/search.py +463 -0
  99. gobby/skills/sync.py +119 -0
  100. gobby/skills/updater.py +385 -0
  101. gobby/skills/validator.py +368 -0
  102. gobby/storage/clones.py +378 -0
  103. gobby/storage/database.py +1 -1
  104. gobby/storage/memories.py +43 -13
  105. gobby/storage/migrations.py +180 -6
  106. gobby/storage/sessions.py +73 -0
  107. gobby/storage/skills.py +749 -0
  108. gobby/storage/tasks/_crud.py +4 -4
  109. gobby/storage/tasks/_lifecycle.py +41 -6
  110. gobby/storage/tasks/_manager.py +14 -5
  111. gobby/storage/tasks/_models.py +8 -3
  112. gobby/sync/memories.py +39 -4
  113. gobby/sync/tasks.py +83 -6
  114. gobby/tasks/__init__.py +1 -2
  115. gobby/tasks/validation.py +24 -15
  116. gobby/tui/api_client.py +4 -7
  117. gobby/tui/app.py +5 -3
  118. gobby/tui/screens/orchestrator.py +1 -2
  119. gobby/tui/screens/tasks.py +2 -4
  120. gobby/tui/ws_client.py +1 -1
  121. gobby/utils/daemon_client.py +2 -2
  122. gobby/workflows/actions.py +84 -2
  123. gobby/workflows/context_actions.py +43 -0
  124. gobby/workflows/detection_helpers.py +115 -31
  125. gobby/workflows/engine.py +13 -2
  126. gobby/workflows/lifecycle_evaluator.py +29 -1
  127. gobby/workflows/loader.py +19 -6
  128. gobby/workflows/memory_actions.py +74 -0
  129. gobby/workflows/summary_actions.py +17 -0
  130. gobby/workflows/task_enforcement_actions.py +448 -6
  131. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/METADATA +82 -21
  132. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/RECORD +136 -107
  133. gobby/install/codex/prompts/forget.md +0 -7
  134. gobby/install/codex/prompts/memories.md +0 -7
  135. gobby/install/codex/prompts/recall.md +0 -7
  136. gobby/install/codex/prompts/remember.md +0 -13
  137. gobby/llm/gemini_executor.py +0 -339
  138. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  139. gobby/tasks/context.py +0 -747
  140. gobby/tasks/criteria.py +0 -342
  141. gobby/tasks/expansion.py +0 -626
  142. gobby/tasks/prompts/expand.py +0 -327
  143. gobby/tasks/research.py +0 -421
  144. gobby/tasks/tdd.py +0 -352
  145. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/WHEEL +0 -0
  146. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/entry_points.txt +0 -0
  147. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/licenses/LICENSE.md +0 -0
  148. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/top_level.txt +0 -0
@@ -1,30 +1,19 @@
1
1
  """
2
- Codex (OpenAI) implementation of AgentExecutor.
2
+ Codex (OpenAI) implementation of AgentExecutor for CLI/subscription mode only.
3
3
 
4
- Supports two authentication modes with different capabilities:
4
+ This executor spawns `codex exec --json` CLI and parses JSONL events.
5
+ It uses Codex's built-in tools (bash, file operations, etc.) - NO custom tool
6
+ injection is supported.
5
7
 
6
- 1. api_key mode (OPENAI_API_KEY):
7
- - Uses OpenAI API with function calling
8
- - Full tool injection support
9
- - Requires OPENAI_API_KEY environment variable
10
-
11
- 2. subscription mode (ChatGPT Plus/Pro/Team/Enterprise):
12
- - Spawns `codex exec --json` CLI and parses JSONL events
13
- - Uses Codex's built-in tools (bash, file operations, etc.)
14
- - NO custom tool injection - tools parameter is IGNORED
15
- - Good for delegating complete autonomous tasks
16
-
17
- IMPORTANT: These modes have fundamentally different capabilities.
18
- Use api_key mode if you need custom MCP tool injection.
19
- Use subscription mode for delegating complete tasks to Codex.
8
+ Note: api_key mode is now routed through LiteLLMExecutor for unified cost tracking.
9
+ Use the resolver.create_executor() function which handles routing automatically.
20
10
  """
21
11
 
22
12
  import asyncio
23
13
  import json
24
14
  import logging
25
- import os
26
15
  import shutil
27
- from typing import Any, Literal
16
+ from typing import Literal
28
17
 
29
18
  from gobby.llm.executor import (
30
19
  AgentExecutor,
@@ -36,39 +25,23 @@ from gobby.llm.executor import (
36
25
 
37
26
  logger = logging.getLogger(__name__)
38
27
 
39
- # Auth mode type
40
- CodexAuthMode = Literal["api_key", "subscription"]
28
+ # Auth mode type - subscription/cli only, api_key routes through LiteLLM
29
+ CodexAuthMode = Literal["subscription", "cli"]
41
30
 
42
31
 
43
32
  class CodexExecutor(AgentExecutor):
44
33
  """
45
- Codex (OpenAI) implementation of AgentExecutor.
46
-
47
- Supports two authentication modes with DIFFERENT CAPABILITIES:
48
-
49
- api_key mode:
50
- - Uses OpenAI API function calling (like GPT-4)
51
- - Full tool injection support via tools parameter
52
- - Requires OPENAI_API_KEY environment variable
53
- - Standard agentic loop with custom tools
54
-
55
- subscription mode:
56
- - Spawns `codex exec --json` CLI process
57
- - Parses JSONL events (thread.started, item.completed, turn.completed)
58
- - Uses Codex's built-in tools ONLY (bash, file ops, web search, etc.)
59
- - The `tools` parameter is IGNORED in this mode
60
- - Cannot inject custom MCP tools
61
- - Best for delegating complete autonomous tasks
62
-
63
- Example (api_key mode):
64
- >>> executor = CodexExecutor(auth_mode="api_key")
65
- >>> result = await executor.run(
66
- ... prompt="Create a task",
67
- ... tools=[ToolSchema(name="create_task", ...)],
68
- ... tool_handler=my_handler,
69
- ... )
34
+ Codex (OpenAI) implementation of AgentExecutor for CLI mode only.
35
+
36
+ Spawns `codex exec --json` CLI process and parses JSONL events.
37
+ Uses Codex's built-in tools ONLY (bash, file ops, web search, etc.).
38
+ The `tools` parameter is IGNORED - cannot inject custom MCP tools.
39
+ Best for delegating complete autonomous tasks.
40
+
41
+ For api_key mode with custom tool injection, use LiteLLMExecutor with
42
+ provider="codex" which routes through OpenAI API for unified cost tracking.
70
43
 
71
- Example (subscription mode):
44
+ Example:
72
45
  >>> executor = CodexExecutor(auth_mode="subscription")
73
46
  >>> result = await executor.run(
74
47
  ... prompt="Fix the bug in main.py and run the tests",
@@ -77,83 +50,49 @@ class CodexExecutor(AgentExecutor):
77
50
  ... )
78
51
  """
79
52
 
53
+ _cli_path: str
54
+
80
55
  def __init__(
81
56
  self,
82
- auth_mode: CodexAuthMode = "api_key",
83
- api_key: str | None = None,
57
+ auth_mode: CodexAuthMode = "subscription",
84
58
  default_model: str = "gpt-4o",
85
59
  ):
86
60
  """
87
- Initialize CodexExecutor.
61
+ Initialize CodexExecutor for CLI/subscription mode.
88
62
 
89
63
  Args:
90
- auth_mode: Authentication mode.
91
- - "api_key": Use OpenAI API with function calling (requires OPENAI_API_KEY)
92
- - "subscription": Use Codex CLI with ChatGPT subscription (requires `codex` in PATH)
93
- api_key: OpenAI API key (optional for api_key mode, uses OPENAI_API_KEY env var).
94
- default_model: Default model for api_key mode (default: gpt-4o).
64
+ auth_mode: Must be "subscription" or "cli". API key mode is handled by LiteLLMExecutor.
65
+ default_model: Default model (not used in CLI mode, kept for interface compatibility).
66
+
67
+ Raises:
68
+ ValueError: If auth_mode is not "subscription"/"cli" or Codex CLI not found.
95
69
  """
70
+ if auth_mode not in ("subscription", "cli"):
71
+ raise ValueError(
72
+ "CodexExecutor only supports subscription/cli mode. "
73
+ "For api_key mode with custom tools, use LiteLLMExecutor with provider='codex'."
74
+ )
75
+
96
76
  self.auth_mode = auth_mode
97
77
  self.default_model = default_model
98
78
  self.logger = logger
99
- self._client: Any = None
100
- self._cli_path: str = ""
101
-
102
- if auth_mode == "api_key":
103
- # Use provided key or fall back to environment variable
104
- key = api_key or os.environ.get("OPENAI_API_KEY")
105
- if not key:
106
- raise ValueError(
107
- "API key required for api_key mode. "
108
- "Provide api_key parameter or set OPENAI_API_KEY env var."
109
- )
110
- try:
111
- from openai import AsyncOpenAI
112
-
113
- self._client = AsyncOpenAI(api_key=key)
114
- self.logger.debug("CodexExecutor initialized with API key")
115
- except ImportError as e:
116
- raise ImportError(
117
- "openai package not found. Please install with `pip install openai`."
118
- ) from e
119
-
120
- elif auth_mode == "subscription":
121
- # Verify Codex CLI is available
122
- cli_path = shutil.which("codex")
123
- if not cli_path:
124
- raise ValueError(
125
- "Codex CLI not found in PATH. "
126
- "Install Codex CLI and run `codex login` for subscription mode."
127
- )
128
- self._cli_path = cli_path
129
- self.logger.debug(f"CodexExecutor initialized with CLI at {cli_path}")
130
-
131
- else:
132
- raise ValueError(f"Unknown auth_mode: {auth_mode}")
79
+ self._cli_path = ""
80
+
81
+ # Verify Codex CLI is available
82
+ cli_path = shutil.which("codex")
83
+ if not cli_path:
84
+ raise ValueError(
85
+ "Codex CLI not found in PATH. "
86
+ "Install Codex CLI and run `codex login` for subscription mode."
87
+ )
88
+ self._cli_path = cli_path
89
+ self.logger.debug(f"CodexExecutor initialized with CLI at {cli_path}")
133
90
 
134
91
  @property
135
92
  def provider_name(self) -> str:
136
93
  """Return the provider name."""
137
94
  return "codex"
138
95
 
139
- def _convert_tools_to_openai_format(self, tools: list[ToolSchema]) -> list[dict[str, Any]]:
140
- """Convert ToolSchema list to OpenAI function calling format."""
141
- openai_tools = []
142
- for tool in tools:
143
- # Ensure input_schema has "type": "object"
144
- params = {"type": "object", **tool.input_schema}
145
- openai_tools.append(
146
- {
147
- "type": "function",
148
- "function": {
149
- "name": tool.name,
150
- "description": tool.description,
151
- "parameters": params,
152
- },
153
- }
154
- )
155
- return openai_tools
156
-
157
96
  async def run(
158
97
  self,
159
98
  prompt: str,
@@ -165,200 +104,29 @@ class CodexExecutor(AgentExecutor):
165
104
  timeout: float = 120.0,
166
105
  ) -> AgentResult:
167
106
  """
168
- Execute an agentic loop.
107
+ Execute Codex CLI and parse JSONL events.
108
+
109
+ Note: The tools and tool_handler parameters are IGNORED in CLI mode.
110
+ Codex uses its own built-in tools (bash, file operations, etc.).
169
111
 
170
- For api_key mode: Uses OpenAI function calling with custom tools.
171
- For subscription mode: Spawns Codex CLI (tools parameter is IGNORED).
112
+ For custom tool injection, use LiteLLMExecutor with provider="codex".
172
113
 
173
114
  Args:
174
115
  prompt: The user prompt to process.
175
- tools: List of available tools (IGNORED in subscription mode).
176
- tool_handler: Callback for tool calls (NOT CALLED in subscription mode).
177
- system_prompt: Optional system prompt (api_key mode only).
178
- model: Optional model override (api_key mode only).
179
- max_turns: Maximum turns before stopping (api_key mode only).
116
+ tools: IGNORED - Codex uses its own tools.
117
+ tool_handler: IGNORED - not called in CLI mode.
118
+ system_prompt: IGNORED in CLI mode.
119
+ model: IGNORED in CLI mode.
120
+ max_turns: IGNORED in CLI mode.
180
121
  timeout: Maximum execution time in seconds.
181
122
 
182
123
  Returns:
183
124
  AgentResult with output, status, and tool call records.
184
125
  """
185
- if self.auth_mode == "api_key":
186
- return await self._run_with_api(
187
- prompt=prompt,
188
- tools=tools,
189
- tool_handler=tool_handler,
190
- system_prompt=system_prompt,
191
- model=model or self.default_model,
192
- max_turns=max_turns,
193
- timeout=timeout,
194
- )
195
- else:
196
- return await self._run_with_cli(
197
- prompt=prompt,
198
- timeout=timeout,
199
- )
200
-
201
- async def _run_with_api(
202
- self,
203
- prompt: str,
204
- tools: list[ToolSchema],
205
- tool_handler: ToolHandler,
206
- system_prompt: str | None,
207
- model: str,
208
- max_turns: int,
209
- timeout: float,
210
- ) -> AgentResult:
211
- """Run using OpenAI API with function calling."""
212
- if self._client is None:
213
- return AgentResult(
214
- output="",
215
- status="error",
216
- error="OpenAI client not initialized",
217
- turns_used=0,
218
- )
219
-
220
- tool_calls_list: list[ToolCallRecord] = []
221
- openai_tools = self._convert_tools_to_openai_format(tools)
222
-
223
- # Build initial messages
224
- messages: list[dict[str, Any]] = []
225
- if system_prompt:
226
- messages.append({"role": "system", "content": system_prompt})
227
- messages.append({"role": "user", "content": prompt})
228
-
229
- # Track turns in outer scope so timeout handler can access the count
230
- turns_counter = [0]
231
-
232
- async def _run_loop() -> AgentResult:
233
- nonlocal messages
234
- turns_used = 0
235
- final_output = ""
236
- client = self._client
237
- if client is None:
238
- raise RuntimeError("CodexExecutor client not initialized")
239
-
240
- while turns_used < max_turns:
241
- turns_used += 1
242
- turns_counter[0] = turns_used
243
-
244
- # Call OpenAI
245
- try:
246
- response = await client.chat.completions.create(
247
- model=model,
248
- messages=messages,
249
- tools=openai_tools if openai_tools else None,
250
- max_tokens=8192,
251
- )
252
- except Exception as e:
253
- self.logger.error(f"OpenAI API error: {e}")
254
- return AgentResult(
255
- output="",
256
- status="error",
257
- tool_calls=tool_calls_list,
258
- error=f"OpenAI API error: {e}",
259
- turns_used=turns_used,
260
- )
261
-
262
- # Get the assistant's message
263
- choice = response.choices[0]
264
- message = choice.message
265
-
266
- # Extract text content
267
- if message.content:
268
- final_output = message.content
269
-
270
- # Add assistant message to history
271
- messages.append(message.model_dump())
272
-
273
- # Check if there are tool calls
274
- if not message.tool_calls:
275
- # No tool calls - we're done
276
- return AgentResult(
277
- output=final_output,
278
- status="success",
279
- tool_calls=tool_calls_list,
280
- turns_used=turns_used,
281
- )
282
-
283
- # Handle tool calls
284
- for tool_call in message.tool_calls:
285
- tool_name = tool_call.function.name
286
- try:
287
- arguments = json.loads(tool_call.function.arguments)
288
- except json.JSONDecodeError as e:
289
- self.logger.warning(
290
- f"Failed to parse tool call arguments for '{tool_name}' "
291
- f"(id={getattr(tool_call, 'id', 'unknown')}): {e}. "
292
- f"Arguments: {tool_call.function.arguments!r}"
293
- )
294
- arguments = {}
295
-
296
- # Record the tool call
297
- record = ToolCallRecord(
298
- tool_name=tool_name,
299
- arguments=arguments,
300
- )
301
- tool_calls_list.append(record)
302
-
303
- # Execute via handler
304
- try:
305
- result = await tool_handler(tool_name, arguments)
306
- record.result = result
307
-
308
- # Format result for OpenAI
309
- if result.success:
310
- content = json.dumps(result.result) if result.result else "Success"
311
- else:
312
- content = f"Error: {result.error}"
313
-
314
- except Exception as e:
315
- self.logger.error(f"Tool handler error for {tool_name}: {e}")
316
- from gobby.llm.executor import ToolResult as TR
317
-
318
- record.result = TR(
319
- tool_name=tool_name,
320
- success=False,
321
- error=str(e),
322
- )
323
- content = f"Error: {e}"
324
-
325
- # Add tool result to messages
326
- messages.append(
327
- {
328
- "role": "tool",
329
- "tool_call_id": tool_call.id,
330
- "content": content,
331
- }
332
- )
333
-
334
- # Check finish reason
335
- if choice.finish_reason == "stop":
336
- return AgentResult(
337
- output=final_output,
338
- status="success",
339
- tool_calls=tool_calls_list,
340
- turns_used=turns_used,
341
- )
342
-
343
- # Max turns reached
344
- return AgentResult(
345
- output=final_output,
346
- status="partial",
347
- tool_calls=tool_calls_list,
348
- turns_used=turns_used,
349
- )
350
-
351
- # Run with timeout
352
- try:
353
- return await asyncio.wait_for(_run_loop(), timeout=timeout)
354
- except TimeoutError:
355
- return AgentResult(
356
- output="",
357
- status="timeout",
358
- tool_calls=tool_calls_list,
359
- error=f"Execution timed out after {timeout}s",
360
- turns_used=turns_counter[0],
361
- )
126
+ return await self._run_with_cli(
127
+ prompt=prompt,
128
+ timeout=timeout,
129
+ )
362
130
 
363
131
  async def _run_with_cli(
364
132
  self,
gobby/llm/executor.py CHANGED
@@ -59,6 +59,23 @@ class ToolCallRecord:
59
59
  """Result from the tool execution."""
60
60
 
61
61
 
62
+ @dataclass
63
+ class CostInfo:
64
+ """Cost information from an LLM call."""
65
+
66
+ prompt_tokens: int = 0
67
+ """Number of tokens in the prompt."""
68
+
69
+ completion_tokens: int = 0
70
+ """Number of tokens in the completion."""
71
+
72
+ total_cost: float = 0.0
73
+ """Total cost in USD for this call."""
74
+
75
+ model: str = ""
76
+ """Model used for this call (LiteLLM format with prefix)."""
77
+
78
+
62
79
  @dataclass
63
80
  class AgentResult:
64
81
  """Result from running an agent to completion."""
@@ -93,6 +110,9 @@ class AgentResult:
93
110
  child_session_id: str | None = None
94
111
  """ID of the child session created for this agent (set by AgentRunner)."""
95
112
 
113
+ cost_info: CostInfo | None = None
114
+ """Cost tracking information (populated by LiteLLM executor)."""
115
+
96
116
 
97
117
  # Type alias for the tool handler callback
98
118
  ToolHandler = Callable[[str, dict[str, Any]], Awaitable[ToolResult]]
@@ -310,6 +330,7 @@ class AgentExecutor(ABC):
310
330
  if completion_result is not None:
311
331
  completion_result.tool_calls = result.tool_calls
312
332
  completion_result.turns_used = result.turns_used
333
+ completion_result.cost_info = result.cost_info
313
334
  return completion_result
314
335
 
315
336
  # Otherwise, return the raw result (might be timeout or natural end)