@openlucaskaka/kagent 0.1.7
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.
- package/README.md +353 -0
- package/npm/bin/kagent-serve.js +6 -0
- package/npm/bin/kagent.js +6 -0
- package/npm/lib/App.js +524 -0
- package/npm/lib/app-state.js +224 -0
- package/npm/lib/approval-choice.js +25 -0
- package/npm/lib/commands.js +59 -0
- package/npm/lib/editor.js +188 -0
- package/npm/lib/ink-runner.js +41 -0
- package/npm/lib/kagent-home.js +39 -0
- package/npm/lib/launcher.js +221 -0
- package/npm/lib/protocol.js +33 -0
- package/npm/lib/provider-setup.js +139 -0
- package/npm/lib/python-runner.js +892 -0
- package/npm/lib/runtime-client.js +390 -0
- package/npm/lib/terminal-input.js +127 -0
- package/npm/lib/terminal-text.js +19 -0
- package/npm/lib/terminal-width.js +14 -0
- package/npm/lib/transcript.js +227 -0
- package/npm/lib/ui-components.js +247 -0
- package/npm/lib/update-manager.js +334 -0
- package/package.json +39 -0
- package/pyproject.toml +55 -0
- package/src/kagent/__init__.py +90 -0
- package/src/kagent/cli/__init__.py +5 -0
- package/src/kagent/cli/__main__.py +6 -0
- package/src/kagent/cli/commands.py +112 -0
- package/src/kagent/cli/conversation.py +127 -0
- package/src/kagent/cli/interactive.py +841 -0
- package/src/kagent/cli/main.py +685 -0
- package/src/kagent/cli/memory.py +460 -0
- package/src/kagent/cli/pending_approval.py +169 -0
- package/src/kagent/cli/provider.py +27 -0
- package/src/kagent/cli/session_commands.py +401 -0
- package/src/kagent/cli/stdio_runtime.py +784 -0
- package/src/kagent/cli/trace.py +67 -0
- package/src/kagent/cli/ui.py +931 -0
- package/src/kagent/core/__init__.py +0 -0
- package/src/kagent/core/agent.py +296 -0
- package/src/kagent/core/faults.py +11 -0
- package/src/kagent/core/invariants.py +47 -0
- package/src/kagent/core/normalization.py +81 -0
- package/src/kagent/core/planning.py +48 -0
- package/src/kagent/core/state.py +73 -0
- package/src/kagent/core/summary.py +64 -0
- package/src/kagent/core/tools.py +196 -0
- package/src/kagent/core/trace.py +57 -0
- package/src/kagent/eval/__init__.py +11 -0
- package/src/kagent/eval/cases.py +229 -0
- package/src/kagent/eval/evaluator.py +216 -0
- package/src/kagent/integrations/__init__.py +3 -0
- package/src/kagent/integrations/audit.py +95 -0
- package/src/kagent/integrations/backends.py +131 -0
- package/src/kagent/integrations/memory.py +301 -0
- package/src/kagent/ops/__init__.py +0 -0
- package/src/kagent/ops/batch.py +156 -0
- package/src/kagent/ops/doctor.py +255 -0
- package/src/kagent/ops/metrics.py +214 -0
- package/src/kagent/ops/release_evidence.py +877 -0
- package/src/kagent/ops/release_manifest.py +142 -0
- package/src/kagent/ops/trace_replay.py +285 -0
- package/src/kagent/providers/__init__.py +7 -0
- package/src/kagent/providers/embeddings.py +187 -0
- package/src/kagent/providers/llm.py +770 -0
- package/src/kagent/py.typed +1 -0
- package/src/kagent/runtime/__init__.py +28 -0
- package/src/kagent/runtime/action_graph.py +543 -0
- package/src/kagent/runtime/agent.py +2089 -0
- package/src/kagent/runtime/approval.py +64 -0
- package/src/kagent/runtime/cancellation.py +64 -0
- package/src/kagent/runtime/checkpoint_state.py +146 -0
- package/src/kagent/runtime/checkpoint_storage.py +270 -0
- package/src/kagent/runtime/context.py +65 -0
- package/src/kagent/runtime/file_transaction.py +195 -0
- package/src/kagent/runtime/hooks.py +74 -0
- package/src/kagent/runtime/metadata.py +116 -0
- package/src/kagent/runtime/patch_checkpoints.py +385 -0
- package/src/kagent/runtime/policy.py +50 -0
- package/src/kagent/runtime/presentation.py +205 -0
- package/src/kagent/runtime/redaction.py +130 -0
- package/src/kagent/runtime/sandbox.py +331 -0
- package/src/kagent/runtime/skills.py +66 -0
- package/src/kagent/runtime/steering.py +56 -0
- package/src/kagent/runtime/steps.py +255 -0
- package/src/kagent/runtime/task_state.py +40 -0
- package/src/kagent/runtime/tools.py +3532 -0
- package/src/kagent/runtime/types.py +240 -0
- package/src/kagent/runtime/workspace.py +597 -0
- package/src/kagent/service/__init__.py +26 -0
- package/src/kagent/service/__main__.py +6 -0
- package/src/kagent/service/active_runs.py +178 -0
- package/src/kagent/service/cli.py +737 -0
- package/src/kagent/service/contract.py +2571 -0
- package/src/kagent/service/errors.py +71 -0
- package/src/kagent/service/idempotency.py +584 -0
- package/src/kagent/service/router.py +884 -0
- package/src/kagent/service/run.py +150 -0
- package/src/kagent/service/runtime.py +1915 -0
- package/src/kagent/service/runtime_approval.py +20 -0
- package/src/kagent/service/runtime_cancel.py +192 -0
- package/src/kagent/service/runtime_lifecycle.py +229 -0
- package/src/kagent/service/runtime_metadata.py +21 -0
- package/src/kagent/service/runtime_policy.py +115 -0
- package/src/kagent/service/runtime_recovery.py +573 -0
- package/src/kagent/service/runtime_resume.py +382 -0
- package/src/kagent/service/runtime_resume_claim.py +116 -0
- package/src/kagent/service/runtime_run.py +350 -0
- package/src/kagent/service/runtime_status.py +2007 -0
- package/src/kagent/service/safety.py +139 -0
- package/src/kagent/service/server.py +114 -0
- package/src/kagent/service/status.py +233 -0
- package/src/kagent/service/trace_store.py +322 -0
- package/src/kagent/service/transport.py +24 -0
- package/src/kagent/utils/__init__.py +0 -0
- package/src/kagent/utils/config_validation.py +21 -0
- package/src/kagent/utils/json_output.py +23 -0
- package/src/kagent/utils/paths.py +473 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import socket
|
|
5
|
+
import urllib.parse
|
|
6
|
+
import urllib.request
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, Dict, List
|
|
9
|
+
|
|
10
|
+
_KEY_PART_MAX_LENGTH = 120
|
|
11
|
+
_TEXT_MAX_LENGTH = 20000
|
|
12
|
+
_VECTOR_MAX_ITEMS = 4096
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class RedisShortTermMemory:
|
|
17
|
+
url: str
|
|
18
|
+
timeout_seconds: float = 2.0
|
|
19
|
+
key_prefix: str = "kagent"
|
|
20
|
+
|
|
21
|
+
def put(
|
|
22
|
+
self,
|
|
23
|
+
*,
|
|
24
|
+
namespace: str,
|
|
25
|
+
key: str,
|
|
26
|
+
value: Any,
|
|
27
|
+
ttl_seconds: int = 0,
|
|
28
|
+
) -> Dict[str, Any]:
|
|
29
|
+
normalized_namespace = _validate_key_part(namespace, "namespace")
|
|
30
|
+
normalized_key = _validate_key_part(key, "key")
|
|
31
|
+
normalized_ttl = _validate_ttl(ttl_seconds)
|
|
32
|
+
redis_key = self._redis_key(normalized_namespace, normalized_key)
|
|
33
|
+
encoded_value = json.dumps(
|
|
34
|
+
value,
|
|
35
|
+
ensure_ascii=False,
|
|
36
|
+
separators=(",", ":"),
|
|
37
|
+
sort_keys=True,
|
|
38
|
+
)
|
|
39
|
+
command = ["SET", redis_key, encoded_value]
|
|
40
|
+
if normalized_ttl > 0:
|
|
41
|
+
command.extend(["EX", str(normalized_ttl)])
|
|
42
|
+
response = self._execute(command)
|
|
43
|
+
if response != "OK":
|
|
44
|
+
raise RuntimeError("redis did not acknowledge memory write")
|
|
45
|
+
return {
|
|
46
|
+
"backend": "redis",
|
|
47
|
+
"namespace": normalized_namespace,
|
|
48
|
+
"key": normalized_key,
|
|
49
|
+
"stored": True,
|
|
50
|
+
"ttl_seconds": str(normalized_ttl),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def get(self, *, namespace: str, key: str) -> Dict[str, Any]:
|
|
54
|
+
normalized_namespace = _validate_key_part(namespace, "namespace")
|
|
55
|
+
normalized_key = _validate_key_part(key, "key")
|
|
56
|
+
response = self._execute(["GET", self._redis_key(normalized_namespace, normalized_key)])
|
|
57
|
+
if response is None:
|
|
58
|
+
return {
|
|
59
|
+
"backend": "redis",
|
|
60
|
+
"namespace": normalized_namespace,
|
|
61
|
+
"key": normalized_key,
|
|
62
|
+
"found": False,
|
|
63
|
+
"value": None,
|
|
64
|
+
}
|
|
65
|
+
try:
|
|
66
|
+
value = json.loads(response)
|
|
67
|
+
except json.JSONDecodeError:
|
|
68
|
+
value = response
|
|
69
|
+
return {
|
|
70
|
+
"backend": "redis",
|
|
71
|
+
"namespace": normalized_namespace,
|
|
72
|
+
"key": normalized_key,
|
|
73
|
+
"found": True,
|
|
74
|
+
"value": value,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def _redis_key(self, namespace: str, key: str) -> str:
|
|
78
|
+
return f"{self.key_prefix}:{namespace}:{key}"
|
|
79
|
+
|
|
80
|
+
def _execute(self, command: List[str]) -> Any:
|
|
81
|
+
parsed = urllib.parse.urlparse(self.url)
|
|
82
|
+
if parsed.scheme != "redis":
|
|
83
|
+
raise ValueError("redis url must use redis://")
|
|
84
|
+
if not parsed.hostname:
|
|
85
|
+
raise ValueError("redis url host is required")
|
|
86
|
+
port = parsed.port or 6379
|
|
87
|
+
with socket.create_connection(
|
|
88
|
+
(parsed.hostname, port),
|
|
89
|
+
timeout=self.timeout_seconds,
|
|
90
|
+
) as conn:
|
|
91
|
+
conn.settimeout(self.timeout_seconds)
|
|
92
|
+
conn.sendall(_encode_resp_array(command))
|
|
93
|
+
response = _read_resp(conn)
|
|
94
|
+
if isinstance(response, Exception):
|
|
95
|
+
raise response
|
|
96
|
+
return response
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class MilvusLongTermMemory:
|
|
101
|
+
url: str
|
|
102
|
+
timeout_seconds: float = 2.0
|
|
103
|
+
|
|
104
|
+
def upsert(
|
|
105
|
+
self,
|
|
106
|
+
*,
|
|
107
|
+
collection: str,
|
|
108
|
+
memory_id: str,
|
|
109
|
+
text: str,
|
|
110
|
+
vector: List[float],
|
|
111
|
+
metadata: Dict[str, Any] | None = None,
|
|
112
|
+
) -> Dict[str, Any]:
|
|
113
|
+
normalized_collection = _validate_key_part(collection, "collection")
|
|
114
|
+
normalized_id = _validate_key_part(memory_id, "memory_id")
|
|
115
|
+
normalized_text = _validate_text(text)
|
|
116
|
+
normalized_vector = _validate_vector(vector)
|
|
117
|
+
payload = {
|
|
118
|
+
"collectionName": normalized_collection,
|
|
119
|
+
"data": [
|
|
120
|
+
{
|
|
121
|
+
"id": normalized_id,
|
|
122
|
+
"text": normalized_text,
|
|
123
|
+
"vector": normalized_vector,
|
|
124
|
+
"metadata": metadata or {},
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
}
|
|
128
|
+
self._post("/v2/vectordb/entities/insert", payload)
|
|
129
|
+
return {
|
|
130
|
+
"backend": "milvus",
|
|
131
|
+
"collection": normalized_collection,
|
|
132
|
+
"memory_id": normalized_id,
|
|
133
|
+
"stored": True,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
def search(
|
|
137
|
+
self,
|
|
138
|
+
*,
|
|
139
|
+
collection: str,
|
|
140
|
+
vector: List[float],
|
|
141
|
+
limit: int = 5,
|
|
142
|
+
) -> Dict[str, Any]:
|
|
143
|
+
normalized_collection = _validate_key_part(collection, "collection")
|
|
144
|
+
normalized_vector = _validate_vector(vector)
|
|
145
|
+
normalized_limit = _validate_limit(limit)
|
|
146
|
+
payload = {
|
|
147
|
+
"collectionName": normalized_collection,
|
|
148
|
+
"data": [normalized_vector],
|
|
149
|
+
"limit": normalized_limit,
|
|
150
|
+
"outputFields": ["id", "text", "metadata"],
|
|
151
|
+
}
|
|
152
|
+
response = self._post("/v2/vectordb/entities/search", payload)
|
|
153
|
+
matches = _milvus_matches(response)
|
|
154
|
+
return {
|
|
155
|
+
"backend": "milvus",
|
|
156
|
+
"collection": normalized_collection,
|
|
157
|
+
"matches": matches,
|
|
158
|
+
"match_count": len(matches),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
162
|
+
endpoint = self._endpoint(path)
|
|
163
|
+
request = urllib.request.Request(
|
|
164
|
+
endpoint,
|
|
165
|
+
data=json.dumps(payload, sort_keys=True).encode("utf-8"),
|
|
166
|
+
headers={"Content-Type": "application/json"},
|
|
167
|
+
method="POST",
|
|
168
|
+
)
|
|
169
|
+
with urllib.request.urlopen(request, timeout=self.timeout_seconds) as response:
|
|
170
|
+
body = response.read(1024 * 1024).decode("utf-8")
|
|
171
|
+
if int(response.status) < 200 or int(response.status) >= 300:
|
|
172
|
+
raise RuntimeError("milvus request failed")
|
|
173
|
+
parsed = json.loads(body or "{}")
|
|
174
|
+
if not isinstance(parsed, dict):
|
|
175
|
+
raise RuntimeError("milvus response must be a JSON object")
|
|
176
|
+
code = parsed.get("code", 0)
|
|
177
|
+
if code not in (0, "0", None):
|
|
178
|
+
raise RuntimeError(f"milvus request failed: code={code}")
|
|
179
|
+
return parsed
|
|
180
|
+
|
|
181
|
+
def _endpoint(self, path: str) -> str:
|
|
182
|
+
parsed = urllib.parse.urlparse(self.url)
|
|
183
|
+
if parsed.scheme not in {"http", "https"}:
|
|
184
|
+
raise ValueError("milvus url must use http:// or https://")
|
|
185
|
+
return urllib.parse.urljoin(self.url.rstrip("/") + "/", path.lstrip("/"))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _validate_key_part(value: str, field_name: str) -> str:
|
|
189
|
+
normalized = str(value).strip()
|
|
190
|
+
if not normalized:
|
|
191
|
+
raise ValueError(f"{field_name} is required")
|
|
192
|
+
if len(normalized) > _KEY_PART_MAX_LENGTH:
|
|
193
|
+
raise ValueError(f"{field_name} is too long")
|
|
194
|
+
if any(char in normalized for char in "/\\ \t\r\n:"):
|
|
195
|
+
raise ValueError(f"{field_name} contains unsafe characters")
|
|
196
|
+
return normalized
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _validate_text(value: str) -> str:
|
|
200
|
+
normalized = str(value).strip()
|
|
201
|
+
if not normalized:
|
|
202
|
+
raise ValueError("text is required")
|
|
203
|
+
if len(normalized) > _TEXT_MAX_LENGTH:
|
|
204
|
+
raise ValueError("text is too long")
|
|
205
|
+
return normalized
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _validate_vector(vector: List[float]) -> List[float]:
|
|
209
|
+
if not isinstance(vector, list) or not vector:
|
|
210
|
+
raise ValueError("vector must be a non-empty list")
|
|
211
|
+
if len(vector) > _VECTOR_MAX_ITEMS:
|
|
212
|
+
raise ValueError("vector is too long")
|
|
213
|
+
normalized = []
|
|
214
|
+
for item in vector:
|
|
215
|
+
if isinstance(item, bool) or not isinstance(item, (int, float)):
|
|
216
|
+
raise ValueError("vector must contain only numbers")
|
|
217
|
+
normalized.append(float(item))
|
|
218
|
+
return normalized
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _validate_ttl(ttl_seconds: int) -> int:
|
|
222
|
+
if isinstance(ttl_seconds, bool) or not isinstance(ttl_seconds, int):
|
|
223
|
+
raise ValueError("ttl_seconds must be an integer")
|
|
224
|
+
if ttl_seconds < 0:
|
|
225
|
+
raise ValueError("ttl_seconds must be non-negative")
|
|
226
|
+
return ttl_seconds
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _validate_limit(limit: int) -> int:
|
|
230
|
+
if isinstance(limit, bool) or not isinstance(limit, int):
|
|
231
|
+
raise ValueError("limit must be an integer")
|
|
232
|
+
if limit < 1 or limit > 50:
|
|
233
|
+
raise ValueError("limit must be between 1 and 50")
|
|
234
|
+
return limit
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _encode_resp_array(parts: List[str]) -> bytes:
|
|
238
|
+
chunks = [f"*{len(parts)}\r\n".encode("utf-8")]
|
|
239
|
+
for part in parts:
|
|
240
|
+
encoded = str(part).encode("utf-8")
|
|
241
|
+
chunks.append(f"${len(encoded)}\r\n".encode("utf-8"))
|
|
242
|
+
chunks.append(encoded + b"\r\n")
|
|
243
|
+
return b"".join(chunks)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _read_resp(conn: socket.socket) -> Any:
|
|
247
|
+
prefix = conn.recv(1)
|
|
248
|
+
if prefix == b"+":
|
|
249
|
+
return _read_line(conn)
|
|
250
|
+
if prefix == b"$":
|
|
251
|
+
length = int(_read_line(conn))
|
|
252
|
+
if length == -1:
|
|
253
|
+
return None
|
|
254
|
+
data = _recv_exact(conn, length)
|
|
255
|
+
_recv_exact(conn, 2)
|
|
256
|
+
return data.decode("utf-8")
|
|
257
|
+
if prefix == b"-":
|
|
258
|
+
return RuntimeError(_read_line(conn))
|
|
259
|
+
raise RuntimeError("unsupported redis response")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _read_line(conn: socket.socket) -> str:
|
|
263
|
+
data = bytearray()
|
|
264
|
+
while not data.endswith(b"\r\n"):
|
|
265
|
+
chunk = conn.recv(1)
|
|
266
|
+
if not chunk:
|
|
267
|
+
raise RuntimeError("connection closed while reading redis response")
|
|
268
|
+
data.extend(chunk)
|
|
269
|
+
return data[:-2].decode("utf-8")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _recv_exact(conn: socket.socket, size: int) -> bytes:
|
|
273
|
+
data = bytearray()
|
|
274
|
+
while len(data) < size:
|
|
275
|
+
chunk = conn.recv(size - len(data))
|
|
276
|
+
if not chunk:
|
|
277
|
+
raise RuntimeError("connection closed while reading redis response")
|
|
278
|
+
data.extend(chunk)
|
|
279
|
+
return bytes(data)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _milvus_matches(response: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
283
|
+
data = response.get("data", [])
|
|
284
|
+
if data and isinstance(data[0], list):
|
|
285
|
+
rows = data[0]
|
|
286
|
+
else:
|
|
287
|
+
rows = data
|
|
288
|
+
matches = []
|
|
289
|
+
for row in rows if isinstance(rows, list) else []:
|
|
290
|
+
if not isinstance(row, dict):
|
|
291
|
+
continue
|
|
292
|
+
memory_id = row.get("id", row.get("memory_id", ""))
|
|
293
|
+
matches.append(
|
|
294
|
+
{
|
|
295
|
+
"memory_id": str(memory_id),
|
|
296
|
+
"text": str(row.get("text", "")),
|
|
297
|
+
"score": float(row.get("score", row.get("distance", 0.0))),
|
|
298
|
+
"metadata": row.get("metadata") if isinstance(row.get("metadata"), dict) else {},
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
return matches
|
|
File without changes
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import contextlib
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import warnings
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, Iterable
|
|
10
|
+
|
|
11
|
+
from kagent.utils.config_validation import optional_json_int
|
|
12
|
+
from kagent.utils.json_output import format_and_write_json, json_ready
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run_batch_file(
|
|
16
|
+
input_path: Path,
|
|
17
|
+
output_path: Path,
|
|
18
|
+
*,
|
|
19
|
+
full_trace: bool = False,
|
|
20
|
+
) -> Dict[str, str]:
|
|
21
|
+
records = list(_run_batch_records(input_path, full_trace=full_trace))
|
|
22
|
+
output_path.write_text(
|
|
23
|
+
"".join(json.dumps(record, sort_keys=True) + "\n" for record in records),
|
|
24
|
+
encoding="utf-8",
|
|
25
|
+
)
|
|
26
|
+
succeeded = sum(1 for record in records if record.get("status") == "done")
|
|
27
|
+
return {
|
|
28
|
+
"processed": str(len(records)),
|
|
29
|
+
"succeeded": str(succeeded),
|
|
30
|
+
"failed": str(len(records) - succeeded),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _run_batch_records(
|
|
35
|
+
input_path: Path,
|
|
36
|
+
*,
|
|
37
|
+
full_trace: bool,
|
|
38
|
+
) -> Iterable[Dict[str, Any]]:
|
|
39
|
+
warning_sink = io.StringIO()
|
|
40
|
+
with contextlib.redirect_stderr(warning_sink), warnings.catch_warnings():
|
|
41
|
+
warnings.simplefilter("ignore")
|
|
42
|
+
from kagent.core.agent import AgentConfig, run_agent
|
|
43
|
+
from kagent.core.summary import summarize_run
|
|
44
|
+
|
|
45
|
+
yield from _run_batch_records_with_agent(
|
|
46
|
+
input_path,
|
|
47
|
+
AgentConfig,
|
|
48
|
+
run_agent,
|
|
49
|
+
summarize_run,
|
|
50
|
+
full_trace=full_trace,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _run_batch_records_with_agent(
|
|
55
|
+
input_path: Path,
|
|
56
|
+
AgentConfig,
|
|
57
|
+
run_agent,
|
|
58
|
+
summarize_run,
|
|
59
|
+
*,
|
|
60
|
+
full_trace: bool,
|
|
61
|
+
):
|
|
62
|
+
for line_number, line in enumerate(input_path.read_text().splitlines(), start=1):
|
|
63
|
+
if not line.strip():
|
|
64
|
+
continue
|
|
65
|
+
try:
|
|
66
|
+
payload = json.loads(line)
|
|
67
|
+
except json.JSONDecodeError as exc:
|
|
68
|
+
yield _failed_record(line_number, "", f"invalid JSON: {exc.msg}")
|
|
69
|
+
continue
|
|
70
|
+
if not isinstance(payload, dict):
|
|
71
|
+
yield _failed_record(line_number, "", "input line must be a JSON object")
|
|
72
|
+
continue
|
|
73
|
+
goal = str(payload.get("goal", ""))
|
|
74
|
+
item_id = str(payload.get("id", line_number))
|
|
75
|
+
if not goal.strip():
|
|
76
|
+
yield _failed_record(line_number, item_id, "goal is required")
|
|
77
|
+
continue
|
|
78
|
+
try:
|
|
79
|
+
config = _config_from_payload(AgentConfig, payload)
|
|
80
|
+
except (TypeError, ValueError) as exc:
|
|
81
|
+
yield _failed_record(line_number, item_id, str(exc))
|
|
82
|
+
continue
|
|
83
|
+
state = run_agent(goal, config=config)
|
|
84
|
+
summary = summarize_run(state)
|
|
85
|
+
record = {
|
|
86
|
+
"id": item_id,
|
|
87
|
+
"line_number": str(line_number),
|
|
88
|
+
"status": summary["status"],
|
|
89
|
+
}
|
|
90
|
+
if full_trace:
|
|
91
|
+
record["trace"] = json_ready(state)
|
|
92
|
+
else:
|
|
93
|
+
record["summary"] = summary
|
|
94
|
+
yield record
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _failed_record(line_number: int, item_id: str, error: str) -> Dict[str, str]:
|
|
98
|
+
return {
|
|
99
|
+
"id": item_id,
|
|
100
|
+
"line_number": str(line_number),
|
|
101
|
+
"status": "failed",
|
|
102
|
+
"error": error,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _config_from_payload(AgentConfig, payload: Dict[str, Any]):
|
|
107
|
+
defaults = AgentConfig()
|
|
108
|
+
return AgentConfig(
|
|
109
|
+
max_steps=_optional_int(payload, "max_steps", defaults.max_steps),
|
|
110
|
+
max_retries=_optional_int(payload, "max_retries", defaults.max_retries),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _optional_int(payload: Dict[str, Any], key: str, default: int) -> int:
|
|
115
|
+
return optional_json_int(payload, key, default)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def main() -> None:
|
|
119
|
+
parser = argparse.ArgumentParser(
|
|
120
|
+
description="Run agent goals from a JSONL file and write JSONL summaries."
|
|
121
|
+
)
|
|
122
|
+
parser.add_argument("input_jsonl", help="Input JSONL file with one goal object per line.")
|
|
123
|
+
parser.add_argument("output_jsonl", help="Output JSONL file for result summaries.")
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
"--output",
|
|
126
|
+
default="",
|
|
127
|
+
metavar="PATH",
|
|
128
|
+
help="Write the batch report JSON to PATH as well as stdout.",
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument(
|
|
131
|
+
"--fail-on-failure",
|
|
132
|
+
action="store_true",
|
|
133
|
+
help="Exit with code 1 when any batch record failed.",
|
|
134
|
+
)
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
"--full-trace",
|
|
137
|
+
action="store_true",
|
|
138
|
+
help="Write full agent traces instead of compact summaries.",
|
|
139
|
+
)
|
|
140
|
+
args = parser.parse_args()
|
|
141
|
+
try:
|
|
142
|
+
report = run_batch_file(
|
|
143
|
+
Path(args.input_jsonl),
|
|
144
|
+
Path(args.output_jsonl),
|
|
145
|
+
full_trace=args.full_trace,
|
|
146
|
+
)
|
|
147
|
+
json_payload = format_and_write_json(report, args.output)
|
|
148
|
+
except OSError as exc:
|
|
149
|
+
parser.error(str(exc))
|
|
150
|
+
print(json_payload)
|
|
151
|
+
if args.fail_on_failure and int(report["failed"]) > 0:
|
|
152
|
+
raise SystemExit(1)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__":
|
|
156
|
+
main()
|