@parall/agent-core 1.36.1 → 1.38.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.
Files changed (46) hide show
  1. package/dist/bridge-workspace.d.ts +1 -1
  2. package/dist/bridge-workspace.d.ts.map +1 -1
  3. package/dist/bridge-workspace.js +13 -3
  4. package/dist/dispatch-adapter.d.ts +6 -0
  5. package/dist/dispatch-adapter.d.ts.map +1 -1
  6. package/dist/event-format.d.ts.map +1 -1
  7. package/dist/event-format.js +36 -1
  8. package/dist/gateway-base.d.ts +56 -0
  9. package/dist/gateway-base.d.ts.map +1 -1
  10. package/dist/gateway-base.js +459 -97
  11. package/dist/gateway-lane-flow.d.ts +74 -0
  12. package/dist/gateway-lane-flow.d.ts.map +1 -0
  13. package/dist/gateway-lane-flow.js +167 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -0
  17. package/dist/lane-key.d.ts +45 -0
  18. package/dist/lane-key.d.ts.map +1 -0
  19. package/dist/lane-key.js +34 -0
  20. package/dist/lane-ledger.d.ts +112 -0
  21. package/dist/lane-ledger.d.ts.map +1 -0
  22. package/dist/lane-ledger.js +333 -0
  23. package/dist/platform-config.d.ts +19 -0
  24. package/dist/platform-config.d.ts.map +1 -1
  25. package/dist/platform-config.js +72 -9
  26. package/dist/prompt-fragments.d.ts +1 -1
  27. package/dist/prompt-fragments.d.ts.map +1 -1
  28. package/dist/prompt-fragments.js +2 -0
  29. package/dist/skills/parall-platform.d.ts +1 -1
  30. package/dist/skills/parall-platform.d.ts.map +1 -1
  31. package/dist/skills/parall-platform.js +27 -6
  32. package/dist/types.d.ts +11 -2
  33. package/dist/types.d.ts.map +1 -1
  34. package/package.json +2 -2
  35. package/src/bridge-workspace.ts +13 -3
  36. package/src/dispatch-adapter.ts +6 -0
  37. package/src/event-format.ts +38 -1
  38. package/src/gateway-base.ts +637 -143
  39. package/src/gateway-lane-flow.ts +235 -0
  40. package/src/index.ts +2 -0
  41. package/src/lane-key.ts +67 -0
  42. package/src/lane-ledger.ts +370 -0
  43. package/src/platform-config.ts +85 -9
  44. package/src/prompt-fragments.ts +2 -0
  45. package/src/skills/parall-platform.ts +27 -6
  46. package/src/types.ts +17 -1
@@ -98,16 +98,35 @@ original discussion and its approval/rejection IS the precedent.
98
98
 
