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.
Files changed (149) hide show
  1. aury/__init__.py +2 -0
  2. aury/agents/__init__.py +55 -0
  3. aury/agents/a2a/__init__.py +168 -0
  4. aury/agents/backends/__init__.py +196 -0
  5. aury/agents/backends/artifact/__init__.py +9 -0
  6. aury/agents/backends/artifact/memory.py +130 -0
  7. aury/agents/backends/artifact/types.py +133 -0
  8. aury/agents/backends/code/__init__.py +65 -0
  9. aury/agents/backends/file/__init__.py +11 -0
  10. aury/agents/backends/file/local.py +66 -0
  11. aury/agents/backends/file/types.py +40 -0
  12. aury/agents/backends/invocation/__init__.py +8 -0
  13. aury/agents/backends/invocation/memory.py +81 -0
  14. aury/agents/backends/invocation/types.py +110 -0
  15. aury/agents/backends/memory/__init__.py +8 -0
  16. aury/agents/backends/memory/memory.py +179 -0
  17. aury/agents/backends/memory/types.py +136 -0
  18. aury/agents/backends/message/__init__.py +9 -0
  19. aury/agents/backends/message/memory.py +122 -0
  20. aury/agents/backends/message/types.py +124 -0
  21. aury/agents/backends/sandbox.py +275 -0
  22. aury/agents/backends/session/__init__.py +8 -0
  23. aury/agents/backends/session/memory.py +93 -0
  24. aury/agents/backends/session/types.py +124 -0
  25. aury/agents/backends/shell/__init__.py +11 -0
  26. aury/agents/backends/shell/local.py +110 -0
  27. aury/agents/backends/shell/types.py +55 -0
  28. aury/agents/backends/shell.py +209 -0
  29. aury/agents/backends/snapshot/__init__.py +19 -0
  30. aury/agents/backends/snapshot/git.py +95 -0
  31. aury/agents/backends/snapshot/hybrid.py +125 -0
  32. aury/agents/backends/snapshot/memory.py +86 -0
  33. aury/agents/backends/snapshot/types.py +59 -0
  34. aury/agents/backends/state/__init__.py +29 -0
  35. aury/agents/backends/state/composite.py +49 -0
  36. aury/agents/backends/state/file.py +57 -0
  37. aury/agents/backends/state/memory.py +52 -0
  38. aury/agents/backends/state/sqlite.py +262 -0
  39. aury/agents/backends/state/types.py +178 -0
  40. aury/agents/backends/subagent/__init__.py +165 -0
  41. aury/agents/cli/__init__.py +41 -0
  42. aury/agents/cli/chat.py +239 -0
  43. aury/agents/cli/config.py +236 -0
  44. aury/agents/cli/extensions.py +460 -0
  45. aury/agents/cli/main.py +189 -0
  46. aury/agents/cli/session.py +337 -0
  47. aury/agents/cli/workflow.py +276 -0
  48. aury/agents/context_providers/__init__.py +66 -0
  49. aury/agents/context_providers/artifact.py +299 -0
  50. aury/agents/context_providers/base.py +177 -0
  51. aury/agents/context_providers/memory.py +70 -0
  52. aury/agents/context_providers/message.py +130 -0
  53. aury/agents/context_providers/skill.py +50 -0
  54. aury/agents/context_providers/subagent.py +46 -0
  55. aury/agents/context_providers/tool.py +68 -0
  56. aury/agents/core/__init__.py +83 -0
  57. aury/agents/core/base.py +573 -0
  58. aury/agents/core/context.py +797 -0
  59. aury/agents/core/context_builder.py +303 -0
  60. aury/agents/core/event_bus/__init__.py +15 -0
  61. aury/agents/core/event_bus/bus.py +203 -0
  62. aury/agents/core/factory.py +169 -0
  63. aury/agents/core/isolator.py +97 -0
  64. aury/agents/core/logging.py +95 -0
  65. aury/agents/core/parallel.py +194 -0
  66. aury/agents/core/runner.py +139 -0
  67. aury/agents/core/services/__init__.py +5 -0
  68. aury/agents/core/services/file_session.py +144 -0
  69. aury/agents/core/services/message.py +53 -0
  70. aury/agents/core/services/session.py +53 -0
  71. aury/agents/core/signals.py +109 -0
  72. aury/agents/core/state.py +363 -0
  73. aury/agents/core/types/__init__.py +107 -0
  74. aury/agents/core/types/action.py +176 -0
  75. aury/agents/core/types/artifact.py +135 -0
  76. aury/agents/core/types/block.py +736 -0
  77. aury/agents/core/types/message.py +350 -0
  78. aury/agents/core/types/recall.py +144 -0
  79. aury/agents/core/types/session.py +257 -0
  80. aury/agents/core/types/subagent.py +154 -0
  81. aury/agents/core/types/tool.py +205 -0
  82. aury/agents/eval/__init__.py +331 -0
  83. aury/agents/hitl/__init__.py +57 -0
  84. aury/agents/hitl/ask_user.py +242 -0
  85. aury/agents/hitl/compaction.py +230 -0
  86. aury/agents/hitl/exceptions.py +87 -0
  87. aury/agents/hitl/permission.py +617 -0
  88. aury/agents/hitl/revert.py +216 -0
  89. aury/agents/llm/__init__.py +31 -0
  90. aury/agents/llm/adapter.py +367 -0
  91. aury/agents/llm/openai.py +294 -0
  92. aury/agents/llm/provider.py +476 -0
  93. aury/agents/mcp/__init__.py +153 -0
  94. aury/agents/memory/__init__.py +46 -0
  95. aury/agents/memory/compaction.py +394 -0
  96. aury/agents/memory/manager.py +465 -0
  97. aury/agents/memory/processor.py +177 -0
  98. aury/agents/memory/store.py +187 -0
  99. aury/agents/memory/types.py +137 -0
  100. aury/agents/messages/__init__.py +40 -0
  101. aury/agents/messages/config.py +47 -0
  102. aury/agents/messages/raw_store.py +224 -0
  103. aury/agents/messages/store.py +118 -0
  104. aury/agents/messages/types.py +88 -0
  105. aury/agents/middleware/__init__.py +31 -0
  106. aury/agents/middleware/base.py +341 -0
  107. aury/agents/middleware/chain.py +342 -0
  108. aury/agents/middleware/message.py +129 -0
  109. aury/agents/middleware/message_container.py +126 -0
  110. aury/agents/middleware/raw_message.py +153 -0
  111. aury/agents/middleware/truncation.py +139 -0
  112. aury/agents/middleware/types.py +81 -0
  113. aury/agents/plugin.py +162 -0
  114. aury/agents/react/__init__.py +4 -0
  115. aury/agents/react/agent.py +1923 -0
  116. aury/agents/sandbox/__init__.py +23 -0
  117. aury/agents/sandbox/local.py +239 -0
  118. aury/agents/sandbox/remote.py +200 -0
  119. aury/agents/sandbox/types.py +115 -0
  120. aury/agents/skill/__init__.py +16 -0
  121. aury/agents/skill/loader.py +180 -0
  122. aury/agents/skill/types.py +83 -0
  123. aury/agents/tool/__init__.py +39 -0
  124. aury/agents/tool/builtin/__init__.py +23 -0
  125. aury/agents/tool/builtin/ask_user.py +155 -0
  126. aury/agents/tool/builtin/bash.py +107 -0
  127. aury/agents/tool/builtin/delegate.py +726 -0
  128. aury/agents/tool/builtin/edit.py +121 -0
  129. aury/agents/tool/builtin/plan.py +277 -0
  130. aury/agents/tool/builtin/read.py +91 -0
  131. aury/agents/tool/builtin/thinking.py +111 -0
  132. aury/agents/tool/builtin/yield_result.py +130 -0
  133. aury/agents/tool/decorator.py +252 -0
  134. aury/agents/tool/set.py +204 -0
  135. aury/agents/usage/__init__.py +12 -0
  136. aury/agents/usage/tracker.py +236 -0
  137. aury/agents/workflow/__init__.py +85 -0
  138. aury/agents/workflow/adapter.py +268 -0
  139. aury/agents/workflow/dag.py +116 -0
  140. aury/agents/workflow/dsl.py +575 -0
  141. aury/agents/workflow/executor.py +659 -0
  142. aury/agents/workflow/expression.py +136 -0
  143. aury/agents/workflow/parser.py +182 -0
  144. aury/agents/workflow/state.py +145 -0
  145. aury/agents/workflow/types.py +86 -0
  146. aury_agent-0.0.4.dist-info/METADATA +90 -0
  147. aury_agent-0.0.4.dist-info/RECORD +149 -0
  148. aury_agent-0.0.4.dist-info/WHEEL +4 -0
  149. 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"]