kimi-cli 0.35__py3-none-any.whl → 0.52__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.
- kimi_cli/CHANGELOG.md +165 -0
- kimi_cli/__init__.py +0 -374
- kimi_cli/agents/{koder → default}/agent.yaml +1 -1
- kimi_cli/agents/{koder → default}/system.md +1 -1
- kimi_cli/agentspec.py +115 -0
- kimi_cli/app.py +208 -0
- kimi_cli/cli.py +321 -0
- kimi_cli/config.py +33 -16
- kimi_cli/constant.py +4 -0
- kimi_cli/exception.py +16 -0
- kimi_cli/llm.py +144 -3
- kimi_cli/metadata.py +6 -69
- kimi_cli/prompts/__init__.py +4 -0
- kimi_cli/session.py +103 -0
- kimi_cli/soul/__init__.py +130 -9
- kimi_cli/soul/agent.py +159 -0
- kimi_cli/soul/approval.py +5 -6
- kimi_cli/soul/compaction.py +106 -0
- kimi_cli/soul/context.py +1 -1
- kimi_cli/soul/kimisoul.py +180 -80
- kimi_cli/soul/message.py +6 -6
- kimi_cli/soul/runtime.py +96 -0
- kimi_cli/soul/toolset.py +3 -2
- kimi_cli/tools/__init__.py +35 -31
- kimi_cli/tools/bash/__init__.py +25 -9
- kimi_cli/tools/bash/cmd.md +31 -0
- kimi_cli/tools/dmail/__init__.py +5 -4
- kimi_cli/tools/file/__init__.py +8 -0
- kimi_cli/tools/file/glob.md +1 -1
- kimi_cli/tools/file/glob.py +4 -4
- kimi_cli/tools/file/grep.py +36 -19
- kimi_cli/tools/file/patch.py +52 -10
- kimi_cli/tools/file/read.py +6 -5
- kimi_cli/tools/file/replace.py +16 -4
- kimi_cli/tools/file/write.py +16 -4
- kimi_cli/tools/mcp.py +7 -4
- kimi_cli/tools/task/__init__.py +60 -41
- kimi_cli/tools/task/task.md +1 -1
- kimi_cli/tools/todo/__init__.py +4 -2
- kimi_cli/tools/utils.py +1 -1
- kimi_cli/tools/web/fetch.py +2 -1
- kimi_cli/tools/web/search.py +13 -12
- kimi_cli/ui/__init__.py +0 -68
- kimi_cli/ui/acp/__init__.py +67 -38
- kimi_cli/ui/print/__init__.py +46 -69
- kimi_cli/ui/shell/__init__.py +145 -154
- kimi_cli/ui/shell/console.py +27 -1
- kimi_cli/ui/shell/debug.py +187 -0
- kimi_cli/ui/shell/keyboard.py +183 -0
- kimi_cli/ui/shell/metacmd.py +34 -81
- kimi_cli/ui/shell/prompt.py +245 -28
- kimi_cli/ui/shell/replay.py +104 -0
- kimi_cli/ui/shell/setup.py +19 -19
- kimi_cli/ui/shell/update.py +11 -5
- kimi_cli/ui/shell/visualize.py +576 -0
- kimi_cli/ui/wire/README.md +109 -0
- kimi_cli/ui/wire/__init__.py +340 -0
- kimi_cli/ui/wire/jsonrpc.py +48 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +10 -0
- kimi_cli/utils/changelog.py +6 -2
- kimi_cli/utils/clipboard.py +10 -0
- kimi_cli/utils/message.py +15 -1
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/markdown.py +959 -0
- kimi_cli/utils/rich/markdown_sample.md +108 -0
- kimi_cli/utils/rich/markdown_sample_short.md +2 -0
- kimi_cli/utils/signals.py +41 -0
- kimi_cli/utils/string.py +8 -0
- kimi_cli/utils/term.py +114 -0
- kimi_cli/wire/__init__.py +73 -0
- kimi_cli/wire/message.py +191 -0
- kimi_cli-0.52.dist-info/METADATA +186 -0
- kimi_cli-0.52.dist-info/RECORD +99 -0
- kimi_cli-0.52.dist-info/entry_points.txt +3 -0
- kimi_cli/agent.py +0 -261
- kimi_cli/agents/koder/README.md +0 -3
- kimi_cli/prompts/metacmds/__init__.py +0 -4
- kimi_cli/soul/wire.py +0 -101
- kimi_cli/ui/shell/liveview.py +0 -158
- kimi_cli/utils/provider.py +0 -64
- kimi_cli-0.35.dist-info/METADATA +0 -24
- kimi_cli-0.35.dist-info/RECORD +0 -76
- kimi_cli-0.35.dist-info/entry_points.txt +0 -3
- /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
- /kimi_cli/prompts/{metacmds/compact.md → compact.md} +0 -0
- /kimi_cli/prompts/{metacmds/init.md → init.md} +0 -0
- {kimi_cli-0.35.dist-info → kimi_cli-0.52.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Markdown Sample Document
|
|
2
|
+
|
|
3
|
+
This is a comprehensive sample document showcasing various Markdown elements.
|
|
4
|
+
|
|
5
|
+
## Level 2 Heading
|
|
6
|
+
|
|
7
|
+
### Level 3 Heading
|
|
8
|
+
|
|
9
|
+
Here's some regular text with **bold text**, *italic text*, and `inline code`.
|
|
10
|
+
|
|
11
|
+
## Lists
|
|
12
|
+
|
|
13
|
+
### Unordered List
|
|
14
|
+
|
|
15
|
+
- First item
|
|
16
|
+
- Second item
|
|
17
|
+
- Nested item 1
|
|
18
|
+
- Nested item 2
|
|
19
|
+
- Third item
|
|
20
|
+
|
|
21
|
+
### Ordered List
|
|
22
|
+
|
|
23
|
+
1. First step
|
|
24
|
+
2. Second step
|
|
25
|
+
1. Sub-step A
|
|
26
|
+
2. Sub-step B
|
|
27
|
+
3. Third step
|
|
28
|
+
|
|
29
|
+
### Mixed List
|
|
30
|
+
|
|
31
|
+
1. First item
|
|
32
|
+
- Sub-item with bullet
|
|
33
|
+
- Another sub-item
|
|
34
|
+
2. Second item
|
|
35
|
+
1. Numbered sub-item
|
|
36
|
+
2. Another numbered sub-item
|
|
37
|
+
|
|
38
|
+
## Links and References
|
|
39
|
+
|
|
40
|
+
Here's a [link to GitHub](https://github.com) and another [relative link](../README.md).
|
|
41
|
+
|
|
42
|
+
## Code Blocks
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
def hello_world():
|
|
46
|
+
"""A simple function to demonstrate code blocks."""
|
|
47
|
+
print("Hello, World!")
|
|
48
|
+
return 42
|
|
49
|
+
|
|
50
|
+
# Call the function
|
|
51
|
+
result = hello_world()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Bash example
|
|
56
|
+
echo "This is a bash script"
|
|
57
|
+
ls -la /tmp
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Blockquotes
|
|
61
|
+
|
|
62
|
+
> This is a blockquote.
|
|
63
|
+
> It can span multiple lines.
|
|
64
|
+
>
|
|
65
|
+
> > And it can be nested too!
|
|
66
|
+
|
|
67
|
+
## Tables
|
|
68
|
+
|
|
69
|
+
| Column 1 | Column 2 | Column 3 |
|
|
70
|
+
|----------|----------|----------|
|
|
71
|
+
| Cell 1 | Cell 2 | Cell 3 |
|
|
72
|
+
| Left | Center | Right |
|
|
73
|
+
| Foo | Bar | Baz |
|
|
74
|
+
|
|
75
|
+
## Horizontal Rules
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
Here's some text after a horizontal rule.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Inline Formatting
|
|
84
|
+
|
|
85
|
+
You can combine **bold and *italic*** text, or use `code` within paragraphs.
|
|
86
|
+
|
|
87
|
+
**Important**: Always test your `code` snippets before deployment.
|
|
88
|
+
|
|
89
|
+
## Advanced Features
|
|
90
|
+
|
|
91
|
+
### Task Lists
|
|
92
|
+
|
|
93
|
+
- [x] Completed task
|
|
94
|
+
- [ ] Pending task
|
|
95
|
+
- [ ] Another pending task
|
|
96
|
+
|
|
97
|
+
### Definition Lists
|
|
98
|
+
|
|
99
|
+
Term 1
|
|
100
|
+
: Definition of term 1
|
|
101
|
+
|
|
102
|
+
Term 2
|
|
103
|
+
: Definition of term 2
|
|
104
|
+
: Another definition for term 2
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
*This document demonstrates comprehensive Markdown formatting capabilities.*
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import signal
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def install_sigint_handler(
|
|
8
|
+
loop: asyncio.AbstractEventLoop, handler: Callable[[], None]
|
|
9
|
+
) -> Callable[[], None]:
|
|
10
|
+
"""
|
|
11
|
+
Install a SIGINT handler that works on Unix and Windows.
|
|
12
|
+
|
|
13
|
+
On Unix event loops, prefer `loop.add_signal_handler`.
|
|
14
|
+
On Windows (or other platforms) where it is not implemented, fall back to
|
|
15
|
+
`signal.signal`. The fallback cannot be removed from the loop, but we
|
|
16
|
+
restore the previous handler on uninstall.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
A function that removes the installed handler. It is guaranteed that
|
|
20
|
+
no exceptions are raised when calling the returned function.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
loop.add_signal_handler(signal.SIGINT, handler)
|
|
25
|
+
|
|
26
|
+
def remove() -> None:
|
|
27
|
+
with contextlib.suppress(RuntimeError):
|
|
28
|
+
loop.remove_signal_handler(signal.SIGINT)
|
|
29
|
+
|
|
30
|
+
return remove
|
|
31
|
+
except RuntimeError:
|
|
32
|
+
# Windows ProactorEventLoop and some environments do not support
|
|
33
|
+
# add_signal_handler. Use synchronous signal handling as a fallback.
|
|
34
|
+
previous = signal.getsignal(signal.SIGINT)
|
|
35
|
+
signal.signal(signal.SIGINT, lambda signum, frame: handler())
|
|
36
|
+
|
|
37
|
+
def remove() -> None:
|
|
38
|
+
with contextlib.suppress(RuntimeError):
|
|
39
|
+
signal.signal(signal.SIGINT, previous)
|
|
40
|
+
|
|
41
|
+
return remove
|
kimi_cli/utils/string.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import random
|
|
1
2
|
import re
|
|
3
|
+
import string
|
|
2
4
|
|
|
3
5
|
_NEWLINE_RE = re.compile(r"[\r\n]+")
|
|
4
6
|
|
|
@@ -10,3 +12,9 @@ def shorten_middle(text: str, width: int, remove_newline: bool = True) -> str:
|
|
|
10
12
|
if remove_newline:
|
|
11
13
|
text = _NEWLINE_RE.sub(" ", text)
|
|
12
14
|
return text[: width // 2] + "..." + text[-width // 2 :]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def random_string(length: int = 8) -> str:
|
|
18
|
+
"""Generate a random string of fixed length."""
|
|
19
|
+
letters = string.ascii_lowercase
|
|
20
|
+
return "".join(random.choice(letters) for _ in range(length))
|
kimi_cli/utils/term.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def ensure_new_line() -> None:
|
|
8
|
+
"""Ensure the next prompt starts at column 0 regardless of prior command output."""
|
|
9
|
+
|
|
10
|
+
if not sys.stdout.isatty() or not sys.stdin.isatty():
|
|
11
|
+
return
|
|
12
|
+
|
|
13
|
+
needs_break = True
|
|
14
|
+
if sys.platform == "win32":
|
|
15
|
+
column = _cursor_column_windows()
|
|
16
|
+
needs_break = column not in (None, 0)
|
|
17
|
+
else:
|
|
18
|
+
column = _cursor_column_unix()
|
|
19
|
+
needs_break = column not in (None, 1)
|
|
20
|
+
|
|
21
|
+
if needs_break:
|
|
22
|
+
_write_newline()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _cursor_column_unix() -> int | None:
|
|
26
|
+
assert sys.platform != "win32"
|
|
27
|
+
|
|
28
|
+
import select
|
|
29
|
+
import termios
|
|
30
|
+
import tty
|
|
31
|
+
|
|
32
|
+
_CURSOR_QUERY = "\x1b[6n"
|
|
33
|
+
_CURSOR_POSITION_RE = re.compile(r"\x1b\[(\d+);(\d+)R")
|
|
34
|
+
|
|
35
|
+
fd = sys.stdin.fileno()
|
|
36
|
+
oldterm = termios.tcgetattr(fd)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
tty.setcbreak(fd)
|
|
40
|
+
sys.stdout.write(_CURSOR_QUERY)
|
|
41
|
+
sys.stdout.flush()
|
|
42
|
+
|
|
43
|
+
response = ""
|
|
44
|
+
deadline = time.monotonic() + 0.2
|
|
45
|
+
while time.monotonic() < deadline:
|
|
46
|
+
timeout = max(0.01, deadline - time.monotonic())
|
|
47
|
+
ready, _, _ = select.select([sys.stdin], [], [], timeout)
|
|
48
|
+
if not ready:
|
|
49
|
+
continue
|
|
50
|
+
try:
|
|
51
|
+
chunk = os.read(fd, 32)
|
|
52
|
+
except OSError:
|
|
53
|
+
break
|
|
54
|
+
if not chunk:
|
|
55
|
+
break
|
|
56
|
+
response += chunk.decode(encoding="utf-8", errors="ignore")
|
|
57
|
+
match = _CURSOR_POSITION_RE.search(response)
|
|
58
|
+
if match:
|
|
59
|
+
return int(match.group(2))
|
|
60
|
+
finally:
|
|
61
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, oldterm)
|
|
62
|
+
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _cursor_column_windows() -> int | None:
|
|
67
|
+
assert sys.platform == "win32"
|
|
68
|
+
|
|
69
|
+
import ctypes
|
|
70
|
+
from ctypes import wintypes
|
|
71
|
+
|
|
72
|
+
kernel32 = ctypes.windll.kernel32
|
|
73
|
+
_STD_OUTPUT_HANDLE = -11 # Windows API constant for standard output handle
|
|
74
|
+
handle = kernel32.GetStdHandle(_STD_OUTPUT_HANDLE)
|
|
75
|
+
invalid_handle_value = ctypes.c_void_p(-1).value
|
|
76
|
+
if handle in (0, invalid_handle_value):
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
class COORD(ctypes.Structure):
|
|
80
|
+
_fields_ = [("X", wintypes.SHORT), ("Y", wintypes.SHORT)]
|
|
81
|
+
|
|
82
|
+
class SMALL_RECT(ctypes.Structure):
|
|
83
|
+
_fields_ = [
|
|
84
|
+
("Left", wintypes.SHORT),
|
|
85
|
+
("Top", wintypes.SHORT),
|
|
86
|
+
("Right", wintypes.SHORT),
|
|
87
|
+
("Bottom", wintypes.SHORT),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
|
|
91
|
+
_fields_ = [
|
|
92
|
+
("dwSize", COORD),
|
|
93
|
+
("dwCursorPosition", COORD),
|
|
94
|
+
("wAttributes", wintypes.WORD),
|
|
95
|
+
("srWindow", SMALL_RECT),
|
|
96
|
+
("dwMaximumWindowSize", COORD),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
csbi = CONSOLE_SCREEN_BUFFER_INFO()
|
|
100
|
+
if not kernel32.GetConsoleScreenBufferInfo(handle, ctypes.byref(csbi)):
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
return int(csbi.dwCursorPosition.X)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _write_newline() -> None:
|
|
107
|
+
sys.stdout.write("\n")
|
|
108
|
+
sys.stdout.flush()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
print("test", end="", flush=True)
|
|
113
|
+
ensure_new_line()
|
|
114
|
+
print("next line")
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from kosong.message import ContentPart, ToolCallPart
|
|
4
|
+
|
|
5
|
+
from kimi_cli.utils.logging import logger
|
|
6
|
+
from kimi_cli.wire.message import WireMessage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Wire:
|
|
10
|
+
"""
|
|
11
|
+
A channel for communication between the soul and the UI during a soul run.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self._queue = asyncio.Queue[WireMessage]()
|
|
16
|
+
self._soul_side = WireSoulSide(self._queue)
|
|
17
|
+
self._ui_side = WireUISide(self._queue)
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def soul_side(self) -> "WireSoulSide":
|
|
21
|
+
return self._soul_side
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def ui_side(self) -> "WireUISide":
|
|
25
|
+
return self._ui_side
|
|
26
|
+
|
|
27
|
+
def shutdown(self) -> None:
|
|
28
|
+
logger.debug("Shutting down wire")
|
|
29
|
+
self._queue.shutdown()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WireSoulSide:
|
|
33
|
+
"""
|
|
34
|
+
The soul side of a wire.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, queue: asyncio.Queue[WireMessage]):
|
|
38
|
+
self._queue = queue
|
|
39
|
+
|
|
40
|
+
def send(self, msg: WireMessage) -> None:
|
|
41
|
+
if not isinstance(msg, ContentPart | ToolCallPart):
|
|
42
|
+
logger.debug("Sending wire message: {msg}", msg=msg)
|
|
43
|
+
try:
|
|
44
|
+
self._queue.put_nowait(msg)
|
|
45
|
+
except asyncio.QueueShutDown:
|
|
46
|
+
logger.info("Failed to send wire message, queue is shut down: {msg}", msg=msg)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class WireUISide:
|
|
50
|
+
"""
|
|
51
|
+
The UI side of a wire.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, queue: asyncio.Queue[WireMessage]):
|
|
55
|
+
self._queue = queue
|
|
56
|
+
|
|
57
|
+
async def receive(self) -> WireMessage:
|
|
58
|
+
msg = await self._queue.get()
|
|
59
|
+
if not isinstance(msg, ContentPart | ToolCallPart):
|
|
60
|
+
logger.debug("Receiving wire message: {msg}", msg=msg)
|
|
61
|
+
return msg
|
|
62
|
+
|
|
63
|
+
def receive_nowait(self) -> WireMessage | None:
|
|
64
|
+
"""
|
|
65
|
+
Try receive a message without waiting. If no message is available, return None.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
msg = self._queue.get_nowait()
|
|
69
|
+
except asyncio.QueueEmpty:
|
|
70
|
+
return None
|
|
71
|
+
if not isinstance(msg, ContentPart | ToolCallPart):
|
|
72
|
+
logger.debug("Receiving wire message: {msg}", msg=msg)
|
|
73
|
+
return msg
|
kimi_cli/wire/message.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import uuid
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
6
|
+
|
|
7
|
+
from kosong.message import ContentPart, ToolCall, ToolCallPart
|
|
8
|
+
from kosong.tooling import ToolOk, ToolResult
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from kimi_cli.soul import StatusSnapshot
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StepBegin(NamedTuple):
|
|
15
|
+
n: int
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StepInterrupted:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CompactionBegin:
|
|
23
|
+
"""
|
|
24
|
+
Indicates that a compaction just began.
|
|
25
|
+
This event must be sent during a step, which means, between `StepBegin` and `StepInterrupted`.
|
|
26
|
+
And, there must be a `CompactionEnd` directly following this event.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CompactionEnd:
|
|
33
|
+
"""
|
|
34
|
+
Indicates that a compaction just ended.
|
|
35
|
+
This event must be sent directly after a `CompactionBegin` event.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class StatusUpdate(NamedTuple):
|
|
42
|
+
status: "StatusSnapshot"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SubagentEvent(NamedTuple):
|
|
46
|
+
task_tool_call_id: str
|
|
47
|
+
event: "Event"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
type ControlFlowEvent = StepBegin | StepInterrupted | CompactionBegin | CompactionEnd | StatusUpdate
|
|
51
|
+
type Event = ControlFlowEvent | ContentPart | ToolCall | ToolCallPart | ToolResult | SubagentEvent
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ApprovalResponse(Enum):
|
|
55
|
+
APPROVE = "approve"
|
|
56
|
+
APPROVE_FOR_SESSION = "approve_for_session"
|
|
57
|
+
REJECT = "reject"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ApprovalRequest:
|
|
61
|
+
def __init__(self, tool_call_id: str, sender: str, action: str, description: str):
|
|
62
|
+
self.id = str(uuid.uuid4())
|
|
63
|
+
self.tool_call_id = tool_call_id
|
|
64
|
+
self.sender = sender
|
|
65
|
+
self.action = action
|
|
66
|
+
self.description = description
|
|
67
|
+
self._future = asyncio.Future[ApprovalResponse]()
|
|
68
|
+
|
|
69
|
+
def __repr__(self) -> str:
|
|
70
|
+
return (
|
|
71
|
+
f"ApprovalRequest(id={self.id}, tool_call_id={self.tool_call_id}, "
|
|
72
|
+
f"sender={self.sender}, action={self.action}, description={self.description})"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
async def wait(self) -> ApprovalResponse:
|
|
76
|
+
"""
|
|
77
|
+
Wait for the request to be resolved or cancelled.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
ApprovalResponse: The response to the approval request.
|
|
81
|
+
"""
|
|
82
|
+
return await self._future
|
|
83
|
+
|
|
84
|
+
def resolve(self, response: ApprovalResponse) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Resolve the approval request with the given response.
|
|
87
|
+
This will cause the `wait()` method to return the response.
|
|
88
|
+
"""
|
|
89
|
+
self._future.set_result(response)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def resolved(self) -> bool:
|
|
93
|
+
"""Whether the request is resolved."""
|
|
94
|
+
return self._future.done()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
type WireMessage = Event | ApprovalRequest
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def serialize_event(event: Event) -> dict[str, Any]:
|
|
101
|
+
"""
|
|
102
|
+
Convert an event message into a JSON-serializable dictionary.
|
|
103
|
+
"""
|
|
104
|
+
match event:
|
|
105
|
+
case StepBegin():
|
|
106
|
+
return {"type": "step_begin", "payload": {"n": event.n}}
|
|
107
|
+
case StepInterrupted():
|
|
108
|
+
return {"type": "step_interrupted"}
|
|
109
|
+
case CompactionBegin():
|
|
110
|
+
return {"type": "compaction_begin"}
|
|
111
|
+
case CompactionEnd():
|
|
112
|
+
return {"type": "compaction_end"}
|
|
113
|
+
case StatusUpdate():
|
|
114
|
+
return {
|
|
115
|
+
"type": "status_update",
|
|
116
|
+
"payload": {"context_usage": event.status.context_usage},
|
|
117
|
+
}
|
|
118
|
+
case ContentPart():
|
|
119
|
+
return {
|
|
120
|
+
"type": "content_part",
|
|
121
|
+
"payload": event.model_dump(mode="json", exclude_none=True),
|
|
122
|
+
}
|
|
123
|
+
case ToolCall():
|
|
124
|
+
return {
|
|
125
|
+
"type": "tool_call",
|
|
126
|
+
"payload": event.model_dump(mode="json", exclude_none=True),
|
|
127
|
+
}
|
|
128
|
+
case ToolCallPart():
|
|
129
|
+
return {
|
|
130
|
+
"type": "tool_call_part",
|
|
131
|
+
"payload": event.model_dump(mode="json", exclude_none=True),
|
|
132
|
+
}
|
|
133
|
+
case ToolResult():
|
|
134
|
+
return {
|
|
135
|
+
"type": "tool_result",
|
|
136
|
+
"payload": serialize_tool_result(event),
|
|
137
|
+
}
|
|
138
|
+
case SubagentEvent():
|
|
139
|
+
return {
|
|
140
|
+
"type": "subagent_event",
|
|
141
|
+
"payload": {
|
|
142
|
+
"task_tool_call_id": event.task_tool_call_id,
|
|
143
|
+
"event": serialize_event(event.event),
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def serialize_approval_request(request: ApprovalRequest) -> dict[str, Any]:
|
|
149
|
+
"""
|
|
150
|
+
Convert an ApprovalRequest into a JSON-serializable dictionary.
|
|
151
|
+
"""
|
|
152
|
+
return {
|
|
153
|
+
"id": request.id,
|
|
154
|
+
"tool_call_id": request.tool_call_id,
|
|
155
|
+
"sender": request.sender,
|
|
156
|
+
"action": request.action,
|
|
157
|
+
"description": request.description,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def serialize_tool_result(result: ToolResult) -> dict[str, Any]:
|
|
162
|
+
if isinstance(result.result, ToolOk):
|
|
163
|
+
ok = True
|
|
164
|
+
result_data = {
|
|
165
|
+
"output": _serialize_tool_output(result.result.output),
|
|
166
|
+
"message": result.result.message,
|
|
167
|
+
"brief": result.result.brief,
|
|
168
|
+
}
|
|
169
|
+
else:
|
|
170
|
+
ok = False
|
|
171
|
+
result_data = {
|
|
172
|
+
"output": result.result.output,
|
|
173
|
+
"message": result.result.message,
|
|
174
|
+
"brief": result.result.brief,
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
"tool_call_id": result.tool_call_id,
|
|
178
|
+
"ok": ok,
|
|
179
|
+
"result": result_data,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _serialize_tool_output(
|
|
184
|
+
output: str | ContentPart | Sequence[ContentPart],
|
|
185
|
+
) -> str | list[Any] | dict[str, Any]:
|
|
186
|
+
if isinstance(output, str):
|
|
187
|
+
return output
|
|
188
|
+
elif isinstance(output, ContentPart):
|
|
189
|
+
return output.model_dump(mode="json", exclude_none=True)
|
|
190
|
+
else: # Sequence[ContentPart]
|
|
191
|
+
return [part.model_dump(mode="json", exclude_none=True) for part in output]
|