agent-postbox 0.2.0__tar.gz → 0.3.0__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.
- {agent_postbox-0.2.0 → agent_postbox-0.3.0}/PKG-INFO +2 -1
- {agent_postbox-0.2.0 → agent_postbox-0.3.0}/README.md +1 -0
- {agent_postbox-0.2.0 → agent_postbox-0.3.0}/pyproject.toml +1 -1
- {agent_postbox-0.2.0 → agent_postbox-0.3.0}/src/agent_board/cli.py +45 -0
- {agent_postbox-0.2.0 → agent_postbox-0.3.0}/src/agent_board/store.py +54 -0
- {agent_postbox-0.2.0 → agent_postbox-0.3.0}/LICENSE +0 -0
- {agent_postbox-0.2.0 → agent_postbox-0.3.0}/src/agent_board/__init__.py +0 -0
- {agent_postbox-0.2.0 → agent_postbox-0.3.0}/src/agent_board/__main__.py +0 -0
- {agent_postbox-0.2.0 → agent_postbox-0.3.0}/src/agent_board/config.py +0 -0
- {agent_postbox-0.2.0 → agent_postbox-0.3.0}/src/agent_board/log.py +0 -0
- {agent_postbox-0.2.0 → agent_postbox-0.3.0}/src/agent_board/models.py +0 -0
- {agent_postbox-0.2.0 → agent_postbox-0.3.0}/src/agent_board/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-postbox
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A local, filesystem-backed message board that lets AI coding agents open private channels, exchange messages and artifacts, and tear them down when done.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -94,6 +94,7 @@ command is `agent-board`** and the **import name is `agent_board`** — only the
|
|
|
94
94
|
| `list-agents [--json]` | the directory of registered agents |
|
|
95
95
|
| `create --to <id\|name> [--topic T] [--json]` | open a channel and invite the target |
|
|
96
96
|
| `check-messages [--json] [--no-advance]` | new invites + messages since last check |
|
|
97
|
+
| `wait [--channel C] [--timeout N] [--follow]` | block until new activity (run via `run_in_background` or `Monitor`) |
|
|
97
98
|
| `list-channels [--json]` | the channels you belong to, with unread counts |
|
|
98
99
|
| `accept <channel>` | join a channel you were invited to |
|
|
99
100
|
| `post <channel> [--text T] [--artifact PATH] [--json]` | send a message and/or a file |
|
|
@@ -67,6 +67,7 @@ command is `agent-board`** and the **import name is `agent_board`** — only the
|
|
|
67
67
|
| `list-agents [--json]` | the directory of registered agents |
|
|
68
68
|
| `create --to <id\|name> [--topic T] [--json]` | open a channel and invite the target |
|
|
69
69
|
| `check-messages [--json] [--no-advance]` | new invites + messages since last check |
|
|
70
|
+
| `wait [--channel C] [--timeout N] [--follow]` | block until new activity (run via `run_in_background` or `Monitor`) |
|
|
70
71
|
| `list-channels [--json]` | the channels you belong to, with unread counts |
|
|
71
72
|
| `accept <channel>` | join a channel you were invited to |
|
|
72
73
|
| `post <channel> [--text T] [--artifact PATH] [--json]` | send a message and/or a file |
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# the existing PyPI project "agentboard".
|
|
5
5
|
[project]
|
|
6
6
|
name = "agent-postbox"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "A local, filesystem-backed message board that lets AI coding agents open private channels, exchange messages and artifacts, and tear them down when done."
|
|
9
9
|
authors = [{ name = "Ben Ballintyn", email = "benballintyn@gmail.com" }]
|
|
10
10
|
readme = "README.md"
|
|
@@ -18,6 +18,7 @@ import json
|
|
|
18
18
|
import os
|
|
19
19
|
import shutil
|
|
20
20
|
import sys
|
|
21
|
+
import time
|
|
21
22
|
from pathlib import Path
|
|
22
23
|
from typing import Any, TextIO
|
|
23
24
|
|
|
@@ -163,6 +164,42 @@ def cmd_check_messages(args: argparse.Namespace, out: TextIO) -> int:
|
|
|
163
164
|
return 0
|
|
164
165
|
|
|
165
166
|
|
|
167
|
+
def cmd_wait(args: argparse.Namespace, out: TextIO) -> int:
|
|
168
|
+
"""Block until new board activity arrives (run via Bash run_in_background / Monitor).
|
|
169
|
+
|
|
170
|
+
Light internal poll inside a single process -- the model never sees the
|
|
171
|
+
polling, only the result. Bounded by ``--timeout``; prints nothing during
|
|
172
|
+
quiet stretches (so it can't flood context). One-shot by default (exits on the
|
|
173
|
+
first activity); ``--follow`` streams one line per new message for Monitor.
|
|
174
|
+
Read-only: it does not advance your real cursor, so a normal check-messages/
|
|
175
|
+
read still works after you're woken.
|
|
176
|
+
"""
|
|
177
|
+
agent_id = _require_agent_id(args)
|
|
178
|
+
if store.get_agent(agent_id) is None:
|
|
179
|
+
out.write("not on the board yet -- create/accept/post first, then wait\n")
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
baseline = store.get_cursors(agent_id)
|
|
183
|
+
deadline = time.monotonic() + args.timeout
|
|
184
|
+
got = False
|
|
185
|
+
while True:
|
|
186
|
+
result, baseline = store.poll_updates(agent_id, baseline, channel=args.channel)
|
|
187
|
+
if store.has_updates(result):
|
|
188
|
+
out.write(_format_updates(result) + "\n")
|
|
189
|
+
out.flush()
|
|
190
|
+
got = True
|
|
191
|
+
if not args.follow:
|
|
192
|
+
return 0
|
|
193
|
+
if time.monotonic() >= deadline:
|
|
194
|
+
break
|
|
195
|
+
time.sleep(min(args.interval, max(0.0, deadline - time.monotonic())))
|
|
196
|
+
|
|
197
|
+
if not got and not args.follow:
|
|
198
|
+
out.write("(no new messages)\n")
|
|
199
|
+
return 3
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
|
|
166
203
|
def cmd_accept(args: argparse.Namespace, out: TextIO) -> int:
|
|
167
204
|
"""Join a channel you were invited to."""
|
|
168
205
|
agent_id = _require_agent_id(args)
|
|
@@ -404,6 +441,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
404
441
|
p.add_argument("--json", action="store_true", help="emit JSON")
|
|
405
442
|
p.set_defaults(func=cmd_list_channels)
|
|
406
443
|
|
|
444
|
+
p = sub.add_parser("wait", help="block until new activity (via run_in_background or Monitor)")
|
|
445
|
+
add_id(p)
|
|
446
|
+
p.add_argument("--channel", help="watch only this channel id")
|
|
447
|
+
p.add_argument("--timeout", type=float, default=300.0, help="max seconds to wait")
|
|
448
|
+
p.add_argument("--interval", type=float, default=1.0, help="poll interval seconds")
|
|
449
|
+
p.add_argument("--follow", action="store_true", help="stream for Monitor (vs exit on first)")
|
|
450
|
+
p.set_defaults(func=cmd_wait)
|
|
451
|
+
|
|
407
452
|
p = sub.add_parser("accept", help="join a channel you were invited to")
|
|
408
453
|
add_id(p)
|
|
409
454
|
p.add_argument("channel", help="channel id")
|
|
@@ -724,6 +724,60 @@ def channel_overviews(agent_id: str) -> list[dict[str, Any]]:
|
|
|
724
724
|
return overviews
|
|
725
725
|
|
|
726
726
|
|
|
727
|
+
def poll_updates(
|
|
728
|
+
agent_id: str, baseline: dict[str, int], channel: str | None = None
|
|
729
|
+
) -> tuple[dict[str, Any], dict[str, int]]:
|
|
730
|
+
"""Non-mutating check for activity newer than ``baseline``.
|
|
731
|
+
|
|
732
|
+
Unlike :func:`check_messages`, this never touches the agent's stored cursors --
|
|
733
|
+
it backs the blocking ``wait`` command, which only *notifies* of new activity
|
|
734
|
+
and leaves the authoritative read (and cursor advance) to the agent's own
|
|
735
|
+
check-messages/read once it's woken.
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
agent_id: The waiting agent.
|
|
739
|
+
baseline: Per-source last-seen seq (``"inbox"`` plus channel ids). Seed it
|
|
740
|
+
from :func:`get_cursors` to also surface anything already unread.
|
|
741
|
+
channel: If set, watch only this channel and ignore the inbox.
|
|
742
|
+
|
|
743
|
+
Returns:
|
|
744
|
+
``(result, advanced_baseline)`` -- ``result`` has the same shape as
|
|
745
|
+
:func:`check_messages` (others' events only); ``advanced_baseline`` is
|
|
746
|
+
``baseline`` moved past everything seen this poll (so a follow loop won't
|
|
747
|
+
re-report it).
|
|
748
|
+
"""
|
|
749
|
+
result: dict[str, Any] = {"invites": [], "channels": {}}
|
|
750
|
+
advanced = dict(baseline)
|
|
751
|
+
|
|
752
|
+
if channel is None:
|
|
753
|
+
inbox = _agent_home(agent_id) / "inbox"
|
|
754
|
+
inbox_seqs = _seq_files(inbox)
|
|
755
|
+
last = baseline.get("inbox", 0)
|
|
756
|
+
invites = [_read_json(inbox / f"{s:0{_SEQ_WIDTH}d}.json") for s in inbox_seqs if s > last]
|
|
757
|
+
if invites:
|
|
758
|
+
result["invites"] = invites
|
|
759
|
+
if inbox_seqs:
|
|
760
|
+
advanced["inbox"] = inbox_seqs[-1]
|
|
761
|
+
|
|
762
|
+
channels = member_channels(agent_id)
|
|
763
|
+
if channel is not None:
|
|
764
|
+
channels = [c for c in channels if c.id == channel]
|
|
765
|
+
for ch in channels:
|
|
766
|
+
try:
|
|
767
|
+
events, head = read_events(ch.id, baseline.get(ch.id, 0))
|
|
768
|
+
except BoardError:
|
|
769
|
+
continue # channel closed/deleted between listing and read
|
|
770
|
+
foreign = [e for e in events if e.author != agent_id]
|
|
771
|
+
if foreign:
|
|
772
|
+
result["channels"][ch.id] = {
|
|
773
|
+
"topic": ch.topic,
|
|
774
|
+
"events": [e.to_dict() for e in foreign],
|
|
775
|
+
}
|
|
776
|
+
if events:
|
|
777
|
+
advanced[ch.id] = head
|
|
778
|
+
return result, advanced
|
|
779
|
+
|
|
780
|
+
|
|
727
781
|
# --------------------------------------------------------------------------- #
|
|
728
782
|
# Garbage collection
|
|
729
783
|
# --------------------------------------------------------------------------- #
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|