@delta-infra/cli 1.0.79 → 1.0.81

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 (61) hide show
  1. package/README.md +427 -437
  2. package/checksums.txt +5 -5
  3. package/package.json +2 -13
  4. package/scripts/install-wizard.js +625 -616
  5. package/scripts/install.js +417 -417
  6. package/scripts/run.js +65 -65
  7. package/skills/delta-sandbox/SKILL.md +356 -356
  8. package/skills/delta-sandbox/agents/openai.yaml +4 -4
  9. package/skills/delta-sandbox/references/commands.md +56 -56
  10. package/skills/delta-sandbox/references/lifecycle.md +92 -92
  11. package/skills/delta-sandbox/references/recipes.md +133 -133
  12. package/skills/delta-science/SKILL.md +149 -170
  13. package/skills/delta-science/agents/openai.yaml +4 -4
  14. package/skills/delta-science/references/antbo.md +100 -0
  15. package/skills/delta-science/references/cli-contract.md +74 -75
  16. package/skills/delta-science/references/delta-bo.md +90 -0
  17. package/skills/{gsasii/references/endpoints.md → delta-science/references/gsasii.md} +116 -130
  18. package/skills/{lammps/references/endpoints.md → delta-science/references/lammps.md} +66 -61
  19. package/skills/{ldm-bo/references/endpoints.md → delta-science/references/ldm-bo.md} +127 -124
  20. package/skills/delta-science/references/ldm.md +72 -0
  21. package/skills/{pubchem/references/endpoints.md → delta-science/references/pubchem.md} +127 -122
  22. package/skills/{pymatgen/references/endpoints.md → delta-science/references/pymatgen.md} +68 -68
  23. package/skills/{rdkit/references/endpoints.md → delta-science/references/rdkit.md} +139 -133
  24. package/skills/delta-science/references/routing-index.md +36 -0
  25. package/skills/delta-science/references/strbo.md +87 -0
  26. package/skills/{synbo-service/references/endpoints.md → delta-science/references/synbo.md} +197 -157
  27. package/skills/delta-science/references/workflows.md +58 -41
  28. package/skills/delta-science/scripts/catalog.py +92 -0
  29. package/skills/delta-science/scripts/invoke.py +310 -227
  30. package/skills/delta-shared/SKILL.md +95 -95
  31. package/skills/delta-shared/agents/openai.yaml +4 -4
  32. package/skills/manifest.json +3 -1
  33. package/skills/antbo-ldm-guard/SKILL.md +0 -58
  34. package/skills/antbo-ldm-guard/agents/openai.yaml +0 -4
  35. package/skills/antbo-service/SKILL.md +0 -79
  36. package/skills/antbo-service/agents/openai.yaml +0 -4
  37. package/skills/antbo-service/references/operations.md +0 -45
  38. package/skills/delta-bo/SKILL.md +0 -36
  39. package/skills/delta-bo/agents/openai.yaml +0 -4
  40. package/skills/delta-bo/references/endpoints.md +0 -263
  41. package/skills/delta-science/references/generated-catalog.md +0 -48
  42. package/skills/delta-science/references/operation-contracts.md +0 -137
  43. package/skills/delta-science/references/services.md +0 -112
  44. package/skills/delta-science/scripts/generated_catalog.py +0 -69
  45. package/skills/gsasii/SKILL.md +0 -42
  46. package/skills/gsasii/agents/openai.yaml +0 -4
  47. package/skills/lammps/SKILL.md +0 -39
  48. package/skills/lammps/agents/openai.yaml +0 -4
  49. package/skills/ldm-bo/SKILL.md +0 -35
  50. package/skills/ldm-bo/agents/openai.yaml +0 -4
  51. package/skills/pubchem/SKILL.md +0 -93
  52. package/skills/pubchem/agents/openai.yaml +0 -4
  53. package/skills/pubchem/references/operations.md +0 -41
  54. package/skills/pymatgen/SKILL.md +0 -54
  55. package/skills/pymatgen/agents/openai.yaml +0 -4
  56. package/skills/rdkit/SKILL.md +0 -112
  57. package/skills/rdkit/agents/openai.yaml +0 -4
  58. package/skills/rdkit/references/operations.md +0 -64
  59. package/skills/science-tools.json +0 -127
  60. package/skills/synbo-service/SKILL.md +0 -36
  61. package/skills/synbo-service/agents/openai.yaml +0 -4
