codeer-cli 0.1.9__tar.gz → 0.1.11__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 (38) hide show
  1. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/API_REFERENCE.md +52 -26
  2. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/PKG-INFO +37 -6
  3. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/README.md +36 -5
  4. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/pyproject.toml +1 -1
  5. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/__init__.py +2 -1
  6. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/_validate.py +42 -1
  7. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/agents.py +9 -1
  8. codeer_cli-0.1.11/src/codeer_cli/chats.py +201 -0
  9. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/cli.py +8 -0
  10. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/client.py +30 -5
  11. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/commands/agent.py +26 -3
  12. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/commands/history.py +26 -12
  13. codeer_cli-0.1.11/src/codeer_cli/commands/model.py +43 -0
  14. codeer_cli-0.1.11/src/codeer_cli/models.py +16 -0
  15. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/parse.py +7 -8
  16. codeer_cli-0.1.11/tests/test_agent_handoff.py +153 -0
  17. codeer_cli-0.1.11/tests/test_chats_v2.py +135 -0
  18. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/tests/test_client_transport.py +48 -0
  19. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/tests/test_history_send.py +52 -12
  20. codeer_cli-0.1.11/tests/test_models.py +68 -0
  21. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/uv.lock +1 -1
  22. codeer_cli-0.1.9/src/codeer_cli/chats.py +0 -87
  23. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/.gitignore +0 -0
  24. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/commands/__init__.py +0 -0
  25. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/commands/_util.py +0 -0
  26. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/commands/check.py +0 -0
  27. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/commands/eval_cmd.py +0 -0
  28. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/commands/kb.py +0 -0
  29. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/commands/profile.py +0 -0
  30. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/constants.py +0 -0
  31. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/eval_.py +0 -0
  32. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/histories.py +0 -0
  33. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/src/codeer_cli/kb.py +0 -0
  34. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/tests/test_eval_labels.py +0 -0
  35. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/tests/test_eval_pairs.py +0 -0
  36. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/tests/test_kb_nodes.py +0 -0
  37. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/tests/test_kb_ranges.py +0 -0
  38. {codeer_cli-0.1.9 → codeer_cli-0.1.11}/tests/test_util.py +0 -0
@@ -3,8 +3,9 @@
3
3
  > This file is the request-shape reference for maintaining `codeer-cli`.
4
4
 
5
5
  The 9 stages below mirror the user-docs lifecycle (`agent-creation` →
6
- `optimization-loop` → `publish`). Every path is under `/api/v1/`. All endpoints
7
- authenticate via `x-api-key` from `CODEER_API_KEY`.
6
+ `optimization-loop` → `publish`). Agent, KB, eval, history, and model paths are
7
+ under `/api/v1/`; Chat create/message/read paths use `/api/v2/chats`. All
8
+ endpoints authenticate via `x-api-key` from `CODEER_API_KEY`.
8
9
 
9
10
  Envelope: successful responses look like
10
11
  `{"error_code": 0, "message": "", "pagination": null, "data": <payload>}`.
@@ -49,6 +50,23 @@ like `knowledge_node_ids`, `domain`, `agent_id`, `http_request` config.
49
50
 
50
51
  Limits: 10 tools per agent, ≤5 `call_agent`, ≤1 `memory`.
51
52
 
53
+ Agent create/update payloads may also include:
54
+
55
+ ```json
56
+ {
57
+ "human_handoff": {
58
+ "enabled": true,
59
+ "idle_timeout_minutes": null,
60
+ "handoff_instructions": "Hand off when the user asks for a person."
61
+ }
62
+ }
63
+ ```
64
+
65
+ `idle_timeout_minutes` must be greater than zero when provided. The runtime
66
+ only exposes the handoff tool to an external conversation with a non-empty
67
+ `external_user_id`, or to an evaluation run. Internal editor Live Test does not
68
+ activate human mode.
69
+
52
70
  ## Stage 2 — Knowledge bases
