@cryptolibertus/pi-peer 0.3.2

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.
@@ -0,0 +1,240 @@
1
+ import { appendPeerGoalEvent } from "./goal-board.mjs";
2
+ import { normalizePeerMessageResponseBody, redactPeerAuditValue } from "./protocol.mjs";
3
+ import { renderPeerCommunicationGuidance } from "./guidance.mjs";
4
+
5
+ export const PI_PEER_INBOUND_CUSTOM_TYPE = "pi-peer-inbound";
6
+
7
+ export function createInboundPromptBridge(options = {}) {
8
+ const pi = options.pi;
9
+ const responseTimeoutMs = Number.isInteger(options.responseTimeoutMs) ? options.responseTimeoutMs : 30 * 60 * 1000;
10
+ const queue = [];
11
+ let activeEntry;
12
+
13
+ function activateNext() {
14
+ if (activeEntry || !queue.length) return;
15
+
16
+ const entry = queue.shift();
17
+ activeEntry = entry;
18
+ entry.context?.markActive?.();
19
+ startActiveTimer(entry);
20
+
21
+ try {
22
+ pi.sendMessage({
23
+ customType: PI_PEER_INBOUND_CUSTOM_TYPE,
24
+ content: renderInboundPeerPrompt(entry.envelope, { responderProfile: options.responderProfile, homeDir: options.homeDir }),
25
+ display: true,
26
+ envelope: summarizeEnvelope(entry.envelope),
27
+ }, { deliverAs: "followUp", triggerTurn: true });
28
+ } catch (error) {
29
+ activeEntry = undefined;
30
+ settleEntry(entry, { status: "ERROR", summary: error?.message || String(error) });
31
+ activateNext();
32
+ }
33
+ }
34
+
35
+ function startActiveTimer(entry) {
36
+ entry.timer = setTimeout(() => {
37
+ if (activeEntry !== entry || entry.settled) return;
38
+ activeEntry = undefined;
39
+ settleEntry(entry, { status: "ERROR", summary: `Timed out waiting for agent_end response for peer message '${entry.envelope.id}'` }, { clearTimer: false });
40
+ activateNext();
41
+ }, responseTimeoutMs);
42
+ }
43
+
44
+ return {
45
+ async handleEnvelope(envelope, context = {}) {
46
+ if (!pi || typeof pi.sendMessage !== "function") {
47
+ return { status: "ERROR", summary: "Inbound peer prompt bridge is not connected to pi.sendMessage" };
48
+ }
49
+
50
+ return new Promise((resolve) => {
51
+ if (activeEntry) context.markQueued?.();
52
+ const entry = {
53
+ envelope,
54
+ messageId: envelope.id,
55
+ conversationId: envelope.conversationId,
56
+ context,
57
+ resolve,
58
+ timer: undefined,
59
+ settled: false,
60
+ };
61
+ queue.push(entry);
62
+ activateNext();
63
+ });
64
+ },
65
+
66
+ handleAgentEnd(event) {
67
+ const entry = activeEntry;
68
+ if (!entry) return false;
69
+ activeEntry = undefined;
70
+
71
+ if (!entry.settled) {
72
+ const finalAssistantMessage = extractFinalAssistantText(event);
73
+ settleEntry(entry, normalizePeerMessageResponseBody({
74
+ status: finalAssistantMessage ? "OK" : "ERROR",
75
+ finalAssistantMessage,
76
+ summary: finalAssistantMessage ? "Peer turn completed" : "agent_end did not include final assistant text",
77
+ }));
78
+ }
79
+ activateNext();
80
+ return true;
81
+ },
82
+
83
+ recordProgress(input = {}) {
84
+ const entry = activeEntry;
85
+ if (!entry || entry.settled) return { ok: false, reason: "no active inbound peer task" };
86
+ const progress = normalizeProgress(input);
87
+ entry.context?.progress?.(progress);
88
+ void recordGoalProgress(entry, progress, options).catch(() => {});
89
+ return { ok: true, messageId: entry.messageId, conversationId: entry.conversationId, progress };
90
+ },
91
+
92
+ pendingCount() {
93
+ return queue.length + (activeEntry && !activeEntry.settled ? 1 : 0);
94
+ },
95
+
96
+ dispose(reason = "Inbound peer bridge disposed") {
97
+ const entry = activeEntry;
98
+ activeEntry = undefined;
99
+ if (entry && !entry.settled) settleEntry(entry, { status: "CANCELLED", summary: reason });
100
+ while (queue.length) {
101
+ const queued = queue.shift();
102
+ settleEntry(queued, { status: "CANCELLED", summary: reason });
103
+ }
104
+ },
105
+ };
106
+ }
107
+
108
+ export function renderInboundPeerPrompt(envelope, options = {}) {
109
+ const source = envelope?.source?.peerId || "unknown-peer";
110
+ const intent = envelope?.body?.intent || "ask";
111
+ const prompt = redactForPrompt(envelope?.body?.prompt || "", options);
112
+ const refs = Array.isArray(envelope?.body?.contextRefs) && envelope.body.contextRefs.length
113
+ ? `\n\nContext refs:\n${envelope.body.contextRefs.map((ref) => `- ${ref.type || "ref"}: ${redactForPrompt(ref.value || "", options)}`).join("\n")}`
114
+ : "";
115
+ const responderInstructions = renderResponderInstructions(envelope, options.responderProfile, options);
116
+ const claimedPaths = renderClaimedPaths(envelope, options);
117
+ const handoff = renderTaskHandoffGuidance(envelope);
118
+ return `[Pi peer inbound]\n${responderInstructions}\n\nFrom: ${source}\nConversation: ${envelope.conversationId}\nMessage: ${envelope.id}\nIntent: ${intent}${claimedPaths}\n\n${prompt}${refs}${handoff}`;
119
+ }
120
+
121
+ function renderResponderInstructions(envelope, profile = {}, options = {}) {
122
+ const peerId = profile.peerId || envelope?.target?.peerId || "this-peer";
123
+ const lines = [
124
+ "Responder instructions:",
125
+ `- You are local Pi peer '${redactForPrompt(peerId, options)}'.`,
126
+ ];
127
+ if (profile.role) lines.push(`- Role: ${redactForPrompt(profile.role, options)}`);
128
+ if (profile.persona) lines.push(`- Persona: ${redactForPrompt(profile.persona, options)}`);
129
+ lines.push("", "Peer communication guidance:", renderPeerCommunicationGuidance());
130
+
131
+ const guidance = [profile.agentInstructions, profile.agentMdContent].filter(Boolean).map((item) => redactForPrompt(item, options)).join("\n\n").trim();
132
+ if (guidance) lines.push("", "Configured AGENT.md-style guidance:", truncatePromptSection(guidance));
133
+ return lines.join("\n");
134
+ }
135
+
136
+ function renderClaimedPaths(envelope, options = {}) {
137
+ const paths = envelope?.body?.metadata?.claimedPaths;
138
+ if (!Array.isArray(paths) || paths.length === 0) return "";
139
+ const clean = paths.filter((item) => typeof item === "string" && item.trim()).map((item) => redactForPrompt(item, options));
140
+ return clean.length ? `\nClaimed paths: ${clean.join(", ")}` : "";
141
+ }
142
+
143
+ function renderTaskHandoffGuidance(envelope) {
144
+ const intent = envelope?.body?.intent || "ask";
145
+ const claimedPaths = envelope?.body?.metadata?.claimedPaths;
146
+ const taskLike = intent === "task" || (Array.isArray(claimedPaths) && claimedPaths.length > 0);
147
+ if (!taskLike) return "";
148
+ return `\n\nLong-running peer task guidance:\n- Use peer_progress to report meaningful checkpoints before final response when available.\n- Required final handoff for this peer task:\n - Status: done | blocked | partial\n - Files changed: path list or none\n - Verification: command + exit status, or not run with reason\n - Blockers/risks: concise bullets or none\n - Safe for review: yes | no`;
149
+ }
150
+
151
+ function normalizeProgress(input = {}) {
152
+ const summary = typeof input.summary === "string" && input.summary.trim() ? input.summary.trim() : "Peer task progress";
153
+ return {
154
+ summary,
155
+ ...(typeof input.status === "string" && input.status.trim() ? { status: input.status.trim() } : {}),
156
+ ...(typeof input.phase === "string" && input.phase.trim() ? { phase: input.phase.trim() } : {}),
157
+ ...(input.detail !== undefined ? { detail: input.detail } : {}),
158
+ };
159
+ }
160
+
161
+ async function recordGoalProgress(entry, progress, options = {}) {
162
+ const metadata = entry.envelope?.body?.metadata || {};
163
+ if (!options.cwd || !metadata.goalId || !metadata.goalClaimId) return;
164
+ await appendPeerGoalEvent(options.cwd, metadata.goalId, {
165
+ type: "heartbeat",
166
+ peerId: entry.envelope?.target?.peerId || "unknown",
167
+ resolves: metadata.goalClaimId,
168
+ summary: progress.phase ? `${progress.phase}: ${progress.summary}` : progress.summary,
169
+ metadata: {
170
+ messageId: entry.messageId,
171
+ conversationId: entry.conversationId,
172
+ progress: true,
173
+ ...(progress.status ? { status: progress.status } : {}),
174
+ },
175
+ });
176
+ }
177
+
178
+ function redactForPrompt(value, options = {}) {
179
+ if (value === undefined || value === null) return "";
180
+ const redacted = redactPeerAuditValue(value, { homeDir: options.homeDir || process.env.HOME || "" });
181
+ if (typeof redacted === "string") return redacted;
182
+ return JSON.stringify(redacted);
183
+ }
184
+
185
+ function truncatePromptSection(value, maxLength = 12_000) {
186
+ return value.length > maxLength ? `${value.slice(0, maxLength)}\n[truncated]` : value;
187
+ }
188
+
189
+ export function extractFinalAssistantText(event) {
190
+ if (typeof event === "string") return event.trim();
191
+ if (!event || typeof event !== "object") return "";
192
+ for (const key of ["finalAssistantText", "finalText", "text", "content"]) {
193
+ const value = event[key];
194
+ const text = contentToText(value);
195
+ if (text) return text;
196
+ }
197
+ if (event.message?.role === "assistant") return contentToText(event.message.content);
198
+ if (event.finalMessage?.role === "assistant") return contentToText(event.finalMessage.content);
199
+ if (Array.isArray(event.messages)) {
200
+ for (let index = event.messages.length - 1; index >= 0; index -= 1) {
201
+ const message = event.messages[index];
202
+ if (message?.role === "assistant") {
203
+ const text = contentToText(message.content);
204
+ if (text) return text;
205
+ }
206
+ }
207
+ }
208
+ return "";
209
+ }
210
+
211
+ function contentToText(value) {
212
+ if (typeof value === "string") return value.trim();
213
+ if (Array.isArray(value)) {
214
+ return value.map((item) => {
215
+ if (typeof item === "string") return item;
216
+ if (typeof item?.text === "string") return item.text;
217
+ if (typeof item?.content === "string") return item.content;
218
+ return "";
219
+ }).filter(Boolean).join("\n").trim();
220
+ }
221
+ if (typeof value?.text === "string") return value.text.trim();
222
+ return "";
223
+ }
224
+
225
+ function summarizeEnvelope(envelope) {
226
+ return {
227
+ messageId: envelope.id,
228
+ conversationId: envelope.conversationId,
229
+ source: envelope.source,
230
+ target: envelope.target,
231
+ intent: envelope.body?.intent || "ask",
232
+ };
233
+ }
234
+
235
+ function settleEntry(entry, response, options = {}) {
236
+ if (entry.settled) return;
237
+ entry.settled = true;
238
+ if (options.clearTimer !== false) clearTimeout(entry.timer);
239
+ entry.resolve(response);
240
+ }