@parall/parel-channel 1.37.0
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 +30 -0
- package/dist/channel-prompt.d.ts.map +1 -0
- package/dist/channel-prompt.js +54 -0
- package/dist/connect.d.ts +22 -0
- package/dist/connect.d.ts.map +1 -0
- package/dist/connect.js +57 -0
- package/dist/delivery.d.ts +24 -0
- package/dist/delivery.d.ts.map +1 -0
- package/dist/delivery.js +120 -0
- package/dist/inbound.d.ts +28 -0
- package/dist/inbound.d.ts.map +1 -0
- package/dist/inbound.js +525 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/session.d.ts +110 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +284 -0
- package/package.json +37 -0
- package/parel.plugin.json +26 -0
- package/src/channel-prompt.ts +71 -0
- package/src/connect.ts +63 -0
- package/src/delivery.ts +144 -0
- package/src/inbound.ts +623 -0
- package/src/index.ts +28 -0
- package/src/session.ts +333 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt framing for external IM channel messages (channel_message
|
|
3
|
+
* dispatches). Mirrors agent-core's event-format.ts channel branch, adapted
|
|
4
|
+
* to the parel runtime's tool surface:
|
|
5
|
+
*
|
|
6
|
+
* - The audience is OUTSIDE Parall — no prll:// refs, no internal cards or
|
|
7
|
+
* attachment links in the reply (dead links there).
|
|
8
|
+
* - The reply goes out through the provider clip's `send_message` command.
|
|
9
|
+
* A parel agent invokes clips from its sandbox via the parall CLI, so the
|
|
10
|
+
* hint spells out the concrete `parall clip invoke …` command (agent-core
|
|
11
|
+
* leaves the invocation mechanism to the bridge's own clip tooling).
|
|
12
|
+
* - Provider metadata is best-effort (the connection lookup can fail) —
|
|
13
|
+
* never instruct the agent to invoke a made-up clip alias.
|
|
14
|
+
* - When the inbound external message id is known, offer the in-thread reply
|
|
15
|
+
* form ({"message_id": …}) so threaded conversations aren't nudged toward
|
|
16
|
+
* new top-level messages.
|
|
17
|
+
*/
|
|
18
|
+
export interface ChannelPromptArgs {
|
|
19
|
+
/** Provider name (= clip alias, e.g. "feishu"); undefined when unresolved. */
|
|
20
|
+
provider?: string;
|
|
21
|
+
/** Conversation shape as recorded on the ChannelConversation (dm/group). */
|
|
22
|
+
conversationType?: string;
|
|
23
|
+
externalConversationId?: string;
|
|
24
|
+
externalMessageId?: string;
|
|
25
|
+
senderName: string;
|
|
26
|
+
/** The inbound message text (raw, un-framed). */
|
|
27
|
+
text: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function buildChannelPrompt(args: ChannelPromptArgs): string;
|
|
30
|
+
//# sourceMappingURL=channel-prompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-prompt.d.ts","sourceRoot":"","sources":["../src/channel-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AASH,MAAM,WAAW,iBAAiB;IAChC,8EAA8E;IAC9E,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;CACd;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAkBlE"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt framing for external IM channel messages (channel_message
|
|
3
|
+
* dispatches). Mirrors agent-core's event-format.ts channel branch, adapted
|
|
4
|
+
* to the parel runtime's tool surface:
|
|
5
|
+
*
|
|
6
|
+
* - The audience is OUTSIDE Parall — no prll:// refs, no internal cards or
|
|
7
|
+
* attachment links in the reply (dead links there).
|
|
8
|
+
* - The reply goes out through the provider clip's `send_message` command.
|
|
9
|
+
* A parel agent invokes clips from its sandbox via the parall CLI, so the
|
|
10
|
+
* hint spells out the concrete `parall clip invoke …` command (agent-core
|
|
11
|
+
* leaves the invocation mechanism to the bridge's own clip tooling).
|
|
12
|
+
* - Provider metadata is best-effort (the connection lookup can fail) —
|
|
13
|
+
* never instruct the agent to invoke a made-up clip alias.
|
|
14
|
+
* - When the inbound external message id is known, offer the in-thread reply
|
|
15
|
+
* form ({"message_id": …}) so threaded conversations aren't nudged toward
|
|
16
|
+
* new top-level messages.
|
|
17
|
+
*/
|
|
18
|
+
function sanitizeMeta(value) {
|
|
19
|
+
return value
|
|
20
|
+
.replace(/[\r\n]+/g, ' ')
|
|
21
|
+
.replace(/[[\]|]/g, ' ')
|
|
22
|
+
.trim();
|
|
23
|
+
}
|
|
24
|
+
export function buildChannelPrompt(args) {
|
|
25
|
+
const providerLabel = sanitizeMeta(args.provider ?? 'external IM');
|
|
26
|
+
const convLabel = args.externalConversationId
|
|
27
|
+
? `${sanitizeMeta(args.externalConversationId)} (${sanitizeMeta(args.conversationType ?? 'conversation')})`
|
|
28
|
+
: sanitizeMeta(args.conversationType ?? 'conversation');
|
|
29
|
+
const lines = [
|
|
30
|
+
'[Event: channel.message]',
|
|
31
|
+
`[Channel: ${providerLabel} | conversation: ${convLabel}]`,
|
|
32
|
+
`[From: ${sanitizeMeta(args.senderName)} (external user, not a Parall member)]`,
|
|
33
|
+
];
|
|
34
|
+
if (args.externalMessageId) {
|
|
35
|
+
lines.push(`[External message ID: ${sanitizeMeta(args.externalMessageId)}]`);
|
|
36
|
+
}
|
|
37
|
+
lines.push(`[Audience: this conversation lives on ${providerLabel}, OUTSIDE Parall. Readers cannot open prll:// links, Parall cards, or internal attachments — never include them in replies. Write plain conversational text.]`);
|
|
38
|
+
lines.push('', args.text);
|
|
39
|
+
return lines.join('\n') + buildChannelReplyHint(args);
|
|
40
|
+
}
|
|
41
|
+
function buildChannelReplyHint(args) {
|
|
42
|
+
const target = JSON.stringify({
|
|
43
|
+
chat_id: args.externalConversationId || '<conversation id>',
|
|
44
|
+
text: '...',
|
|
45
|
+
});
|
|
46
|
+
const threadAlt = args.externalMessageId
|
|
47
|
+
? ` To reply in-thread to this specific message, use '${JSON.stringify({ message_id: args.externalMessageId, text: '...' })}' instead.`
|
|
48
|
+
: '';
|
|
49
|
+
if (args.provider) {
|
|
50
|
+
const alias = args.provider;
|
|
51
|
+
return `\n<system-reminder>To reply, invoke the \`${alias}\` clip's \`send_message\` command from your sandbox: \`parall clip invoke ${alias} send_message '${target}'\` — your plain text output is NOT delivered to the external conversation.${threadAlt} The same clip's \`call\` command reaches the wider ${alias} API when needed.</system-reminder>`;
|
|
52
|
+
}
|
|
53
|
+
return `\n<system-reminder>To reply, invoke your channel provider clip's \`send_message\` command from your sandbox: \`parall clip invoke <alias> send_message '${target}'\` (find the alias with \`parall clip list\`) — your plain text output is NOT delivered to the external conversation.${threadAlt} The same clip's \`call\` command reaches the wider external platform API when needed.</system-reminder>`;
|
|
54
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ConnectorContext, ConnectRequest } from '@parel/plugin-sdk';
|
|
2
|
+
/**
|
|
3
|
+
* Open the Parall gateway WebSocket as this agent. Auth is the agent's agk_: we
|
|
4
|
+
* fetch a short-lived WS ticket and hand parel the ws:// url to connect to. The
|
|
5
|
+
* parel runtime owns the socket lifecycle from there.
|
|
6
|
+
*
|
|
7
|
+
* Contract (confirmed against parel #102/#103): one long-lived managed_ws
|
|
8
|
+
* connection per declaration; the host rewrites ws(s):// to http(s):// for the
|
|
9
|
+
* Workers fetch dial, owns keepalive/liveness, and re-invokes connect() on
|
|
10
|
+
* every (re)connect — so the short-lived ticket is minted fresh each time.
|
|
11
|
+
*/
|
|
12
|
+
export declare function connectParall(ctx: ConnectorContext): Promise<ConnectRequest>;
|
|
13
|
+
export declare function parallApiUrl(ctx: ConnectorContext): string;
|
|
14
|
+
export declare function parallOrgId(ctx: ConnectorContext): string;
|
|
15
|
+
/**
|
|
16
|
+
* The parall agent's user id (usr_), needed for the path-scoped session/step
|
|
17
|
+
* reporting endpoints. Empty on configs deployed before progress reporting
|
|
18
|
+
* shipped — reporting degrades to off until the agent is redeployed.
|
|
19
|
+
*/
|
|
20
|
+
export declare function parallAgentId(ctx: ConnectorContext): string;
|
|
21
|
+
export declare function requireAgk(ctx: ConnectorContext): string;
|
|
22
|
+
//# sourceMappingURL=connect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAE1E;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,CAAC,CAyBlF;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM,CAE1D;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM,CAE3D;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM,CAMxD"}
|
package/dist/connect.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open the Parall gateway WebSocket as this agent. Auth is the agent's agk_: we
|
|
3
|
+
* fetch a short-lived WS ticket and hand parel the ws:// url to connect to. The
|
|
4
|
+
* parel runtime owns the socket lifecycle from there.
|
|
5
|
+
*
|
|
6
|
+
* Contract (confirmed against parel #102/#103): one long-lived managed_ws
|
|
7
|
+
* connection per declaration; the host rewrites ws(s):// to http(s):// for the
|
|
8
|
+
* Workers fetch dial, owns keepalive/liveness, and re-invokes connect() on
|
|
9
|
+
* every (re)connect — so the short-lived ticket is minted fresh each time.
|
|
10
|
+
*/
|
|
11
|
+
export async function connectParall(ctx) {
|
|
12
|
+
const apiUrl = parallApiUrl(ctx);
|
|
13
|
+
const agk = requireAgk(ctx);
|
|
14
|
+
// connect() runs on every (re)connect — fail fast rather than hang the host's
|
|
15
|
+
// reconnect loop on a wedged ticket endpoint.
|
|
16
|
+
const res = await fetch(`${apiUrl}/api/v1/ws/ticket`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { Authorization: `Bearer ${agk}` },
|
|
19
|
+
signal: AbortSignal.timeout(10_000),
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
throw new Error(`parel-channel: ws ticket failed (${res.status})`);
|
|
23
|
+
}
|
|
24
|
+
const body = (await res.json().catch(() => null));
|
|
25
|
+
const ticket = body?.ticket;
|
|
26
|
+
const wsUrl = body?.ws_url;
|
|
27
|
+
if (!ticket) {
|
|
28
|
+
throw new Error('parel-channel: ws ticket response missing ticket');
|
|
29
|
+
}
|
|
30
|
+
// Prefer the server-advertised gateway URL — the API and WS hosts differ in
|
|
31
|
+
// some deployments (local dev ports, split domains). Fall back to deriving
|
|
32
|
+
// from the API origin (prod-style single domain routing /ws).
|
|
33
|
+
const base = wsUrl || `${apiUrl.replace(/^http/, 'ws')}/ws`;
|
|
34
|
+
const sep = base.includes('?') ? '&' : '?';
|
|
35
|
+
return { url: `${base}${sep}ticket=${encodeURIComponent(ticket)}` };
|
|
36
|
+
}
|
|
37
|
+
export function parallApiUrl(ctx) {
|
|
38
|
+
return String(ctx.config.parallApiUrl ?? '').replace(/\/+$/, '');
|
|
39
|
+
}
|
|
40
|
+
export function parallOrgId(ctx) {
|
|
41
|
+
return String(ctx.config.parallOrgId ?? '');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The parall agent's user id (usr_), needed for the path-scoped session/step
|
|
45
|
+
* reporting endpoints. Empty on configs deployed before progress reporting
|
|
46
|
+
* shipped — reporting degrades to off until the agent is redeployed.
|
|
47
|
+
*/
|
|
48
|
+
export function parallAgentId(ctx) {
|
|
49
|
+
return String(ctx.config.parallAgentId ?? '');
|
|
50
|
+
}
|
|
51
|
+
export function requireAgk(ctx) {
|
|
52
|
+
const agk = ctx.secrets.parallApiKey;
|
|
53
|
+
if (!agk) {
|
|
54
|
+
throw new Error('parel-channel: parallApiKey secret is required');
|
|
55
|
+
}
|
|
56
|
+
return agk;
|
|
57
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ChannelDelivery, ConnectorContext, ConnectorEffect } from '@parel/plugin-sdk';
|
|
2
|
+
/**
|
|
3
|
+
* Post the agent's reply back to the Parall chat. The chat id rides the
|
|
4
|
+
* replyRoute.data set in inbound.ts.
|
|
5
|
+
*
|
|
6
|
+
* Contract (confirmed against parel #104): provider_http reply routes are
|
|
7
|
+
* auto-delivered at turn end via this hook; `payload` is the plain reply text
|
|
8
|
+
* string and `replyRoute` is the one captured from the originating envelope.
|
|
9
|
+
*
|
|
10
|
+
* The reply POST is awaited (not returned as a host fetch effect) so the
|
|
11
|
+
* session link resolution and failure handling stay in one place; a
|
|
12
|
+
* retryable failure falls back to the host effect (at-least-once, idempotent
|
|
13
|
+
* by delivery id + message idempotency_key). The turn's PRIMARY done signal
|
|
14
|
+
* lives in onAgentEvent (turn_completed); the idle here is a deliberate
|
|
15
|
+
* redundant close for bindings without `observe: [turn]` — see the inline
|
|
16
|
+
* comment at the bottom.
|
|
17
|
+
*
|
|
18
|
+
* External IM turns (channel_message dispatches) route here too but deliver
|
|
19
|
+
* NOTHING: their replyRoute carries a conversationId and no chatId, and the
|
|
20
|
+
* agent replies through the provider clip itself — this hook only runs the
|
|
21
|
+
* same redundant session idle for them.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildParallDelivery(delivery: ChannelDelivery, ctx: ConnectorContext): Promise<ConnectorEffect[]>;
|
|
24
|
+
//# sourceMappingURL=delivery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery.d.ts","sourceRoot":"","sources":["../src/delivery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAU5F;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,eAAe,EACzB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CA2F5B"}
|
package/dist/delivery.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
|
|
2
|
+
import { ensureSession, invalidateSession, patchSession, REQUEST_TIMEOUT_MS, turnEnvelopeKey, } from './session.js';
|
|
3
|
+
/**
|
|
4
|
+
* Post the agent's reply back to the Parall chat. The chat id rides the
|
|
5
|
+
* replyRoute.data set in inbound.ts.
|
|
6
|
+
*
|
|
7
|
+
* Contract (confirmed against parel #104): provider_http reply routes are
|
|
8
|
+
* auto-delivered at turn end via this hook; `payload` is the plain reply text
|
|
9
|
+
* string and `replyRoute` is the one captured from the originating envelope.
|
|
10
|
+
*
|
|
11
|
+
* The reply POST is awaited (not returned as a host fetch effect) so the
|
|
12
|
+
* session link resolution and failure handling stay in one place; a
|
|
13
|
+
* retryable failure falls back to the host effect (at-least-once, idempotent
|
|
14
|
+
* by delivery id + message idempotency_key). The turn's PRIMARY done signal
|
|
15
|
+
* lives in onAgentEvent (turn_completed); the idle here is a deliberate
|
|
16
|
+
* redundant close for bindings without `observe: [turn]` — see the inline
|
|
17
|
+
* comment at the bottom.
|
|
18
|
+
*
|
|
19
|
+
* External IM turns (channel_message dispatches) route here too but deliver
|
|
20
|
+
* NOTHING: their replyRoute carries a conversationId and no chatId, and the
|
|
21
|
+
* agent replies through the provider clip itself — this hook only runs the
|
|
22
|
+
* same redundant session idle for them.
|
|
23
|
+
*/
|
|
24
|
+
export async function buildParallDelivery(delivery, ctx) {
|
|
25
|
+
const apiUrl = parallApiUrl(ctx);
|
|
26
|
+
const agk = requireAgk(ctx);
|
|
27
|
+
const orgId = parallOrgId(ctx);
|
|
28
|
+
const route = (delivery.replyRoute?.data ?? {});
|
|
29
|
+
const chatId = route.chatId;
|
|
30
|
+
if (!chatId) {
|
|
31
|
+
// External IM turn (channel_message): the reply already went out through
|
|
32
|
+
// the provider clip, agent-invoked — the turn-end text is deliberately
|
|
33
|
+
// NOT delivered anywhere (same contract as agent-core: plain output is
|
|
34
|
+
// not the reply). Only run the redundant idle so a binding without
|
|
35
|
+
// `observe: [turn]` still closes the session.
|
|
36
|
+
if (route.conversationId) {
|
|
37
|
+
const sessionId = await ensureSession(ctx, route.conversationId, { timeoutMs: 3_000 });
|
|
38
|
+
if (sessionId) {
|
|
39
|
+
await idleUnlessSuperseded(ctx, route.conversationId, route.envelopeId, sessionId);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const payload = delivery.payload;
|
|
45
|
+
const text = typeof payload === 'string' ? payload : (payload?.text ?? '');
|
|
46
|
+
if (!text)
|
|
47
|
+
return [];
|
|
48
|
+
// Short timeout: this sits in front of the user-visible reply POST. Cache
|
|
49
|
+
// hit (every message after a chat's first) is a store read; on a slow miss
|
|
50
|
+
// we degrade to a reply without the session link rather than delay it.
|
|
51
|
+
const sessionId = await ensureSession(ctx, chatId, { timeoutMs: 3_000 });
|
|
52
|
+
const request = {
|
|
53
|
+
url: `${apiUrl}/api/v1/orgs/${orgId}/chats/${chatId}/messages`,
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
Authorization: `Bearer ${agk}`,
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
},
|
|
59
|
+
// message_type must be explicit: the server persists it verbatim, and
|
|
60
|
+
// a blank type skips text normalization (prll:// refs, mentions) and
|
|
61
|
+
// renders as an unknown-type fallback on clients. agent_session_id links
|
|
62
|
+
// the reply bubble to its session panel (server validates ownership).
|
|
63
|
+
// idempotency_key dedupes the ambiguous-failure case where the direct
|
|
64
|
+
// POST committed but its response was lost and the host retry re-sends —
|
|
65
|
+
// same body on both paths, so the server collapses them to one message.
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
message_type: 'text',
|
|
68
|
+
content: { text },
|
|
69
|
+
idempotency_key: `parel-reply:${delivery.id}`,
|
|
70
|
+
// A reply to a thread message stays in that thread.
|
|
71
|
+
...(route.threadRootId ? { thread_root_id: route.threadRootId } : {}),
|
|
72
|
+
...(sessionId ? { agent_session_id: sessionId } : {}),
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch(request.url, {
|
|
77
|
+
method: request.method,
|
|
78
|
+
headers: request.headers,
|
|
79
|
+
body: request.body,
|
|
80
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
81
|
+
});
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
// 4xx (bar 429) is permanent — retrying an unprocessable reply burns
|
|
84
|
+
// requests forever. Drop it (logged); turn_completed still closes the
|
|
85
|
+
// turn's indicator.
|
|
86
|
+
const permanent = res.status >= 400 && res.status < 500 && res.status !== 429;
|
|
87
|
+
if (!permanent)
|
|
88
|
+
throw new Error(`reply post failed (${res.status})`);
|
|
89
|
+
console.error(`[parel-channel] reply rejected (${res.status}) — dropping`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
console.error('[parel-channel] direct reply failed; falling back to host retry', err);
|
|
94
|
+
return [{ type: 'fetch', request, idempotencyKey: `reply:${delivery.id}` }];
|
|
95
|
+
}
|
|
96
|
+
// Redundant done signal, kept deliberately: turn_completed (onAgentEvent)
|
|
97
|
+
// is the primary close for every turn — including no-reply turns this hook
|
|
98
|
+
// never sees — but a binding deployed without `observe: [turn]` that still
|
|
99
|
+
// resolves this connector (npm ^1 fallback path; the artifact path freezes
|
|
100
|
+
// connector+config atomically) would otherwise never idle. Both signals
|
|
101
|
+
// run the same turnEnvelope staleness guard and idle is idempotent, so the
|
|
102
|
+
// overlap is harmless.
|
|
103
|
+
if (sessionId) {
|
|
104
|
+
await idleUnlessSuperseded(ctx, chatId, route.envelopeId, sessionId);
|
|
105
|
+
}
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Idle the subject's session unless a newer envelope owns the turn (the same
|
|
110
|
+
* turnEnvelope staleness guard onAgentEvent runs). Fail-open when the marker
|
|
111
|
+
* is unavailable — a stuck ring is worse than a blink.
|
|
112
|
+
*/
|
|
113
|
+
async function idleUnlessSuperseded(ctx, subject, envelopeId, sessionId) {
|
|
114
|
+
const current = await ctx.store?.get(turnEnvelopeKey(subject)).catch(() => null);
|
|
115
|
+
if (!(typeof current === 'string' && current) || !envelopeId || current === envelopeId) {
|
|
116
|
+
const result = await patchSession(ctx, sessionId, { status: 'idle' });
|
|
117
|
+
if (result === 'stale')
|
|
118
|
+
await invalidateSession(ctx, subject);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AgentEvent, AgentEventEffect, ConnectorContext, ConnectorEffect, WebSocketFrame } from '@parel/plugin-sdk';
|
|
2
|
+
export declare function handleParallFrame(frame: WebSocketFrame, ctx: ConnectorContext): Promise<ConnectorEffect[]>;
|
|
3
|
+
/**
|
|
4
|
+
* Sweep pending dispatches on (re)connect — the live WS push has no replay.
|
|
5
|
+
* Paginates with the server cursor so a long outage's backlog is not silently
|
|
6
|
+
* truncated (bounded to keep a pathological backlog from wedging onOpen; the
|
|
7
|
+
* remainder is picked up on the next reconnect sweep).
|
|
8
|
+
*/
|
|
9
|
+
export declare function catchUpParall(ctx: ConnectorContext): Promise<ConnectorEffect[]>;
|
|
10
|
+
/**
|
|
11
|
+
* Agent execution events (parel E1+E2, plugin-sdk 0.9.0, opt-in via the
|
|
12
|
+
* channel declaration's `observe: [turn, steps]`).
|
|
13
|
+
*
|
|
14
|
+
* E1 (turn lifecycle): turn_completed fires unconditionally on every cleanly
|
|
15
|
+
* finished turn — INCLUDING no-reply turns (hadOutput: false), the reliable
|
|
16
|
+
* "processing ended" signal that used to need a 30min turnwatch timer.
|
|
17
|
+
* turn_failed closes the turn the same way.
|
|
18
|
+
*
|
|
19
|
+
* E2 (step trace): model_reasoning / tool_call / tool_result become parall
|
|
20
|
+
* AgentSteps — the session panel gets the same live execution feed CC agents
|
|
21
|
+
* have (reasoning is aggregated host-side; tool output is a bounded preview).
|
|
22
|
+
*
|
|
23
|
+
* Events are best-effort (display feed, never data-correctness): a missed
|
|
24
|
+
* done event just means the ring clears on the next turn's trigger change; a
|
|
25
|
+
* missed trace step is a gap in the panel, nothing more.
|
|
26
|
+
*/
|
|
27
|
+
export declare function handleParallAgentEvent(event: AgentEvent, ctx: ConnectorContext): Promise<AgentEventEffect[]>;
|
|
28
|
+
//# sourceMappingURL=inbound.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbound.d.ts","sourceRoot":"","sources":["../src/inbound.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACf,MAAM,mBAAmB,CAAC;AA4C3B,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAiB5B;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAiFrF;AA+XD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA2C7B"}
|