53
71
 
54
72
  Base path: `/organizations/{org_id}/workspaces/{ws_id}/knowledge_bases`
@@ -123,19 +141,21 @@ codeer kb files --kb-id <kb-id>
123
141
  codeer kb faq-create --context-object-id <snapshot-object-id> --question "..." --range 12:0-12:42 --dry-run
124
142
  ```
125
143
 
126
- ## Stage 3 — Live Test on a specific version
144
+ ## Stage 3 — Live Test on a specific version (legacy V1 compatibility)
127
145
 
128
146
  | Method & path | Purpose |
129
147
  | --- | --- |
130
- | `POST /chats` | Create a new chat session bound to an agent |
131
- | `POST /chats/{chat_id}/messages` | Send a message; **SSE stream** of tool calls + reasoning + final text |
132
- | `GET /chats/{chat_id}/messages` | Read historical messages for a chat |
133
- | `POST /chats/{chat_id}/regenerate` | Re-run the last turn |
134
- | `POST /chats/{chat_id}/messages/{msg_id}/feedbacks` | Thumbs up/down on a reply |
135
-
136
- `POST /chats/.../messages` requires `agent_history_id` — this is the key hook
137
- for the apply → test → publish workflow. Pass the draft `AgentHistory.id` from
138
- `PUT /agents/{id}` to test an unpublished version.
148
+ | `POST /api/v1/chats` | Create a legacy chat session bound to an agent |
149
+ | `POST /api/v1/chats/{chat_id}/messages` | Send a version-pinned message through legacy SSE |
150
+ | `GET /api/v1/chats/{chat_id}/messages` | Read legacy messages for a chat |
151
+ | `POST /api/v1/chats/{chat_id}/regenerate` | Re-run the last turn |
152
+ | `POST /api/v1/chats/{chat_id}/messages/{msg_id}/feedbacks` | Thumbs up/down on a reply |
153
+
154
+ Legacy V1 `POST /chats/.../messages` accepts `agent_history_id` — this is the
155
+ compatibility hook for the apply → test → publish workflow. Pass the draft
156
+ `AgentHistory.id` from `PUT /agents/{id}` to test an unpublished version.
157
+ The API-key external flow in Chat V2 accepts `agent_id` and runs its published
158
+ version; it does not currently replace this draft-pinning path.
139
159
 
140
160
  ## Stage 4 — Version management
141
161
 
@@ -250,8 +270,9 @@ the public CLI.
250
270
 
251
271
  | Method & path | Purpose |
252
272
  | --- | --- |
253
- | `POST /chats` | Create a persisted history using an agent's current published version |
254
- | `POST /chats/{id}/messages` | Append a turn to an existing persisted history using the current published version |
273
+ | `POST /api/v2/chats` | Create a persisted history using an agent's current published version |
274
+ | `POST /api/v2/chats/{id}/messages` | Append a turn through structured SSE using the current published version |
275
+ | `GET /api/v2/chats/{id}/messages` | Read persisted Chat V2 conversation parts |
255
276
  | `GET /histories?agent_id=X&feedback_filter=improve_feedback&external_user_id=…` | List conversations with filters |
256
277
  | `GET /histories/{id}` | Read one history's metadata |
257
278
  | `GET /histories/{id}/conversations` | Full conversation turns incl. tool calls |
@@ -259,9 +280,11 @@ the public CLI.
259
280
  | `POST /histories/{hid}/conversations/{cid}/score` | Numeric score |
260
281
 
261
282
  The CLI exposes the first two operations as `codeer history create` and
262
- `codeer history send`. Non-streaming message requests default to a 120-second
263
- per-message timeout. A timeout has an uncertain write outcome, so read the
264
- history before retrying to avoid duplicate turns.
283
+ `codeer history send`. Messages explicitly set `stream: true`, consume Chat V2
284
+ SSE, and require `response.completed` before reporting success. Their
285
+ per-message SSE read timeout defaults to 240 seconds. A timeout,
286
+ `response.failed`, or disconnect before completion has an uncertain write
287
+ outcome, so read the history before retrying to avoid duplicate turns.
265
288
 
266
289
  `feedback_filter` accepts the `FeedbackFilterType` enum values:
267
290
  `no_feedback`, `with_feedback`, `helpful_feedback`, `improve_feedback`.
@@ -280,7 +303,7 @@ Non-destructive: older versions stay in `GET /agents/{id}/histories`.
280
303
  | --- | --- |
281
304
  | `GET /accounts/me` | Sanity-check session, read workspace_organization_map |
282
305
  | `GET /organizations` | List orgs visible to the user |
283
- | `GET /llm/models` | List available LLM model IDs to use as `llm_model` |
306
+ | `GET /llm/models` | List available LLM model IDs to use as `llm_model` (`codeer model list`) |
284
307
  | `GET /retrieval/...` | Shared retrieval helpers (file upload for attachments, markdown conversion) |
285
308
 
286
309
  ---
@@ -340,7 +363,7 @@ said (scope, factuality, tool-use rules).
340
363
 
341
364
  ### 4. Agent version pinning works everywhere — use it
342
365
 
343
- Both `POST /chats/{id}/messages` (`agent_history_id` required) and
366
+ Legacy V1 `POST /chats/{id}/messages` (`agent_history_id` required) and
344
367
  `POST /eval/trigger` (`agent_history_id` optional, null = live state) accept
345
368
  the draft history id. The apply-→-test-→-publish loop:
346
369
 
@@ -446,9 +469,10 @@ pass. Workspace scope is inferred from the API-key virtual user profile.
446
469
 
447
470
  ### 11. Tool args + outputs are NOT persisted in history reads
448
471
 
449
- Conversations have only three roles (`OpenAIChatRole = system | user | assistant`)
450
- — there is no `tool` role row. When you read a history, here's what you can
451
- and can't recover from each assistant turn:
472
+ Legacy V1 `History` conversation rows have only three roles
473
+ (`OpenAIChatRole = system | user | assistant`) — there is no `tool` role row.
474
+ When you read those rows through `/histories/{id}/conversations`, here's what
475
+ you can and can't recover from each assistant turn:
452
476
 
453
477
  | Recoverable | Where |
454
478
  | --- | --- |
@@ -464,10 +488,12 @@ and can't recover from each assistant turn:
464
488
  | Tool **outputs** (raw JSON returned by the tool) | same — stored only as derived `primary_sources` for retrieval tools |
465
489
  | Reasoning steps mid-turn | `meta.reasoning_steps` is currently always `null` |
466
490
 
467
- If you need full tool I/O, capture it at execution time via the chat SSE
468
- stream (`POST /chats/{id}/messages`), not from history reads. For after-the-
469
- fact analysis, the persisted shape is sufficient to surface tool-selection
470
- patterns, token costs, and which sources the agent ended up citing.
491
+ Chat V2 improves this contract: structured SSE emits tool calls and returns as
492
+ `response.part.created` / `response.part.completed`, and
493
+ `GET /api/v2/chats/{id}/messages` reads the persisted conversation parts.
494
+ Capture the SSE artifact with `--out` when exact event order matters; use the
495
+ V2 message read for persisted after-the-fact tool I/O. The legacy v1 history
496
+ read remains useful for compact turn-level analysis.
471
497
 
472
498
  ### 10. A KB has exactly ONE level of folders — no nesting
473
499
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeer-cli
3
- Version: 0.1.9
3
+ Version: 0.1.11
4
4
  Summary: Command line tools for managing Codeer agents over the Codeer API.
5
5
  Project-URL: Homepage, https://www.codeer.ai
6
6
  Author: Codeer.AI
@@ -117,6 +117,35 @@ Validate setup before API work:
117
117
  codeer check
118
118
  ```
