codeer-cli 0.1.15__py3-none-any.whl → 0.1.16__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.
- codeer_cli/_http_contracts.py +109 -0
- codeer_cli/_validate.py +8 -2
- codeer_cli/agents.py +1 -1
- codeer_cli/cli.py +5 -1
- codeer_cli/client.py +4 -3
- codeer_cli/commands/_util.py +7 -2
- codeer_cli/commands/agent.py +31 -10
- codeer_cli/commands/history.py +96 -0
- codeer_cli/histories.py +123 -24
- {codeer_cli-0.1.15.dist-info → codeer_cli-0.1.16.dist-info}/METADATA +146 -11
- {codeer_cli-0.1.15.dist-info → codeer_cli-0.1.16.dist-info}/RECORD +13 -12
- {codeer_cli-0.1.15.dist-info → codeer_cli-0.1.16.dist-info}/WHEEL +1 -1
- {codeer_cli-0.1.15.dist-info → codeer_cli-0.1.16.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""HTTP input *configuration* checks; value conversion belongs to the server.
|
|
2
|
+
|
|
3
|
+
Key generation follows collect_http_request_agent_placeholders in the backend's
|
|
4
|
+
http_request/execution.py. Keep its traversal and collision rules in sync.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_AGENT_PLACEHOLDER_RE = re.compile(r"\{\{\s*agent\[(.*?)\]\s*\}\}")
|
|
14
|
+
_CONTRACT_FIELDS = {
|
|
15
|
+
"type": ("string", "number", "integer", "boolean", "object", "array"),
|
|
16
|
+
"format": ("text", "json"),
|
|
17
|
+
"json_type": ("any", "object", "array"),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _contract_defaults(contract: Any, key: str) -> dict[str, str]:
|
|
22
|
+
prefix = f"input_contracts[{key!r}]"
|
|
23
|
+
if not isinstance(contract, dict):
|
|
24
|
+
raise ValueError(f"{prefix} must be an object.")
|
|
25
|
+
if any(field not in _CONTRACT_FIELDS for field in contract):
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"{prefix} accepts only type, format, json_type (snake_case, not jsonType)."
|
|
28
|
+
)
|
|
29
|
+
resolved = {}
|
|
30
|
+
for field, choices in _CONTRACT_FIELDS.items():
|
|
31
|
+
value = contract.get(field, choices[0])
|
|
32
|
+
if not isinstance(value, str) or value not in choices:
|
|
33
|
+
raise ValueError(f"{prefix}.{field} must be one of: {', '.join(choices)}.")
|
|
34
|
+
resolved[field] = value
|
|
35
|
+
if resolved["format"] == "json" and resolved["type"] != "string":
|
|
36
|
+
raise ValueError(f"{prefix}: format=json is only available for type=string.")
|
|
37
|
+
if resolved["format"] != "json" and resolved["json_type"] != "any":
|
|
38
|
+
raise ValueError(f"{prefix}: json_type requires format=json unless it is any.")
|
|
39
|
+
return resolved
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def validate_http_input_contracts(body: Any) -> list[dict[str, Any]]:
|
|
43
|
+
"""Validate without mutation and return value-free input format summaries.
|
|
44
|
+
|
|
45
|
+
An omitted body is supplied as {} by callers; template may be null/absent.
|
|
46
|
+
Unconfigured placeholders retain string/text/any, including mixed text.
|
|
47
|
+
Unknown body fields follow the backend's existing ignore policy, except the
|
|
48
|
+
frontend alias inputContracts: reject it rather than silently losing intent.
|
|
49
|
+
Contract entries themselves are strict and reject all unknown fields.
|
|
50
|
+
"""
|
|
51
|
+
if not isinstance(body, dict):
|
|
52
|
+
raise ValueError("body must be an object; omit it for no body.")
|
|
53
|
+
if "inputContracts" in body:
|
|
54
|
+
raise ValueError("use input_contracts (snake_case), not inputContracts.")
|
|
55
|
+
contracts = body.get("input_contracts", {})
|
|
56
|
+
if not isinstance(contracts, dict) or any(
|
|
57
|
+
not isinstance(key, str) for key in contracts
|
|
58
|
+
):
|
|
59
|
+
raise ValueError("input_contracts must be an object with string keys.")
|
|
60
|
+
resolved = {
|
|
61
|
+
key: _contract_defaults(contract, key) for key, contract in contracts.items()
|
|
62
|
+
}
|
|
63
|
+
summaries: list[dict[str, Any]] = []
|
|
64
|
+
used_keys: set[str] = set()
|
|
65
|
+
|
|
66
|
+
def walk(value: Any, path: str) -> None:
|
|
67
|
+
if isinstance(value, dict):
|
|
68
|
+
for key, item in value.items():
|
|
69
|
+
walk(item, f"{path}.{key}" if path else key)
|
|
70
|
+
return
|
|
71
|
+
if isinstance(value, list):
|
|
72
|
+
for index, item in enumerate(value):
|
|
73
|
+
walk(item, f"{path}[{index}]")
|
|
74
|
+
return
|
|
75
|
+
if not isinstance(value, str):
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
matches = list(_AGENT_PLACEHOLDER_RE.finditer(value))
|
|
79
|
+
for index, match in enumerate(matches):
|
|
80
|
+
if not match.group(1).strip():
|
|
81
|
+
raise ValueError("Agent placeholder instruction cannot be empty.")
|
|
82
|
+
base = (
|
|
83
|
+
re.sub(r"[^a-zA-Z0-9]+", "_", path or "body").strip("_").lower()
|
|
84
|
+
or "body"
|
|
85
|
+
)
|
|
86
|
+
if len(matches) > 1:
|
|
87
|
+
base = f"{base}_{index + 1}"
|
|
88
|
+
key = base
|
|
89
|
+
suffix = 2
|
|
90
|
+
while key in used_keys:
|
|
91
|
+
key = f"{base}_{suffix}"
|
|
92
|
+
suffix += 1
|
|
93
|
+
used_keys.add(key)
|
|
94
|
+
contract = resolved.get(key) or _contract_defaults({}, key)
|
|
95
|
+
if (contract["type"] != "string" or contract["format"] == "json") and (
|
|
96
|
+
len(matches) != 1 or match.group(0) != value
|
|
97
|
+
):
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Input {key!r}: typed and JSON-text inputs must occupy the entire template value."
|
|
100
|
+
)
|
|
101
|
+
summaries.append({"key": key, **contract, "configured": key in contracts})
|
|
102
|
+
|
|
103
|
+
walk(body.get("template"), "")
|
|
104
|
+
if set(contracts) - used_keys:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
"input_contracts contains keys that do not match current placeholders; "
|
|
107
|
+
"remove stale contracts or regenerate keys after changing template paths/order."
|
|
108
|
+
)
|
|
109
|
+
return summaries
|
codeer_cli/_validate.py
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
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
|
|
5
5
|
render as blank fields in the web builder. Catching the common mistakes here
|
|
6
|
-
gives actionable errors before the
|
|
6
|
+
gives actionable errors before the PATCH/POST round-trip. HTTP input contracts
|
|
7
|
+
also need strict configuration checks before a draft is saved.
|
|
7
8
|
"""
|
|
8
9
|
|
|
9
10
|
from __future__ import annotations
|
|
10
11
|
|
|
11
12
|
from typing import Any, Iterable
|
|
12
13
|
|
|
14
|
+
from ._http_contracts import validate_http_input_contracts
|
|
13
15
|
from .constants import (
|
|
14
16
|
FORM_FIELD_TYPES,
|
|
15
17
|
MAX_CALL_AGENT_TOOLS,
|
|
@@ -121,10 +123,14 @@ def _validate_single_tool(tool: dict[str, Any], index: int) -> None:
|
|
|
121
123
|
raise ToolValidationError(f"{prefix}: call_agent tool requires agent_id.")
|
|
122
124
|
elif tool_type == "http_request":
|
|
123
125
|
cfg = tool.get("http_request")
|
|
124
|
-
if not cfg or not cfg.get("method") or not cfg.get("url_template"):
|
|
126
|
+
if not isinstance(cfg, dict) or not cfg.get("method") or not cfg.get("url_template"):
|
|
125
127
|
raise ToolValidationError(
|
|
126
128
|
f"{prefix}: http_request tool requires http_request.method and http_request.url_template."
|
|
127
129
|
)
|
|
130
|
+
try:
|
|
131
|
+
validate_http_input_contracts(cfg.get("body", {}))
|
|
132
|
+
except ValueError as exc:
|
|
133
|
+
raise ToolValidationError(f"{prefix}.http_request.body: {exc}") from None
|
|
128
134
|
|
|
129
135
|
|
|
130
136
|
def _validate_form_schema(schema: Any, prefix: str) -> None:
|
codeer_cli/agents.py
CHANGED
|
@@ -66,7 +66,7 @@ def update(
|
|
|
66
66
|
attachment_ids: Optional[List[str]] = None,
|
|
67
67
|
human_handoff: Optional[dict[str, Any]] = None,
|
|
68
68
|
) -> dict:
|
|
69
|
-
"""
|
|
69
|
+
"""PATCH replaces settings and creates a draft; unified_tools is the full list."""
|
|
70
70
|
validated_tools = validate_unified_tools(unified_tools)
|
|
71
71
|
validated_handoff = validate_human_handoff(human_handoff)
|
|
72
72
|
body: dict[str, Any] = {
|
codeer_cli/cli.py
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
codeer model list
|
|
6
6
|
codeer kb list|files|export|upload|node-rename|node-delete|faq-list|faq-get|faq-create|faq-update|faq-delete
|
|
7
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
|
|
8
|
-
codeer history list|get|conversations|negative-feedback|create|send
|
|
8
|
+
codeer history list|get|conversations|ai-drafts|negative-feedback|create|send
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
@@ -16,6 +16,7 @@ import sys
|
|
|
16
16
|
|
|
17
17
|
from .client import AuthError, CodeerClient, CodeerError
|
|
18
18
|
from .commands import check
|
|
19
|
+
from .histories import HistoryExportError
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -127,6 +128,9 @@ Use --out <path> for large raw artifacts; stdout defaults to compact summaries.
|
|
|
127
128
|
|
|
128
129
|
try:
|
|
129
130
|
return args.func(args, client)
|
|
131
|
+
except HistoryExportError as e:
|
|
132
|
+
print(f"error: {e}", file=sys.stderr)
|
|
133
|
+
return 1
|
|
130
134
|
except AuthError as e:
|
|
131
135
|
print(f"auth: {e}", file=sys.stderr)
|
|
132
136
|
return 3
|
codeer_cli/client.py
CHANGED
|
@@ -161,6 +161,7 @@ class CodeerClient:
|
|
|
161
161
|
files: Any = None,
|
|
162
162
|
data: Any = None,
|
|
163
163
|
timeout: Optional[float] = None,
|
|
164
|
+
unwrap: bool = True,
|
|
164
165
|
) -> Any:
|
|
165
166
|
url = _api_url(path, api_version=api_version)
|
|
166
167
|
request_kwargs: dict[str, Any] = {}
|
|
@@ -186,7 +187,7 @@ class CodeerClient:
|
|
|
186
187
|
) from exc
|
|
187
188
|
except httpx.RequestError as exc:
|
|
188
189
|
raise self._transport_error(method_upper, path, exc) from exc
|
|
189
|
-
return self._parse(r)
|
|
190
|
+
return self._parse(r, unwrap=unwrap)
|
|
190
191
|
|
|
191
192
|
def get(self, path: str, **kwargs: Any) -> Any:
|
|
192
193
|
return self.request("GET", path, **kwargs)
|
|
@@ -290,7 +291,7 @@ class CodeerClient:
|
|
|
290
291
|
},
|
|
291
292
|
)
|
|
292
293
|
|
|
293
|
-
def _parse(self, r: httpx.Response) -> Any:
|
|
294
|
+
def _parse(self, r: httpx.Response, *, unwrap: bool = True) -> Any:
|
|
294
295
|
text = r.text
|
|
295
296
|
try:
|
|
296
297
|
payload = r.json() if text else None
|
|
@@ -306,7 +307,7 @@ class CodeerClient:
|
|
|
306
307
|
if isinstance(payload, dict) and "error_code" in payload and "data" in payload:
|
|
307
308
|
if payload.get("error_code") not in (0, None):
|
|
308
309
|
raise CodeerError(r.status_code, payload.get("message") or "error", payload)
|
|
309
|
-
return payload["data"]
|
|
310
|
+
return payload["data"] if unwrap else payload
|
|
310
311
|
return payload
|
|
311
312
|
|
|
312
313
|
def _raise_for_error(self, status: int, payload: Any) -> None:
|
codeer_cli/commands/_util.py
CHANGED
|
@@ -34,7 +34,12 @@ NOISY_KEYS = {
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def strip_noisy_fields(value: Any) -> Any:
|
|
37
|
-
"""
|
|
37
|
+
"""Simplify metadata on one API resource (or a list of resources).
|
|
38
|
+
|
|
39
|
+
Callers pass unwrapped resource records, not response envelopes. Nested
|
|
40
|
+
dictionaries are opaque content: templates, schemas, tool results and other
|
|
41
|
+
user data can legally contain any of the metadata names. Do not recurse.
|
|
42
|
+
"""
|
|
38
43
|
if isinstance(value, list):
|
|
39
44
|
return [strip_noisy_fields(item) for item in value]
|
|
40
45
|
if not isinstance(value, dict):
|
|
@@ -51,7 +56,7 @@ def strip_noisy_fields(value: Any) -> Any:
|
|
|
51
56
|
if item.get(k) is not None
|
|
52
57
|
}
|
|
53
58
|
continue
|
|
54
|
-
out[key] =
|
|
59
|
+
out[key] = item
|
|
55
60
|
return out
|
|
56
61
|
|
|
57
62
|
|
codeer_cli/commands/agent.py
CHANGED
|
@@ -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 .._http_contracts import validate_http_input_contracts
|
|
9
10
|
from .._validate import validate_human_handoff, validate_unified_tools
|
|
10
11
|
from ..client import CodeerClient
|
|
11
12
|
from ._util import log, print_json, strip_noisy_fields, truncate, write_json
|
|
@@ -32,6 +33,7 @@ def register(subparsers):
|
|
|
32
33
|
help="Read one agent. Defaults to summary; use --full for prompt/tool detail or --out for an artifact.",
|
|
33
34
|
)
|
|
34
35
|
p.add_argument("agent_id")
|
|
36
|
+
p.add_argument("--history", default=None, help="Read this exact AgentHistory UUID instead of the editable agent.")
|
|
35
37
|
p.add_argument("--full", action="store_true",
|
|
36
38
|
help="Print stripped full agent config, including system_prompt and tools.")
|
|
37
39
|
p.add_argument("--out", default=None,
|
|
@@ -41,8 +43,8 @@ def register(subparsers):
|
|
|
41
43
|
# codeer agent apply --payload agent.json
|
|
42
44
|
p = sub.add_parser("apply", help="Create or update agent from JSON payload; run --dry-run first")
|
|
43
45
|
p.add_argument("--payload", required=True, help="Path to agent payload JSON")
|
|
44
|
-
p.add_argument("--agent-id", default=None, help="If set,
|
|
45
|
-
p.add_argument("--note", default="", help="version_note for
|
|
46
|
+
p.add_argument("--agent-id", default=None, help="If set, PATCH (update). Else POST (create).")
|
|
47
|
+
p.add_argument("--note", default="", help="version_note for PATCH")
|
|
46
48
|
p.add_argument("--dry-run", action="store_true",
|
|
47
49
|
help="Validate payload and print intended mutation without writing server state.")
|
|
48
50
|
p.add_argument("--out", default=None, help="Write result JSON to this file too")
|
|
@@ -61,13 +63,13 @@ def register(subparsers):
|
|
|
61
63
|
# codeer agent versions --agent <id>
|
|
62
64
|
p = sub.add_parser(
|
|
63
65
|
"versions",
|
|
64
|
-
help="List version
|
|
66
|
+
help="List version metadata; use agent get --history <id> for a stored snapshot.",
|
|
65
67
|
)
|
|
66
68
|
p.add_argument("--agent", required=True)
|
|
67
69
|
p.add_argument("--full", action="store_true",
|
|
68
|
-
help="Add
|
|
70
|
+
help="Add prompt/tool size metadata when included by the server.")
|
|
69
71
|
p.add_argument("--out", default=None,
|
|
70
|
-
help="Write
|
|
72
|
+
help="Write the full version listing (metadata, not snapshots); stdout stays compact.")
|
|
71
73
|
p.set_defaults(func=run_versions)
|
|
72
74
|
|
|
73
75
|
p = sub.add_parser("impact", help="Check downstream agents affected by this agent")
|
|
@@ -130,6 +132,8 @@ def _agent_summary(agent: dict, *, full: bool = False) -> dict:
|
|
|
130
132
|
"human_handoff_enabled": bool(human_handoff.get("enabled")),
|
|
131
133
|
"system_prompt_chars": len(agent.get("system_prompt") or ""),
|
|
132
134
|
}
|
|
135
|
+
if "version_number" in agent:
|
|
136
|
+
row.update({key: agent.get(key) for key in ("agent_id", "version_number", "status")})
|
|
133
137
|
if full:
|
|
134
138
|
row["description"] = agent.get("description") or ""
|
|
135
139
|
row["use_search"] = agent.get("use_search")
|
|
@@ -150,7 +154,11 @@ def run_list(args, client) -> int:
|
|
|
150
154
|
|
|
151
155
|
|
|
152
156
|
def run_get(args, client) -> int:
|
|
153
|
-
|
|
157
|
+
history_id = getattr(args, "history", None)
|
|
158
|
+
result = (
|
|
159
|
+
agents_mod.get_version(client, args.agent_id, history_id)
|
|
160
|
+
if history_id else agents_mod.get(client, args.agent_id)
|
|
161
|
+
)
|
|
154
162
|
full_result = strip_noisy_fields(result)
|
|
155
163
|
write_json(args.out, full_result)
|
|
156
164
|
print_json(full_result if args.full else _agent_summary(result))
|
|
@@ -183,6 +191,15 @@ def run_apply(args, client) -> int:
|
|
|
183
191
|
"name": body.get("name"),
|
|
184
192
|
"system_prompt_chars": len(body.get("system_prompt") or ""),
|
|
185
193
|
"tool_count": len(validated_tools),
|
|
194
|
+
"http_inputs": [
|
|
195
|
+
{
|
|
196
|
+
"tool_index": index,
|
|
197
|
+
"body_inputs_used": tool["http_request"]["method"] not in ("GET", "HEAD"),
|
|
198
|
+
"inputs": validate_http_input_contracts(tool["http_request"].get("body", {})),
|
|
199
|
+
}
|
|
200
|
+
for index, tool in enumerate(validated_tools)
|
|
201
|
+
if tool.get("type") == "http_request"
|
|
202
|
+
],
|
|
186
203
|
"use_search": body.get("use_search", False),
|
|
187
204
|
"llm_model": body.get("llm_model"),
|
|
188
205
|
"llm_model_settings_provided": llm_model_settings_provided,
|
|
@@ -223,7 +240,7 @@ def run_apply(args, client) -> int:
|
|
|
223
240
|
**model_settings_kwargs,
|
|
224
241
|
)
|
|
225
242
|
agent_id = args.agent_id
|
|
226
|
-
log(f"
|
|
243
|
+
log(f"PATCH /agents/{agent_id} ok")
|
|
227
244
|
else:
|
|
228
245
|
if not body.get("workspace_id"):
|
|
229
246
|
body["workspace_id"] = client.resolve_scope()[0]
|
|
@@ -274,8 +291,10 @@ def run_versions(args, client) -> int:
|
|
|
274
291
|
"created_at": v.get("created_at"),
|
|
275
292
|
}
|
|
276
293
|
if args.full:
|
|
277
|
-
|
|
278
|
-
|
|
294
|
+
if "system_prompt" in v:
|
|
295
|
+
row["system_prompt_chars"] = len(v.get("system_prompt") or "")
|
|
296
|
+
if "unified_tools" in v or "tools" in v:
|
|
297
|
+
row["tool_count"] = len(v.get("unified_tools") or v.get("tools") or [])
|
|
279
298
|
rows.append(row)
|
|
280
299
|
print_json(rows)
|
|
281
300
|
return 0
|
|
@@ -371,7 +390,9 @@ def _summarize_tool(t: dict) -> str:
|
|
|
371
390
|
"knowledge_node_ids", "domain", "agent_id",
|
|
372
391
|
"custom_form_schema", "http_request")
|
|
373
392
|
safe = {k: t.get(k) for k in keep_keys if t.get(k) is not None}
|
|
374
|
-
|
|
393
|
+
# HTTP placeholder keys depend on template insertion order. Sorting would
|
|
394
|
+
# hide a reordering that changes which input receives a collision suffix.
|
|
395
|
+
return json.dumps(safe, ensure_ascii=False, indent=2, sort_keys=t.get("type") != "http_request")
|
|
375
396
|
|
|
376
397
|
|
|
377
398
|
def run_diff(args, client) -> int:
|
codeer_cli/commands/history.py
CHANGED
|
@@ -23,6 +23,8 @@ def register(subparsers):
|
|
|
23
23
|
p.add_argument("--agent", default=None)
|
|
24
24
|
p.add_argument("--user", default=None, help="Filter by external_user_id")
|
|
25
25
|
p.add_argument("--feedback", default=None, help="positive / negative / any")
|
|
26
|
+
p.add_argument("--has-ai-drafts", action="store_true",
|
|
27
|
+
help="Only list histories with at least one AI Draft.")
|
|
26
28
|
p.add_argument("--exclude-users", default=None,
|
|
27
29
|
help="Comma-separated external_user_ids to exclude")
|
|
28
30
|
p.add_argument("--version", type=int, default=None,
|
|
@@ -61,6 +63,18 @@ def register(subparsers):
|
|
|
61
63
|
help="external_user_id required with --client-visible.")
|
|
62
64
|
p.set_defaults(func=run_conversations)
|
|
63
65
|
|
|
66
|
+
# codeer history ai-drafts <id>
|
|
67
|
+
p = sub.add_parser(
|
|
68
|
+
"ai-drafts",
|
|
69
|
+
help="Export every paginated AI Draft lifecycle record returned for one History.",
|
|
70
|
+
)
|
|
71
|
+
p.add_argument("history_id", type=int)
|
|
72
|
+
p.add_argument("--full", action="store_true",
|
|
73
|
+
help="Require --out and opt into sensitive content previews on stdout.")
|
|
74
|
+
p.add_argument("--out", default=None,
|
|
75
|
+
help="Write every AI Draft record, refinement signal, outcome, tool activity, and delivery.")
|
|
76
|
+
p.set_defaults(func=run_ai_drafts)
|
|
77
|
+
|
|
64
78
|
# codeer history negative-feedback
|
|
65
79
|
p = sub.add_parser("negative-feedback", help="Surface assistant turns with negative feedback")
|
|
66
80
|
p.add_argument("--agent", required=True)
|
|
@@ -148,6 +162,15 @@ def _history_summary(row: dict, *, full: bool = False) -> dict:
|
|
|
148
162
|
"feedback_counts": _feedback_counts(row),
|
|
149
163
|
"snippet_preview": truncate(row.get("snippet") or "", 240),
|
|
150
164
|
}
|
|
165
|
+
for key in (
|
|
166
|
+
"ai_draft_count",
|
|
167
|
+
"dismissed_draft_count",
|
|
168
|
+
"regenerated_draft_count",
|
|
169
|
+
"applied_draft_count",
|
|
170
|
+
"sent_from_ai_draft_count",
|
|
171
|
+
):
|
|
172
|
+
if key in row:
|
|
173
|
+
out[key] = row.get(key)
|
|
151
174
|
if full:
|
|
152
175
|
out["share_type"] = row.get("share_type")
|
|
153
176
|
meta = row.get("meta") or {}
|
|
@@ -190,6 +213,9 @@ def _part_summary(part: dict, idx: int, *, full: bool = False) -> dict:
|
|
|
190
213
|
"attachment_count": len(part.get("attached_files") or []),
|
|
191
214
|
"feedback_count": len(part.get("feedbacks") or []),
|
|
192
215
|
}
|
|
216
|
+
if is_tool_part:
|
|
217
|
+
# Exact tool payloads belong in the artifact, never terminal previews.
|
|
218
|
+
row.pop("content_preview", None)
|
|
193
219
|
if is_tool_part and isinstance(raw_content, dict):
|
|
194
220
|
row.update({
|
|
195
221
|
"tool_name": raw_content.get("tool_name"),
|
|
@@ -224,6 +250,7 @@ def run_list(args, client) -> int:
|
|
|
224
250
|
organization_id=organization_id,
|
|
225
251
|
external_user_id=args.user,
|
|
226
252
|
feedback_filter=args.feedback,
|
|
253
|
+
has_ai_drafts=True if getattr(args, "has_ai_drafts", False) else None,
|
|
227
254
|
exclude_users=exclude,
|
|
228
255
|
limit=args.limit,
|
|
229
256
|
offset=args.offset,
|
|
@@ -301,11 +328,80 @@ def run_conversations(args, client) -> int:
|
|
|
301
328
|
"part_summaries_truncated": len(parts) > len(shown_parts),
|
|
302
329
|
"wrote_full_detail": bool(args.out),
|
|
303
330
|
"stdout_is_summary": True,
|
|
331
|
+
"read_contract": result.get("export_contract") or "client-visible-chat-v2",
|
|
332
|
+
"provider_raw_trace": result.get("provider_raw_trace", "not_included"),
|
|
333
|
+
"missing_parts_do_not_prove_non_execution": True,
|
|
334
|
+
"omitted_part_summaries": len(parts) - len(shown_parts),
|
|
304
335
|
"parts": [_part_summary(p, i, full=args.full) for i, p in enumerate(shown_parts)],
|
|
305
336
|
})
|
|
306
337
|
return 0
|
|
307
338
|
|
|
308
339
|
|
|
340
|
+
def _ai_draft_summary(draft: dict, *, full: bool = False) -> dict:
|
|
341
|
+
delivery = draft.get("delivery") or {}
|
|
342
|
+
row = {
|
|
343
|
+
"id": draft.get("id"),
|
|
344
|
+
"history_id": draft.get("history_id"),
|
|
345
|
+
"thread_key": draft.get("thread_key"),
|
|
346
|
+
"context_through_sequence": draft.get("context_through_sequence"),
|
|
347
|
+
"outcome": draft.get("outcome"),
|
|
348
|
+
"result_type": draft.get("result_type"),
|
|
349
|
+
"refinement_source_draft_id": draft.get("refinement_source_draft_id"),
|
|
350
|
+
"has_generation_instruction": bool(draft.get("generation_instruction")),
|
|
351
|
+
"dismiss_reason": draft.get("dismiss_reason"),
|
|
352
|
+
"has_dismiss_feedback": bool(draft.get("dismiss_feedback")),
|
|
353
|
+
"has_generated_content": bool(draft.get("content")),
|
|
354
|
+
"has_actual_content": bool(delivery.get("actual_content")),
|
|
355
|
+
"delivery_status": delivery.get("status"),
|
|
356
|
+
"tool_activity_count": len(draft.get("tool_activities") or []),
|
|
357
|
+
"has_proposed_actions": draft.get("proposed_actions") is not None,
|
|
358
|
+
"created_at": draft.get("created_at"),
|
|
359
|
+
}
|
|
360
|
+
if full:
|
|
361
|
+
row.update({
|
|
362
|
+
"generation_instruction_preview": truncate(draft.get("generation_instruction") or "", 600),
|
|
363
|
+
"dismiss_feedback_preview": truncate(draft.get("dismiss_feedback") or "", 600),
|
|
364
|
+
"content_preview": truncate(draft.get("content") or "", 600),
|
|
365
|
+
"actual_content_preview": truncate(delivery.get("actual_content") or "", 600),
|
|
366
|
+
})
|
|
367
|
+
return row
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def run_ai_drafts(args, client) -> int:
|
|
371
|
+
if args.full and not args.out:
|
|
372
|
+
log("error: AI Draft content previews require --out <path>")
|
|
373
|
+
return 2
|
|
374
|
+
|
|
375
|
+
result = hist_mod.list_ai_drafts(client, args.history_id)
|
|
376
|
+
drafts = result["drafts"]
|
|
377
|
+
write_json(args.out, result)
|
|
378
|
+
stdout_limit = 50 if args.full else 20
|
|
379
|
+
shown_drafts = drafts[:stdout_limit]
|
|
380
|
+
outcome_counts: dict[str, int] = {}
|
|
381
|
+
for draft in drafts:
|
|
382
|
+
outcome = str(draft.get("outcome") or "unknown")
|
|
383
|
+
outcome_counts[outcome] = outcome_counts.get(outcome, 0) + 1
|
|
384
|
+
print_json({
|
|
385
|
+
"history_id": args.history_id,
|
|
386
|
+
"draft_count": len(drafts),
|
|
387
|
+
"outcome_counts": outcome_counts,
|
|
388
|
+
"regenerated_draft_count": sum(
|
|
389
|
+
1 for draft in drafts if draft.get("refinement_source_draft_id") is not None
|
|
390
|
+
),
|
|
391
|
+
"sent_from_ai_draft_count": sum(
|
|
392
|
+
1 for draft in drafts if (draft.get("delivery") or {}).get("status") == "sent"
|
|
393
|
+
),
|
|
394
|
+
"draft_summaries_shown": len(shown_drafts),
|
|
395
|
+
"draft_summaries_truncated": len(drafts) > len(shown_drafts),
|
|
396
|
+
"pages_fetched": result["pages_fetched"],
|
|
397
|
+
"snapshot_consistency": result.get("snapshot_consistency", "best-effort"),
|
|
398
|
+
"wrote_full_detail": bool(args.out),
|
|
399
|
+
"stdout_is_summary": True,
|
|
400
|
+
"drafts": [_ai_draft_summary(draft, full=args.full) for draft in shown_drafts],
|
|
401
|
+
})
|
|
402
|
+
return 0
|
|
403
|
+
|
|
404
|
+
|
|
309
405
|
def run_negative_feedback(args, client) -> int:
|
|
310
406
|
exclude = _parse_exclude(args.exclude_users)
|
|
311
407
|
workspace_id, organization_id = client.resolve_scope()
|
codeer_cli/histories.py
CHANGED
|
@@ -4,10 +4,12 @@ Use this after an agent has been published and running for a while, to pull
|
|
|
4
4
|
recent traffic, filter by feedback, and feed the failing cases back into the
|
|
5
5
|
evaluation loop.
|
|
6
6
|
|
|
7
|
-
Pagination: ``/histories
|
|
8
|
-
``offset`` (NOT ``page`` / ``page_size``). History
|
|
9
|
-
caller-selected page.
|
|
10
|
-
|
|
7
|
+
Pagination: ``/histories``, AI Draft lifecycle export, and the management parts
|
|
8
|
+
export use ``limit`` + ``offset`` (NOT ``page`` / ``page_size``). History
|
|
9
|
+
metadata remains a bounded caller-selected page. Draft and parts exports follow
|
|
10
|
+
every server page. The parts export additionally rejects a cross-page revision
|
|
11
|
+
change instead of returning a mixed snapshot. The AI Draft endpoint does not
|
|
12
|
+
provide a revision token, so its multi-page export is explicitly best-effort.
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
15
|
from __future__ import annotations
|
|
@@ -15,7 +17,11 @@ from __future__ import annotations
|
|
|
15
17
|
import builtins
|
|
16
18
|
from typing import Any, Iterable, Optional
|
|
17
19
|
|
|
18
|
-
from .client import CodeerClient
|
|
20
|
+
from .client import CodeerClient, CodeerError
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HistoryExportError(ValueError):
|
|
24
|
+
"""The server cannot provide a complete, consistent History parts export."""
|
|
19
25
|
|
|
20
26
|
|
|
21
27
|
def list(
|
|
@@ -26,6 +32,7 @@ def list(
|
|
|
26
32
|
organization_id: Optional[str] = None,
|
|
27
33
|
external_user_id: Optional[str] = None,
|
|
28
34
|
feedback_filter: Optional[str] = None,
|
|
35
|
+
has_ai_drafts: Optional[bool] = None,
|
|
29
36
|
exclude_users: Iterable[str] = (),
|
|
30
37
|
limit: int = 500,
|
|
31
38
|
offset: int = 0,
|
|
@@ -48,6 +55,8 @@ def list(
|
|
|
48
55
|
params["external_user_id"] = external_user_id
|
|
49
56
|
if feedback_filter:
|
|
50
57
|
params["feedback_filter"] = feedback_filter
|
|
58
|
+
if has_ai_drafts is not None:
|
|
59
|
+
params["has_ai_drafts"] = has_ai_drafts
|
|
51
60
|
rows = client.get("/external/histories", params=params)
|
|
52
61
|
drop = {e.lower() for e in exclude_users}
|
|
53
62
|
if drop:
|
|
@@ -55,6 +64,85 @@ def list(
|
|
|
55
64
|
return rows
|
|
56
65
|
|
|
57
66
|
|
|
67
|
+
def list_ai_drafts(client: CodeerClient, history_id: int, *, limit: int = 500) -> dict[str, Any]:
|
|
68
|
+
"""Export every AI Draft lifecycle record returned while paging one History.
|
|
69
|
+
|
|
70
|
+
The endpoint returns pagination beside ``data`` in the standard response
|
|
71
|
+
envelope, so this read explicitly preserves the envelope and follows the
|
|
72
|
+
server-reported page size. Draft rows are returned unmodified. Count changes
|
|
73
|
+
and duplicate IDs are rejected, but without a server revision token this is
|
|
74
|
+
not a transactionally consistent point-in-time snapshot.
|
|
75
|
+
"""
|
|
76
|
+
if limit <= 0:
|
|
77
|
+
raise ValueError("limit must be greater than zero")
|
|
78
|
+
|
|
79
|
+
offset = 0
|
|
80
|
+
pages_fetched = 0
|
|
81
|
+
total_records: int | None = None
|
|
82
|
+
drafts: list[dict] = []
|
|
83
|
+
seen_ids: set[Any] = set()
|
|
84
|
+
|
|
85
|
+
while True:
|
|
86
|
+
envelope = client.get(
|
|
87
|
+
f"/external/histories/{history_id}/ai-drafts",
|
|
88
|
+
params={"limit": limit, "offset": offset},
|
|
89
|
+
unwrap=False,
|
|
90
|
+
)
|
|
91
|
+
if not isinstance(envelope, dict):
|
|
92
|
+
raise ValueError("AI Draft response must be an envelope object")
|
|
93
|
+
page_drafts = envelope.get("data")
|
|
94
|
+
page_info = envelope.get("pagination")
|
|
95
|
+
if not isinstance(page_drafts, builtins.list):
|
|
96
|
+
raise ValueError("AI Draft response must contain a data list")
|
|
97
|
+
if not isinstance(page_info, dict):
|
|
98
|
+
raise ValueError("AI Draft response must contain pagination metadata")
|
|
99
|
+
|
|
100
|
+
page_offset = page_info.get("offset")
|
|
101
|
+
page_total = page_info.get("total_records")
|
|
102
|
+
page_limit = page_info.get("limit")
|
|
103
|
+
if not all(isinstance(value, int) for value in (page_offset, page_total, page_limit)):
|
|
104
|
+
raise ValueError("AI Draft pagination must contain integer limit/offset/total_records")
|
|
105
|
+
if page_offset != offset:
|
|
106
|
+
raise ValueError(f"AI Draft page offset mismatch: requested {offset}, received {page_offset}")
|
|
107
|
+
if page_total < 0 or page_limit <= 0:
|
|
108
|
+
raise ValueError("AI Draft pagination metadata is invalid")
|
|
109
|
+
|
|
110
|
+
if total_records is None:
|
|
111
|
+
total_records = page_total
|
|
112
|
+
elif page_total != total_records:
|
|
113
|
+
raise ValueError("AI Draft total_records changed while paging; retry the export")
|
|
114
|
+
if len(page_drafts) > page_limit or offset + len(page_drafts) > total_records:
|
|
115
|
+
raise ValueError("AI Draft page contains more rows than its pagination metadata allows")
|
|
116
|
+
|
|
117
|
+
for draft in page_drafts:
|
|
118
|
+
if not isinstance(draft, dict):
|
|
119
|
+
raise ValueError("AI Draft data rows must be objects")
|
|
120
|
+
draft_id = draft.get("id")
|
|
121
|
+
if not isinstance(draft_id, int):
|
|
122
|
+
raise ValueError("AI Draft data rows must contain an integer id")
|
|
123
|
+
if draft_id in seen_ids:
|
|
124
|
+
raise ValueError("AI Draft pagination returned a duplicate draft id")
|
|
125
|
+
seen_ids.add(draft_id)
|
|
126
|
+
drafts.append(draft)
|
|
127
|
+
|
|
128
|
+
pages_fetched += 1
|
|
129
|
+
offset += len(page_drafts)
|
|
130
|
+
if offset >= total_records:
|
|
131
|
+
break
|
|
132
|
+
if not page_drafts:
|
|
133
|
+
raise ValueError("AI Draft pagination stopped before total_records was reached")
|
|
134
|
+
|
|
135
|
+
assert total_records is not None
|
|
136
|
+
return {
|
|
137
|
+
"history_id": history_id,
|
|
138
|
+
"source_endpoint": f"/api/v1/external/histories/{history_id}/ai-drafts",
|
|
139
|
+
"snapshot_consistency": "best-effort",
|
|
140
|
+
"total_records": total_records,
|
|
141
|
+
"pages_fetched": pages_fetched,
|
|
142
|
+
"drafts": drafts,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
58
146
|
def list_negative_feedback_turns(
|
|
59
147
|
client: CodeerClient,
|
|
60
148
|
*,
|
|
@@ -162,14 +250,14 @@ def list_messages(
|
|
|
162
250
|
) -> dict:
|
|
163
251
|
"""Return every persisted diagnostic part through the management export.
|
|
164
252
|
|
|
165
|
-
This route
|
|
253
|
+
This route requires a workspace admin API key to analyze a History. It is distinct
|
|
166
254
|
from the external client-owner Chat V2 route and must return the explicit
|
|
167
255
|
``history-parts-v1`` contract. Tool calls and returns are preserved in the
|
|
168
256
|
returned artifact. System prompts and provider raw traces are outside the
|
|
169
257
|
contract.
|
|
170
258
|
"""
|
|
171
259
|
if limit <= 0:
|
|
172
|
-
raise
|
|
260
|
+
raise HistoryExportError("limit must be greater than zero")
|
|
173
261
|
|
|
174
262
|
offset = 0
|
|
175
263
|
pages_fetched = 0
|
|
@@ -179,43 +267,54 @@ def list_messages(
|
|
|
179
267
|
messages: list[dict] = []
|
|
180
268
|
|
|
181
269
|
while True:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
270
|
+
try:
|
|
271
|
+
page = client.get(
|
|
272
|
+
f"/external/histories/{history_id}/messages",
|
|
273
|
+
params={"limit": limit, "offset": offset},
|
|
274
|
+
)
|
|
275
|
+
except CodeerError as exc:
|
|
276
|
+
if exc.status == 404:
|
|
277
|
+
raise CodeerError(404,
|
|
278
|
+
"History or its parts export endpoint is unavailable. "
|
|
279
|
+
"This command requires a server supporting history-parts-v1; no fallback was attempted.",
|
|
280
|
+
exc.body) from exc
|
|
281
|
+
raise
|
|
186
282
|
if not isinstance(page, dict):
|
|
187
|
-
raise
|
|
283
|
+
raise HistoryExportError("History parts response must be an object")
|
|
188
284
|
if page.get("export_contract") != "history-parts-v1":
|
|
189
|
-
raise
|
|
285
|
+
raise HistoryExportError("History parts response is missing export_contract=history-parts-v1")
|
|
190
286
|
|
|
191
287
|
page_messages = page.get("messages")
|
|
192
288
|
page_info = page.get("page")
|
|
193
289
|
revision = page.get("part_revision")
|
|
194
290
|
if not isinstance(page_messages, builtins.list):
|
|
195
|
-
raise
|
|
291
|
+
raise HistoryExportError("History parts response must contain a messages list")
|
|
196
292
|
if not isinstance(page_info, dict):
|
|
197
|
-
raise
|
|
293
|
+
raise HistoryExportError("History parts response must contain page metadata")
|
|
198
294
|
if not isinstance(revision, str) or not revision:
|
|
199
|
-
raise
|
|
295
|
+
raise HistoryExportError("History parts response must contain part_revision")
|
|
200
296
|
|
|
201
297
|
page_offset = page_info.get("offset")
|
|
202
298
|
page_total = page_info.get("total_records")
|
|
203
299
|
page_limit = page_info.get("limit")
|
|
204
300
|
if not all(isinstance(value, int) for value in (page_offset, page_total, page_limit)):
|
|
205
|
-
raise
|
|
301
|
+
raise HistoryExportError("History parts page metadata must contain integer limit/offset/total_records")
|
|
206
302
|
if page_offset != offset:
|
|
207
|
-
raise
|
|
303
|
+
raise HistoryExportError(f"History parts page offset mismatch: requested {offset}, received {page_offset}")
|
|
208
304
|
if page_total < 0 or page_limit <= 0:
|
|
209
|
-
raise
|
|
305
|
+
raise HistoryExportError("History parts page metadata is invalid")
|
|
210
306
|
|
|
211
307
|
if total_records is None:
|
|
212
308
|
total_records = page_total
|
|
213
309
|
elif page_total != total_records:
|
|
214
|
-
raise
|
|
310
|
+
raise HistoryExportError("History parts total_records changed while paging; retry the export")
|
|
215
311
|
if part_revision is None:
|
|
216
312
|
part_revision = revision
|
|
217
313
|
elif revision != part_revision:
|
|
218
|
-
raise
|
|
314
|
+
raise HistoryExportError("History parts changed while paging; retry the export")
|
|
315
|
+
|
|
316
|
+
if len(page_messages) > page_limit or offset + len(page_messages) > total_records:
|
|
317
|
+
raise HistoryExportError("History parts page contains more rows than its pagination metadata allows")
|
|
219
318
|
|
|
220
319
|
if result is None:
|
|
221
320
|
result = dict(page)
|
|
@@ -223,10 +322,10 @@ def list_messages(
|
|
|
223
322
|
pages_fetched += 1
|
|
224
323
|
offset += len(page_messages)
|
|
225
324
|
|
|
226
|
-
if offset
|
|
325
|
+
if offset == total_records:
|
|
227
326
|
break
|
|
228
|
-
if
|
|
229
|
-
raise
|
|
327
|
+
if len(page_messages) != page_limit:
|
|
328
|
+
raise HistoryExportError("Incomplete History messages page")
|
|
230
329
|
|
|
231
330
|
assert result is not None
|
|
232
331
|
assert total_records is not None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: codeer-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.16
|
|
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
|
|
@@ -175,6 +175,104 @@ becomes available in live published-agent conversations with a non-empty
|
|
|
175
175
|
`external_user_id`; editor Live Test conversations are internal and cannot
|
|
176
176
|
activate human mode.
|
|
177
177
|
|
|
178
|
+
## HTTP input contracts
|
|
179
|
+
|
|
180
|
+
`codeer agent apply --payload` and SDK `agents.create` / `agents.update` accept
|
|
181
|
+
`unified_tools[].http_request.body.input_contracts`. No separate HTTP command is
|
|
182
|
+
needed. The target backend must have the HTTP input-contract feature deployed
|
|
183
|
+
(codeer-copilot #1495); installing this CLI alone does not enable runtime support.
|
|
184
|
+
A local dry-run cannot establish server deployment or API business-rule success.
|
|
185
|
+
|
|
186
|
+
Example payload:
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"name": "Order helper",
|
|
191
|
+
"system_prompt": "Use the configured API for approved order changes.",
|
|
192
|
+
"use_search": false,
|
|
193
|
+
"unified_tools": [{
|
|
194
|
+
"id": "submit",
|
|
195
|
+
"type": "http_request",
|
|
196
|
+
"http_request": {
|
|
197
|
+
"method": "POST",
|
|
198
|
+
"url_template": "https://example.com/orders",
|
|
199
|
+
"body": {
|
|
200
|
+
"template": {
|
|
201
|
+
"quantity": "{{agent[Requested quantity]}}",
|
|
202
|
+
"payload": "{{agent[Order details]}}",
|
|
203
|
+
"changes": "{{agent[Changes as JSON text]}}"
|
|
204
|
+
},
|
|
205
|
+
"input_contracts": {
|
|
206
|
+
"quantity": {"type": "integer"},
|
|
207
|
+
"payload": {"type": "object"},
|
|
208
|
+
"changes": {"type": "string", "format": "json", "json_type": "array"}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}]
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
- `type`: `string` (default), `number`, `integer`, `boolean`, `object`, `array`.
|
|
217
|
+
- `format`: `text` (default) or `json`; `json` requires `type: string`.
|
|
218
|
+
- `json_type`: `any` (default), `object`, `array`; outside JSON format, only
|
|
219
|
+
`any` is valid. API names are snake_case; the CLI rejects `inputContracts`,
|
|
220
|
+
`jsonType`, and unknown fields inside individual contracts.
|
|
221
|
+
|
|
222
|
+
`type: object` / `array` sends a native JSON value. `type: string, format: json`
|
|
223
|
+
sends a string containing JSON. Existing valid JSON text is sent unchanged;
|
|
224
|
+
empty strings also pass unchanged, while non-empty text must parse and match
|
|
225
|
+
`json_type`. Plain strings retain existing behavior, including malformed JSON.
|
|
226
|
+
The backend converts supported representations before checking runtime values;
|
|
227
|
+
the CLI only validates configuration and never executes the configured HTTP
|
|
228
|
+
request. Contracts do not configure nested JSON Schema constraints or defaults.
|
|
229
|
+
|
|
230
|
+
Keys come from template paths, not instructions: `order.count` → `order_count`,
|
|
231
|
+
`items[0].id` → `items_0_id`, root string → `body`. Non-ASCII-alphanumeric runs
|
|
232
|
+
become `_`, edge underscores are removed, and keys are lowercased. Multiple
|
|
233
|
+
placeholders in one string add `_1`, `_2`; traversal collisions add `_2`, `_3`.
|
|
234
|
+
Object insertion order matters: preserve it when editing/exporting. Typed and
|
|
235
|
+
JSON-text placeholders must occupy the entire template value. Stale contract
|
|
236
|
+
keys fail validation; omitted entries remain ordinary strings.
|
|
237
|
+
|
|
238
|
+
For an existing Agent:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
codeer agent get <agent-id> --out .codeer/current/agent.json
|
|
242
|
+
# Prepare local_draft_agent.json from current writable settings; review its diff.
|
|
243
|
+
codeer agent apply --agent-id <agent-id> --payload .codeer/current/local_draft_agent.json --dry-run
|
|
244
|
+
# After approval:
|
|
245
|
+
codeer agent apply --agent-id <agent-id> --payload .codeer/current/local_draft_agent.json
|
|
246
|
+
codeer agent get <agent-id> --out .codeer/current/agent.json
|
|
247
|
+
codeer agent get <agent-id> --history <history-id-from-apply> --out .codeer/current/agent-version.json
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The external update uses PATCH, but it is **not a nested partial update**.
|
|
251
|
+
Preserve `name`, `system_prompt`, `use_search`, the complete `unified_tools` list
|
|
252
|
+
(including other tools, templates, auth and `draft_policy`), and the full desired
|
|
253
|
+
contract map. Also preserve description, attachments, suggested questions,
|
|
254
|
+
model settings, handoff and other writable settings. Sending one changed tool
|
|
255
|
+
replaces the list; omitting a contract entry resets that input to ordinary string.
|
|
256
|
+
GET responses and writable payloads have different shapes; reconstruct attachment
|
|
257
|
+
IDs and other absent writable fields from current version evidence as needed.
|
|
258
|
+
Do not apply an update if a current setting cannot be preserved by the CLI. See
|
|
259
|
+
[the skill workflow](../codeer-agent/reference/http-input-contracts.md) for details.
|
|
260
|
+
|
|
261
|
+
Dry-run's `http_inputs` lists tool indexes and each generated key's effective
|
|
262
|
+
`type` / `format` / `json_type`, with `configured: false` for defaults. It excludes
|
|
263
|
+
HTTP URLs, auth, headers, query values, instructions and template content.
|
|
264
|
+
`body_inputs_used` is false for GET/HEAD, whose body inputs are unused at runtime.
|
|
265
|
+
`--full` and `--out` deliberately retain complete nested content, including
|
|
266
|
+
credentials; metadata cleanup is limited to resource-level account fields and
|
|
267
|
+
workspace identity. These exports are not redacted artifacts.
|
|
268
|
+
|
|
269
|
+
Compare the fresh GET and exact version snapshot with the intended tools and
|
|
270
|
+
contracts; the server may materialize omitted defaults. `agent versions --out`
|
|
271
|
+
exports version metadata, not snapshots; use `agent get --history` for a snapshot.
|
|
272
|
+
Apply saves a draft. Publish the verified version separately, after approval,
|
|
273
|
+
using `codeer agent publish --agent <agent-id> --history <history-id>` (preview
|
|
274
|
+
with `--dry-run` first).
|
|
275
|
+
|
|
178
276
|
## Upgrade and uninstall
|
|
179
277
|
|
|
180
278
|
Upgrade the CLI:
|
|
@@ -200,8 +298,9 @@ Use this pattern during agent lifecycle work:
|
|
|
200
298
|
|
|
201
299
|
```bash
|
|
202
300
|
codeer agent list
|
|
203
|
-
codeer history list --agent <agent-id> --limit 50
|
|
301
|
+
codeer history list --agent <agent-id> --has-ai-drafts --limit 50
|
|
204
302
|
codeer history conversations <history-id> --out .codeer/current/history-<history-id>.json
|
|
303
|
+
codeer history ai-drafts <history-id> --out .codeer/current/ai-drafts-<history-id>.json
|
|
205
304
|
codeer history create --agent <agent-id> --message "Review this plan" --timeout 240
|
|
206
305
|
codeer history send <history-id> --message "Use the recommended options" --timeout 240
|
|
207
306
|
codeer eval run --agent <agent-id> --cases <case-ids> --evaluator <evaluator-id> --out .codeer/eval_run.json
|
|
@@ -220,19 +319,55 @@ workspace.
|
|
|
220
319
|
|
|
221
320
|
Flags:
|
|
222
321
|
|
|
223
|
-
- `--full` prints bounded extra detail for human inspection.
|
|
224
|
-
|
|
322
|
+
- `--full` prints bounded extra detail for human inspection. Some commands,
|
|
323
|
+
including `agent get`, can expose configuration credentials; `history
|
|
324
|
+
ai-drafts` can expose sensitive conversation text and therefore requires
|
|
325
|
+
`--out`. Use each command's flag description as the output contract, and
|
|
326
|
+
inspect complete artifacts locally without flooding LLM context.
|
|
225
327
|
- `--out <path>` writes complete diagnostic artifacts to a local file. Use it
|
|
226
328
|
for raw eval results, full conversation turns, full rubric matrices, and
|
|
227
329
|
other data that can grow with cases, versions, or turns.
|
|
228
330
|
|
|
229
|
-
`history conversations`
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
331
|
+
`history conversations` reads `/api/v1/external/histories/{id}/messages`
|
|
332
|
+
using a workspace admin API key and follows all pages automatically. Member
|
|
333
|
+
keys retain existing History visibility but are intentionally rejected by this
|
|
334
|
+
complete tool-payload export. This requires a server supporting
|
|
335
|
+
`history-parts-v1`; it never falls back to a
|
|
336
|
+
different authorization contract. Stdout shows at most 20 part summaries (50
|
|
337
|
+
with `--full`) and omits tool payload previews. `--out` retains native tool
|
|
338
|
+
args/results/outcomes, group/part IDs, attachments, feedback, and metadata.
|
|
339
|
+
Attachment URLs remain permission-checked History download endpoints rather
|
|
340
|
+
than direct storage/source URLs.
|
|
341
|
+
Legacy projections have `source: legacy-adapter`; tool outcomes absent from
|
|
342
|
+
the original records are omitted and marked `outcome_not_recorded`. System
|
|
343
|
+
prompts and provider raw traces are not included. Missing parts do not prove a tool never ran. Keep export files private.
|
|
344
|
+
|
|
345
|
+
`--client-visible --user <external-user-id>` explicitly selects the existing
|
|
346
|
+
Chat V2 owner/allowlist contract. No external identity is inferred from History
|
|
347
|
+
metadata. `history get` and the low-level legacy `get_conversations` reader
|
|
348
|
+
remain compatible. Management exports do not hydrate display-only tool payloads.
|
|
349
|
+
|
|
350
|
+
Release order: deploy the backend supporting `history-parts-v1` first, verify
|
|
351
|
+
an authorized management export across multiple pages, then release/install
|
|
352
|
+
this CLI. Existing CLI versions retain their previous behavior until upgraded.
|
|
353
|
+
If the backend endpoint is unavailable, the new CLI fails explicitly with no
|
|
354
|
+
fallback; keep the previous CLI installed until backend verification passes.
|
|
355
|
+
The management endpoint can remain available if the CLI release is rolled back.
|
|
356
|
+
|
|
357
|
+
`history list --has-ai-drafts` narrows the history page to conversations with
|
|
358
|
+
at least one AI Draft and includes lifecycle counts in compact output.
|
|
359
|
+
`history ai-drafts` follows every server page and writes every returned draft
|
|
360
|
+
lifecycle record to `--out`: generated content, refinement lineage,
|
|
361
|
+
`generation_instruction`, `dismiss_reason`, `dismiss_feedback`, outcomes, tool
|
|
362
|
+
activities, proposed actions, operator attribution, and the correlated actual
|
|
363
|
+
delivery when one exists. Default stdout shows structural flags and counts but
|
|
364
|
+
no generated, operator, customer, or tool text. `--full --out <path>` explicitly
|
|
365
|
+
opts into bounded content previews. The endpoint has no revision token, so a
|
|
366
|
+
multi-page artifact is marked `snapshot_consistency: best-effort`: count changes
|
|
367
|
+
and duplicate IDs fail the export, but lifecycle fields can still change during
|
|
368
|
+
paging. Re-run when point-in-time consistency matters. These fields are evidence
|
|
369
|
+
for an improvement analysis; the CLI does not invent a recommended Agent change
|
|
370
|
+
from them.
|
|
236
371
|
|
|
237
372
|
Use the external client-owner contract only when that distinction is the point
|
|
238
373
|
of the test:
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
codeer_cli/__init__.py,sha256=z3rkUdVCt11c8QE3-YFkG5cFObM6QhPe2yKugA8k3aA,1915
|
|
2
|
-
codeer_cli/
|
|
3
|
-
codeer_cli/
|
|
2
|
+
codeer_cli/_http_contracts.py,sha256=W1YSs9DhWCImDEloin47361qxFNBePuzZKnWRUb16CY,4612
|
|
3
|
+
codeer_cli/_validate.py,sha256=7GEIwLzjP5fPthfsCNtsnqBYiFWqIbuOe_SltrHCE7Q,7302
|
|
4
|
+
codeer_cli/agents.py,sha256=kCvCF7LHQrDOVR80x3i9sWTeC08qvCvR-aUDIMMx5xc,6399
|
|
4
5
|
codeer_cli/chats.py,sha256=Vg1DIu8fT6mPyPpfqLpExKvXhdZEWCyJGPvalQy3BXg,7989
|
|
5
|
-
codeer_cli/cli.py,sha256=
|
|
6
|
-
codeer_cli/client.py,sha256=
|
|
6
|
+
codeer_cli/cli.py,sha256=Y5f_HdCAd-gMKlgh49OO-T396f9o_BhpTOFI971NItk,4907
|
|
7
|
+
codeer_cli/client.py,sha256=7fFDAMW5jliMx6PLFovfHDLOLj8z4Uyj5Nm_GdDAr8A,13266
|
|
7
8
|
codeer_cli/constants.py,sha256=D1pV3wCoqYybrKGKeoupYjjFWLfaFviKp1yL7oh6Qso,2323
|
|
8
9
|
codeer_cli/eval_.py,sha256=TjDFGCGF19JaUmSaP3PiKIRrBIvYaU-EJS4dC-N7eBI,17868
|
|
9
|
-
codeer_cli/histories.py,sha256=
|
|
10
|
+
codeer_cli/histories.py,sha256=UvFGOMrWeQTh0CjGxBB7rNlRarJrp8kHJVwi9sT7FYI,14592
|
|
10
11
|
codeer_cli/kb.py,sha256=-HodcGwTvIKgvuRVc5sPFu51si-GlzLEsPSHmnfrVQU,10615
|
|
11
12
|
codeer_cli/models.py,sha256=Ama9fx8hyRMdysiUNA3Ty0sYIYwzFDkOcAZg-id3-vQ,362
|
|
12
13
|
codeer_cli/parse.py,sha256=-txMtIxPfeg_i4McEskVKxo8p4438gKS-sFTI38RoBs,21772
|
|
13
14
|
codeer_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
codeer_cli/commands/_util.py,sha256=
|
|
15
|
-
codeer_cli/commands/agent.py,sha256=
|
|
15
|
+
codeer_cli/commands/_util.py,sha256=AM-ks4Q_HwMlACBEj8pNKqo7yWoOWuQZeHVehy9MwJQ,1915
|
|
16
|
+
codeer_cli/commands/agent.py,sha256=vNPWMboU1pr6pD-8agfP9TjY_UThoJwGKIvshueanig,17471
|
|
16
17
|
codeer_cli/commands/check.py,sha256=lTxolx1mIJ8jldPhJ5FXqie9nbCLVOO-sDPOHTSy1-w,3817
|
|
17
18
|
codeer_cli/commands/eval_cmd.py,sha256=S-ydzWZppPLMCu2i_HOK0OW5ZaoebxcLO--WDYOIuAk,73548
|
|
18
|
-
codeer_cli/commands/history.py,sha256=
|
|
19
|
+
codeer_cli/commands/history.py,sha256=3VqyllZP2PvYvWbgw_GG2kK_VOFtkLndp_RIXuUBm3A,23166
|
|
19
20
|
codeer_cli/commands/kb.py,sha256=1lwrKTC0NpnAvXZ0u5DPsfx7ZXcHLURyBrqRViv03Yk,42349
|
|
20
21
|
codeer_cli/commands/model.py,sha256=91LDo_YeHi3POZV7KTCkoCcgtK5p87LwrHX1lZgfCps,1698
|
|
21
22
|
codeer_cli/commands/profile.py,sha256=IdlXC_6cqobsfN3JRrAnt-1OgBUsIFneS9QtR4Un6Kc,6521
|
|
22
|
-
codeer_cli-0.1.
|
|
23
|
-
codeer_cli-0.1.
|
|
24
|
-
codeer_cli-0.1.
|
|
25
|
-
codeer_cli-0.1.
|
|
23
|
+
codeer_cli-0.1.16.dist-info/METADATA,sha256=jYT-occxU9SDNf_CKXOzwJcaPXzC81_eDFVk4ujSFeU,18263
|
|
24
|
+
codeer_cli-0.1.16.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
25
|
+
codeer_cli-0.1.16.dist-info/entry_points.txt,sha256=-nXIrlm5SR5r7gg3y8AS0tN66MwmvNHsrlwLNQNGD50,47
|
|
26
|
+
codeer_cli-0.1.16.dist-info/RECORD,,
|
|
File without changes
|