@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 +12 -2
- package/dist/index.d.ts +14 -5
- package/dist/index.js +18 -7
- package/dist/native.d.ts +8 -0
- package/dist/native.js +11 -1
- package/dist/session.d.ts +2 -2
- package/dist/session.js +4 -4
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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
|
|
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 = "
|
|
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
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
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
|
|
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 = "
|
|
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:
|
|
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
|
|
49
|
-
*
|
|
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 {
|
|
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
|
|
15
|
-
*
|
|
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:
|
|
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("");
|