@parall/agent-core 1.42.1 → 1.43.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/bin/channel-exec.d.ts +4 -0
- package/dist/bin/channel-exec.d.ts.map +1 -0
- package/dist/bin/channel-exec.js +246 -0
- package/dist/channel-capability.d.ts +16 -0
- package/dist/channel-capability.d.ts.map +1 -0
- package/dist/channel-capability.js +155 -0
- package/dist/channel-token.d.ts +19 -0
- package/dist/channel-token.d.ts.map +1 -0
- package/dist/channel-token.js +73 -0
- package/dist/event-format.d.ts.map +1 -1
- package/dist/event-format.js +21 -16
- package/dist/gateway-base.d.ts +12 -0
- package/dist/gateway-base.d.ts.map +1 -1
- package/dist/gateway-base.js +30 -3
- package/dist/gateway-lane-flow.d.ts +20 -1
- package/dist/gateway-lane-flow.d.ts.map +1 -1
- package/dist/gateway-lane-flow.js +75 -6
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/platform-config.d.ts +15 -0
- package/dist/platform-config.d.ts.map +1 -1
- package/dist/platform-config.js +28 -0
- package/dist/prompt-fragments.d.ts +1 -1
- package/dist/prompt-fragments.d.ts.map +1 -1
- package/dist/prompt-fragments.js +29 -7
- package/dist/skills/index.js +1 -1
- 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 +23 -4
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/bin/channel-exec.ts +262 -0
- package/src/channel-capability.ts +187 -0
- package/src/channel-token.ts +92 -0
- package/src/event-format.ts +21 -16
- package/src/gateway-base.ts +44 -6
- package/src/gateway-lane-flow.ts +89 -4
- package/src/index.ts +3 -0
- package/src/platform-config.ts +44 -0
- package/src/prompt-fragments.ts +29 -7
- package/src/skills/index.ts +1 -1
- package/src/skills/parall-platform.ts +23 -4
- package/src/types.ts +5 -1
package/src/gateway-base.ts
CHANGED
|
@@ -148,6 +148,14 @@ export type ParallGatewayOptions = {
|
|
|
148
148
|
* this directory. Absent → legacy received/ack flow (openclaw / hermes).
|
|
149
149
|
*/
|
|
150
150
|
dispatchContextDir?: string;
|
|
151
|
+
/**
|
|
152
|
+
* Live view of the agent's platform-granted capability keys (bridges wire
|
|
153
|
+
* PlatformConfigManager.capabilities().map(c => c.key)). Read at
|
|
154
|
+
* channel-event build time so the reply hint routes to the capability
|
|
155
|
+
* affordance (e.g. feishu-cli → lark-cli) — the single outbound path.
|
|
156
|
+
* Absent/empty → the hint states outbound is disabled.
|
|
157
|
+
*/
|
|
158
|
+
getCapabilityKeys?: () => string[];
|
|
151
159
|
onConfigUpdate?: (data: AgentConfigUpdateData) => Promise<void> | void;
|
|
152
160
|
onSessionReady?: (state: {
|
|
153
161
|
activeSessionId?: string;
|
|
@@ -293,6 +301,9 @@ export class ParallAgentGateway {
|
|
|
293
301
|
// (stable mapping; avoids one connection fetch per inbound message).
|
|
294
302
|
private readonly channelConnectionProviders = new Map<string, string>();
|
|
295
303
|
private readonly dispatchedMessages = new Set<string>();
|
|
304
|
+
// Per-WorkItem failure backoff for typed dispatch consumption — see
|
|
305
|
+
// LaneFlowHost.typedRedriveBackoff in gateway-lane-flow.ts.
|
|
306
|
+
readonly typedRedriveBackoff = new Map<string, { failures: number; until: number }>();
|
|
296
307
|
private readonly forkStates = new Map<string, ActiveForkState>();
|
|
297
308
|
private readonly dispatchState: DispatchState = {
|
|
298
309
|
mainDispatching: false,
|
|
@@ -448,8 +459,17 @@ export class ParallAgentGateway {
|
|
|
448
459
|
source_id: data.id,
|
|
449
460
|
})
|
|
450
461
|
.then(
|
|
451
|
-
() =>
|
|
452
|
-
() =>
|
|
462
|
+
() => true,
|
|
463
|
+
(err) => {
|
|
464
|
+
// Same contract as ackDispatchEvent: a failed ack is a
|
|
465
|
+
// failed consume (arms backoff) and must free the hot-path
|
|
466
|
+
// dedupe so the re-drive isn't rejected by this pod forever.
|
|
467
|
+
this.dispatchedTasks.delete(`${data.id}:${data.updated_at}`);
|
|
468
|
+
this.opts.log?.warn(
|
|
469
|
+
`dispatch ack failed for task ${data.id}, releasing for re-drive: ${String(err)}`,
|
|
470
|
+
);
|
|
471
|
+
return false;
|
|
472
|
+
},
|
|
453
473
|
);
|
|
454
474
|
},
|
|
455
475
|
);
|
|
@@ -680,17 +700,20 @@ export class ParallAgentGateway {
|
|
|
680
700
|
private consumeTypedDispatch(
|
|
681
701
|
ref: { dispatchEventId?: string; sourceType?: string; sourceId?: string },
|
|
682
702
|
run: (dispatchEventId?: string) => Promise<boolean>,
|
|
683
|
-
ack: (dispatchEventId?: string) => void | Promise<void>,
|
|
703
|
+
ack: (dispatchEventId?: string) => boolean | void | Promise<boolean | void>,
|
|
684
704
|
): Promise<void> {
|
|
685
705
|
return consumeTypedDispatch(this.laneFlowHost(), ref, run, ack);
|
|
686
706
|
}
|
|
687
707
|
|
|
688
708
|
// Typed completion must wait until the administrative ack has either
|
|
689
709
|
// committed or failed. Errors stay best-effort: a failed ack leaves the row
|
|
690
|
-
// received, so Complete releases and re-drives it safely.
|
|
691
|
-
|
|
710
|
+
// received, so Complete releases and re-drives it safely. The boolean
|
|
711
|
+
// outcome feeds the typed-consume backoff — an ack that failed must count
|
|
712
|
+
// as a failed consume, or an ack outage would clear the backoff entry and
|
|
713
|
+
// let the release re-drive spin at wire speed.
|
|
714
|
+
private ackDispatchEvent(dispatchEventId: string, onFailure?: () => void): Promise<boolean> {
|
|
692
715
|
return this.opts.client.ackDispatchByID(this.opts.config.org_id, dispatchEventId).then(
|
|
693
|
-
() =>
|
|
716
|
+
() => true,
|
|
694
717
|
(err) => {
|
|
695
718
|
// Complete will return the still-received item to pending and publish
|
|
696
719
|
// an immediate hint. Free its hot-path claim first, otherwise that
|
|
@@ -699,6 +722,7 @@ export class ParallAgentGateway {
|
|
|
699
722
|
this.opts.log?.warn(
|
|
700
723
|
`dispatch ack failed for ${dispatchEventId}, releasing for re-drive: ${String(err)}`,
|
|
701
724
|
);
|
|
725
|
+
return false;
|
|
702
726
|
},
|
|
703
727
|
);
|
|
704
728
|
}
|
|
@@ -2471,6 +2495,19 @@ export class ParallAgentGateway {
|
|
|
2471
2495
|
}
|
|
2472
2496
|
}
|
|
2473
2497
|
|
|
2498
|
+
// The reply hint routes on the live capability grant: `<provider>-cli`
|
|
2499
|
+
// present → the vendor CLI is on PATH (broker shim) and is THE reply
|
|
2500
|
+
// path; absent → outbound is disabled for this org (flag/connection off)
|
|
2501
|
+
// and the hint must say so instead of pointing at a retired clip. The
|
|
2502
|
+
// provider label lookup above is best-effort/cosmetic — when it fails,
|
|
2503
|
+
// ANY granted `*-cli` capability keeps the hint on the CLI path: a
|
|
2504
|
+
// transient metadata miss must not flip an actively granted agent's
|
|
2505
|
+
// hint to "outbound disabled" and strand a valid external message.
|
|
2506
|
+
const keys = this.opts.getCapabilityKeys?.() ?? [];
|
|
2507
|
+
const cliCapable = provider
|
|
2508
|
+
? keys.includes(`${provider}-cli`)
|
|
2509
|
+
: keys.some((k) => k.endsWith('-cli'));
|
|
2510
|
+
|
|
2474
2511
|
const event: ParallEvent = {
|
|
2475
2512
|
type: 'channel_message',
|
|
2476
2513
|
targetId: conv.id,
|
|
@@ -2485,6 +2522,7 @@ export class ParallAgentGateway {
|
|
|
2485
2522
|
channelConversationType: conv.conversation_type || undefined,
|
|
2486
2523
|
channelExternalConversationId: conv.external_conversation_id,
|
|
2487
2524
|
channelExternalMessageId: msg.external_message_id,
|
|
2525
|
+
channelCliCapable: cliCapable,
|
|
2488
2526
|
ackSourceType: 'channel_message',
|
|
2489
2527
|
ackSourceId: msg.id,
|
|
2490
2528
|
};
|
package/src/gateway-lane-flow.ts
CHANGED
|
@@ -18,6 +18,15 @@ export interface LaneFlowHost {
|
|
|
18
18
|
ledgerDisabled: boolean;
|
|
19
19
|
shuttingDown: boolean;
|
|
20
20
|
dispatchedMessages: Set<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Per-WorkItem failure backoff for typed dispatch consumption. A consume
|
|
23
|
+
* that ends without an ack re-arms the entry; the next attempt for the
|
|
24
|
+
* same WorkItem is delayed (not skipped — a skipped attempt would strand
|
|
25
|
+
* the pending row until reconnect catch-up) so a claim→fail→complete
|
|
26
|
+
* tight loop is throttled to exponential intervals instead of spinning at
|
|
27
|
+
* wire speed against the server's immediate re-drive (2026-07-11 OOM).
|
|
28
|
+
*/
|
|
29
|
+
typedRedriveBackoff: Map<string, { failures: number; until: number }>;
|
|
21
30
|
opts: {
|
|
22
31
|
client: ParallClient;
|
|
23
32
|
log?: GatewayLogger;
|
|
@@ -122,21 +131,91 @@ export async function dispatchLaneGroup(
|
|
|
122
131
|
return 'dispatched';
|
|
123
132
|
}
|
|
124
133
|
|
|
134
|
+
/** Failure backoff pacing for typed dispatch retries (base 2s, cap 5min). */
|
|
135
|
+
const TYPED_BACKOFF_BASE_MS = 2_000;
|
|
136
|
+
const TYPED_BACKOFF_CAP_MS = 5 * 60_000;
|
|
137
|
+
const TYPED_BACKOFF_MAP_CAP = 512;
|
|
138
|
+
|
|
125
139
|
/**
|
|
126
140
|
* Consume one typed dispatch (task/comment/schedule/trigger/approval) under
|
|
127
141
|
* its typed-lane occupancy guard: claim the dsp:<id> lane (skip when another
|
|
128
142
|
* pod holds it or the WorkItem is already resolved), run the handler, ack on
|
|
129
143
|
* success (the doc's option (b): notification delivered, tracked elsewhere),
|
|
130
144
|
* then release the lane. Legacy run+ack flow when the ledger is unavailable.
|
|
145
|
+
*
|
|
146
|
+
* Repeated failures back off: a consume that ends un-acked re-arms the
|
|
147
|
+
* WorkItem's backoff entry, and the next attempt sleeps out the remaining
|
|
148
|
+
* window before claiming. Without this, complete's release re-drives the
|
|
149
|
+
* item instantly and a persistently-failing consume (e.g. buffered behind a
|
|
150
|
+
* saturated fork pool) spins at wire speed — the client half of the
|
|
151
|
+
* 2026-07-11 poison loop (the server half is the redrive budget).
|
|
131
152
|
*/
|
|
132
153
|
export async function consumeTypedDispatch(
|
|
133
154
|
host: LaneFlowHost,
|
|
134
155
|
ref: { dispatchEventId?: string; sourceType?: string; sourceId?: string },
|
|
135
156
|
run: (dispatchEventId?: string) => Promise<boolean>,
|
|
136
|
-
ack: (dispatchEventId?: string) => void | Promise<void>,
|
|
157
|
+
ack: (dispatchEventId?: string) => boolean | void | Promise<boolean | void>,
|
|
137
158
|
): Promise<void> {
|
|
159
|
+
const backoffKey = ref.dispatchEventId ?? `${ref.sourceType}:${ref.sourceId}`;
|
|
160
|
+
const armed = host.typedRedriveBackoff.get(backoffKey);
|
|
161
|
+
if (armed) {
|
|
162
|
+
const waitMs = armed.until - Date.now();
|
|
163
|
+
if (waitMs > 0) {
|
|
164
|
+
host.opts.log?.info(
|
|
165
|
+
`typed dispatch ${backoffKey} backing off ${Math.ceil(waitMs / 1000)}s after ${armed.failures} failed consume(s)`,
|
|
166
|
+
);
|
|
167
|
+
// unref: the wait must never be what keeps the process alive — on
|
|
168
|
+
// SIGTERM the gateway drains and exits while this timer is pending
|
|
169
|
+
// (the re-drive/renotify path re-delivers on the next pod).
|
|
170
|
+
await new Promise<void>((resolve) => {
|
|
171
|
+
const timer = setTimeout(resolve, waitMs);
|
|
172
|
+
timer.unref?.();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (host.shuttingDown) return;
|
|
176
|
+
}
|
|
177
|
+
// An ack callback that returns void (legacy custom acks) counts as success;
|
|
178
|
+
// an explicit false (ackDispatchEvent's failed HTTP ack) is a failed
|
|
179
|
+
// consume — clearing backoff there would let an ack outage re-create the
|
|
180
|
+
// wire-speed release/re-drive loop against a pre-budget server.
|
|
181
|
+
const settleAck = (ackResult: boolean | void) => ackResult !== false;
|
|
182
|
+
const settle = (acked: boolean) => {
|
|
183
|
+
if (acked) {
|
|
184
|
+
host.typedRedriveBackoff.delete(backoffKey);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const failures = (host.typedRedriveBackoff.get(backoffKey)?.failures ?? 0) + 1;
|
|
188
|
+
const backoffMs = Math.min(TYPED_BACKOFF_CAP_MS, TYPED_BACKOFF_BASE_MS * 2 ** (failures - 1));
|
|
189
|
+
// True LRU: delete-then-set moves an updated key to the tail of the
|
|
190
|
+
// Map's insertion order, so capacity eviction always removes the
|
|
191
|
+
// least-recently-FAILING key — an actively-failing old key must not be
|
|
192
|
+
// evicted ahead of a quieter newer one. Evict only when inserting a new
|
|
193
|
+
// key at capacity (an in-place update never shrinks the map).
|
|
194
|
+
if (
|
|
195
|
+
host.typedRedriveBackoff.delete(backoffKey) === false &&
|
|
196
|
+
host.typedRedriveBackoff.size >= TYPED_BACKOFF_MAP_CAP
|
|
197
|
+
) {
|
|
198
|
+
const oldest = host.typedRedriveBackoff.keys().next().value;
|
|
199
|
+
if (oldest !== undefined) host.typedRedriveBackoff.delete(oldest);
|
|
200
|
+
}
|
|
201
|
+
host.typedRedriveBackoff.set(backoffKey, { failures, until: Date.now() + backoffMs });
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Legacy run+ack (no ledger): same settle-in-finally contract as the lane
|
|
205
|
+
// path — a thrown run/ack must arm backoff, not skip it.
|
|
206
|
+
const runLegacy = async () => {
|
|
207
|
+
let acked = false;
|
|
208
|
+
try {
|
|
209
|
+
if (await run(ref.dispatchEventId)) {
|
|
210
|
+
acked = settleAck(await ack(ref.dispatchEventId));
|
|
211
|
+
}
|
|
212
|
+
} finally {
|
|
213
|
+
settle(acked);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
138
217
|
if (!host.laneLedger || host.ledgerDisabled) {
|
|
139
|
-
|
|
218
|
+
await runLegacy();
|
|
140
219
|
return;
|
|
141
220
|
}
|
|
142
221
|
let lane: Awaited<ReturnType<LaneLedger['claimTyped']>>;
|
|
@@ -145,23 +224,29 @@ export async function consumeTypedDispatch(
|
|
|
145
224
|
} catch (err) {
|
|
146
225
|
if (err instanceof LedgerUnsupportedError) {
|
|
147
226
|
host.disableLedger('claim endpoint missing');
|
|
148
|
-
|
|
227
|
+
await runLegacy();
|
|
149
228
|
return;
|
|
150
229
|
}
|
|
151
230
|
throw err;
|
|
152
231
|
}
|
|
153
232
|
if (!lane) {
|
|
233
|
+
// Held elsewhere or already resolved — not a local failure; leave the
|
|
234
|
+
// backoff state as-is (a stale entry is cleared by the next local ack).
|
|
154
235
|
host.opts.log?.info(
|
|
155
236
|
`typed dispatch ${ref.dispatchEventId ?? `${ref.sourceType}:${ref.sourceId}`} not claimable (held elsewhere or already resolved) — skipping`,
|
|
156
237
|
);
|
|
157
238
|
return;
|
|
158
239
|
}
|
|
240
|
+
let acked = false;
|
|
159
241
|
try {
|
|
160
242
|
// Ack must settle before Complete. A fire-and-forget ack races the lane
|
|
161
243
|
// release: Complete can return the still-received typed item to pending
|
|
162
244
|
// and publish a re-drive while its successful ack is still in flight.
|
|
163
|
-
if (await run(lane.typedDispatchEventId))
|
|
245
|
+
if (await run(lane.typedDispatchEventId)) {
|
|
246
|
+
acked = settleAck(await ack(lane.typedDispatchEventId));
|
|
247
|
+
}
|
|
164
248
|
} finally {
|
|
249
|
+
settle(acked);
|
|
165
250
|
// Release the occupancy row. A buffered dispatch may outlive this guard
|
|
166
251
|
// (lane TTL) — acceptable at-least-once; the ledger's resolution paths
|
|
167
252
|
// still dedupe the persistent side effects.
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from './provider-config.js';
|
|
|
2
2
|
export * from './types.js';
|
|
3
3
|
export * from './lane-key.js';
|
|
4
4
|
export type { LaneFlowHost } from './gateway-lane-flow.js';
|
|
5
|
+
export { consumeTypedDispatch } from './gateway-lane-flow.js';
|
|
5
6
|
export * from './session-state.js';
|
|
6
7
|
export * from './routing.js';
|
|
7
8
|
export * from './event-format.js';
|
|
@@ -11,6 +12,8 @@ export * from './dispatch-adapter.js';
|
|
|
11
12
|
export { createLogger, childLogger } from './logger.js';
|
|
12
13
|
export * from './gateway-base.js';
|
|
13
14
|
export * from './platform-config.js';
|
|
15
|
+
export * from './channel-capability.js';
|
|
16
|
+
export * from './channel-token.js';
|
|
14
17
|
export { writeSkillFiles, buildSkillReferences, SKILLS } from './skills/index.js';
|
|
15
18
|
export type { SkillMeta } from './skills/index.js';
|
|
16
19
|
export {
|
package/src/platform-config.ts
CHANGED
|
@@ -17,6 +17,46 @@ export interface PlatformConfigManager {
|
|
|
17
17
|
fetch(): Promise<PlatformDefaults>;
|
|
18
18
|
current(): PlatformDefaults;
|
|
19
19
|
rawConfig(): Record<string, unknown> | null;
|
|
20
|
+
/**
|
|
21
|
+
* Channel capabilities delivered in `agents.capabilities[]` — the
|
|
22
|
+
* declaration plane of the channel-capability broker. Reads the current
|
|
23
|
+
* (possibly LKG-cached) config with NO freshness gate: the credential is
|
|
24
|
+
* pull-at-use (the mint endpoint re-evaluates the grant on every call), so
|
|
25
|
+
* a stale declaration fail-closes there with a self-explanatory 403 — like
|
|
26
|
+
* a stale model, it is suboptimal, never dangerous.
|
|
27
|
+
*/
|
|
28
|
+
capabilities(): AgentCapability[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// One platform-granted capability (agents.capabilities[] in platform-config).
|
|
32
|
+
// `fragment` is the complete system-prompt declaration text; the server is
|
|
33
|
+
// the SSOT for channel knowledge, runtimes just splice it in.
|
|
34
|
+
export interface AgentCapability {
|
|
35
|
+
key: string;
|
|
36
|
+
source: string;
|
|
37
|
+
fragment: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Defensive extraction: entries missing a non-empty string key/fragment are
|
|
41
|
+
// skipped so a malformed or future-shaped payload can never inject a blank
|
|
42
|
+
// declaration into a system prompt.
|
|
43
|
+
export function extractCapabilities(config: Record<string, unknown>): AgentCapability[] {
|
|
44
|
+
const agents = (config.agents ?? {}) as Record<string, unknown>;
|
|
45
|
+
const raw = agents.capabilities;
|
|
46
|
+
if (!Array.isArray(raw)) return [];
|
|
47
|
+
const out: AgentCapability[] = [];
|
|
48
|
+
for (const entry of raw) {
|
|
49
|
+
if (typeof entry !== 'object' || entry === null) continue;
|
|
50
|
+
const e = entry as Record<string, unknown>;
|
|
51
|
+
if (typeof e.key !== 'string' || !e.key) continue;
|
|
52
|
+
if (typeof e.fragment !== 'string' || !e.fragment) continue;
|
|
53
|
+
out.push({
|
|
54
|
+
key: e.key,
|
|
55
|
+
source: typeof e.source === 'string' ? e.source : '',
|
|
56
|
+
fragment: e.fragment,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
20
60
|
}
|
|
21
61
|
|
|
22
62
|
export interface PlatformManagementProfile {
|
|
@@ -276,5 +316,9 @@ export function createPlatformConfigManager(opts: {
|
|
|
276
316
|
rawConfig(): Record<string, unknown> | null {
|
|
277
317
|
return currentRawConfig;
|
|
278
318
|
},
|
|
319
|
+
|
|
320
|
+
capabilities(): AgentCapability[] {
|
|
321
|
+
return currentRawConfig ? extractCapabilities(currentRawConfig) : [];
|
|
322
|
+
},
|
|
279
323
|
};
|
|
280
324
|
}
|
package/src/prompt-fragments.ts
CHANGED
|
@@ -212,10 +212,10 @@ resolves and renders the entity title automatically.
|
|
|
212
212
|
|
|
213
213
|
prll://usr_xxx user prll://prj_xxx project
|
|
214
214
|
prll://tsk_xxx task prll://wik_xxx wiki
|
|
215
|
-
prll://msg_xxx message prll://
|
|
216
|
-
prll://cht_xxx chat prll://
|
|
217
|
-
prll://att_xxx attachment prll://
|
|
218
|
-
|
|
215
|
+
prll://msg_xxx message prll://cmt_xxx comment
|
|
216
|
+
prll://cht_xxx chat prll://tcm_xxx task comment (legacy)
|
|
217
|
+
prll://att_xxx attachment prll://ase_xxx agent session
|
|
218
|
+
prll://sch_xxx schedule prll://srn_xxx schedule run
|
|
219
219
|
|
|
220
220
|
**Wiki** — path is file path, fragment is a typed anchor:
|
|
221
221
|
|
|
@@ -261,15 +261,37 @@ session already has continuity, so skip the fetch unless something is unclear.
|
|
|
261
261
|
|
|
262
262
|
Same pattern for any other entity referenced in the event: \`tasks get\`,
|
|
263
263
|
\`projects get\`, \`users get\`, \`chats get\`. Follow the reflink, don't ask.
|
|
264
|
+
When one entity isn't enough — you need what's *around* it — walk the
|
|
265
|
+
reference graph instead of guessing (see "Walk the reference graph" below).
|
|
264
266
|
|
|
265
267
|
### Find context with search first
|
|
266
268
|
|
|
267
269
|
Reach for unified semantic search before paging chat history:
|
|
268
270
|
|
|
269
|
-
parall search "pricing decision june"
|
|
271
|
+
parall search "pricing decision june" --limit 10
|
|
270
272
|
|
|
271
|
-
It spans messages, tasks, and
|
|
272
|
-
recent flow of one chat, not for discovery.
|
|
273
|
+
It spans messages, tasks, wiki, and comments. Page \`messages list\` only for the
|
|
274
|
+
verbatim recent flow of one chat, not for discovery.
|
|
275
|
+
|
|
276
|
+
### Walk the reference graph
|
|
277
|
+
|
|
278
|
+
References form a traversable graph, and you can query it — don't stop at
|
|
279
|
+
fetching entities one by one:
|
|
280
|
+
|
|
281
|
+
# entity metadata (title, status, preview)
|
|
282
|
+
parall refs resolve prll://tsk_xxx prll://wik_xxx
|
|
283
|
+
# who references this entity
|
|
284
|
+
parall refs backlinks prll://tsk_xxx
|
|
285
|
+
# connected sub-graph around it
|
|
286
|
+
parall refs graph prll://tsk_xxx --depth 2
|
|
287
|
+
|
|
288
|
+
Use \`refs backlinks\` when you need "where is this discussed / used"; use
|
|
289
|
+
\`refs graph\` when you need the full picture around an entity (related tasks,
|
|
290
|
+
docs, conversations — edges carry the author's annotation for why they linked).
|
|
291
|
+
Then \`refs resolve\` the interesting node URIs in one batch to get titles and
|
|
292
|
+
status. \`refs graph\` takes entity-level URIs only (\`prll://wik_xxx\`, not
|
|
293
|
+
\`prll://wik_xxx/docs/a.md\`). All results are filtered to what you can see.
|
|
294
|
+
Details: parall-platform skill.
|
|
273
295
|
|
|
274
296
|
### File attachments
|
|
275
297
|
|
package/src/skills/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ export const SKILLS: SkillMeta[] = [
|
|
|
21
21
|
{
|
|
22
22
|
name: 'parall-platform',
|
|
23
23
|
description:
|
|
24
|
-
"Parall platform queries and lightweight agent provisioning: list org members, agents, chats, read message history, check identity,
|
|
24
|
+
"Parall platform queries and lightweight agent provisioning: list org members, agents, chats, read message history, check identity, create another agent, or walk the prll:// reference graph (resolve URIs, backlinks, multi-hop graph). Use when: user asks about org members, who's online, chat history, agent list, creating an agent, identity/auth questions, or you need to find what an entity is connected to / who references it.",
|
|
25
25
|
content: PARALL_PLATFORM_SKILL,
|
|
26
26
|
},
|
|
27
27
|
{
|
|
@@ -76,11 +76,11 @@ settled questions or repeat known mistakes. This searches live org data
|
|
|
76
76
|
can see.
|
|
77
77
|
|
|
78
78
|
\`\`\`bash
|
|
79
|
-
# Semantic + keyword search across messages, tasks, and
|
|
79
|
+
# Semantic + keyword search across messages, tasks, wiki, and comments
|
|
80
80
|
parall search "auth v5 upgrade"
|
|
81
81
|
|
|
82
|
-
# Restrict entity types (m=message, t=task, w=wiki). --channel
|
|
83
|
-
# MESSAGE hits to one chat (tasks/wiki are unaffected
|
|
82
|
+
# Restrict entity types (m=message, t=task, w=wiki, c=comment). --channel
|
|
83
|
+
# narrows the MESSAGE hits to one chat (tasks/wiki/comments are unaffected).
|
|
84
84
|
parall search "auth v5 upgrade" --types m,w --channel prll://cht_eng
|
|
85
85
|
|
|
86
86
|
# Time-box to recent activity (RFC3339 or YYYY-MM-DD). Narrows messages + tasks;
|
|
@@ -201,6 +201,8 @@ Every entity is addressable with a \`prll://\` URI. Common prefixes you'll see i
|
|
|
201
201
|
| \`prll://usr_\` | User (human or agent) | parall-platform |
|
|
202
202
|
| \`prll://cht_\` | Chat | parall-platform |
|
|
203
203
|
| \`prll://msg_\` | Message | parall-platform |
|
|
204
|
+
| \`prll://cmt_\` | Comment (on tasks, wiki pages, changesets) | by target: task comment → parall-tasks, wiki/changeset comment → parall-wiki |
|
|
205
|
+
| \`prll://ase_\` | Agent session | parall-platform |
|
|
204
206
|
| \`prll://tsk_\` | Task | parall-tasks |
|
|
205
207
|
| \`prll://prj_\` | Project | parall-tasks |
|
|
206
208
|
| \`prll://sch_\` | Schedule (time trigger) | parall-schedules |
|
|
@@ -236,7 +238,24 @@ parall refs graph prll://tsk_xxx --depth 2
|
|
|
236
238
|
|
|
237
239
|
\`refs graph\` traverses both directions (inbound + outbound) and returns \`nodes\`
|
|
238
240
|
and \`edges\` with each node's hop \`depth\`. \`truncated: true\` means a size cap clipped
|
|
239
|
-
the result — narrow it with a smaller \`--depth\`.
|
|
241
|
+
the result — narrow it with a smaller \`--depth\`. Edges carry \`context\` — the
|
|
242
|
+
author's annotation from \`[context](prll://...)\` — telling you *why* two
|
|
243
|
+
entities are linked, not just that they are.
|
|
244
|
+
|
|
245
|
+
The graph returns bare node URIs (no titles). The usual two-step: \`refs graph\`
|
|
246
|
+
for topology, then batch-\`refs resolve\` the node URIs you care about for
|
|
247
|
+
titles/status. If graph rejects your URI with a path/anchor error, strip it to
|
|
248
|
+
the entity root (\`prll://wik_xxx/docs/a.md\` → \`prll://wik_xxx\`) and re-query —
|
|
249
|
+
but note this WIDENS the query to the whole entity, not that one file: the
|
|
250
|
+
graph seeds from the wiki id, so a specific file's outbound links may sit
|
|
251
|
+
deeper in the result (or past the size caps). For refs pointing AT one file
|
|
252
|
+
(inbound), \`refs backlinks\` on the full file URI is precise. There is no
|
|
253
|
+
precise query for one file's OUTBOUND edges today — the widened root graph is
|
|
254
|
+
best-effort for those, or read the file itself for its \`prll://\` links.
|
|
255
|
+
Wiki-file nodes inside a graph *result* do legitimately carry paths.
|
|
256
|
+
|
|
257
|
+
\`refs backlinks\` items include a \`snippet\` of the referencing content — often
|
|
258
|
+
enough to judge relevance without fetching the source entity.
|
|
240
259
|
|
|
241
260
|
CLI success output is JSON. Errors print a JSON line (\`{"error","status","code",...}\`) and, on a \`PERMISSION_DENIED\`, may add a plain-text \`Request approval:\` line — read both.
|
|
242
261
|
`;
|
package/src/types.ts
CHANGED
|
@@ -69,10 +69,14 @@ export type ParallEvent = {
|
|
|
69
69
|
/** External IM channel metadata, used for channel_message events. */
|
|
70
70
|
channelProvider?: string;
|
|
71
71
|
channelConversationType?: string;
|
|
72
|
-
/** Provider-side conversation id (the
|
|
72
|
+
/** Provider-side conversation id (the reply's addressing target). */
|
|
73
73
|
channelExternalConversationId?: string;
|
|
74
74
|
/** Provider-side message id (in-thread reply target). */
|
|
75
75
|
channelExternalMessageId?: string;
|
|
76
|
+
/** Live capability grant: the `<provider>-cli` capability is active, so
|
|
77
|
+
* the vendor CLI (broker shim) is on PATH and is THE reply path. False /
|
|
78
|
+
* absent → outbound is disabled for this org; the hint says so. */
|
|
79
|
+
channelCliCapable?: boolean;
|
|
76
80
|
/** Original event timestamp (e.g., message.created_at). When present,
|
|
77
81
|
* input steps use this instead of server insertion time for ordering. */
|
|
78
82
|
sentAt?: string;
|