@parall/parel-channel 1.56.2 → 1.57.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/dist/channel-prompt.d.ts +1 -0
- package/dist/channel-prompt.d.ts.map +1 -1
- package/dist/channel-prompt.js +47 -11
- 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 +48 -11
- package/src/inbound-channel.ts +27 -12
- package/src/inbound.ts +4 -4
package/dist/channel-prompt.d.ts
CHANGED
|
@@ -70,6 +70,7 @@ export interface ChannelPromptArgs {
|
|
|
70
70
|
/** Conversation shape as recorded on the ChannelConversation (dm/group). */
|
|
71
71
|
conversationType?: string;
|
|
72
72
|
externalConversationId?: string;
|
|
73
|
+
externalConversationName?: string;
|
|
73
74
|
externalMessageId?: string;
|
|
74
75
|
senderName: string;
|
|
75
76
|
/** 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,qEAAqE;IACrE,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,CAqB5D;AAED,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,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,
|
|
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,qEAAqE;IACrE,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,CAqB5D;AAED,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
|
@@ -98,9 +98,18 @@ export function buildChatPrompt(args) {
|
|
|
98
98
|
}
|
|
99
99
|
export function buildChannelPrompt(args) {
|
|
100
100
|
const providerLabel = sanitizeMeta(args.provider ?? 'external IM');
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
const convType = sanitizeMeta(args.conversationType ?? 'conversation');
|
|
102
|
+
const convId = args.externalConversationId
|
|
103
|
+
? sanitizeMeta(args.externalConversationId)
|
|
104
|
+
: undefined;
|
|
105
|
+
const convName = args.externalConversationName
|
|
106
|
+
? sanitizeMeta(args.externalConversationName)
|
|
107
|
+
: undefined;
|
|
108
|
+
const convLabel = convName
|
|
109
|
+
? `${convName} (${convId ? `${convId}, ` : ''}${convType})`
|
|
110
|
+
: convId
|
|
111
|
+
? `${convId} (${convType})`
|
|
112
|
+
: convType;
|
|
104
113
|
const lines = [
|
|
105
114
|
'[Event: channel.message]',
|
|
106
115
|
`[Channel: ${providerLabel} | conversation: ${convLabel}]`,
|
|
@@ -114,14 +123,23 @@ export function buildChannelPrompt(args) {
|
|
|
114
123
|
return lines.join('\n') + buildChannelReplyHint(args);
|
|
115
124
|
}
|
|
116
125
|
function buildChannelReplyHint(args) {
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
126
|
+
// The outbound path is a property of the PROVIDER, not of the agent's
|
|
127
|
+
// grants. Feishu replies go through the vendor CLI with a platform-brokered
|
|
128
|
+
// credential; Slack and WeChat replies are platform verbs the `parall` CLI
|
|
129
|
+
// already carries under the agent's own identity — nothing to mint.
|
|
130
|
+
//
|
|
131
|
+
// Deciding on the Feishu grant alone told a Slack or WeChat inbound that
|
|
132
|
+
// outbound was disabled while its own system prompt declared the matching
|
|
133
|
+
// `parall <provider> send` verb, and could hand a dual-bound agent a
|
|
134
|
+
// `lark-cli` instruction addressed to a Slack conversation.
|
|
135
|
+
//
|
|
136
|
+
// Whether the command is ALLOWED stays a runtime question: every verb and
|
|
137
|
+
// the Feishu broker re-check the grant at call time. This only names the
|
|
138
|
+
// right door.
|
|
139
|
+
// Both halves are required: the mint is Feishu-only and says nothing about
|
|
140
|
+
// where the message arrived, so a dual-bound agent's Slack inbound must not
|
|
141
|
+
// fall into this branch just because its Feishu grant is live.
|
|
142
|
+
if (args.cliCapable && args.provider === 'feishu') {
|
|
125
143
|
const convRef = args.externalConversationId
|
|
126
144
|
? `chat_id "${args.externalConversationId}"`
|
|
127
145
|
: 'the conversation id named in this event';
|
|
@@ -130,6 +148,24 @@ function buildChannelReplyHint(args) {
|
|
|
130
148
|
: '';
|
|
131
149
|
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
150
|
}
|
|
151
|
+
if (args.provider === 'slack') {
|
|
152
|
+
// The flag and its value together, as agent-core renders it: writing the
|
|
153
|
+
// option separately from a labelled reference produced
|
|
154
|
+
// `--channel channel "C123"`.
|
|
155
|
+
const channelArg = args.externalConversationId
|
|
156
|
+
? ` --channel "${args.externalConversationId}"`
|
|
157
|
+
: ' --channel <conversation id from this event>';
|
|
158
|
+
// Channel sends REQUIRE a reply anchor; DMs take one too so a question
|
|
159
|
+
// asked in a thread is answered in that thread.
|
|
160
|
+
const replyTo = args.externalMessageId ? ` --reply-to "${args.externalMessageId}"` : '';
|
|
161
|
+
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>`;
|
|
162
|
+
}
|
|
163
|
+
if (args.provider === 'wechat') {
|
|
164
|
+
const toArg = args.externalConversationId
|
|
165
|
+
? `"${args.externalConversationId}"`
|
|
166
|
+
: '<wxid from this event>';
|
|
167
|
+
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>`;
|
|
168
|
+
}
|
|
133
169
|
const platform = args.provider ?? 'the external platform';
|
|
134
170
|
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
171
|
}
|
|
@@ -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.1",
|
|
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
|
@@ -141,6 +141,7 @@ export interface ChannelPromptArgs {
|
|
|
141
141
|
/** Conversation shape as recorded on the ChannelConversation (dm/group). */
|
|
142
142
|
conversationType?: string;
|
|
143
143
|
externalConversationId?: string;
|
|
144
|
+
externalConversationName?: string;
|
|
144
145
|
externalMessageId?: string;
|
|
145
146
|
senderName: string;
|
|
146
147
|
/** The inbound message text (raw, un-framed). */
|
|
@@ -156,9 +157,18 @@ export interface ChannelPromptArgs {
|
|
|
156
157
|
|
|
157
158
|
export function buildChannelPrompt(args: ChannelPromptArgs): string {
|
|
158
159
|
const providerLabel = sanitizeMeta(args.provider ?? 'external IM');
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
const convType = sanitizeMeta(args.conversationType ?? 'conversation');
|
|
161
|
+
const convId = args.externalConversationId
|
|
162
|
+
? sanitizeMeta(args.externalConversationId)
|
|
163
|
+
: undefined;
|
|
164
|
+
const convName = args.externalConversationName
|
|
165
|
+
? sanitizeMeta(args.externalConversationName)
|
|
166
|
+
: undefined;
|
|
167
|
+
const convLabel = convName
|
|
168
|
+
? `${convName} (${convId ? `${convId}, ` : ''}${convType})`
|
|
169
|
+
: convId
|
|
170
|
+
? `${convId} (${convType})`
|
|
171
|
+
: convType;
|
|
162
172
|
const lines = [
|
|
163
173
|
'[Event: channel.message]',
|
|
164
174
|
`[Channel: ${providerLabel} | conversation: ${convLabel}]`,
|
|
@@ -175,14 +185,23 @@ export function buildChannelPrompt(args: ChannelPromptArgs): string {
|
|
|
175
185
|
}
|
|
176
186
|
|
|
177
187
|
function buildChannelReplyHint(args: ChannelPromptArgs): string {
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
188
|
+
// The outbound path is a property of the PROVIDER, not of the agent's
|
|
189
|
+
// grants. Feishu replies go through the vendor CLI with a platform-brokered
|
|
190
|
+
// credential; Slack and WeChat replies are platform verbs the `parall` CLI
|
|
191
|
+
// already carries under the agent's own identity — nothing to mint.
|
|
192
|
+
//
|
|
193
|
+
// Deciding on the Feishu grant alone told a Slack or WeChat inbound that
|
|
194
|
+
// outbound was disabled while its own system prompt declared the matching
|
|
195
|
+
// `parall <provider> send` verb, and could hand a dual-bound agent a
|
|
196
|
+
// `lark-cli` instruction addressed to a Slack conversation.
|
|
197
|
+
//
|
|
198
|
+
// Whether the command is ALLOWED stays a runtime question: every verb and
|
|
199
|
+
// the Feishu broker re-check the grant at call time. This only names the
|
|
200
|
+
// right door.
|
|
201
|
+
// Both halves are required: the mint is Feishu-only and says nothing about
|
|
202
|
+
// where the message arrived, so a dual-bound agent's Slack inbound must not
|
|
203
|
+
// fall into this branch just because its Feishu grant is live.
|
|
204
|
+
if (args.cliCapable && args.provider === 'feishu') {
|
|
186
205
|
const convRef = args.externalConversationId
|
|
187
206
|
? `chat_id "${args.externalConversationId}"`
|
|
188
207
|
: 'the conversation id named in this event';
|
|
@@ -191,6 +210,24 @@ function buildChannelReplyHint(args: ChannelPromptArgs): string {
|
|
|
191
210
|
: '';
|
|
192
211
|
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
212
|
}
|
|
213
|
+
if (args.provider === 'slack') {
|
|
214
|
+
// The flag and its value together, as agent-core renders it: writing the
|
|
215
|
+
// option separately from a labelled reference produced
|
|
216
|
+
// `--channel channel "C123"`.
|
|
217
|
+
const channelArg = args.externalConversationId
|
|
218
|
+
? ` --channel "${args.externalConversationId}"`
|
|
219
|
+
: ' --channel <conversation id from this event>';
|
|
220
|
+
// Channel sends REQUIRE a reply anchor; DMs take one too so a question
|
|
221
|
+
// asked in a thread is answered in that thread.
|
|
222
|
+
const replyTo = args.externalMessageId ? ` --reply-to "${args.externalMessageId}"` : '';
|
|
223
|
+
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>`;
|
|
224
|
+
}
|
|
225
|
+
if (args.provider === 'wechat') {
|
|
226
|
+
const toArg = args.externalConversationId
|
|
227
|
+
? `"${args.externalConversationId}"`
|
|
228
|
+
: '<wxid from this event>';
|
|
229
|
+
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>`;
|
|
230
|
+
}
|
|
194
231
|
const platform = args.provider ?? 'the external platform';
|
|
195
232
|
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
233
|
}
|
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';
|