makefile-agent 0.4.1__tar.gz → 0.4.3__tar.gz
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.
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/PKG-INFO +1 -1
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/agent.py +35 -11
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_shell/run.py +1 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_shell/shell.py +44 -13
- makefile_agent-0.4.3/make_agent/agent_shell/user_messages.py +110 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/builtin_tools/skill_tools.py +19 -3
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/main.py +53 -6
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/memory/memory.py +15 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/skill_backend.py +11 -3
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/tool_handler/handler.py +20 -4
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/makefile_agent.egg-info/PKG-INFO +1 -1
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/makefile_agent.egg-info/SOURCES.txt +4 -1
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/pyproject.toml +1 -1
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_agent.py +71 -21
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_agent_shell.py +10 -2
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_bridge.py +38 -12
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_builtin_tools.py +63 -16
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_commands.py +2 -2
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_compact.py +159 -75
- makefile_agent-0.4.3/tests/test_enabled_skills.py +210 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_main.py +32 -9
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_memory.py +200 -114
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_middleware.py +29 -7
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_parser.py +54 -14
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_tools.py +13 -2
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_trusted_skill.py +28 -8
- makefile_agent-0.4.3/tests/test_user_messages.py +127 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/LICENSE +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/README.md +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/__init__.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/__init__.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/bridge.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/constants.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/events.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/export.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/loop.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/middleware.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_core/provider.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/agent_shell/__init__.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/app_dirs.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/builtin_tools/__init__.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/builtin_tools/file_tools.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/memory/__init__.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/memory/tools.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/parser.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/protocols.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/provider/__init__.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/provider/anthropic.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/provider/base.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/provider/openai.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/templates/makefile/SYSTEM.md +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/tool_display.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/tool_handler/__init__.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/make_agent/tool_handler/runner.py +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/makefile_agent.egg-info/dependency_links.txt +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/makefile_agent.egg-info/entry_points.txt +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/makefile_agent.egg-info/requires.txt +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/makefile_agent.egg-info/top_level.txt +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/setup.cfg +0 -0
- {makefile_agent-0.4.1 → makefile_agent-0.4.3}/tests/test_app_dirs.py +0 -0
|
@@ -67,7 +67,9 @@ class AgentManager:
|
|
|
67
67
|
middlewares: list[MiddlewareBase] | None = None,
|
|
68
68
|
) -> None:
|
|
69
69
|
self._tool_handler = tool_handler
|
|
70
|
-
self._middlewares: list[MiddlewareBase] =
|
|
70
|
+
self._middlewares: list[MiddlewareBase] = (
|
|
71
|
+
middlewares if middlewares is not None else []
|
|
72
|
+
)
|
|
71
73
|
self._sessions: dict[str, AgenticLoop] = {}
|
|
72
74
|
|
|
73
75
|
@staticmethod
|
|
@@ -103,11 +105,15 @@ class AgentManager:
|
|
|
103
105
|
async for event in self._run_loop(loop, request.message):
|
|
104
106
|
yield event
|
|
105
107
|
|
|
106
|
-
async def _run_loop(
|
|
108
|
+
async def _run_loop(
|
|
109
|
+
self, loop: AgenticLoop, message: str
|
|
110
|
+
) -> AsyncIterator[AgentEvent]:
|
|
107
111
|
"""Iterate one agent turn, translating callbacks to AgentEvents."""
|
|
108
112
|
async for cb in loop.astream(message):
|
|
109
113
|
if isinstance(cb, CompactCallback):
|
|
110
|
-
yield CompactEvent(
|
|
114
|
+
yield CompactEvent(
|
|
115
|
+
attempt=cb.attempt, messages_dropped=cb.messages_dropped
|
|
116
|
+
)
|
|
111
117
|
elif isinstance(cb, TokenCallback):
|
|
112
118
|
yield TokenEvent(text=cb.message)
|
|
113
119
|
elif isinstance(cb, MessageCallback):
|
|
@@ -121,10 +127,14 @@ class AgentManager:
|
|
|
121
127
|
elif isinstance(cb, ToolCallback):
|
|
122
128
|
if cb.tool_name == "execute_skill":
|
|
123
129
|
skill_name = cb.tool_args.get("name", "")
|
|
124
|
-
target = cb.tool_args.get("target") or cb.tool_args.get(
|
|
130
|
+
target = cb.tool_args.get("target") or cb.tool_args.get(
|
|
131
|
+
"command", ""
|
|
132
|
+
)
|
|
125
133
|
if not self._tool_handler.is_skill_trusted(skill_name, target):
|
|
126
134
|
kwargs = cb.tool_args.get("kwargs") or {}
|
|
127
|
-
confirm = ConfirmEvent(
|
|
135
|
+
confirm = ConfirmEvent(
|
|
136
|
+
skill_name=skill_name, target=target, kwargs=kwargs
|
|
137
|
+
)
|
|
128
138
|
yield confirm
|
|
129
139
|
allowed = await confirm.wait()
|
|
130
140
|
if not allowed:
|
|
@@ -137,7 +147,9 @@ class AgentManager:
|
|
|
137
147
|
description=cb.description,
|
|
138
148
|
)
|
|
139
149
|
start_time = time.monotonic()
|
|
140
|
-
result = await self._tool_handler.execute(
|
|
150
|
+
result = await self._tool_handler.execute(
|
|
151
|
+
cb.tool_name, cb.tool_args, loop._max_tool_output
|
|
152
|
+
)
|
|
141
153
|
cb.set_response(result.output, is_error=result.is_error)
|
|
142
154
|
duration_ms = (time.monotonic() - start_time) * 1000
|
|
143
155
|
cb.duration_ms = duration_ms
|
|
@@ -150,7 +162,9 @@ class AgentManager:
|
|
|
150
162
|
|
|
151
163
|
def _build_chain(self) -> Callable[[Request], AsyncIterator[AgentEvent]]:
|
|
152
164
|
"""Build the middleware chain; first middleware in the list is innermost."""
|
|
153
|
-
current: Callable[[Request], AsyncIterator[AgentEvent]] =
|
|
165
|
+
current: Callable[[Request], AsyncIterator[AgentEvent]] = (
|
|
166
|
+
self._stream_events_core
|
|
167
|
+
)
|
|
154
168
|
for mw in self._middlewares:
|
|
155
169
|
prev = current
|
|
156
170
|
|
|
@@ -163,7 +177,9 @@ class AgentManager:
|
|
|
163
177
|
current = make_wrapper(mw, prev)
|
|
164
178
|
return current
|
|
165
179
|
|
|
166
|
-
async def astream_events(
|
|
180
|
+
async def astream_events(
|
|
181
|
+
self, session_id: str, message: str
|
|
182
|
+
) -> AsyncIterator[AgentEvent]:
|
|
167
183
|
"""Stream :class:`AgentEvent` objects for one agent turn.
|
|
168
184
|
|
|
169
185
|
Tool execution and skill confirmation are handled internally.
|
|
@@ -266,14 +282,20 @@ class AgentManager:
|
|
|
266
282
|
if active_turn_task and not active_turn_task.done():
|
|
267
283
|
active_turn_task.cancel()
|
|
268
284
|
else:
|
|
269
|
-
await event_queue.put(
|
|
285
|
+
await event_queue.put(
|
|
286
|
+
ManagerError(message="No active turn to cancel")
|
|
287
|
+
)
|
|
270
288
|
|
|
271
289
|
elif isinstance(cmd, (ApproveSkill, DenySkill)):
|
|
272
290
|
future = pending_approvals.pop(cmd.request_id, None)
|
|
273
291
|
if future and not future.done():
|
|
274
292
|
future.set_result(isinstance(cmd, ApproveSkill))
|
|
275
293
|
else:
|
|
276
|
-
await event_queue.put(
|
|
294
|
+
await event_queue.put(
|
|
295
|
+
ManagerError(
|
|
296
|
+
message=f"Unknown approval request: {cmd.request_id}"
|
|
297
|
+
)
|
|
298
|
+
)
|
|
277
299
|
|
|
278
300
|
async def _execute_bridge_turn(
|
|
279
301
|
self,
|
|
@@ -291,7 +313,9 @@ class AgentManager:
|
|
|
291
313
|
try:
|
|
292
314
|
async for event in self.astream_events(session_id, message):
|
|
293
315
|
if isinstance(event, TokenEvent):
|
|
294
|
-
await event_queue.put(
|
|
316
|
+
await event_queue.put(
|
|
317
|
+
TokenEmitted(turn_id=turn_id, text=event.text)
|
|
318
|
+
)
|
|
295
319
|
elif isinstance(event, ToolStartEvent):
|
|
296
320
|
current_tool_id = str(uuid4())
|
|
297
321
|
await event_queue.put(
|
|
@@ -19,16 +19,6 @@ from pathlib import Path
|
|
|
19
19
|
from typing import Any, Optional
|
|
20
20
|
from uuid import uuid4
|
|
21
21
|
|
|
22
|
-
from prompt_toolkit.application import Application
|
|
23
|
-
from prompt_toolkit.completion import WordCompleter
|
|
24
|
-
from prompt_toolkit.filters import Condition
|
|
25
|
-
from prompt_toolkit.history import FileHistory
|
|
26
|
-
from prompt_toolkit.key_binding import KeyBindings
|
|
27
|
-
from prompt_toolkit.layout import HSplit, Layout, VSplit, Window
|
|
28
|
-
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
29
|
-
from prompt_toolkit.styles import Style
|
|
30
|
-
from prompt_toolkit.widgets import Frame, TextArea
|
|
31
|
-
|
|
32
22
|
from make_agent.agent_core import (
|
|
33
23
|
AgentManager,
|
|
34
24
|
ApprovalRequested,
|
|
@@ -42,14 +32,24 @@ from make_agent.agent_core import (
|
|
|
42
32
|
Shutdown,
|
|
43
33
|
StartTurn,
|
|
44
34
|
StatusChanged,
|
|
35
|
+
TokenEmitted,
|
|
45
36
|
ToolFinished,
|
|
46
37
|
ToolStarted,
|
|
47
|
-
TokenEmitted,
|
|
48
38
|
TurnCancelled,
|
|
49
39
|
TurnFinished,
|
|
50
40
|
TurnStarted,
|
|
51
41
|
)
|
|
42
|
+
from make_agent.memory import Memory
|
|
43
|
+
from prompt_toolkit.application import Application
|
|
44
|
+
from prompt_toolkit.completion import WordCompleter
|
|
45
|
+
from prompt_toolkit.filters import Condition
|
|
46
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
47
|
+
from prompt_toolkit.layout import HSplit, Layout, VSplit, Window
|
|
48
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
49
|
+
from prompt_toolkit.styles import Style
|
|
50
|
+
from prompt_toolkit.widgets import Frame, TextArea
|
|
52
51
|
|
|
52
|
+
from .user_messages import UserMessagesManager
|
|
53
53
|
|
|
54
54
|
# ── status / enums ──────────────────────────────────────────────────────────────
|
|
55
55
|
|
|
@@ -299,6 +299,7 @@ class MakeAgentShell:
|
|
|
299
299
|
session_id: str,
|
|
300
300
|
model: str,
|
|
301
301
|
history_path: Path,
|
|
302
|
+
memory: Optional[Memory] = None,
|
|
302
303
|
) -> None:
|
|
303
304
|
self._agent_manager = agent_manager
|
|
304
305
|
self._session_id = session_id
|
|
@@ -310,6 +311,7 @@ class MakeAgentShell:
|
|
|
310
311
|
self._app: Optional[Application] = None
|
|
311
312
|
self._response_area: Optional[TextArea] = None
|
|
312
313
|
self._tools_area: Optional[TextArea] = None
|
|
314
|
+
self._history_manager = UserMessagesManager(memory) if memory else None
|
|
313
315
|
self._commands: dict[str, Any] = {
|
|
314
316
|
"exit": self._cmd_exit,
|
|
315
317
|
"quit": self._cmd_exit,
|
|
@@ -418,7 +420,6 @@ class MakeAgentShell:
|
|
|
418
420
|
prompt="> ",
|
|
419
421
|
multiline=True,
|
|
420
422
|
completer=completer,
|
|
421
|
-
history=FileHistory(str(self._history_path)),
|
|
422
423
|
wrap_lines=False,
|
|
423
424
|
)
|
|
424
425
|
self._composer_input = composer_input
|
|
@@ -474,7 +475,7 @@ class MakeAgentShell:
|
|
|
474
475
|
else [
|
|
475
476
|
(
|
|
476
477
|
"class:hint",
|
|
477
|
-
" /help /stats /export /exit Alt+Enter newline Ctrl+T transcript",
|
|
478
|
+
" /help /stats /export /exit ↑↓ history Alt+Enter newline Ctrl+T transcript",
|
|
478
479
|
)
|
|
479
480
|
]
|
|
480
481
|
)
|
|
@@ -557,10 +558,15 @@ class MakeAgentShell:
|
|
|
557
558
|
def _on_enter(event) -> None:
|
|
558
559
|
if state.status != AgentStatus.IDLE or state.transcript_focused:
|
|
559
560
|
return
|
|
561
|
+
|
|
560
562
|
text = composer_input.text.strip()
|
|
561
563
|
if not text:
|
|
562
564
|
return
|
|
565
|
+
|
|
563
566
|
composer_input.text = ""
|
|
567
|
+
if self._history_manager is not None:
|
|
568
|
+
self._history_manager.submit() # Reset nav state after sending
|
|
569
|
+
|
|
564
570
|
if text.startswith("/"):
|
|
565
571
|
should_exit = self._dispatch_command(text[1:])
|
|
566
572
|
if should_exit:
|
|
@@ -570,6 +576,31 @@ class MakeAgentShell:
|
|
|
570
576
|
else:
|
|
571
577
|
asyncio.ensure_future(self._run_turn(text))
|
|
572
578
|
|
|
579
|
+
# Only bind up/down for history navigation when composer is focused,
|
|
580
|
+
# idle, and memory is available — so arrow keys still scroll the
|
|
581
|
+
# transcript area when it's focused.
|
|
582
|
+
composer_history_filter = Condition(
|
|
583
|
+
lambda: (
|
|
584
|
+
not state.transcript_focused
|
|
585
|
+
and state.status == AgentStatus.IDLE
|
|
586
|
+
and self._history_manager is not None
|
|
587
|
+
)
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
@kb.add("up", filter=composer_history_filter)
|
|
591
|
+
def _on_up(event) -> None:
|
|
592
|
+
msg = self._history_manager.previous(composer_input.text)
|
|
593
|
+
if msg is not None:
|
|
594
|
+
composer_input.text = msg
|
|
595
|
+
self._refresh()
|
|
596
|
+
|
|
597
|
+
@kb.add("down", filter=composer_history_filter)
|
|
598
|
+
def _on_down(event) -> None:
|
|
599
|
+
msg = self._history_manager.next()
|
|
600
|
+
if msg is not None:
|
|
601
|
+
composer_input.text = msg
|
|
602
|
+
self._refresh()
|
|
603
|
+
|
|
573
604
|
@kb.add("escape", "enter")
|
|
574
605
|
def _on_alt_enter(event) -> None:
|
|
575
606
|
if state.status == AgentStatus.IDLE and not state.transcript_focused:
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""User message history manager for the agent shell.
|
|
2
|
+
|
|
3
|
+
Provides navigation through past user messages backed by persistent Memory.
|
|
4
|
+
The shell interacts with this manager, not Memory directly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from make_agent.memory import Memory
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UserMessagesManager:
|
|
15
|
+
"""Manages user message history for shell navigation.
|
|
16
|
+
|
|
17
|
+
Refreshes messages from Memory on each navigation start so that
|
|
18
|
+
messages from the current session are always included. Supports
|
|
19
|
+
forward/backward navigation with index tracking.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, memory: Memory, limit: int = 200) -> None:
|
|
23
|
+
"""Initialize with a Memory instance.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
memory: The Memory backend to fetch messages from.
|
|
27
|
+
limit: Maximum number of historical messages to load (default: 200).
|
|
28
|
+
"""
|
|
29
|
+
self._memory = memory
|
|
30
|
+
self._limit = limit
|
|
31
|
+
self._messages: list[str] = [] # Refreshed on each nav start, newest first
|
|
32
|
+
self._index: int = -1 # -1 = not navigating; 0 = most recent
|
|
33
|
+
self._original_text: str = "" # Text user had typed before pressing UP
|
|
34
|
+
|
|
35
|
+
def _refresh(self) -> None:
|
|
36
|
+
"""Reload user messages from Memory so current-session messages
|
|
37
|
+
are always visible."""
|
|
38
|
+
self._messages = self._memory.recent_user(self._limit)
|
|
39
|
+
|
|
40
|
+
def start_navigating(self, current_text: str) -> Optional[str]:
|
|
41
|
+
"""Begin navigation from the current composer text.
|
|
42
|
+
|
|
43
|
+
Called when user first presses UP. Reloads messages from Memory
|
|
44
|
+
so current-session messages are included. Saves the current text
|
|
45
|
+
as the restoration point (returned when DOWN exits navigation).
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
current_text: What the user has typed so far.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The most recent past user message, or None if history is empty.
|
|
52
|
+
"""
|
|
53
|
+
self._refresh()
|
|
54
|
+
if not self._messages:
|
|
55
|
+
return None
|
|
56
|
+
self._original_text = current_text
|
|
57
|
+
self._index = 0
|
|
58
|
+
return self._messages[0]
|
|
59
|
+
|
|
60
|
+
def previous(self, current_text: str = "") -> Optional[str]:
|
|
61
|
+
"""Navigate to the next older message.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
current_text: What the user has typed so far (saved as restoration point
|
|
65
|
+
on first UP press).
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The previous message, or None if already at the oldest.
|
|
69
|
+
"""
|
|
70
|
+
if self._index < 0:
|
|
71
|
+
# First press — start navigating, save current text as restoration point
|
|
72
|
+
return self.start_navigating(current_text)
|
|
73
|
+
if self._index < len(self._messages) - 1:
|
|
74
|
+
self._index += 1
|
|
75
|
+
return self._messages[self._index]
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
def next(self) -> Optional[str]:
|
|
79
|
+
"""Navigate to the next newer message or restore original text.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
The next newer message, or the original typed text when
|
|
83
|
+
navigation returns to the start, or None if not navigating.
|
|
84
|
+
"""
|
|
85
|
+
if self._index < 0:
|
|
86
|
+
return None
|
|
87
|
+
if self._index > 0:
|
|
88
|
+
self._index -= 1
|
|
89
|
+
return self._messages[self._index]
|
|
90
|
+
# Back at start — restore original text
|
|
91
|
+
text = self._original_text
|
|
92
|
+
self._index = -1
|
|
93
|
+
self._original_text = ""
|
|
94
|
+
return text
|
|
95
|
+
|
|
96
|
+
def cancel(self) -> None:
|
|
97
|
+
"""Cancel navigation and restore the original typed text."""
|
|
98
|
+
self._index = -1
|
|
99
|
+
|
|
100
|
+
def submit(self) -> None:
|
|
101
|
+
"""Called when the user sends a message (presses Enter).
|
|
102
|
+
|
|
103
|
+
Resets navigation state so the next UP press starts fresh.
|
|
104
|
+
"""
|
|
105
|
+
self._index = -1
|
|
106
|
+
self._original_text = ""
|
|
107
|
+
|
|
108
|
+
def is_navigating(self) -> bool:
|
|
109
|
+
"""Check if the user is currently in navigation mode."""
|
|
110
|
+
return self._index >= 0
|
|
@@ -120,16 +120,31 @@ def _skill_description(mk_path: Path) -> str:
|
|
|
120
120
|
return " (no description)"
|
|
121
121
|
|
|
122
122
|
|
|
123
|
-
def list_skills(skills_dir: str) -> str:
|
|
124
|
-
"""List all available skills with their names and descriptions.
|
|
123
|
+
def list_skills(skills_dir: str, enabled_skills: frozenset[str] | None = None) -> str:
|
|
124
|
+
"""List all available skills with their names and descriptions.
|
|
125
|
+
|
|
126
|
+
If *enabled_skills* is provided and is not ``{"*"}`` or empty, only those
|
|
127
|
+
skill names are shown. When the set contains ``"*"`` or is None, all
|
|
128
|
+
discovered skills are listed (default: show everything).
|
|
129
|
+
"""
|
|
125
130
|
path = Path(skills_dir)
|
|
126
131
|
if not path.exists():
|
|
127
132
|
return "No skills found (directory does not exist)"
|
|
128
|
-
|
|
133
|
+
|
|
134
|
+
all_skill_dirs = sorted(
|
|
129
135
|
p for p in path.iterdir() if p.is_dir() and (p / "skill.mk").exists()
|
|
130
136
|
)
|
|
137
|
+
if not all_skill_dirs:
|
|
138
|
+
return "No skills found"
|
|
139
|
+
|
|
140
|
+
if enabled_skills is not None:
|
|
141
|
+
skill_dirs = [sd for sd in all_skill_dirs if sd.name in enabled_skills]
|
|
142
|
+
else:
|
|
143
|
+
skill_dirs = all_skill_dirs
|
|
144
|
+
|
|
131
145
|
if not skill_dirs:
|
|
132
146
|
return "No skills found"
|
|
147
|
+
|
|
133
148
|
entries = []
|
|
134
149
|
for sd in skill_dirs:
|
|
135
150
|
desc = _skill_description(sd / "skill.mk")
|
|
@@ -168,6 +183,7 @@ def execute_skill(
|
|
|
168
183
|
injected as environment variables; tokens after ``make`` are passed as make
|
|
169
184
|
arguments (targets and/or make-style variable assignments).
|
|
170
185
|
"""
|
|
186
|
+
|
|
171
187
|
if not _valid_skill_name(name):
|
|
172
188
|
return f"Error: invalid skill name {name!r}. Use letters, numbers, hyphens, underscores, and dots only."
|
|
173
189
|
safe_paths = _resolve_safe_skill_path(skills_dir, name, "skill.mk")
|
|
@@ -10,6 +10,7 @@ from make_agent.agent_core import (
|
|
|
10
10
|
DEFAULT_COMPACT_MODE,
|
|
11
11
|
DEFAULT_MAX_TOKENS,
|
|
12
12
|
DEFAULT_MAX_TOOL_OUTPUT,
|
|
13
|
+
DEFAULT_TOOL_TIMEOUT,
|
|
13
14
|
DEFAULT_USE_PROMPT_CACHE,
|
|
14
15
|
)
|
|
15
16
|
from make_agent.agent_shell import run
|
|
@@ -37,7 +38,9 @@ def _init_logging(loglevel: str) -> None:
|
|
|
37
38
|
# Remove any existing root handlers so basicConfig always adds our file handler.
|
|
38
39
|
for h in logging.root.handlers[:]:
|
|
39
40
|
logging.root.removeHandler(h)
|
|
40
|
-
logging.basicConfig(
|
|
41
|
+
logging.basicConfig(
|
|
42
|
+
filename=log_file(), level=level, format="%(asctime)s %(levelname)s %(message)s"
|
|
43
|
+
)
|
|
41
44
|
|
|
42
45
|
|
|
43
46
|
def _resolve_system_prompt(args: argparse.Namespace) -> str:
|
|
@@ -76,7 +79,10 @@ def _parse_disabled_tools(value: str | None, mode: str) -> frozenset[str]:
|
|
|
76
79
|
names = frozenset(name.strip() for name in value.split(",") if name.strip())
|
|
77
80
|
unknown = names - available
|
|
78
81
|
if unknown:
|
|
79
|
-
sys.exit(
|
|
82
|
+
sys.exit(
|
|
83
|
+
"make-agent: unknown built-in tool(s): "
|
|
84
|
+
f"{', '.join(sorted(unknown))}. Valid names for {mode}: {', '.join(sorted(available))}"
|
|
85
|
+
)
|
|
80
86
|
return names
|
|
81
87
|
|
|
82
88
|
|
|
@@ -89,8 +95,36 @@ def _parse_trusted_skills(value: str | None) -> frozenset[str]:
|
|
|
89
95
|
return frozenset(name.strip() for name in value.split(",") if name.strip())
|
|
90
96
|
|
|
91
97
|
|
|
92
|
-
def
|
|
93
|
-
|
|
98
|
+
def _parse_enabled_skills(
|
|
99
|
+
value: str | None, all_names: frozenset[str]
|
|
100
|
+
) -> frozenset[str] | None:
|
|
101
|
+
"""Parse --enabled-skills into a frozenset.
|
|
102
|
+
|
|
103
|
+
Returns None when the user didn't pass the flag (meaning: use all discovered skills).
|
|
104
|
+
When the flag is passed, returns the parsed set. 'all' maps to * (keep all).
|
|
105
|
+
Raises sys.exit on unknown skill names.
|
|
106
|
+
"""
|
|
107
|
+
if not value:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
names = frozenset(name.strip() for name in value.split(",") if name.strip())
|
|
111
|
+
unknown = names - all_names
|
|
112
|
+
if unknown:
|
|
113
|
+
sys.exit(
|
|
114
|
+
"make-agent: unknown skill(s): "
|
|
115
|
+
f"{', '.join(sorted(unknown))}. Valid names: {', '.join(sorted(all_names))}"
|
|
116
|
+
)
|
|
117
|
+
return names
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _discover_skill_names(skills_dir: str) -> list[str]:
|
|
121
|
+
"""Return a sorted list of all discoverable skill names from *skills_dir*."""
|
|
122
|
+
path = Path(skills_dir)
|
|
123
|
+
if not path.exists():
|
|
124
|
+
return []
|
|
125
|
+
return sorted(
|
|
126
|
+
p.name for p in path.iterdir() if p.is_dir() and (p / "skill.mk").exists()
|
|
127
|
+
)
|
|
94
128
|
|
|
95
129
|
|
|
96
130
|
def _cmd_run(args: argparse.Namespace) -> None:
|
|
@@ -112,8 +146,13 @@ def _cmd_run(args: argparse.Namespace) -> None:
|
|
|
112
146
|
else:
|
|
113
147
|
skills_dir = str(default_skills_dir(_SKILL_MODE))
|
|
114
148
|
|
|
149
|
+
all_names = _discover_skill_names(skills_dir)
|
|
150
|
+
enabled_skills = _parse_enabled_skills(args.enabled_skills, frozenset(all_names))
|
|
151
|
+
|
|
115
152
|
memory = Memory(mode_memory_path(_SKILL_MODE))
|
|
116
|
-
backend =
|
|
153
|
+
backend = MakefileSkillBackend(
|
|
154
|
+
skills_dir, DEFAULT_TOOL_TIMEOUT, Path.cwd(), enabled_skills
|
|
155
|
+
)
|
|
117
156
|
trusted_skills = _parse_trusted_skills(getattr(args, "trusted_skills", None))
|
|
118
157
|
tool_handler = ToolHandler(backend, memory, disabled, trusted_skills)
|
|
119
158
|
|
|
@@ -143,7 +182,9 @@ def main() -> None:
|
|
|
143
182
|
subparsers = parser.add_subparsers(dest="command")
|
|
144
183
|
|
|
145
184
|
run_p = subparsers.add_parser("run", help="Start the interactive agent (default)")
|
|
146
|
-
run_p.add_argument(
|
|
185
|
+
run_p.add_argument(
|
|
186
|
+
"--model", default=None, metavar="MODEL", help="litellm model string (required)"
|
|
187
|
+
)
|
|
147
188
|
system_g = run_p.add_mutually_exclusive_group()
|
|
148
189
|
system_g.add_argument(
|
|
149
190
|
"--system",
|
|
@@ -224,6 +265,12 @@ def main() -> None:
|
|
|
224
265
|
metavar="EFFORT",
|
|
225
266
|
help=f"Reasoning effort level ({'/'.join(_REASONING_EFFORT_VALUES)}, default: auto)",
|
|
226
267
|
)
|
|
268
|
+
run_p.add_argument(
|
|
269
|
+
"--enabled-skills",
|
|
270
|
+
default=None,
|
|
271
|
+
metavar="SKILLS",
|
|
272
|
+
help="Comma-separated skill names to enable. By default all discovered skills are enabled.",
|
|
273
|
+
)
|
|
227
274
|
run_p.add_argument(
|
|
228
275
|
"--trusted-skills",
|
|
229
276
|
default=None,
|
|
@@ -216,6 +216,21 @@ class Memory:
|
|
|
216
216
|
"""Search past agent replies using FTS5 via the ``agent_memory`` view."""
|
|
217
217
|
return self._search("agent_memory", query, limit, from_date, to_date)
|
|
218
218
|
|
|
219
|
+
def recent_user(
|
|
220
|
+
self,
|
|
221
|
+
limit: int = 10,
|
|
222
|
+
) -> list[str]:
|
|
223
|
+
"""Return the *limit* most recent user messages as plain strings, newest first.
|
|
224
|
+
|
|
225
|
+
Used by the shell for UP/DOWN arrow navigation through past user messages.
|
|
226
|
+
"""
|
|
227
|
+
conn = self._get_conn()
|
|
228
|
+
rows = conn.execute(
|
|
229
|
+
"SELECT message FROM messages WHERE sender = 'user' ORDER BY id DESC LIMIT ?",
|
|
230
|
+
(limit,),
|
|
231
|
+
).fetchall()
|
|
232
|
+
return [row["message"] for row in rows]
|
|
233
|
+
|
|
219
234
|
def recent(
|
|
220
235
|
self,
|
|
221
236
|
limit: int = 10,
|
|
@@ -11,7 +11,9 @@ from make_agent.builtin_tools.skill_tools import create_skill as create_makefile
|
|
|
11
11
|
from make_agent.builtin_tools.skill_tools import execute_skill as execute_makefile_skill
|
|
12
12
|
from make_agent.builtin_tools.skill_tools import list_skills as list_makefile_skills
|
|
13
13
|
from make_agent.builtin_tools.skill_tools import read_skill as read_makefile_skill
|
|
14
|
-
from make_agent.builtin_tools.skill_tools import
|
|
14
|
+
from make_agent.builtin_tools.skill_tools import (
|
|
15
|
+
validate_skill as validate_makefile_skill,
|
|
16
|
+
)
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
class SkillBackend(Protocol):
|
|
@@ -30,14 +32,20 @@ class MakefileSkillBackend:
|
|
|
30
32
|
skills_dir: str,
|
|
31
33
|
tool_timeout: int = 600,
|
|
32
34
|
base_dir: Path | None = None,
|
|
35
|
+
enabled_skills: frozenset[str] | None = None,
|
|
33
36
|
) -> None:
|
|
34
37
|
self._skills_dir = skills_dir
|
|
35
38
|
self._tool_timeout = tool_timeout
|
|
36
39
|
self._base_dir = base_dir if base_dir is not None else Path.cwd()
|
|
40
|
+
self._enabled_skills = enabled_skills
|
|
37
41
|
self._schemas = MAKEFILE_SKILL_SCHEMAS + FILE_SCHEMAS
|
|
38
42
|
self._executors: dict[str, Any] = {
|
|
39
|
-
"list_skills": lambda **_kw: list_makefile_skills(
|
|
40
|
-
|
|
43
|
+
"list_skills": lambda **_kw: list_makefile_skills(
|
|
44
|
+
self._skills_dir, self._enabled_skills
|
|
45
|
+
),
|
|
46
|
+
"read_skill": lambda name, **_kw: read_makefile_skill(
|
|
47
|
+
name, self._skills_dir
|
|
48
|
+
),
|
|
41
49
|
"execute_skill": lambda name, command, **_kw: execute_makefile_skill(
|
|
42
50
|
name,
|
|
43
51
|
command,
|
|
@@ -26,12 +26,28 @@ class ToolHandler:
|
|
|
26
26
|
disabled: frozenset[str] = frozenset(),
|
|
27
27
|
trusted_skills: frozenset[str] = frozenset(),
|
|
28
28
|
) -> None:
|
|
29
|
-
active_backend_schemas = [
|
|
30
|
-
|
|
29
|
+
active_backend_schemas = [
|
|
30
|
+
schema
|
|
31
|
+
for schema in backend.schemas
|
|
32
|
+
if schema["function"]["name"] not in disabled
|
|
33
|
+
]
|
|
34
|
+
active_memory_schemas = [
|
|
35
|
+
schema
|
|
36
|
+
for schema in MEMORY_SCHEMAS
|
|
37
|
+
if schema["function"]["name"] not in disabled
|
|
38
|
+
]
|
|
31
39
|
self._schemas: list[dict] = active_backend_schemas + active_memory_schemas
|
|
32
40
|
self._executors: dict[str, Any] = {
|
|
33
|
-
**{
|
|
34
|
-
|
|
41
|
+
**{
|
|
42
|
+
name: executor
|
|
43
|
+
for name, executor in backend.executors.items()
|
|
44
|
+
if name not in disabled
|
|
45
|
+
},
|
|
46
|
+
**{
|
|
47
|
+
name: executor
|
|
48
|
+
for name, executor in get_memory_executors(memory).items()
|
|
49
|
+
if name not in disabled
|
|
50
|
+
},
|
|
35
51
|
}
|
|
36
52
|
self._backend = backend
|
|
37
53
|
self._trusted_skills = trusted_skills
|
|
@@ -20,6 +20,7 @@ make_agent/agent_core/provider.py
|
|
|
20
20
|
make_agent/agent_shell/__init__.py
|
|
21
21
|
make_agent/agent_shell/run.py
|
|
22
22
|
make_agent/agent_shell/shell.py
|
|
23
|
+
make_agent/agent_shell/user_messages.py
|
|
23
24
|
make_agent/builtin_tools/__init__.py
|
|
24
25
|
make_agent/builtin_tools/file_tools.py
|
|
25
26
|
make_agent/builtin_tools/skill_tools.py
|
|
@@ -47,9 +48,11 @@ tests/test_bridge.py
|
|
|
47
48
|
tests/test_builtin_tools.py
|
|
48
49
|
tests/test_commands.py
|
|
49
50
|
tests/test_compact.py
|
|
51
|
+
tests/test_enabled_skills.py
|
|
50
52
|
tests/test_main.py
|
|
51
53
|
tests/test_memory.py
|
|
52
54
|
tests/test_middleware.py
|
|
53
55
|
tests/test_parser.py
|
|
54
56
|
tests/test_tools.py
|
|
55
|
-
tests/test_trusted_skill.py
|
|
57
|
+
tests/test_trusted_skill.py
|
|
58
|
+
tests/test_user_messages.py
|