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.
Files changed (43) hide show
  1. delos_cli-0.1.0/.gitignore +145 -0
  2. delos_cli-0.1.0/PKG-INFO +104 -0
  3. delos_cli-0.1.0/README.md +75 -0
  4. delos_cli-0.1.0/delos_cli/__init__.py +3 -0
  5. delos_cli-0.1.0/delos_cli/agent/__init__.py +34 -0
  6. delos_cli-0.1.0/delos_cli/agent/session.py +111 -0
  7. delos_cli-0.1.0/delos_cli/agent/tools.py +131 -0
  8. delos_cli-0.1.0/delos_cli/agent/transport.py +102 -0
  9. delos_cli-0.1.0/delos_cli/apps/__init__.py +6 -0
  10. delos_cli-0.1.0/delos_cli/apps/base.py +101 -0
  11. delos_cli-0.1.0/delos_cli/apps/chat/__init__.py +5 -0
  12. delos_cli-0.1.0/delos_cli/apps/chat/app.py +149 -0
  13. delos_cli-0.1.0/delos_cli/apps/chat/commands.py +17 -0
  14. delos_cli-0.1.0/delos_cli/apps/chat/render.py +188 -0
  15. delos_cli-0.1.0/delos_cli/apps/chat/replay.py +108 -0
  16. delos_cli-0.1.0/delos_cli/auth/__init__.py +24 -0
  17. delos_cli-0.1.0/delos_cli/auth/config.py +282 -0
  18. delos_cli-0.1.0/delos_cli/auth/mfa.py +120 -0
  19. delos_cli-0.1.0/delos_cli/auth/oauth.py +336 -0
  20. delos_cli-0.1.0/delos_cli/auth/token_manager.py +136 -0
  21. delos_cli-0.1.0/delos_cli/commands/__init__.py +10 -0
  22. delos_cli-0.1.0/delos_cli/commands/base.py +54 -0
  23. delos_cli-0.1.0/delos_cli/commands/builtin.py +160 -0
  24. delos_cli-0.1.0/delos_cli/ctx.py +65 -0
  25. delos_cli-0.1.0/delos_cli/loop.py +19 -0
  26. delos_cli-0.1.0/delos_cli/main.py +230 -0
  27. delos_cli-0.1.0/delos_cli/state.py +28 -0
  28. delos_cli-0.1.0/delos_cli/tools/__init__.py +20 -0
  29. delos_cli-0.1.0/delos_cli/tools/edit_content.py +193 -0
  30. delos_cli-0.1.0/delos_cli/tools/run_shell.py +150 -0
  31. delos_cli-0.1.0/delos_cli/tools/write_content.py +120 -0
  32. delos_cli-0.1.0/delos_cli/transport/__init__.py +24 -0
  33. delos_cli-0.1.0/delos_cli/transport/chats.py +235 -0
  34. delos_cli-0.1.0/delos_cli/transport/client.py +321 -0
  35. delos_cli-0.1.0/delos_cli/transport/models.py +19 -0
  36. delos_cli-0.1.0/delos_cli/ui/__init__.py +6 -0
  37. delos_cli-0.1.0/delos_cli/ui/chat_picker.py +151 -0
  38. delos_cli-0.1.0/delos_cli/ui/completer.py +68 -0
  39. delos_cli-0.1.0/delos_cli/ui/lexer.py +62 -0
  40. delos_cli-0.1.0/delos_cli/ui/output.py +180 -0
  41. delos_cli-0.1.0/delos_cli/ui/repl.py +679 -0
  42. delos_cli-0.1.0/delos_cli/ui/style.py +24 -0
  43. 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
@@ -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,3 @@
1
+ """Delos CLI — terminal REPL for the Cosmos chat agent."""
2
+
3
+ __version__ = "0.1.0"
@@ -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)