@parall/agent-core 1.36.1 → 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 +27 -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 +27 -6
- package/src/types.ts +15 -1
package/src/platform-config.ts
CHANGED
|
@@ -6,6 +6,11 @@ import type { GatewayLogger } from './dispatch-adapter.js';
|
|
|
6
6
|
export interface PlatformDefaults {
|
|
7
7
|
model: string | null;
|
|
8
8
|
thinkingEffort: string | null;
|
|
9
|
+
// Optional so adding it didn't break existing constructors of this exported
|
|
10
|
+
// type. Absent/undefined = old server didn't send model_is_pin → fall back to
|
|
11
|
+
// model_management; true = operator PIN (override, beats env); false = catalog
|
|
12
|
+
// FLOOR (loses to env).
|
|
13
|
+
modelIsPin?: boolean;
|
|
9
14
|
}
|
|
10
15
|
|
|
11
16
|
export interface PlatformConfigManager {
|
|
@@ -19,6 +24,12 @@ export interface PlatformManagementProfile {
|
|
|
19
24
|
model_management?: string | null;
|
|
20
25
|
}
|
|
21
26
|
|
|
27
|
+
/**
|
|
28
|
+
* @deprecated Bridges now derive pin-vs-floor via {@link deriveModelIsPin}
|
|
29
|
+
* (presence-gated dual-read of the server's `model_is_pin`). This is retained
|
|
30
|
+
* only as the legacy fallback inside `deriveModelIsPin` (for old servers that
|
|
31
|
+
* omit the field) and for any third-party consumers. Prefer `deriveModelIsPin`.
|
|
32
|
+
*/
|
|
22
33
|
export function isPlatformManagedProfile(
|
|
23
34
|
profile: PlatformManagementProfile | null | undefined,
|
|
24
35
|
): boolean {
|
|
@@ -34,6 +45,10 @@ export function isPlatformManagedProfile(
|
|
|
34
45
|
* are not. Pass the result as `resolveRuntimeModel`'s first argument. Keeping
|
|
35
46
|
* this derivation in one place avoids drift across the bridges' (re)config
|
|
36
47
|
* sites where the subtle null-hosted-vs-explicit-self distinction matters.
|
|
48
|
+
*
|
|
49
|
+
* @deprecated Used only as the legacy fallback inside {@link deriveModelIsPin}
|
|
50
|
+
* (old servers that omit `model_is_pin`). New code should read `model_is_pin`
|
|
51
|
+
* via `deriveModelIsPin` rather than calling this directly.
|
|
37
52
|
*/
|
|
38
53
|
export function isPlatformModelOverride(
|
|
39
54
|
profile: PlatformManagementProfile | null | undefined,
|
|
@@ -64,6 +79,19 @@ export function resolveRuntimeModel(
|
|
|
64
79
|
return operatorOverride ?? configModel ?? platformModel ?? undefined;
|
|
65
80
|
}
|
|
66
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Whether the platform-delivered model is an operator PIN (override) vs a catalog
|
|
84
|
+
* FLOOR. Presence-gated dual-read: a new server sends `model_is_pin` (use it); an
|
|
85
|
+
* old server omits it (`undefined`) so we fall back to the legacy
|
|
86
|
+
* `model_management`-derived signal. Pass the result to `resolveRuntimeModel`.
|
|
87
|
+
*/
|
|
88
|
+
export function deriveModelIsPin(
|
|
89
|
+
defaults: PlatformDefaults,
|
|
90
|
+
profile: PlatformManagementProfile | null | undefined,
|
|
91
|
+
): boolean {
|
|
92
|
+
return defaults.modelIsPin !== undefined ? defaults.modelIsPin : isPlatformModelOverride(profile);
|
|
93
|
+
}
|
|
94
|
+
|
|
67
95
|
interface PlatformModelDef {
|
|
68
96
|
id?: unknown;
|
|
69
97
|
runtime_names?: unknown;
|
|
@@ -72,6 +100,9 @@ interface PlatformModelDef {
|
|
|
72
100
|
interface CachedPlatformConfig {
|
|
73
101
|
version: string;
|
|
74
102
|
config: Record<string, unknown>;
|
|
103
|
+
// Top-level sibling of `config` (model_is_pin lives on the response, not inside
|
|
104
|
+
// config). Old cache files lack it → undefined → bridge falls back.
|
|
105
|
+
modelIsPin?: boolean;
|
|
75
106
|
fetchedAt: string;
|
|
76
107
|
}
|
|
77
108
|
|
|
@@ -95,6 +126,7 @@ function saveCache(stateDir: string, response: PlatformConfigResponse): void {
|
|
|
95
126
|
const cached: CachedPlatformConfig = {
|
|
96
127
|
version: response.version,
|
|
97
128
|
config: response.config,
|
|
129
|
+
modelIsPin: response.model_is_pin,
|
|
98
130
|
fetchedAt: new Date().toISOString(),
|
|
99
131
|
};
|
|
100
132
|
const filePath = cachePath(stateDir);
|
|
@@ -110,25 +142,49 @@ function runtimeModelName(
|
|
|
110
142
|
config: Record<string, unknown>,
|
|
111
143
|
): string | null {
|
|
112
144
|
const runtime = runtimeType?.trim();
|
|
113
|
-
if (!runtime || runtime === 'openclaw') return canonicalModel;
|
|
145
|
+
if (!runtime || runtime === 'openclaw' || runtime === 'hermes') return canonicalModel;
|
|
114
146
|
|
|
115
147
|
const models = (config.models ?? {}) as Record<string, unknown>;
|
|
116
148
|
const providers = (models.providers ?? {}) as Record<string, unknown>;
|
|
117
149
|
const parall = (providers.parall ?? {}) as Record<string, unknown>;
|
|
118
150
|
const catalog = Array.isArray(parall.models) ? (parall.models as PlatformModelDef[]) : [];
|
|
119
151
|
const match = catalog.find((model) => model.id === canonicalModel);
|
|
120
|
-
|
|
152
|
+
// Unknown to the catalog: let the bridge fall back to env/default.
|
|
153
|
+
if (!match) return null;
|
|
154
|
+
const runtimeNames = (match.runtime_names ?? {}) as Record<string, unknown>;
|
|
121
155
|
const runtimeName = runtimeNames[runtime];
|
|
122
|
-
|
|
156
|
+
if (typeof runtimeName === 'string' && runtimeName) return runtimeName;
|
|
157
|
+
// Cross-family on the Parall proxy route: this catalog model carries no
|
|
158
|
+
// native name for this runtime's CLI. Fall back to the canonical
|
|
159
|
+
// provider/model id — the Parall proxy accepts it and routes via OpenRouter's
|
|
160
|
+
// universal skin, so the CLI's native wire format still reaches the upstream.
|
|
161
|
+
// Mirrors the server's RuntimeModelName; only reachable on the Parall route
|
|
162
|
+
// (own routes are family-locked at write time to same-family models).
|
|
163
|
+
return canonicalModel;
|
|
123
164
|
}
|
|
124
165
|
|
|
125
|
-
|
|
166
|
+
// Exported for unit testing of the proxy-prefix normalization (see
|
|
167
|
+
// test/platform-config.test.mjs). Not part of the bridge-facing API surface.
|
|
168
|
+
// modelIsPin is NOT extracted here — it lives on the response/cache envelope
|
|
169
|
+
// (a sibling of `config`), so callers compose it in themselves.
|
|
170
|
+
export function extractDefaults(
|
|
171
|
+
config: Record<string, unknown>,
|
|
172
|
+
runtimeType: string | undefined,
|
|
173
|
+
): Omit<PlatformDefaults, 'modelIsPin'> {
|
|
126
174
|
const agents = (config.agents ?? {}) as Record<string, unknown>;
|
|
127
175
|
const defaults = (agents.defaults ?? {}) as Record<string, unknown>;
|
|
128
176
|
|
|
129
177
|
let model: string | null = null;
|
|
130
178
|
if (typeof defaults.model === 'string' && defaults.model) {
|
|
131
|
-
|
|
179
|
+
// Strip the proxy prefix before catalog translation. OpenClaw writers may
|
|
180
|
+
// rewrite parall/anthropic/... → parall-anthropic/anthropic/... (prompt-cache
|
|
181
|
+
// transport); both are proxy forms and a shared LKG cache can hold either.
|
|
182
|
+
// Strip parall-anthropic/ first, then any remaining parall/ — sequential
|
|
183
|
+
// (not either/or) and both via the same anchored regex, so a doubly-prefixed
|
|
184
|
+
// value normalizes fully and the two branches can't diverge on edge cases.
|
|
185
|
+
const canonicalModel = defaults.model
|
|
186
|
+
.replace(/^parall-anthropic\//, '')
|
|
187
|
+
.replace(/^parall\//, '');
|
|
132
188
|
model = runtimeModelName(canonicalModel, runtimeType, config);
|
|
133
189
|
}
|
|
134
190
|
|
|
@@ -148,14 +204,31 @@ export function createPlatformConfigManager(opts: {
|
|
|
148
204
|
}): PlatformConfigManager {
|
|
149
205
|
const { client, stateDir, runtimeType, log } = opts;
|
|
150
206
|
let cachedVersion: string | undefined;
|
|
151
|
-
let currentDefaults: PlatformDefaults = {
|
|
207
|
+
let currentDefaults: PlatformDefaults = {
|
|
208
|
+
model: null,
|
|
209
|
+
thinkingEffort: null,
|
|
210
|
+
modelIsPin: undefined,
|
|
211
|
+
};
|
|
152
212
|
let currentRawConfig: Record<string, unknown> | null = null;
|
|
153
213
|
|
|
154
214
|
const cached = loadCache(stateDir);
|
|
155
215
|
if (cached) {
|
|
156
|
-
|
|
216
|
+
// Pre-model_is_pin cache schema (no modelIsPin field): keep the LKG
|
|
217
|
+
// defaults, but DROP the ETag so the next fetch returns a full 200 and
|
|
218
|
+
// rewrites the cache in the new schema. With the ETag, a server whose
|
|
219
|
+
// config hasn't changed would 304 forever and strand
|
|
220
|
+
// modelIsPin=undefined (permanent legacy model_management fallback) on a
|
|
221
|
+
// fully-upgraded stack — the rollout order "server first, bridge later"
|
|
222
|
+
// makes an old-schema cache holding a CURRENT ETag the normal case.
|
|
223
|
+
// Against an old server (which never sends model_is_pin) this costs one
|
|
224
|
+
// full fetch per process start; acceptable, and disappears once the
|
|
225
|
+
// server is upgraded.
|
|
226
|
+
cachedVersion = cached.modelIsPin === undefined ? undefined : cached.version;
|
|
157
227
|
currentRawConfig = cached.config;
|
|
158
|
-
currentDefaults =
|
|
228
|
+
currentDefaults = {
|
|
229
|
+
...extractDefaults(cached.config, runtimeType),
|
|
230
|
+
modelIsPin: cached.modelIsPin,
|
|
231
|
+
};
|
|
159
232
|
}
|
|
160
233
|
|
|
161
234
|
return {
|
|
@@ -189,7 +262,10 @@ export function createPlatformConfigManager(opts: {
|
|
|
189
262
|
saveCache(stateDir, fresh);
|
|
190
263
|
cachedVersion = fresh.version;
|
|
191
264
|
currentRawConfig = fresh.config;
|
|
192
|
-
currentDefaults =
|
|
265
|
+
currentDefaults = {
|
|
266
|
+
...extractDefaults(fresh.config, runtimeType),
|
|
267
|
+
modelIsPin: fresh.model_is_pin,
|
|
268
|
+
};
|
|
193
269
|
return currentDefaults;
|
|
194
270
|
},
|
|
195
271
|
|
package/src/prompt-fragments.ts
CHANGED
|
@@ -199,6 +199,8 @@ Or upload first and reuse across chats:
|
|
|
199
199
|
parall messages send prll://cht_aaa --attachment att_yyy --text "Report"
|
|
200
200
|
parall messages send prll://cht_bbb --attachment att_yyy --text "FYI"
|
|
201
201
|
|
|
202
|
+
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 \`$(...)\`.
|
|
203
|
+
|
|
202
204
|
### When to reference
|
|
203
205
|
|
|
204
206
|
- **Origin** — always link the message or task that triggered your work
|
|
@@ -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
|
-
#
|
|
103
|
-
|
|
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
|
|
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
|
|
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\`
|
|
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/src/types.ts
CHANGED
|
@@ -31,6 +31,7 @@ export type ParallEvent = {
|
|
|
31
31
|
| 'wiki_comment'
|
|
32
32
|
| 'schedule'
|
|
33
33
|
| 'external_trigger'
|
|
34
|
+
| 'channel_message'
|
|
34
35
|
| 'approval';
|
|
35
36
|
targetId: string;
|
|
36
37
|
targetName?: string;
|
|
@@ -65,10 +66,23 @@ export type ParallEvent = {
|
|
|
65
66
|
externalConnectionDisplayName?: string;
|
|
66
67
|
externalIngressEventId?: string;
|
|
67
68
|
externalIngressEventType?: string;
|
|
69
|
+
/** External IM channel metadata, used for channel_message events. */
|
|
70
|
+
channelProvider?: string;
|
|
71
|
+
channelConversationType?: string;
|
|
72
|
+
/** Provider-side conversation id (the send_message target). */
|
|
73
|
+
channelExternalConversationId?: string;
|
|
74
|
+
/** Provider-side message id (in-thread reply target). */
|
|
75
|
+
channelExternalMessageId?: string;
|
|
68
76
|
/** Original event timestamp (e.g., message.created_at). When present,
|
|
69
77
|
* input steps use this instead of server insertion time for ordering. */
|
|
70
78
|
sentAt?: string;
|
|
71
|
-
ackSourceType?:
|
|
79
|
+
ackSourceType?:
|
|
80
|
+
| 'message'
|
|
81
|
+
| 'task_activity'
|
|
82
|
+
| 'comment'
|
|
83
|
+
| 'schedule_run'
|
|
84
|
+
| 'external_trigger_run'
|
|
85
|
+
| 'channel_message';
|
|
72
86
|
ackSourceId?: string;
|
|
73
87
|
/** Unread message count in the target chat since agent's last interaction. */
|
|
74
88
|
unreadCount?: number;
|