@byokit/openclaw 0.23.0 → 0.25.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/CHANGELOG.md +13 -0
- package/README.md +18 -6
- package/dist/bridge.js +17 -8
- package/dist/config.js +3 -2
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +5 -2
- package/dist/delegations.d.ts +20 -2
- package/dist/delegations.js +138 -16
- package/dist/keys.js +14 -2
- package/dist/runs.js +1 -1
- package/dist/testing/fake-gateway.js +16 -6
- package/dist/types.d.ts +4 -3
- package/package.json +2 -2
- package/plugin/catalog.js +13 -0
- package/plugin/index.js +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.25.0 (2026-10-20)
|
|
6
|
+
|
|
7
|
+
- Dependency update: pins @byokit/accounts 0.29.0.
|
|
8
|
+
|
|
9
|
+
- FEAT: a MiniMax API key now lists and runs any model MiniMax's own `/models` lists, with no BYOKit model list: `kit.models({ authChoice: 'minimax-global-api' })` answers the vendor's models, and `addKey({ ..., model })` takes an id the list lags (such as `MiniMax-M3.1-Flash-Preview`) with the route's own capabilities (image input included) instead of refusing it or falling back to `MiniMax-M3`.
|
|
10
|
+
- FIX: When a child a run sent work to (`sessions_send`, `timeoutSeconds: 0`) finished, the engine's own reply run bringing its report back to the sender's session was refused `unknown run` at every app tool. That return run is now admitted as the sender: same member and helper, same app-tool subset and delegates, and the sender's system text as its persona; `RunRef.parent` names the finished child. Any other run on that session is still refused.
|
|
11
|
+
|
|
12
|
+
## 0.24.0 (2026-10-20)
|
|
13
|
+
|
|
14
|
+
- FEAT: a helper's delegated run now speaks as its helper. A native `sessions_send` carries no system text, so the kit's plugin adds the helper's recorded persona (from `ensureHelper`) as system context to the admitted helper child's run, and only to it. No app change: Chief's `helperDelegates` send to `agent:<agent>:h-<helper>:<rest>` is enough. The kit plugin's config entry now sets `hooks.allowConversationAccess: true` for this hook. `sessions_spawn` children get no persona.
|
|
15
|
+
- FEAT: a tracked `sessions_spawn` into the run's own agent (`delegates` naming it, e.g. Chief spawning under its own signed-in agent) reaches the engine as its own-agent spawn, so an app `subagents.allowAgents` list that names only other agents no longer refuses it.
|
|
16
|
+
- FIX: a delegated child the engine no longer knows (as after an engine restart, when its end never reached the kit) now ends `lost` (new `DelegatedEvent.reason`, outcome unknown) at reconnect instead of staying admitted forever, and a dispatch that finds the 1024-entry journal full reconciles with the engine first, so those children can no longer block delegation. One unreadable `agent.wait` no longer fails the whole reconnect.
|
|
17
|
+
|
|
5
18
|
## 0.23.0 (2026-10-20)
|
|
6
19
|
|
|
7
20
|
- FEATURE: A signed-out `claude-cli` run now says why: its end carries `cause: { code: 'needs_cli_login', cli: 'claude', home, configDir?, signIn }` (the new `CliLoginCause` type, also on `./device`). `signIn: true` means `kit.signIn(member, { authChoice: 'anthropic-cli' })` runs the Claude Code login into the kit's own folder (subscription billing); `signIn: false` means the engine reads the person's own Claude Code login, so they run `claude` on that machine. An `anthropic-cli` sign-in the kit cannot run carries the same `cause` on its failed view. `kind`, `message` and every other run end are unchanged.
|
package/README.md
CHANGED
|
@@ -443,14 +443,24 @@ Pass `delegates: { scout: ['report'] }` on `kit.run` to permit native delegation
|
|
|
443
443
|
that app-tool subset. Ensure the target member first and configure the engine's own A2A/visibility/subagent policy;
|
|
444
444
|
this option does not enable engine tools or bypass `host.gate`. The parent subset narrows descendants too.
|
|
445
445
|
Tracked `sessions_send` requires an explicit full `sessionKey` and `timeoutSeconds: 0`; tracked `sessions_spawn`
|
|
446
|
-
requires an explicit allowed `agentId` and native runtime (not ACP).
|
|
446
|
+
requires an explicit allowed `agentId` and native runtime (not ACP). A tracked spawn into the run's own agent
|
|
447
|
+
(`delegates: { chief: [...] }` on a `chief` run) goes to the engine as its own-agent spawn, so an app
|
|
448
|
+
`subagents.allowAgents` list for other agents does not refuse it. Unknown children fail closed.
|
|
447
449
|
|
|
448
450
|
`kit.delegatedRuns()` lists admitted children. `kit.onEvent('byokit.delegated', fn)` reports admission, native streams
|
|
449
451
|
and terminal observations with exact `runId`, `agentId`, `member`, `sessionKey`, `parent` and `root` identity.
|
|
450
452
|
The same identity reaches `ToolHost`; each delegated action has its own gate/approval. Tool arguments/results
|
|
451
453
|
are omitted from this event surface. An accepted native dispatch is not finished work or proof of delivery.
|
|
452
454
|
|
|
453
|
-
|
|
455
|
+
When a sent child finishes, the engine starts its own reply run on the sender's session to bring the report back.
|
|
456
|
+
The kit admits that exact run as the sender: same member and helper, same app-tool subset and delegates, the sender's
|
|
457
|
+
`system` as its persona, `RunRef.parent` naming the finished child. Only a run whose prompt the engine headed with that
|
|
458
|
+
child's session, on the sender's exact key, within a minute of the child's (or the previous return turn's) end, binds;
|
|
459
|
+
any other run there stays unknown. Return runs are not on `delegatedRuns()` or `byokit.delegated`; read them from the
|
|
460
|
+
engine's `agent` events on the sender's key. The pinned engine starts no reply run for a child that took over 30 s.
|
|
461
|
+
|
|
462
|
+
Admitted exact run IDs survive parent settlement and are reconciled on reconnect/start; a child the engine no longer
|
|
463
|
+
knows (as after an engine restart) ends `lost`, and a dispatch that finds the journal full reconciles first. `abort(rootSessionKey)`
|
|
454
464
|
durably revokes its recorded subtree, closes parked gates/active calls, then asks the engine to abort; a child key
|
|
455
465
|
revokes its subtree. An engine resume with a different run ID, or a dispatch with no retained receipt, is not
|
|
456
466
|
silently granted authority. No prefix grants or `allowOnce` bypass are used.
|
|
@@ -496,8 +506,9 @@ above, builtin gating on): the engine's own `sessions_send` to the exact key `ag
|
|
|
496
506
|
(`timeoutSeconds: 0`) is admitted from its receipt as writer's run, `RunRef.helper: 'writer'` on every gate and call.
|
|
497
507
|
A target that is not a helper key, a helper on another account, one never ensured, or one the run was not given is
|
|
498
508
|
refused before dispatch; the parent's tool result reads `session: …`, `bound: …` or `unknown_helper: …`.
|
|
499
|
-
`abort(rootSessionKey)` revokes the children, and a helper whose account is removed loses them at their next call.
|
|
500
|
-
|
|
509
|
+
`abort(rootSessionKey)` revokes the children, and a helper whose account is removed loses them at their next call. A
|
|
510
|
+
native send carries no system text, so the kit adds the helper's recorded persona as system context to the admitted
|
|
511
|
+
child's run; a `sessions_spawn` child gets none.
|
|
501
512
|
|
|
502
513
|
Conversations from before helpers (plain member sessions) move into a helper with
|
|
503
514
|
`kit.adoptHelperSession({ member, sessionKey }, helper)`: the whole transcript forks to
|
|
@@ -510,8 +521,9 @@ both. A helper key, another member's key or a helper never ensured is a typed `H
|
|
|
510
521
|
|
|
511
522
|
`kit.onEvent('byokit.delegated', fn)` follows each helper child start to finish: `admitted` (with `run.helper` and
|
|
512
523
|
`run.tools`, its app-tool subset), the engine's events, then one `ended` whose `reason` is `terminal`, `cancelled` (an
|
|
513
|
-
abort, or its admission ended), `error` (with `message`)
|
|
514
|
-
|
|
524
|
+
abort, or its admission ended), `error` (with `message`), `lost` (the engine no longer knows the run: outcome unknown)
|
|
525
|
+
or `resting`: the model's usage limit ended it, and the kit rests the helper's account `until` the stated reset, as a
|
|
526
|
+
kit run's resting end does. An approval raised by a helper's
|
|
515
527
|
run names it in `Approval.helper`.
|
|
516
528
|
|
|
517
529
|
## Owned browser (unqualified handoff)
|
package/dist/bridge.js
CHANGED
|
@@ -316,28 +316,37 @@ export class Bridge {
|
|
|
316
316
|
return;
|
|
317
317
|
}
|
|
318
318
|
if (message.kind.startsWith('delegation-') && this.delegations) {
|
|
319
|
-
|
|
319
|
+
const delegations = this.delegations;
|
|
320
|
+
void (async () => {
|
|
320
321
|
if (message.kind === 'delegation-prepare') {
|
|
321
322
|
if (typeof message.key !== 'string' || typeof message.runId !== 'string'
|
|
322
|
-
|| (!this.isRegisteredRun(message.key, message.runId) && !
|
|
323
|
+
|| (!this.isRegisteredRun(message.key, message.runId) && !delegations.has(message.key, message.runId)))
|
|
323
324
|
return this.deny(socket, 'unknown delegation parent');
|
|
324
|
-
this.reply(socket, { tracked:
|
|
325
|
+
this.reply(socket, { tracked: await delegations.prepare(message) });
|
|
325
326
|
}
|
|
326
327
|
else if (message.kind === 'delegation-receipt') {
|
|
327
|
-
|
|
328
|
+
delegations.receipt(message);
|
|
328
329
|
this.reply(socket, { ok: true });
|
|
329
330
|
}
|
|
330
331
|
else if (message.kind === 'delegation-event' && isRecord(message.event)) {
|
|
331
|
-
|
|
332
|
+
delegations.event(message.event);
|
|
332
333
|
this.reply(socket, { ok: true });
|
|
333
334
|
}
|
|
335
|
+
// The plugin's prompt hook on a helper key or an inter-session prompt: an admitted helper child speaks as its
|
|
336
|
+
// helper (5.8.3), a return run as the kit run that sent the work (5.8.4).
|
|
337
|
+
else if (message.kind === 'delegation-persona' && typeof message.key === 'string') {
|
|
338
|
+
const runId = typeof message.runId === 'string' ? message.runId : undefined;
|
|
339
|
+
// A kit run carries its own persona; only an engine-created child is looked up (it may wait for its receipt).
|
|
340
|
+
const persona = runId && this.isRegisteredRun(message.key, runId) ? undefined
|
|
341
|
+
: await delegations.persona(message.key, runId, typeof message.header === 'string' ? message.header : undefined);
|
|
342
|
+
this.reply(socket, persona === undefined ? {} : { persona });
|
|
343
|
+
}
|
|
334
344
|
else
|
|
335
345
|
this.deny(socket, 'invalid delegation message');
|
|
336
|
-
}
|
|
337
|
-
catch (error) {
|
|
346
|
+
})().catch((error) => {
|
|
338
347
|
// A typed helper refusal (5.8.3) reaches the parent's tool result with its code.
|
|
339
348
|
this.deny(socket, error instanceof HelperError ? `${error.code}: ${error.message}` : error instanceof Error ? error.message : 'delegation unavailable');
|
|
340
|
-
}
|
|
349
|
+
});
|
|
341
350
|
return;
|
|
342
351
|
}
|
|
343
352
|
if (message.kind === 'gate')
|
package/dist/config.js
CHANGED
|
@@ -100,7 +100,9 @@ export function reconcileConfig(saved, o) {
|
|
|
100
100
|
...o.plugins ?? [],
|
|
101
101
|
...telegram ? ['telegram'] : []])];
|
|
102
102
|
c.plugins.entries ??= {};
|
|
103
|
-
|
|
103
|
+
// Conversation access lets the plugin's prompt hook give an admitted helper child its persona (5.8.3).
|
|
104
|
+
c.plugins.entries[o.pluginId] = merge(c.plugins.entries[o.pluginId] ?? {}, { hooks: { timeouts: { before_tool_call: 200_000 },
|
|
105
|
+
allowConversationAccess: true } });
|
|
104
106
|
if (o.browser) {
|
|
105
107
|
// Replace, never merge caller-controlled profiles/targets. The dead default fails closed without our hook.
|
|
106
108
|
c.browser = { enabled: true, defaultProfile: 'byokit-none', evaluateEnabled: false,
|
|
@@ -108,7 +110,6 @@ export function reconcileConfig(saved, o) {
|
|
|
108
110
|
profiles: { 'byokit-none': { cdpUrl: 'http://127.0.0.1:1', attachOnly: true }, ...o.browser.profiles } };
|
|
109
111
|
c.plugins.allow = [...new Set([...c.plugins.allow, 'browser'])];
|
|
110
112
|
c.plugins.entries.browser = { enabled: true };
|
|
111
|
-
c.plugins.entries[o.pluginId].hooks.allowConversationAccess = true;
|
|
112
113
|
c.tools ??= {};
|
|
113
114
|
const browserTools = [...new Set([...(c.tools.alsoAllow ?? []), 'browser', 'request_sign_in', ...o.browser.tools])];
|
|
114
115
|
// The stock schema rejects allow + alsoAllow in one scope; an explicit closed allow must absorb additions.
|
package/dist/constants.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ export declare const ENGINE_VERSION = "2026.8.35";
|
|
|
2
2
|
export declare const PROTOCOL_VERSION = 4;
|
|
3
3
|
export declare const LIVE_CATALOGS: string[];
|
|
4
4
|
export declare const REGISTERED_CATALOGS: string[];
|
|
5
|
+
export declare const OPEN_CATALOGS: string[];
|
|
5
6
|
export declare const OPERATOR_SCOPES: readonly ['operator.read', 'operator.write', 'operator.admin', 'operator.approvals', 'operator.questions', 'operator.pairing', 'operator.talk'];
|
package/dist/constants.js
CHANGED
|
@@ -4,10 +4,13 @@ export const ENGINE_VERSION = '2026.8.35';
|
|
|
4
4
|
export const PROTOCOL_VERSION = 4;
|
|
5
5
|
// Providers whose live catalog lists and checks a key's chosen model (5.6), through the catalog hook. OpenRouter is
|
|
6
6
|
// pay-as-you-go: its key takes any model its own catalog lists, with no plan to include it.
|
|
7
|
-
export const LIVE_CATALOGS = ['opencode-go', 'openrouter'];
|
|
7
|
+
export const LIVE_CATALOGS = ['opencode-go', 'openrouter', 'minimax'];
|
|
8
8
|
// Live catalogs the kit registers whole. The engine canonicalizes Google preview ids on config write, so OpenRouter's
|
|
9
9
|
// list collides with itself (`google/x-preview` and `google/x`); an OpenRouter add registers only the chosen model.
|
|
10
|
-
export const REGISTERED_CATALOGS = ['opencode-go'];
|
|
10
|
+
export const REGISTERED_CATALOGS = ['opencode-go', 'minimax'];
|
|
11
|
+
// Pay-as-you-go catalogs whose vendor list can lag a model that already runs: an id the list lacks is warned about, never
|
|
12
|
+
// refused, and takes the capabilities of the route's own models.
|
|
13
|
+
export const OPEN_CATALOGS = ['minimax'];
|
|
11
14
|
export const OPERATOR_SCOPES = [
|
|
12
15
|
'operator.read',
|
|
13
16
|
'operator.write',
|
package/dist/delegations.d.ts
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import type { DelegatedEvent, DelegatedRun, GatewayTransport, RunRef } from './types.ts';
|
|
2
|
+
type Back = {
|
|
3
|
+
member: string;
|
|
4
|
+
helper?: string;
|
|
5
|
+
tools: string[];
|
|
6
|
+
helpers?: Record<string, string[]>;
|
|
7
|
+
};
|
|
2
8
|
type Grant = {
|
|
3
9
|
ref: DelegatedRun;
|
|
4
10
|
tools: string[];
|
|
5
11
|
delegates: Record<string, string[]>;
|
|
6
12
|
helpers?: Record<string, string[]>;
|
|
7
13
|
sourceKey: string;
|
|
14
|
+
back?: Back;
|
|
8
15
|
};
|
|
9
16
|
export declare class Delegations {
|
|
10
17
|
private loaded;
|
|
11
18
|
private roots;
|
|
12
19
|
private children;
|
|
13
20
|
private pending;
|
|
21
|
+
private personas;
|
|
22
|
+
private armed;
|
|
23
|
+
private returns;
|
|
14
24
|
private changed;
|
|
15
25
|
private early;
|
|
16
26
|
constructor(privateOptions: {
|
|
@@ -20,6 +30,7 @@ export declare class Delegations {
|
|
|
20
30
|
helper(member: string, helper: string): {
|
|
21
31
|
agentId: string;
|
|
22
32
|
account: string;
|
|
33
|
+
persona: string;
|
|
23
34
|
};
|
|
24
35
|
rest(member: string, account: string, until: number): Promise<void>;
|
|
25
36
|
request: GatewayTransport['request'];
|
|
@@ -30,12 +41,19 @@ export declare class Delegations {
|
|
|
30
41
|
private load;
|
|
31
42
|
private save;
|
|
32
43
|
validate(delegates: Record<string, string[]>, member?: string, helpers?: Record<string, string[]>): void;
|
|
33
|
-
register(ref: RunRef, runId: string, tools: readonly string[] | undefined, delegates: Record<string, string[]>, sourceKey?: string, helpers?: Record<string, string[]
|
|
44
|
+
register(ref: RunRef, runId: string, tools: readonly string[] | undefined, delegates: Record<string, string[]>, sourceKey?: string, helpers?: Record<string, string[]>, system?: string): () => void;
|
|
34
45
|
list(): DelegatedRun[];
|
|
35
46
|
has(key: string, runId: string): boolean;
|
|
36
|
-
|
|
47
|
+
private full;
|
|
48
|
+
prepare(m: Record<string, any>): Promise<boolean>;
|
|
37
49
|
receipt(m: Record<string, any>): void;
|
|
38
50
|
resolve(key: string, runId?: string): Promise<Grant | undefined>;
|
|
51
|
+
/** The persona of the admitted helper child on this exact key and run, as its own run would carry it (5.8.3), or of
|
|
52
|
+
* the return run on its sender's key (5.8.4): `header` is the engine's inter-session header on that run's prompt. */
|
|
53
|
+
persona(key: string, runId?: string, header?: string): Promise<string | undefined>;
|
|
54
|
+
private bind;
|
|
55
|
+
private returned;
|
|
56
|
+
private arm;
|
|
39
57
|
private bound;
|
|
40
58
|
event(event: Record<string, any>): void;
|
|
41
59
|
private ended;
|
package/dist/delegations.js
CHANGED
|
@@ -5,6 +5,11 @@ import { classify } from './classify.js';
|
|
|
5
5
|
import { HELPER_ID, HelperError, helperOf } from './helpers.js';
|
|
6
6
|
// A resting end whose message names no time rests the account this long, as a kit run's does (runs.ts).
|
|
7
7
|
const REST_MS = 300_000;
|
|
8
|
+
// A return binds within this long of its child's end (or of its previous turn's end): the engine's own reply step waits
|
|
9
|
+
// at most 60 s for the child before it starts.
|
|
10
|
+
const RETURN_MS = 60_000;
|
|
11
|
+
// The engine's own header on an inter-session prompt (pin `annotateInterSessionPromptText`), naming the source session.
|
|
12
|
+
const INTER_SESSION = /^\[Inter-session message\] sourceSession=(\S+)(?: sourceChannel=\S+)? sourceTool=sessions_send isUser=false$/;
|
|
8
13
|
const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
9
14
|
const agentOf = (key) => /^agent:([^:]+):.+/.exec(key)?.[1];
|
|
10
15
|
const actionKey = (m) => JSON.stringify([m.key, m.runId, m.callId]);
|
|
@@ -13,6 +18,10 @@ export class Delegations {
|
|
|
13
18
|
roots = new Map();
|
|
14
19
|
children = new Map();
|
|
15
20
|
pending = new Map();
|
|
21
|
+
// In memory only (no prompt is persisted): a kit run's system text by its run id, its return runs' persona.
|
|
22
|
+
personas = new Map();
|
|
23
|
+
armed = [];
|
|
24
|
+
returns = new Map();
|
|
16
25
|
changed = new Set();
|
|
17
26
|
early = new Map();
|
|
18
27
|
constructor(privateOptions) { this.o = privateOptions; }
|
|
@@ -56,9 +65,13 @@ export class Delegations {
|
|
|
56
65
|
this.o.helper(member, helper); // never ensured, or its account gone: unknown_helper
|
|
57
66
|
}
|
|
58
67
|
}
|
|
59
|
-
register(ref, runId, tools, delegates, sourceKey = ref.sessionKey, helpers) {
|
|
68
|
+
register(ref, runId, tools, delegates, sourceKey = ref.sessionKey, helpers, system = '') {
|
|
60
69
|
this.load();
|
|
61
70
|
this.validate(delegates, ref.member, helpers);
|
|
71
|
+
// ponytail: the oldest 256 kit runs' system texts are kept; a child of an older one gets no return run (fail closed).
|
|
72
|
+
this.personas.set(runId, system);
|
|
73
|
+
while (this.personas.size > 256)
|
|
74
|
+
this.personas.delete(this.personas.keys().next().value);
|
|
62
75
|
const root = { ref: { ...ref, runId, root: { sessionKey: ref.sessionKey, runId } },
|
|
63
76
|
tools: [...tools ?? this.o.tools], delegates: structuredClone(delegates), ...(helpers ? { helpers: structuredClone(helpers) } : {}), sourceKey };
|
|
64
77
|
this.roots.set(runId, root);
|
|
@@ -68,11 +81,22 @@ export class Delegations {
|
|
|
68
81
|
list() { this.load(); return [...this.children.values()].map(g => structuredClone(g.ref)); }
|
|
69
82
|
has(key, runId) {
|
|
70
83
|
this.load();
|
|
71
|
-
return (this.roots.get(runId) ?? this.children.get(runId))?.ref.sessionKey === key;
|
|
84
|
+
return (this.roots.get(runId) ?? this.children.get(runId) ?? this.returns.get(runId))?.ref.sessionKey === key;
|
|
72
85
|
}
|
|
73
|
-
|
|
86
|
+
full() {
|
|
87
|
+
for (const [id, p] of this.pending)
|
|
88
|
+
if (p.expires < Date.now())
|
|
89
|
+
this.pending.delete(id);
|
|
90
|
+
return this.pending.size + this.children.size >= 1024;
|
|
91
|
+
}
|
|
92
|
+
async prepare(m) {
|
|
74
93
|
this.load();
|
|
75
|
-
|
|
94
|
+
// Children whose end never reached the kit would fill the journal for good: settle them with the engine first,
|
|
95
|
+
// before the parent is read, so an abort meanwhile is seen.
|
|
96
|
+
// ponytail: a full journal of live children re-reads all 1024 per dispatch; index ends by age if that shows up.
|
|
97
|
+
if (this.full())
|
|
98
|
+
await this.reconcile();
|
|
99
|
+
const parent = this.roots.get(m.runId) ?? this.children.get(m.runId) ?? this.returns.get(m.runId);
|
|
76
100
|
if (!parent || parent.ref.sessionKey !== m.key)
|
|
77
101
|
return false;
|
|
78
102
|
if (typeof m.callId !== 'string' || !m.callId || !object(m.input))
|
|
@@ -98,13 +122,13 @@ export class Delegations {
|
|
|
98
122
|
throw new HelperError('session', `refused: "${String(m.input.sessionKey ?? agent)}" is not a helper session`);
|
|
99
123
|
throw new Error('delegation target or mode refused');
|
|
100
124
|
}
|
|
101
|
-
|
|
102
|
-
if (p.expires < Date.now())
|
|
103
|
-
this.pending.delete(id);
|
|
104
|
-
if (this.pending.size + this.children.size >= 1024)
|
|
125
|
+
if (this.full())
|
|
105
126
|
throw new Error('delegated run journal is full');
|
|
106
|
-
|
|
107
|
-
|
|
127
|
+
const { persona: _, ...grant } = parent; // a return's persona is never persisted
|
|
128
|
+
this.pending.set(actionKey(m), { parent: structuredClone(grant), agent: agent,
|
|
129
|
+
...(send ? { key: m.input.sessionKey } : {}), ...(helper !== undefined ? { helper } : {}),
|
|
130
|
+
// The engine brings a sent child's report back to the sender's key, as a return run (5.8.4); a spawn's is not one.
|
|
131
|
+
...(send && !this.children.has(m.runId) ? { back: true } : {}), expires: Date.now() + 60_000 });
|
|
108
132
|
this.save();
|
|
109
133
|
return true;
|
|
110
134
|
}
|
|
@@ -135,7 +159,9 @@ export class Delegations {
|
|
|
135
159
|
parent: { sessionKey: p.parent.ref.sessionKey, runId: p.parent.ref.runId }, root: p.parent.ref.root, tools };
|
|
136
160
|
const grant = { ref, delegates: p.parent.delegates,
|
|
137
161
|
...(p.parent.helpers && ref.member === p.parent.ref.member ? { helpers: p.parent.helpers } : {}),
|
|
138
|
-
sourceKey: p.parent.sourceKey, tools
|
|
162
|
+
sourceKey: p.parent.sourceKey, tools,
|
|
163
|
+
...(p.back ? { back: { member: p.parent.ref.member, ...(p.parent.ref.helper !== undefined ? { helper: p.parent.ref.helper } : {}),
|
|
164
|
+
tools: p.parent.tools, ...(p.parent.helpers ? { helpers: p.parent.helpers } : {}) } } : {}) };
|
|
139
165
|
// A steered/already-admitted run keeps its original parent and cannot acquire a broader grant.
|
|
140
166
|
if (this.children.has(ref.runId)) {
|
|
141
167
|
this.save();
|
|
@@ -152,6 +178,13 @@ export class Delegations {
|
|
|
152
178
|
this.load();
|
|
153
179
|
if (!runId)
|
|
154
180
|
return undefined;
|
|
181
|
+
const back = this.returns.get(runId);
|
|
182
|
+
if (back?.ref.sessionKey === key) {
|
|
183
|
+
if (back.ref.helper === undefined || this.bound(back.ref.member, back.ref.helper, back.ref.agentId))
|
|
184
|
+
return back;
|
|
185
|
+
this.returned(runId);
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
155
188
|
const get = () => { const g = this.children.get(runId); return g?.ref.sessionKey === key ? g : undefined; };
|
|
156
189
|
if (!get() && [...this.pending.values()].some(p => p.expires > Date.now())) {
|
|
157
190
|
await new Promise(resolve => {
|
|
@@ -171,6 +204,68 @@ export class Delegations {
|
|
|
171
204
|
}
|
|
172
205
|
return grant;
|
|
173
206
|
}
|
|
207
|
+
/** The persona of the admitted helper child on this exact key and run, as its own run would carry it (5.8.3), or of
|
|
208
|
+
* the return run on its sender's key (5.8.4): `header` is the engine's inter-session header on that run's prompt. */
|
|
209
|
+
async persona(key, runId, header) {
|
|
210
|
+
this.load();
|
|
211
|
+
const source = header === undefined ? undefined : INTER_SESSION.exec(header)?.[1];
|
|
212
|
+
if (runId && source !== undefined && !this.returns.has(runId) && !this.children.has(runId))
|
|
213
|
+
await this.bind(key, runId, source);
|
|
214
|
+
const back = runId ? this.returns.get(runId) : undefined;
|
|
215
|
+
if (back?.ref.sessionKey === key)
|
|
216
|
+
return back.persona || undefined;
|
|
217
|
+
if (!/^agent:[^:]+:h-/.test(key))
|
|
218
|
+
return undefined;
|
|
219
|
+
const helper = (await this.resolve(key, runId))?.ref;
|
|
220
|
+
return helper?.helper === undefined ? undefined : this.o.helper(helper.member, helper.helper).persona;
|
|
221
|
+
}
|
|
222
|
+
// The engine's reply step for a sent child starts a fresh run on the sender's exact key, its prompt headed by the
|
|
223
|
+
// child's exact session (pin `runSessionsSendA2AFlow`): bound only to a return armed by that child's own terminal end,
|
|
224
|
+
// one run at a time. A child still running when its report arrives is waited for briefly.
|
|
225
|
+
async bind(key, runId, source) {
|
|
226
|
+
const find = () => {
|
|
227
|
+
const now = Date.now();
|
|
228
|
+
this.armed = this.armed.filter(a => a.expires > now);
|
|
229
|
+
return this.armed.find(a => a.key === key && a.child.sessionKey === source && a.live === undefined);
|
|
230
|
+
};
|
|
231
|
+
if (!find() && [...this.children.values()].some(g => g.back && g.ref.sessionKey === source && g.ref.parent.sessionKey === key)) {
|
|
232
|
+
await new Promise(resolve => {
|
|
233
|
+
const done = () => { clearTimeout(timer); this.changed.delete(check); resolve(); };
|
|
234
|
+
const check = () => { if (find())
|
|
235
|
+
done(); };
|
|
236
|
+
const timer = setTimeout(done, 5_000);
|
|
237
|
+
this.changed.add(check);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const armed = find();
|
|
241
|
+
if (!armed)
|
|
242
|
+
return;
|
|
243
|
+
armed.live = runId;
|
|
244
|
+
this.returns.set(runId, { ...structuredClone(armed.grant), ref: { ...structuredClone(armed.grant.ref), runId }, persona: armed.persona });
|
|
245
|
+
}
|
|
246
|
+
// A return run ended: its report's next turn on the same key may bind within the window (the engine's ping-pong).
|
|
247
|
+
returned(runId) {
|
|
248
|
+
const back = this.returns.get(runId);
|
|
249
|
+
if (!back)
|
|
250
|
+
return;
|
|
251
|
+
this.returns.delete(runId);
|
|
252
|
+
this.o.revoke(back.ref.sessionKey, runId);
|
|
253
|
+
const armed = this.armed.find(a => a.live === runId);
|
|
254
|
+
if (armed) {
|
|
255
|
+
armed.live = undefined;
|
|
256
|
+
armed.expires = Date.now() + RETURN_MS;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
arm(child) {
|
|
260
|
+
const { back, ref } = child, persona = this.personas.get(ref.root.runId);
|
|
261
|
+
if (!back || persona === undefined || this.armed.length >= 64)
|
|
262
|
+
return; // the root's persona unknown (a restart): none
|
|
263
|
+
const key = ref.parent.sessionKey;
|
|
264
|
+
this.armed.push({ key, child: { sessionKey: ref.sessionKey, runId: ref.runId }, persona, expires: Date.now() + RETURN_MS,
|
|
265
|
+
grant: { ref: { sessionKey: key, member: back.member, ...(back.helper !== undefined ? { helper: back.helper } : {}),
|
|
266
|
+
agentId: agentOf(key), parent: { sessionKey: ref.sessionKey, runId: ref.runId }, root: ref.root, tools: back.tools },
|
|
267
|
+
tools: back.tools, delegates: child.delegates, ...(back.helpers ? { helpers: back.helpers } : {}), sourceKey: child.sourceKey } });
|
|
268
|
+
}
|
|
174
269
|
bound(member, helper, agent) {
|
|
175
270
|
try {
|
|
176
271
|
return this.o.helper(member, helper).agentId === agent;
|
|
@@ -181,6 +276,12 @@ export class Delegations {
|
|
|
181
276
|
}
|
|
182
277
|
event(event) {
|
|
183
278
|
this.load();
|
|
279
|
+
const back = this.returns.get(event.runId);
|
|
280
|
+
if (back) {
|
|
281
|
+
if (event.sessionKey === back.ref.sessionKey && event.stream === 'lifecycle' && ['end', 'error'].includes(event.data?.phase))
|
|
282
|
+
this.returned(event.runId);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
184
285
|
const grant = this.children.get(event.runId);
|
|
185
286
|
if (!grant) {
|
|
186
287
|
if (this.pending.size && typeof event.runId === 'string' && this.early.size < 64) {
|
|
@@ -219,22 +320,37 @@ export class Delegations {
|
|
|
219
320
|
}
|
|
220
321
|
end(grant, end) {
|
|
221
322
|
this.children.delete(grant.ref.runId);
|
|
323
|
+
if (end.reason === 'terminal')
|
|
324
|
+
this.arm(grant); // before save: a return run waiting on this end sees it armed
|
|
222
325
|
this.save();
|
|
223
326
|
this.o.revoke(grant.ref.sessionKey, grant.ref.runId);
|
|
224
327
|
this.o.emit({ type: 'ended', run: structuredClone(grant.ref), ...end });
|
|
225
328
|
}
|
|
329
|
+
// An exact recorded run the engine finished ends as it ended. One the engine no longer knows (no job, no queued turn,
|
|
330
|
+
// no live run, as after an engine restart: a plain `timeout` in the `queue` phase that never reached a provider) ends
|
|
331
|
+
// `lost`. A live or queued run stays admitted; a run whose wait fails stays for the next reconcile.
|
|
226
332
|
async reconcile() {
|
|
227
333
|
for (const ref of this.list()) {
|
|
228
|
-
|
|
334
|
+
let end;
|
|
335
|
+
try {
|
|
336
|
+
end = await this.o.request('agent.wait', { runId: ref.runId, timeoutMs: 1 });
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
229
341
|
const grant = this.children.get(ref.runId);
|
|
230
|
-
if (grant
|
|
342
|
+
if (!grant)
|
|
343
|
+
continue;
|
|
344
|
+
if (end.status === 'ok' || end.status === 'error')
|
|
231
345
|
this.end(grant, { reason: end.status === 'ok' ? 'terminal' : 'error' });
|
|
346
|
+
else if (end.status === 'timeout' && end.timeoutPhase === 'queue' && end.providerStarted === false)
|
|
347
|
+
this.end(grant, { reason: 'lost' });
|
|
232
348
|
}
|
|
233
349
|
}
|
|
234
350
|
async abort(key) {
|
|
235
351
|
this.load();
|
|
236
352
|
const ids = new Set();
|
|
237
|
-
for (const g of [...this.roots.values(), ...this.children.values()])
|
|
353
|
+
for (const g of [...this.roots.values(), ...this.children.values(), ...this.returns.values()])
|
|
238
354
|
if (g.ref.sessionKey === key || g.ref.parent?.sessionKey === key || g.ref.root.sessionKey === key || g.sourceKey === key)
|
|
239
355
|
ids.add(g.ref.runId);
|
|
240
356
|
for (;;) {
|
|
@@ -246,6 +362,10 @@ export class Delegations {
|
|
|
246
362
|
break;
|
|
247
363
|
}
|
|
248
364
|
let removed = false;
|
|
365
|
+
const armed = this.armed.length;
|
|
366
|
+
this.armed = this.armed.filter(a => a.key !== key && a.grant.ref.root.sessionKey !== key && !ids.has(a.child.runId) && !ids.has(a.grant.ref.root.runId));
|
|
367
|
+
if (this.armed.length !== armed)
|
|
368
|
+
removed = true;
|
|
249
369
|
for (const [id, p] of this.pending)
|
|
250
370
|
if (ids.has(p.parent.ref.runId) || p.parent.ref.sessionKey === key || p.parent.ref.root.sessionKey === key || p.parent.sourceKey === key) {
|
|
251
371
|
this.pending.delete(id);
|
|
@@ -254,9 +374,11 @@ export class Delegations {
|
|
|
254
374
|
if (!ids.size && !removed)
|
|
255
375
|
return;
|
|
256
376
|
const cancelled = [...this.children.values()].filter(g => ids.has(g.ref.runId));
|
|
257
|
-
const revoked = [...this.roots.values(), ...cancelled].filter(g => ids.has(g.ref.runId));
|
|
258
|
-
for (const id of ids)
|
|
377
|
+
const revoked = [...this.roots.values(), ...this.returns.values(), ...cancelled].filter(g => ids.has(g.ref.runId));
|
|
378
|
+
for (const id of ids) {
|
|
259
379
|
this.roots.delete(id);
|
|
380
|
+
this.returns.delete(id);
|
|
381
|
+
}
|
|
260
382
|
for (const g of cancelled)
|
|
261
383
|
this.children.delete(g.ref.runId);
|
|
262
384
|
this.save(); // revoke durably before any awaited transport call
|
package/dist/keys.js
CHANGED
|
@@ -5,7 +5,7 @@ import { activate } from './activation.js';
|
|
|
5
5
|
import { keyAgentId, MEMBER_ID, KEY_PREFIX, untilListed } from './members.js';
|
|
6
6
|
import { providerOf } from './pick.js';
|
|
7
7
|
import { kitId, modelsOf } from './accounts.js';
|
|
8
|
-
import { LIVE_CATALOGS, REGISTERED_CATALOGS } from './constants.js';
|
|
8
|
+
import { LIVE_CATALOGS, OPEN_CATALOGS, REGISTERED_CATALOGS } from './constants.js';
|
|
9
9
|
export function createKeys(ctx) {
|
|
10
10
|
// Serialize key replacement and key runs, so an in-flight run never sees a half-installed replacement.
|
|
11
11
|
const tails = new Map();
|
|
@@ -93,6 +93,16 @@ export function createKeys(ctx) {
|
|
|
93
93
|
throw new Error(keyed.length ? `${authChoice} takes no API key: add the key with ${keyed.map((r) => r.choice).join(' or ')}.`
|
|
94
94
|
: `${authChoice} takes no API key.`);
|
|
95
95
|
};
|
|
96
|
+
// The vendor's list can lag a model that runs: an id it lacks rides the route's own models (the richest row's api,
|
|
97
|
+
// base URL and context, every input modality any row takes), unpriced. Nothing is added to an empty list.
|
|
98
|
+
const unlisted = (provider, rows, model) => {
|
|
99
|
+
if (!rows.length || rows.some((m) => kitId(provider, String(m.id)) === model))
|
|
100
|
+
return [];
|
|
101
|
+
const base = rows.reduce((a, b) => (Number(b.contextWindow) || 0) > (Number(a.contextWindow) || 0) ? b : a);
|
|
102
|
+
const input = ['text', 'image'].filter((i) => rows.some((m) => Array.isArray(m.input) && m.input.includes(i)));
|
|
103
|
+
const id = model.slice(provider.length + 1);
|
|
104
|
+
return [{ ...base, id, name: id, input, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } }];
|
|
105
|
+
};
|
|
96
106
|
const listed = (provider, rows) => modelsOf(provider, { models: rows.map((m) => ({ ...m, provider })) });
|
|
97
107
|
const models = async (member, o) => {
|
|
98
108
|
if (!check(member))
|
|
@@ -217,6 +227,8 @@ export function createKeys(ctx) {
|
|
|
217
227
|
if (result.late)
|
|
218
228
|
return abandon(failure('retry', 'busy'));
|
|
219
229
|
const rows = model === undefined ? [] : await catalog(member, route.provider);
|
|
230
|
+
const lagging = model !== undefined && OPEN_CATALOGS.includes(route.provider) ? unlisted(route.provider, rows, model) : [];
|
|
231
|
+
rows.push(...lagging);
|
|
220
232
|
if (model !== undefined && !listed(route.provider, rows).some((m) => m.id === model && m.available))
|
|
221
233
|
return abandon(failure('not_included', 'model'));
|
|
222
234
|
// Prepare before publishing the ready marker: failed preparation remains unusable even if clear fails.
|
|
@@ -229,7 +241,7 @@ export function createKeys(ctx) {
|
|
|
229
241
|
if (ac.signal.aborted)
|
|
230
242
|
return abandon(cancelled());
|
|
231
243
|
// A live catalog the kit does not register whole (OpenRouter) registers the chosen model alone.
|
|
232
|
-
if (model !== undefined && !REGISTERED_CATALOGS.includes(route.provider))
|
|
244
|
+
if (model !== undefined && (!REGISTERED_CATALOGS.includes(route.provider) || lagging.length))
|
|
233
245
|
await ctx.registerModels(route.provider, rows.filter((m) => kitId(route.provider, String(m.id)) === model)).catch(() => { });
|
|
234
246
|
if (model !== undefined && LIVE_CATALOGS.includes(route.provider) && !(await registered(route.provider, model)))
|
|
235
247
|
return abandon(failure('retry', 'model'));
|
package/dist/runs.js
CHANGED
|
@@ -347,7 +347,7 @@ export function createRuns(ctx) {
|
|
|
347
347
|
try {
|
|
348
348
|
const ref = { sessionKey, member: spec.member, ...(spec.helper !== undefined ? { helper: spec.helper } : {}) };
|
|
349
349
|
releaseDelegation = spec.delegates || spec.helperDelegates
|
|
350
|
-
? ctx.delegations.register(ref, idempotencyKey, spec.tools, spec.delegates ?? {}, spec.sessionKey, spec.helperDelegates) : undefined;
|
|
350
|
+
? ctx.delegations.register(ref, idempotencyKey, spec.tools, spec.delegates ?? {}, spec.sessionKey, spec.helperDelegates, system) : undefined;
|
|
351
351
|
release = spec.register !== false ? ctx.bridge.register(ref, spec.tools, idempotencyKey) : undefined;
|
|
352
352
|
if (picked && !chosen) {
|
|
353
353
|
// The picked account must be the one called: check it is signed in for this member before the run, since
|
|
@@ -329,6 +329,12 @@ export function fakeGateway(script, o) {
|
|
|
329
329
|
setTimeout(() => {
|
|
330
330
|
void (async () => {
|
|
331
331
|
const table = bridgeSock ? pluginTable(bridgeSock) : undefined;
|
|
332
|
+
// As the plugin's prompt hook: a child on a helper key asks the kit for its helper's persona (5.8.3).
|
|
333
|
+
if (child && bridgeSock && /^agent:[^:]+:h-[a-z][a-z0-9-]{0,15}:/.test(run.sessionKey)) {
|
|
334
|
+
const reply = await bridgeRequest(bridgeSock, { kind: 'delegation-persona', key: run.sessionKey, runId: run.runId }).catch(() => ({}));
|
|
335
|
+
calls.push({ method: 'before_prompt_build', params: { sessionKey: run.sessionKey, runId: run.runId,
|
|
336
|
+
...(typeof reply.persona === 'string' ? { appendSystemContext: reply.persona } : {}) } });
|
|
337
|
+
}
|
|
332
338
|
let n = 0;
|
|
333
339
|
for (const call of toolCalls(message)) {
|
|
334
340
|
const toolCallId = `call-${++n}`;
|
|
@@ -688,14 +694,18 @@ export function fakeGateway(script, o) {
|
|
|
688
694
|
return { status: 'cancelled' };
|
|
689
695
|
},
|
|
690
696
|
agent: (p, bridgeSock) => startRun(p, bridgeSock),
|
|
691
|
-
|
|
692
|
-
|
|
697
|
+
// The pin: a run it never had (or forgot, as after a restart) waits out as a `timeout` in the `queue` phase that never
|
|
698
|
+
// reached a provider; a live run past `timeoutMs` is a `timeout` while it drains.
|
|
699
|
+
'agent.wait': (p) => new Promise((resolve) => {
|
|
700
|
+
const runId = String(p.runId);
|
|
701
|
+
const run = runs.get(runId);
|
|
693
702
|
if (!run)
|
|
694
|
-
return
|
|
703
|
+
return resolve({ runId, status: 'timeout', timeoutPhase: 'queue', providerStarted: false });
|
|
695
704
|
if (run.result)
|
|
696
|
-
resolve(run.result);
|
|
697
|
-
|
|
698
|
-
|
|
705
|
+
return resolve(run.result);
|
|
706
|
+
run.waiters.push(resolve);
|
|
707
|
+
if (typeof p.timeoutMs === 'number')
|
|
708
|
+
setTimeout(() => resolve({ runId, status: 'timeout', timeoutPhase: 'gateway_draining' }), p.timeoutMs).unref();
|
|
699
709
|
}),
|
|
700
710
|
'sessions.steer': () => ({}),
|
|
701
711
|
// The pin: a key the agent holds answers its canonical key; a missing one errors unless `allowMissing`, which answers `{ ok: false }`.
|
package/dist/types.d.ts
CHANGED
|
@@ -46,12 +46,13 @@ export type DelegatedRun = RunRef & Required<Pick<RunRef, 'runId' | 'agentId' |
|
|
|
46
46
|
tools: string[];
|
|
47
47
|
};
|
|
48
48
|
/** A delegated child's lifecycle (5.8.3 S3): `admitted`, its engine `event`s, then one `ended` whose `reason` is
|
|
49
|
-
* `terminal` (finished), `error` (`message`), `resting` (its helper's account hit its limit and rests `until`)
|
|
50
|
-
* `cancelled` (aborted by the kit or the engine, or its admission ended)
|
|
49
|
+
* `terminal` (finished), `error` (`message`), `resting` (its helper's account hit its limit and rests `until`),
|
|
50
|
+
* `cancelled` (aborted by the kit or the engine, or its admission ended) or `lost` (the engine no longer knows the run,
|
|
51
|
+
* as after an engine restart: outcome unknown, admission ended). */
|
|
51
52
|
export type DelegatedEvent = {
|
|
52
53
|
type: 'admitted' | 'event' | 'ended';
|
|
53
54
|
run: DelegatedRun;
|
|
54
|
-
reason?: 'terminal' | 'error' | 'resting' | 'cancelled';
|
|
55
|
+
reason?: 'terminal' | 'error' | 'resting' | 'cancelled' | 'lost';
|
|
55
56
|
until?: number;
|
|
56
57
|
message?: string;
|
|
57
58
|
event?: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/openclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "The OpenClaw runtime kit: the pinned engine's full operator surface with plain-words helpers for members, sign-in, runs and approvals.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@byokit/accounts": "0.
|
|
63
|
+
"@byokit/accounts": "0.29.0",
|
|
64
64
|
"@byokit/pair": "0.10.4",
|
|
65
65
|
"@byokit/relay": "0.8.2",
|
|
66
66
|
"@byokit/discover": "0.8.4",
|
package/plugin/catalog.js
CHANGED
|
@@ -38,6 +38,19 @@ export function registerCatalog(api) {
|
|
|
38
38
|
const models = [...live.models, ...(pinned?.models ?? []).filter((m) => !liveIds.has(m.id))]
|
|
39
39
|
.map(({ provider: _p, apiKey: _k, headers: _h, ...model }) =>
|
|
40
40
|
(endpoint.has(model.api) ? { ...model, baseUrl: endpoint.get(model.api) } : model));
|
|
41
|
+
// MiniMax's engine catalog is a fixed list: its vendor `/models` lists newer ids, which join it with the richest
|
|
42
|
+
// row's api, base URL and capabilities (the vendor lists ids only). A failed fetch keeps the engine's list.
|
|
43
|
+
if (provider === 'minimax' && models.length) {
|
|
44
|
+
const base = models.reduce((a, b) => (b.contextWindow ?? 0) > (a.contextWindow ?? 0) ? b : a);
|
|
45
|
+
const input = ['text', 'image'].filter((i) => models.some((m) => m.input?.includes(i)));
|
|
46
|
+
const listed = await fetch(`${String(base.baseUrl).replace(/\/$/, '')}/v1/models`,
|
|
47
|
+
{ headers: { authorization: `Bearer ${apiKey}`, 'anthropic-version': '2023-06-01' }, signal: AbortSignal.timeout(10_000) })
|
|
48
|
+
.then((r) => r.ok ? r.json() : null, () => null);
|
|
49
|
+
const have = new Set(models.map((m) => m.id));
|
|
50
|
+
for (const m of Array.isArray(listed?.data) ? listed.data : [])
|
|
51
|
+
if (typeof m?.id === 'string' && m.id && !have.has(m.id))
|
|
52
|
+
models.push({ ...base, id: m.id, name: m.display_name || m.id, input, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } });
|
|
53
|
+
}
|
|
41
54
|
return respond(true, { ok: true, models });
|
|
42
55
|
}
|
|
43
56
|
respond(true, { ok: false, reason: unreadable ? 'failed' : 'no-key' });
|
package/plugin/index.js
CHANGED
|
@@ -159,6 +159,21 @@ export default {
|
|
|
159
159
|
callId: ctx?.toolCallId ?? event.toolCallId,
|
|
160
160
|
result: { details: d && { status: d.status, runId: d.runId, sessionKey: d.sessionKey, childSessionKey: d.childSessionKey } } });
|
|
161
161
|
});
|
|
162
|
+
// A native send carries no system text: an admitted helper child gets its helper's persona from the kit (5.8.3).
|
|
163
|
+
// The engine's reply step bringing a sent child's report back (an inter-session prompt) asks too: the kit binds that
|
|
164
|
+
// exact run as the sender's return run (5.8.4) and answers the sender's persona. Any other run, or no answer, adds
|
|
165
|
+
// nothing; the persona is a voice, never a grant.
|
|
166
|
+
api.on('before_prompt_build', async (event, ctx) => {
|
|
167
|
+
const key = ctx?.sessionKey;
|
|
168
|
+
const header = typeof event?.prompt === 'string' && event.prompt.startsWith('[Inter-session message] ')
|
|
169
|
+
? event.prompt.slice(0, 1024).split('\n', 1)[0] : undefined;
|
|
170
|
+
if (typeof key !== 'string' || (!header && !/^agent:[^:]+:h-[a-z][a-z0-9-]{0,15}:/.test(key))) return undefined;
|
|
171
|
+
try {
|
|
172
|
+
const reply = await bridgeRequest({ kind: 'delegation-persona', key, runId: ctx?.runId, ...(header ? { header } : {}) }, { timeoutMs: 10_000 });
|
|
173
|
+
if (typeof reply?.persona === 'string' && reply.persona) return { appendSystemContext: reply.persona };
|
|
174
|
+
} catch { /* no kit: nothing added */ }
|
|
175
|
+
return undefined;
|
|
176
|
+
});
|
|
162
177
|
api.registerAgentEventSubscription?.({ id: 'byokit-delegated-events', handle(event) {
|
|
163
178
|
if (!['assistant', 'tool', 'thinking', 'lifecycle'].includes(event.stream)) return;
|
|
164
179
|
const data = Object.fromEntries(['text', 'name', 'phase', 'toolCallId', 'isError', 'progressTokens', 'aborted', 'error', 'provider', 'model']
|
|
@@ -281,6 +296,12 @@ export default {
|
|
|
281
296
|
callId: ctx?.toolCallId ?? event.toolCallId, tool: event.toolName, input: params });
|
|
282
297
|
if (prepared.allow === false) return { block: true, blockReason: prepared.reason };
|
|
283
298
|
} catch { return { block: true, blockReason: 'delegation admission unavailable' }; }
|
|
299
|
+
// A tracked spawn into the run's own agent goes without `agentId`, the engine's own-agent spawn, so an app's
|
|
300
|
+
// `subagents.allowAgents` list for other agents never refuses it. The kit already admitted that exact agent.
|
|
301
|
+
// Hook params merge over the model's, so the key is overridden, not left out.
|
|
302
|
+
const own = ctx?.agentId ?? /^agent:([^:]+):/.exec(key)?.[1];
|
|
303
|
+
if (event.toolName === 'sessions_spawn' && typeof own === 'string' && params.agentId === own)
|
|
304
|
+
return { params: { ...params, agentId: undefined } };
|
|
284
305
|
}
|
|
285
306
|
if (builtin) return BROWSER && event.toolName === 'browser' ? { params } : undefined;
|
|
286
307
|
// The bridge mints a permit for permitted tools and admits the rest ticket-side; either way the run key
|