agent-manager-cli 0.1.3__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-manager-cli
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: CLI для удалённого управления AI-агентами — Node Agent, daemon, сканер сессий
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -79,7 +79,8 @@ def map_stream_event(line: str) -> list[dict]:
79
79
  return []
80
80
 
81
81
  if msg_type == "assistant":
82
- content_blocks = msg.get("message", {}).get("content", [])
82
+ message = msg.get("message", {})
83
+ content_blocks = message.get("content", [])
83
84
  events = []
84
85
  for block in content_blocks:
85
86
  block_type = block.get("type")
@@ -108,6 +109,20 @@ def map_stream_event(line: str) -> list[dict]:
108
109
  },
109
110
  }
110
111
  )
112
+ # Emit token usage delta if present in this assistant message.
113
+ usage = message.get("usage") or {}
114
+ if usage:
115
+ events.append(
116
+ {
117
+ "event_type": "usage",
118
+ "data": {
119
+ "input_tokens": usage.get("input_tokens", 0),
120
+ "output_tokens": usage.get("output_tokens", 0),
121
+ "cache_creation_input_tokens": usage.get("cache_creation_input_tokens", 0),
122
+ "cache_read_input_tokens": usage.get("cache_read_input_tokens", 0),
123
+ },
124
+ }
125
+ )
111
126
  return events
112
127
 
113
128
  if msg_type == "user":
@@ -372,14 +387,16 @@ async def run_daemon_bidirectional(ws_url: str, ws_token: str, command: list[str
372
387
 
373
388
  try:
374
389
  raw = json.loads(line)
375
- if raw.get("type") == "system" and raw.get("subtype") == "init":
390
+ raw_type = raw.get("type", "?")
391
+ _log("raw", f"#{line_num} type={raw_type}")
392
+ if raw_type == "system" and raw.get("subtype") == "init":
376
393
  session_id = raw.get("session_id")
377
- elif raw.get("type") == "result":
394
+ elif raw_type == "result":
378
395
  sid = raw.get("session_id")
379
396
  if sid:
380
397
  session_id = sid
381
398
  except (json.JSONDecodeError, KeyError):
382
- pass
399
+ _log("raw", f"#{line_num} (not json) {line[:80]}")
383
400
 
384
401
  events = map_stream_event(line)
385
402
  if not events:
@@ -630,7 +647,8 @@ async def _run_node_agent(host: str, secure: bool, access_token: str) -> None:
630
647
  )
631
648
  )
632
649
  _log("node", f"Sent {len(sessions)} sessions")
633
- except websockets.exceptions.ConnectionClosed:
650
+ except websockets.exceptions.ConnectionClosed as e:
651
+ _log("node", f"scan: connection closed ({e.code} {e.reason or '-'})")
634
652
  break
635
653
  except Exception as e:
636
654
  _log("node", f"Scan error: {e}")
@@ -641,7 +659,8 @@ async def _run_node_agent(host: str, secure: bool, access_token: str) -> None:
641
659
  await asyncio.sleep(25)
642
660
  try:
643
661
  await ws.send(json.dumps({"type": "heartbeat"}))
644
- except websockets.exceptions.ConnectionClosed:
662
+ except websockets.exceptions.ConnectionClosed as e:
663
+ _log("node", f"heartbeat: connection closed ({e.code} {e.reason or '-'})")
645
664
  break
646
665
 
647
666
  async def _listen_commands():
@@ -654,8 +673,8 @@ async def _run_node_agent(host: str, secure: bool, access_token: str) -> None:
654
673
 
655
674
  if cmd.get("type") == "attach":
656
675
  await _handle_attach(cmd, host, secure, daemon_procs)
657
- except websockets.exceptions.ConnectionClosed:
658
- pass
676
+ except websockets.exceptions.ConnectionClosed as e:
677
+ _log("node", f"listen: connection closed ({e.code} {e.reason or '-'})")
659
678
 
660
679
  scan_task = asyncio.create_task(_scan_and_send())
661
680
  hb_task = asyncio.create_task(_heartbeat())
@@ -763,18 +782,45 @@ def cmd_connect(args: argparse.Namespace) -> None:
763
782
  save_config(config)
764
783
  _log("connect", "Token refreshed.")
765
784
 
766
- try:
767
- asyncio.run(_run_node_agent(host, secure, access_token))
768
- except KeyboardInterrupt:
769
- print("\n[connect] Stopped.", file=sys.stderr)
770
- except websockets.exceptions.InvalidStatusCode as e:
771
- _log("connect", f"WebSocket error: {e}")
772
- if e.status_code in (401, 403):
773
- _log("connect", "Token may be expired. Run `am login` again.")
774
- sys.exit(1)
775
- except Exception as e:
776
- _log("connect", f"Error: {e}")
777
- sys.exit(1)
785
+ # Reconnect loop with exponential backoff. On clean disconnect (network
786
+ # blip, backend restart, proxy timeout) we retry automatically. Auth
787
+ # errors (401/403) bail out immediately — the user must re-login.
788
+ import time as _time
789
+
790
+ backoff = 1
791
+ while True:
792
+ try:
793
+ asyncio.run(_run_node_agent(host, secure, access_token))
794
+ # _run_node_agent returned normally → ws was closed by server or
795
+ # one of the tasks finished. Reconnect after a short delay.
796
+ _log("connect", f"Disconnected. Reconnecting in {backoff}s...")
797
+ except KeyboardInterrupt:
798
+ print("\n[connect] Stopped.", file=sys.stderr)
799
+ return
800
+ except websockets.exceptions.InvalidStatusCode as e:
801
+ if e.status_code in (401, 403):
802
+ _log("connect", f"Auth failed ({e.status_code}). Run `am login` again.")
803
+ sys.exit(1)
804
+ _log("connect", f"WebSocket error: {e}, retrying in {backoff}s...")
805
+ except OSError as e:
806
+ # DNS / connect refused / network unreachable — keep trying.
807
+ _log("connect", f"Network error: {e}, retrying in {backoff}s...")
808
+ except Exception as e:
809
+ _log("connect", f"Error: {e} ({type(e).__name__}), retrying in {backoff}s...")
810
+
811
+ _time.sleep(backoff)
812
+ backoff = min(backoff * 2, 30) # cap at 30 seconds
813
+
814
+ # After a reconnect attempt, try to refresh the token again in case
815
+ # it expired while we were disconnected for a long period.
816
+ if refresh_token:
817
+ tokens = _refresh_token(host, secure, refresh_token)
818
+ if tokens:
819
+ access_token = tokens["access_token"]
820
+ refresh_token = tokens["refresh_token"]
821
+ config["access_token"] = access_token
822
+ config["refresh_token"] = refresh_token
823
+ save_config(config)
778
824
 
779
825
 
780
826
  # ---------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-manager-cli"
3
- version = "0.1.3"
3
+ version = "0.1.4"
4
4
  description = "CLI для удалённого управления AI-агентами — Node Agent, daemon, сканер сессий"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"