@parall/parel-channel 1.56.2 → 1.57.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.
- package/dist/channel-prompt.d.ts +6 -1
- package/dist/channel-prompt.d.ts.map +1 -1
- package/dist/channel-prompt.js +84 -19
- package/dist/inbound-channel.d.ts.map +1 -1
- package/dist/inbound-channel.js +27 -14
- package/dist/inbound.js +3 -3
- package/package.json +1 -1
- package/src/channel-prompt.ts +93 -19
- package/src/inbound-channel.ts +27 -12
- package/src/inbound.ts +4 -4
package/dist/channel-prompt.d.ts
CHANGED
|
@@ -36,8 +36,12 @@ export interface ChatPromptArgs {
|
|
|
36
36
|
chatId: string;
|
|
37
37
|
/** Display name of the chat; undefined when unresolved (best-effort). */
|
|
38
38
|
chatName?: string;
|
|
39
|
-
/** Sender user id (usr_)
|
|
39
|
+
/** Sender user id (usr_). */
|
|
40
40
|
senderId?: string;
|
|
41
|
+
/** Sender display name; rendered before the id when resolved (best-effort). */
|
|
42
|
+
senderName?: string;
|
|
43
|
+
/** Chat shape (direct/group); the type segment is omitted when unresolved. */
|
|
44
|
+
chatType?: string;
|
|
41
45
|
/**
|
|
42
46
|
* The inbound message id (msg_); anchors the forwarded-message hint's
|
|
43
47
|
* explicit --from so coalesced turns don't lean on the single trigger id.
|
|
@@ -70,6 +74,7 @@ export interface ChannelPromptArgs {
|
|
|
70
74
|
/** Conversation shape as recorded on the ChannelConversation (dm/group). */
|
|
71
75
|
conversationType?: string;
|
|
72
76
|
externalConversationId?: string;
|
|
77
|
+
externalConversationName?: string;
|
|
73
78
|
externalMessageId?: string;
|
|
74
79
|
senderName: string;
|
|
75
80
|
/** The inbound message text (raw, un-framed). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-prompt.d.ts","sourceRoot":"","sources":["../src/channel-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AA+BH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,
|
|
1
|
+
{"version":3,"file":"channel-prompt.d.ts","sourceRoot":"","sources":["../src/channel-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AA+BH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,8FAA8F;IAC9F,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAmBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAwC5D;AAgBD,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CA2BlE"}
|
package/dist/channel-prompt.js
CHANGED
|
@@ -73,16 +73,31 @@ function attachmentLines(attachments) {
|
|
|
73
73
|
* not re-created here.
|
|
74
74
|
*/
|
|
75
75
|
export function buildChatPrompt(args) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
// agent-core's event-format.ts message branch is the SSOT for this frame:
|
|
77
|
+
// same lines, same order, same send-hint. The only sanctioned deltas are
|
|
78
|
+
// absent-field fallbacks (no chat name → bare URI, no sender name → bare
|
|
79
|
+
// id, no chat type → segment omitted) — agent-core's host always has them.
|
|
80
|
+
const lines = [`[Event: message.new]`];
|
|
81
|
+
const chatUri = `prll://${args.chatId}`;
|
|
82
|
+
const chatLabel = args.chatName ? `"${sanitizeMeta(args.chatName)}" (${chatUri})` : chatUri;
|
|
83
|
+
const typeSeg = args.chatType ? ` | type: ${sanitizeMeta(args.chatType)}` : '';
|
|
84
|
+
lines.push(`[Chat: ${chatLabel}${typeSeg}]`);
|
|
85
|
+
if (args.senderId) {
|
|
86
|
+
lines.push(args.senderName
|
|
87
|
+
? `[From: ${sanitizeMeta(args.senderName)} (prll://${args.senderId})]`
|
|
88
|
+
: `[From: prll://${args.senderId}]`);
|
|
89
|
+
}
|
|
90
|
+
if (args.messageId)
|
|
91
|
+
lines.push(`[Message ID: prll://${args.messageId}]`);
|
|
82
92
|
if (args.threadRootId)
|
|
83
|
-
lines.push(`[Thread:
|
|
93
|
+
lines.push(`[Thread: prll://${sanitizeMeta(args.threadRootId)}]`);
|
|
84
94
|
if (args.deliveryReason)
|
|
85
95
|
lines.push(`[Delivery: ${sanitizeMeta(args.deliveryReason)}]`);
|
|
96
|
+
if (args.deliveryReason === 'watcher' && args.threadRootId) {
|
|
97
|
+
// agent-core's threadWatcherHint, verbatim — the cooperative nudge under
|
|
98
|
+
// the server's agent-loop ceiling (agent-dm-loop-prevention.md Layer 4).
|
|
99
|
+
lines.push(`[Hint: watcher — you were not addressed in this reply. Reply only if you add something new; otherwise run \`parall no-reply\`. If this thread is not relevant to you, or you have nothing more to contribute, run \`parall messages unwatch prll://${sanitizeMeta(args.threadRootId)}\` so later replies stop waking you.]`);
|
|
100
|
+
}
|
|
86
101
|
if (args.noReply)
|
|
87
102
|
lines.push(`[Hint: no_reply]`);
|
|
88
103
|
if (isForwardedMessageBody(args.text)) {
|
|
@@ -94,13 +109,36 @@ export function buildChatPrompt(args) {
|
|
|
94
109
|
}
|
|
95
110
|
lines.push(...attachmentLines(args.attachments));
|
|
96
111
|
lines.push('', args.text);
|
|
97
|
-
return lines.join('\n');
|
|
112
|
+
return lines.join('\n') + chatSendHint(args);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* agent-core's buildSendMessageHint chat branch, verbatim: every chat event
|
|
116
|
+
* ends with the concrete send command. The standing system prompt teaches the
|
|
117
|
+
* CLI-only reply contract too, but a per-event reminder is what the standard
|
|
118
|
+
* runtimes ship — and weak models demonstrably drop the send step without it.
|
|
119
|
+
*/
|
|
120
|
+
function chatSendHint(args) {
|
|
121
|
+
if (args.noReply)
|
|
122
|
+
return '';
|
|
123
|
+
if (args.threadRootId) {
|
|
124
|
+
return `\n<system-reminder>To reply in this thread, run \`parall messages send prll://${args.chatId} --thread-root-id ${sanitizeMeta(args.threadRootId)} --text-file - <<'EOF'\` … \`EOF\` — the quoted heredoc keeps \`$\`, backticks and apostrophes literal (plain \`--text "$1,000"\` sends \`,000\`). Your plain text output is not delivered to the chat.</system-reminder>`;
|
|
125
|
+
}
|
|
126
|
+
return `\n<system-reminder>To reply, run \`parall messages send prll://${args.chatId} --text-file - <<'EOF'\` … \`EOF\` — the quoted heredoc keeps \`$\`, backticks and apostrophes literal (plain \`--text "$1,000"\` sends \`,000\`). Your plain text output is not delivered to the chat.</system-reminder>`;
|
|
98
127
|
}
|
|
99
128
|
export function buildChannelPrompt(args) {
|
|
100
129
|
const providerLabel = sanitizeMeta(args.provider ?? 'external IM');
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
130
|
+
const convType = sanitizeMeta(args.conversationType ?? 'conversation');
|
|
131
|
+
const convId = args.externalConversationId
|
|
132
|
+
? sanitizeMeta(args.externalConversationId)
|
|
133
|
+
: undefined;
|
|
134
|
+
const convName = args.externalConversationName
|
|
135
|
+
? sanitizeMeta(args.externalConversationName)
|
|
136
|
+
: undefined;
|
|
137
|
+
const convLabel = convName
|
|
138
|
+
? `${convName} (${convId ? `${convId}, ` : ''}${convType})`
|
|
139
|
+
: convId
|
|
140
|
+
? `${convId} (${convType})`
|
|
141
|
+
: convType;
|
|
104
142
|
const lines = [
|
|
105
143
|
'[Event: channel.message]',
|
|
106
144
|
`[Channel: ${providerLabel} | conversation: ${convLabel}]`,
|
|
@@ -114,14 +152,23 @@ export function buildChannelPrompt(args) {
|
|
|
114
152
|
return lines.join('\n') + buildChannelReplyHint(args);
|
|
115
153
|
}
|
|
116
154
|
function buildChannelReplyHint(args) {
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
155
|
+
// The outbound path is a property of the PROVIDER, not of the agent's
|
|
156
|
+
// grants. Feishu replies go through the vendor CLI with a platform-brokered
|
|
157
|
+
// credential; Slack and WeChat replies are platform verbs the `parall` CLI
|
|
158
|
+
// already carries under the agent's own identity — nothing to mint.
|
|
159
|
+
//
|
|
160
|
+
// Deciding on the Feishu grant alone told a Slack or WeChat inbound that
|
|
161
|
+
// outbound was disabled while its own system prompt declared the matching
|
|
162
|
+
// `parall <provider> send` verb, and could hand a dual-bound agent a
|
|
163
|
+
// `lark-cli` instruction addressed to a Slack conversation.
|
|
164
|
+
//
|
|
165
|
+
// Whether the command is ALLOWED stays a runtime question: every verb and
|
|
166
|
+
// the Feishu broker re-check the grant at call time. This only names the
|
|
167
|
+
// right door.
|
|
168
|
+
// Both halves are required: the mint is Feishu-only and says nothing about
|
|
169
|
+
// where the message arrived, so a dual-bound agent's Slack inbound must not
|
|
170
|
+
// fall into this branch just because its Feishu grant is live.
|
|
171
|
+
if (args.cliCapable && args.provider === 'feishu') {
|
|
125
172
|
const convRef = args.externalConversationId
|
|
126
173
|
? `chat_id "${args.externalConversationId}"`
|
|
127
174
|
: 'the conversation id named in this event';
|
|
@@ -130,6 +177,24 @@ function buildChannelReplyHint(args) {
|
|
|
130
177
|
: '';
|
|
131
178
|
return `\n<system-reminder>To reply, use the official Feishu CLI in your sandbox: send a message to ${convRef} with \`lark-cli im\` (see \`lark-cli im --help\` for send syntax; auth is provisioned in your environment — if lark-cli is missing, install it once with \`npm i -g @larksuite/cli\`).${threadAlt} lark-cli is the ONLY outbound path — your plain text output is NOT delivered to the external conversation.</system-reminder>`;
|
|
132
179
|
}
|
|
180
|
+
if (args.provider === 'slack') {
|
|
181
|
+
// The flag and its value together, as agent-core renders it: writing the
|
|
182
|
+
// option separately from a labelled reference produced
|
|
183
|
+
// `--channel channel "C123"`.
|
|
184
|
+
const channelArg = args.externalConversationId
|
|
185
|
+
? ` --channel "${args.externalConversationId}"`
|
|
186
|
+
: ' --channel <conversation id from this event>';
|
|
187
|
+
// Channel sends REQUIRE a reply anchor; DMs take one too so a question
|
|
188
|
+
// asked in a thread is answered in that thread.
|
|
189
|
+
const replyTo = args.externalMessageId ? ` --reply-to "${args.externalMessageId}"` : '';
|
|
190
|
+
return `\n<system-reminder>To reply, use \`parall slack send${channelArg}${replyTo} --text <text>\` from your sandbox (use \`--text-file -\` with a quoted heredoc for anything containing \`$\`, backticks or quotes). That verb is the ONLY outbound path — your plain text output is NOT delivered to the external conversation.</system-reminder>`;
|
|
191
|
+
}
|
|
192
|
+
if (args.provider === 'wechat') {
|
|
193
|
+
const toArg = args.externalConversationId
|
|
194
|
+
? `"${args.externalConversationId}"`
|
|
195
|
+
: '<wxid from this event>';
|
|
196
|
+
return `\n<system-reminder>To reply, use \`parall wechat send --to ${toArg} --text <text>\` from your sandbox (use \`--text-file -\` with a quoted heredoc for anything containing \`$\`, backticks or quotes). WeChat has no threads: a group reply lands in the whole room. That verb is the ONLY outbound path — your plain text output is NOT delivered to the external conversation.</system-reminder>`;
|
|
197
|
+
}
|
|
133
198
|
const platform = args.provider ?? 'the external platform';
|
|
134
199
|
return `\n<system-reminder>This message arrived from ${platform}, but outbound replies are currently disabled for this org (no channel capability granted). Do NOT attempt to reply on the external platform. If action is needed, surface it inside Parall (\`parall messages send\` / \`parall dm\`). Your plain text output is not delivered anywhere.</system-reminder>`;
|
|
135
200
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inbound-channel.d.ts","sourceRoot":"","sources":["../src/inbound-channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAkB3E;;;;GAIG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,OAAQ,CAAC;AAEhD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"inbound-channel.d.ts","sourceRoot":"","sources":["../src/inbound-channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAkB3E;;;;GAIG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,OAAQ,CAAC;AAEhD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAmM5B"}
|
package/dist/inbound-channel.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { channelCapabilityEnv } from './channel-capability.js';
|
|
2
2
|
import { buildChannelPrompt } from './channel-prompt.js';
|
|
3
3
|
import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
|
|
4
|
-
import { claimDispatchSource, completeSourceEffect, createChannelInputStep, ensureSession, invalidateSession, nextEnvelopeId, patchSession,
|
|
4
|
+
import { claimDispatchSource, completeSourceEffect, createChannelInputStep, ensureSession, invalidateSession, nextEnvelopeId, patchSession, REQUEST_TIMEOUT_MS, readActiveEmit, TURN_ENVELOPE_KEY, writeActiveEmit, } from './session.js';
|
|
5
5
|
/**
|
|
6
6
|
* External IM inbound (channel_message dispatches — Feishu/Slack via the
|
|
7
7
|
* platform-mediated channel, external-im-channel-design.md). Split from
|
|
@@ -70,13 +70,18 @@ export async function effectsForChannelDispatch(sourceId, ctx) {
|
|
|
70
70
|
}
|
|
71
71
|
const conv = (await c.json().catch(() => null));
|
|
72
72
|
// Provider (= clip alias) for prompt labeling + the reply hint.
|
|
73
|
-
//
|
|
73
|
+
// A gone connection degrades the wording; an unreachable platform throws,
|
|
74
|
+
// because the provider now decides the turn's outbound path (see the
|
|
75
|
+
// resolver).
|
|
74
76
|
const connectionId = conv?.connection_id ? String(conv.connection_id) : undefined;
|
|
75
77
|
const provider = connectionId ? await resolveChannelProvider(ctx, connectionId, get) : undefined;
|
|
76
78
|
const senderName = msg?.external_user_name || msg?.external_user_id || 'external user';
|
|
77
79
|
const externalConversationId = conv?.external_conversation_id
|
|
78
80
|
? String(conv.external_conversation_id)
|
|
79
81
|
: undefined;
|
|
82
|
+
const externalConversationName = conv?.external_conversation_name
|
|
83
|
+
? String(conv.external_conversation_name)
|
|
84
|
+
: undefined;
|
|
80
85
|
const externalMessageId = msg?.external_message_id ? String(msg.external_message_id) : undefined;
|
|
81
86
|
// Progress reporting — identical ordering contract to the message path
|
|
82
87
|
// (see the comments there): received settles before this function returns;
|
|
@@ -141,7 +146,10 @@ export async function effectsForChannelDispatch(sourceId, ctx) {
|
|
|
141
146
|
await writeActiveEmit(ctx, 'channel_message', sourceId, envelopeId, sessionId ?? undefined);
|
|
142
147
|
// Live capability grant → this turn's lark-cli auth env (mint gate = the
|
|
143
148
|
// no-drift revocation point) + the hint teaching the single reply path.
|
|
144
|
-
|
|
149
|
+
// The mint is Feishu-only. Asking for it on a Slack or WeChat inbound buys
|
|
150
|
+
// nothing (those replies are platform verbs under the agent's own identity)
|
|
151
|
+
// and would put a Feishu credential in a turn that has no use for one.
|
|
152
|
+
const capabilityEnv = provider === 'feishu' ? await channelCapabilityEnv(ctx) : null;
|
|
145
153
|
return [
|
|
146
154
|
{
|
|
147
155
|
type: 'emitEvent',
|
|
@@ -163,6 +171,7 @@ export async function effectsForChannelDispatch(sourceId, ctx) {
|
|
|
163
171
|
provider,
|
|
164
172
|
conversationType: conv?.conversation_type ? String(conv.conversation_type) : undefined,
|
|
165
173
|
externalConversationId,
|
|
174
|
+
externalConversationName,
|
|
166
175
|
externalMessageId,
|
|
167
176
|
senderName,
|
|
168
177
|
text,
|
|
@@ -172,6 +181,7 @@ export async function effectsForChannelDispatch(sourceId, ctx) {
|
|
|
172
181
|
channelMessageId: sourceId,
|
|
173
182
|
provider,
|
|
174
183
|
externalConversationId,
|
|
184
|
+
externalConversationName,
|
|
175
185
|
externalMessageId,
|
|
176
186
|
},
|
|
177
187
|
// No chatId on purpose: the reply is the agent's own lark-cli send,
|
|
@@ -225,17 +235,20 @@ async function resolveChannelProvider(ctx, connectionId, get) {
|
|
|
225
235
|
const cached = await ctx.store?.get(cacheKey).catch(() => null);
|
|
226
236
|
if (typeof cached === 'string' && cached)
|
|
227
237
|
return cached;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
await ctx.store?.set(cacheKey, provider).catch(() => { });
|
|
236
|
-
return provider;
|
|
237
|
-
}
|
|
238
|
-
catch {
|
|
238
|
+
const res = await get(`/channel-connections/${connectionId}`, PROVIDER_LOOKUP_TIMEOUT_MS);
|
|
239
|
+
// A gone connection is an ANSWER (no provider); anything else is the
|
|
240
|
+
// platform failing to answer. That distinction used to be moot — the
|
|
241
|
+
// provider was prompt wording — but it now decides which outbound path the
|
|
242
|
+
// turn is taught, and a turn taught nothing still completes its source and
|
|
243
|
+
// is never revisited. Throwing leaves the dispatch for the catch-up pass.
|
|
244
|
+
if (res.status === 404 || res.status === 410)
|
|
239
245
|
return undefined;
|
|
246
|
+
if (!res.ok) {
|
|
247
|
+
throw new Error(`channel connection lookup failed (${res.status})`);
|
|
240
248
|
}
|
|
249
|
+
const body = (await res.json().catch(() => null));
|
|
250
|
+
const provider = body?.provider ? String(body.provider) : undefined;
|
|
251
|
+
if (provider)
|
|
252
|
+
await ctx.store?.set(cacheKey, provider).catch(() => { });
|
|
253
|
+
return provider;
|
|
241
254
|
}
|
package/dist/inbound.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { armCatchUpEffect, beginCatchUp, CATCHUP_TIMER_KEY, deferToCatchUp, runCatchUpRound, } from './catchup.js';
|
|
1
2
|
import { channelCapabilityEnv } from './channel-capability.js';
|
|
2
3
|
import { buildChatPrompt } from './channel-prompt.js';
|
|
3
|
-
import { armCatchUpEffect, beginCatchUp, CATCHUP_TIMER_KEY, deferToCatchUp, runCatchUpRound, } from './catchup.js';
|
|
4
4
|
import { parallApiUrl, parallOrgId, requireAgk, sandboxEnabled } from './connect.js';
|
|
5
|
-
import { effectsForChannelDispatch, PROVIDER_LOOKUP_TIMEOUT_MS } from './inbound-channel.js';
|
|
6
5
|
import { buildForkScopePrefix, bumpForkRetryAttempts, childConfirmed, childRefKnown, clearForkState, clearForkSubject, FORK_RETRY_DELAY_MS, FORK_RETRY_PREFIX, handleChildSpawnFailed, MAX_SPAWN_VERIFY_ATTEMPTS, noteMainTurnEvent, planForkDecision, recordSpawn, settleChildEvent, trackFollowUpAck, } from './fork.js';
|
|
7
|
-
import {
|
|
6
|
+
import { effectsForChannelDispatch, PROVIDER_LOOKUP_TIMEOUT_MS } from './inbound-channel.js';
|
|
7
|
+
import { ackByIdEffect, claimDispatchSource, completeFencedEnvelopes, completeSourceEffect, conversationKey, createErrorStep, createInputStep, createTraceStep, createTypedInputStep, EMIT_TTL_MS, ensureChildSession, ensureSession, invalidateChildSession, invalidateSession, nextEnvelopeId, noteTurnEventsObserved, patchSession, REQUEST_TIMEOUT_MS, readActiveEmit, reportingEnabled, TURN_ENVELOPE_KEY, writeActiveEmit, } from './session.js';
|
|
8
8
|
import { isTypedEventType, planTypedDispatch } from './typed-dispatch.js';
|
|
9
9
|
/**
|
|
10
10
|
* Connection-keyed hook serialization — a v1-host compatibility shim. The v1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/parel-channel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.57.2",
|
|
4
4
|
"description": "Parall channel plugin for the parel runtime — lets a parel agent treat Parall as a managed_ws channel (receive dispatches, reply via the Parall API)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/src/channel-prompt.ts
CHANGED
|
@@ -67,8 +67,12 @@ export interface ChatPromptArgs {
|
|
|
67
67
|
chatId: string;
|
|
68
68
|
/** Display name of the chat; undefined when unresolved (best-effort). */
|
|
69
69
|
chatName?: string;
|
|
70
|
-
/** Sender user id (usr_)
|
|
70
|
+
/** Sender user id (usr_). */
|
|
71
71
|
senderId?: string;
|
|
72
|
+
/** Sender display name; rendered before the id when resolved (best-effort). */
|
|
73
|
+
senderName?: string;
|
|
74
|
+
/** Chat shape (direct/group); the type segment is omitted when unresolved. */
|
|
75
|
+
chatType?: string;
|
|
72
76
|
/**
|
|
73
77
|
* The inbound message id (msg_); anchors the forwarded-message hint's
|
|
74
78
|
* explicit --from so coalesced turns don't lean on the single trigger id.
|
|
@@ -113,13 +117,32 @@ function attachmentLines(attachments: PromptAttachment[] | undefined): string[]
|
|
|
113
117
|
* not re-created here.
|
|
114
118
|
*/
|
|
115
119
|
export function buildChatPrompt(args: ChatPromptArgs): string {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
// agent-core's event-format.ts message branch is the SSOT for this frame:
|
|
121
|
+
// same lines, same order, same send-hint. The only sanctioned deltas are
|
|
122
|
+
// absent-field fallbacks (no chat name → bare URI, no sender name → bare
|
|
123
|
+
// id, no chat type → segment omitted) — agent-core's host always has them.
|
|
124
|
+
const lines = [`[Event: message.new]`];
|
|
125
|
+
const chatUri = `prll://${args.chatId}`;
|
|
126
|
+
const chatLabel = args.chatName ? `"${sanitizeMeta(args.chatName)}" (${chatUri})` : chatUri;
|
|
127
|
+
const typeSeg = args.chatType ? ` | type: ${sanitizeMeta(args.chatType)}` : '';
|
|
128
|
+
lines.push(`[Chat: ${chatLabel}${typeSeg}]`);
|
|
129
|
+
if (args.senderId) {
|
|
130
|
+
lines.push(
|
|
131
|
+
args.senderName
|
|
132
|
+
? `[From: ${sanitizeMeta(args.senderName)} (prll://${args.senderId})]`
|
|
133
|
+
: `[From: prll://${args.senderId}]`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (args.messageId) lines.push(`[Message ID: prll://${args.messageId}]`);
|
|
137
|
+
if (args.threadRootId) lines.push(`[Thread: prll://${sanitizeMeta(args.threadRootId)}]`);
|
|
122
138
|
if (args.deliveryReason) lines.push(`[Delivery: ${sanitizeMeta(args.deliveryReason)}]`);
|
|
139
|
+
if (args.deliveryReason === 'watcher' && args.threadRootId) {
|
|
140
|
+
// agent-core's threadWatcherHint, verbatim — the cooperative nudge under
|
|
141
|
+
// the server's agent-loop ceiling (agent-dm-loop-prevention.md Layer 4).
|
|
142
|
+
lines.push(
|
|
143
|
+
`[Hint: watcher — you were not addressed in this reply. Reply only if you add something new; otherwise run \`parall no-reply\`. If this thread is not relevant to you, or you have nothing more to contribute, run \`parall messages unwatch prll://${sanitizeMeta(args.threadRootId)}\` so later replies stop waking you.]`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
123
146
|
if (args.noReply) lines.push(`[Hint: no_reply]`);
|
|
124
147
|
if (isForwardedMessageBody(args.text)) {
|
|
125
148
|
// Embed the concrete --from anchor: parel snapshots one invocation
|
|
@@ -132,7 +155,21 @@ export function buildChatPrompt(args: ChatPromptArgs): string {
|
|
|
132
155
|
}
|
|
133
156
|
lines.push(...attachmentLines(args.attachments));
|
|
134
157
|
lines.push('', args.text);
|
|
135
|
-
return lines.join('\n');
|
|
158
|
+
return lines.join('\n') + chatSendHint(args);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* agent-core's buildSendMessageHint chat branch, verbatim: every chat event
|
|
163
|
+
* ends with the concrete send command. The standing system prompt teaches the
|
|
164
|
+
* CLI-only reply contract too, but a per-event reminder is what the standard
|
|
165
|
+
* runtimes ship — and weak models demonstrably drop the send step without it.
|
|
166
|
+
*/
|
|
167
|
+
function chatSendHint(args: ChatPromptArgs): string {
|
|
168
|
+
if (args.noReply) return '';
|
|
169
|
+
if (args.threadRootId) {
|
|
170
|
+
return `\n<system-reminder>To reply in this thread, run \`parall messages send prll://${args.chatId} --thread-root-id ${sanitizeMeta(args.threadRootId)} --text-file - <<'EOF'\` … \`EOF\` — the quoted heredoc keeps \`$\`, backticks and apostrophes literal (plain \`--text "$1,000"\` sends \`,000\`). Your plain text output is not delivered to the chat.</system-reminder>`;
|
|
171
|
+
}
|
|
172
|
+
return `\n<system-reminder>To reply, run \`parall messages send prll://${args.chatId} --text-file - <<'EOF'\` … \`EOF\` — the quoted heredoc keeps \`$\`, backticks and apostrophes literal (plain \`--text "$1,000"\` sends \`,000\`). Your plain text output is not delivered to the chat.</system-reminder>`;
|
|
136
173
|
}
|
|
137
174
|
|
|
138
175
|
export interface ChannelPromptArgs {
|
|
@@ -141,6 +178,7 @@ export interface ChannelPromptArgs {
|
|
|
141
178
|
/** Conversation shape as recorded on the ChannelConversation (dm/group). */
|
|
142
179
|
conversationType?: string;
|
|
143
180
|
externalConversationId?: string;
|
|
181
|
+
externalConversationName?: string;
|
|
144
182
|
externalMessageId?: string;
|
|
145
183
|
senderName: string;
|
|
146
184
|
/** The inbound message text (raw, un-framed). */
|
|
@@ -156,9 +194,18 @@ export interface ChannelPromptArgs {
|
|
|
156
194
|
|
|
157
195
|
export function buildChannelPrompt(args: ChannelPromptArgs): string {
|
|
158
196
|
const providerLabel = sanitizeMeta(args.provider ?? 'external IM');
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
197
|
+
const convType = sanitizeMeta(args.conversationType ?? 'conversation');
|
|
198
|
+
const convId = args.externalConversationId
|
|
199
|
+
? sanitizeMeta(args.externalConversationId)
|
|
200
|
+
: undefined;
|
|
201
|
+
const convName = args.externalConversationName
|
|
202
|
+
? sanitizeMeta(args.externalConversationName)
|
|
203
|
+
: undefined;
|
|
204
|
+
const convLabel = convName
|
|
205
|
+
? `${convName} (${convId ? `${convId}, ` : ''}${convType})`
|
|
206
|
+
: convId
|
|
207
|
+
? `${convId} (${convType})`
|
|
208
|
+
: convType;
|
|
162
209
|
const lines = [
|
|
163
210
|
'[Event: channel.message]',
|
|
164
211
|
`[Channel: ${providerLabel} | conversation: ${convLabel}]`,
|
|
@@ -175,14 +222,23 @@ export function buildChannelPrompt(args: ChannelPromptArgs): string {
|
|
|
175
222
|
}
|
|
176
223
|
|
|
177
224
|
function buildChannelReplyHint(args: ChannelPromptArgs): string {
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
225
|
+
// The outbound path is a property of the PROVIDER, not of the agent's
|
|
226
|
+
// grants. Feishu replies go through the vendor CLI with a platform-brokered
|
|
227
|
+
// credential; Slack and WeChat replies are platform verbs the `parall` CLI
|
|
228
|
+
// already carries under the agent's own identity — nothing to mint.
|
|
229
|
+
//
|
|
230
|
+
// Deciding on the Feishu grant alone told a Slack or WeChat inbound that
|
|
231
|
+
// outbound was disabled while its own system prompt declared the matching
|
|
232
|
+
// `parall <provider> send` verb, and could hand a dual-bound agent a
|
|
233
|
+
// `lark-cli` instruction addressed to a Slack conversation.
|
|
234
|
+
//
|
|
235
|
+
// Whether the command is ALLOWED stays a runtime question: every verb and
|
|
236
|
+
// the Feishu broker re-check the grant at call time. This only names the
|
|
237
|
+
// right door.
|
|
238
|
+
// Both halves are required: the mint is Feishu-only and says nothing about
|
|
239
|
+
// where the message arrived, so a dual-bound agent's Slack inbound must not
|
|
240
|
+
// fall into this branch just because its Feishu grant is live.
|
|
241
|
+
if (args.cliCapable && args.provider === 'feishu') {
|
|
186
242
|
const convRef = args.externalConversationId
|
|
187
243
|
? `chat_id "${args.externalConversationId}"`
|
|
188
244
|
: 'the conversation id named in this event';
|
|
@@ -191,6 +247,24 @@ function buildChannelReplyHint(args: ChannelPromptArgs): string {
|
|
|
191
247
|
: '';
|
|
192
248
|
return `\n<system-reminder>To reply, use the official Feishu CLI in your sandbox: send a message to ${convRef} with \`lark-cli im\` (see \`lark-cli im --help\` for send syntax; auth is provisioned in your environment — if lark-cli is missing, install it once with \`npm i -g @larksuite/cli\`).${threadAlt} lark-cli is the ONLY outbound path — your plain text output is NOT delivered to the external conversation.</system-reminder>`;
|
|
193
249
|
}
|
|
250
|
+
if (args.provider === 'slack') {
|
|
251
|
+
// The flag and its value together, as agent-core renders it: writing the
|
|
252
|
+
// option separately from a labelled reference produced
|
|
253
|
+
// `--channel channel "C123"`.
|
|
254
|
+
const channelArg = args.externalConversationId
|
|
255
|
+
? ` --channel "${args.externalConversationId}"`
|
|
256
|
+
: ' --channel <conversation id from this event>';
|
|
257
|
+
// Channel sends REQUIRE a reply anchor; DMs take one too so a question
|
|
258
|
+
// asked in a thread is answered in that thread.
|
|
259
|
+
const replyTo = args.externalMessageId ? ` --reply-to "${args.externalMessageId}"` : '';
|
|
260
|
+
return `\n<system-reminder>To reply, use \`parall slack send${channelArg}${replyTo} --text <text>\` from your sandbox (use \`--text-file -\` with a quoted heredoc for anything containing \`$\`, backticks or quotes). That verb is the ONLY outbound path — your plain text output is NOT delivered to the external conversation.</system-reminder>`;
|
|
261
|
+
}
|
|
262
|
+
if (args.provider === 'wechat') {
|
|
263
|
+
const toArg = args.externalConversationId
|
|
264
|
+
? `"${args.externalConversationId}"`
|
|
265
|
+
: '<wxid from this event>';
|
|
266
|
+
return `\n<system-reminder>To reply, use \`parall wechat send --to ${toArg} --text <text>\` from your sandbox (use \`--text-file -\` with a quoted heredoc for anything containing \`$\`, backticks or quotes). WeChat has no threads: a group reply lands in the whole room. That verb is the ONLY outbound path — your plain text output is NOT delivered to the external conversation.</system-reminder>`;
|
|
267
|
+
}
|
|
194
268
|
const platform = args.provider ?? 'the external platform';
|
|
195
269
|
return `\n<system-reminder>This message arrived from ${platform}, but outbound replies are currently disabled for this org (no channel capability granted). Do NOT attempt to reply on the external platform. If action is needed, surface it inside Parall (\`parall messages send\` / \`parall dm\`). Your plain text output is not delivered anywhere.</system-reminder>`;
|
|
196
270
|
}
|
package/src/inbound-channel.ts
CHANGED
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
invalidateSession,
|
|
11
11
|
nextEnvelopeId,
|
|
12
12
|
patchSession,
|
|
13
|
-
readActiveEmit,
|
|
14
13
|
REQUEST_TIMEOUT_MS,
|
|
14
|
+
readActiveEmit,
|
|
15
15
|
TURN_ENVELOPE_KEY,
|
|
16
16
|
writeActiveEmit,
|
|
17
17
|
} from './session.js';
|
|
@@ -100,11 +100,14 @@ export async function effectsForChannelDispatch(
|
|
|
100
100
|
const conv = (await c.json().catch(() => null)) as {
|
|
101
101
|
connection_id?: string;
|
|
102
102
|
external_conversation_id?: string;
|
|
103
|
+
external_conversation_name?: string;
|
|
103
104
|
conversation_type?: string;
|
|
104
105
|
} | null;
|
|
105
106
|
|
|
106
107
|
// Provider (= clip alias) for prompt labeling + the reply hint.
|
|
107
|
-
//
|
|
108
|
+
// A gone connection degrades the wording; an unreachable platform throws,
|
|
109
|
+
// because the provider now decides the turn's outbound path (see the
|
|
110
|
+
// resolver).
|
|
108
111
|
const connectionId = conv?.connection_id ? String(conv.connection_id) : undefined;
|
|
109
112
|
const provider = connectionId ? await resolveChannelProvider(ctx, connectionId, get) : undefined;
|
|
110
113
|
|
|
@@ -112,6 +115,9 @@ export async function effectsForChannelDispatch(
|
|
|
112
115
|
const externalConversationId = conv?.external_conversation_id
|
|
113
116
|
? String(conv.external_conversation_id)
|
|
114
117
|
: undefined;
|
|
118
|
+
const externalConversationName = conv?.external_conversation_name
|
|
119
|
+
? String(conv.external_conversation_name)
|
|
120
|
+
: undefined;
|
|
115
121
|
const externalMessageId = msg?.external_message_id ? String(msg.external_message_id) : undefined;
|
|
116
122
|
|
|
117
123
|
// Progress reporting — identical ordering contract to the message path
|
|
@@ -178,7 +184,10 @@ export async function effectsForChannelDispatch(
|
|
|
178
184
|
|
|
179
185
|
// Live capability grant → this turn's lark-cli auth env (mint gate = the
|
|
180
186
|
// no-drift revocation point) + the hint teaching the single reply path.
|
|
181
|
-
|
|
187
|
+
// The mint is Feishu-only. Asking for it on a Slack or WeChat inbound buys
|
|
188
|
+
// nothing (those replies are platform verbs under the agent's own identity)
|
|
189
|
+
// and would put a Feishu credential in a turn that has no use for one.
|
|
190
|
+
const capabilityEnv = provider === 'feishu' ? await channelCapabilityEnv(ctx) : null;
|
|
182
191
|
|
|
183
192
|
return [
|
|
184
193
|
{
|
|
@@ -201,6 +210,7 @@ export async function effectsForChannelDispatch(
|
|
|
201
210
|
provider,
|
|
202
211
|
conversationType: conv?.conversation_type ? String(conv.conversation_type) : undefined,
|
|
203
212
|
externalConversationId,
|
|
213
|
+
externalConversationName,
|
|
204
214
|
externalMessageId,
|
|
205
215
|
senderName,
|
|
206
216
|
text,
|
|
@@ -210,6 +220,7 @@ export async function effectsForChannelDispatch(
|
|
|
210
220
|
channelMessageId: sourceId,
|
|
211
221
|
provider,
|
|
212
222
|
externalConversationId,
|
|
223
|
+
externalConversationName,
|
|
213
224
|
externalMessageId,
|
|
214
225
|
},
|
|
215
226
|
// No chatId on purpose: the reply is the agent's own lark-cli send,
|
|
@@ -272,14 +283,18 @@ async function resolveChannelProvider(
|
|
|
272
283
|
const cacheKey = `channelProvider:${connectionId}`;
|
|
273
284
|
const cached = await ctx.store?.get(cacheKey).catch(() => null);
|
|
274
285
|
if (typeof cached === 'string' && cached) return cached;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
286
|
+
const res = await get(`/channel-connections/${connectionId}`, PROVIDER_LOOKUP_TIMEOUT_MS);
|
|
287
|
+
// A gone connection is an ANSWER (no provider); anything else is the
|
|
288
|
+
// platform failing to answer. That distinction used to be moot — the
|
|
289
|
+
// provider was prompt wording — but it now decides which outbound path the
|
|
290
|
+
// turn is taught, and a turn taught nothing still completes its source and
|
|
291
|
+
// is never revisited. Throwing leaves the dispatch for the catch-up pass.
|
|
292
|
+
if (res.status === 404 || res.status === 410) return undefined;
|
|
293
|
+
if (!res.ok) {
|
|
294
|
+
throw new Error(`channel connection lookup failed (${res.status})`);
|
|
284
295
|
}
|
|
296
|
+
const body = (await res.json().catch(() => null)) as { provider?: string } | null;
|
|
297
|
+
const provider = body?.provider ? String(body.provider) : undefined;
|
|
298
|
+
if (provider) await ctx.store?.set(cacheKey, provider).catch(() => {});
|
|
299
|
+
return provider;
|
|
285
300
|
}
|
package/src/inbound.ts
CHANGED
|
@@ -5,8 +5,6 @@ import type {
|
|
|
5
5
|
ConnectorEffect,
|
|
6
6
|
WebSocketFrame,
|
|
7
7
|
} from '@parel/plugin-sdk';
|
|
8
|
-
import { channelCapabilityEnv } from './channel-capability.js';
|
|
9
|
-
import { buildChatPrompt, type PromptAttachment } from './channel-prompt.js';
|
|
10
8
|
import {
|
|
11
9
|
armCatchUpEffect,
|
|
12
10
|
beginCatchUp,
|
|
@@ -14,8 +12,9 @@ import {
|
|
|
14
12
|
deferToCatchUp,
|
|
15
13
|
runCatchUpRound,
|
|
16
14
|
} from './catchup.js';
|
|
15
|
+
import { channelCapabilityEnv } from './channel-capability.js';
|
|
16
|
+
import { buildChatPrompt, type PromptAttachment } from './channel-prompt.js';
|
|
17
17
|
import { parallApiUrl, parallOrgId, requireAgk, sandboxEnabled } from './connect.js';
|
|
18
|
-
import { effectsForChannelDispatch, PROVIDER_LOOKUP_TIMEOUT_MS } from './inbound-channel.js';
|
|
19
18
|
import {
|
|
20
19
|
buildForkScopePrefix,
|
|
21
20
|
bumpForkRetryAttempts,
|
|
@@ -33,6 +32,7 @@ import {
|
|
|
33
32
|
settleChildEvent,
|
|
34
33
|
trackFollowUpAck,
|
|
35
34
|
} from './fork.js';
|
|
35
|
+
import { effectsForChannelDispatch, PROVIDER_LOOKUP_TIMEOUT_MS } from './inbound-channel.js';
|
|
36
36
|
import {
|
|
37
37
|
ackByIdEffect,
|
|
38
38
|
claimDispatchSource,
|
|
@@ -51,9 +51,9 @@ import {
|
|
|
51
51
|
nextEnvelopeId,
|
|
52
52
|
noteTurnEventsObserved,
|
|
53
53
|
patchSession,
|
|
54
|
+
REQUEST_TIMEOUT_MS,
|
|
54
55
|
readActiveEmit,
|
|
55
56
|
reportingEnabled,
|
|
56
|
-
REQUEST_TIMEOUT_MS,
|
|
57
57
|
TURN_ENVELOPE_KEY,
|
|
58
58
|
writeActiveEmit,
|
|
59
59
|
} from './session.js';
|