@byokit/openclaw 0.10.0 → 0.11.1
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 +21 -0
- package/README.md +7 -1
- package/dist/accounts.d.ts +47 -0
- package/dist/accounts.js +499 -0
- package/dist/auth-store.js +20 -0
- package/dist/device.d.ts +4 -1
- package/dist/engine.js +1 -1
- package/dist/kit.d.ts +25 -1
- package/dist/kit.js +51 -0
- package/dist/locks.d.ts +5 -0
- package/dist/locks.js +85 -0
- package/dist/members.d.ts +3 -0
- package/dist/members.js +13 -9
- package/dist/pick.d.ts +7 -3
- package/dist/pick.js +34 -5
- package/dist/runs.d.ts +7 -0
- package/dist/runs.js +87 -9
- package/dist/signin.d.ts +1 -0
- package/dist/signin.js +12 -2
- package/dist/testing/contract.js +0 -1
- package/dist/testing/fake-gateway.d.ts +6 -0
- package/dist/testing/fake-gateway.js +300 -11
- package/dist/types.d.ts +19 -0
- package/package.json +6 -6
- package/plugin/accounts.js +224 -0
- package/plugin/index.js +2 -1
package/dist/kit.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { SealingAdapter } from '@byokit/secrets';
|
|
|
2
2
|
import { type AddKeyResult } from './keys.ts';
|
|
3
3
|
import { type RouteView } from './routes.ts';
|
|
4
4
|
import type { BrowserHost, BrowserOptions } from './browser.ts';
|
|
5
|
-
import type { Approval, CallOptions, Decision, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, KitEventName, KitEventPayload, LearningCapture, LearningMode, Member, RunEnd, RunEvent, RunSpec, OutputSchema, SchemaOutput, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
|
|
5
|
+
import type { Account, AccountId, Approval, CallOptions, Decision, Defaults, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, KitEventName, KitEventPayload, LearningCapture, LearningMode, Member, ModelInfo, Room, RunEnd, RunEvent, RunSpec, OutputSchema, SchemaOutput, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
|
|
6
6
|
export type KitOptions = {
|
|
7
7
|
stateDir: string;
|
|
8
8
|
authSeal?: SealingAdapter;
|
|
@@ -54,6 +54,9 @@ export declare class OpenClawKit {
|
|
|
54
54
|
private transport?;
|
|
55
55
|
private greeting?;
|
|
56
56
|
private members?;
|
|
57
|
+
private readonly locks;
|
|
58
|
+
private readonly routeMap;
|
|
59
|
+
private accountsCtl?;
|
|
57
60
|
private readonly bridge;
|
|
58
61
|
private readonly keys;
|
|
59
62
|
private readonly admission;
|
|
@@ -110,6 +113,27 @@ export declare class OpenClawKit {
|
|
|
110
113
|
done: Promise<SignInView>;
|
|
111
114
|
};
|
|
112
115
|
signOut(member: Member, provider: string): Promise<void>;
|
|
116
|
+
accounts(member: Member): Promise<Account[]>;
|
|
117
|
+
models(member: Member, id: AccountId): Promise<ModelInfo[]>;
|
|
118
|
+
room(member: Member, id: AccountId, demand?: string[]): Promise<Room>;
|
|
119
|
+
/** Sign in one more account: on the member agent while it holds none of the provider, else on a new sealed agent. */
|
|
120
|
+
addAccount(member: Member, o: {
|
|
121
|
+
authChoice: string;
|
|
122
|
+
via?: 'browser' | 'code';
|
|
123
|
+
name?: string;
|
|
124
|
+
again?: AccountId;
|
|
125
|
+
}, on: (v: SignInView) => void): Promise<{
|
|
126
|
+
id: AccountId;
|
|
127
|
+
paste(text: string): void;
|
|
128
|
+
cancel(): void;
|
|
129
|
+
done: Promise<{
|
|
130
|
+
view: SignInView;
|
|
131
|
+
id: AccountId;
|
|
132
|
+
}>;
|
|
133
|
+
}>;
|
|
134
|
+
renameAccount(member: Member, id: AccountId, name: string): Promise<Account>;
|
|
135
|
+
defaults(member: Member): Promise<Defaults>;
|
|
136
|
+
setDefaults(member: Member, d: Defaults): Promise<void>;
|
|
113
137
|
/** Save an explicitly supplied API key (billed per use) in this member's separate key agent. */
|
|
114
138
|
addKey(agentId: Member, o: {
|
|
115
139
|
authChoice: string;
|
package/dist/kit.js
CHANGED
|
@@ -10,6 +10,8 @@ import { Engine, refusedOnly } from "./engine.js";
|
|
|
10
10
|
import { AuthStoreSealSizeError } from "./auth-store.js";
|
|
11
11
|
import { gatewayTransport } from "./transport.js";
|
|
12
12
|
import { createMembers } from "./members.js";
|
|
13
|
+
import { createAccounts } from "./accounts.js";
|
|
14
|
+
import { createLocks } from "./locks.js";
|
|
13
15
|
import { keyAgentId, KEY_PREFIX, MEMBER_ID } from "./members.js";
|
|
14
16
|
import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLogin } from "./migrate.js";
|
|
15
17
|
import { createRuns, signedInProviders } from "./runs.js";
|
|
@@ -429,6 +431,9 @@ export class OpenClawKit {
|
|
|
429
431
|
transport;
|
|
430
432
|
greeting;
|
|
431
433
|
members;
|
|
434
|
+
locks = createLocks();
|
|
435
|
+
routeMap = new Map();
|
|
436
|
+
accountsCtl;
|
|
432
437
|
// One bridge and one approval surface per kit: listeners and run registrations survive reconnects (B3).
|
|
433
438
|
// Only the socket itself is per-connection. Approvals always reaches the live transport through this.request().
|
|
434
439
|
bridge;
|
|
@@ -741,6 +746,10 @@ export class OpenClawKit {
|
|
|
741
746
|
if (!operatorMethods.has(name) && !nodeMethods.has(name))
|
|
742
747
|
this.o.log?.(`gateway has unknown method: ${name}`);
|
|
743
748
|
this.members = createMembers({ request: transport.request.bind(transport), root: this.engine.root });
|
|
749
|
+
this.accountsCtl = createAccounts({ request: transport.request.bind(transport), root: this.engine.root,
|
|
750
|
+
ensure: (member) => this.ensureMember(member), locks: this.locks,
|
|
751
|
+
signIn: (agentId, o, on, fresh) => startSignIn({ ...this.signInCtx(), ensure: async () => ({ agentId }), fresh }, agentId, o, on),
|
|
752
|
+
log: this.o.log });
|
|
744
753
|
// The fail-closed tool bridge feeds the kit's single Approval surface (5.9, O5).
|
|
745
754
|
await this.bridge.start();
|
|
746
755
|
this.off.push(transport.onEvent((e) => this.approvalsCtl.handleEvent(e)));
|
|
@@ -789,6 +798,7 @@ export class OpenClawKit {
|
|
|
789
798
|
this.transport = undefined;
|
|
790
799
|
this.greeting = undefined;
|
|
791
800
|
this.members = undefined;
|
|
801
|
+
this.accountsCtl = undefined;
|
|
792
802
|
this.browserPolicySessions.clear();
|
|
793
803
|
this.bridge.stop();
|
|
794
804
|
await transport?.stop();
|
|
@@ -919,6 +929,44 @@ export class OpenClawKit {
|
|
|
919
929
|
signOut(member, provider) {
|
|
920
930
|
return engineSignOut(this.signInCtx(), member, provider);
|
|
921
931
|
}
|
|
932
|
+
// accounts (5.15, O14 steps S5, S6, S9 and S10): the per-member index with listing, adding, defaults, rename, models
|
|
933
|
+
// and room; `run({ account })` picks among them. Removing, `pick` and move arrive in later steps.
|
|
934
|
+
accounts(member) {
|
|
935
|
+
if (!this.accountsCtl)
|
|
936
|
+
return Promise.reject(new Error('gateway not ready'));
|
|
937
|
+
return this.accountsCtl.list(member);
|
|
938
|
+
}
|
|
939
|
+
models(member, id) {
|
|
940
|
+
if (!this.accountsCtl)
|
|
941
|
+
return Promise.reject(new Error('gateway not ready'));
|
|
942
|
+
return this.accountsCtl.models(member, id);
|
|
943
|
+
}
|
|
944
|
+
room(member, id, demand) {
|
|
945
|
+
if (!this.accountsCtl)
|
|
946
|
+
return Promise.reject(new Error('gateway not ready'));
|
|
947
|
+
return this.accountsCtl.room(member, id, demand);
|
|
948
|
+
}
|
|
949
|
+
/** Sign in one more account: on the member agent while it holds none of the provider, else on a new sealed agent. */
|
|
950
|
+
addAccount(member, o, on) {
|
|
951
|
+
if (!this.accountsCtl)
|
|
952
|
+
return Promise.reject(new Error('gateway not ready'));
|
|
953
|
+
return this.accountsCtl.add(member, o, on);
|
|
954
|
+
}
|
|
955
|
+
renameAccount(member, id, name) {
|
|
956
|
+
if (!this.accountsCtl)
|
|
957
|
+
return Promise.reject(new Error('gateway not ready'));
|
|
958
|
+
return this.accountsCtl.rename(member, id, name);
|
|
959
|
+
}
|
|
960
|
+
defaults(member) {
|
|
961
|
+
if (!this.accountsCtl)
|
|
962
|
+
return Promise.reject(new Error('gateway not ready'));
|
|
963
|
+
return this.accountsCtl.defaults(member);
|
|
964
|
+
}
|
|
965
|
+
setDefaults(member, d) {
|
|
966
|
+
if (!this.accountsCtl)
|
|
967
|
+
return Promise.reject(new Error('gateway not ready'));
|
|
968
|
+
return this.accountsCtl.setDefaults(member, d);
|
|
969
|
+
}
|
|
922
970
|
/** Save an explicitly supplied API key (billed per use) in this member's separate key agent. */
|
|
923
971
|
addKey(agentId, o) {
|
|
924
972
|
return this.keys.add(agentId, o);
|
|
@@ -946,6 +994,9 @@ export class OpenClawKit {
|
|
|
946
994
|
ensure: (member) => this.ensureMember(member),
|
|
947
995
|
bridge: { register: (run, tools, runId) => this.bridge.register(run, tools && this.o.browser ? [...tools, 'request_sign_in'] : tools, runId) },
|
|
948
996
|
tools: new Set(this.toolNames()),
|
|
997
|
+
...(this.accountsCtl ? { accounts: this.accountsCtl } : {}),
|
|
998
|
+
locks: this.locks,
|
|
999
|
+
routes: this.routeMap,
|
|
949
1000
|
});
|
|
950
1001
|
}
|
|
951
1002
|
/** The app tool names (`KitOptions.tools`): the only names `RunSpec.tools` may carry. */
|
package/dist/locks.d.ts
ADDED
package/dist/locks.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export function createLocks() {
|
|
2
|
+
const slots = new Map();
|
|
3
|
+
const slot = (id) => {
|
|
4
|
+
let s = slots.get(id);
|
|
5
|
+
if (!s) {
|
|
6
|
+
s = { readers: 0, writer: false, queue: [] };
|
|
7
|
+
slots.set(id, s);
|
|
8
|
+
}
|
|
9
|
+
return s;
|
|
10
|
+
};
|
|
11
|
+
// FIFO: an exclusive waiter blocks later shared waiters until the readers ahead of it drain.
|
|
12
|
+
const pump = (s) => {
|
|
13
|
+
if (s.writer)
|
|
14
|
+
return;
|
|
15
|
+
const head = s.queue[0];
|
|
16
|
+
if (!head)
|
|
17
|
+
return;
|
|
18
|
+
if (head.mode === 'exclusive') {
|
|
19
|
+
if (s.readers === 0) {
|
|
20
|
+
s.queue.shift();
|
|
21
|
+
s.writer = true;
|
|
22
|
+
head.grant();
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
while (s.queue[0]?.mode === 'shared') {
|
|
27
|
+
const w = s.queue.shift();
|
|
28
|
+
s.readers++;
|
|
29
|
+
w.grant();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const acquire = (id, mode) => {
|
|
33
|
+
const s = slot(id);
|
|
34
|
+
if (mode === 'shared' && !s.writer && s.queue.every((w) => w.mode === 'shared')) {
|
|
35
|
+
s.readers++;
|
|
36
|
+
return Promise.resolve();
|
|
37
|
+
}
|
|
38
|
+
if (mode === 'exclusive' && !s.writer && s.readers === 0 && s.queue.length === 0) {
|
|
39
|
+
s.writer = true;
|
|
40
|
+
return Promise.resolve();
|
|
41
|
+
}
|
|
42
|
+
return new Promise((grant) => {
|
|
43
|
+
s.queue.push(mode === 'exclusive' ? { mode, grant: () => { s.writer = true; grant(); } } : { mode, grant });
|
|
44
|
+
pump(s);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
const release = (id, mode) => {
|
|
48
|
+
const s = slot(id);
|
|
49
|
+
if (mode === 'shared')
|
|
50
|
+
s.readers = Math.max(0, s.readers - 1);
|
|
51
|
+
else
|
|
52
|
+
s.writer = false;
|
|
53
|
+
pump(s);
|
|
54
|
+
if (s.readers === 0 && !s.writer && s.queue.length === 0)
|
|
55
|
+
slots.delete(id);
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
async shared(agentId, work) {
|
|
59
|
+
await acquire(agentId, 'shared');
|
|
60
|
+
try {
|
|
61
|
+
return await work();
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
release(agentId, 'shared');
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
async exclusive(agentIds, work) {
|
|
68
|
+
// Sorted so two exclusive takes over overlapping sets cannot deadlock each other.
|
|
69
|
+
const ids = [...new Set(agentIds)].sort();
|
|
70
|
+
for (const id of ids)
|
|
71
|
+
await acquire(id, 'exclusive');
|
|
72
|
+
try {
|
|
73
|
+
return await work();
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
for (const id of [...ids].reverse())
|
|
77
|
+
release(id, 'exclusive');
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
live(agentId) {
|
|
81
|
+
const s = slots.get(agentId);
|
|
82
|
+
return !!s && (s.readers > 0 || s.writer);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
package/dist/members.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { GatewayTransport, Member } from './types.ts';
|
|
2
2
|
export declare const MEMBER_ID: RegExp;
|
|
3
|
+
export declare const NEW_MEMBER_ID: RegExp;
|
|
3
4
|
export declare const KEY_PREFIX = "byokit-key-";
|
|
4
5
|
export declare const BASE_AGENT = "byokit-base";
|
|
5
6
|
export declare const keyAgentId: (member: Member) => string;
|
|
7
|
+
/** Resolves once agents.list shows `id`, which agents.create answers before. */
|
|
8
|
+
export declare function untilListed(request: GatewayTransport['request'], id: string): Promise<void>;
|
|
6
9
|
export declare function createMembers(ctx: {
|
|
7
10
|
request: GatewayTransport['request'];
|
|
8
11
|
root: string;
|
package/dist/members.js
CHANGED
|
@@ -2,7 +2,7 @@ import { join } from 'node:path';
|
|
|
2
2
|
// Any member that may run: D9's older rule, which a member created before O14 still matches (D17).
|
|
3
3
|
export const MEMBER_ID = /^[a-z][a-z0-9-]{0,31}$/;
|
|
4
4
|
// D9 for a member the kit creates: no `--` (account agents are `<member>--<6 hex>`) and at most 24 characters.
|
|
5
|
-
const NEW_MEMBER_ID = /^[a-z](?!.*--)[a-z0-9-]{0,23}$/;
|
|
5
|
+
export const NEW_MEMBER_ID = /^[a-z](?!.*--)[a-z0-9-]{0,23}$/;
|
|
6
6
|
export const KEY_PREFIX = 'byokit-key-';
|
|
7
7
|
// The inherited auth base every agent reads through (5.15): the kit never creates, signs in to or runs it.
|
|
8
8
|
export const BASE_AGENT = 'byokit-base';
|
|
@@ -13,9 +13,18 @@ export const keyAgentId = (member) => `${KEY_PREFIX}${member}`;
|
|
|
13
13
|
// at once fails as 'unknown agent id'. ensure resolves only once agents.list shows the member.
|
|
14
14
|
const VISIBLE_TIMEOUT_MS = 5_000;
|
|
15
15
|
const VISIBLE_POLL_MS = 25;
|
|
16
|
+
const listed = (request, id) => request('agents.list').then((list) => list.agents.some((agent) => agent.id === id));
|
|
17
|
+
/** Resolves once agents.list shows `id`, which agents.create answers before. */
|
|
18
|
+
export async function untilListed(request, id) {
|
|
19
|
+
const deadline = Date.now() + VISIBLE_TIMEOUT_MS;
|
|
20
|
+
while (!await listed(request, id)) {
|
|
21
|
+
if (Date.now() >= deadline)
|
|
22
|
+
throw new Error(`agent ${id} is not listed by agents.list after agents.create`);
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, VISIBLE_POLL_MS));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
16
26
|
export function createMembers(ctx) {
|
|
17
27
|
const cache = new Map();
|
|
18
|
-
const listed = (member) => ctx.request('agents.list').then((list) => list.agents.some((agent) => agent.id === member));
|
|
19
28
|
return { ensure(member) {
|
|
20
29
|
const invalid = () => new Error(`invalid member id: ${member}`);
|
|
21
30
|
if (!MEMBER_ID.test(member) || member.startsWith(KEY_PREFIX) || RESERVED.has(member))
|
|
@@ -25,16 +34,11 @@ export function createMembers(ctx) {
|
|
|
25
34
|
return existing;
|
|
26
35
|
const workspace = join(ctx.root, 'workspaces', member);
|
|
27
36
|
const result = (async () => {
|
|
28
|
-
if (!await listed(member)) {
|
|
37
|
+
if (!await listed(ctx.request, member)) {
|
|
29
38
|
if (!NEW_MEMBER_ID.test(member) || member.startsWith('byokit-'))
|
|
30
39
|
throw invalid();
|
|
31
40
|
await ctx.request('agents.create', { name: member, workspace });
|
|
32
|
-
|
|
33
|
-
while (!await listed(member)) {
|
|
34
|
-
if (Date.now() >= deadline)
|
|
35
|
-
throw new Error(`agent ${member} is not listed by agents.list after agents.create`);
|
|
36
|
-
await new Promise((resolve) => setTimeout(resolve, VISIBLE_POLL_MS));
|
|
37
|
-
}
|
|
41
|
+
await untilListed(ctx.request, member);
|
|
38
42
|
}
|
|
39
43
|
return { agentId: member, workspace };
|
|
40
44
|
})();
|
package/dist/pick.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { Account, AccountId, AccountPick, Considered, Defaults, ModelInfo, PickWhy, Room, RunSelection } from './types.ts';
|
|
2
2
|
export type RoomOf = (a: Account, demand: readonly string[]) => Room;
|
|
3
|
+
/** The lowercased provider of a well-formed `provider/model` reference; undefined when the reference is malformed. */
|
|
4
|
+
export declare function providerOf(model: string): string | undefined;
|
|
3
5
|
/** Every account's `Considered` row, in list order, with the kit-only `bound` applied. */
|
|
4
|
-
export declare function consider(accounts: readonly Account[], defaults: Defaults, sel: RunSelection, room: RoomOf, nowMs: number, models?: (a: Account) => readonly ModelInfo[], bound?: readonly AccountId[]): Considered[];
|
|
6
|
+
export declare function consider(accounts: readonly Account[], defaults: Defaults, sel: RunSelection, room: RoomOf, nowMs: number, models?: (a: Account) => readonly ModelInfo[] | undefined, bound?: readonly AccountId[]): Considered[];
|
|
5
7
|
/** Auto's winner only; `candidates` are the eligible accounts. Pure and portable; no timers, reads or mid-run switching. */
|
|
6
8
|
export declare function chooseAccount(candidates: readonly Account[], room: (a: Account) => Room, nowMs: number): {
|
|
7
9
|
account: Account;
|
|
8
10
|
why: PickWhy;
|
|
9
11
|
} | undefined;
|
|
10
|
-
/** Resolve once before starting. Without `models`, membership is host-validated and a missing model is ''.
|
|
11
|
-
|
|
12
|
+
/** Resolve once before starting. Without `models`, membership is host-validated and a missing model is ''. A row whose
|
|
13
|
+
* models are unknown (`undefined`) host-validates membership by provider; with no model asked for it runs the saved
|
|
14
|
+
* default, else model '' (no override: the engine's own default). */
|
|
15
|
+
export declare function resolveSelection(accounts: readonly Account[], defaults: Defaults, sel: RunSelection, room: RoomOf, nowMs: number, models?: (a: Account) => readonly ModelInfo[] | undefined, bound?: readonly AccountId[]): AccountPick;
|
package/dist/pick.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { words } from "./words.js";
|
|
2
2
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
3
3
|
const finite = (n) => typeof n === 'number' && Number.isFinite(n);
|
|
4
|
+
/** The lowercased provider of a well-formed `provider/model` reference; undefined when the reference is malformed. */
|
|
5
|
+
export function providerOf(model) {
|
|
6
|
+
const at = model.indexOf('/');
|
|
7
|
+
return at <= 0 || at === model.length - 1 || /[\s@]/.test(model) ? undefined : model.slice(0, at).toLowerCase();
|
|
8
|
+
}
|
|
4
9
|
const reset = (r) => r.resetsAt ?? Infinity;
|
|
5
10
|
const roomWords = (left, span) => words(`room.${span}`, { left: `${Math.round(left)}%` });
|
|
6
11
|
// "3:40pm", or "Fri 3:40pm" when it isn't today (matches @byokit/accounts' clock).
|
|
@@ -35,8 +40,18 @@ function considerRows(accounts, room, nowMs, demand = [], models, provider, name
|
|
|
35
40
|
else if (account.billing !== 'subscription')
|
|
36
41
|
row.out = 'billing';
|
|
37
42
|
}
|
|
43
|
+
if (!row.out && models) {
|
|
44
|
+
const listed = models(account) ?? [];
|
|
45
|
+
if (listed.length && listed.every((m) => m.why === 'resting')) {
|
|
46
|
+
row.out = 'resting';
|
|
47
|
+
const untils = listed.map((m) => m.until).filter(finite);
|
|
48
|
+
if (untils.length)
|
|
49
|
+
row.until = Math.min(...untils);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
38
52
|
if (!row.out && demand.length && models) {
|
|
39
|
-
const available = models(account)
|
|
53
|
+
const available = models(account) ?? demand.filter((id) => id.split('/')[0].toLowerCase() === account.provider.toLowerCase())
|
|
54
|
+
.map((id) => ({ id, name: id, available: true }));
|
|
40
55
|
const missing = demand.find((id) => !available.some((m) => m.id === id && m.available));
|
|
41
56
|
if (missing !== undefined) {
|
|
42
57
|
row.out = 'model';
|
|
@@ -105,7 +120,9 @@ function nameOf(accounts, defaults, sel, bound) {
|
|
|
105
120
|
(bound === undefined || bound.includes(defaultAccount.id));
|
|
106
121
|
return sel.account === 'default' ? (usable ? defaultAccount.id : undefined) : sel.account === 'auto' ? undefined : sel.account;
|
|
107
122
|
}
|
|
108
|
-
/** Resolve once before starting. Without `models`, membership is host-validated and a missing model is ''.
|
|
123
|
+
/** Resolve once before starting. Without `models`, membership is host-validated and a missing model is ''. A row whose
|
|
124
|
+
* models are unknown (`undefined`) host-validates membership by provider; with no model asked for it runs the saved
|
|
125
|
+
* default, else model '' (no override: the engine's own default). */
|
|
109
126
|
export function resolveSelection(accounts, defaults, sel, room, nowMs, models, bound) {
|
|
110
127
|
const demand = [...new Set([...(sel.model ? [sel.model] : []), ...(sel.needs ?? [])])];
|
|
111
128
|
const defaultAccount = accounts.find((a) => a.id === defaults.account);
|
|
@@ -114,10 +131,14 @@ export function resolveSelection(accounts, defaults, sel, room, nowMs, models, b
|
|
|
114
131
|
const rows = considerRows(accounts, (a) => room(a, demand), nowMs, demand, models, provider, named, bound);
|
|
115
132
|
const considered = rows.map((c) => c.row);
|
|
116
133
|
const auto = rank(rows); // also fills explanations for every eligible row, even on explicit selection
|
|
134
|
+
const restingPick = (c) => ({ ok: false, code: 'resting', account: c.account.id, until: c.row.until,
|
|
135
|
+
reason: words('pick.out.resting', { time: finite(c.row.until) ? clock(c.row.until) : '' }), considered });
|
|
117
136
|
if (named !== undefined) {
|
|
118
137
|
const chosen = rows.find((c) => c.account.id === named);
|
|
119
138
|
if (!chosen)
|
|
120
139
|
return { ok: false, code: 'unknown_account', reason: words('auto.none', { provider: provider ?? '' }), considered };
|
|
140
|
+
if (chosen.row.out === 'resting')
|
|
141
|
+
return restingPick(chosen);
|
|
121
142
|
if (chosen.row.out)
|
|
122
143
|
return { ok: false, code: 'not_included', reason: words(`pick.out.${chosen.row.out}`, { model: chosen.row.missing ?? '' }), considered };
|
|
123
144
|
return finish(chosen.account, chosen.row, sel.account === 'default' ? 'default' : 'chosen', sel.account === 'default' ? 'default' : 'chosen');
|
|
@@ -129,11 +150,19 @@ export function resolveSelection(accounts, defaults, sel, room, nowMs, models, b
|
|
|
129
150
|
}
|
|
130
151
|
else if (auto)
|
|
131
152
|
return finish(auto.account, auto.row, 'auto', auto.why);
|
|
153
|
+
const eligible = rows.filter((c) => !c.row.out || c.row.out === 'resting');
|
|
154
|
+
const rests = eligible.filter((c) => c.row.out === 'resting');
|
|
155
|
+
if (rests.length)
|
|
156
|
+
return restingPick(rests.reduce((a, b) => ((b.row.until ?? Infinity) < (a.row.until ?? Infinity) ? b : a)));
|
|
132
157
|
return { ok: false, code: 'none', reason: words('auto.none', { provider: provider ?? '' }), considered };
|
|
133
158
|
function finish(account, row, how, why) {
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
159
|
+
const listed = models?.(account);
|
|
160
|
+
const ready = listed?.filter((m) => m.available && !m.why);
|
|
161
|
+
const usable = ready?.length ? ready : listed?.filter((m) => m.available && m.why === 'signed_out');
|
|
162
|
+
const saved = defaults.model && (usable ? usable.some((m) => m.id === defaults.model)
|
|
163
|
+
: providerOf(defaults.model) === account.provider.toLowerCase()) ? defaults.model : undefined;
|
|
164
|
+
const model = sel.model ?? saved ?? usable?.[0]?.id ?? '';
|
|
165
|
+
if (listed && !model)
|
|
137
166
|
return { ok: false, code: 'not_included', reason: words('pick.out.model', { model: sel.model ?? '' }), considered };
|
|
138
167
|
row.reason = why;
|
|
139
168
|
const slots = { name: account.name, provider: account.provider };
|
package/dist/runs.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Bridge } from './bridge.ts';
|
|
2
|
+
import type { createAccounts } from './accounts.ts';
|
|
3
|
+
import type { createLocks } from './locks.ts';
|
|
2
4
|
import { authStatus } from './auth-status.ts';
|
|
3
5
|
import { outputSchema, type OutputSchema, type SchemaOutput } from './output.ts';
|
|
4
6
|
import type { GatewayTransport, Member, RunEnd, RunEvent, RunSpec } from './types.ts';
|
|
@@ -18,6 +20,11 @@ export declare function createRuns(ctx: {
|
|
|
18
20
|
}>;
|
|
19
21
|
bridge: Pick<Bridge, 'register'>;
|
|
20
22
|
tools: ReadonlySet<string>;
|
|
23
|
+
accounts?: Pick<ReturnType<typeof createAccounts>, 'pick' | 'rest'>;
|
|
24
|
+
locks?: ReturnType<typeof createLocks>;
|
|
25
|
+
routes: Map<string, {
|
|
26
|
+
session: string;
|
|
27
|
+
}>;
|
|
21
28
|
}): {
|
|
22
29
|
run<const S extends OutputSchema | undefined = undefined>(spec: RunSpec<S>, on?: (e: RunEvent) => void, keyAgent?: string, preparedOutput?: ReturnType<typeof outputSchema>): Promise<RunEnd<SchemaOutput<S>>>;
|
|
23
30
|
steer(k: string, t: string): Promise<void>;
|
package/dist/runs.js
CHANGED
|
@@ -5,12 +5,15 @@ import { authStatus } from "./auth-status.js";
|
|
|
5
5
|
import { outputSchema } from "./output.js";
|
|
6
6
|
import { words } from "./words.js";
|
|
7
7
|
import { classify } from "./classify.js";
|
|
8
|
+
import { providerOf } from "./pick.js";
|
|
8
9
|
// The gateway gives up on a silent run after an hour; we wait ten minutes longer for the reply frame itself.
|
|
9
10
|
const WAIT_MS = 3_600_000;
|
|
10
11
|
const WAIT_CLIENT_MS = 3_610_000;
|
|
11
12
|
// The `agent` request's final frame lands with the run's end; a run already over waits no longer than this for it.
|
|
12
13
|
const FINAL_GRACE_MS = 5_000;
|
|
13
14
|
const STATUS_MS = 5_000;
|
|
15
|
+
// A resting end whose message names no time rests the account this long (5.15 Runs step 7).
|
|
16
|
+
const REST_MS = 300_000;
|
|
14
17
|
export const USABLE = new Set(['ok', 'expiring', 'static']);
|
|
15
18
|
const usable = (row) => typeof row === 'string'
|
|
16
19
|
|| (Array.isArray(row.profiles) && row.profiles.length > 0 ? row.profiles.some((p) => USABLE.has(String(p?.status)))
|
|
@@ -62,10 +65,10 @@ function planOf(provider, row) {
|
|
|
62
65
|
* a session preference it may rotate away from (docs/runtime-kits.md 5.8), so the kit never sends one.
|
|
63
66
|
*/
|
|
64
67
|
function account(model) {
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
68
|
+
const provider = providerOf(model);
|
|
69
|
+
if (provider === undefined)
|
|
67
70
|
throw new Error(`refused: "${model}" is not a provider/model reference`);
|
|
68
|
-
return { provider: model.slice(
|
|
71
|
+
return { provider, model: model.slice(model.indexOf('/') + 1) }; // the engine lowercases ids
|
|
69
72
|
}
|
|
70
73
|
export function createRuns(ctx) {
|
|
71
74
|
// The plan window the engine last read for the run's provider: `models.authStatus` answers from its cache without
|
|
@@ -97,11 +100,83 @@ export function createRuns(ctx) {
|
|
|
97
100
|
const output = preparedOutput ?? (spec.schema === undefined ? undefined : outputSchema(spec.schema));
|
|
98
101
|
const system = [spec.system, output?.prompt].filter(Boolean).join('\n\n');
|
|
99
102
|
const picked = spec.model === undefined ? undefined : account(spec.model);
|
|
103
|
+
for (const need of spec.needs ?? [])
|
|
104
|
+
account(need);
|
|
100
105
|
for (const tool of spec.tools ?? [])
|
|
101
106
|
if (!ctx.tools.has(tool))
|
|
102
107
|
throw new Error(`refused: "${tool}" is not one of this kit's tools`);
|
|
103
|
-
const
|
|
104
|
-
|
|
108
|
+
const tail = spec.sessionKey.slice(`agent:${spec.member}:`.length);
|
|
109
|
+
// Runs step 3 (5.15): with `account` and `needs` both absent, exactly the run on the member agent it always was.
|
|
110
|
+
if (spec.account === undefined && !spec.needs?.length) {
|
|
111
|
+
const agentId = keyAgent ?? (await ctx.ensure(spec.member)).agentId;
|
|
112
|
+
return start(spec, on, { agentId, sessionKey: keyAgent ? `agent:${keyAgent}:${tail}` : spec.sessionKey, idempotencyKey, system, picked, output });
|
|
113
|
+
}
|
|
114
|
+
if (keyAgent)
|
|
115
|
+
throw new Error('refused: an API-key run uses its own account');
|
|
116
|
+
if (!ctx.accounts || !ctx.locks)
|
|
117
|
+
throw new Error('gateway not ready');
|
|
118
|
+
const accounts = ctx.accounts;
|
|
119
|
+
const locks = ctx.locks;
|
|
120
|
+
const restAccount = async (id, until) => {
|
|
121
|
+
const at = until ?? Date.now() + REST_MS;
|
|
122
|
+
await accounts.rest(spec.member, id, at);
|
|
123
|
+
return at;
|
|
124
|
+
};
|
|
125
|
+
const signedIn = async (agentId, provider) => !!(await ctx.request('byokit.accounts', { action: 'ready', agentId, provider })).ok;
|
|
126
|
+
const skipped = [];
|
|
127
|
+
for (;;) {
|
|
128
|
+
// Step 4: the selection is resolved once per attempt, before any request; `needs` alone resolves as 'default'.
|
|
129
|
+
const { pick, agent } = await accounts.pick(spec.member, { account: spec.account ?? 'default',
|
|
130
|
+
...(spec.model !== undefined ? { model: spec.model } : {}), ...(spec.needs ? { needs: spec.needs } : {}) }, skipped);
|
|
131
|
+
if (!pick.ok) {
|
|
132
|
+
if (pick.code === 'unknown_account')
|
|
133
|
+
throw new Error(`unknown account: ${spec.account}`); // a caller bug
|
|
134
|
+
if (pick.code === 'resting')
|
|
135
|
+
return { ok: false, kind: 'resting', until: await restAccount(pick.account, pick.until), message: pick.reason };
|
|
136
|
+
return { ok: false, kind: pick.code === 'none' ? 'signed-out' : pick.code === 'not_included' ? 'plan' : 'other', message: pick.reason };
|
|
137
|
+
}
|
|
138
|
+
const sessionKey = `agent:${agent}:${tail}`;
|
|
139
|
+
const automatic = pick.how === 'auto' || pick.why === 'first_ready';
|
|
140
|
+
// An unbound key runs on the picked account's agent, under the app key's tail. The run holds the agent's shared
|
|
141
|
+
// lock from its ready check to its end. An automatic pick not signed in is skipped before any event; an explicit
|
|
142
|
+
// one announces itself, then ends signed-out.
|
|
143
|
+
const end = await locks.shared(agent, async () => {
|
|
144
|
+
let ready;
|
|
145
|
+
try {
|
|
146
|
+
ready = await signedIn(agent, pick.account.provider);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
150
|
+
}
|
|
151
|
+
if (!ready && automatic)
|
|
152
|
+
return undefined;
|
|
153
|
+
const route = { session: sessionKey };
|
|
154
|
+
ctx.routes.set(spec.sessionKey, route);
|
|
155
|
+
try {
|
|
156
|
+
on?.({ type: 'account', account: pick.account.id, model: pick.model, sessionKey, how: pick.how, why: pick.why, considered: pick.considered });
|
|
157
|
+
if (!ready)
|
|
158
|
+
return { ok: false, kind: 'signed-out', message: `${pick.account.name} is not signed in` };
|
|
159
|
+
return await start(spec, on, { agentId: agent, sessionKey, idempotencyKey, system,
|
|
160
|
+
picked: pick.model ? account(pick.model) : undefined, output, chosen: pick }); // '' = no override
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
if (ctx.routes.get(spec.sessionKey) === route)
|
|
164
|
+
ctx.routes.delete(spec.sessionKey);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
if (end === undefined) {
|
|
168
|
+
skipped.push(pick.account.id);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// Step 7: a resting end rests the account, so the next Auto skips it until then.
|
|
172
|
+
if (end.ok || !('kind' in end) || end.kind !== 'resting')
|
|
173
|
+
return end;
|
|
174
|
+
const until = await restAccount(pick.account.id, end.until);
|
|
175
|
+
return { ...end, until };
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const start = async (spec, on, t) => {
|
|
179
|
+
const { agentId, sessionKey, idempotencyKey, system, picked, output, chosen } = t;
|
|
105
180
|
const release = spec.register !== false ? ctx.bridge.register({ sessionKey, member: spec.member }, spec.tools, idempotencyKey) : undefined;
|
|
106
181
|
let last;
|
|
107
182
|
let runId = ''; // gateway events for other runs carry a real runId and never match the empty one
|
|
@@ -140,7 +215,7 @@ export function createRuns(ctx) {
|
|
|
140
215
|
}
|
|
141
216
|
});
|
|
142
217
|
try {
|
|
143
|
-
if (picked) {
|
|
218
|
+
if (picked && !chosen) {
|
|
144
219
|
// The picked account must be the one called: check it is signed in for this member before the run, since
|
|
145
220
|
// the engine would otherwise fail the run only after admitting it. An explicit provider/model is strict on
|
|
146
221
|
// the pinned engine (never another provider or model), so this check plus the override is the guarantee.
|
|
@@ -207,7 +282,10 @@ export function createRuns(ctx) {
|
|
|
207
282
|
const usage = usageOf(agentMeta);
|
|
208
283
|
const planWindow = typeof agentMeta.provider === 'string'
|
|
209
284
|
? await planWindowOf(agentId, agentMeta.provider.toLowerCase()) : undefined;
|
|
210
|
-
|
|
285
|
+
const reported = typeof agentMeta.provider === 'string' && typeof agentMeta.model === 'string'
|
|
286
|
+
? `${agentMeta.provider}/${agentMeta.model}`.toLowerCase() : '';
|
|
287
|
+
return { ok: true, text, ...(parsed ? { data: parsed.data } : {}), ...(usage ? { usage } : {}), ...(planWindow ? { planWindow } : {}),
|
|
288
|
+
...(chosen ? { account: chosen.account.id, model: chosen.model || reported } : {}) };
|
|
211
289
|
}
|
|
212
290
|
if (result.stopReason === 'aborted' || (abortedByEngine && result.status !== 'ok'))
|
|
213
291
|
return { ok: false, aborted: true };
|
|
@@ -227,7 +305,7 @@ export function createRuns(ctx) {
|
|
|
227
305
|
};
|
|
228
306
|
return {
|
|
229
307
|
run,
|
|
230
|
-
steer: (k, t) => ctx.request('sessions.steer', { sessionKey: k, message: t }),
|
|
231
|
-
abort: async (k) => { await ctx.request('chat.abort', { sessionKey: k }); },
|
|
308
|
+
steer: (k, t) => ctx.request('sessions.steer', { sessionKey: ctx.routes.get(k)?.session ?? k, message: t }),
|
|
309
|
+
abort: async (k) => { await ctx.request('chat.abort', { sessionKey: ctx.routes.get(k)?.session ?? k }); },
|
|
232
310
|
};
|
|
233
311
|
}
|
package/dist/signin.d.ts
CHANGED
package/dist/signin.js
CHANGED
|
@@ -41,6 +41,16 @@ export function signIn(ctx, member, o, on) {
|
|
|
41
41
|
const { signal } = owner;
|
|
42
42
|
const driveOwner = new AbortController();
|
|
43
43
|
const wantsCallback = via === 'browser' && o.authChoice === 'openai';
|
|
44
|
+
// The pin builds ChatGPT's authorize URL unsigned, so a fresh sign-in can ask for the login screen itself.
|
|
45
|
+
const shown = (url) => {
|
|
46
|
+
if (!ctx.fresh || !wantsCallback)
|
|
47
|
+
return url;
|
|
48
|
+
const parsed = URL.canParse(url) ? new URL(url) : undefined;
|
|
49
|
+
if (parsed?.origin !== 'https://auth.openai.com' || parsed.pathname !== '/oauth/authorize')
|
|
50
|
+
return url;
|
|
51
|
+
parsed.searchParams.set('prompt', 'login');
|
|
52
|
+
return parsed.toString();
|
|
53
|
+
};
|
|
44
54
|
let server;
|
|
45
55
|
let sessionId = '';
|
|
46
56
|
let starting;
|
|
@@ -295,7 +305,7 @@ export function signIn(ctx, member, o, on) {
|
|
|
295
305
|
codeTimer = setTimeout(() => { codeExpired = true; owner.abort(); }, duration);
|
|
296
306
|
const message = deviceCode.message ?? current.message;
|
|
297
307
|
// The code is on the card; acknowledging it lets the engine poll the provider itself.
|
|
298
|
-
say({ ...(deviceCode.code ? { code: deviceCode.code } : {}), ...(current.externalUrl ? { url: current.externalUrl } : {}),
|
|
308
|
+
say({ ...(deviceCode.code ? { code: deviceCode.code } : {}), ...(current.externalUrl ? { url: shown(current.externalUrl) } : {}),
|
|
299
309
|
...(timed ? { expiresAt: Date.now() + duration } : {}), ...(message ? { message } : {}) });
|
|
300
310
|
const next = await answer(current);
|
|
301
311
|
if (next.done) {
|
|
@@ -326,7 +336,7 @@ export function signIn(ctx, member, o, on) {
|
|
|
326
336
|
}
|
|
327
337
|
if (current.type === 'note' || current.type === 'confirm' || current.type === 'select' || current.type === 'action') {
|
|
328
338
|
if (current.externalUrl)
|
|
329
|
-
say({ url: current.externalUrl });
|
|
339
|
+
say({ url: shown(current.externalUrl) });
|
|
330
340
|
const next = await answer(current);
|
|
331
341
|
if (next.done) {
|
|
332
342
|
terminal = next;
|
package/dist/testing/contract.js
CHANGED
|
@@ -18,7 +18,6 @@ const REQUIRED_METHODS = [
|
|
|
18
18
|
'plugin.approval.resolve', 'question.resolve', 'wizard.cancel', 'wizard.next',
|
|
19
19
|
];
|
|
20
20
|
// Driven by the kit but not advertised in hello (O11 probe, e.g. sessions.steer): routable, just not listed.
|
|
21
|
-
// The engine job's generated.test.ts audits the full generated table against hello instead of asserting it here.
|
|
22
21
|
const REQUIRED_EVENTS = [
|
|
23
22
|
'agent',
|
|
24
23
|
'exec.approval.requested', 'exec.approval.resolved',
|
|
@@ -20,6 +20,12 @@ export type FakeGateway = {
|
|
|
20
20
|
failNext(method: string, message: string): void;
|
|
21
21
|
drop(why: string): void;
|
|
22
22
|
handle(method: string, fn: FakeHandler): void;
|
|
23
|
+
/** Usage windows a later `models.authStatus` read reports for the agent's provider (O14: the Auto input). */
|
|
24
|
+
setUsage(agentId: string, provider: string, windows: {
|
|
25
|
+
label: string;
|
|
26
|
+
usedPercent?: number;
|
|
27
|
+
resetAt?: number;
|
|
28
|
+
}[]): void;
|
|
23
29
|
} & {
|
|
24
30
|
transport: GatewayTransport;
|
|
25
31
|
};
|