@@ -1,263 +1,346 @@
1
- #!/usr/bin/env python3
2
- """Invoke one Delta Science operation strictly through delta-cli."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import json
8
- import os
9
- from pathlib import Path
10
- import shutil
11
- import subprocess
1
+ #!/usr/bin/env python3
2
+ """Invoke one Delta Science operation strictly through delta-cli."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ import shutil
11
+ import subprocess
12
12
  import sys
13
13
  import time
14
14
  from typing import Any
15
- from urllib.parse import urlsplit
16
-
17
- from generated_catalog import LEGACY_ENDPOINTS, LEGACY_TOOL_ALIASES
18
-
19
-
15
+
20
16
  SKILL_DIR = Path(__file__).resolve().parents[1]
21
-
22
-
23
- def _json_object(raw: str | None, flag: str) -> dict[str, Any] | None:
24
- if raw is None:
25
- return None
26
- try:
27
- value = json.loads(raw)
28
- except json.JSONDecodeError as exc:
29
- raise ValueError(f"{flag} is not valid JSON: {exc}") from exc
30
- if not isinstance(value, dict):
31
- raise ValueError(f"{flag} must decode to a JSON object")
32
- return value
33
-
34
-
35
- def _runtime_config() -> dict[str, Any]:
36
- path = SKILL_DIR / "runtime.local.json"
37
- if not path.is_file():
38
- return {}
39
- try:
40
- value = json.loads(path.read_text(encoding="utf-8"))
41
- except (OSError, json.JSONDecodeError):
42
- return {}
43
- return value if isinstance(value, dict) else {}
44
-
45
-
46
- def _resolve_cli(explicit: str | None, runtime: dict[str, Any]) -> str:
47
- candidates: list[str | None] = [
48
- explicit,
49
- os.environ.get("DELTA_CLI_PATH"),
50
- runtime.get("cli_path"),
51
- shutil.which("delta-cli"),
52
- shutil.which("delta-cli.exe"),
53
- ]
54
- package_root = SKILL_DIR.parents[1]
55
- exe_name = "delta-cli.exe" if os.name == "nt" else "delta-cli"
56
- candidates.append(str(package_root / "bin" / exe_name))
57
- for candidate in candidates:
58
- if candidate and Path(candidate).is_file():
59
- return str(Path(candidate).resolve())
60
- raise FileNotFoundError(
61
- "delta-cli not found; install @delta-infra/cli, set DELTA_CLI_PATH, "
62
- "or add cli_path to delta-science/runtime.local.json"
63
- )
64
-
65
-
66
- def _cli_science_base_url(cli: str, env: dict[str, str], timeout: float) -> str:
67
- explicit = env.get("DELTA_INFRA_SCIENCE_BASE_URL", "").strip()
68
- if explicit:
69
- return explicit
70
- completed = subprocess.run(
71
- [cli, "config", "show"],
72
- capture_output=True,
73
- text=True,
74
- encoding="utf-8",
75
- errors="replace",
76
- env=env,
77
- timeout=min(timeout, 15.0),
78
- check=False,
79
- )
80
- if completed.returncode != 0:
81
- return ""
82
- try:
83
- root = json.loads(completed.stdout)
84
- value = root.get("data", {}).get("science_base_url", "")
85
- except (AttributeError, json.JSONDecodeError):
86
- return ""
87
- return str(value).strip()
88
-
89
-
90
- def _catalog_profile(base_url: str, requested: str) -> str:
91
- if requested != "auto":
92
- return requested
93
- try:
94
- path = urlsplit(base_url).path.rstrip("/").lower()
95
- except ValueError:
96
- path = ""
97
- return "legacy" if path.endswith("/science_tool") else "canonical"
98
-
99
-
100
- def _resolve_catalog_names(tool: str, endpoint: str, profile: str) -> tuple[str, str]:
101
- if profile == "canonical":
102
- return tool, endpoint
103
- canonical_tool = tool.removesuffix("-service") if tool in {"antbo-service", "synbo-service"} else tool
104
- resolved_tool = LEGACY_TOOL_ALIASES.get(canonical_tool, canonical_tool)
105
- if endpoint.startswith(("chem_", "biology_")):
106
- return resolved_tool, endpoint
107
- resolved_endpoint = LEGACY_ENDPOINTS.get(canonical_tool, {}).get(endpoint)
108
- if not resolved_endpoint:
109
- raise ValueError(
110
- f"no legacy science_tool mapping for {canonical_tool}/{endpoint}"
111
- )
112
- return resolved_tool, resolved_endpoint
113
-
114
-
17
+ _CLI_ERROR_TYPES = {
18
+ 2: "validation",
19
+ 3: "auth",
20
+ 4: "permission",
21
+ 5: "not_found",
22
+ 6: "network",
23
+ 7: "api",
24
+ 10: "internal",
25
+ }
26
+
27
+
28
+ def _json_object(raw: str | None, flag: str) -> dict[str, Any] | None:
29
+ if raw is None:
30
+ return None
31
+ try:
32
+ value = json.loads(raw)
33
+ except json.JSONDecodeError as exc:
34
+ raise ValueError(f"{flag} is not valid JSON: {exc}") from exc
35
+ if not isinstance(value, dict):
36
+ raise ValueError(f"{flag} must decode to a JSON object")
37
+ return value
38
+
39
+
40
+ def _normalize_synbo_payload(
41
+ tool: str,
42
+ endpoint: str,
43
+ data: dict[str, Any] | None,
44
+ ) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
45
+ """Normalize SynBO's discrete condition contract before any remote request."""
46
+ canonical_tool = tool.removesuffix("-service")
47
+ if canonical_tool != "synbo" or endpoint not in {"initialize", "optimize"}:
48
+ return data, None
49
+ if data is None:
50
+ raise ValueError(f"synbo/{endpoint} requires --data-json")
51
+
52
+ condition_dict = data.get("condition_dict")
53
+ if not isinstance(condition_dict, dict) or not condition_dict:
54
+ raise ValueError("SynBO condition_dict must be a non-empty object")
55
+
56
+ normalized_conditions: dict[str, list[str]] = {}
57
+ for name, raw_values in condition_dict.items():
58
+ if not isinstance(raw_values, list) or not raw_values:
59
+ raise ValueError(
60
+ f"SynBO condition_dict.{name} must be a non-empty explicit list; "
61
+ "range objects are not supported"
62
+ )
63
+ values = [str(value) for value in raw_values]
64
+ if any(value == "" for value in values):
65
+ raise ValueError(f"SynBO condition_dict.{name} contains an empty value")
66
+ if len(set(values)) != len(values):
67
+ raise ValueError(
68
+ f"SynBO condition_dict.{name} contains duplicate values after "
69
+ "string normalization"
70
+ )
71
+ normalized_conditions[str(name)] = values
72
+
73
+ normalized = dict(data)
74
+ normalized["condition_dict"] = normalized_conditions
75
+ normalization = {
76
+ "condition_values": "strings",
77
+ "condition_space_size": _condition_space_size(normalized_conditions),
78
+ }
79
+
80
+ if endpoint == "initialize":
81
+ return normalized, normalization
82
+
83
+ metrics = normalized.get("opt_metrics")
84
+ if not isinstance(metrics, list) or not metrics:
85
+ raise ValueError("SynBO optimize requires a non-empty opt_metrics list")
86
+ metric_names = [str(metric) for metric in metrics]
87
+ normalized["opt_metrics"] = metric_names
88
+
89
+ previous_results = normalized.get("previous_results")
90
+ if not isinstance(previous_results, list) or not previous_results:
91
+ raise ValueError("SynBO optimize requires non-empty previous_results")
92
+
93
+ normalized_rows: list[dict[str, Any]] = []
94
+ for index, raw_row in enumerate(previous_results):
95
+ if not isinstance(raw_row, dict):
96
+ raise ValueError(f"SynBO previous_results[{index}] must be an object")
97
+ row = dict(raw_row)
98
+ for condition_name, allowed_values in normalized_conditions.items():
99
+ if condition_name not in row:
100
+ raise ValueError(
101
+ f"SynBO previous_results[{index}] is missing condition "
102
+ f"{condition_name!r}"
103
+ )
104
+ value = str(row[condition_name])
105
+ if value not in allowed_values:
106
+ raise ValueError(
107
+ f"SynBO previous_results[{index}].{condition_name}={value!r} "
108
+ "is not present in condition_dict"
109
+ )
110
+ row[condition_name] = value
111
+ for metric_name in metric_names:
112
+ metric_value = row.get(metric_name)
113
+ if (
114
+ metric_name not in row
115
+ or isinstance(metric_value, bool)
116
+ or not isinstance(metric_value, (int, float))
117
+ ):
118
+ raise ValueError(
119
+ f"SynBO previous_results[{index}].{metric_name} must be numeric"
120
+ )
121
+ normalized_rows.append(row)
122
+
123
+ normalized["previous_results"] = normalized_rows
124
+ normalized.setdefault("accuracy", "tiny")
125
+ normalized.setdefault("device", "cpu")
126
+ normalized.setdefault("surrogate_model", "RF")
127
+ normalized.setdefault("acq_func", "UCB")
128
+ normalization["defaults"] = {
129
+ key: normalized[key]
130
+ for key in ("accuracy", "device", "surrogate_model", "acq_func")
131
+ }
132
+ return normalized, normalization
133
+
134
+
135
+ def _condition_space_size(condition_dict: dict[str, list[str]]) -> int:
136
+ size = 1
137
+ for values in condition_dict.values():
138
+ size *= len(values)
139
+ return size
140
+
141
+
142
+ def _runtime_config() -> dict[str, Any]:
143
+ path = SKILL_DIR / "runtime.local.json"
144
+ if not path.is_file():
145
+ return {}
146
+ try:
147
+ value = json.loads(path.read_text(encoding="utf-8"))
148
+ except (OSError, json.JSONDecodeError):
149
+ return {}
150
+ return value if isinstance(value, dict) else {}
151
+
152
+
153
+ def _resolve_cli(explicit: str | None, runtime: dict[str, Any]) -> str:
154
+ candidates: list[str | None] = [
155
+ explicit,
156
+ os.environ.get("DELTA_CLI_PATH"),
157
+ runtime.get("cli_path"),
158
+ shutil.which("delta-cli"),
159
+ shutil.which("delta-cli.exe"),
160
+ ]
161
+ package_root = SKILL_DIR.parents[1]
162
+ exe_name = "delta-cli.exe" if os.name == "nt" else "delta-cli"
163
+ candidates.append(str(package_root / "bin" / exe_name))
164
+ for candidate in candidates:
165
+ if candidate and Path(candidate).is_file():
166
+ return str(Path(candidate).resolve())
167
+ raise FileNotFoundError(
168
+ "delta-cli not found; install @delta-infra/cli, set DELTA_CLI_PATH, "
169
+ "or add cli_path to delta-science/runtime.local.json"
170
+ )
171
+
172
+
115
173
  def _unwrap(stdout: str) -> tuple[Any, int]:
