cade-cli 0.3.3__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.
- cade_cli-0.3.3.dist-info/METADATA +151 -0
- cade_cli-0.3.3.dist-info/RECORD +44 -0
- cade_cli-0.3.3.dist-info/WHEEL +4 -0
- cade_cli-0.3.3.dist-info/entry_points.txt +2 -0
- cadecoder/__init__.py +1 -0
- cadecoder/ai/__init__.py +6 -0
- cadecoder/ai/prompts.py +572 -0
- cadecoder/cli/__init__.py +0 -0
- cadecoder/cli/app.py +147 -0
- cadecoder/cli/auth.py +483 -0
- cadecoder/cli/commands/__init__.py +5 -0
- cadecoder/cli/commands/auth.py +143 -0
- cadecoder/cli/commands/chat.py +264 -0
- cadecoder/cli/commands/mcp.py +477 -0
- cadecoder/cli/commands/tools.py +226 -0
- cadecoder/core/__init__.py +12 -0
- cadecoder/core/config.py +380 -0
- cadecoder/core/constants.py +281 -0
- cadecoder/core/errors.py +145 -0
- cadecoder/core/logging.py +148 -0
- cadecoder/core/types.py +235 -0
- cadecoder/core/utils.py +279 -0
- cadecoder/execution/__init__.py +46 -0
- cadecoder/execution/context_window.py +521 -0
- cadecoder/execution/orchestrator.py +562 -0
- cadecoder/execution/parallel.py +287 -0
- cadecoder/providers/__init__.py +60 -0
- cadecoder/providers/base.py +294 -0
- cadecoder/providers/openai.py +251 -0
- cadecoder/storage/__init__.py +0 -0
- cadecoder/storage/threads.py +489 -0
- cadecoder/templates/login_failed.html +21 -0
- cadecoder/templates/login_success.html +21 -0
- cadecoder/templates/styles.css +87 -0
- cadecoder/tools/__init__.py +19 -0
- cadecoder/tools/builtin.py +644 -0
- cadecoder/tools/filesystem.py +315 -0
- cadecoder/tools/git.py +221 -0
- cadecoder/tools/manager.py +1635 -0
- cadecoder/ui/__init__.py +7 -0
- cadecoder/ui/display.py +338 -0
- cadecoder/ui/input.py +145 -0
- cadecoder/ui/session.py +455 -0
- cadecoder/ui/state.py +20 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
"""Orchestrator for task execution."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from cadecoder.ai.prompts import AGENT_SYSTEM_PROMPT, get_environment_context
|
|
10
|
+
from cadecoder.core.config import get_config
|
|
11
|
+
from cadecoder.core.constants import (
|
|
12
|
+
DEFAULT_TEMPERATURE,
|
|
13
|
+
MAX_EXECUTION_ITERATIONS,
|
|
14
|
+
)
|
|
15
|
+
from cadecoder.core.logging import log
|
|
16
|
+
from cadecoder.core.types import ExecutionEventType, extract_tool_output_content
|
|
17
|
+
from cadecoder.execution.context_window import (
|
|
18
|
+
CompactionStrategy,
|
|
19
|
+
ContextWindowManager,
|
|
20
|
+
create_context_manager,
|
|
21
|
+
)
|
|
22
|
+
from cadecoder.execution.parallel import ParallelToolExecutor
|
|
23
|
+
from cadecoder.providers.base import (
|
|
24
|
+
Provider,
|
|
25
|
+
ProviderRequest,
|
|
26
|
+
provider_registry,
|
|
27
|
+
)
|
|
28
|
+
from cadecoder.tools.manager import (
|
|
29
|
+
CompositeToolManager,
|
|
30
|
+
ToolAuthorizationRequired,
|
|
31
|
+
ToolManager,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_orchestrator(
|
|
36
|
+
provider: Provider | None = None,
|
|
37
|
+
tool_manager: ToolManager | None = None,
|
|
38
|
+
default_model: str | None = None,
|
|
39
|
+
context_manager: ContextWindowManager | None = None,
|
|
40
|
+
) -> "Orchestrator":
|
|
41
|
+
"""Factory function to create an orchestrator with sensible defaults.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
provider: LLM provider (uses default if not specified)
|
|
45
|
+
tool_manager: Tool manager (creates CompositeToolManager if not specified)
|
|
46
|
+
default_model: Default model to use
|
|
47
|
+
context_manager: Context window manager (created if not specified)
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Configured Orchestrator instance
|
|
51
|
+
"""
|
|
52
|
+
if tool_manager is None:
|
|
53
|
+
tool_manager = CompositeToolManager()
|
|
54
|
+
|
|
55
|
+
return Orchestrator(
|
|
56
|
+
provider=provider,
|
|
57
|
+
tool_manager=tool_manager,
|
|
58
|
+
default_model=default_model,
|
|
59
|
+
context_manager=context_manager,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# --- Execution Models ---
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ExecutionMode:
|
|
67
|
+
"""Execution mode constants."""
|
|
68
|
+
|
|
69
|
+
STREAMING = "streaming"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ExecutionContext(BaseModel):
|
|
73
|
+
"""Context for task execution."""
|
|
74
|
+
|
|
75
|
+
task: str
|
|
76
|
+
conversation_history: list[dict[str, Any]] = Field(default_factory=list)
|
|
77
|
+
mode: str = ExecutionMode.STREAMING
|
|
78
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ExecutionEvent(BaseModel):
|
|
82
|
+
"""Event emitted during execution."""
|
|
83
|
+
|
|
84
|
+
type: str
|
|
85
|
+
content: str | None = None
|
|
86
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ExecutionResult(BaseModel):
|
|
90
|
+
"""Result of task execution."""
|
|
91
|
+
|
|
92
|
+
content: str
|
|
93
|
+
tool_calls: list[dict[str, Any]] | None = None
|
|
94
|
+
tool_results: list[dict[str, Any]] | None = None
|
|
95
|
+
mode: str = ExecutionMode.STREAMING
|
|
96
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ContinuationDecision(BaseModel):
|
|
100
|
+
"""Decision about whether to continue execution."""
|
|
101
|
+
|
|
102
|
+
should_continue: bool = False
|
|
103
|
+
reason: str = ""
|
|
104
|
+
needs_user_input: bool = False
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Orchestrator:
|
|
108
|
+
"""Orchestrator for task execution.
|
|
109
|
+
|
|
110
|
+
This orchestrator:
|
|
111
|
+
- Executes tasks in streaming mode with iterative execution
|
|
112
|
+
- Manages tool execution with parallel support
|
|
113
|
+
- Handles both execute() and stream() interfaces
|
|
114
|
+
- Manages context window with automatic compaction
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
provider: Provider | None = None,
|
|
120
|
+
tool_manager: ToolManager | None = None,
|
|
121
|
+
default_model: str | None = None,
|
|
122
|
+
context_manager: ContextWindowManager | None = None,
|
|
123
|
+
):
|
|
124
|
+
"""Initialize orchestrator.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
provider: LLM provider instance
|
|
128
|
+
tool_manager: Tool manager for executing tools
|
|
129
|
+
default_model: Default model to use
|
|
130
|
+
context_manager: Context window manager (created if not provided)
|
|
131
|
+
"""
|
|
132
|
+
self.provider = provider or provider_registry.get_default()
|
|
133
|
+
if not self.provider:
|
|
134
|
+
raise ValueError("No provider available")
|
|
135
|
+
|
|
136
|
+
self.tool_manager = tool_manager
|
|
137
|
+
self.default_model = default_model or get_config().settings.default_model
|
|
138
|
+
self._tools_cache: list[dict[str, Any]] | None = None
|
|
139
|
+
self._tools_description_cache: str | None = None
|
|
140
|
+
|
|
141
|
+
# Context window management
|
|
142
|
+
self.context_manager = context_manager or create_context_manager(model=self.default_model)
|
|
143
|
+
|
|
144
|
+
# Parallel executor
|
|
145
|
+
self._parallel_executor = None
|
|
146
|
+
if self.tool_manager:
|
|
147
|
+
self._parallel_executor = ParallelToolExecutor(self.tool_manager)
|
|
148
|
+
|
|
149
|
+
async def execute(self, context: ExecutionContext) -> ExecutionResult:
|
|
150
|
+
"""Execute a task in streaming mode."""
|
|
151
|
+
context.mode = ExecutionMode.STREAMING
|
|
152
|
+
|
|
153
|
+
log.info(f"Executing task with mode: {context.mode}")
|
|
154
|
+
|
|
155
|
+
result_content = ""
|
|
156
|
+
tool_calls = []
|
|
157
|
+
final_metadata: dict[str, Any] = {}
|
|
158
|
+
|
|
159
|
+
async for event in self.stream(context):
|
|
160
|
+
if event.type == ExecutionEventType.CONTENT:
|
|
161
|
+
result_content += event.content or ""
|
|
162
|
+
elif event.type == ExecutionEventType.TOOL_CALL:
|
|
163
|
+
tool_calls.append(event.metadata.get("tool_call", {}))
|
|
164
|
+
elif event.type == ExecutionEventType.COMPLETE:
|
|
165
|
+
final_metadata = event.metadata
|
|
166
|
+
|
|
167
|
+
return ExecutionResult(
|
|
168
|
+
content=result_content,
|
|
169
|
+
tool_calls=tool_calls if tool_calls else None,
|
|
170
|
+
tool_results=None,
|
|
171
|
+
mode=ExecutionMode.STREAMING,
|
|
172
|
+
metadata={"streamed": True, **final_metadata},
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
async def stream(self, context: ExecutionContext) -> AsyncIterator[ExecutionEvent]:
|
|
176
|
+
"""Stream execution events."""
|
|
177
|
+
context.mode = ExecutionMode.STREAMING
|
|
178
|
+
|
|
179
|
+
# Load tools
|
|
180
|
+
tools = await self._load_tools()
|
|
181
|
+
|
|
182
|
+
# Build messages
|
|
183
|
+
messages = self._build_messages(context)
|
|
184
|
+
|
|
185
|
+
# Clear tool outputs for new execution
|
|
186
|
+
self.context_manager.clear_tool_outputs()
|
|
187
|
+
|
|
188
|
+
iteration = 0
|
|
189
|
+
should_continue = True
|
|
190
|
+
|
|
191
|
+
while should_continue and iteration < MAX_EXECUTION_ITERATIONS:
|
|
192
|
+
iteration += 1
|
|
193
|
+
log.info(f"Execution iteration {iteration}")
|
|
194
|
+
|
|
195
|
+
# Check context window status before each turn
|
|
196
|
+
context_status = self.context_manager.check_context_status(messages)
|
|
197
|
+
log.debug(
|
|
198
|
+
f"Context status: {context_status['token_count']:,} tokens "
|
|
199
|
+
f"({context_status['percentage_used']}% used)"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Compact context if needed
|
|
203
|
+
if context_status["needs_compaction"]:
|
|
204
|
+
log.warning(
|
|
205
|
+
f"Context window near limit ({context_status['percentage_used']}%), "
|
|
206
|
+
"compacting..."
|
|
207
|
+
)
|
|
208
|
+
yield ExecutionEvent(
|
|
209
|
+
type=ExecutionEventType.CONTEXT_COMPACTION,
|
|
210
|
+
metadata={
|
|
211
|
+
"before_tokens": context_status["token_count"],
|
|
212
|
+
"strategy": CompactionStrategy.KEEP_RECENT.value,
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
messages, backup = self.context_manager.compact_context(
|
|
217
|
+
messages,
|
|
218
|
+
strategy=CompactionStrategy.KEEP_RECENT,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
new_status = self.context_manager.check_context_status(messages)
|
|
222
|
+
log.info(
|
|
223
|
+
f"Context compacted: {context_status['token_count']:,} -> "
|
|
224
|
+
f"{new_status['token_count']:,} tokens"
|
|
225
|
+
)
|
|
226
|
+
yield ExecutionEvent(
|
|
227
|
+
type=ExecutionEventType.CONTEXT_COMPACTION,
|
|
228
|
+
metadata={
|
|
229
|
+
"after_tokens": new_status["token_count"],
|
|
230
|
+
"backup_timestamp": backup.timestamp.isoformat(),
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Build provider request
|
|
235
|
+
request = ProviderRequest(
|
|
236
|
+
messages=messages,
|
|
237
|
+
model=self.default_model,
|
|
238
|
+
tools=tools if tools else None,
|
|
239
|
+
temperature=DEFAULT_TEMPERATURE,
|
|
240
|
+
max_tokens=None,
|
|
241
|
+
tool_choice="auto",
|
|
242
|
+
system_prompt=None,
|
|
243
|
+
stream=True,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Stream from provider
|
|
247
|
+
accumulated_content = ""
|
|
248
|
+
tool_calls: list[dict[str, Any]] = []
|
|
249
|
+
|
|
250
|
+
async for stream_event in self.provider.stream(request):
|
|
251
|
+
if stream_event.content:
|
|
252
|
+
accumulated_content += stream_event.content
|
|
253
|
+
yield ExecutionEvent(
|
|
254
|
+
type=ExecutionEventType.CONTENT,
|
|
255
|
+
content=stream_event.content,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Handle tool call events from provider
|
|
259
|
+
if stream_event.type == ExecutionEventType.TOOL_CALL.value:
|
|
260
|
+
tool_call = stream_event.metadata.get("tool_call")
|
|
261
|
+
if tool_call:
|
|
262
|
+
tool_calls.append(tool_call)
|
|
263
|
+
yield ExecutionEvent(
|
|
264
|
+
type=ExecutionEventType.TOOL_CALL,
|
|
265
|
+
metadata={"tool_call": tool_call},
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# If we have tool calls, execute them
|
|
269
|
+
if tool_calls:
|
|
270
|
+
yield ExecutionEvent(
|
|
271
|
+
type=ExecutionEventType.TOOL_EXECUTION_START,
|
|
272
|
+
metadata={"tool_count": len(tool_calls)},
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
tool_results = await self._execute_tool_calls(tool_calls)
|
|
276
|
+
|
|
277
|
+
for tc, result in zip(tool_calls, tool_results):
|
|
278
|
+
tool_name = result.get("name", "unknown")
|
|
279
|
+
tool_content = result.get("content", "")
|
|
280
|
+
tool_call_id = tc.get("id", "")
|
|
281
|
+
|
|
282
|
+
# Track tool output in context manager
|
|
283
|
+
self.context_manager.add_tool_output(
|
|
284
|
+
tool_name=tool_name,
|
|
285
|
+
output=tool_content,
|
|
286
|
+
tool_call_id=tool_call_id,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
yield ExecutionEvent(
|
|
290
|
+
type=ExecutionEventType.TOOL_RESULT,
|
|
291
|
+
content=tool_content,
|
|
292
|
+
metadata={
|
|
293
|
+
"tool_name": tool_name,
|
|
294
|
+
"tool_call_id": tool_call_id,
|
|
295
|
+
},
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Add assistant message with tool calls
|
|
299
|
+
messages.append(
|
|
300
|
+
{
|
|
301
|
+
"role": "assistant",
|
|
302
|
+
"content": accumulated_content if accumulated_content else None,
|
|
303
|
+
"tool_calls": tool_calls,
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Add tool results as messages
|
|
308
|
+
for tc, result in zip(tool_calls, tool_results):
|
|
309
|
+
messages.append(
|
|
310
|
+
{
|
|
311
|
+
"role": "tool",
|
|
312
|
+
"tool_call_id": tc.get("id", ""),
|
|
313
|
+
"content": result.get("content", ""),
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
# Check if we should continue
|
|
318
|
+
decision = self._decide_continuation(accumulated_content, tool_results)
|
|
319
|
+
should_continue = decision.should_continue and not decision.needs_user_input
|
|
320
|
+
else:
|
|
321
|
+
# No tool calls, check if we should continue
|
|
322
|
+
decision = self._decide_continuation(accumulated_content, [])
|
|
323
|
+
should_continue = decision.should_continue and not decision.needs_user_input
|
|
324
|
+
|
|
325
|
+
if accumulated_content:
|
|
326
|
+
messages.append(
|
|
327
|
+
{
|
|
328
|
+
"role": "assistant",
|
|
329
|
+
"content": accumulated_content,
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
yield ExecutionEvent(
|
|
334
|
+
type=ExecutionEventType.COMPLETE,
|
|
335
|
+
metadata={"iterations": iteration},
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
async def _load_tools(self) -> list[dict[str, Any]]:
|
|
339
|
+
"""Load available tools."""
|
|
340
|
+
if not self.tool_manager:
|
|
341
|
+
return []
|
|
342
|
+
|
|
343
|
+
if self._tools_cache is None:
|
|
344
|
+
self._tools_cache = await self.tool_manager.get_tools()
|
|
345
|
+
|
|
346
|
+
return self._tools_cache
|
|
347
|
+
|
|
348
|
+
async def _execute_tool_calls(self, tool_calls: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
349
|
+
"""Execute tool calls and return results."""
|
|
350
|
+
if not self.tool_manager:
|
|
351
|
+
return []
|
|
352
|
+
|
|
353
|
+
if self._parallel_executor:
|
|
354
|
+
log.info(f"Using parallel executor for {len(tool_calls)} tool calls")
|
|
355
|
+
parallel_results = await self._parallel_executor.execute_tools(
|
|
356
|
+
tool_calls, preserve_order=True
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
formatted_results: list[dict[str, Any]] = []
|
|
360
|
+
for result in parallel_results:
|
|
361
|
+
result_dict: dict[str, Any] = {
|
|
362
|
+
"name": result.name,
|
|
363
|
+
"content": result.content,
|
|
364
|
+
"status": result.status,
|
|
365
|
+
}
|
|
366
|
+
if result.authorization_url:
|
|
367
|
+
result_dict["authorization_url"] = result.authorization_url
|
|
368
|
+
formatted_results.append(result_dict)
|
|
369
|
+
return formatted_results
|
|
370
|
+
|
|
371
|
+
# Fallback to sequential execution
|
|
372
|
+
log.info(f"Using sequential execution for {len(tool_calls)} tool calls")
|
|
373
|
+
sequential_results: list[dict[str, Any]] = []
|
|
374
|
+
|
|
375
|
+
for tool_call in tool_calls:
|
|
376
|
+
function = tool_call.get("function", {})
|
|
377
|
+
name = function.get("name", "unknown")
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
args = json.loads(function.get("arguments", "{}"))
|
|
381
|
+
result_content = await self.tool_manager.execute(name, args)
|
|
382
|
+
actual_content = extract_tool_output_content(result_content)
|
|
383
|
+
|
|
384
|
+
sequential_results.append(
|
|
385
|
+
{
|
|
386
|
+
"name": name,
|
|
387
|
+
"content": str(actual_content),
|
|
388
|
+
"status": "success",
|
|
389
|
+
}
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
except Exception as e:
|
|
393
|
+
auth_url = None
|
|
394
|
+
if isinstance(e, ToolAuthorizationRequired):
|
|
395
|
+
auth_url = e.authorization_url
|
|
396
|
+
log.error(f"Authorization required for tool {name}")
|
|
397
|
+
else:
|
|
398
|
+
log.error(f"Tool execution failed for {name}: {e}")
|
|
399
|
+
|
|
400
|
+
error_dict: dict[str, Any] = {
|
|
401
|
+
"name": name,
|
|
402
|
+
"content": str(e),
|
|
403
|
+
"status": "error",
|
|
404
|
+
}
|
|
405
|
+
if auth_url:
|
|
406
|
+
error_dict["authorization_url"] = auth_url
|
|
407
|
+
|
|
408
|
+
sequential_results.append(error_dict)
|
|
409
|
+
|
|
410
|
+
return sequential_results
|
|
411
|
+
|
|
412
|
+
def _decide_continuation(
|
|
413
|
+
self, content: str, tool_results: list[dict[str, Any]]
|
|
414
|
+
) -> ContinuationDecision:
|
|
415
|
+
"""Decide whether to continue execution."""
|
|
416
|
+
# Check for explicit signals
|
|
417
|
+
content_lower = content.lower() if content else ""
|
|
418
|
+
|
|
419
|
+
if "[task_complete]" in content_lower or "task complete" in content_lower:
|
|
420
|
+
return ContinuationDecision(
|
|
421
|
+
should_continue=False,
|
|
422
|
+
reason="Task marked as complete",
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
if "[need_user_input]" in content_lower or "need user input" in content_lower:
|
|
426
|
+
return ContinuationDecision(
|
|
427
|
+
should_continue=False,
|
|
428
|
+
reason="Needs user input",
|
|
429
|
+
needs_user_input=True,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
if "[continue]" in content_lower:
|
|
433
|
+
return ContinuationDecision(
|
|
434
|
+
should_continue=True,
|
|
435
|
+
reason="Explicit continue signal",
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
# If there were tool calls with results, continue
|
|
439
|
+
if tool_results:
|
|
440
|
+
return ContinuationDecision(
|
|
441
|
+
should_continue=True,
|
|
442
|
+
reason="Tool results available",
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Default: don't continue
|
|
446
|
+
return ContinuationDecision(
|
|
447
|
+
should_continue=False,
|
|
448
|
+
reason="No continuation signal",
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def _build_messages(self, context: ExecutionContext) -> list[dict[str, Any]]:
|
|
452
|
+
"""Build messages for the provider.
|
|
453
|
+
|
|
454
|
+
Constructs the full message list including:
|
|
455
|
+
- System prompt with environment context and tools list
|
|
456
|
+
- Conversation history
|
|
457
|
+
- Current task as user message
|
|
458
|
+
"""
|
|
459
|
+
messages: list[dict[str, Any]] = []
|
|
460
|
+
|
|
461
|
+
# Check if we need to add a system prompt
|
|
462
|
+
has_system_message = any(
|
|
463
|
+
msg.get("role") == "system" for msg in context.conversation_history
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
if not has_system_message:
|
|
467
|
+
# Build tools list
|
|
468
|
+
tools_list = "(No tools available)"
|
|
469
|
+
if self._tools_description_cache:
|
|
470
|
+
tools_list = self._tools_description_cache
|
|
471
|
+
elif self.tool_manager:
|
|
472
|
+
tools_list = "(Tools available - use tools to see list)"
|
|
473
|
+
|
|
474
|
+
# Build environment context
|
|
475
|
+
try:
|
|
476
|
+
env_context = get_environment_context()
|
|
477
|
+
except Exception as e:
|
|
478
|
+
log.warning(f"Failed to get environment context: {e}")
|
|
479
|
+
env_context = "(Environment context unavailable)"
|
|
480
|
+
|
|
481
|
+
# Replace placeholders in system prompt
|
|
482
|
+
system_content = AGENT_SYSTEM_PROMPT.replace(
|
|
483
|
+
"{_TOOLS_BULLET_LIST}", tools_list
|
|
484
|
+
).replace("{_ENVIRONMENT_CONTEXT}", env_context)
|
|
485
|
+
|
|
486
|
+
messages.append(
|
|
487
|
+
{
|
|
488
|
+
"role": "system",
|
|
489
|
+
"content": system_content,
|
|
490
|
+
}
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
# Add conversation history
|
|
494
|
+
messages.extend(context.conversation_history)
|
|
495
|
+
|
|
496
|
+
# Add the current task
|
|
497
|
+
messages.append(
|
|
498
|
+
{
|
|
499
|
+
"role": "user",
|
|
500
|
+
"content": context.task,
|
|
501
|
+
}
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
return messages
|
|
505
|
+
|
|
506
|
+
def get_context_status(self, conversation_history: list[dict[str, Any]]) -> dict[str, Any]:
|
|
507
|
+
"""Get current context window status.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
conversation_history: Current conversation messages
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
Dict with token_count, percentage_used, needs_compaction, etc.
|
|
514
|
+
"""
|
|
515
|
+
return self.context_manager.check_context_status(conversation_history)
|
|
516
|
+
|
|
517
|
+
def get_tool_outputs_summary(self) -> dict[str, Any]:
|
|
518
|
+
"""Get summary of collected tool outputs.
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
Dict with total_outputs, unique_tools, total_size_chars, estimated_tokens
|
|
522
|
+
"""
|
|
523
|
+
return self.context_manager.get_tool_outputs_summary()
|
|
524
|
+
|
|
525
|
+
def get_all_tool_outputs(self) -> list[dict[str, Any]]:
|
|
526
|
+
"""Get all collected tool outputs.
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
List of all tool output records
|
|
530
|
+
"""
|
|
531
|
+
return self.context_manager.tool_outputs.get_all_outputs()
|
|
532
|
+
|
|
533
|
+
def get_final_tool_outputs(self) -> dict[str, str]:
|
|
534
|
+
"""Get only the final output for each tool.
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
Dict mapping tool_name to final output
|
|
538
|
+
"""
|
|
539
|
+
return self.context_manager.tool_outputs.get_final_outputs()
|
|
540
|
+
|
|
541
|
+
def compact_context(
|
|
542
|
+
self,
|
|
543
|
+
messages: list[dict[str, Any]],
|
|
544
|
+
strategy: CompactionStrategy = CompactionStrategy.KEEP_RECENT,
|
|
545
|
+
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
|
546
|
+
"""Manually compact context.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
messages: Current messages to compact
|
|
550
|
+
strategy: Compaction strategy to use
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
Tuple of (compacted_messages, backup_info)
|
|
554
|
+
"""
|
|
555
|
+
compacted, backup = self.context_manager.compact_context(messages, strategy)
|
|
556
|
+
return compacted, {
|
|
557
|
+
"timestamp": backup.timestamp.isoformat(),
|
|
558
|
+
"original_token_count": backup.token_count,
|
|
559
|
+
"new_token_count": self.context_manager.estimate_tokens(compacted),
|
|
560
|
+
"messages_before": len(messages),
|
|
561
|
+
"messages_after": len(compacted),
|
|
562
|
+
}
|