119
119
 
120
+ List the active cloud models without opening the Codeer web app:
121
+
122
+ ```bash
123
+ codeer model list --type text
124
+ ```
125
+
126
+ ## Agent human handoff
127
+
128
+ `codeer agent apply` accepts the same `human_handoff` object as the Agent API.
129
+ The dry-run validates it and shows whether handoff is enabled before any server
130
+ write:
131
+
132
+ ```json
133
+ {
134
+ "name": "Support Agent",
135
+ "system_prompt": "Help the user safely.",
136
+ "human_handoff": {
137
+ "enabled": true,
138
+ "idle_timeout_minutes": null,
139
+ "handoff_instructions": "Hand off when the user asks for a person."
140
+ }
141
+ }
142
+ ```
143
+
144
+ `idle_timeout_minutes` must be a positive integer or `null`. Human handoff only
145
+ becomes available in live published-agent conversations with a non-empty
146
+ `external_user_id`; editor Live Test conversations are internal and cannot
147
+ activate human mode.
148
+
120
149
  ## Upgrade and uninstall
121
150
 
122
151
  Upgrade the CLI:
@@ -143,15 +172,17 @@ Use this pattern during agent lifecycle work:
143
172
  ```bash
144
173
  codeer agent list
145
174
  codeer history list --agent <agent-id> --limit 50
146
- codeer history create --agent <agent-id> --message "Review this plan" --timeout 120
147
- codeer history send <history-id> --message "Use the recommended options" --timeout 120
175
+ codeer history create --agent <agent-id> --message "Review this plan" --timeout 240
176
+ codeer history send <history-id> --message "Use the recommended options" --timeout 240
148
177
  codeer eval run --agent <agent-id> --cases <case-ids> --evaluator <evaluator-id> --out .codeer/eval_run.json
149
178
  ```
