@relaymessenger/pi 0.1.12-staging.0 → 0.1.12-staging.1

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.
package/README.md CHANGED
@@ -102,8 +102,18 @@ not also run session mode on the same Agent Token.
102
102
 
103
103
  ## Parts, silence and tools
104
104
 
105
+ Relay describes itself once, in the system prompt, the way Hermes and
106
+ OpenClaw give each platform a short hint: what renders in a Relay chat, that
107
+ the final answer is sent to the chat, and that the `message` tool can text,
108
+ react or send a file, images included, at any time. `runPiChannel` passes it
109
+ with `--append-system-prompt`; session mode adds it as a `relay` section from
110
+ `before_agent_start` while Relay is attached. Each Message reaches Pi as its
111
+ own words only: the text, the Message it replies to, and its Relay message id.
112
+ The pi-channels adapter adds no hint: pi-channels answers in its own
113
+ subprocesses, started with `--no-extensions`.
114
+
105
115
  Pi's final text carries Relay's parts as fenced blocks, read by the SDK's
106
- `answerMessages`, and every prompt teaches them: `buttons`, `selection`,
116
+ `answerMessages`, and the hint teaches them: `buttons`, `selection`,
107
117
  `form`, `rich_card`, `carousel` (2 to 10 cards), `place`, `payment` and
108
118
  `rating_request`, plus a URL alone on a line for a link card. A final answer
109
119
  with no words is Pi choosing to stay silent: nothing is sent, in the channel
@@ -130,7 +140,7 @@ aliases of `message` file and react.
130
140
  `RELAY_AGENT_TOKEN` and `RELAY_BASE_URL` set, so that Pi gets the tools; in
131
141
  session mode they act on the chat whose Message is being answered.
132
142
 
133
- Each prompt ends with `[Relay message id: <id>]`. An answer that contains
143
+ Each Message ends with `[Relay message id: <id>]`. An answer that contains
134
144
  `[[reply_to_current]]` is sent as a reply to that Message, and one with
135
145
  `[[reply_to:<id>]]` as a reply to the Message named; the tag is removed from
