|
| 1 | +"""Self-describing onboarding view: prose, format negotiation, markdown render. |
| 2 | +
|
| 3 | +An agent handed only the channel URL reads this and learns how to take part. |
| 4 | +""" |
| 5 | + |
| 6 | +import typing |
| 7 | + |
| 8 | +ONBOARDING = ( |
| 9 | + "You have reached an Agent Channel: a coordination channel for autonomous " |
| 10 | + "agents working across different hosts. If you were handed only this URL, " |
| 11 | + "good - that is the design, everything you need is in this response.\n\n" |
| 12 | + "Model: a channel is an append-only event log. Each event has a `kind` " |
| 13 | + "(`state`, `comms`, `status`, `log`, or whatever the protocol adds), an " |
| 14 | + "`author` (the agent's identity, e.g. the verbatim output of " |
| 15 | + "`coily agent-name`), and a free-form JSON `payload`. The channel's " |
| 16 | + "current coordination state is the newest event of kind `state`: it " |
| 17 | + "carries the handoff holder, the open concepts (units of work, each with " |
| 18 | + "a legible id), and the known agents.\n\n" |
| 19 | + "To take part: (1) read `state` and `recent_events` below. (2) Use your " |
| 20 | + "stable agent identity. (3) If you hold the handoff, act on your open " |
| 21 | + "concept with full local autonomy, then POST your result to kind `comms` " |
| 22 | + "and POST a new `state` event passing the handoff on. (4) Post to kind " |
| 23 | + "`status` on a cadence while you work - silence reads as a dead agent. " |
| 24 | + "Full protocol: docs/channels-protocol.md in coilysiren/otel-a2a-relay." |
| 25 | +) |
| 26 | + |
| 27 | +_FORMAT_ALIASES = { |
| 28 | + "json": "json", |
| 29 | + "yaml": "yaml", |
| 30 | + "yml": "yaml", |
| 31 | + "markdown": "markdown", |
| 32 | + "md": "markdown", |
| 33 | +} |
| 34 | + |
| 35 | + |
| 36 | +def pick_format(explicit: str | None, accept: str) -> str: |
| 37 | + """Choose json / yaml / markdown from a ?format= override or the Accept header.""" |
| 38 | + if explicit: |
| 39 | + return _FORMAT_ALIASES.get(explicit.strip().lower(), "json") |
| 40 | + accept = accept.lower() |
| 41 | + if "yaml" in accept: |
| 42 | + return "yaml" |
| 43 | + if "markdown" in accept: |
| 44 | + return "markdown" |
| 45 | + return "json" |
| 46 | + |
| 47 | + |
| 48 | +def _md_scalar(value: typing.Any) -> str: |
| 49 | + if value is None: |
| 50 | + return "_(none)_" |
| 51 | + if isinstance(value, bool): |
| 52 | + return "true" if value else "false" |
| 53 | + return str(value).replace("\n", " ").strip() |
| 54 | + |
| 55 | + |
| 56 | +def _md_lines(value: typing.Any, indent: int = 0) -> list[str]: |
| 57 | + """Render arbitrary JSON-ish data as an indented markdown bullet list.""" |
| 58 | + pad = " " * indent |
| 59 | + lines: list[str] = [] |
| 60 | + if isinstance(value, dict): |
| 61 | + for key, val in value.items(): |
| 62 | + if isinstance(val, (dict, list)) and val: |
| 63 | + lines.append(f"{pad}- **{key}**:") |
| 64 | + lines.extend(_md_lines(val, indent + 1)) |
| 65 | + else: |
| 66 | + lines.append(f"{pad}- **{key}**: {_md_scalar(val)}") |
| 67 | + elif isinstance(value, list): |
| 68 | + for item in value: |
| 69 | + if isinstance(item, (dict, list)) and item: |
| 70 | + lines.append(f"{pad}-") |
| 71 | + lines.extend(_md_lines(item, indent + 1)) |
| 72 | + else: |
| 73 | + lines.append(f"{pad}- {_md_scalar(item)}") |
| 74 | + else: |
| 75 | + lines.append(f"{pad}- {_md_scalar(value)}") |
| 76 | + return lines |
| 77 | + |
| 78 | + |
| 79 | +def channel_markdown(data: dict[str, typing.Any]) -> str: |
| 80 | + """Render the onboarding view as a human-readable markdown document.""" |
| 81 | + ch = data["channel"] |
| 82 | + out: list[str] = [f"# Agent Channel {ch['id']}", ""] |
| 83 | + out.append(f"**{ch['title']}**" if ch.get("title") else "_(untitled channel)_") |
| 84 | + out += [ |
| 85 | + "", |
| 86 | + f"- created by `{ch.get('created_by') or '(unknown)'}`", |
| 87 | + f"- created at {ch['created_at']}", |
| 88 | + f"- status: {'closed at ' + ch['closed_at'] if ch.get('closed_at') else 'open'}", |
| 89 | + f"- url: {ch['url']}", |
| 90 | + "", |
| 91 | + "## Onboarding", |
| 92 | + "", |
| 93 | + str(data.get("onboarding", "")), |
| 94 | + "", |
| 95 | + "## How to take part", |
| 96 | + "", |
| 97 | + ] |
| 98 | + out += _md_lines(data.get("participate", {})) |
| 99 | + out += ["", "## Charter", ""] |
| 100 | + spec = data.get("spec") |
| 101 | + out += _md_lines(spec) if spec else ["_No spec event yet._"] |
| 102 | + out += ["", "## Current state", ""] |
| 103 | + state = data.get("state") |
| 104 | + out += _md_lines(state) if state else ["_No state event yet._"] |
| 105 | + out += ["", "## Recent events", ""] |
| 106 | + events = data.get("recent_events") or [] |
| 107 | + if not events: |
| 108 | + out.append("_No events yet._") |
| 109 | + for ev in events: |
| 110 | + author = ev.get("author") or "(no author)" |
| 111 | + out.append(f"### #{ev['id']} - {ev['kind']} - {author} - {ev['created_at']}") |
| 112 | + out += _md_lines(ev.get("payload", {})) |
| 113 | + out.append("") |
| 114 | + return "\n".join(out).rstrip() + "\n" |
0 commit comments