@borgee/agents-host 0.2.31 → 0.2.32

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.
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import re
6
7
  import sys
7
8
  from pathlib import Path
8
9
  from urllib.error import HTTPError
@@ -17,15 +18,23 @@ TASK_THREAD_MISSING_TASK_ID_ERROR = (
17
18
  "Task assignment thread could not resolve the current task from persisted context "
18
19
  "or local fallback. Pass --task-id explicitly."
19
20
  )
21
+ # Python ints are arbitrary precision, so nothing can be rounded on the way into the query
22
+ # string; the bound is here only so both packaged CLIs accept and reject exactly the same
23
+ # inputs. It is the Node CLI that needs it: there an out-of-range magnitude re-stringifies as
24
+ # `1e+21`, which the gateway's `Number.parseInt(value, 10)` reads back as `1`.
25
+ INTEGER_ARGUMENT_PATTERN = re.compile(r"[+-]?[0-9]+")
26
+ MAX_SAFE_INTEGER = 2**53 - 1
20
27
 
21
28
 
22
29
  def parse_int(flag: str, value: str | None) -> int:
23
30
  if value is None:
24
31
  raise ValueError(f"Missing value after {flag}")
25
- try:
26
- return int(value)
27
- except ValueError as exc:
28
- raise ValueError(f"Invalid integer for {flag}: {value}") from exc
32
+ if not INTEGER_ARGUMENT_PATTERN.fullmatch(value):
33
+ raise ValueError(f"Invalid integer for {flag}: {value}")
34
+ parsed = int(value)
35
+ if abs(parsed) > MAX_SAFE_INTEGER:
36
+ raise ValueError(f"Integer out of range for {flag}: {value}")
37
+ return parsed
29
38
 
30
39
 
31
40
  def parse_args(argv: list[str]) -> dict[str, object]:
@@ -80,6 +89,8 @@ def parse_args(argv: list[str]) -> dict[str, object]:
80
89
  set_action("get-me")
81
90
  elif arg == "--read-history":
82
91
  set_action("read-history")
92
+ elif arg == "--read-task-history":
93
+ set_action("read-task-history")
83
94
  elif arg == "--read-draft":
84
95
  set_action("read-draft")
85
96
  elif arg == "--list-users":
@@ -185,22 +196,42 @@ def ensure_visible_mentions(body: str, participant_ids: list[str]) -> str:
185
196
  return next_body
186
197
 
187
198
 
188
- def explicit_task_id_provided(options: dict[str, object]) -> bool:
199
+ def explicit_task_id(options: dict[str, object]) -> str | None:
189
200
  task_id = options.get("task_id")
190
- return isinstance(task_id, str) and bool(task_id.strip())
201
+ trimmed = task_id.strip() if isinstance(task_id, str) else ""
202
+ return trimmed if trimmed else None
203
+
204
+
205
+ def missing_task_id_error(action: str) -> ValueError:
206
+ return ValueError(f"Missing required --task-id <value> for --{action}")
207
+
208
+
209
+ def require_explicit_task_id(action: str, options: dict[str, object]) -> str:
210
+ task_id = explicit_task_id(options)
211
+ if task_id is None:
212
+ raise missing_task_id_error(action)
213
+ return task_id
214
+
215
+
216
+ def history_query_suffix(options: dict[str, object]) -> str:
217
+ query: dict[str, str] = {}
218
+ for key in ("limit", "before", "after"):
219
+ if options.get(key) is not None:
220
+ query[key] = str(options[key])
221
+ return f"?{urlencode(query)}" if query else ""
191
222
 
192
223
 
193
224
  def resolve_task_id(payload: dict[str, object], action: str, options: dict[str, object]) -> str | None:
194
- task_id = options.get("task_id")
195
- if isinstance(task_id, str) and task_id.strip():
196
- return task_id.strip()
225
+ task_id = explicit_task_id(options)
226
+ if task_id is not None:
227
+ return task_id
197
228
  task_assignment_context = payload.get("taskAssignmentContext")
198
229
  if isinstance(task_assignment_context, dict) and task_assignment_context.get("active") is True:
199
230
  current_task_id = task_assignment_context.get("currentTaskId")
200
231
  if isinstance(current_task_id, str) and current_task_id.strip():
201
232
  return current_task_id.strip()
202
233
  return None
203
- raise ValueError(f"Missing required --task-id <value> for --{action}")
234
+ raise missing_task_id_error(action)
204
235
 
205
236
 