99
99
  Each \`[Event: message.new]\` includes \`[Chat: ... (prll://cht_xxx)]\` — use that chat URI to reply.
100
100
 
101
+ > **How you pass the message body matters — your command runs through a shell.**
102
+ > Inside double quotes the shell expands \`$\`, backticks, and \`$(...)\` *before*
103
+ > the CLI sees them: \`--text "That costs $1,000"\` sends \`That costs ,000\`, and
104
+ > \`--text "$(cmd)"\` runs \`cmd\`. Single quotes instead break on apostrophes
105
+ > (\`I'm\`, \`don't\`). So do **not** wrap real message content in quotes — pass it
106
+ > through \`--text-file\` (a written file, or a quoted heredoc \`<<'EOF'\` that
107
+ > disables all expansion). Reserve \`--text "..."\` for short literals with no
108
+ > \`$\`, backtick, or apostrophe.
109
+
101
110
  \`\`\`bash
102
- # Reply to a chat (use the chat URI from the event)
103
- parall messages send prll://cht_xxx --text "Your reply"
111
+ # One-off reply quoted heredoc into stdin. The quoted delimiter <<'EOF'
112
+ # disables ALL shell expansion, so $, backticks and apostrophes pass verbatim.
113
+ parall messages send prll://cht_xxx --text-file - <<'PARALL_EOF'
114
+ Sure — that's $1,000, and $(whoami) stays literal. I'm on it.
115
+ PARALL_EOF
116
+
117
+ # Longer / multi-line reply → write it with your file tool (no shell touches
118
+ # the body), then point --text-file at the file.
119
+ parall messages send prll://cht_xxx --text-file /tmp/reply.md
120
+
121
+ # Short literal with no $, backtick, or apostrophe → --text is fine.
122
+ parall messages send prll://cht_xxx --text "On it"
104
123
 
105
- # Direct message by user URI or display name
106
- parall dm prll://usr_xxx --text "Hello"
124
+ # Direct message by user URI or display name (same --text-file / heredoc rules)
125
+ parall dm prll://usr_xxx --text-file /tmp/reply.md
107
126
  parall dm "Alice" --text "Hello"
108
127
 
109
128
  # Thread reply
110
- parall messages send prll://cht_xxx --text "Reply" --thread-root-id 01JWC...
129
+ parall messages send prll://cht_xxx --text-file /tmp/reply.md --thread-root-id 01JWC...
111
130
 
112
131
  # FYI message (no response expected — the recipient sees \`[Hint: no_reply]\`)
113
132
  parall messages send prll://cht_xxx --text "FYI: done" --no-reply
@@ -139,7 +158,9 @@ parall messages send prll://cht_xxx --attachment att_xxx --text "See attached"
139
158
  parall dm "Alice" --file /tmp/report.pdf --text "Report attached"
140
159
  \`\`\`
141
160
 
142
- \`--file\` and \`--attachment\` are mutually exclusive. \`--text\` can be combined with either.
161
+ \`--file\` and \`--attachment\` are mutually exclusive. A caption (\`--text\` for
162
+ short literals, or \`--text-file\` for anything with \`$\`, backticks, or quotes)
163
+ can be combined with either.
143
164
 
144
165
  ## Approvals
145
166
 
package/dist/types.d.ts CHANGED
@@ -26,7 +26,7 @@ export type DispatchState = {
26
26
  };
27
27
  /** Normalized inbound event from Parall. */
28
28
  export type ParallEvent = {
29
- type: 'message' | 'task' | 'task_comment' | 'wiki_comment' | 'schedule' | 'external_trigger' | 'approval';
29
+ type: 'message' | 'task' | 'task_comment' | 'wiki_comment' | 'schedule' | 'external_trigger' | 'channel_message' | 'approval';
30
30
  targetId: string;
31
31
  targetName?: string;
32
32
  targetType?: string;
@@ -60,11 +60,20 @@ export type ParallEvent = {
60
60
  externalConnectionDisplayName?: string;
61
61
  externalIngressEventId?: string;
62
62
  externalIngressEventType?: string;
63
+ /** External IM channel metadata, used for channel_message events. */
64
+ channelProvider?: string;
65
+ channelConversationType?: string;
66
+ /** Provider-side conversation id (the send_message target). */
67
+ channelExternalConversationId?: string;
68
+ /** Provider-side message id (in-thread reply target). */
69
+ channelExternalMessageId?: string;
63
70
  /** Original event timestamp (e.g., message.created_at). When present,
64
71
  * input steps use this instead of server insertion time for ordering. */
65
72
  sentAt?: string;
66
- ackSourceType?: 'message' | 'task_activity' | 'comment' | 'schedule_run' | 'external_trigger_run';
73
+ ackSourceType?: 'message' | 'task_activity' | 'comment' | 'schedule_run' | 'external_trigger_run' | 'channel_message';
67
74
  ackSourceId?: string;
75
+ /** WorkItem id, when known (dispatch catch-up / re-drive hints carry it; live message.new does not). */
76
+ dispatchEventId?: string;
68
77
  /** Unread message count in the target chat since agent's last interaction. */
69
78
  unreadCount?: number;
70
79
  /** Channel cursor: the last message ID the agent read. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,MAAM,MAAM,UAAU,GAAG;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACjE,mFAAmF;IACnF,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,2DAA2D;AAC3D,MAAM,MAAM,aAAa,GAAG;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,kBAAkB,EAAE,UAAU,EAAE,CAAC;IACjC,UAAU,EAAE,WAAW,EAAE,CAAC;IAC1B,oFAAoF;IACpF,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EACA,SAAS,GACT,MAAM,GACN,cAAc,GACd,cAAc,GACd,UAAU,GACV,kBAAkB,GAClB,UAAU,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,KAAK,CAAC;QAClB,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;IACH,8DAA8D;IAC9D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC;8EAC0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,SAAS,GAAG,eAAe,GAAG,SAAS,GAAG,cAAc,GAAG,sBAAsB,CAAC;IAClG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/agent-core",
3
- "version": "1.36.1",
3
+ "version": "1.38.0",
4
4
  "description": "Shared agent runtime orchestration helpers for Parall",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "@opentelemetry/sdk-logs": "^0.57.0",
36
36
  "@opentelemetry/sdk-metrics": "^1.30.0",
37
37
  "@opentelemetry/sdk-trace-node": "^1.30.0",
38
- "@parall/sdk": "1.36.1"
38
+ "@parall/sdk": "1.38.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^22.0.0",
@@ -24,11 +24,21 @@ To say something in a chat, you **must** invoke the Parall CLI via your shell/ex
24
24
 
25
25
  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.
26
26
 
27
- - \`parall messages send prll://cht_xxx --text "..."\` — reply into the triggering chat
28
- - \`parall dm prll://usr_xxx --text "..." [--no-reply]\` — direct message another user
27
+ - \`parall messages send prll://cht_xxx --text-file -\` — reply into the triggering chat (pipe the body via a quoted heredoc; see Shell-safety below)
28
+ - \`parall dm prll://usr_xxx --text-file - [--no-reply]\` — direct message another user
29
29
  - \`parall tasks update prll://tsk_xxx --status in_progress\` — task state
30
30
  - \`parall no-reply [--reason "..."]\` — explicitly declare this turn silent (audit signal; not required for silence, just clarifies intent)
31
31
 
32
+ **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:
33
+
34
+ \`\`\`bash
35
+ parall messages send prll://cht_xxx --text-file - <<'EOF'
36
+ That costs $1,000, and $(whoami) stays literal. I'm on it.
37
+ EOF
38
+ \`\`\`
39
+
40
+ Keep \`--text "..."\` for short literals with no \`$\`, backtick, or apostrophe.
41
+
32
42
  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.
33
43
 
34
44
  ## Attachments
@@ -73,7 +83,7 @@ Messages may arrive with a \`[Thread: prll://msg_xxx]\` line in the event block,
73
83
 
74
84
  ### CLI usage
75
85
  - The \`msg_xxx\` from the \`[Thread: prll://msg_xxx]\` line is the thread root ID — pass it as \`--thread-root-id\`
76
- - Reply in a thread: \`parall messages send <chatId> --text "..." --thread-root-id <msgId>\`
86
+ - Reply in a thread: \`parall messages send <chatId> --text-file /tmp/reply.md --thread-root-id <msgId>\`
77
87
  - Read thread replies: \`parall messages list <chatId> --thread-root-id <msgId>\`
78
88
 
79
89
  ### Thread etiquette
@@ -23,6 +23,12 @@ export type DispatchContext = {
23
23
  contextFilePath?: string;
24
24
  /** @deprecated Use contextFilePath. Kept for runtimes that haven't migrated. */
25
25
  stepIdFilePath?: string;
26
+ /**
27
+ * PRLL_CONTEXT_DIR contract: the per-agent directory holding per-lane
28
+ * dispatch context files (`<lane-key>.json`, see lane-key.ts). Stable for
29
+ * the life of the bridge — safe to pin into spawn env.
30
+ */
31
+ contextDirPath?: string;
26
32
  client: ParallClient;
27
33
  log?: GatewayLogger;
28
34
  };
@@ -89,6 +89,24 @@ export function buildEventBody(event: ParallEvent): string {
89
89
  if (event.scheduledFireAt) lines.push(`[Scheduled at: ${sanitizeMeta(event.scheduledFireAt)}]`);
90
90
  if (event.attachedUri) lines.push(`[Attached: ${sanitizeMeta(event.attachedUri)}]`);
91
91
  lines.push('', event.body);
92
+ } else if (event.type === 'channel_message') {
93
+ // External IM channel: the audience is OUTSIDE Parall. Frame the
94
+ // event so the agent writes for that audience — no prll:// refs, no
95
+ // internal cards/links in the reply body (they are dead links there).
96
+ lines.push(`[Event: channel.message]`);
97
+ const providerLabel = sanitizeMeta(event.channelProvider ?? 'external IM');
98
+ const convLabel = event.channelExternalConversationId
99
+ ? `${sanitizeMeta(event.channelExternalConversationId)} (${sanitizeMeta(event.channelConversationType ?? 'conversation')})`
100
+ : sanitizeMeta(event.channelConversationType ?? 'conversation');
101
+ lines.push(`[Channel: ${providerLabel} | conversation: ${convLabel}]`);
102
+ lines.push(`[From: ${sanitizeMeta(event.senderName)} (external user, not a Parall member)]`);
103
+ if (event.channelExternalMessageId) {
104
+ lines.push(`[External message ID: ${sanitizeMeta(event.channelExternalMessageId)}]`);
105
+ }
106
+ lines.push(
107
+ `[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.]`,
108
+ );
109
+ lines.push('', event.body);
92
110
  } else if (event.type === 'external_trigger') {
93
111
  lines.push(`[Event: external.trigger]`);
94
112
  lines.push(`[Trigger: prll://${event.targetId}]`);
@@ -134,7 +152,7 @@ function buildSendMessageHint(event: ParallEvent): string {
134
152
  }
135
153
 
136
154
  if (event.targetId.startsWith('cht_')) {
137
- return `\n<system-reminder>To reply, run: \`parall messages send prll://${event.targetId} --text "..."\` your plain text output is not delivered to the chat.</system-reminder>`;
155
+ 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>`;
138
156
  }
139
157
 
140
158
  if (event.targetId.startsWith('tsk_')) {
@@ -145,6 +163,25 @@ function buildSendMessageHint(event: ParallEvent): string {
145
163
  return `\n<system-reminder>To communicate, use the CLI: \`parall messages send\` / \`parall dm\`. Your plain text output is not delivered.</system-reminder>`;
146
164
  }
147
165
 
166
+ if (event.type === 'channel_message') {
167
+ // Provider metadata is best-effort (the gateway's connection lookup can
168
+ // fail) — never instruct the agent to invoke a made-up clip alias.
169
+ const clipLabel = event.channelProvider
170
+ ? `the \`${event.channelProvider}\` clip's`
171
+ : "your channel provider clip's";
172
+ const apiLabel = event.channelProvider ?? 'external platform';
173
+ const target = event.channelExternalConversationId
174
+ ? `{"chat_id": "${event.channelExternalConversationId}", "text": "..."}`
175
+ : `{"chat_id": "<conversation id>", "text": "..."}`;
176
+ // Offer the in-thread alternative whenever the inbound message id is
177
+ // known — otherwise the hint nudges every threaded conversation toward a
178
+ // new top-level message.
179
+ const threadAlt = event.channelExternalMessageId
180
+ ? ` To reply in-thread to this specific message, use {"message_id": "${event.channelExternalMessageId}", "text": "..."} instead.`
181
+ : '';
182
+ 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>`;
183
+ }
184
+
148
185
  if (event.type === 'external_trigger' || event.targetId.startsWith('xtr_')) {
149
186
  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
187
  }