116
- try:
117
- root = json.loads(stdout)
118
- except json.JSONDecodeError as exc:
119
- raise RuntimeError(f"CLI stdout is not JSON: {exc}") from exc
120
- if not isinstance(root, dict):
121
- raise RuntimeError("CLI stdout root is not an object")
122
- if root.get("ok") is not True:
123
- raise RuntimeError(json.dumps(root.get("error", root), ensure_ascii=False))
124
- if "data" not in root:
125
- raise RuntimeError("CLI success envelope is missing data")
126
-
127
- value: Any = root["data"]
128
- depth = 0
129
- while isinstance(value, dict):
130
- if "status_code" in value and "data" in value:
131
- status_code = value.get("status_code")
132
- if not isinstance(status_code, int) or not 200 <= status_code < 300:
133
- raise RuntimeError(json.dumps(value, ensure_ascii=False))
134
- value = value["data"]
135
- elif (
136
- "code" in value
137
- and "data" in value
138
- and set(value).intersection({"message", "msg"})
139
- ):
140
- code = value.get("code")
141
- if code not in (0, "0", None):
142
- raise RuntimeError(json.dumps(value, ensure_ascii=False))
143
- value = value["data"]
144
- else:
145
- break
146
- depth += 1
147
- if depth > 4:
148
- raise RuntimeError("too many nested service envelopes")
149
- if isinstance(value, dict) and value.get("valid") is False:
150
- raise RuntimeError(json.dumps({
151
- "valid": False,
152
- "error": value.get("error"),
153
- "warnings": value.get("warnings"),
154
- }, ensure_ascii=False))
155
- return value, depth
174
+ try:
175
+ root = json.loads(stdout)
176
+ except json.JSONDecodeError as exc:
177
+ raise RuntimeError(f"CLI stdout is not JSON: {exc}") from exc
178
+ if not isinstance(root, dict):
179
+ raise RuntimeError("CLI stdout root is not an object")
180
+ if root.get("ok") is not True:
181
+ raise RuntimeError(json.dumps(root.get("error", root), ensure_ascii=False))
182
+ if "data" not in root:
183
+ raise RuntimeError("CLI success envelope is missing data")
184
+
185
+ value: Any = root["data"]
186
+ depth = 0
187
+ while isinstance(value, dict):
188
+ if "status_code" in value and "data" in value:
189
+ status_code = value.get("status_code")
190
+ if not isinstance(status_code, int) or not 200 <= status_code < 300:
191
+ raise RuntimeError(json.dumps(value, ensure_ascii=False))
192
+ value = value["data"]
193
+ elif (
194
+ "code" in value
195
+ and "data" in value
196
+ and set(value).intersection({"message", "msg"})
197
+ ):
198
+ code = value.get("code")
199
+ if code not in (0, "0", None):
200
+ raise RuntimeError(json.dumps(value, ensure_ascii=False))
201
+ value = value["data"]
202
+ else:
203
+ break
204
+ depth += 1
205
+ if depth > 4:
206
+ raise RuntimeError("too many nested service envelopes")
207
+ if isinstance(value, dict) and value.get("valid") is False:
208
+ raise RuntimeError(json.dumps({
209
+ "valid": False,
210
+ "error": value.get("error"),
211
+ "warnings": value.get("warnings"),
212
+ }, ensure_ascii=False))
213
+ return value, depth
214
+
215
+
216
+ def _validate_native(tool: str, endpoint: str, native: Any) -> None:
217
+ canonical_tool = tool.removesuffix("-service")
218
+ if canonical_tool == "synbo" and endpoint == "optimize":
219
+ if not isinstance(native, dict):
220
+ raise RuntimeError("SynBO optimize returned a non-object result")
221
+ recommendations = native.get("recommendations")
222
+ if not isinstance(recommendations, list) or not recommendations:
223
+ raise RuntimeError("SynBO optimize returned no recommendations")
224
+
225
+
226
+ def _parse_cli_error(completed: subprocess.CompletedProcess[str]) -> tuple[str, str]:
227
+ detail = completed.stderr.strip() or completed.stdout.strip()
228
+ error_type = _CLI_ERROR_TYPES.get(completed.returncode, "cli")
229
+ try:
230
+ envelope = json.loads(detail)
231
+ error = envelope.get("error", {}) if isinstance(envelope, dict) else {}
232
+ if isinstance(error, dict):
233
+ error_type = str(error.get("type") or error_type)
234
+ message = error.get("message")
235
+ if message:
236
+ detail = str(message)
237
+ except (AttributeError, json.JSONDecodeError):
238
+ pass
239
+ return error_type, detail
156
240
 