206
237
  def resolve_gateway_request(
@@ -225,15 +256,10 @@ def resolve_gateway_request(
225
256
  if action == "get-me":
226
257
  return f"{base_url}/v1/channels/{channel_id}/me", "GET", None, False
227
258
  if action == "read-history":
228
- query: dict[str, str] = {}
229
- if options.get("limit") is not None:
230
- query["limit"] = str(options["limit"])
231
- if options.get("before") is not None:
232
- query["before"] = str(options["before"])
233
- if options.get("after") is not None:
234
- query["after"] = str(options["after"])
235
- suffix = f"?{urlencode(query)}" if query else ""
236
- return f"{base_url}/v1/channels/{channel_id}/history{suffix}", "GET", None, False
259
+ return f"{base_url}/v1/channels/{channel_id}/history{history_query_suffix(options)}", "GET", None, False
260
+ if action == "read-task-history":
261
+ task_id = quote(require_explicit_task_id(action, options), safe="")
262
+ return f"{base_url}/v1/tasks/{task_id}/history{history_query_suffix(options)}", "GET", None, False
237
263
  if action == "read-draft":
238
264
  turn_execution_id = options.get("turn_execution_id")
239
265
  if not isinstance(turn_execution_id, str) or not turn_execution_id.strip():
@@ -258,7 +284,7 @@ def resolve_gateway_request(
258
284
  return f"{base_url}/v1/channels/{channel_id}/tasks", "GET", None, False
259
285
  if action == "get-task":
260
286
  task_id = resolve_task_id(payload, action, options)
261
- if isinstance(task_assignment_context, dict) and task_assignment_context.get("active") is True and not explicit_task_id_provided(options):
287
+ if isinstance(task_assignment_context, dict) and task_assignment_context.get("active") is True and explicit_task_id(options) is None:
262
288
  return f"{base_url}/v1/channels/{channel_id}/current-task", "GET", None, True
263
289
  return f"{base_url}/v1/tasks/{quote(str(task_id), safe='')}", "GET", None, False
264
290
  if action == "update-task":
@@ -274,7 +300,7 @@ def resolve_gateway_request(
274
300
  request_body["description"] = options["description"]
275
301
  if not request_body:
276
302
  raise ValueError("At least one of --status, --assignee-id, --title, or --description is required for --update-task")
277
- if isinstance(task_assignment_context, dict) and task_assignment_context.get("active") is True and not explicit_task_id_provided(options):
303
+ if isinstance(task_assignment_context, dict) and task_assignment_context.get("active") is True and explicit_task_id(options) is None:
278
304
  return f"{base_url}/v1/channels/{channel_id}/current-task", "PATCH", request_body, True
279
305
  return f"{base_url}/v1/tasks/{quote(str(task_id), safe='')}", "PATCH", request_body, False
280
306
  raise ValueError(f"Unsupported gateway action: {action}")
@@ -296,11 +322,11 @@ def read_gateway_auth(auth_path: Path | None, expected_channel_id: object) -> di
296
322
 
297
323
 
298
324
  def call_gateway(payload: dict[str, object], action: str, options: dict[str, object]) -> object:
325
+ url, method, request_body, uses_current_thread_fallback = resolve_gateway_request(payload, action, options)
299
326
  auth = read_gateway_auth(
300
327
  options.get("auth_path") if isinstance(options.get("auth_path"), Path) else None,
301
328
  payload.get("channelId"),
302
329
  )
303
- url, method, request_body, uses_current_thread_fallback = resolve_gateway_request(payload, action, options)
304
330
 
305
331
  data = None
306
332
  headers = {"Authorization": f"Bearer {auth['token']}"}
@@ -341,11 +367,12 @@ def call_gateway(payload: dict[str, object], action: str, options: dict[str, obj
341
367
  response_body = response.read().decode("utf-8")
342
368
  return json.loads(response_body) if response_body else None
343
369
  except HTTPError as exc:
344
- if uses_current_thread_fallback and exc.code == 404:
370
+ error_body = exc.read().decode("utf-8")
371
+ error_payload = json.loads(error_body) if error_body else None
372
+ if uses_current_thread_fallback and isinstance(error_payload, dict) and error_payload.get("error") == "not_found":
345
373
  raise RuntimeError(TASK_THREAD_MISSING_TASK_ID_ERROR) from exc
346
- raise RuntimeError(f"Gateway request failed: {exc}") from exc
347
- except Exception as exc:
348
- raise RuntimeError(f"Gateway request failed: {exc}") from exc
374
+ reported_body = json.dumps(error_payload, separators=(",", ":"), ensure_ascii=False)
375
+ raise RuntimeError(f"Gateway request failed with {exc.code}: {reported_body}") from exc
349
376
 
350
377
 
351
378
  def redact_bootstrap(payload: dict[str, object], context_path: Path) -> dict[str, object]: