skilllite 0.1.0__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.
@@ -0,0 +1,770 @@
1
+ """
2
+ Agentic Loops - Continuous tool execution loops for LLM interactions.
3
+
4
+ This module provides a unified agentic loop implementation that supports
5
+ both OpenAI-compatible APIs and Claude's native API through a single interface.
6
+ """
7
+
8
+ import json
9
+ from enum import Enum
10
+ from typing import Any, List, Optional, TYPE_CHECKING, Dict, Callable
11
+
12
+ if TYPE_CHECKING:
13
+ from .manager import SkillManager
14
+
15
+
16
+ class ApiFormat(Enum):
17
+ """Supported API formats."""
18
+ OPENAI = "openai"
19
+ CLAUDE_NATIVE = "claude_native"
20
+
21
+
22
+ class AgenticLoop:
23
+ """
24
+ Unified agentic loop for LLM-tool interactions.
25
+
26
+ Supports both OpenAI-compatible APIs and Claude's native API through
27
+ a single interface. Handles the back-and-forth between the LLM and
28
+ tool execution until completion.
29
+
30
+ Works with:
31
+ - OpenAI (GPT-4, GPT-3.5, etc.)
32
+ - Azure OpenAI
33
+ - Anthropic Claude (both OpenAI-compatible and native)
34
+ - Ollama, vLLM, LMStudio
35
+ - DeepSeek, Qwen, Moonshot, etc.
36
+
37
+ Example:
38
+ ```python
39
+ # OpenAI-compatible (default)
40
+ loop = AgenticLoop(manager, client, model="gpt-4")
41
+
42
+ # Claude native API
43
+ loop = AgenticLoop(manager, client, model="claude-3-opus",
44
+ api_format=ApiFormat.CLAUDE_NATIVE)
45
+ ```
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ manager: "SkillManager",
51
+ client: Any,
52
+ model: str,
53
+ system_prompt: Optional[str] = None,
54
+ max_iterations: int = 10,
55
+ api_format: ApiFormat = ApiFormat.OPENAI,
56
+ custom_tool_handler: Optional[Callable] = None,
57
+ enable_task_planning: bool = True,
58
+ verbose: bool = True,
59
+ **kwargs
60
+ ):
61
+ """
62
+ Initialize the agentic loop.
63
+
64
+ Args:
65
+ manager: SkillManager instance
66
+ client: LLM client (OpenAI or Anthropic)
67
+ model: Model name to use
68
+ system_prompt: Optional system prompt
69
+ max_iterations: Maximum number of iterations
70
+ api_format: API format to use (OPENAI or CLAUDE_NATIVE)
71
+ custom_tool_handler: Optional custom tool handler function
72
+ enable_task_planning: Whether to generate task list before execution
73
+ verbose: Whether to print detailed logs
74
+ **kwargs: Additional arguments passed to the LLM
75
+ """
76
+ self.manager = manager
77
+ self.client = client
78
+ self.model = model
79
+ self.system_prompt = system_prompt
80
+ self.max_iterations = max_iterations
81
+ self.api_format = api_format
82
+ self.custom_tool_handler = custom_tool_handler
83
+ self.enable_task_planning = enable_task_planning
84
+ self.verbose = verbose
85
+ self.extra_kwargs = kwargs
86
+ self.task_list: List[Dict] = []
87
+
88
+ def _log(self, message: str) -> None:
89
+ """Print log message if verbose mode is enabled."""
90
+ if self.verbose:
91
+ print(message)
92
+
93
+ def _get_execution_system_prompt(self) -> str:
94
+ """
95
+ Generate the main execution system prompt for skill selection and file operations.
96
+
97
+ This prompt guides the LLM to:
98
+ 1. Analyze tasks and select appropriate skills
99
+ 2. Determine when to use built-in file read/write capabilities
100
+ 3. Execute tasks step by step
101
+ """
102
+ # Get available skills info
103
+ skills_info = []
104
+ for skill in self.manager.list_skills():
105
+ skill_desc = {
106
+ "name": skill.name,
107
+ "description": skill.description or "No description",
108
+ "executable": self.manager.is_executable(skill.name),
109
+ "path": str(skill.path) if hasattr(skill, 'path') else ""
110
+ }
111
+ skills_info.append(skill_desc)
112
+
113
+ skills_list_str = "\n".join([
114
+ f" - **{s['name']}**: {s['description']} {'[Executable]' if s['executable'] else '[Reference Only]'}"
115
+ for s in skills_info
116
+ ])
117
+
118
+ # Determine skills directory
119
+ skills_dir = ".skills" # Default
120
+ if skills_info and skills_info[0].get("path"):
121
+ # Extract skills directory from first skill path
122
+ first_path = skills_info[0]["path"]
123
+ if ".skills" in first_path:
124
+ skills_dir = ".skills"
125
+ elif "skills" in first_path:
126
+ skills_dir = "skills"
127
+
128
+ return f"""You are an intelligent task execution assistant responsible for planning and executing tasks based on user requirements.
129
+
130
+ ## Project Structure
131
+
132
+ **Skills Directory**: `{skills_dir}/`
133
+
134
+ All skills are stored in the `{skills_dir}/` directory, each skill is an independent subdirectory.
135
+
136
+ ## Available Skills
137
+
138
+ {skills_list_str}
139
+
140
+ ## Built-in File Operations
141
+
142
+ In addition to the above Skills, you have the following built-in file operation capabilities:
143
+
144
+ 1. **read_file**: Read file content
145
+ - Used to view existing files, understand project structure, read configurations, etc.
146
+ - Parameter: `file_path` (string, file path)
147
+
148
+ 2. **write_file**: Write/create files
149
+ - Used to create new files or modify existing file content
150
+ - Parameters: `file_path` (string, file path), `content` (string, file content)
151
+
152
+ 3. **list_directory**: List directory contents
153
+ - Used to view directory structure, understand project layout
154
+ - Parameter: `directory_path` (string, directory path, e.g., "." or ".skills")
155
+
156
+ 4. **file_exists**: Check if file exists
157
+ - Used to confirm file status before operations
158
+ - Parameter: `file_path` (string, file path)
159
+
160
+ **Note**: Parameter names must be used exactly as defined above, otherwise errors will occur.
161
+
162
+ ## Task Execution Strategy
163
+
164
+ ### 1. Task Analysis
165
+ - Carefully analyze user requirements and understand the final goal
166
+ - Break down complex tasks into executable sub-steps
167
+ - Identify the tools needed for each step (Skill or built-in file operations)
168
+
169
+ ### 2. Tool Selection Principles
170
+
171
+ **When to prioritize Skills:**
172
+ - Tasks involve specialized domain processing (e.g., data analysis, text processing, HTTP requests)
173
+ - Skills have encapsulated complex business logic
174
+ - Need to call external services or APIs
175
+
176
+ **When to use built-in file operations:**
177
+ - Need to read existing files to understand content or structure
178
+ - Need to create new files or modify existing files
179
+ - Need to view directory structure to locate files
180
+ - Need to prepare input data before calling Skills
181
+ - Need to save output results after calling Skills
182
+
183
+ ### 3. Execution Order
184
+
185
+ 1. **Information Gathering Phase**: Use read_file, list_directory to understand current state
186
+ 2. **Planning Phase**: Determine which Skills to use and operation order
187
+ 3. **Execution Phase**: Call Skills and file operations in sequence
188
+ 4. **Verification Phase**: Check execution results, make corrections if necessary
189
+
190
+ ### 4. Error Handling
191
+
192
+ - If Skill execution fails, analyze the error cause and try to fix it
193
+ - If file operation fails, check if the path is correct
194
+ - When encountering unsolvable problems, explain the situation to the user and request help
195
+
196
+ ## Output Guidelines
197
+
198
+ - After completing each task step, explicitly declare: "Task X completed"
199
+ - Provide clear execution process explanations
200
+ - Give a complete summary of execution results at the end
201
+ """
202
+
203
+ def _generate_task_list(self, user_message: str) -> List[Dict]:
204
+ """Generate task list from user message using LLM."""
205
+ # Get available skills for context
206
+ skills_names = self.manager.skill_names()
207
+ skills_info = ", ".join(skills_names) if skills_names else "None"
208
+
209
+ planning_prompt = f"""You are a task planning assistant. Based on user requirements, determine whether tools are needed and generate a task list.
210
+
211
+ ## Core Principle: Minimize Tool Usage
212
+
213
+ **Important**: Not all tasks require tools! Follow these principles:
214
+
215
+ 1. **Complete simple tasks directly**: If a task can be completed directly by the LLM (such as writing, translation, Q&A, creative generation, etc.), return an empty task list `[]` and let the LLM answer directly
216
+ 2. **Use tools only when necessary**: Only plan tool-using tasks when the task truly requires external capabilities (such as calculations, HTTP requests, file operations, data analysis, etc.)
217
+
218
+ ## Examples of tasks that DON'T need tools (return empty list `[]`)
219
+
220
+ - Writing poems, articles, stories
221
+ - Translating text
222
+ - Answering knowledge-based questions
223
+ - Code explanation, code review suggestions
224
+ - Creative generation, brainstorming
225
+ - Summarizing, rewriting, polishing text
226
+
227
+ ## Examples of tasks that NEED tools
228
+
229
+ - Precise calculations (use calculator)
230
+ - Sending HTTP requests (use http-request)
231
+ - Reading/writing files (use built-in file operations)
232
+ - Querying real-time weather (use weather)
233
+ - Creating new Skills (use skill-creator)
234
+
235
+ ## Available Resources
236
+
237
+ **Available Skills**: {skills_info}
238
+
239
+ **Built-in capabilities**: read_file (read files), write_file (write files), list_directory (list directory), file_exists (check file existence)
240
+
241
+ ## Planning Principles
242
+
243
+ 1. **Task decomposition**: Break down user requirements into specific, executable steps
244
+ 2. **Tool matching**: Select appropriate tools for each step (Skill or built-in file operations)
245
+ 3. **Dependency order**: Ensure tasks are arranged in correct dependency order
246
+ 4. **Verifiability**: Each task should have clear completion criteria
247
+
248
+ ## Output Format
249
+
250
+ Must return pure JSON format, no other text.
251
+ Task list is an array, each task contains:
252
+ - id: Task ID (number)
253
+ - description: Task description (concise and clear, stating what to do)
254
+ - tool_hint: Suggested tool (skill name or "file_operation" or "analysis")
255
+ - completed: Whether completed (initially false)
256
+
257
+ Example format:
258
+ [
259
+ {{"id": 1, "description": "Use list_directory to view project structure", "tool_hint": "file_operation", "completed": false}},
260
+ {{"id": 2, "description": "Use skill-creator to create basic skill structure", "tool_hint": "skill-creator", "completed": false}},
261
+ {{"id": 3, "description": "Use write_file to write main skill code", "tool_hint": "file_operation", "completed": false}},
262
+ {{"id": 4, "description": "Verify the created skill is correct", "tool_hint": "analysis", "completed": false}}
263
+ ]
264
+ - If task can be completed directly by LLM, return: `[]`
265
+ - If tools are needed, return task array, each task contains:
266
+ - id: Task ID (number)
267
+ - description: Task description
268
+ - tool_hint: Suggested tool (skill name or "file_operation")
269
+ - completed: false
270
+
271
+ Example 1 - Simple task (writing poetry):
272
+ User request: "Write a poem praising spring"
273
+ Return: []
274
+
275
+ Example 2 - Task requiring tools:
276
+ User request: "Calculate 123 * 456 + 789 for me"
277
+ Return: [{{"id": 1, "description": "Use calculator to compute expression", "tool_hint": "calculator", "completed": false}}]
278
+
279
+ Return only JSON, no other content."""
280
+
281
+ try:
282
+ if self.api_format == ApiFormat.OPENAI:
283
+ response = self.client.chat.completions.create(
284
+ model=self.model,
285
+ messages=[
286
+ {"role": "system", "content": planning_prompt},
287
+ {"role": "user", "content": f"User request:\n{user_message}\n\nPlease generate task list:"}
288
+ ],
289
+ temperature=0.3
290
+ )
291
+ result = response.choices[0].message.content.strip()
292
+ else: # CLAUDE_NATIVE
293
+ response = self.client.messages.create(
294
+ model=self.model,
295
+ max_tokens=2048,
296
+ system=planning_prompt,
297
+ messages=[
298
+ {"role": "user", "content": f"User request:\n{user_message}\n\nPlease generate task list:"}
299
+ ]
300
+ )
301
+ result = response.content[0].text.strip()
302
+
303
+ # Parse JSON
304
+ if result.startswith("```json"):
305
+ result = result[7:]
306
+ if result.startswith("```"):
307
+ result = result[3:]
308
+ if result.endswith("```"):
309
+ result = result[:-3]
310
+
311
+ task_list = json.loads(result.strip())
312
+
313
+ for task in task_list:
314
+ if "completed" not in task:
315
+ task["completed"] = False
316
+
317
+ # Check if any task involves creating a skill using skill-creator
318
+ # If so, automatically add a task to write SKILL.md content (if not already present)
319
+ has_skill_creation = any(
320
+ "skill-creator" in task.get("description", "").lower() or
321
+ "skill-creator" in task.get("tool_hint", "").lower()
322
+ for task in task_list
323
+ )
324
+
325
+ # Check if SKILL.md writing task already exists
326
+ has_skillmd_task = any(
327
+ "skill.md" in task.get("description", "").lower() or
328
+ "skill.md" in task.get("tool_hint", "").lower()
329
+ for task in task_list
330
+ )
331
+
332
+ if has_skill_creation and not has_skillmd_task:
333
+ # Add task to write SKILL.md actual content
334
+ max_id = max((task["id"] for task in task_list), default=0)
335
+ new_task = {
336
+ "id": max_id + 1,
337
+ "description": "Use write_file to write actual SKILL.md content (skill description, usage, parameter documentation, etc.)",
338
+ "tool_hint": "file_operation",
339
+ "completed": False
340
+ }
341
+ task_list.append(new_task)
342
+ self._log(f"\nšŸ’” Detected skill creation task, automatically adding SKILL.md writing task")
343
+
344
+ self._log(f"\nšŸ“‹ Generated task list ({len(task_list)} tasks):")
345
+ for task in task_list:
346
+ status = "āœ…" if task["completed"] else "⬜"
347
+ self._log(f" {status} [{task['id']}] {task['description']}")
348
+
349
+ return task_list
350
+
351
+ except Exception as e:
352
+ self._log(f"āš ļø Failed to generate task list: {e}")
353
+ return [{"id": 1, "description": user_message, "completed": False}]
354
+
355
+ def _update_task_list(self, completed_task_id: Optional[int] = None) -> None:
356
+ """Update task list display."""
357
+ if completed_task_id is not None:
358
+ for task in self.task_list:
359
+ if task["id"] == completed_task_id:
360
+ task["completed"] = True
361
+ break
362
+
363
+ completed = sum(1 for t in self.task_list if t["completed"])
364
+ self._log(f"\nšŸ“‹ Current task progress ({completed}/{len(self.task_list)}):")
365
+ for task in self.task_list:
366
+ status = "āœ…" if task["completed"] else "⬜"
367
+ self._log(f" {status} [{task['id']}] {task['description']}")
368
+
369
+ def _check_all_tasks_completed(self) -> bool:
370
+ """Check if all tasks are completed."""
371
+ return all(task["completed"] for task in self.task_list)
372
+
373
+ def _check_task_completion_in_content(self, content: str) -> Optional[int]:
374
+ """Check if any task was completed based on LLM response content."""
375
+ if not content:
376
+ return None
377
+ content_lower = content.lower()
378
+ for task in self.task_list:
379
+ if not task["completed"]:
380
+ if f"task {task['id']} completed" in content_lower or f"task{task['id']} completed" in content_lower:
381
+ return task["id"]
382
+ return None
383
+
384
+ def _get_task_system_prompt(self) -> str:
385
+ """Generate system prompt with task list and execution guidance."""
386
+ # Get the main execution system prompt
387
+ execution_prompt = self._get_execution_system_prompt()
388
+
389
+ # Format task list
390
+ task_list_str = json.dumps(self.task_list, ensure_ascii=False, indent=2)
391
+ current_task = next((t for t in self.task_list if not t["completed"]), None)
392
+ current_task_info = ""
393
+ if current_task:
394
+ tool_hint = current_task.get("tool_hint", "")
395
+ hint_str = f"(Suggested tool: {tool_hint})" if tool_hint else ""
396
+ current_task_info = f"\n\nšŸŽÆ **Current task to execute**: Task {current_task['id']} - {current_task['description']} {hint_str}"
397
+
398
+ task_rules = f"""
399
+ ---
400
+
401
+ ## Current Task List
402
+
403
+ {task_list_str}
404
+
405
+ ## Execution Rules
406
+
407
+ 1. **Strict sequential execution**: Must execute tasks in order, do not skip tasks
408
+ 2. **Focus on current task**: Focus only on executing the current task at a time
409
+ 3. **Explicit completion declaration**: After completing a task, must explicitly declare in response: "Task X completed" (X is task ID)
410
+ 4. **Sequential progression**: Can only start next task after current task is completed
411
+ 5. **Avoid repetition**: Do not repeat already completed tasks
412
+ 6. **Multi-step tasks**: If a task requires multiple tool calls to complete, continue calling tools until the task is truly completed before declaring
413
+ {current_task_info}
414
+
415
+ āš ļø **Important**: You must explicitly declare after completing each task so the system can track progress and know when to end.
416
+ """
417
+
418
+ return execution_prompt + task_rules
419
+
420
+ def _get_skill_docs_for_tools(self, tool_calls: List[Any]) -> Optional[str]:
421
+ """
422
+ Get full SKILL.md documentation for the tools being called.
423
+
424
+ This implements progressive disclosure - the LLM only gets the full
425
+ documentation when it decides to use a specific skill.
426
+
427
+ Tracks which skills have already been documented to avoid duplicates.
428
+
429
+ Args:
430
+ tool_calls: List of tool calls from LLM response
431
+
432
+ Returns:
433
+ Formatted string with full SKILL.md content for each skill,
434
+ or None if no new skill documentation is available
435
+ """
436
+ # Initialize the set to track documented skills if not exists
437
+ if not hasattr(self, '_documented_skills'):
438
+ self._documented_skills = set()
439
+
440
+ docs_parts = []
441
+
442
+ for tc in tool_calls:
443
+ tool_name = tc.function.name if hasattr(tc, 'function') else tc.get('function', {}).get('name', '')
444
+
445
+ # Skip built-in tools (read_file, write_file, etc.)
446
+ if tool_name in ['read_file', 'write_file', 'list_directory', 'file_exists']:
447
+ continue
448
+
449
+ # Skip if already documented in this session
450
+ if tool_name in self._documented_skills:
451
+ continue
452
+
453
+ # Get skill info - handle both regular skills and multi-script tools
454
+ skill_info = self.manager.get_skill(tool_name)
455
+ if not skill_info:
456
+ # Try to get parent skill for multi-script tools (e.g., "skill-creator:init-skill")
457
+ if ':' in tool_name:
458
+ parent_name = tool_name.split(':')[0]
459
+ skill_info = self.manager.get_skill(parent_name)
460
+ # Mark both the parent and the specific tool as documented
461
+ if skill_info:
462
+ self._documented_skills.add(parent_name)
463
+
464
+ if skill_info:
465
+ full_content = skill_info.get_full_content()
466
+ if full_content:
467
+ # Mark this skill as documented
468
+ self._documented_skills.add(tool_name)
469
+
470
+ docs_parts.append(f"""
471
+ ## šŸ“– Skill Documentation: {tool_name}
472
+
473
+ Below is the complete documentation for `{tool_name}`. Please read the documentation to understand how to use this tool correctly:
474
+
475
+ ---
476
+ {full_content}
477
+ ---
478
+ """)
479
+
480
+ if docs_parts:
481
+ header = """
482
+ # šŸ” Skill Detailed Documentation
483
+
484
+ You are calling the following Skills. Here is their complete documentation. Please read carefully to understand:
485
+ 1. The functionality and purpose of this Skill
486
+ 2. What parameters need to be passed
487
+ 3. The format and type of parameters
488
+ 4. Usage examples
489
+
490
+ Based on the documentation, call the tools with correct parameters.
491
+ """
492
+ return header + "\n".join(docs_parts)
493
+
494
+ return None
495
+
496
+ # ==================== OpenAI-compatible API ====================
497
+
498
+ def _run_openai(
499
+ self,
500
+ user_message: str,
501
+ allow_network: Optional[bool] = None,
502
+ timeout: Optional[int] = None
503
+ ) -> Any:
504
+ """Run loop using OpenAI-compatible API."""
505
+ messages = []
506
+
507
+ if self.system_prompt:
508
+ messages.append({"role": "system", "content": self.system_prompt})
509
+
510
+ if self.enable_task_planning and self.task_list:
511
+ messages.append({"role": "system", "content": self._get_task_system_prompt()})
512
+
513
+ messages.append({"role": "user", "content": user_message})
514
+
515
+ tools = self.manager.get_tools()
516
+ response = None
517
+
518
+ for iteration in range(self.max_iterations):
519
+ self._log(f"\nšŸ”„ Iteration #{iteration + 1}/{self.max_iterations}")
520
+
521
+ self._log("ā³ Calling LLM...")
522
+ response = self.client.chat.completions.create(
523
+ model=self.model,
524
+ messages=messages,
525
+ tools=tools if tools else None,
526
+ **self.extra_kwargs
527
+ )
528
+
529
+ message = response.choices[0].message
530
+ finish_reason = response.choices[0].finish_reason
531
+
532
+ self._log(f"āœ… LLM response completed (finish_reason: {finish_reason})")
533
+
534
+ # No tool calls
535
+ if not message.tool_calls:
536
+ self._log("šŸ“ LLM did not call any tools")
537
+
538
+ if self.enable_task_planning:
539
+ completed_id = self._check_task_completion_in_content(message.content)
540
+ if completed_id:
541
+ self._update_task_list(completed_id)
542
+
543
+ if self._check_all_tasks_completed():
544
+ self._log("šŸŽÆ All tasks completed, ending iteration")
545
+ return response
546
+ else:
547
+ return response
548
+
549
+ # Handle tool calls
550
+ self._log(f"\nšŸ”§ LLM decided to call {len(message.tool_calls)} tools:")
551
+ for idx, tc in enumerate(message.tool_calls, 1):
552
+ self._log(f" {idx}. {tc.function.name}")
553
+ self._log(f" Arguments: {tc.function.arguments}")
554
+
555
+ # Get full SKILL.md content for tools that haven't been documented yet
556
+ skill_docs = self._get_skill_docs_for_tools(message.tool_calls)
557
+
558
+ # If we have new skill docs, inject them into the prompt first
559
+ # and ask LLM to re-call with correct parameters
560
+ if skill_docs:
561
+ self._log(f"\nšŸ“– Injecting Skill documentation into prompt...")
562
+ messages.append({
563
+ "role": "system",
564
+ "content": skill_docs
565
+ })
566
+ messages.append({
567
+ "role": "user",
568
+ "content": "Please re-call the tools with correct parameters based on the complete Skill documentation above."
569
+ })
570
+ continue
571
+
572
+ messages.append(message)
573
+
574
+ self._log(f"\nāš™ļø Executing tools...")
575
+ if self.custom_tool_handler:
576
+ tool_results = self.custom_tool_handler(
577
+ response, self.manager, allow_network, timeout
578
+ )
579
+ else:
580
+ tool_results = self.manager.handle_tool_calls(
581
+ response, allow_network=allow_network, timeout=timeout
582
+ )
583
+
584
+ self._log(f"\nšŸ“Š Tool execution results:")
585
+ for idx, (result, tc) in enumerate(zip(tool_results, message.tool_calls), 1):
586
+ output = result.content
587
+ if len(output) > 500:
588
+ output = output[:500] + "... (truncated)"
589
+ self._log(f" {idx}. {tc.function.name}")
590
+ self._log(f" Result: {output}")
591
+
592
+ for result in tool_results:
593
+ messages.append(result.to_openai_format())
594
+
595
+ # Check task completion
596
+ if self.enable_task_planning:
597
+ if message.content:
598
+ completed_id = self._check_task_completion_in_content(message.content)
599
+ if completed_id:
600
+ self._update_task_list(completed_id)
601
+
602
+ if self._check_all_tasks_completed():
603
+ self._log("šŸŽÆ All tasks completed, ending iteration")
604
+ final_response = self.client.chat.completions.create(
605
+ model=self.model, messages=messages, tools=None
606
+ )
607
+ return final_response
608
+
609
+ # Update task focus
610
+ current_task = next((t for t in self.task_list if not t["completed"]), None)
611
+ if current_task:
612
+ task_list_str = json.dumps(self.task_list, ensure_ascii=False, indent=2)
613
+ messages.append({
614
+ "role": "system",
615
+ "content": f"Task progress update:\n{task_list_str}\n\nCurrent task to execute: Task {current_task['id']} - {current_task['description']}\n\nPlease continue to focus on completing the current task."
616
+ })
617
+
618
+ self._log(f"\nāš ļø Reached maximum iterations ({self.max_iterations}), stopping execution")
619
+ return response
620
+
621
+ # ==================== Claude Native API ====================
622
+
623
+ def _run_claude_native(
624
+ self,
625
+ user_message: str,
626
+ allow_network: Optional[bool] = None,
627
+ timeout: Optional[int] = None
628
+ ) -> Any:
629
+ """Run loop using Claude's native API."""
630
+ messages = [{"role": "user", "content": user_message}]
631
+ tools = self.manager.get_tools_for_claude_native()
632
+
633
+ # Build system prompt
634
+ system = self.system_prompt or ""
635
+ if self.enable_task_planning and self.task_list:
636
+ system = (system + "\n\n" if system else "") + self._get_task_system_prompt()
637
+
638
+ response = None
639
+
640
+ for iteration in range(self.max_iterations):
641
+ self._log(f"\nšŸ”„ Iteration #{iteration + 1}/{self.max_iterations}")
642
+
643
+ self._log("ā³ Calling LLM...")
644
+
645
+ kwargs = {
646
+ "model": self.model,
647
+ "max_tokens": self.extra_kwargs.get("max_tokens", 4096),
648
+ "tools": tools,
649
+ "messages": messages,
650
+ **{k: v for k, v in self.extra_kwargs.items() if k != "max_tokens"}
651
+ }
652
+ if system:
653
+ kwargs["system"] = system
654
+
655
+ response = self.client.messages.create(**kwargs)
656
+
657
+ self._log(f"āœ… LLM response completed (stop_reason: {response.stop_reason})")
658
+
659
+ # No tool use
660
+ if response.stop_reason != "tool_use":
661
+ self._log("šŸ“ LLM did not call any tools")
662
+
663
+ if self.enable_task_planning:
664
+ # Extract text content
665
+ text_content = ""
666
+ for block in response.content:
667
+ if hasattr(block, 'text'):
668
+ text_content += block.text
669
+
670
+ completed_id = self._check_task_completion_in_content(text_content)
671
+ if completed_id:
672
+ self._update_task_list(completed_id)
673
+
674
+ if self._check_all_tasks_completed():
675
+ self._log("šŸŽÆ All tasks completed, ending iteration")
676
+ return response
677
+ else:
678
+ self._log("ā³ There are still pending tasks, continuing execution...")
679
+ messages.append({"role": "assistant", "content": response.content})
680
+ messages.append({"role": "user", "content": "Please continue to complete the remaining tasks."})
681
+ continue
682
+ else:
683
+ return response
684
+
685
+ # Handle tool calls
686
+ tool_use_blocks = [b for b in response.content if hasattr(b, 'type') and b.type == 'tool_use']
687
+ self._log(f"\nšŸ”§ LLM decided to call {len(tool_use_blocks)} tools:")
688
+ for idx, block in enumerate(tool_use_blocks, 1):
689
+ self._log(f" {idx}. {block.name}")
690
+ self._log(f" Arguments: {json.dumps(block.input, ensure_ascii=False)}")
691
+
692
+ messages.append({"role": "assistant", "content": response.content})
693
+
694
+ self._log(f"\nāš™ļø Executing tools...")
695
+ tool_results = self.manager.handle_tool_calls_claude_native(
696
+ response, allow_network=allow_network, timeout=timeout
697
+ )
698
+
699
+ self._log(f"\nšŸ“Š Tool execution results:")
700
+ for idx, result in enumerate(tool_results, 1):
701
+ output = result.content
702
+ if len(output) > 500:
703
+ output = output[:500] + "... (truncated)"
704
+ self._log(f" {idx}. Result: {output}")
705
+
706
+ formatted_results = self.manager.format_tool_results_claude_native(tool_results)
707
+ messages.append({"role": "user", "content": formatted_results})
708
+
709
+ # Check task completion
710
+ if self.enable_task_planning:
711
+ text_content = ""
712
+ for block in response.content:
713
+ if hasattr(block, 'text'):
714
+ text_content += block.text
715
+
716
+ completed_id = self._check_task_completion_in_content(text_content)
717
+ if completed_id:
718
+ self._update_task_list(completed_id)
719
+
720
+ if self._check_all_tasks_completed():
721
+ self._log("šŸŽÆ All tasks completed, ending iteration")
722
+ final_response = self.client.messages.create(
723
+ model=self.model,
724
+ max_tokens=self.extra_kwargs.get("max_tokens", 4096),
725
+ system=system if system else None,
726
+ messages=messages
727
+ )
728
+ return final_response
729
+
730
+ self._log(f"\nāš ļø Reached maximum iterations ({self.max_iterations}), stopping execution")
731
+ return response
732
+
733
+ # ==================== Public API ====================
734
+
735
+ def run(
736
+ self,
737
+ user_message: str,
738
+ allow_network: Optional[bool] = None,
739
+ timeout: Optional[int] = None
740
+ ) -> Any:
741
+ """
742
+ Run the agentic loop until completion.
743
+
744
+ Args:
745
+ user_message: The user's message
746
+ allow_network: Override default network setting for skill execution
747
+ timeout: Execution timeout per tool call in seconds
748
+
749
+ Returns:
750
+ The final LLM response
751
+ """
752
+ # Generate task list if enabled
753
+ if self.enable_task_planning:
754
+ self.task_list = self._generate_task_list(user_message)
755
+
756
+ # If task list is empty, the task can be completed by LLM directly
757
+ # Disable task planning mode for this run
758
+ if not self.task_list:
759
+ self._log("\nšŸ’” Task can be completed directly by LLM, no tools needed")
760
+ self.enable_task_planning = False
761
+
762
+ # Dispatch to appropriate implementation
763
+ if self.api_format == ApiFormat.OPENAI:
764
+ return self._run_openai(user_message, allow_network, timeout)
765
+ else:
766
+ return self._run_claude_native(user_message, allow_network, timeout)
767
+
768
+
769
+ # Backward compatibility alias
770
+ AgenticLoopClaudeNative = AgenticLoop