157
241
 
158
242
  def _parser() -> argparse.ArgumentParser:
159
- parser = argparse.ArgumentParser(description=__doc__)
160
- parser.add_argument("--tool", required=True)
161
- parser.add_argument("--endpoint", required=True)
162
- body = parser.add_mutually_exclusive_group()
163
- body.add_argument("--data-json")
243
+ parser = argparse.ArgumentParser(description=__doc__)
244
+ parser.add_argument("--tool", required=True)
245
+ parser.add_argument("--endpoint", required=True)
246
+ body = parser.add_mutually_exclusive_group()
247
+ body.add_argument("--data-json")
164
248
  body.add_argument("--params-json")
165
249
  parser.add_argument("--cli")
166
250
  parser.add_argument("--science-base-url")
167
- parser.add_argument(
168
- "--catalog-profile",
169
- choices=("auto", "canonical", "legacy"),
170
- default="auto",
171
- )
172
251
  parser.add_argument("--timeout", type=float, default=120.0)
173
- return parser
174
-
175
-
176
- def main() -> int:
252
+ return parser
253
+
254
+
255
+ def main() -> int:
177
256
  args = _parser().parse_args()
178
257
  started = time.perf_counter()
179
258
  stage = "arguments"
180
- profile = "unknown"
181
- resolved_tool = args.tool
182
- resolved_endpoint = args.endpoint
183
- try:
184
- data = _json_object(args.data_json, "--data-json")
185
- params = _json_object(args.params_json, "--params-json")
186
- runtime = _runtime_config()
187
- stage = "cli-resolution"
188
- cli = _resolve_cli(args.cli, runtime)
189
-
190
- env = os.environ.copy()
259
+ cli_exit_code: int | None = None
260
+ error_type = "unknown"
261
+ normalization: dict[str, Any] | None = None
262
+ try:
263
+ data = _json_object(args.data_json, "--data-json")
264
+ params = _json_object(args.params_json, "--params-json")
265
+ data, normalization = _normalize_synbo_payload(
266
+ args.tool, args.endpoint, data
267
+ )
268
+ runtime = _runtime_config()
269
+ stage = "cli-resolution"
270
+ cli = _resolve_cli(args.cli, runtime)
271
+
272
+ env = os.environ.copy()
191
273
  base_url = args.science_base_url or runtime.get("science_base_url")
