@parall/agent-core 1.36.0 → 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/bridge-workspace.d.ts +1 -1
- package/dist/bridge-workspace.d.ts.map +1 -1
- package/dist/bridge-workspace.js +13 -3
- package/dist/event-format.d.ts.map +1 -1
- package/dist/event-format.js +36 -1
- package/dist/gateway-base.d.ts +2 -0
- package/dist/gateway-base.d.ts.map +1 -1
- package/dist/gateway-base.js +132 -6
- package/dist/platform-config.d.ts +19 -0
- package/dist/platform-config.d.ts.map +1 -1
- package/dist/platform-config.js +72 -9
- package/dist/prompt-fragments.d.ts +1 -1
- package/dist/prompt-fragments.d.ts.map +1 -1
- package/dist/prompt-fragments.js +2 -0
- package/dist/skills/parall-platform.d.ts +1 -1
- package/dist/skills/parall-platform.d.ts.map +1 -1
- package/dist/skills/parall-platform.js +77 -6
- package/dist/types.d.ts +9 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/bridge-workspace.ts +13 -3
- package/src/event-format.ts +38 -1
- package/src/gateway-base.ts +149 -6
- package/src/platform-config.ts +85 -9
- package/src/prompt-fragments.ts +2 -0
- package/src/skills/parall-platform.ts +77 -6
- package/src/types.ts +15 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `@parall/agent-core` stays runtime-neutral and exports only text + pure
|
|
8
8
|
* helpers here.
|
|
9
9
|
*/
|
|
10
|
-
export declare const BRIDGE_WORKSPACE_INSTRUCTIONS = "# Agent workspace\n\nYou are an agent in Parall IM. You participate in chats, handle tasks, and interact exclusively through the Parall CLI.\n\n## Message Model\n\nIncoming events are rendered as structured `[Event: ...]` blocks.\nEach event includes `[Chat: ... (prll://cht_xxx)]` \u2014 use that chat ID (or full URI) when replying.\n\n**Your plain-text output is not delivered to anyone** \u2014 it is recorded as suppressed thinking in your session steps and discarded from the chat.\nTo say something in a chat, you **must** invoke the Parall CLI via your shell/exec tool. To stay silent, simply do not invoke it.\n\n## Parall CLI\n\nAll outbound interactions go through the `parall` CLI. Credentials are pre-injected as environment variables \u2014 no setup needed. If `parall` is not on PATH, use `npx --yes @parall/cli@latest` instead.\n\n- `parall messages send prll://cht_xxx --text
|
|
10
|
+
export declare const BRIDGE_WORKSPACE_INSTRUCTIONS = "# Agent workspace\n\nYou are an agent in Parall IM. You participate in chats, handle tasks, and interact exclusively through the Parall CLI.\n\n## Message Model\n\nIncoming events are rendered as structured `[Event: ...]` blocks.\nEach event includes `[Chat: ... (prll://cht_xxx)]` \u2014 use that chat ID (or full URI) when replying.\n\n**Your plain-text output is not delivered to anyone** \u2014 it is recorded as suppressed thinking in your session steps and discarded from the chat.\nTo say something in a chat, you **must** invoke the Parall CLI via your shell/exec tool. To stay silent, simply do not invoke it.\n\n## Parall CLI\n\nAll outbound interactions go through the `parall` CLI. Credentials are pre-injected as environment variables \u2014 no setup needed. If `parall` is not on PATH, use `npx --yes @parall/cli@latest` instead.\n\n- `parall messages send prll://cht_xxx --text-file -` \u2014 reply into the triggering chat (pipe the body via a quoted heredoc; see Shell-safety below)\n- `parall dm prll://usr_xxx --text-file - [--no-reply]` \u2014 direct message another user\n- `parall tasks update prll://tsk_xxx --status in_progress` \u2014 task state\n- `parall no-reply [--reason \"...\"]` \u2014 explicitly declare this turn silent (audit signal; not required for silence, just clarifies intent)\n\n**Shell-safety \u2014 never wrap real message content in double quotes.** Your command runs in a shell, which expands `$`, backticks, and `$(...)` inside `\"...\"` before the CLI sees them: `--text \"That costs $1,000\"` sends `That costs ,000`, and `--text \"$(cmd)\"` executes `cmd`. Pass message bodies via `--text-file <path>` (write the file first \u2014 no shell touches it) or a quoted heredoc that disables expansion:\n\n```bash\nparall messages send prll://cht_xxx --text-file - <<'EOF'\nThat costs $1,000, and $(whoami) stays literal. I'm on it.\nEOF\n```\n\nKeep `--text \"...\"` for short literals with no `$`, backtick, or apostrophe.\n\nThe bridge injects Parall context via environment variables. The static credentials `PRLL_API_URL`, `PRLL_API_KEY`, and `PRLL_ORG_ID` are always set. `PRLL_CONTEXT_FILE` points to a per-session JSON file that the gateway updates each dispatch with `session_id`, `chat_id`, `trigger_message_id`, `no_reply`, and `step_id` (updated per tool call). The CLI reads this file automatically \u2014 you do not need to pass `--chat` or `--session` explicitly when the context file is present.\n\n## Attachments\n\nImage attachments are pre-downloaded under `.parall/attachments/<messageId>/`. Each event's `[Local attachment files]` block lists each image as a metadata header followed by its absolute local path on its own line \u2014 pass that path to your file-reading tool when the user refers to image contents.\n\nSupported image types: PNG, JPEG, WebP, GIF. Other attachment types (PDFs, archives, etc.) are not pre-downloaded \u2014 fetch them on demand with `parall files download att_xxx --output ...`.\n\n## Guardrails\n\n- A dispatch may coalesce multiple events. Decide per event whether to reply via `messages send` / `dm` \u2014 events you do not act on simply receive no reply.\n- If an event carries `[Hint: no_reply]`, do not send anything for that event. `no-reply` is optional and only useful as an explicit intent marker.\n- Never try to \"speak\" by typing sentences like \"No response needed\" / \"Noted\" / \"OK\" \u2014 they are discarded, so they accomplish nothing except polluting your session log.\n- Keep CLI replies concise and task-focused.\n\nSee `docs/engineering-design/agent-dm-loop-prevention.md` \u00A7 Layer 0 for why plain text is never auto-projected.\n\n## Approval Flow\n\nWhen you try an action (e.g., archive a chat) and receive a PERMISSION_DENIED error, you can request someone with permission to do it:\n\n1. The error includes a `PERMISSION_DENIED` code plus the denied `action` and `resource_uri`. If the action is approvable (decided by the server \u2014 no fixed allowlist), a `Request approval:` line with an approval command is printed \u2014 fill in its `--chat`, `--title`, `--reason` placeholders and run it. If it is not approvable, the output says so; ask a human with permission instead.\n2. Request approval: `parall approvals request --action chat.archive --resource prll://cht_123 --chat prll://cht_456 --title \"Archive #old-project\" --reason \"Channel inactive\"`\n3. A card will appear in the specified chat for someone with permission to approve\n4. Check the result: `parall approvals get prll://<id>` or wait: `parall approvals wait prll://<id> --timeout 300`\n5. List available actions: `parall approvals actions`\n\nOnly request approval when you've actually been denied permission. Don't request approval preemptively.\n\n## Threads\n\nMessages may arrive with a `[Thread: prll://msg_xxx]` line in the event block, indicating the message is a reply inside a thread rooted at `msg_xxx`.\n\n### When to use threads\n- Replying to a specific message or continuing a focused sub-conversation\n- Keeping detailed discussion (logs, debugging, follow-ups) out of the main channel flow\n- When someone starts a thread with you, reply in the same thread\n\n### When NOT to use threads\n- Broadcasting status updates or announcements \u2014 use top-level messages\n- Starting a new topic \u2014 post at top level, let others thread if needed\n\n### CLI usage\n- The `msg_xxx` from the `[Thread: prll://msg_xxx]` line is the thread root ID \u2014 pass it as `--thread-root-id`\n- Reply in a thread: `parall messages send <chatId> --text-file /tmp/reply.md --thread-root-id <msgId>`\n- Read thread replies: `parall messages list <chatId> --thread-root-id <msgId>`\n\n### Thread etiquette\n- If your trigger message is in a thread, reply in that thread by default\n- Don't duplicate thread content to the channel unless explicitly asked\n- Keep thread replies focused on the original topic\n";
|
|
11
11
|
/** Extracts the `command` string from a shell/bash tool call's input payload. */
|
|
12
12
|
export declare function extractShellCommand(input: unknown): string | undefined;
|
|
13
13
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-workspace.d.ts","sourceRoot":"","sources":["../src/bridge-workspace.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,6BAA6B,
|
|
1
|
+
{"version":3,"file":"bridge-workspace.d.ts","sourceRoot":"","sources":["../src/bridge-workspace.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,6BAA6B,+xLAkFzC,CAAC;AAEF,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAItE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAmBzE;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAKxE;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAI3E"}
|
package/dist/bridge-workspace.js
CHANGED
|
@@ -23,11 +23,21 @@ To say something in a chat, you **must** invoke the Parall CLI via your shell/ex
|
|
|
23
23
|
|
|
24
24
|
All outbound interactions go through the \`parall\` CLI. Credentials are pre-injected as environment variables — no setup needed. If \`parall\` is not on PATH, use \`npx --yes @parall/cli@latest\` instead.
|
|
25
25
|
|
|
26
|
-
- \`parall messages send prll://cht_xxx --text
|
|
27
|
-
- \`parall dm prll://usr_xxx --text
|
|
26
|
+
- \`parall messages send prll://cht_xxx --text-file -\` — reply into the triggering chat (pipe the body via a quoted heredoc; see Shell-safety below)
|
|
27
|
+
- \`parall dm prll://usr_xxx --text-file - [--no-reply]\` — direct message another user
|
|
28
28
|
- \`parall tasks update prll://tsk_xxx --status in_progress\` — task state
|
|
29
29
|
- \`parall no-reply [--reason "..."]\` — explicitly declare this turn silent (audit signal; not required for silence, just clarifies intent)
|
|
30
30
|
|
|
31
|
+
**Shell-safety — never wrap real message content in double quotes.** Your command runs in a shell, which expands \`$\`, backticks, and \`$(...)\` inside \`"..."\` before the CLI sees them: \`--text "That costs $1,000"\` sends \`That costs ,000\`, and \`--text "$(cmd)"\` executes \`cmd\`. Pass message bodies via \`--text-file <path>\` (write the file first — no shell touches it) or a quoted heredoc that disables expansion:
|
|
32
|
+
|
|
33
|
+
\`\`\`bash
|
|
34
|
+
parall messages send prll://cht_xxx --text-file - <<'EOF'
|
|
35
|
+
That costs $1,000, and $(whoami) stays literal. I'm on it.
|
|
36
|
+
EOF
|
|
37
|
+
\`\`\`
|
|
38
|
+
|
|
39
|
+
Keep \`--text "..."\` for short literals with no \`$\`, backtick, or apostrophe.
|
|
40
|
+
|
|
31
41
|
The bridge injects Parall context via environment variables. The static credentials \`PRLL_API_URL\`, \`PRLL_API_KEY\`, and \`PRLL_ORG_ID\` are always set. \`PRLL_CONTEXT_FILE\` points to a per-session JSON file that the gateway updates each dispatch with \`session_id\`, \`chat_id\`, \`trigger_message_id\`, \`no_reply\`, and \`step_id\` (updated per tool call). The CLI reads this file automatically — you do not need to pass \`--chat\` or \`--session\` explicitly when the context file is present.
|
|
32
42
|
|
|
33
43
|
## Attachments
|
|
@@ -72,7 +82,7 @@ Messages may arrive with a \`[Thread: prll://msg_xxx]\` line in the event block,
|
|
|
72
82
|
|
|
73
83
|
### CLI usage
|
|
74
84
|
- The \`msg_xxx\` from the \`[Thread: prll://msg_xxx]\` line is the thread root ID — pass it as \`--thread-root-id\`
|
|
75
|
-
- Reply in a thread: \`parall messages send <chatId> --text
|
|
85
|
+
- Reply in a thread: \`parall messages send <chatId> --text-file /tmp/reply.md --thread-root-id <msgId>\`
|
|
76
86
|
- Read thread replies: \`parall messages list <chatId> --thread-root-id <msgId>\`
|
|
77
87
|
|
|
78
88
|
### Thread etiquette
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-format.d.ts","sourceRoot":"","sources":["../src/event-format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAS1D,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"event-format.d.ts","sourceRoot":"","sources":["../src/event-format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAS1D,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAkIzD;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAEtE;AAgDD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAK/D;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CAkBnE"}
|
package/dist/event-format.js
CHANGED
|
@@ -93,6 +93,23 @@ export function buildEventBody(event) {
|
|
|
93
93
|
lines.push(`[Attached: ${sanitizeMeta(event.attachedUri)}]`);
|
|
94
94
|
lines.push('', event.body);
|
|
95
95
|
}
|
|
96
|
+
else if (event.type === 'channel_message') {
|
|
97
|
+
// External IM channel: the audience is OUTSIDE Parall. Frame the
|
|
98
|
+
// event so the agent writes for that audience — no prll:// refs, no
|
|
99
|
+
// internal cards/links in the reply body (they are dead links there).
|
|
100
|
+
lines.push(`[Event: channel.message]`);
|
|
101
|
+
const providerLabel = sanitizeMeta(event.channelProvider ?? 'external IM');
|
|
102
|
+
const convLabel = event.channelExternalConversationId
|
|
103
|
+
? `${sanitizeMeta(event.channelExternalConversationId)} (${sanitizeMeta(event.channelConversationType ?? 'conversation')})`
|
|
104
|
+
: sanitizeMeta(event.channelConversationType ?? 'conversation');
|
|
105
|
+
lines.push(`[Channel: ${providerLabel} | conversation: ${convLabel}]`);
|
|
106
|
+
lines.push(`[From: ${sanitizeMeta(event.senderName)} (external user, not a Parall member)]`);
|
|
107
|
+
if (event.channelExternalMessageId) {
|
|
108
|
+
lines.push(`[External message ID: ${sanitizeMeta(event.channelExternalMessageId)}]`);
|
|
109
|
+
}
|
|
110
|
+
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.]`);
|
|
111
|
+
lines.push('', event.body);
|
|
112
|
+
}
|
|
96
113
|
else if (event.type === 'external_trigger') {
|
|
97
114
|
lines.push(`[Event: external.trigger]`);
|
|
98
115
|
lines.push(`[Trigger: prll://${event.targetId}]`);
|
|
@@ -137,7 +154,7 @@ function buildSendMessageHint(event) {
|
|
|
137
154
|
return `\n<system-reminder>To reply on ${where}, run: \`parall comments add --target "${event.replyTargetUri}" --body "..."\` (read the thread first with \`parall comments list --target "${event.replyTargetUri}"\`). To message someone instead, use \`parall messages send\` / \`parall dm\`. Your plain text output is not delivered.</system-reminder>`;
|
|
138
155
|
}
|
|
139
156
|
if (event.targetId.startsWith('cht_')) {
|
|
140
|
-
return `\n<system-reminder>To reply, run
|
|
157
|
+
return `\n<system-reminder>To reply, run \`parall messages send prll://${event.targetId} --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>`;
|
|
141
158
|
}
|
|
142
159
|
if (event.targetId.startsWith('tsk_')) {
|
|
143
160
|
return `\n<system-reminder>To respond, use the CLI: \`parall tasks update\` / \`parall tasks comments add\`. To message someone, use \`parall messages send\` / \`parall dm\`. Your plain text output is not delivered.</system-reminder>`;
|
|
@@ -145,6 +162,24 @@ function buildSendMessageHint(event) {
|
|
|
145
162
|
if (event.targetId.startsWith('sch_')) {
|
|
146
163
|
return `\n<system-reminder>To communicate, use the CLI: \`parall messages send\` / \`parall dm\`. Your plain text output is not delivered.</system-reminder>`;
|
|
147
164
|
}
|
|
165
|
+
if (event.type === 'channel_message') {
|
|
166
|
+
// Provider metadata is best-effort (the gateway's connection lookup can
|
|
167
|
+
// fail) — never instruct the agent to invoke a made-up clip alias.
|
|
168
|
+
const clipLabel = event.channelProvider
|
|
169
|
+
? `the \`${event.channelProvider}\` clip's`
|
|
170
|
+
: "your channel provider clip's";
|
|
171
|
+
const apiLabel = event.channelProvider ?? 'external platform';
|
|
172
|
+
const target = event.channelExternalConversationId
|
|
173
|
+
? `{"chat_id": "${event.channelExternalConversationId}", "text": "..."}`
|
|
174
|
+
: `{"chat_id": "<conversation id>", "text": "..."}`;
|
|
175
|
+
// Offer the in-thread alternative whenever the inbound message id is
|
|
176
|
+
// known — otherwise the hint nudges every threaded conversation toward a
|
|
177
|
+
// new top-level message.
|
|
178
|
+
const threadAlt = event.channelExternalMessageId
|
|
179
|
+
? ` To reply in-thread to this specific message, use {"message_id": "${event.channelExternalMessageId}", "text": "..."} instead.`
|
|
180
|
+
: '';
|
|
181
|
+
return `\n<system-reminder>To reply, invoke ${clipLabel} \`send_message\` command with ${target} — your plain text output is NOT delivered to the external conversation.${threadAlt} The same clip's \`call\` command reaches the wider ${apiLabel} API when needed.</system-reminder>`;
|
|
182
|
+
}
|
|
148
183
|
if (event.type === 'external_trigger' || event.targetId.startsWith('xtr_')) {
|
|
149
184
|
return `\n<system-reminder>This external trigger is incoming-only. Your plain text output is not sent back to the external provider. To communicate in Parall, use \`parall messages send\` / \`parall dm\`; provider-specific outbound actions require a separate capability.</system-reminder>`;
|
|
150
185
|
}
|
package/dist/gateway-base.d.ts
CHANGED
|
@@ -48,6 +48,7 @@ export declare class ParallAgentGateway {
|
|
|
48
48
|
private readonly opts;
|
|
49
49
|
private readonly chatInfoMap;
|
|
50
50
|
private readonly dispatchedTasks;
|
|
51
|
+
private readonly channelConnectionProviders;
|
|
51
52
|
private readonly dispatchedMessages;
|
|
52
53
|
private readonly forkStates;
|
|
53
54
|
private readonly dispatchState;
|
|
@@ -105,6 +106,7 @@ export declare class ParallAgentGateway {
|
|
|
105
106
|
private fetchAndHandleScheduleFire;
|
|
106
107
|
private handleScheduleFire;
|
|
107
108
|
private fetchAndHandleExternalTriggerRun;
|
|
109
|
+
private fetchAndHandleChannelMessage;
|
|
108
110
|
private handleExternalTriggerRun;
|
|
109
111
|
private fetchAndHandleApprovalDecided;
|
|
110
112
|
private catchUpFromDispatch;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway-base.d.ts","sourceRoot":"","sources":["../src/gateway-base.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiC,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,KAAK,EACV,qBAAqB,
|
|
1
|
+
{"version":3,"file":"gateway-base.d.ts","sourceRoot":"","sources":["../src/gateway-base.ts"],"names":[],"mappings":"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"}
|
package/dist/gateway-base.js
CHANGED
|
@@ -53,6 +53,9 @@ function resolveStepTarget(event) {
|
|
|
53
53
|
if (event.type === 'external_trigger' || event.targetId.startsWith('xtr_')) {
|
|
54
54
|
return { target_type: 'external_trigger', target_id: event.targetId };
|
|
55
55
|
}
|
|
56
|
+
if (event.type === 'channel_message' || event.targetId.startsWith('chv_')) {
|
|
57
|
+
return { target_type: 'channel_conversation', target_id: event.targetId };
|
|
58
|
+
}
|
|
56
59
|
if (event.type === 'wiki_comment') {
|
|
57
60
|
// target_id is the full wiki target_uri (scheme-stripped routing key). The
|
|
58
61
|
// server stores target_type freely and only publishes step WS events /
|
|
@@ -115,6 +118,9 @@ export class ParallAgentGateway {
|
|
|
115
118
|
opts;
|
|
116
119
|
chatInfoMap = new Map();
|
|
117
120
|
dispatchedTasks = new Set();
|
|
121
|
+
// connection id → provider alias, for channel_message prompt labeling
|
|
122
|
+
// (stable mapping; avoids one connection fetch per inbound message).
|
|
123
|
+
channelConnectionProviders = new Map();
|
|
118
124
|
dispatchedMessages = new Set();
|
|
119
125
|
forkStates = new Map();
|
|
120
126
|
dispatchState = {
|
|
@@ -302,6 +308,19 @@ export class ParallAgentGateway {
|
|
|
302
308
|
this.opts.log?.error(`external trigger dispatch failed for ${data.source_id}: ${String(err)}`);
|
|
303
309
|
}
|
|
304
310
|
}
|
|
311
|
+
else if (data.event_type === 'channel_message') {
|
|
312
|
+
if (!data.source_id)
|
|
313
|
+
return;
|
|
314
|
+
try {
|
|
315
|
+
const dispatched = await this.fetchAndHandleChannelMessage(data.source_id);
|
|
316
|
+
if (dispatched) {
|
|
317
|
+
this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => { });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
this.opts.log?.error(`channel message dispatch failed for ${data.source_id}: ${String(err)}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
305
324
|
else if (data.event_type === 'approval_decided') {
|
|
306
325
|
if (!data.source_id)
|
|
307
326
|
return;
|
|
@@ -400,9 +419,11 @@ export class ParallAgentGateway {
|
|
|
400
419
|
? 'schedule_fire'
|
|
401
420
|
: event.type === 'external_trigger'
|
|
402
421
|
? 'external_trigger'
|
|
403
|
-
: event.type === '
|
|
404
|
-
? '
|
|
405
|
-
: '
|
|
422
|
+
: event.type === 'channel_message'
|
|
423
|
+
? 'channel_message'
|
|
424
|
+
: event.type === 'approval'
|
|
425
|
+
? 'approval_decided'
|
|
426
|
+
: 'mention',
|
|
406
427
|
trigger_ref: event.type === 'task'
|
|
407
428
|
? { task_id: event.targetId }
|
|
408
429
|
: event.type === 'task_comment'
|
|
@@ -418,9 +439,16 @@ export class ParallAgentGateway {
|
|
|
418
439
|
connection_id: event.externalConnectionId,
|
|
419
440
|
ingress_event_id: event.externalIngressEventId,
|
|
420
441
|
}
|
|
421
|
-
: event.type === '
|
|
422
|
-
? {
|
|
423
|
-
|
|
442
|
+
: event.type === 'channel_message'
|
|
443
|
+
? {
|
|
444
|
+
conversation_id: event.targetId,
|
|
445
|
+
channel_message_id: event.messageId,
|
|
446
|
+
provider: event.channelProvider,
|
|
447
|
+
external_conversation_id: event.channelExternalConversationId,
|
|
448
|
+
}
|
|
449
|
+
: event.type === 'approval'
|
|
450
|
+
? { approval_id: event.messageId }
|
|
451
|
+
: { message_id: event.messageId },
|
|
424
452
|
sender_id: event.senderId,
|
|
425
453
|
sender_name: event.senderName,
|
|
426
454
|
summary: event.body.substring(0, 200),
|
|
@@ -694,7 +722,20 @@ export class ParallAgentGateway {
|
|
|
694
722
|
context: dispatchContext,
|
|
695
723
|
})) {
|
|
696
724
|
if (runtimeEvent.type === 'runtime_session') {
|
|
725
|
+
const priorAgentSessionId = binding?.agentSessionId;
|
|
697
726
|
binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath);
|
|
727
|
+
if (event.targetType === 'channel_conversation' &&
|
|
728
|
+
binding.agentSessionId !== priorAgentSessionId) {
|
|
729
|
+
// Record the durable chv_ ↔ ase_ mapping (ops drill-down from a
|
|
730
|
+
// conversation into its session). Best-effort bookkeeping —
|
|
731
|
+
// never fail the dispatch over it.
|
|
732
|
+
try {
|
|
733
|
+
await this.opts.client.setChannelConversationSession(this.opts.config.org_id, event.targetId, binding.agentSessionId);
|
|
734
|
+
}
|
|
735
|
+
catch (err) {
|
|
736
|
+
this.opts.log?.warn(`failed to record session mapping for channel conversation ${event.targetId}: ${String(err)}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
698
739
|
if (!inputStepsCreated) {
|
|
699
740
|
// Persist input steps for "earlier events" (batched events that arrived
|
|
700
741
|
// while a dispatch was in flight) inside the in-flight window so a
|
|
@@ -1612,6 +1653,88 @@ export class ParallAgentGateway {
|
|
|
1612
1653
|
return true;
|
|
1613
1654
|
return this.handleExternalTriggerRun(run);
|
|
1614
1655
|
}
|
|
1656
|
+
// fetchAndHandleChannelMessage resolves a channel_message dispatch to its
|
|
1657
|
+
// durable ChannelMessage + conversation and hands it to the inbound
|
|
1658
|
+
// pipeline. targetId = the ChannelConversation id, so per-conversation
|
|
1659
|
+
// multi-turn continuity rides the same per-target session mechanics as
|
|
1660
|
+
// chats. Design: docs/engineering-design/external-im-channel-design.md.
|
|
1661
|
+
async fetchAndHandleChannelMessage(messageId) {
|
|
1662
|
+
if (this.shuttingDown)
|
|
1663
|
+
return false;
|
|
1664
|
+
// Capped dedupe (the chat-message path, not the unbounded task set): a busy
|
|
1665
|
+
// external IM conversation would otherwise retain one key per message ever
|
|
1666
|
+
// handled on a long-lived agent. On a RETRYABLE failure the claim is
|
|
1667
|
+
// released so a later dispatch.new / catch-up re-fetches (matching the
|
|
1668
|
+
// chat-message path); a 404/stale result keeps the claim and acks.
|
|
1669
|
+
const claimKey = `channel_message:${messageId}`;
|
|
1670
|
+
if (!this.tryClaimMessage(claimKey))
|
|
1671
|
+
return false;
|
|
1672
|
+
let msg = null;
|
|
1673
|
+
let conv = null;
|
|
1674
|
+
try {
|
|
1675
|
+
msg = await this.opts.client.getChannelMessage(this.opts.config.org_id, messageId);
|
|
1676
|
+
conv = await this.opts.client.getChannelConversation(this.opts.config.org_id, msg.conversation_id);
|
|
1677
|
+
}
|
|
1678
|
+
catch (err) {
|
|
1679
|
+
const status = err?.status;
|
|
1680
|
+
if (status === 404) {
|
|
1681
|
+
this.opts.log?.warn(`channel message ${messageId} not accessible (404), acking stale dispatch`);
|
|
1682
|
+
return true;
|
|
1683
|
+
}
|
|
1684
|
+
this.dispatchedMessages.delete(claimKey);
|
|
1685
|
+
this.opts.log?.warn(`channel message fetch failed for ${messageId}, leaving pending: ${String(err)}`);
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
if (!msg || !conv) {
|
|
1689
|
+
return true;
|
|
1690
|
+
}
|
|
1691
|
+
this.opts.log?.info(`channel message: ${msg.id} (conversation ${conv.id})`);
|
|
1692
|
+
// Resolve the provider from the conversation's connection for prompt
|
|
1693
|
+
// labeling + the reply-clip hint. The connection id → provider mapping
|
|
1694
|
+
// is stable, so a tiny cache avoids one fetch per message.
|
|
1695
|
+
let provider = this.channelConnectionProviders.get(conv.connection_id);
|
|
1696
|
+
if (!provider) {
|
|
1697
|
+
try {
|
|
1698
|
+
const connection = await this.opts.client.getChannelConnection(this.opts.config.org_id, conv.connection_id);
|
|
1699
|
+
provider = connection.provider;
|
|
1700
|
+
this.channelConnectionProviders.set(conv.connection_id, provider);
|
|
1701
|
+
}
|
|
1702
|
+
catch {
|
|
1703
|
+
provider = undefined; // label degrades; reply hint still names the clip generically
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
const event = {
|
|
1707
|
+
type: 'channel_message',
|
|
1708
|
+
targetId: conv.id,
|
|
1709
|
+
targetName: conv.external_user_name || conv.external_conversation_id,
|
|
1710
|
+
targetType: 'channel_conversation',
|
|
1711
|
+
senderId: msg.external_user_id || 'external',
|
|
1712
|
+
senderName: msg.external_user_name || msg.external_user_id || 'external user',
|
|
1713
|
+
messageId: msg.id,
|
|
1714
|
+
body: msg.text,
|
|
1715
|
+
sentAt: msg.received_at,
|
|
1716
|
+
channelProvider: provider,
|
|
1717
|
+
channelConversationType: conv.conversation_type || undefined,
|
|
1718
|
+
channelExternalConversationId: conv.external_conversation_id,
|
|
1719
|
+
channelExternalMessageId: msg.external_message_id,
|
|
1720
|
+
ackSourceType: 'channel_message',
|
|
1721
|
+
ackSourceId: msg.id,
|
|
1722
|
+
};
|
|
1723
|
+
// Release the claim if the event isn't actually dispatched (or throws) so
|
|
1724
|
+
// a retry can re-attempt — same contract as the chat-message path.
|
|
1725
|
+
let dispatched;
|
|
1726
|
+
try {
|
|
1727
|
+
dispatched = await this.handleInboundEvent(event);
|
|
1728
|
+
}
|
|
1729
|
+
catch (err) {
|
|
1730
|
+
this.dispatchedMessages.delete(claimKey);
|
|
1731
|
+
throw err;
|
|
1732
|
+
}
|
|
1733
|
+
if (!dispatched) {
|
|
1734
|
+
this.dispatchedMessages.delete(claimKey);
|
|
1735
|
+
}
|
|
1736
|
+
return dispatched;
|
|
1737
|
+
}
|
|
1615
1738
|
async handleExternalTriggerRun(run) {
|
|
1616
1739
|
if (this.shuttingDown)
|
|
1617
1740
|
return false;
|
|
@@ -1780,6 +1903,9 @@ export class ParallAgentGateway {
|
|
|
1780
1903
|
else if (item.event_type === 'external_trigger' && item.source_id) {
|
|
1781
1904
|
dispatched = await this.fetchAndHandleExternalTriggerRun(item.source_id);
|
|
1782
1905
|
}
|
|
1906
|
+
else if (item.event_type === 'channel_message' && item.source_id) {
|
|
1907
|
+
dispatched = await this.fetchAndHandleChannelMessage(item.source_id);
|
|
1908
|
+
}
|
|
1783
1909
|
else if (item.event_type === 'approval_decided' && item.source_id) {
|
|
1784
1910
|
dispatched = await this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null);
|
|
1785
1911
|
}
|
|
@@ -3,6 +3,7 @@ import type { GatewayLogger } from './dispatch-adapter.js';
|
|
|
3
3
|
export interface PlatformDefaults {
|
|
4
4
|
model: string | null;
|
|
5
5
|
thinkingEffort: string | null;
|
|
6
|
+
modelIsPin?: boolean;
|
|
6
7
|
}
|
|
7
8
|
export interface PlatformConfigManager {
|
|
8
9
|
fetch(): Promise<PlatformDefaults>;
|
|
@@ -13,6 +14,12 @@ export interface PlatformManagementProfile {
|
|
|
13
14
|
machine_id?: string | null;
|
|
14
15
|
model_management?: string | null;
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated Bridges now derive pin-vs-floor via {@link deriveModelIsPin}
|
|
19
|
+
* (presence-gated dual-read of the server's `model_is_pin`). This is retained
|
|
20
|
+
* only as the legacy fallback inside `deriveModelIsPin` (for old servers that
|
|
21
|
+
* omit the field) and for any third-party consumers. Prefer `deriveModelIsPin`.
|
|
22
|
+
*/
|
|
16
23
|
export declare function isPlatformManagedProfile(profile: PlatformManagementProfile | null | undefined): boolean;
|
|
17
24
|
/**
|
|
18
25
|
* Whether the platform-delivered model should be treated as an operator
|
|
@@ -23,6 +30,10 @@ export declare function isPlatformManagedProfile(profile: PlatformManagementProf
|
|
|
23
30
|
* are not. Pass the result as `resolveRuntimeModel`'s first argument. Keeping
|
|
24
31
|
* this derivation in one place avoids drift across the bridges' (re)config
|
|
25
32
|
* sites where the subtle null-hosted-vs-explicit-self distinction matters.
|
|
33
|
+
*
|
|
34
|
+
* @deprecated Used only as the legacy fallback inside {@link deriveModelIsPin}
|
|
35
|
+
* (old servers that omit `model_is_pin`). New code should read `model_is_pin`
|
|
36
|
+
* via `deriveModelIsPin` rather than calling this directly.
|
|
26
37
|
*/
|
|
27
38
|
export declare function isPlatformModelOverride(profile: PlatformManagementProfile | null | undefined): boolean;
|
|
28
39
|
/**
|
|
@@ -40,6 +51,14 @@ export declare function isPlatformModelOverride(profile: PlatformManagementProfi
|
|
|
40
51
|
* the server delivers no floor.
|
|
41
52
|
*/
|
|
42
53
|
export declare function resolveRuntimeModel(isOperatorOverride: boolean, platformModel: string | null, configModel: string | null | undefined): string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Whether the platform-delivered model is an operator PIN (override) vs a catalog
|
|
56
|
+
* FLOOR. Presence-gated dual-read: a new server sends `model_is_pin` (use it); an
|
|
57
|
+
* old server omits it (`undefined`) so we fall back to the legacy
|
|
58
|
+
* `model_management`-derived signal. Pass the result to `resolveRuntimeModel`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function deriveModelIsPin(defaults: PlatformDefaults, profile: PlatformManagementProfile | null | undefined): boolean;
|
|
61
|
+
export declare function extractDefaults(config: Record<string, unknown>, runtimeType: string | undefined): Omit<PlatformDefaults, 'modelIsPin'>;
|
|
43
62
|
export declare function createPlatformConfigManager(opts: {
|
|
44
63
|
client: ParallClient;
|
|
45
64
|
stateDir: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-config.d.ts","sourceRoot":"","sources":["../src/platform-config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAA0B,MAAM,aAAa,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"platform-config.d.ts","sourceRoot":"","sources":["../src/platform-config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAA0B,MAAM,aAAa,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAK9B,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACnC,OAAO,IAAI,gBAAgB,CAAC;IAC5B,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC7C;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,yBAAyB,GAAG,IAAI,GAAG,SAAS,GACpD,OAAO,CAET;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,yBAAyB,GAAG,IAAI,GAAG,SAAS,GACpD,OAAO,CAET;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,kBAAkB,EAAE,OAAO,EAC3B,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC,MAAM,GAAG,SAAS,CAGpB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,yBAAyB,GAAG,IAAI,GAAG,SAAS,GACpD,OAAO,CAET;AA6ED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,IAAI,CAAC,gBAAgB,EAAE,YAAY,CAAC,CAwBtC;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE;IAChD,MAAM,EAAE,YAAY,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,aAAa,CAAC;CACrB,GAAG,qBAAqB,CA4ExB"}
|
package/dist/platform-config.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated Bridges now derive pin-vs-floor via {@link deriveModelIsPin}
|
|
5
|
+
* (presence-gated dual-read of the server's `model_is_pin`). This is retained
|
|
6
|
+
* only as the legacy fallback inside `deriveModelIsPin` (for old servers that
|
|
7
|
+
* omit the field) and for any third-party consumers. Prefer `deriveModelIsPin`.
|
|
8
|
+
*/
|
|
3
9
|
export function isPlatformManagedProfile(profile) {
|
|
4
10
|
return profile?.machine_id != null || profile?.model_management === 'platform';
|
|
5
11
|
}
|
|
@@ -12,6 +18,10 @@ export function isPlatformManagedProfile(profile) {
|
|
|
12
18
|
* are not. Pass the result as `resolveRuntimeModel`'s first argument. Keeping
|
|
13
19
|
* this derivation in one place avoids drift across the bridges' (re)config
|
|
14
20
|
* sites where the subtle null-hosted-vs-explicit-self distinction matters.
|
|
21
|
+
*
|
|
22
|
+
* @deprecated Used only as the legacy fallback inside {@link deriveModelIsPin}
|
|
23
|
+
* (old servers that omit `model_is_pin`). New code should read `model_is_pin`
|
|
24
|
+
* via `deriveModelIsPin` rather than calling this directly.
|
|
15
25
|
*/
|
|
16
26
|
export function isPlatformModelOverride(profile) {
|
|
17
27
|
return isPlatformManagedProfile(profile) && profile?.model_management !== 'self';
|
|
@@ -34,6 +44,15 @@ export function resolveRuntimeModel(isOperatorOverride, platformModel, configMod
|
|
|
34
44
|
const operatorOverride = isOperatorOverride ? platformModel : null;
|
|
35
45
|
return operatorOverride ?? configModel ?? platformModel ?? undefined;
|
|
36
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether the platform-delivered model is an operator PIN (override) vs a catalog
|
|
49
|
+
* FLOOR. Presence-gated dual-read: a new server sends `model_is_pin` (use it); an
|
|
50
|
+
* old server omits it (`undefined`) so we fall back to the legacy
|
|
51
|
+
* `model_management`-derived signal. Pass the result to `resolveRuntimeModel`.
|
|
52
|
+
*/
|
|
53
|
+
export function deriveModelIsPin(defaults, profile) {
|
|
54
|
+
return defaults.modelIsPin !== undefined ? defaults.modelIsPin : isPlatformModelOverride(profile);
|
|
55
|
+
}
|
|
37
56
|
const CACHE_FILENAME = 'parall-platform-config.json';
|
|
38
57
|
const SUPPORTED_SCHEMA_VERSION = 1;
|
|
39
58
|
function cachePath(stateDir) {
|
|
@@ -52,6 +71,7 @@ function saveCache(stateDir, response) {
|
|
|
52
71
|
const cached = {
|
|
53
72
|
version: response.version,
|
|
54
73
|
config: response.config,
|
|
74
|
+
modelIsPin: response.model_is_pin,
|
|
55
75
|
fetchedAt: new Date().toISOString(),
|
|
56
76
|
};
|
|
57
77
|
const filePath = cachePath(stateDir);
|
|
@@ -62,23 +82,46 @@ function saveCache(stateDir, response) {
|
|
|
62
82
|
}
|
|
63
83
|
function runtimeModelName(canonicalModel, runtimeType, config) {
|
|
64
84
|
const runtime = runtimeType?.trim();
|
|
65
|
-
if (!runtime || runtime === 'openclaw')
|
|
85
|
+
if (!runtime || runtime === 'openclaw' || runtime === 'hermes')
|
|
66
86
|
return canonicalModel;
|
|
67
87
|
const models = (config.models ?? {});
|
|
68
88
|
const providers = (models.providers ?? {});
|
|
69
89
|
const parall = (providers.parall ?? {});
|
|
70
90
|
const catalog = Array.isArray(parall.models) ? parall.models : [];
|
|
71
91
|
const match = catalog.find((model) => model.id === canonicalModel);
|
|
72
|
-
|
|
92
|
+
// Unknown to the catalog: let the bridge fall back to env/default.
|
|
93
|
+
if (!match)
|
|
94
|
+
return null;
|
|
95
|
+
const runtimeNames = (match.runtime_names ?? {});
|
|
73
96
|
const runtimeName = runtimeNames[runtime];
|
|
74
|
-
|
|
97
|
+
if (typeof runtimeName === 'string' && runtimeName)
|
|
98
|
+
return runtimeName;
|
|
99
|
+
// Cross-family on the Parall proxy route: this catalog model carries no
|
|
100
|
+
// native name for this runtime's CLI. Fall back to the canonical
|
|
101
|
+
// provider/model id — the Parall proxy accepts it and routes via OpenRouter's
|
|
102
|
+
// universal skin, so the CLI's native wire format still reaches the upstream.
|
|
103
|
+
// Mirrors the server's RuntimeModelName; only reachable on the Parall route
|
|
104
|
+
// (own routes are family-locked at write time to same-family models).
|
|
105
|
+
return canonicalModel;
|
|
75
106
|
}
|
|
76
|
-
|
|
107
|
+
// Exported for unit testing of the proxy-prefix normalization (see
|
|
108
|
+
// test/platform-config.test.mjs). Not part of the bridge-facing API surface.
|
|
109
|
+
// modelIsPin is NOT extracted here — it lives on the response/cache envelope
|
|
110
|
+
// (a sibling of `config`), so callers compose it in themselves.
|
|
111
|
+
export function extractDefaults(config, runtimeType) {
|
|
77
112
|
const agents = (config.agents ?? {});
|
|
78
113
|
const defaults = (agents.defaults ?? {});
|
|
79
114
|
let model = null;
|
|
80
115
|
if (typeof defaults.model === 'string' && defaults.model) {
|
|
81
|
-
|
|
116
|
+
// Strip the proxy prefix before catalog translation. OpenClaw writers may
|
|
117
|
+
// rewrite parall/anthropic/... → parall-anthropic/anthropic/... (prompt-cache
|
|
118
|
+
// transport); both are proxy forms and a shared LKG cache can hold either.
|
|
119
|
+
// Strip parall-anthropic/ first, then any remaining parall/ — sequential
|
|
120
|
+
// (not either/or) and both via the same anchored regex, so a doubly-prefixed
|
|
121
|
+
// value normalizes fully and the two branches can't diverge on edge cases.
|
|
122
|
+
const canonicalModel = defaults.model
|
|
123
|
+
.replace(/^parall-anthropic\//, '')
|
|
124
|
+
.replace(/^parall\//, '');
|
|
82
125
|
model = runtimeModelName(canonicalModel, runtimeType, config);
|
|
83
126
|
}
|
|
84
127
|
let thinkingEffort = null;
|
|
@@ -90,13 +133,30 @@ function extractDefaults(config, runtimeType) {
|
|
|
90
133
|
export function createPlatformConfigManager(opts) {
|
|
91
134
|
const { client, stateDir, runtimeType, log } = opts;
|
|
92
135
|
let cachedVersion;
|
|
93
|
-
let currentDefaults = {
|
|
136
|
+
let currentDefaults = {
|
|
137
|
+
model: null,
|
|
138
|
+
thinkingEffort: null,
|
|
139
|
+
modelIsPin: undefined,
|
|
140
|
+
};
|
|
94
141
|
let currentRawConfig = null;
|
|
95
142
|
const cached = loadCache(stateDir);
|
|
96
143
|
if (cached) {
|
|
97
|
-
|
|
144
|
+
// Pre-model_is_pin cache schema (no modelIsPin field): keep the LKG
|
|
145
|
+
// defaults, but DROP the ETag so the next fetch returns a full 200 and
|
|
146
|
+
// rewrites the cache in the new schema. With the ETag, a server whose
|
|
147
|
+
// config hasn't changed would 304 forever and strand
|
|
148
|
+
// modelIsPin=undefined (permanent legacy model_management fallback) on a
|
|
149
|
+
// fully-upgraded stack — the rollout order "server first, bridge later"
|
|
150
|
+
// makes an old-schema cache holding a CURRENT ETag the normal case.
|
|
151
|
+
// Against an old server (which never sends model_is_pin) this costs one
|
|
152
|
+
// full fetch per process start; acceptable, and disappears once the
|
|
153
|
+
// server is upgraded.
|
|
154
|
+
cachedVersion = cached.modelIsPin === undefined ? undefined : cached.version;
|
|
98
155
|
currentRawConfig = cached.config;
|
|
99
|
-
currentDefaults =
|
|
156
|
+
currentDefaults = {
|
|
157
|
+
...extractDefaults(cached.config, runtimeType),
|
|
158
|
+
modelIsPin: cached.modelIsPin,
|
|
159
|
+
};
|
|
100
160
|
}
|
|
101
161
|
return {
|
|
102
162
|
async fetch() {
|
|
@@ -123,7 +183,10 @@ export function createPlatformConfigManager(opts) {
|
|
|
123
183
|
saveCache(stateDir, fresh);
|
|
124
184
|
cachedVersion = fresh.version;
|
|
125
185
|
currentRawConfig = fresh.config;
|
|
126
|
-
currentDefaults =
|
|
186
|
+
currentDefaults = {
|
|
187
|
+
...extractDefaults(fresh.config, runtimeType),
|
|
188
|
+
modelIsPin: fresh.model_is_pin,
|
|
189
|
+
};
|
|
127
190
|
return currentDefaults;
|
|
128
191
|
},
|
|
129
192
|
current() {
|
|
@@ -18,7 +18,7 @@ export declare function buildIdentity(agent?: AgentIdentity): string;
|
|
|
18
18
|
/** @deprecated Use buildIdentity() instead. Kept for backward compat during migration. */
|
|
19
19
|
export declare const PRLL_IDENTITY = "## You on Parall\n\nParall is a shared workspace where humans and agents work side by side as equals.\nYou are a participant here, not a service. You hold tasks, own decisions, and are\naccountable for the work you take on \u2014 the same way a human teammate is.\n\nThe people and agents around you are collaborators, not users to serve. Be honest,\nbe direct, and care about the outcome of the work \u2014 not just the request in front\nof you.";
|
|
20
20
|
export declare const PRLL_BEHAVIOR = "## How to work here\n\n### Move work forward\nDon't wait for instructions. If you see the next step, take it. If something is\nambiguous, clarify once and proceed. If you're blocked, say what's blocking you\n\u2014 don't go silent. Initiative is expected.\n\n### Work in the open\nNothing you do exists until the system can see it. Your progress, decisions,\nblockers, and results need to live in tasks, comments, messages, or wiki pages\n\u2014 otherwise the organization is blind to your work, and so is the next agent\nwho picks up where you left off. Leave traces as you go, not at the end.\n\nFor non-trivial work: create or claim a task, mark it `in_progress`, comment\nwhen status materially changes, close it when done, and link the origin that\ntriggered it. Details: load the `parall-tasks` skill.\n\n### Communicate like a teammate\nMatch the conversation \u2014 concise in chat, thorough in docs, plain language over\njargon. Say what matters; stop when you're done. Don't narrate every tool call\nor pad replies to seem thorough.\n\n### Respect what's shared\nYou have broad latitude inside your own work. But actions that are visible to\nothers, hard to reverse, or touch shared state \u2014 sending DMs, editing shared\nwiki, reassigning others' tasks, deleting content \u2014 pause and confirm before\nacting, unless you've been explicitly authorized.\n\n### Shared workspace\nOther agents share this workspace. Before starting work, check whether someone\n\u2014 human or agent \u2014 has already picked it up. Coordination beats racing.\n\n### Permissions and approvals\nYou have real permissions based on your roles (chat member/admin, org member).\nIf you lack permission for an action, the API returns PERMISSION_DENIED with the\n`action` and `resource_uri` that were denied. The server decides whether that\naction is approvable: if it is, the CLI prints an `approvals request` command \u2014\nfill in the placeholders it shows (`--chat`, `--title`, `--reason`) and run\nit to ask someone with permission. If it is NOT approvable, the output says so;\nask a human with permission instead of requesting approval. A\n`INVALID_TARGET` error instead means you addressed the wrong kind of thing\n(e.g. a `usr_` id where a chat is expected) \u2014 follow the message (e.g. use\n`dm` for a user). Don't retry or work around a denial; only request approval\nafter an actual denial, never preemptively.\n\n### When in doubt\nPrefer asking over guessing. Prefer \"I don't know\" over fabricating. Your\ncredibility is what you bring to the workspace \u2014 protect it.";
|
|
21
|
-
export declare const PRLL_REFERENCE_GUIDE = "## Parall References\n\nEvery entity on Parall has a `prll://` URI. Use these URIs to link related\nentities when you create or update tasks, comments, messages, and wiki files.\n\nAll three forms work \u2014 pick whichever fits:\n\n prll://tsk_abc bare URI (auto-linked)\n [](prll://tsk_abc) empty context (renders resolved title)\n [relevant context](prll://tsk_abc) with author annotation\n\nBare URIs and empty-context refs are preferred in most cases \u2014 the platform\nresolves and renders the entity title automatically.\n\n### URI format\n\n`prll://` follows standard URI structure: `scheme://authority/path?query#fragment`.\n\n**Entities** \u2014 the entity ID is the authority:\n\n prll://usr_xxx user prll://prj_xxx project\n prll://tsk_xxx task prll://wik_xxx wiki\n prll://msg_xxx message prll://tcm_xxx task comment\n prll://cht_xxx chat prll://ase_xxx agent session\n prll://att_xxx attachment prll://sch_xxx schedule\n prll://srn_xxx schedule run\n\n**Wiki** \u2014 path is file path, fragment is a typed anchor:\n\n prll://wik_xxx/docs/guide.md file\n prll://wik_xxx/docs/guide.md#h=Auth::OAuth heading (:: = hierarchy)\n prll://wik_xxx/src/auth.go?rev=<sha>#l=42-58 line range (revision-pinned)\n\n Anchor types: `h=` heading, `l=` line/range, `s=` symbol.\n Line anchors in persistent content require `?rev=<full-40-char-sha>`.\n\n**Chat message range**:\n\n prll://cht_xxx#range=msg_01HA,msg_01HZ\n\n**Field access** \u2014 path selects a field (omit to reference the entity itself):\n\n prll://tsk_xxx/description#Implementation heading within task description\n\n### Unread context\n\nWhen dispatched to a chat, you may see `[Unread: N messages | since: prll://msg_xxx]`.\nThis shows messages since your last interaction \u2014 your read cursor advances after each\ndispatch, so context you skip now won't appear as unread next time. Use\n`parall messages list <chat> --limit 20` to fetch recent context. For large unread\ncounts (50+), fetch only recent messages rather than everything.\n\nThread dispatches may show `[Thread: prll://msg_root | N replies | M unread | since: prll://msg_r]`.\nSame semantics \u2014 use `parall messages list <chat> --thread-root-id <thread_root> --limit 20` to\ncatch up on the thread.\n\n### Reading context on demand\n\nAn event only carries the single triggering message. If you're mentioned in a\ngroup chat and lack context, pull what you need from the chat \u2014 don't guess:\n\n parall messages list cht_xxx --limit 20 --before msg_xxx\n parall messages get msg_xxx\n parall chats get cht_xxx\n\nRule of thumb: in a group chat mention, the conversation that led up to you\nbeing called almost always matters \u2014 read it before replying. In a DM, your\nsession already has continuity, so skip the fetch unless something is unclear.\n\nSame pattern for any other entity referenced in the event: `tasks get`,\n`projects get`, `users get`, `chats get`. Follow the reflink, don't ask.\n\n### File attachments\n\nMessages may include attachments. They appear in events as:\n\n [Attachment: prll://att_xxx | image/png | 1.2MB | screenshot.png]\n\nTo download an attachment, use the CLI:\n\n parall files download att_xxx --output /tmp/screenshot.png\n\nTo send a file:\n\n parall messages send prll://cht_xxx --file /tmp/output.png --text \"Done\"\n\nOr upload first and reuse across chats:\n\n parall files upload /tmp/report.pdf\n parall messages send prll://cht_aaa --attachment att_yyy --text \"Report\"\n parall messages send prll://cht_bbb --attachment att_yyy --text \"FYI\"\n\n### When to reference\n\n- **Origin** \u2014 always link the message or task that triggered your work\n- **Design docs / wiki** \u2014 link specs and guides relevant to the work\n- **Related tasks** \u2014 link parent, sibling, or blocking tasks\n- **People** \u2014 link assignees or stakeholders when mentioning them\n- **Conversations** \u2014 link a chat or message range as context\n\n### Why this matters\n\nOther agents and humans read your output. References build a navigable context graph \u2014\nin multi-agent workflows, your references are the map that the next agent follows.";
|
|
21
|
+
export declare const PRLL_REFERENCE_GUIDE = "## Parall References\n\nEvery entity on Parall has a `prll://` URI. Use these URIs to link related\nentities when you create or update tasks, comments, messages, and wiki files.\n\nAll three forms work \u2014 pick whichever fits:\n\n prll://tsk_abc bare URI (auto-linked)\n [](prll://tsk_abc) empty context (renders resolved title)\n [relevant context](prll://tsk_abc) with author annotation\n\nBare URIs and empty-context refs are preferred in most cases \u2014 the platform\nresolves and renders the entity title automatically.\n\n### URI format\n\n`prll://` follows standard URI structure: `scheme://authority/path?query#fragment`.\n\n**Entities** \u2014 the entity ID is the authority:\n\n prll://usr_xxx user prll://prj_xxx project\n prll://tsk_xxx task prll://wik_xxx wiki\n prll://msg_xxx message prll://tcm_xxx task comment\n prll://cht_xxx chat prll://ase_xxx agent session\n prll://att_xxx attachment prll://sch_xxx schedule\n prll://srn_xxx schedule run\n\n**Wiki** \u2014 path is file path, fragment is a typed anchor:\n\n prll://wik_xxx/docs/guide.md file\n prll://wik_xxx/docs/guide.md#h=Auth::OAuth heading (:: = hierarchy)\n prll://wik_xxx/src/auth.go?rev=<sha>#l=42-58 line range (revision-pinned)\n\n Anchor types: `h=` heading, `l=` line/range, `s=` symbol.\n Line anchors in persistent content require `?rev=<full-40-char-sha>`.\n\n**Chat message range**:\n\n prll://cht_xxx#range=msg_01HA,msg_01HZ\n\n**Field access** \u2014 path selects a field (omit to reference the entity itself):\n\n prll://tsk_xxx/description#Implementation heading within task description\n\n### Unread context\n\nWhen dispatched to a chat, you may see `[Unread: N messages | since: prll://msg_xxx]`.\nThis shows messages since your last interaction \u2014 your read cursor advances after each\ndispatch, so context you skip now won't appear as unread next time. Use\n`parall messages list <chat> --limit 20` to fetch recent context. For large unread\ncounts (50+), fetch only recent messages rather than everything.\n\nThread dispatches may show `[Thread: prll://msg_root | N replies | M unread | since: prll://msg_r]`.\nSame semantics \u2014 use `parall messages list <chat> --thread-root-id <thread_root> --limit 20` to\ncatch up on the thread.\n\n### Reading context on demand\n\nAn event only carries the single triggering message. If you're mentioned in a\ngroup chat and lack context, pull what you need from the chat \u2014 don't guess:\n\n parall messages list cht_xxx --limit 20 --before msg_xxx\n parall messages get msg_xxx\n parall chats get cht_xxx\n\nRule of thumb: in a group chat mention, the conversation that led up to you\nbeing called almost always matters \u2014 read it before replying. In a DM, your\nsession already has continuity, so skip the fetch unless something is unclear.\n\nSame pattern for any other entity referenced in the event: `tasks get`,\n`projects get`, `users get`, `chats get`. Follow the reflink, don't ask.\n\n### File attachments\n\nMessages may include attachments. They appear in events as:\n\n [Attachment: prll://att_xxx | image/png | 1.2MB | screenshot.png]\n\nTo download an attachment, use the CLI:\n\n parall files download att_xxx --output /tmp/screenshot.png\n\nTo send a file:\n\n parall messages send prll://cht_xxx --file /tmp/output.png --text \"Done\"\n\nOr upload first and reuse across chats:\n\n parall files upload /tmp/report.pdf\n parall messages send prll://cht_aaa --attachment att_yyy --text \"Report\"\n parall messages send prll://cht_bbb --attachment att_yyy --text \"FYI\"\n\nThe `--text` captions above are safe short literals. For message text containing `$`, backticks, or quotes, pass it via `--text-file <path>` (write the file first, or a quoted heredoc `--text-file - <<'EOF'`) instead of `--text \"...\"` \u2014 inside double quotes the shell turns `$1,000` into `,000` and executes `$(...)`.\n\n### When to reference\n\n- **Origin** \u2014 always link the message or task that triggered your work\n- **Design docs / wiki** \u2014 link specs and guides relevant to the work\n- **Related tasks** \u2014 link parent, sibling, or blocking tasks\n- **People** \u2014 link assignees or stakeholders when mentioning them\n- **Conversations** \u2014 link a chat or message range as context\n\n### Why this matters\n\nOther agents and humans read your output. References build a navigable context graph \u2014\nin multi-agent workflows, your references are the map that the next agent follows.";
|
|
22
22
|
export type PreparedLocalImage = {
|
|
23
23
|
attachmentId: string;
|
|
24
24
|
fileName: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt-fragments.d.ts","sourceRoot":"","sources":["../src/prompt-fragments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAuBD,wBAAgB,aAAa,CAAC,KAAK,CAAC,EAAE,aAAa,GAAG,MAAM,CAgB3D;AAED,0FAA0F;AAC1F,eAAO,MAAM,aAAa,mcAAqB,CAAC;AAEhD,eAAO,MAAM,aAAa,yhFA+CmC,CAAC;AAE9D,eAAO,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"prompt-fragments.d.ts","sourceRoot":"","sources":["../src/prompt-fragments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAuBD,wBAAgB,aAAa,CAAC,KAAK,CAAC,EAAE,aAAa,GAAG,MAAM,CAgB3D;AAED,0FAA0F;AAC1F,eAAO,MAAM,aAAa,mcAAqB,CAAC;AAEhD,eAAO,MAAM,aAAa,yhFA+CmC,CAAC;AAE9D,eAAO,MAAM,oBAAoB,msJAyGkD,CAAC;AAEpF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7B,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAmBnF;AASD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjD"}
|
package/dist/prompt-fragments.js
CHANGED
|
@@ -184,6 +184,8 @@ Or upload first and reuse across chats:
|
|
|
184
184
|
parall messages send prll://cht_aaa --attachment att_yyy --text "Report"
|
|
185
185
|
parall messages send prll://cht_bbb --attachment att_yyy --text "FYI"
|
|
186
186
|
|
|
187
|
+
The \`--text\` captions above are safe short literals. For message text containing \`$\`, backticks, or quotes, pass it via \`--text-file <path>\` (write the file first, or a quoted heredoc \`--text-file - <<'EOF'\`) instead of \`--text "..."\` — inside double quotes the shell turns \`$1,000\` into \`,000\` and executes \`$(...)\`.
|
|
188
|
+
|
|
187
189
|
### When to reference
|
|
188
190
|
|
|
189
191
|
- **Origin** — always link the message or task that triggered your work
|