delos-cli 2.0.0__tar.gz → 2.0.2__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-2.0.0 → delos_cli-2.0.2}/.gitignore +35 -4
- {delos_cli-2.0.0 → delos_cli-2.0.2}/PKG-INFO +14 -12
- {delos_cli-2.0.0 → delos_cli-2.0.2}/README.md +10 -8
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/agent/__init__.py +4 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/agent/session.py +6 -18
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/agent/tools.py +53 -9
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/agent/transport.py +6 -2
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/agent/turn.py +1 -1
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/base.py +4 -1
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/chat/app.py +22 -108
- delos_cli-2.0.2/delos_cli/apps/chat/commands.py +187 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/chat/render.py +47 -27
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/replay.py +4 -2
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/scribe/app.py +7 -1
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/scribe/tools.py +12 -7
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/auth/config.py +1 -1
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/auth/mfa.py +3 -12
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/auth/oauth.py +35 -10
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/auth/token_manager.py +1 -1
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/commands/prompts.py +6 -2
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ctx.py +5 -2
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/git_context.py +1 -1
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/main.py +57 -196
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/project_context.py +1 -1
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/serve/app.py +13 -6
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/serve/confirm.py +1 -2
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/serve/server.py +39 -25
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/state.py +2 -41
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/ask_user_input.py +5 -6
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/edit_content.py +7 -9
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/explore.py +76 -14
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/glob_tool.py +5 -17
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/grep.py +8 -18
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/read.py +11 -13
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/run_shell.py +5 -12
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/task.py +6 -10
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/todo_write.py +2 -8
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/verify.py +9 -17
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/write_content.py +11 -11
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/transport/chats.py +1 -3
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/transport/documents.py +10 -4
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/transport/integrations.py +4 -6
- delos_cli-2.0.2/delos_cli/ui/collapse.py +158 -0
- delos_cli-2.0.2/delos_cli/ui/completer.py +290 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/lexer.py +2 -1
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/output.py +34 -17
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/repl.py +171 -47
- delos_cli-2.0.2/delos_cli/usage_report.py +173 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/pyproject.toml +7 -34
- delos_cli-2.0.0/DELOS.md +0 -785
- delos_cli-2.0.0/delos_cli/apps/chat/commands.py +0 -340
- delos_cli-2.0.0/delos_cli/doctor.py +0 -182
- delos_cli-2.0.0/delos_cli/engine/__init__.py +0 -34
- delos_cli-2.0.0/delos_cli/engine/compaction.py +0 -514
- delos_cli-2.0.0/delos_cli/engine/context.py +0 -577
- delos_cli-2.0.0/delos_cli/engine/frontmatter.py +0 -33
- delos_cli-2.0.0/delos_cli/engine/llm.py +0 -98
- delos_cli-2.0.0/delos_cli/engine/loop.py +0 -837
- delos_cli-2.0.0/delos_cli/engine/memory.py +0 -171
- delos_cli-2.0.0/delos_cli/engine/prompt.py +0 -313
- delos_cli-2.0.0/delos_cli/engine/skills.py +0 -253
- delos_cli-2.0.0/delos_cli/engine/streaming.py +0 -210
- delos_cli-2.0.0/delos_cli/engine/subagent.py +0 -239
- delos_cli-2.0.0/delos_cli/engine/toolset.py +0 -614
- delos_cli-2.0.0/delos_cli/engine/transcript.py +0 -175
- delos_cli-2.0.0/delos_cli/hooks.py +0 -200
- delos_cli-2.0.0/delos_cli/settings.py +0 -148
- delos_cli-2.0.0/delos_cli/tools/readonly_shell.py +0 -99
- delos_cli-2.0.0/delos_cli/ui/collapse.py +0 -106
- delos_cli-2.0.0/delos_cli/ui/completer.py +0 -135
- delos_cli-2.0.0/tests/conftest.py +0 -176
- delos_cli-2.0.0/tests/test_bench_resilience.py +0 -144
- delos_cli-2.0.0/tests/test_chat_app_engine.py +0 -188
- delos_cli-2.0.0/tests/test_doctor.py +0 -106
- delos_cli-2.0.0/tests/test_engine_compaction.py +0 -614
- delos_cli-2.0.0/tests/test_engine_context.py +0 -446
- delos_cli-2.0.0/tests/test_engine_loop.py +0 -531
- delos_cli-2.0.0/tests/test_engine_memory.py +0 -223
- delos_cli-2.0.0/tests/test_engine_plan_mode.py +0 -209
- delos_cli-2.0.0/tests/test_engine_skills.py +0 -456
- delos_cli-2.0.0/tests/test_engine_subagent.py +0 -439
- delos_cli-2.0.0/tests/test_hooks.py +0 -276
- delos_cli-2.0.0/tests/test_local_sessions.py +0 -113
- delos_cli-2.0.0/tests/test_render_tool_blocks.py +0 -195
- delos_cli-2.0.0/tests/test_settings.py +0 -183
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/__init__.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/__init__.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/chat/__init__.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/scribe/__init__.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/scribe/commands.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/auth/__init__.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/commands/__init__.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/commands/base.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/commands/builtin.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/context_report.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/loop.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/serve/__init__.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/serve/protocol.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/serve/rpc.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/__init__.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/transport/__init__.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/transport/client.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/transport/models.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/__init__.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/banner.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/chat_picker.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/document_picker.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/integrations_picker.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/model_picker.py +0 -0
- {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/style.py +0 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
knip.txt
|
|
2
2
|
pentest-*
|
|
3
|
-
lefthook.yml
|
|
4
3
|
backend/.vscode
|
|
5
4
|
backend/cosmos_env
|
|
6
5
|
# Allow backend/.env (has safe localhost defaults), but ignore all other .env files
|
|
@@ -22,16 +21,26 @@ infrastructure/supabase/**/.env
|
|
|
22
21
|
backend/.python-version
|
|
23
22
|
/scripts/
|
|
24
23
|
*/scripts/
|
|
24
|
+
!tooling/scripts/
|
|
25
25
|
# Re-include the tools-catalog export script (CI drift test imports it)
|
|
26
26
|
!backend/scripts/
|
|
27
27
|
backend/scripts/*
|
|
28
28
|
!backend/scripts/__init__.py
|
|
29
29
|
!backend/scripts/export_tools_catalog.py
|
|
30
30
|
!backend/scripts/check_supabase_models.py
|
|
31
|
+
!backend/scripts/dump_openapi.py
|
|
32
|
+
!backend/scripts/backfill_agent_issues.py
|
|
33
|
+
!backend/scripts/backfill_directive_recurrence.py
|
|
34
|
+
!backend/scripts/analyze_prompt_dumps.py
|
|
35
|
+
!backend/scripts/check_route_response_types.py
|
|
36
|
+
!backend/scripts/fly_source_recovery.py
|
|
37
|
+
!backend/scripts/audit_sprite_regions.py
|
|
38
|
+
!backend/scripts/migrate_vibe_sprites.py
|
|
31
39
|
|
|
32
40
|
.claude/worktrees
|
|
33
41
|
|
|
34
42
|
data/documents2
|
|
43
|
+
data/logs
|
|
35
44
|
**/**0
|
|
36
45
|
_local
|
|
37
46
|
.env.old
|
|
@@ -68,10 +77,24 @@ next-env.d.ts
|
|
|
68
77
|
# production
|
|
69
78
|
build
|
|
70
79
|
|
|
80
|
+
# Vite/tsc build output. Global rather than per-app: the office add-ins each
|
|
81
|
+
# went untracked-but-not-ignored until someone built them locally.
|
|
82
|
+
dist/
|
|
83
|
+
|
|
71
84
|
# misc
|
|
72
85
|
.DS_Store
|
|
73
86
|
*.pem
|
|
74
87
|
|
|
88
|
+
# pnpm is the only package manager here, including the standalone services.
|
|
89
|
+
# One recorded exception: cvn/whatsapp-bridge is an npm-managed runtime sidecar
|
|
90
|
+
# (baileys needs the git-only `libsignal`, which blockExoticSubdeps rejects and
|
|
91
|
+
# pnpm has no scoped exception for — pnpm/pnpm#11799); its lockfile is tracked
|
|
92
|
+
# so the git ref stays pinned.
|
|
93
|
+
package-lock.json
|
|
94
|
+
!cvn/whatsapp-bridge/package-lock.json
|
|
95
|
+
yarn.lock
|
|
96
|
+
npm-shrinkwrap.json
|
|
97
|
+
|
|
75
98
|
# debug
|
|
76
99
|
npm-debug.log*
|
|
77
100
|
yarn-debug.log*
|
|
@@ -108,6 +131,10 @@ yarn-error.log*
|
|
|
108
131
|
# process-compose logs
|
|
109
132
|
logs/
|
|
110
133
|
|
|
134
|
+
# python tool caches
|
|
135
|
+
.ruff_cache/
|
|
136
|
+
.pytest_cache/
|
|
137
|
+
|
|
111
138
|
/pentest/
|
|
112
139
|
|
|
113
140
|
# terraform
|
|
@@ -166,6 +193,10 @@ __pycache__/
|
|
|
166
193
|
.sprite
|
|
167
194
|
/apps/web/supabase/snippets
|
|
168
195
|
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
|
|
196
|
+
# Supabase CLI local link state (project ref, pooler URL, remote versions).
|
|
197
|
+
# Rewritten by every `supabase link`, and belongs to a machine, not to the repo.
|
|
198
|
+
**/supabase/.temp/
|
|
199
|
+
apps/workers-mobile/eas-asc-key.p8
|
|
200
|
+
|
|
201
|
+
# backfill_fts.py keyset cursor (local run state)
|
|
202
|
+
projects/0904_drivesearch/.fts-cursor.json
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: delos-cli
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Terminal REPL for the Delos agent — the Claude-Code-style CLI for Delos.
|
|
5
5
|
Project-URL: Homepage, https://delos.so
|
|
6
6
|
Project-URL: Repository, https://github.com/Delos-Intelligence/cosmos-saas
|
|
@@ -16,10 +16,10 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
16
16
|
Classifier: Operating System :: MacOS
|
|
17
17
|
Classifier: Operating System :: POSIX :: Linux
|
|
18
18
|
Classifier: Programming Language :: Python :: 3
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
20
|
Classifier: Topic :: Terminals
|
|
21
21
|
Classifier: Topic :: Utilities
|
|
22
|
-
Requires-Python: ==3.
|
|
22
|
+
Requires-Python: ==3.14.*
|
|
23
23
|
Requires-Dist: httpx>=0.27.0
|
|
24
24
|
Requires-Dist: prompt-toolkit>=3.0.47
|
|
25
25
|
Requires-Dist: questionary>=2.0.1
|
|
@@ -70,13 +70,13 @@ Conversations are scoped to the directory you launch from — running
|
|
|
70
70
|
|
|
71
71
|
### In the REPL
|
|
72
72
|
|
|
73
|
-
| Action
|
|
74
|
-
|
|
75
|
-
| Send a message
|
|
76
|
-
| New line
|
|
77
|
-
| Stop the agent mid-answer | `Esc` (or `/stop`)
|
|
78
|
-
| Quit
|
|
79
|
-
| Scroll
|
|
73
|
+
| Action | Key |
|
|
74
|
+
| ------------------------- | ----------------------------------------------------------- |
|
|
75
|
+
| Send a message | `Enter` |
|
|
76
|
+
| New line | `Alt+Enter` |
|
|
77
|
+
| Stop the agent mid-answer | `Esc` (or `/stop`) |
|
|
78
|
+
| Quit | `Ctrl+D` (or `/quit`) |
|
|
79
|
+
| Scroll | mouse wheel · `PageUp` / `PageDown` · `End` to follow again |
|
|
80
80
|
|
|
81
81
|
### Slash commands
|
|
82
82
|
|
|
@@ -87,6 +87,8 @@ Conversations are scoped to the directory you launch from — running
|
|
|
87
87
|
/delete delete the current chat and go back to the picker
|
|
88
88
|
/resume back to the picker without deleting
|
|
89
89
|
/stop interrupt the agent
|
|
90
|
+
/usage show plan usage limits and remaining credits
|
|
91
|
+
/context show what fills the agent's context window
|
|
90
92
|
/quit exit
|
|
91
93
|
```
|
|
92
94
|
|
|
@@ -104,7 +106,7 @@ See `apps/vscode/DEVELOPING.md` in the monorepo for the protocol reference.
|
|
|
104
106
|
|
|
105
107
|
## Requirements
|
|
106
108
|
|
|
107
|
-
- Python 3.
|
|
109
|
+
- Python 3.14
|
|
108
110
|
- A [Delos](https://delos.so) account
|
|
109
111
|
|
|
110
112
|
## Uninstall
|
|
@@ -39,13 +39,13 @@ Conversations are scoped to the directory you launch from — running
|
|
|
39
39
|
|
|
40
40
|
### In the REPL
|
|
41
41
|
|
|
42
|
-
| Action
|
|
43
|
-
|
|
44
|
-
| Send a message
|
|
45
|
-
| New line
|
|
46
|
-
| Stop the agent mid-answer | `Esc` (or `/stop`)
|
|
47
|
-
| Quit
|
|
48
|
-
| Scroll
|
|
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
49
|
|
|
50
50
|
### Slash commands
|
|
51
51
|
|
|
@@ -56,6 +56,8 @@ Conversations are scoped to the directory you launch from — running
|
|
|
56
56
|
/delete delete the current chat and go back to the picker
|
|
57
57
|
/resume back to the picker without deleting
|
|
58
58
|
/stop interrupt the agent
|
|
59
|
+
/usage show plan usage limits and remaining credits
|
|
60
|
+
/context show what fills the agent's context window
|
|
59
61
|
/quit exit
|
|
60
62
|
```
|
|
61
63
|
|
|
@@ -73,7 +75,7 @@ See `apps/vscode/DEVELOPING.md` in the monorepo for the protocol reference.
|
|
|
73
75
|
|
|
74
76
|
## Requirements
|
|
75
77
|
|
|
76
|
-
- Python 3.
|
|
78
|
+
- Python 3.14
|
|
77
79
|
- A [Delos](https://delos.so) account
|
|
78
80
|
|
|
79
81
|
## Uninstall
|
|
@@ -20,6 +20,8 @@ from .tools import (
|
|
|
20
20
|
ToolRenderer,
|
|
21
21
|
default_handler,
|
|
22
22
|
default_renderer,
|
|
23
|
+
friendly_tool_name,
|
|
24
|
+
tool_line,
|
|
23
25
|
)
|
|
24
26
|
from .transport import AgentTransport
|
|
25
27
|
from .turn import AgentTurn
|
|
@@ -33,4 +35,6 @@ __all__ = [
|
|
|
33
35
|
"ToolRenderer",
|
|
34
36
|
"default_handler",
|
|
35
37
|
"default_renderer",
|
|
38
|
+
"friendly_tool_name",
|
|
39
|
+
"tool_line",
|
|
36
40
|
]
|
|
@@ -15,8 +15,6 @@ import asyncio
|
|
|
15
15
|
from itertools import starmap
|
|
16
16
|
from typing import TYPE_CHECKING, Any
|
|
17
17
|
|
|
18
|
-
from delos_cli.state import tool_allowed_in
|
|
19
|
-
|
|
20
18
|
if TYPE_CHECKING:
|
|
21
19
|
from collections.abc import AsyncIterator
|
|
22
20
|
|
|
@@ -102,22 +100,9 @@ class AgentSession:
|
|
|
102
100
|
# into a unique-constraint 500. Execution is the slow part;
|
|
103
101
|
# the posts are cheap.
|
|
104
102
|
async def _execute(
|
|
105
|
-
tcid: str,
|
|
103
|
+
tcid: str,
|
|
104
|
+
meta: dict[str, Any],
|
|
106
105
|
) -> tuple[str, dict[str, Any], str]:
|
|
107
|
-
# Plan mode is enforced here, not only in ``ctx.confirm``. On
|
|
108
|
-
# this engine the backend builds the tool list and pins
|
|
109
|
-
# ``mode="build"`` (BACKEND_COORDINATION.md item 6), so the
|
|
110
|
-
# mutating tools are still advertised and the model still calls
|
|
111
|
-
# them — but nothing may actually run. Refusing at the dispatch
|
|
112
|
-
# point is what makes plan mode hold for callers whose confirm
|
|
113
|
-
# auto-accepts (``delos serve``, the benchmark harness).
|
|
114
|
-
mode = getattr(ctx.state, "permission_mode", "default")
|
|
115
|
-
if not tool_allowed_in(mode, meta["name"]):
|
|
116
|
-
return tcid, meta, (
|
|
117
|
-
f"Error: '{meta['name']}' is not available in plan mode — it would modify "
|
|
118
|
-
"the user's files. Present your plan instead; nothing is changed until the "
|
|
119
|
-
"user approves it."
|
|
120
|
-
)
|
|
121
106
|
handler = self._registry.handler_for(meta["name"])
|
|
122
107
|
result = await handler(meta["input"], ctx)
|
|
123
108
|
return tcid, meta, result
|
|
@@ -131,7 +116,10 @@ class AgentSession:
|
|
|
131
116
|
raise outcome
|
|
132
117
|
tcid, meta, result = outcome
|
|
133
118
|
await self._transport.submit_tool_result(
|
|
134
|
-
self._chat_id,
|
|
119
|
+
self._chat_id,
|
|
120
|
+
tcid,
|
|
121
|
+
meta["name"],
|
|
122
|
+
result,
|
|
135
123
|
)
|
|
136
124
|
if emit_tool_output:
|
|
137
125
|
yield {
|
|
@@ -16,7 +16,9 @@ Defaults:
|
|
|
16
16
|
|
|
17
17
|
- :func:`default_renderer` — a one-line ``→ tool_name`` indicator on
|
|
18
18
|
``tool-input-available``; nothing on output / delta events. Custom
|
|
19
|
-
renderers
|
|
19
|
+
renderers keep the same ``→`` prefix and the category colours
|
|
20
|
+
(cyan = read-only, magenta = write/agent, green = shell, yellow = user
|
|
21
|
+
prompt); they can opt into args, output, or streaming deltas.
|
|
20
22
|
- :func:`default_handler` — prints the input dict, asks for a free-form
|
|
21
23
|
text answer via a one-shot prompt_toolkit prompt.
|
|
22
24
|
"""
|
|
@@ -63,14 +65,59 @@ class ToolHandler(Protocol):
|
|
|
63
65
|
"""
|
|
64
66
|
|
|
65
67
|
async def __call__(
|
|
66
|
-
self,
|
|
68
|
+
self,
|
|
69
|
+
tool_input: dict[str, Any],
|
|
70
|
+
ctx: Ctx,
|
|
67
71
|
) -> str:
|
|
68
72
|
"""Collect input from the user and return the result string."""
|
|
69
73
|
...
|
|
70
74
|
|
|
71
75
|
|
|
76
|
+
def friendly_tool_name(name: str) -> str:
|
|
77
|
+
"""Humanize a raw tool name the way the VSCode webview does.
|
|
78
|
+
|
|
79
|
+
Same rule as the webview's ``friendlyToolName`` fallback
|
|
80
|
+
(``tool_name.replace(/_/g, ' ')``), capitalised: ``todo_write`` →
|
|
81
|
+
``Todo write``. Tools with a dedicated renderer have their own
|
|
82
|
+
friendly labels (``Read file``, ``Run shell command``, …) matching the
|
|
83
|
+
webview's ``TOOL_FRIENDLY_NAMES``.
|
|
84
|
+
"""
|
|
85
|
+
return name.replace("_", " ").capitalize()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def tool_line(
|
|
89
|
+
marker: str,
|
|
90
|
+
label: str,
|
|
91
|
+
value: str = "",
|
|
92
|
+
*,
|
|
93
|
+
color: str,
|
|
94
|
+
suffix: str = "",
|
|
95
|
+
) -> Text:
|
|
96
|
+
"""Build a one-line tool indicator with a clear visual hierarchy.
|
|
97
|
+
|
|
98
|
+
Layout: ``<marker> <label> <value> <suffix>`` where
|
|
99
|
+
|
|
100
|
+
- ``marker`` (``→`` running / ``✓`` done) is bold + ``color`` — the
|
|
101
|
+
category colour is carried only by this two-char glyph.
|
|
102
|
+
- ``label`` (``Read file``, ``Run shell command``…) is **dim** so it
|
|
103
|
+
reads as a quiet caption and no longer competes with the payload.
|
|
104
|
+
- ``value`` (path, command, pattern…) is bold + ``color`` — this is
|
|
105
|
+
the information the user actually scans for.
|
|
106
|
+
- ``suffix`` (line ranges, elapsed time, question counts…) stays dim.
|
|
107
|
+
"""
|
|
108
|
+
line = Text()
|
|
109
|
+
line.append(f"{marker} ", style=f"bold {color}")
|
|
110
|
+
line.append(label, style="dim")
|
|
111
|
+
if value:
|
|
112
|
+
line.append(" ", style="dim")
|
|
113
|
+
line.append(value, style=f"bold {color}")
|
|
114
|
+
if suffix:
|
|
115
|
+
line.append(f" {suffix}", style="dim")
|
|
116
|
+
return line
|
|
117
|
+
|
|
118
|
+
|
|
72
119
|
def default_renderer(event: dict[str, Any]) -> RenderableType | None:
|
|
73
|
-
"""Show a single ``→
|
|
120
|
+
"""Show a single ``→ Tool name`` line on ``tool-input-available``; ignore the rest.
|
|
74
121
|
|
|
75
122
|
Args are intentionally hidden — most tool calls are noisy enough as JSON
|
|
76
123
|
that they hurt readability for the default case. Custom renderers can
|
|
@@ -79,11 +126,8 @@ def default_renderer(event: dict[str, Any]) -> RenderableType | None:
|
|
|
79
126
|
"""
|
|
80
127
|
if event.get("type") != "tool-input-available":
|
|
81
128
|
return None
|
|
82
|
-
name = event.get("toolName", "?")
|
|
83
|
-
|
|
84
|
-
line.append("→ ", style="bold cyan")
|
|
85
|
-
line.append(name, style="cyan")
|
|
86
|
-
return line
|
|
129
|
+
name = friendly_tool_name(event.get("toolName", "?"))
|
|
130
|
+
return tool_line("→", name, color="cyan")
|
|
87
131
|
|
|
88
132
|
|
|
89
133
|
async def default_handler(tool_input: dict[str, Any], ctx: Ctx) -> str:
|
|
@@ -95,7 +139,7 @@ async def default_handler(tool_input: dict[str, Any], ctx: Ctx) -> str:
|
|
|
95
139
|
"""
|
|
96
140
|
try:
|
|
97
141
|
pretty = json.dumps(tool_input, indent=2, ensure_ascii=False)
|
|
98
|
-
except
|
|
142
|
+
except TypeError, ValueError:
|
|
99
143
|
pretty = str(tool_input)
|
|
100
144
|
ctx.console.print(
|
|
101
145
|
Panel(
|
|
@@ -39,7 +39,8 @@ class AgentTransport:
|
|
|
39
39
|
return f"{base}/{chat_id}" if chat_id is not None else base
|
|
40
40
|
|
|
41
41
|
async def stream_completions(
|
|
42
|
-
self,
|
|
42
|
+
self,
|
|
43
|
+
body: dict[str, Any],
|
|
43
44
|
) -> AsyncIterator[dict[str, Any]]:
|
|
44
45
|
"""``POST /{app}/agent/completions/{org}`` → parsed SSE events."""
|
|
45
46
|
async for event in self._http.sse_post(self._path("completions"), body):
|
|
@@ -90,7 +91,10 @@ class AgentTransport:
|
|
|
90
91
|
return list(queue)
|
|
91
92
|
|
|
92
93
|
async def edit_queued(
|
|
93
|
-
self,
|
|
94
|
+
self,
|
|
95
|
+
chat_id: str,
|
|
96
|
+
message_id: str,
|
|
97
|
+
content: str,
|
|
94
98
|
) -> None:
|
|
95
99
|
"""``PUT /queue/{org}/{chat_id}/{msg_id}``."""
|
|
96
100
|
path = f"{self._path('queue', chat_id)}/{message_id}"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""AgentTurn — a container for one agentic REPL turn that can be interrupted.
|
|
2
2
|
|
|
3
3
|
Modeled on the voice pipeline's ``SpeechHandle``
|
|
4
|
-
(``backend/cosmos/api/functions/
|
|
4
|
+
(``backend/cosmos/api/functions/realtime/speech_handle.py``): a job/turn
|
|
5
5
|
container that owns its running task and races it against an interruption
|
|
6
6
|
future, rather than a standalone interrupt flag that the work has to poll.
|
|
7
7
|
|
|
@@ -29,7 +29,10 @@ class Renderer(Protocol):
|
|
|
29
29
|
live updates on the buffer.
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
@property
|
|
33
|
+
def assistant_text(self) -> str:
|
|
34
|
+
"""The assistant text accumulated since the last block commit."""
|
|
35
|
+
...
|
|
33
36
|
|
|
34
37
|
def apply(self, event: dict[str, Any]) -> None:
|
|
35
38
|
"""Handle one streamed event."""
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
"""ChatApp — the delos-cli agent.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
tables. The chat row is INSERT'd at picker time (cf.
|
|
8
|
-
:func:`main._run_repl`), so the agent always has a row to append to.
|
|
9
|
-
* ``local`` — drives :class:`~delos_cli.engine.LocalEngine` in this process,
|
|
10
|
-
reaching the model through ``/openai-proxy/{org}/chat/completions`` and
|
|
11
|
-
persisting to a JSONL transcript instead of Supabase.
|
|
12
|
-
|
|
13
|
-
Both produce the same v6 event stream, so :meth:`send` is the only place the
|
|
14
|
-
choice shows up and the REPL is engine-agnostic.
|
|
3
|
+
Wraps an :class:`AgentSession` against the ``/cli/agent/*`` backend
|
|
4
|
+
endpoints + ``app_cli`` / ``app_cli_messages`` Supabase tables. The
|
|
5
|
+
chat row is INSERT'd at picker time (cf. :func:`main._run_repl`), so by
|
|
6
|
+
the time we get here the agent always has a row to append messages to.
|
|
15
7
|
|
|
16
8
|
Owns the per-app :class:`ToolRegistry` (so users can plug in custom
|
|
17
9
|
renderers / handlers per tool). The :class:`V6Renderer` returned from
|
|
@@ -23,14 +15,13 @@ from __future__ import annotations
|
|
|
23
15
|
import contextlib
|
|
24
16
|
from dataclasses import dataclass, field
|
|
25
17
|
from pathlib import Path
|
|
26
|
-
from typing import TYPE_CHECKING, Any
|
|
18
|
+
from typing import TYPE_CHECKING, Any, override
|
|
27
19
|
|
|
28
20
|
from rich.text import Text
|
|
29
21
|
|
|
30
22
|
from delos_cli.agent import AgentSession, AgentTransport, ToolRegistry
|
|
31
23
|
from delos_cli.apps.base import App
|
|
32
24
|
from delos_cli.apps.replay import replay_messages
|
|
33
|
-
from delos_cli.engine import LocalEngine, Transcript
|
|
34
25
|
from delos_cli.git_context import collect_git_context
|
|
35
26
|
from delos_cli.state import TodoStore
|
|
36
27
|
from delos_cli.tools import (
|
|
@@ -128,40 +119,9 @@ class ChatApp(App):
|
|
|
128
119
|
#: Set by ``main._run_repl`` after the picker resolves a chat_id, so
|
|
129
120
|
#: history is rendered the moment the REPL takes over the screen.
|
|
130
121
|
pending_replay: list[dict[str, Any]] = field(default_factory=list)
|
|
131
|
-
#: ``"backend"`` (loop on the server, tools here) or ``"local"`` (loop
|
|
132
|
-
#: here, LLM through the OpenAI-compatible proxy). Set by ``--engine``.
|
|
133
|
-
engine: str = "backend"
|
|
134
122
|
_current_session: AgentSession | None = field(default=None, init=False, repr=False)
|
|
135
|
-
#: The in-process loop, created lazily per ``conv_id`` (``/clear`` and
|
|
136
|
-
#: ``/resume`` both change it, and each conversation owns a transcript).
|
|
137
|
-
_local: LocalEngine | None = field(default=None, init=False, repr=False)
|
|
138
|
-
#: Set while a local turn is streaming, so ``/stop`` and mid-turn queueing
|
|
139
|
-
#: have something to reach. Mirrors ``_current_session`` for the backend.
|
|
140
|
-
_active_local: LocalEngine | None = field(default=None, init=False, repr=False)
|
|
141
|
-
|
|
142
|
-
@property
|
|
143
|
-
def is_local(self) -> bool:
|
|
144
|
-
"""True when turns run in this process."""
|
|
145
|
-
return self.engine == "local"
|
|
146
|
-
|
|
147
|
-
@property
|
|
148
|
-
def local_engine(self) -> LocalEngine | None:
|
|
149
|
-
"""The bound in-process engine, or ``None`` before the first local turn.
|
|
150
|
-
|
|
151
|
-
Read-only accessor for introspection commands (``/context``).
|
|
152
|
-
"""
|
|
153
|
-
return self._local
|
|
154
|
-
|
|
155
|
-
def _local_engine(self, ctx: Ctx) -> LocalEngine:
|
|
156
|
-
"""The :class:`LocalEngine` bound to the active conversation."""
|
|
157
|
-
conv_id = ctx.state.conv_id
|
|
158
|
-
model = ctx.state.model
|
|
159
|
-
if self._local is None or self._local.conv_id != conv_id:
|
|
160
|
-
self._local = LocalEngine(conv_id=conv_id, model=model)
|
|
161
|
-
elif model and model != self._local.model:
|
|
162
|
-
self._local.set_model(model)
|
|
163
|
-
return self._local
|
|
164
123
|
|
|
124
|
+
@override
|
|
165
125
|
async def on_enter(self, ctx: Ctx) -> None:
|
|
166
126
|
"""Prefetch model list, then replay any staged conversation history."""
|
|
167
127
|
try:
|
|
@@ -170,34 +130,23 @@ class ChatApp(App):
|
|
|
170
130
|
sink = ctx.output if ctx.output is not None else ctx.console
|
|
171
131
|
sink.print(Text(f"(could not fetch models: {e})", style="yellow"))
|
|
172
132
|
|
|
173
|
-
# Local sessions persist to a JSONL transcript, not to
|
|
174
|
-
# ``app_cli_messages`` — so whatever the picker staged from Supabase is
|
|
175
|
-
# irrelevant and the transcript is the history to show.
|
|
176
|
-
if self.is_local:
|
|
177
|
-
self.pending_replay = Transcript.open(ctx.state.conv_id).replay()
|
|
178
|
-
|
|
179
133
|
if self.pending_replay and ctx.output is not None:
|
|
180
134
|
replay_messages(self.pending_replay, self, ctx.output)
|
|
181
135
|
# Always drain — even a failed replay shouldn't replay again next time.
|
|
182
136
|
self.pending_replay = []
|
|
183
137
|
|
|
138
|
+
@override
|
|
184
139
|
async def send(
|
|
185
140
|
self,
|
|
186
141
|
ctx: Ctx,
|
|
187
142
|
messages: list[dict[str, str]],
|
|
188
143
|
) -> AsyncIterator[dict[str, Any]]:
|
|
189
|
-
"""Stream one turn
|
|
144
|
+
"""Stream one turn through :class:`AgentSession` against ``/cli/agent/...``.
|
|
190
145
|
|
|
191
|
-
``
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
the chat — no ``ensure_chat_row`` dance).
|
|
146
|
+
The ``app_cli`` row is created at picker time (or by ``/clear``),
|
|
147
|
+
so the backend always finds the chat when this runs — no
|
|
148
|
+
``ensure_chat_row`` dance.
|
|
195
149
|
"""
|
|
196
|
-
if self.is_local:
|
|
197
|
-
async for event in self._send_local(ctx, messages):
|
|
198
|
-
yield event
|
|
199
|
-
return
|
|
200
|
-
|
|
201
150
|
transport = AgentTransport(ctx.http, "cli", ctx.cfg.org_uuid)
|
|
202
151
|
session = AgentSession(transport, ctx.state.conv_id, self.tools)
|
|
203
152
|
|
|
@@ -228,63 +177,29 @@ class ChatApp(App):
|
|
|
228
177
|
finally:
|
|
229
178
|
self._current_session = None
|
|
230
179
|
|
|
231
|
-
|
|
232
|
-
self,
|
|
233
|
-
ctx: Ctx,
|
|
234
|
-
messages: list[dict[str, str]],
|
|
235
|
-
) -> AsyncIterator[dict[str, Any]]:
|
|
236
|
-
"""Drive the in-process loop for one user turn.
|
|
237
|
-
|
|
238
|
-
The REPL sends exactly one new user message per turn; anything typed
|
|
239
|
-
while the turn runs goes through :meth:`queue_message` and is injected
|
|
240
|
-
at the next batch boundary, so only the last user message matters here.
|
|
241
|
-
"""
|
|
242
|
-
engine = self._local_engine(ctx)
|
|
243
|
-
user_content = next(
|
|
244
|
-
(m.get("content", "") for m in reversed(messages) if m.get("role") == "user"),
|
|
245
|
-
"",
|
|
246
|
-
)
|
|
247
|
-
# Fresh git snapshot per turn — it rides in the turn's user message (not
|
|
248
|
-
# the system prompt) so the cacheable prefix stays byte-stable.
|
|
249
|
-
git_ctx = await collect_git_context(Path.cwd())
|
|
250
|
-
self._active_local = engine
|
|
251
|
-
try:
|
|
252
|
-
async for event in engine.run_turn(ctx, user_content, git_context=git_ctx):
|
|
253
|
-
yield event
|
|
254
|
-
finally:
|
|
255
|
-
self._active_local = None
|
|
256
|
-
|
|
180
|
+
@override
|
|
257
181
|
def make_renderer(self, output: OutputBuffer) -> V6Renderer:
|
|
258
182
|
"""Build a fresh :class:`V6Renderer` wired to this app's tool registry."""
|
|
259
183
|
return V6Renderer(output, self.tools)
|
|
260
184
|
|
|
185
|
+
@override
|
|
261
186
|
async def cancel(self, ctx: Ctx) -> None:
|
|
262
|
-
"""
|
|
187
|
+
"""Send the backend stop signal for the in-flight turn, if any.
|
|
263
188
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
Backend: send the Redis stop signal. No-op when no turn is active —
|
|
267
|
-
:meth:`send` clears its reference as soon as the iterator finishes.
|
|
189
|
+
No-op when no turn is active — :meth:`send` clears the session ref
|
|
190
|
+
as soon as the iterator finishes.
|
|
268
191
|
"""
|
|
269
192
|
_ = ctx
|
|
270
|
-
if self.is_local:
|
|
271
|
-
if self._active_local is not None:
|
|
272
|
-
self._active_local.request_stop()
|
|
273
|
-
return
|
|
274
193
|
if self._current_session is None:
|
|
275
194
|
return
|
|
276
195
|
# Best-effort — the loop has already shown an interrupt message.
|
|
277
196
|
with contextlib.suppress(TransportError):
|
|
278
197
|
await self._current_session.stop()
|
|
279
198
|
|
|
199
|
+
@override
|
|
280
200
|
async def queue_message(self, ctx: Ctx, content: str) -> bool:
|
|
281
|
-
"""
|
|
201
|
+
"""Append a message to the backend's FIFO queue while a turn is running."""
|
|
282
202
|
_ = ctx
|
|
283
|
-
if self.is_local:
|
|
284
|
-
if self._active_local is None:
|
|
285
|
-
return False
|
|
286
|
-
self._active_local.queue_message(content)
|
|
287
|
-
return True
|
|
288
203
|
if self._current_session is None:
|
|
289
204
|
return False
|
|
290
205
|
try:
|
|
@@ -293,10 +208,9 @@ class ChatApp(App):
|
|
|
293
208
|
return False
|
|
294
209
|
return True
|
|
295
210
|
|
|
211
|
+
@override
|
|
296
212
|
def toolbar_fragment(self, ctx: Ctx) -> str:
|
|
297
|
-
"""Show the active model
|
|
213
|
+
"""Show the active model so the user can see what ``/model`` would change."""
|
|
214
|
+
_ = self
|
|
298
215
|
model = ctx.state.model
|
|
299
|
-
|
|
300
|
-
if self.is_local:
|
|
301
|
-
parts.append("engine: local")
|
|
302
|
-
return " ".join(parts)
|
|
216
|
+
return f" model: {model}" if model else ""
|