1"""Exercise unchanged Hermes over the native Tiny connection."""23import base644import hashlib5import json6import time7import uuid8from urllib.parse import urlsplit910from rpc import Client, answer, public, tag, wait_event111213def check(condition, message):14 if not condition:15 raise AssertionError(message)161718def send(client, text, extra=None, kind=9):19 return client.publish(kind, text, [["h", "lab"], ["p", public("agent")], *(extra or [])])202122def done(client, marker, since):23 event = wait_event(client, {"kinds": [9, 12, 40003], "authors": [public("agent")],24 "#h": ["lab"], "since": since},25 lambda row: f"LAB_DONE {marker}" in row["content"])26 check(not any(row[0] in ("p", "mention") for row in event["tags"]),27 "Ordinary agent output unexpectedly mentions someone")28 return event293031def interaction(client, scenario, run):32 model_scenario = {"DETAILS": "QUESTION", "MULTI_DETAILS": "MULTI", "THREAD": "QUESTION", "ROOM_THREAD": "QUESTION"}.get(scenario, scenario)33 marker = f"LAB_{model_scenario}_{scenario.lower()}_{run}"34 threaded = scenario in ("THREAD", "ROOM_THREAD")35 if threaded:36 root_marker = f"LAB_HELLO_{scenario.lower()}_{run}"37 root = send(client, root_marker, kind=11 if scenario == "THREAD" else 9)38 parent = done(client, root_marker, root["created_at"])39 parent_id = tag(parent, "e") if parent["kind"] == 40003 else parent["id"]40 sent = send(client, marker, [["e", root["id"], "", "root"], ["e", parent_id, "", "reply"]],41 kind=12 if scenario == "THREAD" else 9)42 else:43 sent = send(client, marker)44 root = sent45 request = wait_event(client, {"kinds": [9, 12], "authors": [public("agent")], "#h": ["lab"],46 "#request": ["question"], "since": sent["created_at"]},47 lambda row: marker in row["content"] or (scenario == "APPROVAL" and row["created_at"] >= sent["created_at"]))48 check(request["kind"] == sent["kind"], "Request changed the conversation type")49 check(["e", root["id"], "", "root"] in request["tags"], "Request started a new thread")50 if threaded:51 check(["e", sent["id"], "", "reply"] in request["tags"], "Request lost its direct reply parent")52 check(tag(request, "tinyagent") == "1", "Request lost its native interaction marker")53 check(tag(request, "p") == public("owner"), "Request does not name its human assignee")54 check(tag(request, "mention") is None, "Assignee was turned into an unsolicited mention")55 check(int(tag(request, "expiration") or 0) > time.time(), "Request has no future expiry")56 options = {row[1]: row[2] for row in request["tags"] if len(row) >= 3 and row[0] == "option"}57 check(bool(options) or scenario == "OPEN", "Options were flattened into text")58 if scenario == "OPEN":59 check(tag(request, "selection") == "text", "Open question lost its text input")60 choice = "Custom lab answer"61 elif scenario == "APPROVAL":62 check(tag(request, "interaction") == "approval", "Approval lost its type")63 choice = next(key for key in options if key == "once")64 else:65 check(tag(request, "interaction") == "question", "Question lost its type")66 choice = next(key for key, label in options.items() if label.startswith("Continue"))67 if scenario in ("MULTI", "MULTI_DETAILS"):68 check(tag(request, "selection") == "multiple", "Multiple choice became single choice")69 choice = json.dumps([key for key, label in options.items() if label.startswith(("Continue", "Stop"))])70 if scenario == "CUSTOM":71 check(tag(request, "freeform") == "true", "Choice question lost its custom input")72 choice = json.dumps({"text": "Custom lab answer"})73 if scenario in ("DETAILS", "MULTI_DETAILS", "THREAD", "ROOM_THREAD"):74 selected = list(options) if scenario == "MULTI_DETAILS" else [choice]75 choice = json.dumps({"choices": selected, "text": "Keep the answer in this thread."})76 # A signed response from another room member must not resolve the operator's request.77 with Client("observer") as observer:78 answer(observer, request, choice)79 answer(client, request, "" if scenario == "OPEN" else "option-that-was-not-offered")80 time.sleep(0.5)81 state = client.browse("browseapproval", {"id": request["id"]})82 check(state["item"]["state"] == "open", "An unauthorized or invalid answer settled the card")83 answer(client, request, choice)84 final = done(client, marker, sent["created_at"])85 expected = "tinyagent-lab" if scenario == "APPROVAL" else "Custom lab answer" if scenario in ("OPEN", "CUSTOM") else "Continue"86 check(expected in final["content"], "Hermes did not receive the selected response")87 if scenario in ("MULTI", "MULTI_DETAILS"):88 check("Stop" in final["content"], "Hermes lost a selected choice")89 if scenario in ("DETAILS", "MULTI_DETAILS", "THREAD", "ROOM_THREAD"):90 check("Keep the answer in this thread." in final["content"], "Hermes lost the explanation")91 final_message = client.query({"ids": [tag(final, "e")], "limit": 1})[0] if final["kind"] == 40003 else final92 check(["e", root["id"], "", "root"] in final_message["tags"], "Hermes resumed outside the original thread")93 state = client.browse("browseapproval", {"id": request["id"]})94 check(state["item"]["state"] == "answered", "The valid answer did not settle the card")95 print(f"PASS {scenario.lower()}: native card, silent assignment, authorized answer, Hermes resumed")969798def media(client, run):99 marker = f"LAB_MEDIA_{run}"100 content = f"tinyagent-attachment-{run}\n".encode()101 descriptor = client.call("upload", {"room": "lab", "filename": "lab-note.txt", "type": "text/plain",102 "data": base64.b64encode(content).decode()})103 digest = hashlib.sha256(content).hexdigest()104 check(descriptor["sha256"] == digest, "Uploaded attachment hash differs")105 path = urlsplit(descriptor["url"]).path106 downloaded = client.call("download", {"path": path})107 check(base64.b64decode(downloaded["data"]) == content, "Authenticated download differs")108 sent = send(client, marker, [["imeta", f"url {descriptor['url']}", "m text/plain", f"x {digest}",109 f"size {len(content)}", "filename lab-note.txt"]])110 final = done(client, marker, sent["created_at"])111 check(f"tinyagent-attachment-{run}" in final["content"], "Attachment text did not reach Hermes")112 returned = wait_event(client, {"kinds": [9], "authors": [public("agent")], "#h": ["lab"],113 "since": sent["created_at"]},114 lambda row: any(t[0] == "imeta" and f"x {digest}" in t for t in row["tags"]))115 attachment = next(t for t in returned["tags"] if t[0] == "imeta")116 url = next(t[4:] for t in attachment if t.startswith("url "))117 check(base64.b64decode(client.call("download", {"path": urlsplit(url).path})["data"]) == content,118 "Hermes outbound attachment differs")119 print("PASS media: authenticated upload, Hermes read_file, native outbound attachment, matching bytes")120121122def main():123 run = uuid.uuid4().hex[:12]124 with Client() as client:125 marker = f"LAB_HELLO_{run}"126 sent = send(client, marker)127 done(client, marker, sent["created_at"])128 print("PASS connection: real Hermes replied through Tiny without a mention")129 for scenario in ("QUESTION", "MULTI", "CUSTOM", "OPEN", "APPROVAL", "DETAILS", "MULTI_DETAILS", "THREAD", "ROOM_THREAD"):130 interaction(client, scenario, run)131 media(client, run)132 marker = f"LAB_MENTION_{run}"133 sent = send(client, f"{marker} @{public('owner')}")134 mentioned = wait_event(client, {"kinds": [9, 40003], "authors": [public("agent")],135 "#h": ["lab"], "#p": [public("owner")], "since": sent["created_at"]},136 lambda row: f"LAB_DONE {marker}" in row["content"])137 check(tag(mentioned, "mention") == public("owner"), "Explicit agent mention was not preserved")138 print("PASS mention: explicit model mention becomes a native recipient")139 print(json.dumps({"ok": True, "run": run, "connection": "tiny", "connector": "tinyagent"}))140141142if __name__ == "__main__":143 main()