aury-agent 0.0.4__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.
- aury/__init__.py +2 -0
- aury/agents/__init__.py +55 -0
- aury/agents/a2a/__init__.py +168 -0
- aury/agents/backends/__init__.py +196 -0
- aury/agents/backends/artifact/__init__.py +9 -0
- aury/agents/backends/artifact/memory.py +130 -0
- aury/agents/backends/artifact/types.py +133 -0
- aury/agents/backends/code/__init__.py +65 -0
- aury/agents/backends/file/__init__.py +11 -0
- aury/agents/backends/file/local.py +66 -0
- aury/agents/backends/file/types.py +40 -0
- aury/agents/backends/invocation/__init__.py +8 -0
- aury/agents/backends/invocation/memory.py +81 -0
- aury/agents/backends/invocation/types.py +110 -0
- aury/agents/backends/memory/__init__.py +8 -0
- aury/agents/backends/memory/memory.py +179 -0
- aury/agents/backends/memory/types.py +136 -0
- aury/agents/backends/message/__init__.py +9 -0
- aury/agents/backends/message/memory.py +122 -0
- aury/agents/backends/message/types.py +124 -0
- aury/agents/backends/sandbox.py +275 -0
- aury/agents/backends/session/__init__.py +8 -0
- aury/agents/backends/session/memory.py +93 -0
- aury/agents/backends/session/types.py +124 -0
- aury/agents/backends/shell/__init__.py +11 -0
- aury/agents/backends/shell/local.py +110 -0
- aury/agents/backends/shell/types.py +55 -0
- aury/agents/backends/shell.py +209 -0
- aury/agents/backends/snapshot/__init__.py +19 -0
- aury/agents/backends/snapshot/git.py +95 -0
- aury/agents/backends/snapshot/hybrid.py +125 -0
- aury/agents/backends/snapshot/memory.py +86 -0
- aury/agents/backends/snapshot/types.py +59 -0
- aury/agents/backends/state/__init__.py +29 -0
- aury/agents/backends/state/composite.py +49 -0
- aury/agents/backends/state/file.py +57 -0
- aury/agents/backends/state/memory.py +52 -0
- aury/agents/backends/state/sqlite.py +262 -0
- aury/agents/backends/state/types.py +178 -0
- aury/agents/backends/subagent/__init__.py +165 -0
- aury/agents/cli/__init__.py +41 -0
- aury/agents/cli/chat.py +239 -0
- aury/agents/cli/config.py +236 -0
- aury/agents/cli/extensions.py +460 -0
- aury/agents/cli/main.py +189 -0
- aury/agents/cli/session.py +337 -0
- aury/agents/cli/workflow.py +276 -0
- aury/agents/context_providers/__init__.py +66 -0
- aury/agents/context_providers/artifact.py +299 -0
- aury/agents/context_providers/base.py +177 -0
- aury/agents/context_providers/memory.py +70 -0
- aury/agents/context_providers/message.py +130 -0
- aury/agents/context_providers/skill.py +50 -0
- aury/agents/context_providers/subagent.py +46 -0
- aury/agents/context_providers/tool.py +68 -0
- aury/agents/core/__init__.py +83 -0
- aury/agents/core/base.py +573 -0
- aury/agents/core/context.py +797 -0
- aury/agents/core/context_builder.py +303 -0
- aury/agents/core/event_bus/__init__.py +15 -0
- aury/agents/core/event_bus/bus.py +203 -0
- aury/agents/core/factory.py +169 -0
- aury/agents/core/isolator.py +97 -0
- aury/agents/core/logging.py +95 -0
- aury/agents/core/parallel.py +194 -0
- aury/agents/core/runner.py +139 -0
- aury/agents/core/services/__init__.py +5 -0
- aury/agents/core/services/file_session.py +144 -0
- aury/agents/core/services/message.py +53 -0
- aury/agents/core/services/session.py +53 -0
- aury/agents/core/signals.py +109 -0
- aury/agents/core/state.py +363 -0
- aury/agents/core/types/__init__.py +107 -0
- aury/agents/core/types/action.py +176 -0
- aury/agents/core/types/artifact.py +135 -0
- aury/agents/core/types/block.py +736 -0
- aury/agents/core/types/message.py +350 -0
- aury/agents/core/types/recall.py +144 -0
- aury/agents/core/types/session.py +257 -0
- aury/agents/core/types/subagent.py +154 -0
- aury/agents/core/types/tool.py +205 -0
- aury/agents/eval/__init__.py +331 -0
- aury/agents/hitl/__init__.py +57 -0
- aury/agents/hitl/ask_user.py +242 -0
- aury/agents/hitl/compaction.py +230 -0
- aury/agents/hitl/exceptions.py +87 -0
- aury/agents/hitl/permission.py +617 -0
- aury/agents/hitl/revert.py +216 -0
- aury/agents/llm/__init__.py +31 -0
- aury/agents/llm/adapter.py +367 -0
- aury/agents/llm/openai.py +294 -0
- aury/agents/llm/provider.py +476 -0
- aury/agents/mcp/__init__.py +153 -0
- aury/agents/memory/__init__.py +46 -0
- aury/agents/memory/compaction.py +394 -0
- aury/agents/memory/manager.py +465 -0
- aury/agents/memory/processor.py +177 -0
- aury/agents/memory/store.py +187 -0
- aury/agents/memory/types.py +137 -0
- aury/agents/messages/__init__.py +40 -0
- aury/agents/messages/config.py +47 -0
- aury/agents/messages/raw_store.py +224 -0
- aury/agents/messages/store.py +118 -0
- aury/agents/messages/types.py +88 -0
- aury/agents/middleware/__init__.py +31 -0
- aury/agents/middleware/base.py +341 -0
- aury/agents/middleware/chain.py +342 -0
- aury/agents/middleware/message.py +129 -0
- aury/agents/middleware/message_container.py +126 -0
- aury/agents/middleware/raw_message.py +153 -0
- aury/agents/middleware/truncation.py +139 -0
- aury/agents/middleware/types.py +81 -0
- aury/agents/plugin.py +162 -0
- aury/agents/react/__init__.py +4 -0
- aury/agents/react/agent.py +1923 -0
- aury/agents/sandbox/__init__.py +23 -0
- aury/agents/sandbox/local.py +239 -0
- aury/agents/sandbox/remote.py +200 -0
- aury/agents/sandbox/types.py +115 -0
- aury/agents/skill/__init__.py +16 -0
- aury/agents/skill/loader.py +180 -0
- aury/agents/skill/types.py +83 -0
- aury/agents/tool/__init__.py +39 -0
- aury/agents/tool/builtin/__init__.py +23 -0
- aury/agents/tool/builtin/ask_user.py +155 -0
- aury/agents/tool/builtin/bash.py +107 -0
- aury/agents/tool/builtin/delegate.py +726 -0
- aury/agents/tool/builtin/edit.py +121 -0
- aury/agents/tool/builtin/plan.py +277 -0
- aury/agents/tool/builtin/read.py +91 -0
- aury/agents/tool/builtin/thinking.py +111 -0
- aury/agents/tool/builtin/yield_result.py +130 -0
- aury/agents/tool/decorator.py +252 -0
- aury/agents/tool/set.py +204 -0
- aury/agents/usage/__init__.py +12 -0
- aury/agents/usage/tracker.py +236 -0
- aury/agents/workflow/__init__.py +85 -0
- aury/agents/workflow/adapter.py +268 -0
- aury/agents/workflow/dag.py +116 -0
- aury/agents/workflow/dsl.py +575 -0
- aury/agents/workflow/executor.py +659 -0
- aury/agents/workflow/expression.py +136 -0
- aury/agents/workflow/parser.py +182 -0
- aury/agents/workflow/state.py +145 -0
- aury/agents/workflow/types.py +86 -0
- aury_agent-0.0.4.dist-info/METADATA +90 -0
- aury_agent-0.0.4.dist-info/RECORD +149 -0
- aury_agent-0.0.4.dist-info/WHEEL +4 -0
- aury_agent-0.0.4.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""EditTool - Edit file contents."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from ...core.types.tool import BaseTool, ToolContext, ToolResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EditTool(BaseTool):
|
|
11
|
+
"""Edit file contents with multiple modes."""
|
|
12
|
+
|
|
13
|
+
_name = "edit"
|
|
14
|
+
_description = "Edit file contents (overwrite, append, or insert at line)"
|
|
15
|
+
_parameters = {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"properties": {
|
|
18
|
+
"path": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "Path to the file to edit",
|
|
21
|
+
},
|
|
22
|
+
"content": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "Content to write",
|
|
25
|
+
},
|
|
26
|
+
"mode": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"enum": ["overwrite", "append", "insert"],
|
|
29
|
+
"description": "Edit mode: overwrite (replace), append (add to end), insert (at line)",
|
|
30
|
+
"default": "overwrite",
|
|
31
|
+
},
|
|
32
|
+
"line": {
|
|
33
|
+
"type": "integer",
|
|
34
|
+
"description": "Line number for insert mode (1-indexed)",
|
|
35
|
+
},
|
|
36
|
+
"encoding": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"description": "File encoding (default: utf-8)",
|
|
39
|
+
"default": "utf-8",
|
|
40
|
+
},
|
|
41
|
+
"create_dirs": {
|
|
42
|
+
"type": "boolean",
|
|
43
|
+
"description": "Create parent directories if needed",
|
|
44
|
+
"default": True,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
"required": ["path", "content"],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
def __init__(self, allowed_paths: list[str] | None = None):
|
|
51
|
+
"""Initialize EditTool.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
allowed_paths: List of allowed path prefixes (None = allow all)
|
|
55
|
+
"""
|
|
56
|
+
self._allowed_paths = allowed_paths
|
|
57
|
+
|
|
58
|
+
async def execute(self, params: dict[str, Any], ctx: ToolContext) -> ToolResult:
|
|
59
|
+
file_path = params.get("path", "")
|
|
60
|
+
content = params.get("content", "")
|
|
61
|
+
mode = params.get("mode", "overwrite")
|
|
62
|
+
line = params.get("line")
|
|
63
|
+
encoding = params.get("encoding", "utf-8")
|
|
64
|
+
create_dirs = params.get("create_dirs", True)
|
|
65
|
+
|
|
66
|
+
if not file_path:
|
|
67
|
+
return ToolResult.error("Path is required")
|
|
68
|
+
|
|
69
|
+
path = Path(file_path).expanduser().resolve()
|
|
70
|
+
|
|
71
|
+
# Security check
|
|
72
|
+
if self._allowed_paths:
|
|
73
|
+
if not any(str(path).startswith(p) for p in self._allowed_paths):
|
|
74
|
+
return ToolResult.error(f"Path not allowed: {path}")
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# Create parent directories
|
|
78
|
+
if create_dirs and not path.parent.exists():
|
|
79
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
|
|
81
|
+
if mode == "overwrite":
|
|
82
|
+
path.write_text(content, encoding=encoding)
|
|
83
|
+
return ToolResult(output=f"File written ({len(content)} chars)")
|
|
84
|
+
|
|
85
|
+
elif mode == "append":
|
|
86
|
+
existing = path.read_text(encoding=encoding) if path.exists() else ""
|
|
87
|
+
path.write_text(existing + content, encoding=encoding)
|
|
88
|
+
return ToolResult(output=f"Content appended ({len(content)} chars)")
|
|
89
|
+
|
|
90
|
+
elif mode == "insert":
|
|
91
|
+
if line is None:
|
|
92
|
+
return ToolResult.error("Line number required for insert mode")
|
|
93
|
+
|
|
94
|
+
if path.exists():
|
|
95
|
+
lines = path.read_text(encoding=encoding).splitlines(keepends=True)
|
|
96
|
+
else:
|
|
97
|
+
lines = []
|
|
98
|
+
|
|
99
|
+
# Pad with empty lines if needed
|
|
100
|
+
while len(lines) < line - 1:
|
|
101
|
+
lines.append("\n")
|
|
102
|
+
|
|
103
|
+
# Insert at position
|
|
104
|
+
insert_idx = max(0, line - 1)
|
|
105
|
+
content_lines = content.splitlines(keepends=True)
|
|
106
|
+
if content_lines and not content_lines[-1].endswith("\n"):
|
|
107
|
+
content_lines[-1] += "\n"
|
|
108
|
+
|
|
109
|
+
new_lines = lines[:insert_idx] + content_lines + lines[insert_idx:]
|
|
110
|
+
path.write_text("".join(new_lines), encoding=encoding)
|
|
111
|
+
|
|
112
|
+
return ToolResult(output=f"Content inserted at line {line} ({len(content_lines)} lines)")
|
|
113
|
+
|
|
114
|
+
else:
|
|
115
|
+
return ToolResult.error(f"Unknown mode: {mode}")
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
return ToolResult.error(str(e))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
__all__ = ["EditTool"]
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Plan tool - manage execution plan with checklist.
|
|
2
|
+
|
|
3
|
+
Emits PLAN block and manages plan state.
|
|
4
|
+
Combines planning (design) and tracking (checklist) in one tool.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
from ...core.types.tool import BaseTool, ToolContext, ToolResult
|
|
12
|
+
from ...core.types.block import BlockEvent, BlockKind, BlockOp
|
|
13
|
+
from ...core.types.session import generate_id
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PlanTool(BaseTool):
|
|
17
|
+
"""Manage execution plan with checklist.
|
|
18
|
+
|
|
19
|
+
Actions:
|
|
20
|
+
- create: Create new plan with items
|
|
21
|
+
- add: Add item to existing plan
|
|
22
|
+
- check: Mark item as done
|
|
23
|
+
- uncheck: Mark item as pending
|
|
24
|
+
- update: Update plan title/summary
|
|
25
|
+
- view: View current plan
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
_name = "plan"
|
|
29
|
+
_description = """Manage your execution plan.
|
|
30
|
+
|
|
31
|
+
Actions:
|
|
32
|
+
- create: Create a new plan with checklist items
|
|
33
|
+
- add: Add an item to the plan
|
|
34
|
+
- check: Mark an item as completed
|
|
35
|
+
- uncheck: Mark an item as pending
|
|
36
|
+
- update: Update plan title or summary
|
|
37
|
+
- view: View current plan status
|
|
38
|
+
|
|
39
|
+
Use this to track your progress on complex tasks."""
|
|
40
|
+
|
|
41
|
+
_parameters = {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"properties": {
|
|
44
|
+
"action": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"enum": ["create", "add", "check", "uncheck", "update", "view"],
|
|
47
|
+
"description": "Action to perform",
|
|
48
|
+
},
|
|
49
|
+
"title": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "Plan title (for create/update)",
|
|
52
|
+
},
|
|
53
|
+
"summary": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "Plan summary/notes (for create/update)",
|
|
56
|
+
},
|
|
57
|
+
"items": {
|
|
58
|
+
"type": "array",
|
|
59
|
+
"items": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"properties": {
|
|
62
|
+
"title": {"type": "string"},
|
|
63
|
+
"note": {"type": "string"},
|
|
64
|
+
},
|
|
65
|
+
"required": ["title"],
|
|
66
|
+
},
|
|
67
|
+
"description": "Checklist items (for create)",
|
|
68
|
+
},
|
|
69
|
+
"item": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"properties": {
|
|
72
|
+
"title": {"type": "string"},
|
|
73
|
+
"note": {"type": "string"},
|
|
74
|
+
},
|
|
75
|
+
"description": "Single item to add (for add)",
|
|
76
|
+
},
|
|
77
|
+
"item_id": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"description": "Item ID (for check/uncheck)",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
"required": ["action"],
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def name(self) -> str:
|
|
87
|
+
return self._name
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def description(self) -> str:
|
|
91
|
+
return self._description
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def parameters(self) -> dict[str, Any]:
|
|
95
|
+
return self._parameters
|
|
96
|
+
|
|
97
|
+
async def execute(
|
|
98
|
+
self,
|
|
99
|
+
params: dict[str, Any],
|
|
100
|
+
ctx: ToolContext,
|
|
101
|
+
) -> ToolResult:
|
|
102
|
+
"""Execute plan action."""
|
|
103
|
+
action = params.get("action", "view")
|
|
104
|
+
|
|
105
|
+
# Storage key
|
|
106
|
+
key = f"plan:{ctx.session_id}"
|
|
107
|
+
|
|
108
|
+
# Get storage
|
|
109
|
+
storage = getattr(ctx, 'storage', None)
|
|
110
|
+
if storage is None:
|
|
111
|
+
return ToolResult.error("Storage not configured")
|
|
112
|
+
|
|
113
|
+
# Load current plan
|
|
114
|
+
plan = await storage.get("plan", key) or self._empty_plan()
|
|
115
|
+
|
|
116
|
+
# Execute action
|
|
117
|
+
if action == "create":
|
|
118
|
+
plan = await self._create(params, plan)
|
|
119
|
+
elif action == "add":
|
|
120
|
+
plan = await self._add(params, plan)
|
|
121
|
+
elif action == "check":
|
|
122
|
+
plan = await self._check(params, plan, done=True)
|
|
123
|
+
elif action == "uncheck":
|
|
124
|
+
plan = await self._check(params, plan, done=False)
|
|
125
|
+
elif action == "update":
|
|
126
|
+
plan = await self._update(params, plan)
|
|
127
|
+
elif action == "view":
|
|
128
|
+
pass # Just return current plan
|
|
129
|
+
else:
|
|
130
|
+
return ToolResult.error(f"Unknown action: {action}")
|
|
131
|
+
|
|
132
|
+
# Save plan
|
|
133
|
+
await storage.set("plan", key, plan)
|
|
134
|
+
|
|
135
|
+
# Emit PLAN block
|
|
136
|
+
await self._emit_plan_block(ctx, plan, action)
|
|
137
|
+
|
|
138
|
+
# Format output
|
|
139
|
+
output = self._format_plan(plan)
|
|
140
|
+
|
|
141
|
+
return ToolResult(output=output)
|
|
142
|
+
|
|
143
|
+
def _empty_plan(self) -> dict[str, Any]:
|
|
144
|
+
"""Create empty plan structure."""
|
|
145
|
+
return {
|
|
146
|
+
"block_id": None, # Block ID for emit
|
|
147
|
+
"title": "",
|
|
148
|
+
"summary": "",
|
|
149
|
+
"status": "draft",
|
|
150
|
+
"items": [],
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async def _create(self, params: dict[str, Any], plan: dict) -> dict:
|
|
154
|
+
"""Create new plan."""
|
|
155
|
+
plan = {
|
|
156
|
+
"block_id": generate_id("blk"), # New block_id for new plan
|
|
157
|
+
"title": params.get("title", "Execution Plan"),
|
|
158
|
+
"summary": params.get("summary", ""),
|
|
159
|
+
"status": "in_progress",
|
|
160
|
+
"items": [],
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
items = params.get("items", [])
|
|
164
|
+
for item in items:
|
|
165
|
+
plan["items"].append({
|
|
166
|
+
"id": str(uuid4())[:8],
|
|
167
|
+
"title": item.get("title", ""),
|
|
168
|
+
"note": item.get("note", ""),
|
|
169
|
+
"status": "pending",
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
return plan
|
|
173
|
+
|
|
174
|
+
async def _add(self, params: dict[str, Any], plan: dict) -> dict:
|
|
175
|
+
"""Add item to plan."""
|
|
176
|
+
item = params.get("item", {})
|
|
177
|
+
if not item.get("title"):
|
|
178
|
+
item = {"title": params.get("title", ""), "note": params.get("note", "")}
|
|
179
|
+
|
|
180
|
+
if not item.get("title"):
|
|
181
|
+
raise ValueError("Item title is required")
|
|
182
|
+
|
|
183
|
+
plan["items"].append({
|
|
184
|
+
"id": str(uuid4())[:8],
|
|
185
|
+
"title": item.get("title", ""),
|
|
186
|
+
"note": item.get("note", ""),
|
|
187
|
+
"status": "pending",
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
if plan["status"] == "draft":
|
|
191
|
+
plan["status"] = "in_progress"
|
|
192
|
+
|
|
193
|
+
return plan
|
|
194
|
+
|
|
195
|
+
async def _check(self, params: dict[str, Any], plan: dict, done: bool) -> dict:
|
|
196
|
+
"""Mark item as done/pending."""
|
|
197
|
+
item_id = params.get("item_id", "")
|
|
198
|
+
|
|
199
|
+
for item in plan["items"]:
|
|
200
|
+
if item["id"] == item_id:
|
|
201
|
+
item["status"] = "done" if done else "pending"
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
# Update plan status if all done
|
|
205
|
+
if all(i["status"] == "done" for i in plan["items"]) and plan["items"]:
|
|
206
|
+
plan["status"] = "done"
|
|
207
|
+
elif any(i["status"] == "done" for i in plan["items"]):
|
|
208
|
+
plan["status"] = "in_progress"
|
|
209
|
+
|
|
210
|
+
return plan
|
|
211
|
+
|
|
212
|
+
async def _update(self, params: dict[str, Any], plan: dict) -> dict:
|
|
213
|
+
"""Update plan metadata."""
|
|
214
|
+
if "title" in params:
|
|
215
|
+
plan["title"] = params["title"]
|
|
216
|
+
if "summary" in params:
|
|
217
|
+
plan["summary"] = params["summary"]
|
|
218
|
+
return plan
|
|
219
|
+
|
|
220
|
+
async def _emit_plan_block(self, ctx: ToolContext, plan: dict, action: str) -> None:
|
|
221
|
+
"""Emit PLAN block event."""
|
|
222
|
+
emit = getattr(ctx, 'emit', None)
|
|
223
|
+
if emit is None:
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# Get or generate block_id
|
|
227
|
+
block_id = plan.get("block_id")
|
|
228
|
+
if not block_id:
|
|
229
|
+
block_id = generate_id("blk")
|
|
230
|
+
plan["block_id"] = block_id
|
|
231
|
+
|
|
232
|
+
op = BlockOp.APPLY if action == "create" else BlockOp.PATCH
|
|
233
|
+
|
|
234
|
+
# Don't include block_id in data (it's metadata)
|
|
235
|
+
data = {k: v for k, v in plan.items() if k != "block_id"}
|
|
236
|
+
|
|
237
|
+
block = BlockEvent(
|
|
238
|
+
block_id=block_id,
|
|
239
|
+
kind=BlockKind.PLAN,
|
|
240
|
+
op=op,
|
|
241
|
+
data=data,
|
|
242
|
+
session_id=ctx.session_id,
|
|
243
|
+
invocation_id=ctx.invocation_id,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
await emit(block)
|
|
247
|
+
|
|
248
|
+
def _format_plan(self, plan: dict) -> str:
|
|
249
|
+
"""Format plan as text."""
|
|
250
|
+
lines = []
|
|
251
|
+
|
|
252
|
+
if plan["title"]:
|
|
253
|
+
lines.append(f"# {plan['title']}")
|
|
254
|
+
|
|
255
|
+
if plan["summary"]:
|
|
256
|
+
lines.append(f"\n{plan['summary']}")
|
|
257
|
+
|
|
258
|
+
lines.append(f"\nStatus: {plan['status']}")
|
|
259
|
+
|
|
260
|
+
if plan["items"]:
|
|
261
|
+
lines.append("\nChecklist:")
|
|
262
|
+
for item in plan["items"]:
|
|
263
|
+
icon = "✓" if item["status"] == "done" else "○"
|
|
264
|
+
line = f" [{icon}] {item['title']}"
|
|
265
|
+
if item.get("note"):
|
|
266
|
+
line += f" - {item['note']}"
|
|
267
|
+
line += f" (id: {item['id']})"
|
|
268
|
+
lines.append(line)
|
|
269
|
+
|
|
270
|
+
pending = sum(1 for i in plan["items"] if i["status"] == "pending")
|
|
271
|
+
done = sum(1 for i in plan["items"] if i["status"] == "done")
|
|
272
|
+
lines.append(f"\nProgress: {done}/{len(plan['items'])} completed")
|
|
273
|
+
|
|
274
|
+
return "\n".join(lines)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
__all__ = ["PlanTool"]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""ReadTool - Read file contents."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ...core.types.tool import BaseTool, ToolContext, ToolResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ReadTool(BaseTool):
|
|
11
|
+
"""Read file contents with optional line range."""
|
|
12
|
+
|
|
13
|
+
_name = "read"
|
|
14
|
+
_description = "Read file contents, optionally specifying line range"
|
|
15
|
+
_parameters = {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"properties": {
|
|
18
|
+
"path": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "Path to the file to read",
|
|
21
|
+
},
|
|
22
|
+
"start_line": {
|
|
23
|
+
"type": "integer",
|
|
24
|
+
"description": "Start line number (1-indexed, optional)",
|
|
25
|
+
},
|
|
26
|
+
"end_line": {
|
|
27
|
+
"type": "integer",
|
|
28
|
+
"description": "End line number (inclusive, optional)",
|
|
29
|
+
},
|
|
30
|
+
"encoding": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "File encoding (default: utf-8)",
|
|
33
|
+
"default": "utf-8",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
"required": ["path"],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
def __init__(self, allowed_paths: list[str] | None = None):
|
|
40
|
+
"""Initialize ReadTool.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
allowed_paths: List of allowed path prefixes (None = allow all)
|
|
44
|
+
"""
|
|
45
|
+
self._allowed_paths = allowed_paths
|
|
46
|
+
|
|
47
|
+
async def execute(self, params: dict[str, Any], ctx: ToolContext) -> ToolResult:
|
|
48
|
+
file_path = params.get("path", "")
|
|
49
|
+
start_line = params.get("start_line")
|
|
50
|
+
end_line = params.get("end_line")
|
|
51
|
+
encoding = params.get("encoding", "utf-8")
|
|
52
|
+
|
|
53
|
+
if not file_path:
|
|
54
|
+
return ToolResult.error("Path is required")
|
|
55
|
+
|
|
56
|
+
path = Path(file_path).expanduser().resolve()
|
|
57
|
+
|
|
58
|
+
# Security check
|
|
59
|
+
if self._allowed_paths:
|
|
60
|
+
if not any(str(path).startswith(p) for p in self._allowed_paths):
|
|
61
|
+
return ToolResult.error(f"Path not allowed: {path}")
|
|
62
|
+
|
|
63
|
+
if not path.exists():
|
|
64
|
+
return ToolResult.error(f"File not found: {path}")
|
|
65
|
+
|
|
66
|
+
if not path.is_file():
|
|
67
|
+
return ToolResult.error(f"Not a file: {path}")
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
content = path.read_text(encoding=encoding)
|
|
71
|
+
lines = content.splitlines(keepends=True)
|
|
72
|
+
|
|
73
|
+
# Apply line range
|
|
74
|
+
if start_line is not None or end_line is not None:
|
|
75
|
+
start_idx = (start_line - 1) if start_line else 0
|
|
76
|
+
end_idx = end_line if end_line else len(lines)
|
|
77
|
+
lines = lines[start_idx:end_idx]
|
|
78
|
+
|
|
79
|
+
# Add line numbers
|
|
80
|
+
output_lines = []
|
|
81
|
+
for i, line in enumerate(lines, start=start_idx + 1):
|
|
82
|
+
output_lines.append(f"{i:4d}| {line.rstrip()}")
|
|
83
|
+
content = "\n".join(output_lines)
|
|
84
|
+
|
|
85
|
+
return ToolResult(output=content or "(empty file)")
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
return ToolResult.error(str(e))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
__all__ = ["ReadTool"]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Thinking tool - emit reasoning process.
|
|
2
|
+
|
|
3
|
+
Allows agent to externalize its thinking/reasoning process.
|
|
4
|
+
Useful for transparency, debugging, and chain-of-thought.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ...core.logging import tool_logger as logger
|
|
11
|
+
from ...core.types.tool import BaseTool, ToolContext, ToolResult
|
|
12
|
+
from ...core.types.block import BlockEvent, BlockKind, BlockOp
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ThinkingTool(BaseTool):
|
|
16
|
+
"""Emit thinking/reasoning process.
|
|
17
|
+
|
|
18
|
+
Use this tool to externalize your reasoning:
|
|
19
|
+
- Breaking down complex problems
|
|
20
|
+
- Weighing options and trade-offs
|
|
21
|
+
- Planning approach before acting
|
|
22
|
+
- Explaining decision rationale
|
|
23
|
+
|
|
24
|
+
This creates a THINKING block in the output stream,
|
|
25
|
+
making reasoning visible to users and useful for debugging.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
_name = "thinking"
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def name(self) -> str:
|
|
32
|
+
return self._name
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def description(self) -> str:
|
|
36
|
+
return """Emit your thinking/reasoning process.
|
|
37
|
+
|
|
38
|
+
Use this to externalize your thought process:
|
|
39
|
+
- Analyzing a problem
|
|
40
|
+
- Weighing options
|
|
41
|
+
- Planning an approach
|
|
42
|
+
- Explaining decisions
|
|
43
|
+
|
|
44
|
+
This makes your reasoning visible and traceable."""
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def parameters(self) -> dict[str, Any]:
|
|
48
|
+
return {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"properties": {
|
|
51
|
+
"thought": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "Your thinking/reasoning content",
|
|
54
|
+
},
|
|
55
|
+
"category": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"enum": ["analysis", "planning", "decision", "reflection", "observation"],
|
|
58
|
+
"description": "Category of thinking",
|
|
59
|
+
"default": "analysis",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
"required": ["thought"],
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async def execute(
|
|
66
|
+
self,
|
|
67
|
+
params: dict[str, Any],
|
|
68
|
+
ctx: ToolContext,
|
|
69
|
+
) -> ToolResult:
|
|
70
|
+
"""Execute thinking - emit reasoning block."""
|
|
71
|
+
thought = params.get("thought", "")
|
|
72
|
+
category = params.get("category", "analysis")
|
|
73
|
+
|
|
74
|
+
logger.debug(
|
|
75
|
+
"Agent thinking",
|
|
76
|
+
extra={"category": category, "length": len(thought)},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Emit THINKING block
|
|
80
|
+
await self._emit_thinking_block(ctx, thought, category)
|
|
81
|
+
|
|
82
|
+
# Thinking doesn't produce actionable output
|
|
83
|
+
# It's purely for transparency/logging
|
|
84
|
+
return ToolResult(output=thought)
|
|
85
|
+
|
|
86
|
+
async def _emit_thinking_block(
|
|
87
|
+
self,
|
|
88
|
+
ctx: ToolContext,
|
|
89
|
+
thought: str,
|
|
90
|
+
category: str,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Emit THINKING block."""
|
|
93
|
+
emit = getattr(ctx, 'emit', None)
|
|
94
|
+
if emit is None:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
block = BlockEvent(
|
|
98
|
+
kind=BlockKind.THINKING,
|
|
99
|
+
op=BlockOp.APPLY,
|
|
100
|
+
data={
|
|
101
|
+
"thought": thought,
|
|
102
|
+
"category": category,
|
|
103
|
+
},
|
|
104
|
+
session_id=ctx.session_id,
|
|
105
|
+
invocation_id=ctx.invocation_id,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
await emit(block)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
__all__ = ["ThinkingTool"]
|