codeer-cli 0.1.8__tar.gz → 0.1.10__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.
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/API_REFERENCE.md +34 -7
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/PKG-INFO +41 -1
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/README.md +40 -0
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/pyproject.toml +1 -1
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/__init__.py +4 -3
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/_validate.py +42 -1
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/agents.py +9 -1
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/chats.py +2 -2
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/cli.py +9 -1
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/client.py +96 -24
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/commands/agent.py +26 -3
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/commands/eval_cmd.py +10 -16
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/commands/history.py +125 -13
- codeer_cli-0.1.10/src/codeer_cli/commands/model.py +43 -0
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/eval_.py +5 -6
- codeer_cli-0.1.10/src/codeer_cli/models.py +16 -0
- codeer_cli-0.1.10/tests/test_agent_handoff.py +153 -0
- codeer_cli-0.1.10/tests/test_client_transport.py +84 -0
- codeer_cli-0.1.10/tests/test_eval_labels.py +238 -0
- codeer_cli-0.1.10/tests/test_history_send.py +159 -0
- codeer_cli-0.1.10/tests/test_models.py +68 -0
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/uv.lock +1 -1
- codeer_cli-0.1.8/tests/test_eval_labels.py +0 -101
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/.gitignore +0 -0
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/commands/__init__.py +0 -0
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/commands/_util.py +0 -0
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/commands/check.py +0 -0
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/commands/kb.py +0 -0
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/commands/profile.py +0 -0
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/constants.py +0 -0
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/histories.py +0 -0
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/kb.py +0 -0
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/src/codeer_cli/parse.py +0 -0
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/tests/test_eval_pairs.py +0 -0
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/tests/test_kb_nodes.py +0 -0
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/tests/test_kb_ranges.py +0 -0
- {codeer_cli-0.1.8 → codeer_cli-0.1.10}/tests/test_util.py +0 -0
|
@@ -49,6 +49,23 @@ like `knowledge_node_ids`, `domain`, `agent_id`, `http_request` config.
|
|
|
49
49
|
|
|
50
50
|
Limits: 10 tools per agent, ≤5 `call_agent`, ≤1 `memory`.
|
|
51
51
|
|
|
52
|
+
Agent create/update payloads may also include:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"human_handoff": {
|
|
57
|
+
"enabled": true,
|
|
58
|
+
"idle_timeout_minutes": null,
|
|
59
|
+
"handoff_instructions": "Hand off when the user asks for a person."
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`idle_timeout_minutes` must be greater than zero when provided. The runtime
|
|
65
|
+
only exposes the handoff tool to an external conversation with a non-empty
|
|
66
|
+
`external_user_id`, or to an evaluation run. Internal editor Live Test does not
|
|
67
|
+
activate human mode.
|
|
68
|
+
|
|
52
69
|
## Stage 2 — Knowledge bases
|
|
53
70
|
|
|
54
71
|
Base path: `/organizations/{org_id}/workspaces/{ws_id}/knowledge_bases`
|
|
@@ -149,10 +166,10 @@ for the apply → test → publish workflow. Pass the draft `AgentHistory.id` fr
|
|
|
149
166
|
|
|
150
167
|
| Method & path | Purpose |
|
|
151
168
|
| --- | --- |
|
|
152
|
-
| `GET /eval/
|
|
153
|
-
| `POST /eval/
|
|
154
|
-
| `PUT /eval/case-labels/{label_id}` | Update eval case label
|
|
155
|
-
| `DELETE /eval/case-labels/{label_id}` | Delete eval case label and clear associations |
|
|
169
|
+
| `GET /external/eval/case-labels` | List reusable eval case labels in the API key workspace |
|
|
170
|
+
| `POST /external/eval/case-labels` | Create reusable eval case label (`name`, `color?`) in the API key workspace |
|
|
171
|
+
| `PUT /external/eval/case-labels/{label_id}` | Update an eval case label in the API key workspace |
|
|
172
|
+
| `DELETE /external/eval/case-labels/{label_id}` | Delete an eval case label and clear associations |
|
|
156
173
|
| `POST /eval/cases` | Create case (`input`, `expected_output?`, `rubric?`, `label_ids?`); rubric = user-docs "Standard" |
|
|
157
174
|
| `GET /eval/agents/{agent_id}/cases` | List cases for an agent |
|
|
158
175
|
| `GET /eval/cases/{case_id}` | Read one |
|
|
@@ -171,8 +188,11 @@ for the apply → test → publish workflow. Pass the draft `AgentHistory.id` fr
|
|
|
171
188
|
| `POST /eval/rubric` | Set/override the rubric for one (case, evaluator); also creates assignment |
|
|
172
189
|
| `POST /eval/rubrics/batch` | **Read** rubrics for a batch of (case, evaluator) pairs |
|
|
173
190
|
|
|
174
|
-
Eval case labels are workspace-scoped reusable objects. The
|
|
175
|
-
|
|
191
|
+
Eval case labels are workspace-scoped reusable objects. The external label
|
|
192
|
+
endpoints infer the workspace from the API key and do not accept a workspace
|
|
193
|
+
override; switch CLI profiles to operate on another workspace. The case
|
|
194
|
+
create/update payload uses `label_ids` (stringified label IDs), not freeform
|
|
195
|
+
label names:
|
|
176
196
|
|
|
177
197
|
```json
|
|
178
198
|
{
|
|
@@ -247,12 +267,19 @@ the public CLI.
|
|
|
247
267
|
|
|
248
268
|
| Method & path | Purpose |
|
|
249
269
|
| --- | --- |
|
|
270
|
+
| `POST /chats` | Create a persisted history using an agent's current published version |
|
|
271
|
+
| `POST /chats/{id}/messages` | Append a turn to an existing persisted history using the current published version |
|
|
250
272
|
| `GET /histories?agent_id=X&feedback_filter=improve_feedback&external_user_id=…` | List conversations with filters |
|
|
251
273
|
| `GET /histories/{id}` | Read one history's metadata |
|
|
252
274
|
| `GET /histories/{id}/conversations` | Full conversation turns incl. tool calls |
|
|
253
275
|
| `POST /histories/{hid}/conversations/{cid}/feedbacks` | Leave freeform improvement feedback |
|
|
254
276
|
| `POST /histories/{hid}/conversations/{cid}/score` | Numeric score |
|
|
255
277
|
|
|
278
|
+
The CLI exposes the first two operations as `codeer history create` and
|
|
279
|
+
`codeer history send`. Non-streaming message requests default to a 120-second
|
|
280
|
+
per-message timeout. A timeout has an uncertain write outcome, so read the
|
|
281
|
+
history before retrying to avoid duplicate turns.
|
|
282
|
+
|
|
256
283
|
`feedback_filter` accepts the `FeedbackFilterType` enum values:
|
|
257
284
|
`no_feedback`, `with_feedback`, `helpful_feedback`, `improve_feedback`.
|
|
258
285
|
|
|
@@ -270,7 +297,7 @@ Non-destructive: older versions stay in `GET /agents/{id}/histories`.
|
|
|
270
297
|
| --- | --- |
|
|
271
298
|
| `GET /accounts/me` | Sanity-check session, read workspace_organization_map |
|
|
272
299
|
| `GET /organizations` | List orgs visible to the user |
|
|
273
|
-
| `GET /llm/models` | List available LLM model IDs to use as `llm_model` |
|
|
300
|
+
| `GET /llm/models` | List available LLM model IDs to use as `llm_model` (`codeer model list`) |
|
|
274
301
|
| `GET /retrieval/...` | Shared retrieval helpers (file upload for attachments, markdown conversion) |
|
|
275
302
|
|
|
276
303
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeer-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.10
|
|
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,9 +172,20 @@ 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
|
|
175
|
+
codeer history create --agent <agent-id> --message "Review this plan" --timeout 120
|
|
176
|
+
codeer history send <history-id> --message "Use the recommended options" --timeout 120
|
|
146
177
|
codeer eval run --agent <agent-id> --cases <case-ids> --evaluator <evaluator-id> --out .codeer/eval_run.json
|
|
147
178
|
```
|
|
148
179
|
|
|
180
|
+
`history create` and `history send` use the agent's current published version.
|
|
181
|
+
Their per-message timeout defaults to 120 seconds. If a write request times
|
|
182
|
+
out, inspect the history before retrying: the server may have completed the
|
|
183
|
+
turn after the client stopped waiting.
|
|
184
|
+
|
|
185
|
+
Eval case label commands always operate on the active API-key workspace. They
|
|
186
|
+
do not accept a workspace override; switch CLI profiles to target another
|
|
187
|
+
workspace.
|
|
188
|
+
|
|
149
189
|
Flags:
|
|
150
190
|
|
|
151
191
|
- `--full` prints bounded extra detail for human inspection. It is still
|
|
@@ -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,9 +154,20 @@ 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
|
|
157
|
+
codeer history create --agent <agent-id> --message "Review this plan" --timeout 120
|
|
158
|
+
codeer history send <history-id> --message "Use the recommended options" --timeout 120
|
|
128
159
|
codeer eval run --agent <agent-id> --cases <case-ids> --evaluator <evaluator-id> --out .codeer/eval_run.json
|
|
129
160
|
```
|
|
130
161
|
|
|
162
|
+
`history create` and `history send` use the agent's current published version.
|
|
163
|
+
Their per-message timeout defaults to 120 seconds. If a write request times
|
|
164
|
+
out, inspect the history before retrying: the server may have completed the
|
|
165
|
+
turn after the client stopped waiting.
|
|
166
|
+
|
|
167
|
+
Eval case label commands always operate on the active API-key workspace. They
|
|
168
|
+
do not accept a workspace override; switch CLI profiles to target another
|
|
169
|
+
workspace.
|
|
170
|
+
|
|
131
171
|
Flags:
|
|
132
172
|
|
|
133
173
|
- `--full` prints bounded extra detail for human inspection. It is still
|
|
@@ -18,8 +18,8 @@ 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
|
|
22
|
-
from .client import AuthError, CodeerClient, CodeerError
|
|
21
|
+
from ._validate import HumanHandoffValidationError, ToolValidationError
|
|
22
|
+
from .client import AuthError, CodeerClient, CodeerError, TransportError
|
|
23
23
|
from .parse import (
|
|
24
24
|
AgentSummary,
|
|
25
25
|
ConversationTurn,
|
|
@@ -43,7 +43,8 @@ from .parse import (
|
|
|
43
43
|
)
|
|
44
44
|
|
|
45
45
|
__all__ = [
|
|
46
|
-
"CodeerClient", "CodeerError", "AuthError", "ToolValidationError",
|
|
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
|
|
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
|
|
|
@@ -36,6 +36,7 @@ def send_published_agent_message(
|
|
|
36
36
|
external_user_id: Optional[str] = None,
|
|
37
37
|
attachment_ids: Optional[List[str]] = None,
|
|
38
38
|
stream: bool = False,
|
|
39
|
+
timeout: Optional[float] = None,
|
|
39
40
|
) -> Iterator[dict] | dict:
|
|
40
41
|
"""Send a user message through the API-key external chat flow.
|
|
41
42
|
|
|
@@ -51,7 +52,7 @@ def send_published_agent_message(
|
|
|
51
52
|
path = f"/chats/{chat_id}/messages"
|
|
52
53
|
if stream:
|
|
53
54
|
return client.stream_sse("POST", path, json=body)
|
|
54
|
-
return client.post(path, json=body)
|
|
55
|
+
return client.post(path, json=body, timeout=timeout)
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
def send_message(
|
|
@@ -84,4 +85,3 @@ def list_messages(client: CodeerClient, chat_id: int) -> list[dict]:
|
|
|
84
85
|
|
|
85
86
|
def list_chats(client: CodeerClient) -> list[dict]:
|
|
86
87
|
return client.get("/chats")
|
|
87
|
-
|
|
@@ -2,9 +2,10 @@
|
|
|
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
|
-
codeer history list|get|conversations|negative-feedback
|
|
8
|
+
codeer history list|get|conversations|negative-feedback|create|send
|
|
8
9
|
"""
|
|
9
10
|
|
|
10
11
|
from __future__ import annotations
|
|
@@ -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
|
|
@@ -37,6 +37,16 @@ class ScopeResolutionError(CodeerError):
|
|
|
37
37
|
"""Raised when workspace or organization scope cannot be inferred."""
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
class TransportError(CodeerError):
|
|
41
|
+
"""Raised when an HTTP request fails before a response is available."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, message: str, body: Any = None):
|
|
44
|
+
RuntimeError.__init__(self, message)
|
|
45
|
+
self.status = 0
|
|
46
|
+
self.message = message
|
|
47
|
+
self.body = body
|
|
48
|
+
|
|
49
|
+
|
|
40
50
|
@dataclass
|
|
41
51
|
class CodeerClient:
|
|
42
52
|
"""Thin wrapper around httpx.Client with Codeer API-key auth.
|
|
@@ -148,9 +158,32 @@ class CodeerClient:
|
|
|
148
158
|
json: Any = None,
|
|
149
159
|
files: Any = None,
|
|
150
160
|
data: Any = None,
|
|
161
|
+
timeout: Optional[float] = None,
|
|
151
162
|
) -> Any:
|
|
152
163
|
url = path if path.startswith("http") else f"/api/v1{path if path.startswith('/') else '/' + path}"
|
|
153
|
-
|
|
164
|
+
request_kwargs: dict[str, Any] = {}
|
|
165
|
+
if timeout is not None:
|
|
166
|
+
request_kwargs["timeout"] = timeout
|
|
167
|
+
method_upper = method.upper()
|
|
168
|
+
try:
|
|
169
|
+
r = self._client.request(
|
|
170
|
+
method_upper,
|
|
171
|
+
url,
|
|
172
|
+
params=params,
|
|
173
|
+
json=json,
|
|
174
|
+
files=files,
|
|
175
|
+
data=data,
|
|
176
|
+
**request_kwargs,
|
|
177
|
+
)
|
|
178
|
+
except httpx.TimeoutException as exc:
|
|
179
|
+
raise self._transport_error(
|
|
180
|
+
method_upper,
|
|
181
|
+
path,
|
|
182
|
+
exc,
|
|
183
|
+
timeout_seconds=timeout if timeout is not None else self.timeout,
|
|
184
|
+
) from exc
|
|
185
|
+
except httpx.RequestError as exc:
|
|
186
|
+
raise self._transport_error(method_upper, path, exc) from exc
|
|
154
187
|
return self._parse(r)
|
|
155
188
|
|
|
156
189
|
def get(self, path: str, **kwargs: Any) -> Any:
|
|
@@ -181,29 +214,68 @@ class CodeerClient:
|
|
|
181
214
|
Each event is a dict like ``{"event": "message", "data": <parsed-json-or-str>}``.
|
|
182
215
|
"""
|
|
183
216
|
url = path if path.startswith("http") else f"/api/v1{path if path.startswith('/') else '/' + path}"
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
217
|
+
method_upper = method.upper()
|
|
218
|
+
try:
|
|
219
|
+
with self._client.stream(method_upper, url, params=params, json=json) as r:
|
|
220
|
+
if r.status_code >= 400:
|
|
221
|
+
body = r.read().decode("utf-8", "replace")
|
|
222
|
+
self._raise_for_error(r.status_code, body)
|
|
223
|
+
event = "message"
|
|
224
|
+
buf: list[str] = []
|
|
225
|
+
for line in r.iter_lines():
|
|
226
|
+
if line == "":
|
|
227
|
+
if buf:
|
|
228
|
+
raw = "\n".join(buf)
|
|
229
|
+
yield {"event": event, "data": _maybe_json(raw)}
|
|
230
|
+
buf = []
|
|
231
|
+
event = "message"
|
|
232
|
+
continue
|
|
233
|
+
if line.startswith(":"):
|
|
234
|
+
continue
|
|
235
|
+
if line.startswith("event:"):
|
|
236
|
+
event = line[len("event:"):].strip()
|
|
237
|
+
continue
|
|
238
|
+
if line.startswith("data:"):
|
|
239
|
+
buf.append(line[len("data:"):].lstrip())
|
|
240
|
+
if buf:
|
|
241
|
+
yield {"event": event, "data": _maybe_json("\n".join(buf))}
|
|
242
|
+
except httpx.TimeoutException as exc:
|
|
243
|
+
raise self._transport_error(
|
|
244
|
+
method_upper,
|
|
245
|
+
path,
|
|
246
|
+
exc,
|
|
247
|
+
timeout_seconds=self.timeout,
|
|
248
|
+
) from exc
|
|
249
|
+
except httpx.RequestError as exc:
|
|
250
|
+
raise self._transport_error(method_upper, path, exc) from exc
|
|
251
|
+
|
|
252
|
+
def _transport_error(
|
|
253
|
+
self,
|
|
254
|
+
method: str,
|
|
255
|
+
path: str,
|
|
256
|
+
exc: httpx.RequestError,
|
|
257
|
+
*,
|
|
258
|
+
timeout_seconds: float | None = None,
|
|
259
|
+
) -> TransportError:
|
|
260
|
+
outcome_uncertain = (
|
|
261
|
+
method not in {"GET", "HEAD", "OPTIONS"}
|
|
262
|
+
and not isinstance(exc, httpx.ConnectError)
|
|
263
|
+
)
|
|
264
|
+
if isinstance(exc, httpx.TimeoutException):
|
|
265
|
+
timeout_value = timeout_seconds if timeout_seconds is not None else self.timeout
|
|
266
|
+
message = f"Request timed out after {timeout_value:g}s: {method} {path}."
|
|
267
|
+
if outcome_uncertain:
|
|
268
|
+
message += " The server may have completed the request; inspect current state before retrying."
|
|
269
|
+
else:
|
|
270
|
+
message = f"Request failed: {method} {path}: {exc}"
|
|
271
|
+
return TransportError(
|
|
272
|
+
message,
|
|
273
|
+
{
|
|
274
|
+
"method": method,
|
|
275
|
+
"path": path,
|
|
276
|
+
"outcome_uncertain": outcome_uncertain,
|
|
277
|
+
},
|
|
278
|
+
)
|
|
207
279
|
|
|
208
280
|
def _parse(self, r: httpx.Response) -> Any:
|
|
209
281
|
text = r.text
|
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Optional
|
|
7
7
|
|
|
8
8
|
from .. import agents as agents_mod
|
|
9
|
+
from .._validate import validate_human_handoff, validate_unified_tools
|
|
9
10
|
from ..client import CodeerClient
|
|
10
11
|
from ._util import log, print_json, strip_noisy_fields, truncate, write_json
|
|
11
12
|
|
|
@@ -102,6 +103,12 @@ def _tool_summary(tools: list[dict] | None) -> list[dict]:
|
|
|
102
103
|
|
|
103
104
|
def _agent_summary(agent: dict, *, full: bool = False) -> dict:
|
|
104
105
|
tools = agent.get("unified_tools") or agent.get("tools") or []
|
|
106
|
+
human_handoff = agent.get("human_handoff")
|
|
107
|
+
if not isinstance(human_handoff, dict):
|
|
108
|
+
meta = agent.get("meta")
|
|
109
|
+
human_handoff = meta.get("human_handoff") if isinstance(meta, dict) else {}
|
|
110
|
+
if not isinstance(human_handoff, dict):
|
|
111
|
+
human_handoff = {}
|
|
105
112
|
row = {
|
|
106
113
|
"id": agent.get("id"),
|
|
107
114
|
"name": agent.get("name"),
|
|
@@ -118,6 +125,7 @@ def _agent_summary(agent: dict, *, full: bool = False) -> dict:
|
|
|
118
125
|
"agent_type": agent.get("agent_type"),
|
|
119
126
|
"updated_at": agent.get("updated_at"),
|
|
120
127
|
"tool_count": len(tools),
|
|
128
|
+
"human_handoff_enabled": bool(human_handoff.get("enabled")),
|
|
121
129
|
"system_prompt_chars": len(agent.get("system_prompt") or ""),
|
|
122
130
|
}
|
|
123
131
|
if full:
|
|
@@ -125,6 +133,7 @@ def _agent_summary(agent: dict, *, full: bool = False) -> dict:
|
|
|
125
133
|
row["use_search"] = agent.get("use_search")
|
|
126
134
|
row["suggested_questions"] = agent.get("suggested_questions") or []
|
|
127
135
|
row["tools"] = _tool_summary(tools)
|
|
136
|
+
row["human_handoff"] = human_handoff
|
|
128
137
|
row["system_prompt_preview"] = truncate(agent.get("system_prompt") or "", 1200)
|
|
129
138
|
return row
|
|
130
139
|
|
|
@@ -153,6 +162,13 @@ def run_apply(args, client) -> int:
|
|
|
153
162
|
log(f"error: payload missing required field(s): {', '.join(missing)}")
|
|
154
163
|
return 2
|
|
155
164
|
|
|
165
|
+
try:
|
|
166
|
+
validated_tools = validate_unified_tools(body.get("unified_tools") or [])
|
|
167
|
+
validated_handoff = validate_human_handoff(body.get("human_handoff"))
|
|
168
|
+
except ValueError as exc:
|
|
169
|
+
log(f"error: invalid agent payload: {exc}")
|
|
170
|
+
return 2
|
|
171
|
+
|
|
156
172
|
if args.dry_run:
|
|
157
173
|
operation = "update" if args.agent_id else "create"
|
|
158
174
|
result = {
|
|
@@ -162,9 +178,14 @@ def run_apply(args, client) -> int:
|
|
|
162
178
|
"payload": str(Path(args.payload)),
|
|
163
179
|
"name": body.get("name"),
|
|
164
180
|
"system_prompt_chars": len(body.get("system_prompt") or ""),
|
|
165
|
-
"tool_count": len(
|
|
181
|
+
"tool_count": len(validated_tools),
|
|
166
182
|
"use_search": body.get("use_search", False),
|
|
167
183
|
"llm_model": body.get("llm_model"),
|
|
184
|
+
"human_handoff": {
|
|
185
|
+
"enabled": bool((validated_handoff or {}).get("enabled")),
|
|
186
|
+
"idle_timeout_minutes": (validated_handoff or {}).get("idle_timeout_minutes"),
|
|
187
|
+
"instructions_chars": len((validated_handoff or {}).get("handoff_instructions") or ""),
|
|
188
|
+
},
|
|
168
189
|
"version_note": args.note if args.agent_id else None,
|
|
169
190
|
"would_write_server_state": True,
|
|
170
191
|
"next_step": "Review this summary, then rerun without --dry-run after approval.",
|
|
@@ -178,7 +199,7 @@ def run_apply(args, client) -> int:
|
|
|
178
199
|
client, args.agent_id,
|
|
179
200
|
name=body["name"],
|
|
180
201
|
system_prompt=body["system_prompt"],
|
|
181
|
-
unified_tools=
|
|
202
|
+
unified_tools=validated_tools,
|
|
182
203
|
use_search=body.get("use_search", False),
|
|
183
204
|
version_note=args.note,
|
|
184
205
|
description=body.get("description"),
|
|
@@ -186,6 +207,7 @@ def run_apply(args, client) -> int:
|
|
|
186
207
|
suggested_questions=body.get("suggested_questions") or [],
|
|
187
208
|
primary_object_ids=body.get("primary_object_ids") or [],
|
|
188
209
|
attachment_ids=body.get("attachment_ids") or [],
|
|
210
|
+
human_handoff=validated_handoff,
|
|
189
211
|
)
|
|
190
212
|
agent_id = args.agent_id
|
|
191
213
|
log(f"PUT /agents/{agent_id} ok")
|
|
@@ -197,13 +219,14 @@ def run_apply(args, client) -> int:
|
|
|
197
219
|
workspace_id=body["workspace_id"],
|
|
198
220
|
name=body["name"],
|
|
199
221
|
system_prompt=body["system_prompt"],
|
|
200
|
-
unified_tools=
|
|
222
|
+
unified_tools=validated_tools,
|
|
201
223
|
use_search=body.get("use_search", False),
|
|
202
224
|
description=body.get("description"),
|
|
203
225
|
llm_model=body.get("llm_model"),
|
|
204
226
|
suggested_questions=body.get("suggested_questions") or [],
|
|
205
227
|
primary_object_ids=body.get("primary_object_ids") or [],
|
|
206
228
|
attachment_ids=body.get("attachment_ids") or [],
|
|
229
|
+
human_handoff=validated_handoff,
|
|
207
230
|
)
|
|
208
231
|
agent_id = agent["id"]
|
|
209
232
|
log(f"POST /agents ok, id={agent_id}")
|