192
274
  if base_url:
193
275
  env["DELTA_INFRA_SCIENCE_BASE_URL"] = str(base_url)
194
- configured_base_url = _cli_science_base_url(cli, env, args.timeout)
195
- requested_profile = str(runtime.get("catalog_profile", args.catalog_profile))
196
- if requested_profile not in {"auto", "canonical", "legacy"}:
197
- raise ValueError("catalog_profile must be auto, canonical, or legacy")
198
- profile = _catalog_profile(configured_base_url, requested_profile)
199
- resolved_tool, resolved_endpoint = _resolve_catalog_names(
200
- args.tool, args.endpoint, profile
201
- )
202
276
 
203
277
  command = [
204
278
  cli, "science", "invoke",
205
- "--tool", resolved_tool,
206
- "--endpoint", resolved_endpoint,
279
+ "--tool", args.tool,
280
+ "--endpoint", args.endpoint,
207
281
  ]
208
- if data is not None:
209
- command.extend(["--data", json.dumps(data, ensure_ascii=False, separators=(",", ":"))])
210
- if params is not None:
211
- command.extend(["--params", json.dumps(params, ensure_ascii=False, separators=(",", ":"))])
212
-
213
- stage = "delta-cli"
282
+ if data is not None:
283
+ command.extend(["--data", json.dumps(data, ensure_ascii=False, separators=(",", ":"))])
284
+ if params is not None:
285
+ command.extend(["--params", json.dumps(params, ensure_ascii=False, separators=(",", ":"))])
286
+
287
+ stage = "delta-cli"
214
288
  completed = subprocess.run(
215
- command,
216
- capture_output=True,
217
- text=True,
218
- encoding="utf-8",
219
- errors="replace",
220
- env=env,
221
- timeout=args.timeout,
222
- check=False,
289
+ command,
290
+ capture_output=True,
291
+ text=True,
292
+ encoding="utf-8",
293
+ errors="replace",
294
+ env=env,
295
+ timeout=args.timeout,
296
+ check=False,
223
297
  )