136
146
  the words (the reply tags of OpenClaw's rich output protocol). Pi's typing
package/dist/index.d.ts CHANGED
@@ -92,18 +92,27 @@ export declare const piEnv: (options: Pick<PiChannelOptions, "agentToken" | "bas
92
92
  */
93
93
  export declare const wordsOf: (data: MessageWebhookData) => string;
94
94
  /**
95
- * The prompt pi is given for one message: the words, then how to answer.
96
- * This process sends pi's final text for it, and the same buttons and link
97
- * rules every other runtime carries.
95
+ * What Relay is and what renders there, given once in the system prompt as a
96
+ * platform hint (Hermes PLATFORM_HINTS, OpenClaw inboundFormattingHints), so
97
+ * each Message reaches Pi as the person's own words.
98
98
  */
99
- export declare const piPrompt: (message: string) => string;
99
+ export declare const RELAY_HINT: string;
100
+ /** The system prompt section the hint goes in: Pi wraps it as `<relay>…</relay>`. */
101
+ export declare const RELAY_HINT_SECTION = "relay";
100
102
  /**
101
103
  * The Relay tools the Pi extension registers when Relay starts it for a chat
102
104
  * (`message`, `relay_request_location`, `relay_read_location`), and the reply tags OpenClaw's channels read
103
105
  * (docs/reference/rich-output-protocol.md: `[[reply_to_current]]`,
104
106
  * `[[reply_to:<id>]]`).
105
107
  */
106
- export declare const RELAY_TOOLS_LINE = " In a one-to-one chat, relay_request_location asks the person to share their location and relay_read_location reads where everyone sharing is now. message texts the person now (action send), reacts to a Message with an emoji (react), or sends a file from this machine (file). Each Message names its Relay message id; to thread your answer to a Message, put [[reply_to_current]] (the Message you are answering) or [[reply_to:<id>]] in it: the tag is removed and not shown.";
108
+ export declare const RELAY_TOOLS_LINE = "In a one-to-one chat, relay_request_location asks the person to share their location and relay_read_location reads where everyone sharing is now. message texts the person now (action send), reacts to a Message with an emoji (react), or sends a file from this machine (file). Each Message names its Relay message id; to thread your answer to a Message, put [[reply_to_current]] (the Message you are answering) or [[reply_to:<id>]] in it: the tag is removed and not shown.";
109
+ /** The whole hint: what renders, then the Relay tools. */
110
+ export declare const relayHint: () => string;
111
+ /**
112
+ * One inbound Message as Pi reads it: the person's words, the line naming
113
+ * the Message it replies to, and its Relay message id.
114
+ */
115
+ export declare const inboundText: (words: string, replyLine: string, data: Pick<MessageWebhookData, "id">) => string;
107
116
  /** The id of the Message being answered, as one line of data the Relay tools and reply tags can name. */
108
117
  export declare const messageIdLine: (data: Pick<MessageWebhookData, "id">) => string;
109
118
  /**
package/dist/index.js CHANGED
@@ -73,18 +73,29 @@ const textFromEvent = (event) => {
73
73
  return wordsOf(event.data) || null;
74
74
  };
75
75
  /**
76
- * The prompt pi is given for one message: the words, then how to answer.
77
- * This process sends pi's final text for it, and the same buttons and link
78
- * rules every other runtime carries.
76
+ * What Relay is and what renders there, given once in the system prompt as a
77
+ * platform hint (Hermes PLATFORM_HINTS, OpenClaw inboundFormattingHints), so
78
+ * each Message reaches Pi as the person's own words.
79
79
  */
80
- export const piPrompt = (message) => `${message}\n\nWrite your answer as your final message: Relay sends that answer to the chat for you. Or text the person yourself with the message tool, then end with no text so nothing is sent twice. To stay silent, end with no text: nothing is sent. Write chat text. Inline Markdown draws: bold, italic, strikethrough, code, links. Headings, lists and code fences show as written.\n\n${BUTTONS_BLOCK_INSTRUCTION} ${LINK_LINE_INSTRUCTION} ${BUTTONS_GUIDANCE} ${SELECTION_BLOCK_INSTRUCTION} ${SELECTION_GUIDANCE} ${FORM_BLOCK_INSTRUCTION} ${FORM_GUIDANCE} ${CARD_BLOCK_INSTRUCTION} ${CARD_GUIDANCE} ${PLACE_BLOCK_INSTRUCTION} ${PAYMENT_BLOCK_INSTRUCTION} ${PAYMENT_GUIDANCE} ${RATING_REQUEST_BLOCK_INSTRUCTION} ${RATING_REQUEST_GUIDANCE}${RELAY_TOOLS_LINE}`;
80
+ export const RELAY_HINT = `You are in a Relay chat, a messaging app on the person's phone. Your final message is sent to the chat as your answer, and a final message with no text sends nothing. The message tool can text the person, react to a Message with an emoji, or send a file from this machine, images included, at any time; a run that texted with it sends no final message, so nothing is sent twice. Inline Markdown renders: bold, italic, strikethrough, code, links. Headings, lists and code fences show as written.
81
+
82
+ ${BUTTONS_BLOCK_INSTRUCTION} ${LINK_LINE_INSTRUCTION} ${BUTTONS_GUIDANCE} ${SELECTION_BLOCK_INSTRUCTION} ${SELECTION_GUIDANCE} ${FORM_BLOCK_INSTRUCTION} ${FORM_GUIDANCE} ${CARD_BLOCK_INSTRUCTION} ${CARD_GUIDANCE} ${PLACE_BLOCK_INSTRUCTION} ${PAYMENT_BLOCK_INSTRUCTION} ${PAYMENT_GUIDANCE} ${RATING_REQUEST_BLOCK_INSTRUCTION} ${RATING_REQUEST_GUIDANCE}`;
83
+ /** The system prompt section the hint goes in: Pi wraps it as `<relay>…</relay>`. */
84
+ export const RELAY_HINT_SECTION = "relay";
81
85
  /**
82
86
  * The Relay tools the Pi extension registers when Relay starts it for a chat
83
87
  * (`message`, `relay_request_location`, `relay_read_location`), and the reply tags OpenClaw's channels read
84
88
  * (docs/reference/rich-output-protocol.md: `[[reply_to_current]]`,
85
89
  * `[[reply_to:<id>]]`).
86
90
  */
87
- export const RELAY_TOOLS_LINE = " In a one-to-one chat, relay_request_location asks the person to share their location and relay_read_location reads where everyone sharing is now. message texts the person now (action send), reacts to a Message with an emoji (react), or sends a file from this machine (file). Each Message names its Relay message id; to thread your answer to a Message, put [[reply_to_current]] (the Message you are answering) or [[reply_to:<id>]] in it: the tag is removed and not shown.";
91
+ export const RELAY_TOOLS_LINE = "In a one-to-one chat, relay_request_location asks the person to share their location and relay_read_location reads where everyone sharing is now. message texts the person now (action send), reacts to a Message with an emoji (react), or sends a file from this machine (file). Each Message names its Relay message id; to thread your answer to a Message, put [[reply_to_current]] (the Message you are answering) or [[reply_to:<id>]] in it: the tag is removed and not shown.";
92
+ /** The whole hint: what renders, then the Relay tools. */
93
+ export const relayHint = () => `${RELAY_HINT}\n\n${RELAY_TOOLS_LINE}`;
94
+ /**
95
+ * One inbound Message as Pi reads it: the person's words, the line naming
96
+ * the Message it replies to, and its Relay message id.
97
+ */
98
+ export const inboundText = (words, replyLine, data) => [words, replyLine, messageIdLine(data)].filter(Boolean).join("\n\n");
88
99
  /** The id of the Message being answered, as one line of data the Relay tools and reply tags can name. */
89
100
  export const messageIdLine = (data) => data.id ? `[Relay message id: ${data.id}]` : "";
90
101
  const REPLY_TAG = /\[\[\s*reply_to(?:_current|\s*:\s*([^\]\s]+))\s*\]\]/gu;
@@ -276,7 +287,7 @@ export class PiChannel {
276
287
  const data = event.data;
277
288
  let session = this.#sessions.get(data.chat.id);
278
289
  if (!session) {
279
- session = new ChatSession(this.#spawnPi(this.#options.piCommand ?? "pi", ["--mode", "rpc", ...(this.#options.piArgs ?? [])], data.chat.id), this.#options.approvals);
290
+ session = new ChatSession(this.#spawnPi(this.#options.piCommand ?? "pi", ["--mode", "rpc", "--append-system-prompt", relayHint(), ...(this.#options.piArgs ?? [])], data.chat.id), this.#options.approvals);
280
291
  this.#sessions.set(data.chat.id, session);
281
292
  }
282
293
  const timeout = this.#options.rpcTimeoutMs ?? 60_000;
@@ -285,7 +296,7 @@ export class PiChannel {
285
296
  await typing(this.#relay, data.chat.id, true);
286
297
  session.texted = false;
287
298
  try {
288
- await session.command("prompt", { message: piPrompt([message, replyLine, messageIdLine(data)].filter(Boolean).join("\n\n")) }, timeout, signal);
299
+ await session.command("prompt", { message: inboundText(message, replyLine, data) }, timeout, signal);
289
300
  if (!session.settled) {
290
301
  while (!session.settled)
291
302
  await session.read(timeout, signal);
package/dist/native.d.ts CHANGED
@@ -27,6 +27,14 @@ export declare const relaySettings: (dir?: string) => Promise<RelaySettings>;
27
27
  export declare const piChannelsRelaySettings: (cwd: string, dir?: string) => Promise<RelaySettings | undefined>;
28
28
  /** RELAY_AGENT_TOKEN, else what `agentTokenCommand` prints; never logged. */
29
29
  export declare const agentToken: (settings: RelaySettings) => Promise<string | undefined>;
30
+ /**
31
+ * Puts the Relay hint in the system prompt as its own section, which Pi
32
+ * sends once and patches only when it changes (docs/extensions.md,
33
+ * before_agent_start).
34
+ */
35
+ export declare const addRelayHint: (options: {
36
+ sections: Record<string, string>;
37
+ }) => void;
30
38
  /**
31
39
  * Pi-native entry point. By default it exposes only lifecycle-safe commands,
32
40
  * and Relay ingress is owned by runPiChannel so the CLI and extension share
package/dist/native.js CHANGED
@@ -3,7 +3,7 @@ import { readFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import Relay from "@relaymessenger/sdk";
6
- import { RELAY_CHAT_ID_ENV, runPiChannel } from "./index.js";
6
+ import { RELAY_CHAT_ID_ENV, RELAY_HINT_SECTION, relayHint, runPiChannel } from "./index.js";
7
7
  import { relayTools } from "./tools.js";
8
8
  import { registerRelayAdapter, relayChannelAdapter, RELAY_ADAPTER } from "./pi-channels.js";
9
9
  import { SessionChannel } from "./session.js";
@@ -93,6 +93,8 @@ const attachSession = (pi) => {
93
93
  console.error(`Relay: the session channel stopped: ${error instanceof Error ? error.message : String(error)}`);
94
94
  });
95
95
  });
96
+ pi.on("before_agent_start", async (event) => { if (channel)
97
+ addRelayHint(event.systemPromptOptions); });
96
98
  pi.on("agent_end", async (event) => { channel?.ended(event.messages); });
97
99
  pi.on("agent_settled", async () => { await channel?.settled(); });
98
100
  pi.on("session_shutdown", async () => {
@@ -129,6 +131,14 @@ const attachPiChannels = (pi) => {
129
131
  }), RELAY_ADAPTER);
130
132
  });
131
133
  };
134
+ /**
135
+ * Puts the Relay hint in the system prompt as its own section, which Pi
136
+ * sends once and patches only when it changes (docs/extensions.md,
137
+ * before_agent_start).
138
+ */
139
+ export const addRelayHint = (options) => {
140
+ options.sections[RELAY_HINT_SECTION] = relayHint();
141
+ };
132
142
  /** The Relay tools, as Pi registers a tool (docs/extensions.md, registerTool). */
133
143
  const registerRelayTools = (pi, relay, chatId, messageId) => {
134
144
  for (const tool of relayTools(relay, chatId, messageId))
package/dist/session.d.ts CHANGED
@@ -45,8 +45,8 @@ export interface SessionMedia {
45
45
  readonly transcribe?: Transcribe;
46
46
  }
47
47
  /**
48
- * One Message as the session's user message: its words with the same answer
49
- * rules the channel gives Pi, each photo as image content, each voice note as
48
+ * One Message as the session's user message: its words and Relay message id,
49
+ * each photo as image content, each voice note as
50
50
  * its transcript, and any other file named. Null for a Message with nothing.
51
51
  */
52
52
  export declare const sessionContent: (data: MessageWebhookData, media: SessionMedia, replyLine?: string) => Promise<SessionContent | null>;
package/dist/session.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, writeFileSync } from "node:fs";
2
2
  import Relay, {} from "@relaymessenger/sdk";
3
- import { messageIdLine, piPrompt, repliedContext, replyTag, sendAnswer, sendToChat, typing, wordsOf } from "./index.js";
3
+ import { inboundText, repliedContext, replyTag, sendAnswer, sendToChat, typing, wordsOf } from "./index.js";
4
4
  import { MESSAGE_TOOL } from "./tools.js";
5
5
  /**
6
6
  * The line sent when a run fails before it has words, so the person is not
@@ -11,8 +11,8 @@ export const NO_ANSWER = "Sorry, something went wrong on my side.";
11
11
  /** The image types a model takes as image content; any other file is named in words. */
12
12
  const IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
13
13
  /**
14
- * One Message as the session's user message: its words with the same answer
15
- * rules the channel gives Pi, each photo as image content, each voice note as
14
+ * One Message as the session's user message: its words and Relay message id,
15
+ * each photo as image content, each voice note as
16
16
  * its transcript, and any other file named. Null for a Message with nothing.
17
17
  */
18
18
  export const sessionContent = async (data, media, replyLine = "") => {
@@ -40,7 +40,7 @@ export const sessionContent = async (data, media, replyLine = "") => {
40
40
  const words = [wordsOf(data), lines.join("\n"), replyLine].filter(Boolean).join("\n\n");
41
41
  if (!words)
42
42
  return null;
43
- return [{ type: "text", text: piPrompt([words, messageIdLine(data)].filter(Boolean).join("\n\n")) }, ...images];
43
+ return [{ type: "text", text: inboundText(words, "", data) }, ...images];
44
44
  };
45
45
  const blocksOf = (message) => Array.isArray(message.content) ? message.content : [];
46
46
  const textOf = (message) => typeof message.content === "string" ? message.content : blocksOf(message).flatMap((block) => block?.type === "text" && block.text ? [block.text] : []).join("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relaymessenger/pi",
3
- "version": "0.1.12-staging.0",
3
+ "version": "0.1.12-staging.1",
4
4
  "description": "Relay Messenger channel for Pi over Relay v1 WebSocket delivery.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",