pms-infra-agent-socket 0.0.1.0 → 0.0.2.0
raw patch · 6 files changed
+170/−17 lines, 6 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- PMS.Infra.Agent.Socket.DS.Utility: hexToBytes :: String -> Either String ByteString
Files
- CHANGELOG.md +4/−0
- README.md +161/−0
- pms-infra-agent-socket.cabal +1/−1
- src/PMS/Infra/Agent/Socket/DS/Core.hs +1/−1
- src/PMS/Infra/Agent/Socket/DS/Utility.hs +0/−13
- test/PMS/Infra/Agent/Socket/App/ControlSpec.hs +3/−2
CHANGELOG.md view
@@ -1,5 +1,9 @@ # Revision history for pms-infra-agent-socket +## 0.0.2.0 -- 2026-06-15++* Added pms-infra-agent-server: TCP server listen/accept functionality for AI agents.+ ## 0.0.1.0 -- 2026-05-31 * First version.
README.md view
@@ -65,6 +65,134 @@ ## 💡 Usage Notes +### AI-to-AI communication++Any two AI agents that each have a `pty-mcp-server` instance can communicate with each other directly over TCP — no shared infrastructure, message broker, or custom integration required.++One agent uses the `agent-server-*` tools to act as a TCP server; the other uses the `agent-socket-*` tools (this library) to act as a TCP client. The agents exchange messages using a lightweight text protocol, and each agent interprets and responds to the other's messages autonomously.++```+┌─────────────────────────────┐ ┌─────────────────────────────┐+│ AI Agent A │ │ AI Agent B │+│ (e.g. Claude on Machine A) │ │ (e.g. GPT on Machine B) │+│ │ │ │+│ pty-mcp-server │ TCP │ pty-mcp-server │+│ agent-server-listen :19999 │◄───────►│ agent-socket-open │+│ agent-server-write │ │ agent-socket-read │+│ agent-server-events │ │ agent-socket-write │+└─────────────────────────────┘ └─────────────────────────────┘+ Server role Client role+```++#### What this enables++- **Collaborative task solving** — Agent A breaks down a problem and delegates subtasks to Agent B, collecting results over the socket.+- **Cross-model review** — One model generates code or text; another model on a different machine reviews it and sends back comments.+- **Heterogeneous agent pipelines** — Chain agents of different models or specialisations (Claude, GPT, Gemini, …) into a processing pipeline across machines.+- **Autonomous negotiation** — Agents can exchange proposals, counter-proposals, and decisions without human involvement in each round-trip.+- **Distributed tool use** — Agent B may have access to tools (databases, sensors, local files) that Agent A does not. Agent A requests operations from Agent B over the socket.++#### Communication protocol++The `pty-mcp-server` ecosystem ships a lightweight handshake and messaging protocol for AI-to-AI sessions. Prompt skills (`skill_agent_server.md` / `skill_agent_client.md`) are provided so each agent knows exactly how to play its role.++**Handshake sequence:**+```+Server → Client : HELLO? name?\r\n+Server ← Client : NAME: <name>\r\n+Server → Client : RULES: MSG:<content>\r\n | REPLY:<content>\r\n | BYE\r\n | HEX:<hex>\r\n+Server ← Client : ACK\r\n+```++**Conversation:**+```+Server → Client : MSG: <content>\r\n+Server ← Client : REPLY: <content>\r\n+```++**Graceful shutdown (server-initiated):**+```+Server → Client : BYE\r\n+Server ← Client : ACK\r\n ← server waits for this before closing+Server : agent-server-close+```++#### Full session example++The following shows a complete exchange where Agent A (server role) delegates a task to Agent B (client role).++**Agent A — server side**+```+agent-server-listen host=172.16.0.43 port=19999+ → "listening."++--- Agent B connects ---++agent-server-events+ → [{ "tag": "ClientConnected" }]++agent-server-write-byte "48454C4C4F3F206E616D653F0D0A" ("HELLO? name?\r\n")++agent-server-events (poll until BytesReceived)+ → bytes: "4E414D453A20416765742D420D0A" ("NAME: Agent-B\r\n")++agent-server-write-byte "<hex of RULES: MSG:... | REPLY:... | BYE\r\n>"++agent-server-events (poll for ACK)+ → bytes: "41434B0D0A" ("ACK\r\n")++--- handshake complete ---++agent-server-write-byte "<hex of MSG: Please summarise this text: ...\r\n>"++agent-server-events (poll for REPLY)+ → bytes: "<hex of REPLY: Here is the summary: ...\r\n>"++agent-server-write-byte "<hex of BYE\r\n>"++agent-server-events (poll for ACK)+ → bytes: "41434B0D0A" ("ACK\r\n")++agent-server-close+```++**Agent B — client side**+```+agent-socket-open host=172.16.0.43 port=19999+ → socket connected to 172.16.0.43:19999++agent-socket-read length=256+ → "HELLO? name?\r\n"++agent-socket-write-byte "<hex of NAME: Agent-B\r\n>"++agent-socket-read length=256+ → "RULES: MSG:<content>\r\n | REPLY:<content>\r\n | BYE\r\n | HEX:<hex>\r\n"++agent-socket-write-byte "<hex of ACK\r\n>"++--- handshake complete ---++agent-socket-read length=1024+ → "MSG: Please summarise this text: ...\r\n"++--- Agent B processes the request autonomously ---++agent-socket-write-byte "<hex of REPLY: Here is the summary: ...\r\n>"++agent-socket-read length=256+ → "BYE\r\n"++agent-socket-write-byte "<hex of ACK\r\n>"++agent-socket-close+```++> 💡 Always use `agent-socket-write-byte` (not `agent-socket-write`) to ensure `\r\n` is sent as correct CRLF bytes. +> Generate hex strings with: `python3 -c "print('your message\r\n'.encode().hex())"`++---+ ### IAC / Telnet negotiation Telnet IAC processing is the **responsibility of the agent**, not the library. Use `agent-socket-read-byte` and `agent-socket-write-byte` to handle IAC sequences at the byte level.@@ -78,6 +206,39 @@ ← "localhost login: " (Login prompt — read as UTF-8 string) → "phoityne\r\n" (Username — send as bytes: 70686F6974796E650D0A) ← "Password: " (Password prompt)+```++Full login session example (confirmed with a real Linux Telnet server):+```+agent-socket-open host=172.16.0.171 port=23+ → socket connected to 172.16.0.171:23++← FFFD18FFFD20FFFD23FFFD27+ (IAC DO TERMINAL-TYPE / DO TERMINAL-SPEED / DO X-DISPLAY / DO NEW-ENVIRON)+→ FFFC18FFFC20FFFC23FFFC27+ (IAC WONT x4)++← FFFB03FFFD01FFFD1FFFFB05FFFD21+ (IAC WILL SGA / DO ECHO / DO NAWS / WILL STATUS / DO REMOTE-FLOW-CONTROL)+→ FFFD03FFFB01FFFC1FFFFD05FFFC21+ (IAC DO SGA / WILL ECHO / WONT NAWS / DO STATUS / WONT REMOTE-FLOW-CONTROL)++← FFFE01FFFB01 + "Kernel 6.12.0-...\r\nlocalhost login: "+→ 61692D6167656E740D0A ("ai-agent\r\n" — username as bytes)++← "ai-agent\r\nPassword: "+→ 61692D6167656E740D0A ("ai-agent\r\n" — password as bytes)++← "Last login: ...\r\n[ai-agent@localhost ~]$ "++→ 686F73746E616D650D0A ("hostname\r\n")+← "hostname\r\nlocalhost.localdomain\r\n[ai-agent@localhost ~]$ "++→ 657869740D0A ("exit\r\n")+← "exit\r\nlogout\r\n"++agent-socket-close+ → socket is closed. ``` ### Unix Domain Socket on Windows
pms-infra-agent-socket.cabal view
@@ -20,7 +20,7 @@ -- PVP summary: +-+------- breaking API changes -- | | +----- non-breaking API additions -- | | | +--- code changes with no API change-version: 0.0.1.0+version: 0.0.2.0 -- A short (one-line) description of the package. synopsis: pms-infra-agent-socket
src/PMS/Infra/Agent/Socket/DS/Core.hs view
@@ -374,7 +374,7 @@ handleTMVar <- view handleAppData <$> ask argsDat <- liftEither $ eitherDecode argsBS let hex = argsDat^.dataSocketWriteByteToolParams- bs <- liftEither $ hexToBytes hex+ bs <- liftEither $ DM.hexToBytes hex return $ socketWriteByteTask cmdData resQ handleTMVar bs -- |
src/PMS/Infra/Agent/Socket/DS/Utility.hs view
@@ -12,10 +12,6 @@ import Control.Monad.Reader import qualified Control.Concurrent.STM as STM import Network.Socket-import qualified Data.ByteString as BS-import qualified Data.ByteString.Base16 as B16-import qualified Data.ByteString.Char8 as C8-import Data.Char (toUpper) import qualified PMS.Domain.Model.DM.Type as DM import qualified PMS.Domain.Model.DS.Utility as DM@@ -58,15 +54,6 @@ liftIOE $ STM.atomically $ STM.writeTQueue resQ res ------------------------------------------------------------------------------------ | Decode an uppercase/lowercase hex string to a ByteString.--- Input is uppercased before decoding to tolerate mixed case (e.g. "000a1BFF").--- Returns Left with an error message if the input is not valid hex.-hexToBytes :: String -> Either String BS.ByteString-hexToBytes hex =- case B16.decode (C8.pack (map toUpper hex)) of- Right bs -> Right bs- Left err -> Left $ "hexToBytes: invalid hex string: " ++ err- -- | Convert a TCP host/port to a Handle. -- The socket must not be used directly after socketToHandle. createSocketHandle :: HostName -> ServiceName -> IO Handle
test/PMS/Infra/Agent/Socket/App/ControlSpec.hs view
@@ -20,6 +20,7 @@ import qualified System.Log.FastLogger as FastLogger import qualified PMS.Domain.Model.DM.Type as DM+import qualified PMS.Domain.Model.DS.Utility as DS import qualified PMS.Infra.Agent.Socket.App.Control as SUT import qualified PMS.Infra.Agent.Socket.DM.Type as SUT import qualified PMS.Infra.Agent.Socket.DS.Core as Core@@ -250,7 +251,7 @@ { DM._jsonrpcAgentSocketWriteByteCommandData = mkJsonRpc "agent-socket-write-byte" , DM._argumentsAgentSocketWriteByteCommandData = DM.RawJsonByteString "" }- Right bs <- return $ Util.hexToBytes hex+ Right bs <- return $ DS.hexToBytes hex withConnectedPair $ \clientHdl serverSock -> do _ <- STM.atomically $ STM.swapTMVar (appDat^.SUT.handleAppData) (Just clientHdl) Core.socketWriteByteTask cmdDat@@ -273,7 +274,7 @@ , DM._argumentsAgentSocketWriteByteCommandData = DM.RawJsonByteString "" } mapM_ (\hex -> do- Right bs <- return $ Util.hexToBytes hex+ Right bs <- return $ DS.hexToBytes hex appDat' <- SUT.defaultAppData withConnectedPair $ \clientHdl serverSock -> do _ <- STM.atomically $ STM.swapTMVar (appDat'^.SUT.handleAppData) (Just clientHdl)