224
298
  if completed.returncode != 0:
225
- detail = completed.stderr.strip() or completed.stdout.strip()
299
+ cli_exit_code = completed.returncode
300
+ error_type, detail = _parse_cli_error(completed)
226
301
  raise RuntimeError(f"exit_code={completed.returncode}: {detail}")
227
302
  stage = "response-validation"
228
303
  native, depth = _unwrap(completed.stdout)
229
- result = {
230
- "ok": True,
304
+ _validate_native(args.tool, args.endpoint, native)
305
+ result = {
306
+ "ok": True,
231
307
  "transport": "delta-cli",
232
308
  "tool": args.tool,
233
309
  "endpoint": args.endpoint,
234
- "resolved_tool": resolved_tool,
235
- "resolved_endpoint": resolved_endpoint,
236
- "catalog_profile": profile,
237
310
  "elapsed_seconds": round(time.perf_counter() - started, 3),
238
- "envelope_depth": depth,
239
- "native": native,
240
- }
241
- print(json.dumps(result, ensure_ascii=False))
242
- return 0
243
- except subprocess.TimeoutExpired:
244
- error = f"timeout after {args.timeout}s"
245
- except (OSError, ValueError, RuntimeError) as exc:
246
- error = str(exc)
247
- print(json.dumps({
248
- "ok": False,
311
+ "envelope_depth": depth,
312
+ "native": native,
313
+ }
314
+ if normalization is not None:
315
+ result["request_normalization"] = normalization
316
+ print(json.dumps(result, ensure_ascii=False))
317
+ return 0
318
+ except subprocess.TimeoutExpired:
319
+ error_type = "timeout"
320
+ error = f"timeout after {args.timeout}s"
321
+ except ValueError as exc:
322
+ error_type = "validation"
323
+ error = str(exc)
324
+ except OSError as exc:
325
+ error_type = "cli_resolution"
326
+ error = str(exc)
327
+ except RuntimeError as exc:
328
+ if error_type == "unknown":
329
+ error_type = "response_validation"
330
+ error = str(exc)
331
+ print(json.dumps({
332
+ "ok": False,
249
333
  "transport": "delta-cli",
250
334
  "tool": args.tool,
251
335
  "endpoint": args.endpoint,
252
- "resolved_tool": resolved_tool,
253
- "resolved_endpoint": resolved_endpoint,
254
- "catalog_profile": profile,
255
336
  "stage": stage,
256
- "elapsed_seconds": round(time.perf_counter() - started, 3),
257
- "error": error[:2000],
258
- }, ensure_ascii=False))
259
- return 1
260
-
261
-
262
- if __name__ == "__main__":
263
- sys.exit(main())
337
+ "error_type": error_type,
338
+ "cli_exit_code": cli_exit_code,
339
+ "elapsed_seconds": round(time.perf_counter() - started, 3),
340
+ "error": error[:2000],
341
+ }, ensure_ascii=False))
342
+ return 1
343
+
344
+
345
+ if __name__ == "__main__":
346
+ sys.exit(main())