150
179
 
151
180
  `history create` and `history send` use the agent's current published version.
152
- Their per-message timeout defaults to 120 seconds. If a write request times
153
- out, inspect the history before retrying: the server may have completed the
154
- turn after the client stopped waiting.
181
+ They use Chat V2 structured SSE with `stream: true`; their per-message read
182
+ timeout defaults to 240 seconds. Success requires a `response.completed`
183
+ event. If the stream times out, reports `response.failed`, or disconnects
184
+ early, inspect the history before retrying: the server may already have
185
+ persisted the turn.
155
186
 
156
187
  Eval case label commands always operate on the active API-key workspace. They
157
188
  do not accept a workspace override; switch CLI profiles to target another
@@ -99,6 +99,35 @@ Validate setup before API work:
99
99
  codeer check
100
100
  ```
101
101
 
102
+ List the active cloud models without opening the Codeer web app:
103
+
104
+ ```bash
105
+ codeer model list --type text
106
+ ```
107
+
108
+ ## Agent human handoff
109
+
110
+ `codeer agent apply` accepts the same `human_handoff` object as the Agent API.
111
+ The dry-run validates it and shows whether handoff is enabled before any server
112
+ write:
113
+
114
+ ```json
115
+ {
116
+ "name": "Support Agent",
117
+ "system_prompt": "Help the user safely.",
118
+ "human_handoff": {
119
+ "enabled": true,
120
+ "idle_timeout_minutes": null,
121
+ "handoff_instructions": "Hand off when the user asks for a person."
122
+ }
123
+ }
124
+ ```
125
+
126
+ `idle_timeout_minutes` must be a positive integer or `null`. Human handoff only
127
+ becomes available in live published-agent conversations with a non-empty
128
+ `external_user_id`; editor Live Test conversations are internal and cannot
129
+ activate human mode.
130
+
102
131
  ## Upgrade and uninstall
103
132
 
104
133
  Upgrade the CLI:
@@ -125,15 +154,17 @@ Use this pattern during agent lifecycle work:
125
154
  ```bash
126
155
  codeer agent list
127
156
  codeer history list --agent <agent-id> --limit 50
