mycode-cli 0.17.0__py3-none-any.whl → 0.19.0__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.
- mycode_cli/config.py +57 -108
- mycode_cli/main.py +17 -33
- mycode_cli/runtime.py +6 -11
- mycode_cli/server/routers/chat.py +108 -22
- mycode_cli/server/routers/sessions.py +28 -14
- mycode_cli/server/run_manager.py +161 -40
- mycode_cli/server/schemas.py +26 -14
- mycode_cli/server/static/assets/index-CrhooK9g.js +395 -0
- mycode_cli/server/static/assets/index-D9h4EXyc.css +1 -0
- mycode_cli/server/static/index.html +2 -2
- mycode_cli/sessions.py +82 -33
- mycode_cli/state.py +105 -0
- mycode_cli/system_prompt.py +11 -1
- mycode_cli/tools.py +11 -4
- mycode_cli/tui/chat.py +300 -352
- mycode_cli/tui/markdown.py +362 -0
- mycode_cli/tui/render.py +550 -572
- mycode_cli/tui/terminal.py +675 -0
- mycode_cli/tui/theme.py +72 -89
- {mycode_cli-0.17.0.dist-info → mycode_cli-0.19.0.dist-info}/METADATA +36 -25
- {mycode_cli-0.17.0.dist-info → mycode_cli-0.19.0.dist-info}/RECORD +24 -22
- mycode_cli/server/static/assets/index-D71Nwfrp.css +0 -1
- mycode_cli/server/static/assets/index-DCe0gQLC.js +0 -389
- mycode_cli/tui/state.py +0 -38
- {mycode_cli-0.17.0.dist-info → mycode_cli-0.19.0.dist-info}/WHEEL +0 -0
- {mycode_cli-0.17.0.dist-info → mycode_cli-0.19.0.dist-info}/entry_points.txt +0 -0
- {mycode_cli-0.17.0.dist-info → mycode_cli-0.19.0.dist-info}/licenses/LICENSE +0 -0
mycode_cli/config.py
CHANGED
|
@@ -99,8 +99,6 @@ class WebConfig:
|
|
|
99
99
|
@dataclass(frozen=True)
|
|
100
100
|
class Settings:
|
|
101
101
|
providers: dict[str, ProviderConfig]
|
|
102
|
-
default_provider: str | None
|
|
103
|
-
default_model: str | None
|
|
104
102
|
port: int
|
|
105
103
|
cwd: str
|
|
106
104
|
project: str
|
|
@@ -305,10 +303,14 @@ def _clean_config_layer(data: Any) -> dict[str, Any]:
|
|
|
305
303
|
|
|
306
304
|
out: dict[str, Any] = {}
|
|
307
305
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
306
|
+
compact_threshold = data.get("compact_threshold")
|
|
307
|
+
if compact_threshold is False:
|
|
308
|
+
out["compact_threshold"] = False
|
|
309
|
+
elif compact_threshold is not None:
|
|
310
|
+
parsed = None if isinstance(compact_threshold, bool) else parse_compact_threshold(compact_threshold)
|
|
311
|
+
if parsed is None:
|
|
312
|
+
raise ValueError("compact_threshold must be a number in [0, 1] or false")
|
|
313
|
+
out["compact_threshold"] = parsed
|
|
312
314
|
|
|
313
315
|
if data.get("permission") is not None:
|
|
314
316
|
out["permission"] = _validate_permission_config(data["permission"])
|
|
@@ -345,30 +347,6 @@ def _optional_config_string(raw: dict[str, Any], key: str, label: str) -> str |
|
|
|
345
347
|
return value.strip() or None
|
|
346
348
|
|
|
347
349
|
|
|
348
|
-
def _validate_default_config(raw: Any) -> dict[str, Any]:
|
|
349
|
-
if not isinstance(raw, dict):
|
|
350
|
-
raise ValueError("default must be an object")
|
|
351
|
-
|
|
352
|
-
out: dict[str, Any] = {}
|
|
353
|
-
for key in ("provider", "model"):
|
|
354
|
-
value = _optional_config_string(raw, key, f"default.{key}")
|
|
355
|
-
if value:
|
|
356
|
-
out[key] = value
|
|
357
|
-
|
|
358
|
-
compact_threshold = raw.get("compact_threshold")
|
|
359
|
-
if compact_threshold is False:
|
|
360
|
-
out["compact_threshold"] = False
|
|
361
|
-
elif compact_threshold is not None:
|
|
362
|
-
if isinstance(compact_threshold, bool):
|
|
363
|
-
raise ValueError("default.compact_threshold must be a number in [0, 1] or false")
|
|
364
|
-
parsed = parse_compact_threshold(compact_threshold)
|
|
365
|
-
if parsed is None:
|
|
366
|
-
raise ValueError("default.compact_threshold must be a number in [0, 1] or false")
|
|
367
|
-
out["compact_threshold"] = parsed
|
|
368
|
-
|
|
369
|
-
return out
|
|
370
|
-
|
|
371
|
-
|
|
372
350
|
def _validate_permission_config(raw: Any) -> PermissionLevel | dict[str, Any]:
|
|
373
351
|
if isinstance(raw, str):
|
|
374
352
|
return normalize_permission_level(raw)
|
|
@@ -498,8 +476,6 @@ def get_settings(cwd: str | None = None) -> Settings:
|
|
|
498
476
|
resolved_project = str(resolve_project(resolved_cwd))
|
|
499
477
|
|
|
500
478
|
raw_providers: dict[str, dict[str, Any]] = {}
|
|
501
|
-
default_provider: str | None = None
|
|
502
|
-
default_model: str | None = None
|
|
503
479
|
compact_threshold: float | None = None
|
|
504
480
|
permission = PermissionConfig()
|
|
505
481
|
raw_web: dict[str, Any] = {}
|
|
@@ -534,13 +510,8 @@ def get_settings(cwd: str | None = None) -> Settings:
|
|
|
534
510
|
|
|
535
511
|
raw_providers[name] = merged
|
|
536
512
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
default_provider = default["provider"]
|
|
540
|
-
if "model" in default:
|
|
541
|
-
default_model = default["model"]
|
|
542
|
-
if "compact_threshold" in default:
|
|
543
|
-
compact_threshold = float(default["compact_threshold"])
|
|
513
|
+
if "compact_threshold" in data:
|
|
514
|
+
compact_threshold = float(data["compact_threshold"])
|
|
544
515
|
|
|
545
516
|
if "permission" in data:
|
|
546
517
|
permission = parse_permission(data.get("permission"), permission)
|
|
@@ -561,8 +532,6 @@ def get_settings(cwd: str | None = None) -> Settings:
|
|
|
561
532
|
|
|
562
533
|
return Settings(
|
|
563
534
|
providers=_build_providers(raw_providers),
|
|
564
|
-
default_provider=default_provider,
|
|
565
|
-
default_model=default_model,
|
|
566
535
|
compact_threshold=compact_threshold,
|
|
567
536
|
permission=permission,
|
|
568
537
|
web=_build_web_config(raw_web),
|
|
@@ -691,44 +660,35 @@ def resolve_provider(
|
|
|
691
660
|
api_key: str | None = None,
|
|
692
661
|
api_base: str | None = None,
|
|
693
662
|
) -> ResolvedProvider:
|
|
694
|
-
"""Resolve provider, model, api_key, and api_base from settings and overrides.
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
663
|
+
"""Resolve provider, model, api_key, and api_base from settings and overrides.
|
|
664
|
+
|
|
665
|
+
Without a provider name, the first available provider is used.
|
|
666
|
+
"""
|
|
667
|
+
|
|
668
|
+
selected_name = (provider_name or "").strip()
|
|
669
|
+
if not selected_name:
|
|
670
|
+
refs = _available_provider_references(settings)
|
|
671
|
+
if not refs:
|
|
672
|
+
env_names = list(
|
|
673
|
+
dict.fromkeys(
|
|
674
|
+
env_name
|
|
675
|
+
for provider_id in list_env_discoverable_providers()
|
|
676
|
+
for env_name in provider_env_api_key_names(provider_id)
|
|
677
|
+
)
|
|
705
678
|
)
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
if refs:
|
|
713
|
-
return _resolve_provider_runtime(
|
|
714
|
-
settings,
|
|
715
|
-
selected_name=refs[0],
|
|
716
|
-
model=model,
|
|
717
|
-
api_key=api_key,
|
|
718
|
-
api_base=api_base,
|
|
719
|
-
)
|
|
679
|
+
checked = ", ".join(env_names) or "<api key env>"
|
|
680
|
+
raise ValueError(
|
|
681
|
+
"no available providers found; set one of the supported API key env vars "
|
|
682
|
+
+ f"({checked}) or configure a provider in ~/.mycode/config.json or a project .mycode/config.json"
|
|
683
|
+
)
|
|
684
|
+
selected_name = refs[0]
|
|
720
685
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
)
|
|
728
|
-
checked = ", ".join(env_names) or "<api key env>"
|
|
729
|
-
raise ValueError(
|
|
730
|
-
"no available providers found; set one of the supported API key env vars "
|
|
731
|
-
+ f"({checked}) or configure a provider in ~/.mycode/config.json or a project .mycode/config.json"
|
|
686
|
+
return _resolve_provider_runtime(
|
|
687
|
+
settings,
|
|
688
|
+
selected_name=selected_name,
|
|
689
|
+
model=model,
|
|
690
|
+
api_key=api_key,
|
|
691
|
+
api_base=api_base,
|
|
732
692
|
)
|
|
733
693
|
|
|
734
694
|
|
|
@@ -744,41 +704,32 @@ def resolve_provider_choices(settings: Settings) -> list[ResolvedProvider]:
|
|
|
744
704
|
return choices
|
|
745
705
|
|
|
746
706
|
|
|
747
|
-
def
|
|
748
|
-
"""Return
|
|
749
|
-
|
|
750
|
-
available: list[str] = []
|
|
751
|
-
seen: set[str] = set()
|
|
752
|
-
configured_types_with_credentials: set[str] = set()
|
|
707
|
+
def provider_models(settings: Settings, provider: ResolvedProvider) -> list[str]:
|
|
708
|
+
"""Return a resolved provider's selectable models: configured ones, else the adapter defaults."""
|
|
753
709
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
710
|
+
provider_config = settings.providers.get(provider.provider_name or "")
|
|
711
|
+
if provider_config and provider_config.models:
|
|
712
|
+
return list(provider_config.models)
|
|
713
|
+
return list(provider_default_models(provider.provider)) or [provider.model]
|
|
758
714
|
|
|
759
|
-
provider_config = settings.providers.get(cleaned)
|
|
760
|
-
provider_type = provider_config.type if provider_config else cleaned
|
|
761
|
-
if not is_supported_provider(provider_type):
|
|
762
|
-
return
|
|
763
715
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
return
|
|
767
|
-
configured_types_with_credentials.add(provider_type)
|
|
768
|
-
elif not provider_api_key_from_env(provider_type):
|
|
769
|
-
return
|
|
770
|
-
|
|
771
|
-
seen.add(cleaned)
|
|
772
|
-
available.append(cleaned)
|
|
773
|
-
|
|
774
|
-
add(settings.default_provider)
|
|
716
|
+
def _available_provider_references(settings: Settings) -> list[str]:
|
|
717
|
+
"""Return usable provider names: configured providers in order, then env-discovered built-ins."""
|
|
775
718
|
|
|
776
|
-
|
|
777
|
-
|
|
719
|
+
available: list[str] = []
|
|
720
|
+
configured_types: set[str] = set()
|
|
721
|
+
for name, config in settings.providers.items():
|
|
722
|
+
if is_supported_provider(config.type) and provider_is_available(config):
|
|
723
|
+
available.append(name)
|
|
724
|
+
configured_types.add(config.type)
|
|
778
725
|
|
|
779
726
|
for provider_id in list_env_discoverable_providers():
|
|
780
|
-
if
|
|
781
|
-
|
|
727
|
+
if (
|
|
728
|
+
provider_id not in configured_types
|
|
729
|
+
and provider_id not in settings.providers
|
|
730
|
+
and provider_api_key_from_env(provider_id)
|
|
731
|
+
):
|
|
732
|
+
available.append(provider_id)
|
|
782
733
|
|
|
783
734
|
return available
|
|
784
735
|
|
|
@@ -805,8 +756,6 @@ def _resolve_provider_runtime(
|
|
|
805
756
|
resolved_model = requested_model
|
|
806
757
|
elif provider_config and provider_config.models:
|
|
807
758
|
resolved_model = next(iter(provider_config.models))
|
|
808
|
-
elif selected_name == settings.default_provider and (settings.default_model or "").strip():
|
|
809
|
-
resolved_model = str(settings.default_model).strip()
|
|
810
759
|
else:
|
|
811
760
|
defaults = provider_default_models(provider_type)
|
|
812
761
|
if not defaults:
|
mycode_cli/main.py
CHANGED
|
@@ -5,9 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
|
-
from contextlib import aclosing
|
|
8
|
+
from contextlib import aclosing
|
|
9
9
|
from dataclasses import dataclass, replace
|
|
10
|
-
from typing import
|
|
10
|
+
from typing import Annotated, Any, Literal
|
|
11
11
|
from uuid import uuid4
|
|
12
12
|
|
|
13
13
|
import typer
|
|
@@ -21,17 +21,14 @@ from mycode_cli.config import (
|
|
|
21
21
|
get_settings,
|
|
22
22
|
normalize_reasoning_effort,
|
|
23
23
|
parse_permission,
|
|
24
|
-
resolve_provider,
|
|
25
24
|
resolve_sessions_dir,
|
|
26
25
|
)
|
|
27
26
|
from mycode_cli.permissions import PERMISSION_DENIED_BY_USER_OUTPUT, PERMISSION_DENIED_OUTPUT
|
|
28
27
|
from mycode_cli.sessions import SessionStore
|
|
28
|
+
from mycode_cli.state import load_state, resolve_remembered_provider
|
|
29
29
|
|
|
30
30
|
from .runtime import build_agent
|
|
31
31
|
|
|
32
|
-
if TYPE_CHECKING:
|
|
33
|
-
from .tui.render import TerminalView
|
|
34
|
-
|
|
35
32
|
app = typer.Typer(add_completion=False, pretty_exceptions_enable=False)
|
|
36
33
|
session_app = typer.Typer(help="Session management")
|
|
37
34
|
app.add_typer(session_app, name="session")
|
|
@@ -140,7 +137,6 @@ async def run_noninteractive(agent: Agent, message: str, *, store: SessionStore,
|
|
|
140
137
|
@dataclass
|
|
141
138
|
class _BootstrapContext:
|
|
142
139
|
store: SessionStore
|
|
143
|
-
view: TerminalView
|
|
144
140
|
settings: Settings
|
|
145
141
|
resolved_provider: ResolvedProvider
|
|
146
142
|
resolved_session: ResolvedSession
|
|
@@ -159,21 +155,20 @@ def _bootstrap(
|
|
|
159
155
|
) -> _BootstrapContext:
|
|
160
156
|
"""Shared setup for the chat and run commands."""
|
|
161
157
|
|
|
162
|
-
from .tui.render import TerminalView
|
|
163
|
-
|
|
164
158
|
if session and continue_last:
|
|
165
159
|
raise typer.BadParameter("--session and --continue are mutually exclusive")
|
|
166
160
|
|
|
167
161
|
cwd = os.path.abspath(os.getcwd())
|
|
168
162
|
store = SessionStore(data_dir=resolve_sessions_dir())
|
|
169
|
-
view = TerminalView()
|
|
170
163
|
try:
|
|
171
164
|
settings = get_settings(cwd)
|
|
172
165
|
if permission is not None:
|
|
173
166
|
settings = replace(settings, permission=parse_permission(permission, settings.permission))
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if
|
|
167
|
+
state = load_state()
|
|
168
|
+
resolved_provider = resolve_remembered_provider(settings, state, provider_name=provider, model=model)
|
|
169
|
+
if reasoning_effort is None:
|
|
170
|
+
resolved_effort = state.effort_for(resolved_provider)
|
|
171
|
+
elif (resolved_effort := normalize_reasoning_effort(reasoning_effort)) is not None:
|
|
177
172
|
if not resolved_provider.supports_reasoning_effort:
|
|
178
173
|
raise ValueError(f"provider {resolved_provider.provider!r} does not support reasoning effort")
|
|
179
174
|
if resolved_effort not in resolved_provider.reasoning_efforts:
|
|
@@ -182,6 +177,7 @@ def _bootstrap(
|
|
|
182
177
|
f"reasoning effort {resolved_effort!r} is not supported by model {resolved_provider.model!r}; "
|
|
183
178
|
+ f"supported efforts: {supported}"
|
|
184
179
|
)
|
|
180
|
+
resolved_provider = replace(resolved_provider, reasoning_effort=resolved_effort)
|
|
185
181
|
resolved_session = asyncio.run(
|
|
186
182
|
resolve_session(
|
|
187
183
|
store=store,
|
|
@@ -191,7 +187,9 @@ def _bootstrap(
|
|
|
191
187
|
)
|
|
192
188
|
)
|
|
193
189
|
except ValueError as exc:
|
|
194
|
-
|
|
190
|
+
from .tui.render import console, error_line
|
|
191
|
+
|
|
192
|
+
console.print(error_line(str(exc)))
|
|
195
193
|
raise SystemExit(1) from exc
|
|
196
194
|
|
|
197
195
|
agent = build_agent(
|
|
@@ -202,12 +200,9 @@ def _bootstrap(
|
|
|
202
200
|
session_id=resolved_session.session_id,
|
|
203
201
|
max_turns=max_turns,
|
|
204
202
|
)
|
|
205
|
-
if reasoning_effort is not None:
|
|
206
|
-
agent.reasoning_effort = resolved_effort
|
|
207
203
|
|
|
208
204
|
return _BootstrapContext(
|
|
209
205
|
store=store,
|
|
210
|
-
view=view,
|
|
211
206
|
settings=settings,
|
|
212
207
|
resolved_provider=resolved_provider,
|
|
213
208
|
resolved_session=resolved_session,
|
|
@@ -266,22 +261,11 @@ def chat(
|
|
|
266
261
|
session_id=setup.resolved_session.session_id,
|
|
267
262
|
provider_name=setup.resolved_provider.provider_name,
|
|
268
263
|
reasoning_efforts=setup.resolved_provider.reasoning_efforts,
|
|
269
|
-
view=setup.view,
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
setup.view.print_header(
|
|
273
|
-
provider=setup.resolved_provider.provider,
|
|
274
|
-
model=setup.resolved_provider.model,
|
|
275
264
|
session=setup.resolved_session.session,
|
|
276
265
|
mode=setup.resolved_session.mode,
|
|
277
|
-
|
|
278
|
-
reasoning_effort=setup.agent.reasoning_effort,
|
|
266
|
+
messages=setup.resolved_session.messages,
|
|
279
267
|
)
|
|
280
|
-
|
|
281
|
-
setup.view.print_history_preview(setup.resolved_session.messages)
|
|
282
|
-
|
|
283
|
-
with suppress(KeyboardInterrupt):
|
|
284
|
-
asyncio.run(terminal_chat.run())
|
|
268
|
+
asyncio.run(terminal_chat.run())
|
|
285
269
|
|
|
286
270
|
|
|
287
271
|
@app.command()
|
|
@@ -355,15 +339,15 @@ def session_list(
|
|
|
355
339
|
) -> None:
|
|
356
340
|
"""List saved sessions."""
|
|
357
341
|
|
|
358
|
-
from .tui.render import
|
|
342
|
+
from .tui.render import console, session_list_table
|
|
359
343
|
|
|
360
344
|
cwd = os.path.abspath(os.getcwd())
|
|
361
345
|
store = SessionStore(data_dir=resolve_sessions_dir())
|
|
362
|
-
view = TerminalView()
|
|
363
346
|
|
|
364
347
|
sessions = asyncio.run(store.list_sessions(cwd=None if all_workspaces else cwd))
|
|
365
348
|
heading = "all sessions" if all_workspaces else f"sessions for {cwd}"
|
|
366
|
-
|
|
349
|
+
for renderable in session_list_table(sessions, include_cwd=all_workspaces, heading=heading):
|
|
350
|
+
console.print(renderable)
|
|
367
351
|
|
|
368
352
|
|
|
369
353
|
def main() -> None:
|
mycode_cli/runtime.py
CHANGED
|
@@ -5,26 +5,21 @@ from __future__ import annotations
|
|
|
5
5
|
from mycode.agent import Agent
|
|
6
6
|
from mycode_cli.config import ResolvedProvider, Settings
|
|
7
7
|
from mycode_cli.permissions import ToolReviewCallback, build_permission_hooks
|
|
8
|
-
from mycode_cli.sessions import SessionStore,
|
|
8
|
+
from mycode_cli.sessions import SessionStore, SessionTotals, sum_session_totals
|
|
9
9
|
from mycode_cli.system_prompt import build_system_prompt
|
|
10
10
|
from mycode_cli.tools import DEFAULT_TOOLS
|
|
11
11
|
from mycode_cli.web_tools import build_web_tools
|
|
12
12
|
from mycode_cli.workspace import CliDeps
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def
|
|
16
|
-
|
|
17
|
-
return sum(known) if known else None
|
|
15
|
+
async def load_session_totals(store: SessionStore, session_id: str) -> SessionTotals:
|
|
16
|
+
"""Load the session's cumulative usage and USD cost from its raw JSONL timeline.
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"""Load the session's cumulative USD cost from its raw JSONL timeline.
|
|
22
|
-
|
|
23
|
-
Counts persisted costs from tool loops, compact summaries, and turns
|
|
24
|
-
discarded by rewind. Records without a cost are skipped.
|
|
18
|
+
Counts tool loops, compact summaries, and turns discarded by rewind.
|
|
19
|
+
Records without usage or cost contribute nothing to that total.
|
|
25
20
|
"""
|
|
26
21
|
|
|
27
|
-
return
|
|
22
|
+
return sum_session_totals(await store.load_raw_messages(session_id))
|
|
28
23
|
|
|
29
24
|
|
|
30
25
|
def build_agent(
|
|
@@ -21,7 +21,7 @@ from mycode.attachments import (
|
|
|
21
21
|
detect_document_mime_type,
|
|
22
22
|
detect_image_mime_type,
|
|
23
23
|
)
|
|
24
|
-
from mycode.compact import has_compactable_history
|
|
24
|
+
from mycode.compact import DEFAULT_COMPACT_THRESHOLD, has_compactable_history
|
|
25
25
|
from mycode.messages import (
|
|
26
26
|
ConversationMessage,
|
|
27
27
|
build_message,
|
|
@@ -30,29 +30,32 @@ from mycode.messages import (
|
|
|
30
30
|
image_block,
|
|
31
31
|
text_block,
|
|
32
32
|
)
|
|
33
|
-
from mycode.
|
|
33
|
+
from mycode.models import ModelMetadata
|
|
34
34
|
from mycode_cli.config import (
|
|
35
35
|
ResolvedProvider,
|
|
36
36
|
get_settings,
|
|
37
37
|
normalize_reasoning_effort,
|
|
38
|
+
provider_models,
|
|
38
39
|
resolve_configured_model_metadata,
|
|
39
40
|
resolve_provider,
|
|
40
41
|
resolve_provider_choices,
|
|
41
42
|
)
|
|
42
43
|
from mycode_cli.permissions import ToolReviewDecision, ToolReviewRequest
|
|
43
|
-
from mycode_cli.runtime import build_agent,
|
|
44
|
+
from mycode_cli.runtime import build_agent, load_session_totals
|
|
44
45
|
from mycode_cli.server.deps import RunManagerDep, StoreDep, resolve_workspace_cwd
|
|
45
|
-
from mycode_cli.server.run_manager import ActiveRunError
|
|
46
|
+
from mycode_cli.server.run_manager import ActiveRunError, RunAgent
|
|
46
47
|
from mycode_cli.server.schemas import (
|
|
47
48
|
CancelRunResponse,
|
|
48
49
|
ChatRequest,
|
|
49
50
|
ChatResponse,
|
|
50
51
|
CompactRequest,
|
|
51
|
-
CompactResponse,
|
|
52
52
|
DecideRequest,
|
|
53
|
+
PendingInputRequest,
|
|
53
54
|
RunInfo,
|
|
55
|
+
RunResponse,
|
|
54
56
|
StatusResponse,
|
|
55
57
|
StreamEvent,
|
|
58
|
+
UserInputRequest,
|
|
56
59
|
)
|
|
57
60
|
from mycode_cli.system_prompt import build_skill_snapshot_blocks, discover_slash_skills
|
|
58
61
|
from mycode_cli.workspace import resolve_path
|
|
@@ -86,7 +89,7 @@ def _read_workspace_text_attachment(rel_path: str, *, name: str | None, cwd: str
|
|
|
86
89
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
87
90
|
|
|
88
91
|
|
|
89
|
-
async def _build_user_message(chat:
|
|
92
|
+
async def _build_user_message(chat: UserInputRequest, cwd: str) -> ConversationMessage:
|
|
90
93
|
if not chat.input:
|
|
91
94
|
text = str(chat.message or "").strip()
|
|
92
95
|
snapshots = await asyncio.to_thread(build_skill_snapshot_blocks, text, cwd)
|
|
@@ -143,6 +146,14 @@ async def _build_user_message(chat: ChatRequest, cwd: str) -> ConversationMessag
|
|
|
143
146
|
return build_message("user", blocks)
|
|
144
147
|
|
|
145
148
|
|
|
149
|
+
def _check_input_support(message: ConversationMessage, model: ModelMetadata | RunAgent) -> None:
|
|
150
|
+
content_types = {b.get("type") for b in (message.get("content") or []) if isinstance(b, dict)}
|
|
151
|
+
if "image" in content_types and not model.supports_image_input:
|
|
152
|
+
raise HTTPException(status_code=400, detail="current model does not support image input")
|
|
153
|
+
if "document" in content_types and not model.supports_pdf_input:
|
|
154
|
+
raise HTTPException(status_code=400, detail="current model does not support PDF input")
|
|
155
|
+
|
|
156
|
+
|
|
146
157
|
def _validate_rewind_request(
|
|
147
158
|
*,
|
|
148
159
|
session: dict[str, Any] | None,
|
|
@@ -192,11 +203,7 @@ async def chat(chat: ChatRequest, store: StoreDep, runs: RunManagerDep) -> ChatR
|
|
|
192
203
|
model=resolved.model,
|
|
193
204
|
model_config=model_config,
|
|
194
205
|
)
|
|
195
|
-
|
|
196
|
-
if "image" in content_types and not model_meta.supports_image_input:
|
|
197
|
-
raise HTTPException(status_code=400, detail="current model does not support image input")
|
|
198
|
-
if "document" in content_types and not model_meta.supports_pdf_input:
|
|
199
|
-
raise HTTPException(status_code=400, detail="current model does not support PDF input")
|
|
206
|
+
_check_input_support(user_message, model_meta)
|
|
200
207
|
|
|
201
208
|
reasoning_effort = resolved.reasoning_effort
|
|
202
209
|
if "reasoning_effort" in chat.model_fields_set:
|
|
@@ -274,10 +281,11 @@ async def chat(chat: ChatRequest, store: StoreDep, runs: RunManagerDep) -> ChatR
|
|
|
274
281
|
try:
|
|
275
282
|
run = await runs.start_run(
|
|
276
283
|
session_id=session_id,
|
|
284
|
+
cwd=cwd,
|
|
277
285
|
user_message=user_message,
|
|
278
286
|
base_messages=agent.messages,
|
|
279
287
|
agent=agent,
|
|
280
|
-
|
|
288
|
+
session_base=await load_session_totals(store, session_id),
|
|
281
289
|
)
|
|
282
290
|
except ActiveRunError as exc:
|
|
283
291
|
existing = await runs.get_run(exc.run_id)
|
|
@@ -339,7 +347,7 @@ async def compact_session(
|
|
|
339
347
|
body: CompactRequest,
|
|
340
348
|
store: StoreDep,
|
|
341
349
|
runs: RunManagerDep,
|
|
342
|
-
) ->
|
|
350
|
+
) -> RunResponse:
|
|
343
351
|
"""Start a compact run: summarize the session and append one compact marker."""
|
|
344
352
|
|
|
345
353
|
async with runs.session_operation(session_id):
|
|
@@ -372,9 +380,10 @@ async def compact_session(
|
|
|
372
380
|
try:
|
|
373
381
|
run = await runs.start_compact(
|
|
374
382
|
session_id=session_id,
|
|
383
|
+
cwd=cwd,
|
|
375
384
|
base_messages=agent.messages,
|
|
376
385
|
agent=agent,
|
|
377
|
-
|
|
386
|
+
session_base=data["totals"],
|
|
378
387
|
on_complete=store.touch,
|
|
379
388
|
)
|
|
380
389
|
except ActiveRunError as exc:
|
|
@@ -384,7 +393,86 @@ async def compact_session(
|
|
|
384
393
|
detail["run"] = existing.info()
|
|
385
394
|
raise HTTPException(status_code=409, detail=detail) from exc
|
|
386
395
|
|
|
387
|
-
return
|
|
396
|
+
return RunResponse(run=RunInfo.model_validate(run))
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@router.post("/runs/{run_id}/steer")
|
|
400
|
+
async def steer_run(
|
|
401
|
+
run_id: Annotated[str, PathParam(min_length=1)], body: PendingInputRequest, runs: RunManagerDep
|
|
402
|
+
) -> RunResponse:
|
|
403
|
+
"""Hand a user message to the running chat for its next step boundary."""
|
|
404
|
+
|
|
405
|
+
state = await runs.get_run(run_id)
|
|
406
|
+
if state is None:
|
|
407
|
+
raise HTTPException(status_code=404, detail="run not found")
|
|
408
|
+
message = await _build_user_message(body, state.cwd)
|
|
409
|
+
message["meta"] = {"input_id": body.input_id}
|
|
410
|
+
_check_input_support(message, state.agent)
|
|
411
|
+
if not state.agent.steer(message):
|
|
412
|
+
raise HTTPException(
|
|
413
|
+
status_code=409,
|
|
414
|
+
detail={"message": "run is not accepting steers", "run": state.info()},
|
|
415
|
+
)
|
|
416
|
+
return RunResponse(run=RunInfo.model_validate(state.info()))
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@router.post("/sessions/{session_id}/queue")
|
|
420
|
+
async def queue_message(
|
|
421
|
+
session_id: Annotated[str, PathParam(min_length=1)],
|
|
422
|
+
body: PendingInputRequest,
|
|
423
|
+
store: StoreDep,
|
|
424
|
+
runs: RunManagerDep,
|
|
425
|
+
) -> RunResponse:
|
|
426
|
+
"""Queue a user message as the next turn of the session's running chat."""
|
|
427
|
+
|
|
428
|
+
state = await runs.get_active_run(session_id)
|
|
429
|
+
if state is None:
|
|
430
|
+
raise HTTPException(status_code=409, detail={"message": "session has no running chat to queue on"})
|
|
431
|
+
message = await _build_user_message(body, state.cwd)
|
|
432
|
+
message["meta"] = {"input_id": body.input_id}
|
|
433
|
+
_check_input_support(message, state.agent)
|
|
434
|
+
if not await runs.enqueue(state, message):
|
|
435
|
+
raise HTTPException(status_code=409, detail={"message": "session has no running chat to queue on"})
|
|
436
|
+
await store.record_user_turn(
|
|
437
|
+
session_id,
|
|
438
|
+
cwd=state.cwd,
|
|
439
|
+
text=flatten_message_text(message, include_thinking=False),
|
|
440
|
+
)
|
|
441
|
+
return RunResponse(run=RunInfo.model_validate(state.info()))
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@router.delete("/sessions/{session_id}/queue/{input_id}")
|
|
445
|
+
async def remove_queued_message(
|
|
446
|
+
session_id: Annotated[str, PathParam(min_length=1)],
|
|
447
|
+
input_id: Annotated[str, PathParam(min_length=1)],
|
|
448
|
+
runs: RunManagerDep,
|
|
449
|
+
) -> RunResponse:
|
|
450
|
+
"""Remove a queued message that has not been delivered."""
|
|
451
|
+
|
|
452
|
+
run = await runs.remove_queued(session_id, input_id)
|
|
453
|
+
if run is None:
|
|
454
|
+
raise HTTPException(status_code=404, detail="queued message not found")
|
|
455
|
+
return RunResponse(run=RunInfo.model_validate(run))
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@router.post("/sessions/{session_id}/queue/{input_id}/steer")
|
|
459
|
+
async def steer_queued_message(
|
|
460
|
+
session_id: Annotated[str, PathParam(min_length=1)],
|
|
461
|
+
input_id: Annotated[str, PathParam(min_length=1)],
|
|
462
|
+
runs: RunManagerDep,
|
|
463
|
+
) -> RunResponse:
|
|
464
|
+
"""Move a queued message into the current turn, keeping it as built."""
|
|
465
|
+
|
|
466
|
+
state = await runs.get_active_run(session_id)
|
|
467
|
+
moved = None if state is None else await runs.steer_queued(state, input_id)
|
|
468
|
+
if state is None or moved is None:
|
|
469
|
+
raise HTTPException(status_code=404, detail="queued message not found")
|
|
470
|
+
if not moved:
|
|
471
|
+
raise HTTPException(
|
|
472
|
+
status_code=409,
|
|
473
|
+
detail={"message": "run is not accepting steers", "run": state.info()},
|
|
474
|
+
)
|
|
475
|
+
return RunResponse(run=RunInfo.model_validate(state.info()))
|
|
388
476
|
|
|
389
477
|
|
|
390
478
|
@router.get("/config")
|
|
@@ -402,13 +490,7 @@ async def get_config(cwd: Annotated[str | None, Query()] = None) -> dict[str, An
|
|
|
402
490
|
providers_info: dict[str, Any] = {}
|
|
403
491
|
for provider in resolve_provider_choices(settings):
|
|
404
492
|
provider_config = settings.providers.get(provider.provider_name or "")
|
|
405
|
-
models = (
|
|
406
|
-
list(provider_config.models)
|
|
407
|
-
if provider_config and provider_config.models
|
|
408
|
-
else list(provider_default_models(provider.provider))
|
|
409
|
-
)
|
|
410
|
-
if not models:
|
|
411
|
-
models = [provider.model]
|
|
493
|
+
models = provider_models(settings, provider)
|
|
412
494
|
|
|
413
495
|
info: dict[str, Any] = {
|
|
414
496
|
"name": provider.provider_name,
|
|
@@ -460,6 +542,10 @@ async def get_config(cwd: Annotated[str | None, Query()] = None) -> dict[str, An
|
|
|
460
542
|
"cwd_exists": os.path.isdir(resolved_cwd),
|
|
461
543
|
"project": settings.project,
|
|
462
544
|
"config_paths": settings.config_paths,
|
|
545
|
+
# Effective for this cwd; 0 disables automatic compaction.
|
|
546
|
+
"compact_threshold": (
|
|
547
|
+
settings.compact_threshold if settings.compact_threshold is not None else DEFAULT_COMPACT_THRESHOLD
|
|
548
|
+
),
|
|
463
549
|
"skills": [{"name": skill.name, "description": skill.description} for skill in skills],
|
|
464
550
|
"setup_error": setup_error,
|
|
465
551
|
}
|