delos-cli 0.1.0__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.
- delos_cli-0.1.0/.gitignore +145 -0
- delos_cli-0.1.0/PKG-INFO +104 -0
- delos_cli-0.1.0/README.md +75 -0
- delos_cli-0.1.0/delos_cli/__init__.py +3 -0
- delos_cli-0.1.0/delos_cli/agent/__init__.py +34 -0
- delos_cli-0.1.0/delos_cli/agent/session.py +111 -0
- delos_cli-0.1.0/delos_cli/agent/tools.py +131 -0
- delos_cli-0.1.0/delos_cli/agent/transport.py +102 -0
- delos_cli-0.1.0/delos_cli/apps/__init__.py +6 -0
- delos_cli-0.1.0/delos_cli/apps/base.py +101 -0
- delos_cli-0.1.0/delos_cli/apps/chat/__init__.py +5 -0
- delos_cli-0.1.0/delos_cli/apps/chat/app.py +149 -0
- delos_cli-0.1.0/delos_cli/apps/chat/commands.py +17 -0
- delos_cli-0.1.0/delos_cli/apps/chat/render.py +188 -0
- delos_cli-0.1.0/delos_cli/apps/chat/replay.py +108 -0
- delos_cli-0.1.0/delos_cli/auth/__init__.py +24 -0
- delos_cli-0.1.0/delos_cli/auth/config.py +282 -0
- delos_cli-0.1.0/delos_cli/auth/mfa.py +120 -0
- delos_cli-0.1.0/delos_cli/auth/oauth.py +336 -0
- delos_cli-0.1.0/delos_cli/auth/token_manager.py +136 -0
- delos_cli-0.1.0/delos_cli/commands/__init__.py +10 -0
- delos_cli-0.1.0/delos_cli/commands/base.py +54 -0
- delos_cli-0.1.0/delos_cli/commands/builtin.py +160 -0
- delos_cli-0.1.0/delos_cli/ctx.py +65 -0
- delos_cli-0.1.0/delos_cli/loop.py +19 -0
- delos_cli-0.1.0/delos_cli/main.py +230 -0
- delos_cli-0.1.0/delos_cli/state.py +28 -0
- delos_cli-0.1.0/delos_cli/tools/__init__.py +20 -0
- delos_cli-0.1.0/delos_cli/tools/edit_content.py +193 -0
- delos_cli-0.1.0/delos_cli/tools/run_shell.py +150 -0
- delos_cli-0.1.0/delos_cli/tools/write_content.py +120 -0
- delos_cli-0.1.0/delos_cli/transport/__init__.py +24 -0
- delos_cli-0.1.0/delos_cli/transport/chats.py +235 -0
- delos_cli-0.1.0/delos_cli/transport/client.py +321 -0
- delos_cli-0.1.0/delos_cli/transport/models.py +19 -0
- delos_cli-0.1.0/delos_cli/ui/__init__.py +6 -0
- delos_cli-0.1.0/delos_cli/ui/chat_picker.py +151 -0
- delos_cli-0.1.0/delos_cli/ui/completer.py +68 -0
- delos_cli-0.1.0/delos_cli/ui/lexer.py +62 -0
- delos_cli-0.1.0/delos_cli/ui/output.py +180 -0
- delos_cli-0.1.0/delos_cli/ui/repl.py +679 -0
- delos_cli-0.1.0/delos_cli/ui/style.py +24 -0
- delos_cli-0.1.0/pyproject.toml +44 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
knip.txt
|
|
2
|
+
pentest-*
|
|
3
|
+
lefthook.yml
|
|
4
|
+
backend/.vscode
|
|
5
|
+
backend/cosmos_env
|
|
6
|
+
# Allow backend/.env (has safe localhost defaults), but ignore all other .env files
|
|
7
|
+
apps/**/.env
|
|
8
|
+
collab/.env
|
|
9
|
+
cvn-tunnel/.env
|
|
10
|
+
mail-forward/.env
|
|
11
|
+
tools/**/.env
|
|
12
|
+
infrastructure/supabase/**/.env
|
|
13
|
+
.env
|
|
14
|
+
!backend/.env
|
|
15
|
+
.venv
|
|
16
|
+
.vscode
|
|
17
|
+
**/*.vscode
|
|
18
|
+
backend/.python-version
|
|
19
|
+
/scripts/
|
|
20
|
+
*/scripts/
|
|
21
|
+
|
|
22
|
+
.claude/worktrees
|
|
23
|
+
|
|
24
|
+
data/documents2
|
|
25
|
+
**/**0
|
|
26
|
+
_local
|
|
27
|
+
.env.old
|
|
28
|
+
.cursorignore
|
|
29
|
+
.gitignore.local
|
|
30
|
+
*.code-workspace
|
|
31
|
+
|
|
32
|
+
already_translated_chunk.json
|
|
33
|
+
|
|
34
|
+
.DS_Store
|
|
35
|
+
|
|
36
|
+
.idea/
|
|
37
|
+
.vercel
|
|
38
|
+
|
|
39
|
+
log.txt
|
|
40
|
+
.eslintcache
|
|
41
|
+
|
|
42
|
+
# dependencies
|
|
43
|
+
node_modules
|
|
44
|
+
.wrangler
|
|
45
|
+
.pnp
|
|
46
|
+
.pnp.js
|
|
47
|
+
|
|
48
|
+
# testing
|
|
49
|
+
coverage
|
|
50
|
+
.coverage
|
|
51
|
+
htmlcov/
|
|
52
|
+
|
|
53
|
+
# next.js
|
|
54
|
+
.next/
|
|
55
|
+
out/
|
|
56
|
+
next-env.d.ts
|
|
57
|
+
|
|
58
|
+
# production
|
|
59
|
+
build
|
|
60
|
+
|
|
61
|
+
# misc
|
|
62
|
+
.DS_Store
|
|
63
|
+
*.pem
|
|
64
|
+
|
|
65
|
+
# debug
|
|
66
|
+
npm-debug.log*
|
|
67
|
+
yarn-debug.log*
|
|
68
|
+
yarn-error.log*
|
|
69
|
+
.pnpm-debug.log*
|
|
70
|
+
|
|
71
|
+
# local env files
|
|
72
|
+
.env*.local
|
|
73
|
+
|
|
74
|
+
# vercel
|
|
75
|
+
.vercel
|
|
76
|
+
|
|
77
|
+
# typescript
|
|
78
|
+
*.tsbuildinfo
|
|
79
|
+
|
|
80
|
+
# turbo
|
|
81
|
+
.turbo
|
|
82
|
+
|
|
83
|
+
# ide
|
|
84
|
+
.idea/
|
|
85
|
+
.vscode/
|
|
86
|
+
.zed
|
|
87
|
+
|
|
88
|
+
# contentlayer
|
|
89
|
+
.contentlayer/
|
|
90
|
+
|
|
91
|
+
# process-compose logs
|
|
92
|
+
logs/
|
|
93
|
+
|
|
94
|
+
/pentest/
|
|
95
|
+
|
|
96
|
+
# terraform
|
|
97
|
+
.terraform/
|
|
98
|
+
*.tfstate
|
|
99
|
+
*.tfstate.*
|
|
100
|
+
*.tfstate.backup
|
|
101
|
+
.terraform.lock.hcl
|
|
102
|
+
|
|
103
|
+
.serena/
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# skillz (duplicate of skills/, generated during experiments)
|
|
107
|
+
.skillz/
|
|
108
|
+
|
|
109
|
+
# IDE-specific AI settings (per-user configuration)
|
|
110
|
+
**/.gemini/
|
|
111
|
+
**/.qwen/
|
|
112
|
+
|
|
113
|
+
# serena
|
|
114
|
+
.serena/
|
|
115
|
+
|
|
116
|
+
# Mobile App (Expo/React Native)
|
|
117
|
+
apps/mobile/.expo/
|
|
118
|
+
apps/mobile/dist/
|
|
119
|
+
apps/mobile/web-build/
|
|
120
|
+
apps/mobile/expo-env.d.ts
|
|
121
|
+
apps/mobile/ios/
|
|
122
|
+
apps/mobile/android/
|
|
123
|
+
apps/mobile/.kotlin/
|
|
124
|
+
apps/mobile/google-services.json
|
|
125
|
+
apps/mobile/*.mobileprovision
|
|
126
|
+
apps/mobile/*.p12
|
|
127
|
+
apps/mobile/*.key
|
|
128
|
+
apps/mobile/.metro-health-check*
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
.gitnexus
|
|
132
|
+
|
|
133
|
+
# Python cache
|
|
134
|
+
__pycache__/
|
|
135
|
+
*.pyc
|
|
136
|
+
*.pyo
|
|
137
|
+
*.pyd
|
|
138
|
+
.Python
|
|
139
|
+
*.egg-info/
|
|
140
|
+
.reference
|
|
141
|
+
|
|
142
|
+
# Nvim config
|
|
143
|
+
.nvim.lua
|
|
144
|
+
.nvim.lua.sprite
|
|
145
|
+
.sprite
|
delos_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: delos-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Terminal REPL for the Delos agent — the Claude-Code-style CLI for Delos.
|
|
5
|
+
Project-URL: Homepage, https://delos.so
|
|
6
|
+
Project-URL: Repository, https://github.com/Delos-Intelligence/cosmos-saas
|
|
7
|
+
Project-URL: Issues, https://github.com/Delos-Intelligence/cosmos-saas/issues
|
|
8
|
+
Author: Delos Intelligence
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: agent,ai,chat,cli,delos,repl,terminal
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Terminals
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: ==3.12.*
|
|
23
|
+
Requires-Dist: httpx>=0.27.0
|
|
24
|
+
Requires-Dist: prompt-toolkit>=3.0.47
|
|
25
|
+
Requires-Dist: questionary>=2.0.1
|
|
26
|
+
Requires-Dist: rich>=13.8.0
|
|
27
|
+
Requires-Dist: typer>=0.12.5
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# delos-cli
|
|
31
|
+
|
|
32
|
+
A terminal REPL for [Delos](https://delos.so) — the **Claude-Code-style CLI for the Delos agent**.
|
|
33
|
+
|
|
34
|
+
Talk to your Delos AI from any shell. Streaming Markdown answers, slash
|
|
35
|
+
commands, and tools that read and edit your local files (with your
|
|
36
|
+
approval), all in a TUI that lives in your terminal.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv tool install delos-cli
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or with pipx:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pipx install delos-cli
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Sign in
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
delos login --region eu # or "us" / "ae"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The browser opens, you sign in to your Delos workspace, and the CLI
|
|
57
|
+
saves your tokens to `~/.config/delos/`.
|
|
58
|
+
|
|
59
|
+
## Run
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
delos
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Conversations are scoped to the directory you launch from — running
|
|
66
|
+
`delos` in `~/projects/foo` shows only chats started in
|
|
67
|
+
`~/projects/foo`. Pick an existing one to resume, or start a fresh one.
|
|
68
|
+
|
|
69
|
+
### In the REPL
|
|
70
|
+
|
|
71
|
+
| Action | Key |
|
|
72
|
+
|---|---|
|
|
73
|
+
| Send a message | `Enter` |
|
|
74
|
+
| New line | `Alt+Enter` |
|
|
75
|
+
| Stop the agent mid-answer | `Esc` (or `/stop`) |
|
|
76
|
+
| Quit | `Ctrl+D` (or `/quit`) |
|
|
77
|
+
| Scroll | mouse wheel · `PageUp` / `PageDown` · `End` to follow again |
|
|
78
|
+
|
|
79
|
+
### Slash commands
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
/help list commands
|
|
83
|
+
/clear wipe the screen and start a fresh conversation
|
|
84
|
+
/rename <new name> rename the current chat
|
|
85
|
+
/delete delete the current chat and go back to the picker
|
|
86
|
+
/resume back to the picker without deleting
|
|
87
|
+
/stop interrupt the agent
|
|
88
|
+
/quit exit
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Requirements
|
|
92
|
+
|
|
93
|
+
- Python 3.12
|
|
94
|
+
- A [Delos](https://delos.so) account
|
|
95
|
+
|
|
96
|
+
## Uninstall
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
uv tool uninstall delos-cli
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
Made by [Delos Intelligence](https://delos.so).
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# delos-cli
|
|
2
|
+
|
|
3
|
+
A terminal REPL for [Delos](https://delos.so) — the **Claude-Code-style CLI for the Delos agent**.
|
|
4
|
+
|
|
5
|
+
Talk to your Delos AI from any shell. Streaming Markdown answers, slash
|
|
6
|
+
commands, and tools that read and edit your local files (with your
|
|
7
|
+
approval), all in a TUI that lives in your terminal.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv tool install delos-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or with pipx:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pipx install delos-cli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Sign in
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
delos login --region eu # or "us" / "ae"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The browser opens, you sign in to your Delos workspace, and the CLI
|
|
28
|
+
saves your tokens to `~/.config/delos/`.
|
|
29
|
+
|
|
30
|
+
## Run
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
delos
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Conversations are scoped to the directory you launch from — running
|
|
37
|
+
`delos` in `~/projects/foo` shows only chats started in
|
|
38
|
+
`~/projects/foo`. Pick an existing one to resume, or start a fresh one.
|
|
39
|
+
|
|
40
|
+
### In the REPL
|
|
41
|
+
|
|
42
|
+
| Action | Key |
|
|
43
|
+
|---|---|
|
|
44
|
+
| Send a message | `Enter` |
|
|
45
|
+
| New line | `Alt+Enter` |
|
|
46
|
+
| Stop the agent mid-answer | `Esc` (or `/stop`) |
|
|
47
|
+
| Quit | `Ctrl+D` (or `/quit`) |
|
|
48
|
+
| Scroll | mouse wheel · `PageUp` / `PageDown` · `End` to follow again |
|
|
49
|
+
|
|
50
|
+
### Slash commands
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
/help list commands
|
|
54
|
+
/clear wipe the screen and start a fresh conversation
|
|
55
|
+
/rename <new name> rename the current chat
|
|
56
|
+
/delete delete the current chat and go back to the picker
|
|
57
|
+
/resume back to the picker without deleting
|
|
58
|
+
/stop interrupt the agent
|
|
59
|
+
/quit exit
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Requirements
|
|
63
|
+
|
|
64
|
+
- Python 3.12
|
|
65
|
+
- A [Delos](https://delos.so) account
|
|
66
|
+
|
|
67
|
+
## Uninstall
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv tool uninstall delos-cli
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
Made by [Delos Intelligence](https://delos.so).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""High-level wrapper around the backend's ``/{app}/agent/*`` routes.
|
|
2
|
+
|
|
3
|
+
:class:`AgentSession` drives one user turn end-to-end: streams completions,
|
|
4
|
+
detects unresolved tool calls (= client-side tools), resolves them via
|
|
5
|
+
registered handlers, and re-issues continuation requests until the agent
|
|
6
|
+
run is fully complete.
|
|
7
|
+
|
|
8
|
+
A :class:`ToolRegistry` is the single extension point — apps register:
|
|
9
|
+
|
|
10
|
+
* **Renderers** — how a server-side tool's events show up in the terminal.
|
|
11
|
+
* **Handlers** — how a client-side tool collects input from the user.
|
|
12
|
+
|
|
13
|
+
Both have sensible defaults so apps work out of the box.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .session import AgentSession
|
|
17
|
+
from .tools import (
|
|
18
|
+
ToolHandler,
|
|
19
|
+
ToolRegistry,
|
|
20
|
+
ToolRenderer,
|
|
21
|
+
default_handler,
|
|
22
|
+
default_renderer,
|
|
23
|
+
)
|
|
24
|
+
from .transport import AgentTransport
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"AgentSession",
|
|
28
|
+
"AgentTransport",
|
|
29
|
+
"ToolHandler",
|
|
30
|
+
"ToolRegistry",
|
|
31
|
+
"ToolRenderer",
|
|
32
|
+
"default_handler",
|
|
33
|
+
"default_renderer",
|
|
34
|
+
]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""AgentSession — drives one user turn end-to-end.
|
|
2
|
+
|
|
3
|
+
A turn may span multiple SSE streams: each client-side tool call ends
|
|
4
|
+
one stream and starts another with ``tool_continuation=true``. From the
|
|
5
|
+
caller's POV, :meth:`AgentSession.send_turn` is a single async iterator
|
|
6
|
+
of UI events — all the multi-stream stitching is internal.
|
|
7
|
+
|
|
8
|
+
A :class:`ToolRegistry` resolves both how server-tool events render and
|
|
9
|
+
how client-tool calls collect input from the user.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import AsyncIterator
|
|
18
|
+
|
|
19
|
+
from delos_cli.ctx import Ctx
|
|
20
|
+
|
|
21
|
+
from .tools import ToolRegistry
|
|
22
|
+
from .transport import AgentTransport
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AgentSession:
|
|
26
|
+
"""One session per ``(transport, chat_id)`` pair."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
transport: AgentTransport,
|
|
31
|
+
chat_id: str,
|
|
32
|
+
registry: ToolRegistry,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Bind to a chat row. Multiple turns share the same session."""
|
|
35
|
+
self._transport = transport
|
|
36
|
+
self._chat_id = chat_id
|
|
37
|
+
self._registry = registry
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def chat_id(self) -> str:
|
|
41
|
+
"""The chat row id this session is bound to."""
|
|
42
|
+
return self._chat_id
|
|
43
|
+
|
|
44
|
+
async def send_turn(
|
|
45
|
+
self,
|
|
46
|
+
body: dict[str, Any],
|
|
47
|
+
ctx: Ctx,
|
|
48
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
49
|
+
"""Send one user turn and yield every UI event the agent emits.
|
|
50
|
+
|
|
51
|
+
Streams the initial ``/completions`` request. When the run
|
|
52
|
+
terminates with unresolved tool calls, treats them as client-side,
|
|
53
|
+
invokes the registered handler, posts the result via
|
|
54
|
+
``/tool-result``, and re-streams a continuation request with
|
|
55
|
+
``tool_continuation=true`` and ``messages=[]``. Loops until no
|
|
56
|
+
pending calls remain or the user explicitly stopped.
|
|
57
|
+
"""
|
|
58
|
+
current_body = dict(body)
|
|
59
|
+
while True:
|
|
60
|
+
pending: dict[str, dict[str, Any]] = {}
|
|
61
|
+
stopped = False
|
|
62
|
+
async for event in self._transport.stream_completions(current_body):
|
|
63
|
+
etype = event.get("type", "")
|
|
64
|
+
if etype == "tool-input-available":
|
|
65
|
+
tcid = event.get("toolCallId", "")
|
|
66
|
+
if tcid:
|
|
67
|
+
pending[tcid] = {
|
|
68
|
+
"name": event.get("toolName", ""),
|
|
69
|
+
"input": event.get("input", {}) or {},
|
|
70
|
+
}
|
|
71
|
+
elif etype == "tool-output-available":
|
|
72
|
+
pending.pop(event.get("toolCallId", ""), None)
|
|
73
|
+
elif etype == "data-stopped":
|
|
74
|
+
stopped = True
|
|
75
|
+
yield event
|
|
76
|
+
|
|
77
|
+
if stopped or not pending:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
for tcid, meta in pending.items():
|
|
81
|
+
handler = self._registry.handler_for(meta["name"])
|
|
82
|
+
result = await handler(meta["input"], ctx)
|
|
83
|
+
await self._transport.submit_tool_result(
|
|
84
|
+
self._chat_id, tcid, meta["name"], result,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
current_body = {**body, "messages": [], "tool_continuation": True}
|
|
88
|
+
|
|
89
|
+
async def stop(self) -> None:
|
|
90
|
+
"""Set the backend stop signal. Idempotent."""
|
|
91
|
+
await self._transport.stop(self._chat_id)
|
|
92
|
+
|
|
93
|
+
async def clear_chat(self) -> None:
|
|
94
|
+
"""Delete the chat row on the backend."""
|
|
95
|
+
await self._transport.clear_chat(self._chat_id)
|
|
96
|
+
|
|
97
|
+
async def queue_message(self, content: str) -> str:
|
|
98
|
+
"""Add a message to the FIFO queue. Returns the new message id."""
|
|
99
|
+
return await self._transport.queue_message(self._chat_id, content)
|
|
100
|
+
|
|
101
|
+
async def list_queue(self) -> list[dict[str, Any]]:
|
|
102
|
+
"""Return the current queued-messages list."""
|
|
103
|
+
return await self._transport.list_queue(self._chat_id)
|
|
104
|
+
|
|
105
|
+
async def edit_queued(self, message_id: str, content: str) -> None:
|
|
106
|
+
"""Edit a queued message in place."""
|
|
107
|
+
await self._transport.edit_queued(self._chat_id, message_id, content)
|
|
108
|
+
|
|
109
|
+
async def remove_queued(self, message_id: str) -> None:
|
|
110
|
+
"""Remove a queued message."""
|
|
111
|
+
await self._transport.remove_queued(self._chat_id, message_id)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Tool registry, default renderers, and default handlers.
|
|
2
|
+
|
|
3
|
+
There are two extension points per tool:
|
|
4
|
+
|
|
5
|
+
- **Renderer** — called when the agent emits ``tool-input-available``,
|
|
6
|
+
``tool-output-available``, or ``data-tool-output-delta`` for that tool.
|
|
7
|
+
Returns a Rich renderable (``Panel``, ``Text``, …) or ``None`` to skip.
|
|
8
|
+
Used for server-side tools whose execution happens in the backend.
|
|
9
|
+
|
|
10
|
+
- **Handler** — called when the agent emits ``tool-input-available`` and
|
|
11
|
+
the run terminates without a corresponding ``tool-output-available``
|
|
12
|
+
(= the tool was registered ``client=True`` server-side). Returns the
|
|
13
|
+
result string the next agent turn should see.
|
|
14
|
+
|
|
15
|
+
Defaults:
|
|
16
|
+
|
|
17
|
+
- :func:`default_renderer` — a one-line ``→ tool_name`` indicator on
|
|
18
|
+
``tool-input-available``; nothing on output / delta events. Custom
|
|
19
|
+
renderers can opt into args, output, or streaming deltas.
|
|
20
|
+
- :func:`default_handler` — prints the input dict, asks for a free-form
|
|
21
|
+
text answer via a one-shot prompt_toolkit prompt.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
29
|
+
|
|
30
|
+
from prompt_toolkit import PromptSession
|
|
31
|
+
from prompt_toolkit.formatted_text import HTML
|
|
32
|
+
from rich.panel import Panel
|
|
33
|
+
from rich.text import Text
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from rich.console import RenderableType
|
|
37
|
+
|
|
38
|
+
from delos_cli.ctx import Ctx
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@runtime_checkable
|
|
42
|
+
class ToolRenderer(Protocol):
|
|
43
|
+
"""Render one tool-related event.
|
|
44
|
+
|
|
45
|
+
Return a Rich renderable to print above the live region, or ``None``
|
|
46
|
+
to skip rendering. The same renderer instance receives every event
|
|
47
|
+
for its tool name across the whole REPL session, so it may keep state
|
|
48
|
+
(e.g. dedupe by ``toolCallId``) if needed.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __call__(self, event: dict[str, Any]) -> RenderableType | None:
|
|
52
|
+
"""Render ``event`` and return a Rich renderable, or ``None`` to skip."""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@runtime_checkable
|
|
57
|
+
class ToolHandler(Protocol):
|
|
58
|
+
"""Resolve a client-side tool call.
|
|
59
|
+
|
|
60
|
+
Receives the tool's ``input`` dict (whatever the agent passed) and the
|
|
61
|
+
REPL :class:`~delos_cli.ctx.Ctx` for console / HTTP access. Returns
|
|
62
|
+
the result string the next agent turn should see.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
async def __call__(
|
|
66
|
+
self, tool_input: dict[str, Any], ctx: Ctx,
|
|
67
|
+
) -> str:
|
|
68
|
+
"""Collect input from the user and return the result string."""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def default_renderer(event: dict[str, Any]) -> RenderableType | None:
|
|
73
|
+
"""Show a single ``→ tool_name`` line on ``tool-input-available``; ignore the rest.
|
|
74
|
+
|
|
75
|
+
Args are intentionally hidden — most tool calls are noisy enough as JSON
|
|
76
|
+
that they hurt readability for the default case. Custom renderers can
|
|
77
|
+
opt into args, output (``tool-output-available``), and streaming
|
|
78
|
+
deltas (``data-tool-output-delta``).
|
|
79
|
+
"""
|
|
80
|
+
if event.get("type") != "tool-input-available":
|
|
81
|
+
return None
|
|
82
|
+
name = event.get("toolName", "?")
|
|
83
|
+
line = Text()
|
|
84
|
+
line.append("→ ", style="bold cyan")
|
|
85
|
+
line.append(name, style="cyan")
|
|
86
|
+
return line
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def default_handler(tool_input: dict[str, Any], ctx: Ctx) -> str:
|
|
90
|
+
"""Print the input dict and prompt for a single-line free-form answer.
|
|
91
|
+
|
|
92
|
+
A one-shot :class:`PromptSession` is spun up so the user gets the same
|
|
93
|
+
keybindings as the main REPL (Ctrl+R search, history hints, …) without
|
|
94
|
+
polluting the chat history file with these tool answers.
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
pretty = json.dumps(tool_input, indent=2, ensure_ascii=False)
|
|
98
|
+
except (TypeError, ValueError):
|
|
99
|
+
pretty = str(tool_input)
|
|
100
|
+
ctx.console.print(
|
|
101
|
+
Panel(
|
|
102
|
+
Text(pretty, style="dim"),
|
|
103
|
+
title="? client tool input",
|
|
104
|
+
title_align="left",
|
|
105
|
+
border_style="yellow",
|
|
106
|
+
expand=False,
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
sess: PromptSession[str] = PromptSession()
|
|
110
|
+
answer = await sess.prompt_async(HTML("<ansiyellow>tool result › </ansiyellow>"))
|
|
111
|
+
return answer.strip()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class ToolRegistry:
|
|
116
|
+
"""Per-tool overrides keyed by tool name.
|
|
117
|
+
|
|
118
|
+
Lookups fall back to :func:`default_renderer` / :func:`default_handler`
|
|
119
|
+
when a tool isn't registered.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
renderers: dict[str, ToolRenderer] = field(default_factory=dict)
|
|
123
|
+
handlers: dict[str, ToolHandler] = field(default_factory=dict)
|
|
124
|
+
|
|
125
|
+
def renderer_for(self, name: str) -> ToolRenderer:
|
|
126
|
+
"""Return the registered renderer for ``name``, or the default."""
|
|
127
|
+
return self.renderers.get(name, default_renderer)
|
|
128
|
+
|
|
129
|
+
def handler_for(self, name: str) -> ToolHandler:
|
|
130
|
+
"""Return the registered handler for ``name``, or the default."""
|
|
131
|
+
return self.handlers.get(name, default_handler)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""HTTP wrappers for the ``/{app}/agent/*`` routes.
|
|
2
|
+
|
|
3
|
+
One :class:`AgentTransport` instance is bound to ``(app, org)``; the
|
|
4
|
+
``chat_id`` is passed per call. Methods map 1:1 to the routes the backend
|
|
5
|
+
exposes via ``create_agent_router``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import AsyncIterator
|
|
15
|
+
|
|
16
|
+
from delos_cli.transport.client import AuthedClient
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AgentTransport:
|
|
20
|
+
"""One transport instance per ``(app, org)`` pair."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
http: AuthedClient,
|
|
25
|
+
api_prefix: str,
|
|
26
|
+
org_uuid: str,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Bind to an app's agent router.
|
|
29
|
+
|
|
30
|
+
``api_prefix`` is the app slug (``"chat"``, ``"code"``, …) — it is
|
|
31
|
+
what the backend uses to mount each ``create_agent_router`` instance.
|
|
32
|
+
"""
|
|
33
|
+
self._http = http
|
|
34
|
+
self._prefix = api_prefix
|
|
35
|
+
self._org = org_uuid
|
|
36
|
+
|
|
37
|
+
def _path(self, suffix: str, chat_id: str | None = None) -> str:
|
|
38
|
+
base = f"/{self._prefix}/agent/{suffix}/{self._org}"
|
|
39
|
+
return f"{base}/{chat_id}" if chat_id is not None else base
|
|
40
|
+
|
|
41
|
+
async def stream_completions(
|
|
42
|
+
self, body: dict[str, Any],
|
|
43
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
44
|
+
"""``POST /{app}/agent/completions/{org}`` → parsed SSE events."""
|
|
45
|
+
async for event in self._http.sse_post(self._path("completions"), body):
|
|
46
|
+
yield event
|
|
47
|
+
|
|
48
|
+
async def stop(self, chat_id: str) -> None:
|
|
49
|
+
"""``POST /stop/{org}/{chat_id}`` — set the Redis stop signal."""
|
|
50
|
+
await self._http.json_post(self._path("stop", chat_id), {})
|
|
51
|
+
|
|
52
|
+
async def submit_tool_result(
|
|
53
|
+
self,
|
|
54
|
+
chat_id: str,
|
|
55
|
+
tool_call_id: str,
|
|
56
|
+
tool_name: str,
|
|
57
|
+
content: str,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""``POST /tool-result/{org}/{chat_id}`` — persist a client-tool result."""
|
|
60
|
+
await self._http.json_post(
|
|
61
|
+
self._path("tool-result", chat_id),
|
|
62
|
+
{
|
|
63
|
+
"tool_call_id": tool_call_id,
|
|
64
|
+
"tool_name": tool_name,
|
|
65
|
+
"content": content,
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
async def clear_chat(self, chat_id: str) -> None:
|
|
70
|
+
"""``POST /clear-chat/{org}/{chat_id}`` — delete the chat row."""
|
|
71
|
+
await self._http.json_post(self._path("clear-chat", chat_id), {})
|
|
72
|
+
|
|
73
|
+
async def queue_message(self, chat_id: str, content: str) -> str:
|
|
74
|
+
"""``POST /queue/{org}/{chat_id}`` — append to the FIFO queue.
|
|
75
|
+
|
|
76
|
+
Generates a fresh UUID client-side (mirrors the frontend) and returns
|
|
77
|
+
it so the caller can later edit / remove the queued entry.
|
|
78
|
+
"""
|
|
79
|
+
message_id = str(uuid.uuid4())
|
|
80
|
+
await self._http.json_post(
|
|
81
|
+
self._path("queue", chat_id),
|
|
82
|
+
{"message_id": message_id, "content": content},
|
|
83
|
+
)
|
|
84
|
+
return message_id
|
|
85
|
+
|
|
86
|
+
async def list_queue(self, chat_id: str) -> list[dict[str, Any]]:
|
|
87
|
+
"""``GET /queue/{org}/{chat_id}`` — current queue snapshot."""
|
|
88
|
+
data = await self._http.json_get(self._path("queue", chat_id))
|
|
89
|
+
queue = data.get("queue") or []
|
|
90
|
+
return list(queue)
|
|
91
|
+
|
|
92
|
+
async def edit_queued(
|
|
93
|
+
self, chat_id: str, message_id: str, content: str,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""``PUT /queue/{org}/{chat_id}/{msg_id}``."""
|
|
96
|
+
path = f"{self._path('queue', chat_id)}/{message_id}"
|
|
97
|
+
await self._http.json_request("PUT", path, {"content": content})
|
|
98
|
+
|
|
99
|
+
async def remove_queued(self, chat_id: str, message_id: str) -> None:
|
|
100
|
+
"""``DELETE /queue/{org}/{chat_id}/{msg_id}``."""
|
|
101
|
+
path = f"{self._path('queue', chat_id)}/{message_id}"
|
|
102
|
+
await self._http.json_request("DELETE", path, None)
|