mycode-cli 0.18.0__py3-none-any.whl → 0.19.1__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/runtime.py +6 -11
- mycode_cli/server/routers/chat.py +162 -26
- 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/system_prompt.py +11 -1
- mycode_cli/tools.py +110 -48
- mycode_cli/tui/chat.py +143 -41
- mycode_cli/tui/render.py +29 -8
- mycode_cli/tui/terminal.py +63 -14
- {mycode_cli-0.18.0.dist-info → mycode_cli-0.19.1.dist-info}/METADATA +15 -13
- {mycode_cli-0.18.0.dist-info → mycode_cli-0.19.1.dist-info}/RECORD +19 -19
- mycode_cli/server/static/assets/index-C7MhyuLY.js +0 -389
- mycode_cli/server/static/assets/index-D71Nwfrp.css +0 -1
- {mycode_cli-0.18.0.dist-info → mycode_cli-0.19.1.dist-info}/WHEEL +0 -0
- {mycode_cli-0.18.0.dist-info → mycode_cli-0.19.1.dist-info}/entry_points.txt +0 -0
- {mycode_cli-0.18.0.dist-info → mycode_cli-0.19.1.dist-info}/licenses/LICENSE +0 -0
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(
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import io
|
|
6
7
|
import json
|
|
7
8
|
import os
|
|
8
9
|
from base64 import b64encode
|
|
@@ -10,6 +11,7 @@ from collections.abc import AsyncIterator
|
|
|
10
11
|
from dataclasses import replace
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Annotated, Any, cast
|
|
14
|
+
from uuid import uuid4
|
|
13
15
|
|
|
14
16
|
from fastapi import APIRouter, HTTPException, Query, Request
|
|
15
17
|
from fastapi import Path as PathParam
|
|
@@ -21,7 +23,7 @@ from mycode.attachments import (
|
|
|
21
23
|
detect_document_mime_type,
|
|
22
24
|
detect_image_mime_type,
|
|
23
25
|
)
|
|
24
|
-
from mycode.compact import has_compactable_history
|
|
26
|
+
from mycode.compact import DEFAULT_COMPACT_THRESHOLD, has_compactable_history
|
|
25
27
|
from mycode.messages import (
|
|
26
28
|
ConversationMessage,
|
|
27
29
|
build_message,
|
|
@@ -30,6 +32,7 @@ from mycode.messages import (
|
|
|
30
32
|
image_block,
|
|
31
33
|
text_block,
|
|
32
34
|
)
|
|
35
|
+
from mycode.models import ModelMetadata
|
|
33
36
|
from mycode_cli.config import (
|
|
34
37
|
ResolvedProvider,
|
|
35
38
|
get_settings,
|
|
@@ -40,22 +43,25 @@ from mycode_cli.config import (
|
|
|
40
43
|
resolve_provider_choices,
|
|
41
44
|
)
|
|
42
45
|
from mycode_cli.permissions import ToolReviewDecision, ToolReviewRequest
|
|
43
|
-
from mycode_cli.runtime import build_agent,
|
|
46
|
+
from mycode_cli.runtime import build_agent, load_session_totals
|
|
44
47
|
from mycode_cli.server.deps import RunManagerDep, StoreDep, resolve_workspace_cwd
|
|
45
|
-
from mycode_cli.server.run_manager import ActiveRunError
|
|
48
|
+
from mycode_cli.server.run_manager import ActiveRunError, RunAgent
|
|
46
49
|
from mycode_cli.server.schemas import (
|
|
47
50
|
CancelRunResponse,
|
|
48
51
|
ChatRequest,
|
|
49
52
|
ChatResponse,
|
|
50
53
|
CompactRequest,
|
|
51
|
-
CompactResponse,
|
|
52
54
|
DecideRequest,
|
|
55
|
+
PendingInputRequest,
|
|
53
56
|
RunInfo,
|
|
57
|
+
RunResponse,
|
|
54
58
|
StatusResponse,
|
|
55
59
|
StreamEvent,
|
|
60
|
+
UserInputRequest,
|
|
56
61
|
)
|
|
57
62
|
from mycode_cli.system_prompt import build_skill_snapshot_blocks, discover_slash_skills
|
|
58
|
-
from mycode_cli.
|
|
63
|
+
from mycode_cli.tools import read_text_window
|
|
64
|
+
from mycode_cli.workspace import CliDeps, resolve_path
|
|
59
65
|
|
|
60
66
|
router = APIRouter()
|
|
61
67
|
|
|
@@ -69,7 +75,7 @@ def _resolve_workspace_attachment_path(rel_path: str, *, cwd: str) -> Path:
|
|
|
69
75
|
|
|
70
76
|
|
|
71
77
|
def _read_workspace_text_attachment(rel_path: str, *, name: str | None, cwd: str) -> list[dict[str, Any]]:
|
|
72
|
-
"""Read a workspace text file selected via the @ menu into a `<file>` block
|
|
78
|
+
"""Read a workspace text file selected via the @ menu into a `<file>` block, bounded like ``read``.
|
|
73
79
|
|
|
74
80
|
Re-validates the path at send time: it must resolve inside ``cwd`` (guards
|
|
75
81
|
against a symlink swapped in after selection), be a regular file, and be
|
|
@@ -81,16 +87,40 @@ def _read_workspace_text_attachment(rel_path: str, *, name: str | None, cwd: str
|
|
|
81
87
|
if detect_image_mime_type(path) or detect_document_mime_type(path):
|
|
82
88
|
raise HTTPException(status_code=400, detail=f"not a text file: {rel_path}")
|
|
83
89
|
try:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
with path.open(encoding="utf-8") as file:
|
|
91
|
+
window = read_text_window(file)
|
|
92
|
+
except UnicodeDecodeError as exc:
|
|
93
|
+
raise HTTPException(status_code=400, detail=f"not UTF-8 text: {rel_path}") from exc
|
|
94
|
+
return build_attachment_blocks([Attachment.text(window.render(path), name=name or rel_path)])
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _write_uploads(uploads: dict[Path, str]) -> None:
|
|
98
|
+
for path, text in uploads.items():
|
|
99
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
path.write_text(text, encoding="utf-8")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _remove_uploads(uploads: dict[Path, str]) -> None:
|
|
104
|
+
"""Undo ``_write_uploads`` for a message the run refused."""
|
|
105
|
+
|
|
106
|
+
for path in uploads:
|
|
107
|
+
path.unlink(missing_ok=True)
|
|
87
108
|
|
|
88
109
|
|
|
89
|
-
async def _build_user_message(chat:
|
|
110
|
+
async def _build_user_message(chat: UserInputRequest, deps: CliDeps) -> tuple[ConversationMessage, dict[Path, str]]:
|
|
111
|
+
"""Build the user message and the uploads to save before it is sent.
|
|
112
|
+
|
|
113
|
+
A text upload that does not fit one read window is shown truncated, and its
|
|
114
|
+
full text is returned for ``tool_output_dir`` so the model can read the rest.
|
|
115
|
+
The caller writes it once the request has passed validation.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
cwd = str(deps.cwd)
|
|
119
|
+
uploads: dict[Path, str] = {}
|
|
90
120
|
if not chat.input:
|
|
91
121
|
text = str(chat.message or "").strip()
|
|
92
122
|
snapshots = await asyncio.to_thread(build_skill_snapshot_blocks, text, cwd)
|
|
93
|
-
return build_message("user", [*snapshots, text_block(text)])
|
|
123
|
+
return build_message("user", [*snapshots, text_block(text)]), uploads
|
|
94
124
|
|
|
95
125
|
blocks: list[dict[str, Any]] = []
|
|
96
126
|
visible_text: list[str] = []
|
|
@@ -103,7 +133,12 @@ async def _build_user_message(chat: ChatRequest, cwd: str) -> ConversationMessag
|
|
|
103
133
|
continue
|
|
104
134
|
text = block.text or ""
|
|
105
135
|
if block.is_attachment:
|
|
106
|
-
|
|
136
|
+
name = str(block.name or "attached-file")
|
|
137
|
+
window = read_text_window(io.StringIO(text, newline=None))
|
|
138
|
+
upload_path = deps.tool_output_dir / f"upload-{uuid4().hex}"
|
|
139
|
+
if window.partial:
|
|
140
|
+
uploads[upload_path] = text
|
|
141
|
+
blocks.extend(build_attachment_blocks([Attachment.text(window.render(upload_path), name=name)]))
|
|
107
142
|
elif text:
|
|
108
143
|
visible_text.append(text)
|
|
109
144
|
blocks.append(text_block(text))
|
|
@@ -140,7 +175,15 @@ async def _build_user_message(chat: ChatRequest, cwd: str) -> ConversationMessag
|
|
|
140
175
|
raise HTTPException(status_code=400, detail="input must include at least one non-empty block")
|
|
141
176
|
snapshots = await asyncio.to_thread(build_skill_snapshot_blocks, "\n".join(visible_text), cwd)
|
|
142
177
|
blocks[:0] = snapshots
|
|
143
|
-
return build_message("user", blocks)
|
|
178
|
+
return build_message("user", blocks), uploads
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _check_input_support(message: ConversationMessage, model: ModelMetadata | RunAgent) -> None:
|
|
182
|
+
content_types = {b.get("type") for b in (message.get("content") or []) if isinstance(b, dict)}
|
|
183
|
+
if "image" in content_types and not model.supports_image_input:
|
|
184
|
+
raise HTTPException(status_code=400, detail="current model does not support image input")
|
|
185
|
+
if "document" in content_types and not model.supports_pdf_input:
|
|
186
|
+
raise HTTPException(status_code=400, detail="current model does not support PDF input")
|
|
144
187
|
|
|
145
188
|
|
|
146
189
|
def _validate_rewind_request(
|
|
@@ -182,7 +225,9 @@ async def chat(chat: ChatRequest, store: StoreDep, runs: RunManagerDep) -> ChatR
|
|
|
182
225
|
api_base=chat.api_base,
|
|
183
226
|
)
|
|
184
227
|
session_id = chat.session_id or "default"
|
|
185
|
-
user_message = await _build_user_message(
|
|
228
|
+
user_message, uploads = await _build_user_message(
|
|
229
|
+
chat, CliDeps.for_session(cwd=cwd, data_dir=store.data_dir, session_id=session_id)
|
|
230
|
+
)
|
|
186
231
|
|
|
187
232
|
# Capability check before any disk mutation — a failed check must not
|
|
188
233
|
# leave an empty session on disk or land a premature rewind marker.
|
|
@@ -192,11 +237,7 @@ async def chat(chat: ChatRequest, store: StoreDep, runs: RunManagerDep) -> ChatR
|
|
|
192
237
|
model=resolved.model,
|
|
193
238
|
model_config=model_config,
|
|
194
239
|
)
|
|
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")
|
|
240
|
+
_check_input_support(user_message, model_meta)
|
|
200
241
|
|
|
201
242
|
reasoning_effort = resolved.reasoning_effort
|
|
202
243
|
if "reasoning_effort" in chat.model_fields_set:
|
|
@@ -241,9 +282,10 @@ async def chat(chat: ChatRequest, store: StoreDep, runs: RunManagerDep) -> ChatR
|
|
|
241
282
|
existing_messages = data["messages"] if data else []
|
|
242
283
|
_validate_rewind_request(session=session, messages=existing_messages, rewind_to=chat.rewind_to)
|
|
243
284
|
|
|
244
|
-
# All validation passed.
|
|
245
|
-
# the user turn in the catalog: first turn creates the
|
|
246
|
-
# the title, later turns bump updated_at.
|
|
285
|
+
# All validation passed. Save uploads, land the rewind marker (if any)
|
|
286
|
+
# and register the user turn in the catalog: first turn creates the
|
|
287
|
+
# entry and sets the title, later turns bump updated_at.
|
|
288
|
+
await asyncio.to_thread(_write_uploads, uploads)
|
|
247
289
|
if chat.rewind_to is not None:
|
|
248
290
|
await store.append_rewind(session_id, chat.rewind_to)
|
|
249
291
|
session = await store.record_user_turn(
|
|
@@ -274,10 +316,11 @@ async def chat(chat: ChatRequest, store: StoreDep, runs: RunManagerDep) -> ChatR
|
|
|
274
316
|
try:
|
|
275
317
|
run = await runs.start_run(
|
|
276
318
|
session_id=session_id,
|
|
319
|
+
cwd=cwd,
|
|
277
320
|
user_message=user_message,
|
|
278
321
|
base_messages=agent.messages,
|
|
279
322
|
agent=agent,
|
|
280
|
-
|
|
323
|
+
session_base=await load_session_totals(store, session_id),
|
|
281
324
|
)
|
|
282
325
|
except ActiveRunError as exc:
|
|
283
326
|
existing = await runs.get_run(exc.run_id)
|
|
@@ -339,7 +382,7 @@ async def compact_session(
|
|
|
339
382
|
body: CompactRequest,
|
|
340
383
|
store: StoreDep,
|
|
341
384
|
runs: RunManagerDep,
|
|
342
|
-
) ->
|
|
385
|
+
) -> RunResponse:
|
|
343
386
|
"""Start a compact run: summarize the session and append one compact marker."""
|
|
344
387
|
|
|
345
388
|
async with runs.session_operation(session_id):
|
|
@@ -372,9 +415,10 @@ async def compact_session(
|
|
|
372
415
|
try:
|
|
373
416
|
run = await runs.start_compact(
|
|
374
417
|
session_id=session_id,
|
|
418
|
+
cwd=cwd,
|
|
375
419
|
base_messages=agent.messages,
|
|
376
420
|
agent=agent,
|
|
377
|
-
|
|
421
|
+
session_base=data["totals"],
|
|
378
422
|
on_complete=store.touch,
|
|
379
423
|
)
|
|
380
424
|
except ActiveRunError as exc:
|
|
@@ -384,7 +428,95 @@ async def compact_session(
|
|
|
384
428
|
detail["run"] = existing.info()
|
|
385
429
|
raise HTTPException(status_code=409, detail=detail) from exc
|
|
386
430
|
|
|
387
|
-
return
|
|
431
|
+
return RunResponse(run=RunInfo.model_validate(run))
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@router.post("/runs/{run_id}/steer")
|
|
435
|
+
async def steer_run(
|
|
436
|
+
run_id: Annotated[str, PathParam(min_length=1)], body: PendingInputRequest, store: StoreDep, runs: RunManagerDep
|
|
437
|
+
) -> RunResponse:
|
|
438
|
+
"""Hand a user message to the running chat for its next step boundary."""
|
|
439
|
+
|
|
440
|
+
state = await runs.get_run(run_id)
|
|
441
|
+
if state is None:
|
|
442
|
+
raise HTTPException(status_code=404, detail="run not found")
|
|
443
|
+
message, uploads = await _build_user_message(
|
|
444
|
+
body, CliDeps.for_session(cwd=state.cwd, data_dir=store.data_dir, session_id=state.session_id)
|
|
445
|
+
)
|
|
446
|
+
message["meta"] = {"input_id": body.input_id}
|
|
447
|
+
_check_input_support(message, state.agent)
|
|
448
|
+
# Uploads are on disk before the model can see the message; a refusal removes them.
|
|
449
|
+
await asyncio.to_thread(_write_uploads, uploads)
|
|
450
|
+
if not state.agent.steer(message):
|
|
451
|
+
await asyncio.to_thread(_remove_uploads, uploads)
|
|
452
|
+
raise HTTPException(
|
|
453
|
+
status_code=409,
|
|
454
|
+
detail={"message": "run is not accepting steers", "run": state.info()},
|
|
455
|
+
)
|
|
456
|
+
return RunResponse(run=RunInfo.model_validate(state.info()))
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@router.post("/sessions/{session_id}/queue")
|
|
460
|
+
async def queue_message(
|
|
461
|
+
session_id: Annotated[str, PathParam(min_length=1)],
|
|
462
|
+
body: PendingInputRequest,
|
|
463
|
+
store: StoreDep,
|
|
464
|
+
runs: RunManagerDep,
|
|
465
|
+
) -> RunResponse:
|
|
466
|
+
"""Queue a user message as the next turn of the session's running chat."""
|
|
467
|
+
|
|
468
|
+
state = await runs.get_active_run(session_id)
|
|
469
|
+
if state is None:
|
|
470
|
+
raise HTTPException(status_code=409, detail={"message": "session has no running chat to queue on"})
|
|
471
|
+
message, uploads = await _build_user_message(
|
|
472
|
+
body, CliDeps.for_session(cwd=state.cwd, data_dir=store.data_dir, session_id=session_id)
|
|
473
|
+
)
|
|
474
|
+
message["meta"] = {"input_id": body.input_id}
|
|
475
|
+
_check_input_support(message, state.agent)
|
|
476
|
+
await asyncio.to_thread(_write_uploads, uploads)
|
|
477
|
+
if not await runs.enqueue(state, message):
|
|
478
|
+
await asyncio.to_thread(_remove_uploads, uploads)
|
|
479
|
+
raise HTTPException(status_code=409, detail={"message": "session has no running chat to queue on"})
|
|
480
|
+
await store.record_user_turn(
|
|
481
|
+
session_id,
|
|
482
|
+
cwd=state.cwd,
|
|
483
|
+
text=flatten_message_text(message, include_thinking=False),
|
|
484
|
+
)
|
|
485
|
+
return RunResponse(run=RunInfo.model_validate(state.info()))
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@router.delete("/sessions/{session_id}/queue/{input_id}")
|
|
489
|
+
async def remove_queued_message(
|
|
490
|
+
session_id: Annotated[str, PathParam(min_length=1)],
|
|
491
|
+
input_id: Annotated[str, PathParam(min_length=1)],
|
|
492
|
+
runs: RunManagerDep,
|
|
493
|
+
) -> RunResponse:
|
|
494
|
+
"""Remove a queued message that has not been delivered."""
|
|
495
|
+
|
|
496
|
+
run = await runs.remove_queued(session_id, input_id)
|
|
497
|
+
if run is None:
|
|
498
|
+
raise HTTPException(status_code=404, detail="queued message not found")
|
|
499
|
+
return RunResponse(run=RunInfo.model_validate(run))
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
@router.post("/sessions/{session_id}/queue/{input_id}/steer")
|
|
503
|
+
async def steer_queued_message(
|
|
504
|
+
session_id: Annotated[str, PathParam(min_length=1)],
|
|
505
|
+
input_id: Annotated[str, PathParam(min_length=1)],
|
|
506
|
+
runs: RunManagerDep,
|
|
507
|
+
) -> RunResponse:
|
|
508
|
+
"""Move a queued message into the current turn, keeping it as built."""
|
|
509
|
+
|
|
510
|
+
state = await runs.get_active_run(session_id)
|
|
511
|
+
moved = None if state is None else await runs.steer_queued(state, input_id)
|
|
512
|
+
if state is None or moved is None:
|
|
513
|
+
raise HTTPException(status_code=404, detail="queued message not found")
|
|
514
|
+
if not moved:
|
|
515
|
+
raise HTTPException(
|
|
516
|
+
status_code=409,
|
|
517
|
+
detail={"message": "run is not accepting steers", "run": state.info()},
|
|
518
|
+
)
|
|
519
|
+
return RunResponse(run=RunInfo.model_validate(state.info()))
|
|
388
520
|
|
|
389
521
|
|
|
390
522
|
@router.get("/config")
|
|
@@ -454,6 +586,10 @@ async def get_config(cwd: Annotated[str | None, Query()] = None) -> dict[str, An
|
|
|
454
586
|
"cwd_exists": os.path.isdir(resolved_cwd),
|
|
455
587
|
"project": settings.project,
|
|
456
588
|
"config_paths": settings.config_paths,
|
|
589
|
+
# Effective for this cwd; 0 disables automatic compaction.
|
|
590
|
+
"compact_threshold": (
|
|
591
|
+
settings.compact_threshold if settings.compact_threshold is not None else DEFAULT_COMPACT_THRESHOLD
|
|
592
|
+
),
|
|
457
593
|
"skills": [{"name": skill.name, "description": skill.description} for skill in skills],
|
|
458
594
|
"setup_error": setup_error,
|
|
459
595
|
}
|
|
@@ -11,6 +11,7 @@ from fastapi import Path as PathParam
|
|
|
11
11
|
from mycode.messages import ConversationMessage
|
|
12
12
|
from mycode_cli.server.deps import RunManagerDep, StoreDep, resolve_workspace_cwd
|
|
13
13
|
from mycode_cli.server.schemas import SessionCreateRequest, StatusResponse
|
|
14
|
+
from mycode_cli.sessions import SessionTotals
|
|
14
15
|
|
|
15
16
|
router = APIRouter(prefix="/sessions", tags=["sessions"])
|
|
16
17
|
|
|
@@ -50,6 +51,21 @@ async def list_sessions(
|
|
|
50
51
|
return {"sessions": sessions}
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
@router.get("/search")
|
|
55
|
+
async def search_sessions(
|
|
56
|
+
store: StoreDep,
|
|
57
|
+
runs: RunManagerDep,
|
|
58
|
+
q: Annotated[str, Query(min_length=1)],
|
|
59
|
+
cwd: Annotated[str | None, Query()] = None,
|
|
60
|
+
limit: Annotated[int, Query(ge=1)] = 50,
|
|
61
|
+
) -> dict[str, Any]:
|
|
62
|
+
results = await store.search_sessions(q, cwd=cwd, limit=limit)
|
|
63
|
+
for result in results:
|
|
64
|
+
session = result["session"]
|
|
65
|
+
session["is_running"] = await runs.has_active_run(str(session["id"]))
|
|
66
|
+
return {"results": results}
|
|
67
|
+
|
|
68
|
+
|
|
53
69
|
@router.get("/{session_id}")
|
|
54
70
|
async def load_session(
|
|
55
71
|
session_id: Annotated[str, PathParam(min_length=1)], store: StoreDep, runs: RunManagerDep
|
|
@@ -62,22 +78,31 @@ async def load_session(
|
|
|
62
78
|
return {
|
|
63
79
|
"session": await store.load_metadata(session_id),
|
|
64
80
|
"messages": _redact_document_data(active["messages"]),
|
|
65
|
-
|
|
81
|
+
**active["totals"].payload(),
|
|
66
82
|
"active_run": active["run"],
|
|
67
83
|
"pending_events": active["pending_events"],
|
|
84
|
+
"pending": {kind: _redact_document_data(messages) for kind, messages in active["pending"].items()},
|
|
68
85
|
}
|
|
69
86
|
|
|
70
87
|
data = await store.load_session(session_id)
|
|
71
88
|
|
|
72
89
|
if data is None:
|
|
73
|
-
return {
|
|
90
|
+
return {
|
|
91
|
+
"session": None,
|
|
92
|
+
"messages": [],
|
|
93
|
+
**SessionTotals().payload(),
|
|
94
|
+
"active_run": None,
|
|
95
|
+
"pending_events": [],
|
|
96
|
+
"pending": {"steers": [], "queue": []},
|
|
97
|
+
}
|
|
74
98
|
|
|
75
99
|
return {
|
|
76
100
|
"session": data["session"],
|
|
77
101
|
"messages": _redact_document_data(data["messages"]),
|
|
78
|
-
|
|
102
|
+
**data["totals"].payload(),
|
|
79
103
|
"active_run": None,
|
|
80
104
|
"pending_events": [],
|
|
105
|
+
"pending": {"steers": [], "queue": []},
|
|
81
106
|
}
|
|
82
107
|
|
|
83
108
|
|
|
@@ -90,14 +115,3 @@ async def delete_session(
|
|
|
90
115
|
raise HTTPException(status_code=409, detail="session has a running task")
|
|
91
116
|
await store.delete_session(session_id)
|
|
92
117
|
return StatusResponse(status="ok")
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@router.post("/{session_id}/clear")
|
|
96
|
-
async def clear_session(
|
|
97
|
-
session_id: Annotated[str, PathParam(min_length=1)], store: StoreDep, runs: RunManagerDep
|
|
98
|
-
) -> StatusResponse:
|
|
99
|
-
async with runs.session_operation(session_id):
|
|
100
|
-
if await runs.has_active_run(session_id):
|
|
101
|
-
raise HTTPException(status_code=409, detail="session has a running task")
|
|
102
|
-
await store.clear_session(session_id)
|
|
103
|
-
return StatusResponse(status="ok")
|