128
- codeer history create --agent <agent-id> --message "Review this plan" --timeout 120
129
- codeer history send <history-id> --message "Use the recommended options" --timeout 120
157
+ codeer history create --agent <agent-id> --message "Review this plan" --timeout 240
158
+ codeer history send <history-id> --message "Use the recommended options" --timeout 240
130
159
  codeer eval run --agent <agent-id> --cases <case-ids> --evaluator <evaluator-id> --out .codeer/eval_run.json
131
160
  ```
132
161
 
133
162
  `history create` and `history send` use the agent's current published version.
134
- Their per-message timeout defaults to 120 seconds. If a write request times
135
- out, inspect the history before retrying: the server may have completed the
136
- turn after the client stopped waiting.
163
+ They use Chat V2 structured SSE with `stream: true`; their per-message read
164
+ timeout defaults to 240 seconds. Success requires a `response.completed`
165
+ event. If the stream times out, reports `response.failed`, or disconnects
166
+ early, inspect the history before retrying: the server may already have
167
+ persisted the turn.
137
168
 
138
169
  Eval case label commands always operate on the active API-key workspace. They
139
170
  do not accept a workspace override; switch CLI profiles to target another
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codeer-cli"
7
- version = "0.1.9"
7
+ version = "0.1.11"
8
8
  description = "Command line tools for managing Codeer agents over the Codeer API."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -18,7 +18,7 @@ production and can be overridden from process env. The CLI does not read
18
18
  workspace-local dotenv files or credential files.
19
19
  """
20
20
 
21
- from ._validate import ToolValidationError
21
+ from ._validate import HumanHandoffValidationError, ToolValidationError
22
22
  from .client import AuthError, CodeerClient, CodeerError, TransportError
23
23
  from .parse import (
24
24
  AgentSummary,
@@ -44,6 +44,7 @@ from .parse import (
44
44
 
45
45
  __all__ = [
46
46
  "CodeerClient", "CodeerError", "TransportError", "AuthError", "ToolValidationError",
47
+ "HumanHandoffValidationError",
47
48
  # parsers
48
49
  "AgentSummary", "ConversationTurn", "EvalResultSummary", "HistorySummary",
49
50
  "KBNode", "ToolCall", "EvalToolCall",
@@ -1,4 +1,4 @@
1
- """Client-side validation for unified-tool payloads.
1
+ """Client-side validation for agent payloads.
2
2
 
3
3
  These checks exist because the backend's form-schema validator is lenient
4
4
  (``extra="allow"``) and silently accepts unknown ``type`` strings, which then
@@ -24,6 +24,47 @@ class ToolValidationError(ValueError):
24
24
  """Raised when a unified_tools payload is definitely wrong."""
25
25
 
26
26
 
27
+ class HumanHandoffValidationError(ValueError):
28
+ """Raised when a human_handoff payload is definitely wrong."""
29
+
30
+
31
+ def validate_human_handoff(config: Any) -> dict[str, Any] | None:
32
+ """Validate and normalize an optional human-handoff configuration."""
33
+ if config is None:
34
+ return None
35
+ if not isinstance(config, dict):
36
+ raise HumanHandoffValidationError("human_handoff must be an object.")
37
+
38
+ allowed_keys = {"enabled", "idle_timeout_minutes", "handoff_instructions"}
39
+ unknown_keys = sorted(set(config) - allowed_keys)
40
+ if unknown_keys:
41
+ raise HumanHandoffValidationError(
42
+ f"human_handoff contains unsupported field(s): {', '.join(unknown_keys)}."
43
+ )
44
+
45
+ enabled = config.get("enabled", False)
46
+ if not isinstance(enabled, bool):
47
+ raise HumanHandoffValidationError("human_handoff.enabled must be true or false.")
48
+
49
+ timeout = config.get("idle_timeout_minutes")
50
+ if timeout is not None and (type(timeout) is not int or timeout <= 0):
51
+ raise HumanHandoffValidationError(
52
+ "human_handoff.idle_timeout_minutes must be a positive integer or null."
53
+ )
54
+
55
+ instructions = config.get("handoff_instructions")
56
+ if instructions is not None and not isinstance(instructions, str):
57
+ raise HumanHandoffValidationError(
58
+ "human_handoff.handoff_instructions must be a string or null."
59
+ )
60
+
61
+ return {
62
+ "enabled": enabled,
63
+ "idle_timeout_minutes": timeout,
64
+ "handoff_instructions": instructions,
65
+ }
66
+
67
+
27
68
  def validate_unified_tools(tools: Iterable[dict[str, Any]] | None) -> list[dict[str, Any]]:
28
69
  """Raise ToolValidationError on invalid payloads; otherwise return the list.
29
70
 
@@ -7,7 +7,7 @@ from __future__ import annotations
7
7
 
8
8
  from typing import Any, List, Optional
9
9
 
10
- from ._validate import validate_unified_tools
10
+ from ._validate import validate_human_handoff, validate_unified_tools
11
11
  from .client import CodeerClient
12
12
 
13
13
 
@@ -24,8 +24,10 @@ def create(
24
24
  suggested_questions: Optional[List[str]] = None,
25
25
  primary_object_ids: Optional[List[int]] = None,
26
26
  attachment_ids: Optional[List[str]] = None,
27
+ human_handoff: Optional[dict[str, Any]] = None,
27
28
  ) -> dict:
28
29
  validated_tools = validate_unified_tools(unified_tools)
30
+ validated_handoff = validate_human_handoff(human_handoff)
29
31
  body: dict[str, Any] = {
30
32
  "name": name,
31
33
  "system_prompt": system_prompt,
@@ -39,6 +41,8 @@ def create(
39
41
  body["description"] = description
40
42
  if llm_model is not None:
41
43
  body["llm_model"] = llm_model
44
+ if validated_handoff is not None:
45
+ body["human_handoff"] = validated_handoff
42
46
  return client.post("/external/agents", json=body)
43
47
 
44
48
 
@@ -56,9 +60,11 @@ def update(
56
60
  suggested_questions: Optional[List[str]] = None,
57
61
  primary_object_ids: Optional[List[int]] = None,
58
62
  attachment_ids: Optional[List[str]] = None,
63
+ human_handoff: Optional[dict[str, Any]] = None,
59
64
  ) -> dict:
60
65
  """PUT creates a new AgentHistory snapshot (draft)."""
61
66
  validated_tools = validate_unified_tools(unified_tools)
67
+ validated_handoff = validate_human_handoff(human_handoff)
62
68
  body: dict[str, Any] = {
63
69
  "name": name,
64
70
  "system_prompt": system_prompt,
@@ -73,6 +79,8 @@ def update(
73
79
  body["description"] = description
74
80
  if llm_model is not None:
75
81
  body["llm_model"] = llm_model
82
+ if validated_handoff is not None:
83
+ body["human_handoff"] = validated_handoff
76
84
  return client.patch(f"/external/agents/{agent_id}", json=body)
77
85
 
78
86
 
@@ -0,0 +1,201 @@
1
+ """Chat V2 creation, structured SSE responses, and persisted message reads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Iterator, List, Optional
6
+
7
+ from .client import CodeerClient, TransportError
8
+
9
+
10
+ def create(
11
+ client: CodeerClient,
12
+ *,
13
+ agent_id: str,
14
+ title: Optional[str] = None,
15
+ external_user_id: Optional[str] = None,
16
+ ) -> dict:
17
+ body: dict[str, Any] = {
18
+ "agent_id": agent_id,
19
+ "name": title or "CLI conversation",
20
+ }
21
+ if external_user_id is not None:
22
+ body["external_user_id"] = external_user_id
23
+ return client.post("/chats", api_version="v2", json=body)
24
+
25
+
26
+ def send_published_agent_message(
27
+ client: CodeerClient,
28
+ *,
29
+ chat_id: int,
30
+ message: str,
31
+ agent_id: str,
32
+ external_user_id: Optional[str] = None,
33
+ attachment_ids: Optional[List[str]] = None,
34
+ stream: bool = True,
35
+ timeout: Optional[float] = None,
36
+ ) -> Iterator[dict] | dict:
37
+ """Send a user message through the API-key external chat flow.
38
+
39
+ API-key chat endpoints use the agent's published version. They accept
40
+ ``agent_id`` rather than ``agent_history_id``.
41
+ """
42
+ body: dict[str, Any] = {"message": message, "agent_id": agent_id, "stream": stream}
43
+ if external_user_id is not None:
44
+ body["external_user_id"] = external_user_id
45
+ if attachment_ids:
46
+ body["attached_file_uuids"] = attachment_ids
47
+
48
+ path = f"/chats/{chat_id}/messages"
49
+ if stream:
50
+ return client.stream_sse(
51
+ "POST",
52
+ path,
53
+ api_version="v2",
54
+ json=body,
55
+ timeout=timeout,
56
+ )
57
+ return client.post(path, api_version="v2", json=body, timeout=timeout)
58
+
59
+
60
+ def send_message(
61
+ client: CodeerClient,
62
+ *,
63
+ chat_id: int,
64
+ message: str,
65
+ agent_history_id: str,
66
+ attachment_ids: Optional[List[str]] = None,
67
+ stream: bool = True,
68
+ timeout: Optional[float] = None,
69
+ ) -> Iterator[dict] | dict:
70
+ """Use legacy Chat V1 to pin an unpublished agent version.
71
+
72
+ Chat V2's API-key external flow only accepts the published ``agent_id``.
73
+ Keep this low-level compatibility helper on V1 until V2 supports external
74
+ ``agent_history_id`` pinning. It remains streaming by default.
75
+ """
76
+ body: dict[str, Any] = {
77
+ "message": message,
78
+ "agent_history_id": agent_history_id,
79
+ "stream": stream,
80
+ }
81
+ if attachment_ids:
82
+ body["attachment_ids"] = attachment_ids
83
+
84
+ path = f"/chats/{chat_id}/messages"
85
+ if stream:
86
+ return client.stream_sse(
87
+ "POST",
88
+ path,
89
+ json=body,
90
+ timeout=timeout,
91
+ )
92
+ return client.post(path, json=body, timeout=timeout)
93
+
94
+
95
+ def collect_stream(events: Iterator[dict]) -> dict:
96
+ """Collect a Chat V2 SSE stream and require an explicit completion event."""
97
+ raw_events: list[dict] = []
98
+ parts: list[dict] = []
99
+ interactions: list[dict] = []
100
+ text_deltas: list[str] = []
101
+ final_text: str | None = None
102
+ response_id: str | None = None
103
+ conversation_group_id: str | None = None
104
+ updated_title: str | None = None
105
+ completed = False
106
+
107
+ for event in events:
108
+ data = event.get("data")
109
+ if data == "[DONE]":
110
+ continue
111
+ raw_events.append(event)
112
+ if not isinstance(data, dict):
113
+ continue
114
+
115
+ event_type = str(data.get("type") or event.get("event") or "message")
116
+ response_id = str(data.get("response_id") or response_id or "") or None
117
+ conversation_group_id = str(
118
+ data.get("conversation_group_id") or conversation_group_id or ""
119
+ ) or None
120
+
121
+ if event_type == "response.part.delta":
122
+ delta = data.get("delta")
123
+ if data.get("part_kind") == "text" and isinstance(delta, str):
124
+ text_deltas.append(delta)
125
+ elif event_type in ("response.part.created", "response.part.completed"):
126
+ part = data.get("part")
127
+ if isinstance(part, dict):
128
+ parts.append({"event": event_type, "part": part})
129
+ content = part.get("content")
130
+ if (
131
+ event_type == "response.part.completed"
132
+ and part.get("part_kind") == "text"
133
+ and isinstance(content, dict)
134
+ and isinstance(content.get("content"), str)
135
+ ):
136
+ final_text = content["content"]
137
+ elif event_type in ("response.interaction.created", "response.interaction.resolved"):
138
+ interactions.append(data)
139
+ elif event_type == "response.chat.title.updated":
140
+ name = data.get("name")
141
+ if isinstance(name, str):
142
+ updated_title = name
143
+ elif event_type == "response.failed":
144
+ message = data.get("message") or "Chat V2 stream failed"
145
+ raise TransportError(
146
+ str(message),
147
+ {
148
+ "code": data.get("code"),
149
+ "response_id": response_id,
150
+ "conversation_group_id": conversation_group_id,
151
+ "outcome_uncertain": True,
152
+ "events": raw_events,
153
+ },
154
+ )
155
+ elif event_type == "response.completed":
156
+ completed = True
157
+
158
+ if not completed:
159
+ raise TransportError(
160
+ "Chat V2 stream ended before response.completed. Inspect the history before retrying.",
161
+ {
162
+ "response_id": response_id,
163
+ "conversation_group_id": conversation_group_id,
164
+ "outcome_uncertain": True,
165
+ "events": raw_events,
166
+ },
167
+ )
168
+
169
+ return {
170
+ "stream": True,
171
+ "completed": True,
172
+ "response_id": response_id,
173
+ "conversation_group_id": conversation_group_id,
174
+ "final_text": final_text if final_text is not None else "".join(text_deltas),
175
+ "updated_title": updated_title,
176
+ "parts": parts,
177
+ "interactions": interactions,
178
+ "events": raw_events,
179
+ }
180
+
181
+
182
+ def list_messages(
183
+ client: CodeerClient,
184
+ chat_id: int,
185
+ *,
186
+ external_user_id: Optional[str] = None,
187
+ limit: int = 500,
188
+ ) -> dict:
189
+ params: dict[str, Any] = {"limit": limit, "offset": 0}
190
+ if external_user_id is not None:
191
+ params["external_user_id"] = external_user_id
192
+ return client.get(
193
+ f"/chats/{chat_id}/messages",
194
+ api_version="v2",
195
+ params=params,
196
+ )
197
+
198
+
199
+ def list_chats(client: CodeerClient) -> list[dict]:
200
+ """List chats through the legacy v1 endpoint; Chat V2 has no list route."""
201
+ return client.get("/chats")
@@ -2,6 +2,7 @@
2
2
 
3
3
  codeer check
4
4
  codeer agent list|get|apply|diff|versions
5
+ codeer model list
5
6
  codeer kb list|files|upload|node-rename|node-delete|faq-list|faq-get|faq-create|faq-update|faq-delete
6
7
  codeer eval list|label-list|label-create|label-update|label-delete|case-update|case-delete|evaluators|evaluator-create|evaluator-update|run|export|reconcile|cases-apply|rubrics|rubrics-apply
7
8
  codeer history list|get|conversations|negative-feedback|create|send
@@ -25,6 +26,7 @@ def main(argv: list[str] | None = None) -> int:
25
26
  epilog="""\
26
27
  Safe workflow for coding agents:
27
28
  codeer check --json
29
+ codeer model list --type text
28
30
  codeer agent list
29
31
  codeer agent get <agent-id> --full
30
32
  codeer kb list
@@ -55,6 +57,12 @@ Use --out <path> for large raw artifacts; stdout defaults to compact summaries.
55
57
 
56
58
  check.register(sub)
57
59
 
60
+ try:
61
+ from .commands import model as model_cmd
62
+ model_cmd.register(sub)
63
+ except ImportError:
64
+ pass
65
+
58
66
  # Phase 2-4: agent, kb, eval commands will register here
59
67
  try:
60
68
  from .commands import agent as agent_cmd