@adecore/agents 0.47.0 → 0.48.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/chat/chat-core.d.ts +2 -0
- package/dist/chat/chat-core.js +9 -4
- package/dist/chat/composer-preferences.d.ts +0 -1
- package/dist/chat/composer-preferences.js +3 -3
- package/dist/host/handlers.js +1 -0
- package/dist/providers/accounts/choose.d.ts +18 -0
- package/dist/providers/accounts/choose.js +27 -0
- package/dist/providers/accounts/launch.d.ts +2 -1
- package/dist/providers/accounts/service.d.ts +2 -0
- package/dist/providers/accounts/service.js +11 -4
- package/dist/providers/accounts/store.d.ts +2 -1
- package/dist/providers/accounts/store.js +35 -3
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/package.json +3 -3
- package/src/chat/chat-core.ts +15 -4
- package/src/chat/composer-preferences.ts +3 -5
- package/src/host/handlers.ts +1 -0
- package/src/providers/accounts/choose.ts +57 -0
- package/src/providers/accounts/launch.ts +3 -1
- package/src/providers/accounts/service.ts +22 -3
- package/src/providers/accounts/store.ts +51 -3
- package/src/providers/index.ts +1 -0
package/dist/chat/chat-core.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface InterruptedRun {
|
|
|
26
26
|
export interface ChatCoreHooks {
|
|
27
27
|
admit?(chatId: string, cwd: string): Promise<void>;
|
|
28
28
|
openingSelection?(chatId: string, kind: AgentKind): ModelSelection | undefined;
|
|
29
|
+
openingAccount?(chatId: string, kind: AgentKind, named: string | undefined): string | undefined;
|
|
29
30
|
runtimeModeFor?(chatId: string, mode: RuntimeMode): RuntimeMode;
|
|
30
31
|
instructionsFor?(chatId: string): string | null;
|
|
31
32
|
resumeNoteFor?(chatId: string): string | null;
|
|
@@ -131,6 +132,7 @@ export declare class ChatCore {
|
|
|
131
132
|
codexProcess(info: Pick<ChatInfo, 'cwd' | 'account'>): CodexProcessSpec;
|
|
132
133
|
claudeProjectsDirOf(info: Pick<ChatInfo, 'account'>): string;
|
|
133
134
|
canContinue(kind: AgentKind, from: string | undefined, to: string | undefined): boolean;
|
|
135
|
+
openingAccount(chatId: string, kind: AgentKind, named: string | undefined): string | undefined;
|
|
134
136
|
requireAccount(kind: AgentKind, account: string | undefined): void;
|
|
135
137
|
accountLabel(kind: AgentKind, account: string | undefined): string;
|
|
136
138
|
readChat(chatId: string): Promise<{
|
package/dist/chat/chat-core.js
CHANGED
|
@@ -185,6 +185,10 @@ export class ChatCore {
|
|
|
185
185
|
return this.accounts?.canContinue(kind, from, to) ?? false;
|
|
186
186
|
}
|
|
187
187
|
/* Throws for an account a CLI of this kind cannot start under on this machine. */
|
|
188
|
+
/* The account a start names, after the host had its say; with no account named, the top account of the CLI that is on. */
|
|
189
|
+
openingAccount(chatId, kind, named) {
|
|
190
|
+
return this.hooks.openingAccount?.(chatId, kind, named) ?? named ?? this.accounts?.defaultFor?.(kind);
|
|
191
|
+
}
|
|
188
192
|
requireAccount(kind, account) {
|
|
189
193
|
launchEnv(this.accounts, kind, account, this.env);
|
|
190
194
|
}
|
|
@@ -257,9 +261,9 @@ export class ChatCore {
|
|
|
257
261
|
const cwd = stored?.info.cwd ?? payload.cwd ?? homeFolder(this.env);
|
|
258
262
|
await this.hooks.admit?.(payload.chatId, cwd);
|
|
259
263
|
// A thread on disk keeps its account too. One it lost still opens, and says why on its next turn.
|
|
260
|
-
// A new one without an account takes the
|
|
261
|
-
//
|
|
262
|
-
const requested =
|
|
264
|
+
// A new one without an account takes the host's choice, else the top account of this CLI that is on.
|
|
265
|
+
// It is checked as named, before the default account loses its id, so one that is turned off is refused too.
|
|
266
|
+
const requested = stored ? undefined : this.openingAccount(payload.chatId, kind, payload.account);
|
|
263
267
|
if (!stored) {
|
|
264
268
|
this.requireAccount(kind, requested);
|
|
265
269
|
}
|
|
@@ -377,7 +381,8 @@ export class ChatCore {
|
|
|
377
381
|
if (to === from) {
|
|
378
382
|
return;
|
|
379
383
|
}
|
|
380
|
-
// As named, so a switch to the default account is refused while it is turned off.
|
|
384
|
+
// As named, so a switch to the default account is refused while it is turned off, and one the host turned off for this chat too.
|
|
385
|
+
this.hooks.openingAccount?.(session.id, kind, requested);
|
|
381
386
|
this.requireAccount(kind, requested);
|
|
382
387
|
if (holdsConversation(session.info) && !this.canContinue(kind, from, to)) {
|
|
383
388
|
const label = this.accountLabel(kind, to);
|
|
@@ -2,7 +2,6 @@ import type { AgentKind, ChatPreferencesPayload, ModelSelection, RuntimeMode } f
|
|
|
2
2
|
export interface ComposerPreference {
|
|
3
3
|
runtimeMode?: RuntimeMode;
|
|
4
4
|
selection?: ModelSelection;
|
|
5
|
-
account?: string;
|
|
6
5
|
}
|
|
7
6
|
export declare class ComposerPreferences {
|
|
8
7
|
private readonly byClient;
|
|
@@ -23,10 +23,10 @@ export class ComposerPreferences {
|
|
|
23
23
|
if (newest === null) {
|
|
24
24
|
return {};
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
// The accounts an older client still sends are left out: the order of the accounts decides where a start goes.
|
|
27
|
+
const { runtimeMode, selections } = newest.preference;
|
|
27
28
|
const selection = selections?.[provider];
|
|
28
|
-
|
|
29
|
-
return { ...(runtimeMode ? { runtimeMode } : {}), ...(selection ? { selection } : {}), ...(account ? { account } : {}) };
|
|
29
|
+
return { ...(runtimeMode ? { runtimeMode } : {}), ...(selection ? { selection } : {}) };
|
|
30
30
|
}
|
|
31
31
|
newest() {
|
|
32
32
|
let newest = null;
|
package/dist/host/handlers.js
CHANGED
|
@@ -101,6 +101,7 @@ export function accountHandlers(accounts) {
|
|
|
101
101
|
return {
|
|
102
102
|
'accounts.list': () => accounts.snapshot(),
|
|
103
103
|
'accounts.save': (payload) => accounts.save(payload.accounts),
|
|
104
|
+
'accounts.reorder': (payload) => accounts.reorder(payload.kind, payload.ids),
|
|
104
105
|
'accounts.refresh': () => accounts.refresh(),
|
|
105
106
|
'accounts.create': (payload) => accounts.create(payload),
|
|
106
107
|
'accounts.watchLogin': (payload) => {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type AgentKind, type ProviderAccountMap } from '@adecore/agent-contracts';
|
|
2
|
+
export type AccountUnusable = 'turned off on this machine' | 'signed out' | 'at its usage limit' | 'not available on this machine';
|
|
3
|
+
export interface AccountChoiceInput {
|
|
4
|
+
kind: AgentKind;
|
|
5
|
+
accounts: ProviderAccountMap;
|
|
6
|
+
named: string | undefined;
|
|
7
|
+
preferred: string | undefined;
|
|
8
|
+
off: readonly string[];
|
|
9
|
+
unusable(id: string): AccountUnusable | null;
|
|
10
|
+
}
|
|
11
|
+
export interface ChosenAccount {
|
|
12
|
+
account: string;
|
|
13
|
+
passedOver?: {
|
|
14
|
+
account: string;
|
|
15
|
+
reason: AccountUnusable;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export declare function chooseAccount(input: AccountChoiceInput): ChosenAccount;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { orderedAccountIds } from '@adecore/agent-contracts';
|
|
2
|
+
import { AccountError } from './launch.js';
|
|
3
|
+
/*
|
|
4
|
+
* The account a start takes: a named one unless the place turned it off, which refuses the start; else
|
|
5
|
+
* the preferred one while it is usable; else the first usable account in the person's order that the
|
|
6
|
+
* place offers; else the CLI's default account, which the launch refuses as before when it cannot run.
|
|
7
|
+
*/
|
|
8
|
+
export function chooseAccount(input) {
|
|
9
|
+
const { kind, accounts, named, preferred, off } = input;
|
|
10
|
+
if (named !== undefined) {
|
|
11
|
+
if (off.includes(named)) {
|
|
12
|
+
throw new AccountError('account-off-in-project', `The account '${named}' is turned off in this project.`);
|
|
13
|
+
}
|
|
14
|
+
return { account: named };
|
|
15
|
+
}
|
|
16
|
+
const offered = orderedAccountIds(accounts, kind).filter((id) => !off.includes(id));
|
|
17
|
+
const known = preferred !== undefined && offered.includes(preferred) ? preferred : undefined;
|
|
18
|
+
const reason = known === undefined ? null : input.unusable(known);
|
|
19
|
+
if (known !== undefined && reason === null) {
|
|
20
|
+
return { account: known };
|
|
21
|
+
}
|
|
22
|
+
const next = offered.find((id) => id !== known && input.unusable(id) === null) ?? kind;
|
|
23
|
+
if (off.includes(next)) {
|
|
24
|
+
throw new AccountError('account-off-in-project', 'No account this project offers can start right now.');
|
|
25
|
+
}
|
|
26
|
+
return known === undefined || reason === null ? { account: next } : { account: next, passedOver: { account: known, reason } };
|
|
27
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentKind } from '@adecore/agent-contracts';
|
|
2
2
|
import { CodedError } from '../../coded-error.ts';
|
|
3
3
|
import type { Env } from './accounts.ts';
|
|
4
|
-
export declare class AccountError extends CodedError<'account-unavailable' | 'account-incompatible'> {
|
|
4
|
+
export declare class AccountError extends CodedError<'account-unavailable' | 'account-incompatible' | 'account-off-in-project'> {
|
|
5
5
|
}
|
|
6
6
|
export interface AccountLaunches {
|
|
7
7
|
envFor(kind: AgentKind, account: string | undefined, baseEnv: Env): Env;
|
|
@@ -10,6 +10,7 @@ export interface AccountLaunches {
|
|
|
10
10
|
canContinue(kind: AgentKind, from: string | undefined, to: string | undefined): boolean;
|
|
11
11
|
labelOf(account: string): string;
|
|
12
12
|
launched?(kind: AgentKind, account: string | undefined): void;
|
|
13
|
+
defaultFor?(kind: AgentKind): string;
|
|
13
14
|
}
|
|
14
15
|
export declare function isDefaultAccountOf(kind: AgentKind, account: string | undefined): boolean;
|
|
15
16
|
export declare function storedAccount(kind: AgentKind, account: string | undefined): string | undefined;
|
|
@@ -67,6 +67,8 @@ export declare class ProviderAccountsService implements AccountLaunches {
|
|
|
67
67
|
start(): void;
|
|
68
68
|
stop(): void;
|
|
69
69
|
save(accounts: ProviderAccountMap): Promise<ProviderAccounts>;
|
|
70
|
+
defaultFor(kind: AgentKind): string;
|
|
71
|
+
reorder(kind: AgentKind, ids: readonly string[]): Promise<ProviderAccounts>;
|
|
70
72
|
create(payload: ProviderAccountCreatePayload): Promise<ProviderAccountCreateResult>;
|
|
71
73
|
watchLogin(id: string): Promise<void>;
|
|
72
74
|
refresh(): Promise<ProviderAccounts>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, statSync } from 'node:fs';
|
|
2
2
|
import { mkdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { KNOWN_AGENT_KINDS } from '@adecore/agent-contracts';
|
|
4
|
+
import { KNOWN_AGENT_KINDS, topAccountOf } from '@adecore/agent-contracts';
|
|
5
5
|
import { DEFAULT_CODEX_CLIENT } from '../../chat/codex-transport.js';
|
|
6
6
|
import { ClientSinks } from '../../client-sinks.js';
|
|
7
7
|
import { errorText } from '../../error-text.js';
|
|
@@ -10,7 +10,7 @@ import { accountEnv, accountFolder, canContinue, defaultFolder, expandHome, isDe
|
|
|
10
10
|
import { AccountError, isDefaultAccountOf } from './launch.js';
|
|
11
11
|
import { prepareShadowHome } from './shadow-home.js';
|
|
12
12
|
import { ASKED_KINDS, askAccountAs } from './status.js';
|
|
13
|
-
import { acceptAccounts, accountsPath, InvalidAccountError, readAccounts, wireAccounts, writeAccounts } from './store.js';
|
|
13
|
+
import { acceptAccounts, accountsPath, InvalidAccountError, readAccounts, reorderedAccounts, wireAccounts, writeAccounts } from './store.js';
|
|
14
14
|
import { DEFAULT_ACCOUNTS_HOST, platformSecrets, secretKey, SecretsUnavailableError } from './variables.js';
|
|
15
15
|
/* Who is signed in changes by hand and rarely; often enough to notice, rarely enough not to start CLIs all day. */
|
|
16
16
|
const CHECK_INTERVAL_MS = 15 * 60_000;
|
|
@@ -232,6 +232,13 @@ export class ProviderAccountsService {
|
|
|
232
232
|
save(accounts) {
|
|
233
233
|
return this.writes.run(() => this.commit(accounts, this.folderExists));
|
|
234
234
|
}
|
|
235
|
+
defaultFor(kind) {
|
|
236
|
+
return topAccountOf(this.accounts, kind);
|
|
237
|
+
}
|
|
238
|
+
/* Puts the accounts of one CLI in this order; an id of another CLI or one this machine does not have refuses it. */
|
|
239
|
+
reorder(kind, ids) {
|
|
240
|
+
return this.writes.run(() => this.commit(reorderedAccounts(this.accounts, kind, ids), this.folderExists, true));
|
|
241
|
+
}
|
|
235
242
|
/*
|
|
236
243
|
* An account in a folder of its own under `<home>/accounts`, signed in by nobody yet. A
|
|
237
244
|
* Codex account is a shadow home over the CLI's own folder, so its threads go on under the other.
|
|
@@ -305,8 +312,8 @@ export class ProviderAccountsService {
|
|
|
305
312
|
accountProviders() {
|
|
306
313
|
return { kinds: this.kinds(), get: (kind) => this.providers.get(kind) };
|
|
307
314
|
}
|
|
308
|
-
async commit(accounts, isDirectory) {
|
|
309
|
-
const stored = acceptAccounts(accounts, this.stored, this.accountProviders(), this.env, isDirectory, this.host);
|
|
315
|
+
async commit(accounts, isDirectory, takeOrder = false) {
|
|
316
|
+
const stored = acceptAccounts(accounts, this.stored, this.accountProviders(), this.env, isDirectory, this.host, takeOrder);
|
|
310
317
|
const { writes, removals } = this.planSecrets(stored);
|
|
311
318
|
for (const [key, value] of writes) {
|
|
312
319
|
await this.requireSecrets().write(key, value);
|
|
@@ -10,4 +10,5 @@ export declare function wireAccounts(stored: StoredAccounts, kinds: readonly Age
|
|
|
10
10
|
export declare class InvalidAccountError extends CodedError<'invalid-account'> {
|
|
11
11
|
constructor(message: string);
|
|
12
12
|
}
|
|
13
|
-
export declare function acceptAccounts(saved: ProviderAccountMap, before: StoredAccounts, providers: AccountProviders, env: Env, isDirectory: (path: string) => boolean, host?: AccountsHost): StoredAccounts;
|
|
13
|
+
export declare function acceptAccounts(saved: ProviderAccountMap, before: StoredAccounts, providers: AccountProviders, env: Env, isDirectory: (path: string) => boolean, host?: AccountsHost, takeOrder?: boolean): StoredAccounts;
|
|
14
|
+
export declare function reorderedAccounts(accounts: ProviderAccountMap, kind: AgentKind, ids: readonly string[]): ProviderAccountMap;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { ProviderAccountIdSchema, ProviderAccountSchema } from '@adecore/agent-contracts';
|
|
3
|
+
import { orderedAccountIds, ProviderAccountIdSchema, ProviderAccountSchema } from '@adecore/agent-contracts';
|
|
4
4
|
import { CodedError } from '../../coded-error.js';
|
|
5
5
|
import { isNotFound, writeAtomic } from '../../fs.js';
|
|
6
6
|
import { accountFolder, accountProblem, defaultFolder, expandHome, folderVariablesOf, isDefaultAccount, isOfferedKind, readAccount, withDefaults } from './accounts.js';
|
|
@@ -88,7 +88,9 @@ function linkProblem(id, account, before, provider, env, isDirectory) {
|
|
|
88
88
|
* client that knows the kind no better than this host could only have cut short. Sensitive values
|
|
89
89
|
* are still in it: the caller puts them in the keychain before the file is written.
|
|
90
90
|
*/
|
|
91
|
-
export function acceptAccounts(saved, before, providers, env, isDirectory, host = DEFAULT_ACCOUNTS_HOST
|
|
91
|
+
export function acceptAccounts(saved, before, providers, env, isDirectory, host = DEFAULT_ACCOUNTS_HOST,
|
|
92
|
+
// Only `accounts.reorder` takes the order it is given.
|
|
93
|
+
takeOrder = false) {
|
|
92
94
|
const stored = {};
|
|
93
95
|
const reserved = { host, folderVariables: folderVariablesOf(providers) };
|
|
94
96
|
for (const [id, account] of Object.entries(withDefaults(saved, providers.kinds))) {
|
|
@@ -106,7 +108,37 @@ export function acceptAccounts(saved, before, providers, env, isDirectory, host
|
|
|
106
108
|
if (problem !== null) {
|
|
107
109
|
throw new InvalidAccountError(`${id}: ${problem}`);
|
|
108
110
|
}
|
|
109
|
-
stored[id] = account;
|
|
111
|
+
stored[id] = takeOrder ? account : withKeptOrder(account, before[id]);
|
|
110
112
|
}
|
|
111
113
|
return stored;
|
|
112
114
|
}
|
|
115
|
+
/*
|
|
116
|
+
* Only `accounts.reorder` moves an account, so a save keeps the place the file has: an older client
|
|
117
|
+
* strips `order`, and a newer one may send the order it read before a reorder elsewhere.
|
|
118
|
+
*/
|
|
119
|
+
function withKeptOrder(account, before) {
|
|
120
|
+
const { order: _sent, ...rest } = account;
|
|
121
|
+
const order = typeof before === 'object' && before !== null ? before.order : undefined;
|
|
122
|
+
return typeof order === 'number' ? { ...rest, order } : rest;
|
|
123
|
+
}
|
|
124
|
+
/* The map with the accounts of one CLI in this order; one of that CLI not named, such as one made meanwhile, follows in the order it had. */
|
|
125
|
+
export function reorderedAccounts(accounts, kind, ids) {
|
|
126
|
+
if (new Set(ids).size !== ids.length) {
|
|
127
|
+
throw new InvalidAccountError('an account is named twice in the order');
|
|
128
|
+
}
|
|
129
|
+
for (const id of ids) {
|
|
130
|
+
const account = accounts[id];
|
|
131
|
+
if (account === undefined) {
|
|
132
|
+
throw new InvalidAccountError(`${id}: no account on this machine has that id`);
|
|
133
|
+
}
|
|
134
|
+
if (account.kind !== kind) {
|
|
135
|
+
throw new InvalidAccountError(`${id}: it is an account of ${account.kind}, not of ${kind}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const order = [...ids, ...orderedAccountIds(accounts, kind).filter((id) => !ids.includes(id))];
|
|
139
|
+
const reordered = { ...accounts };
|
|
140
|
+
for (const [place, id] of order.entries()) {
|
|
141
|
+
reordered[id] = { ...accounts[id], order: place };
|
|
142
|
+
}
|
|
143
|
+
return reordered;
|
|
144
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { type AcpCatalogStore } from './acp-agent-state.ts';
|
|
2
2
|
export { ACP_CAPABILITIES, type AcpProviderSpec, createAcpProvider } from './acp-provider.ts';
|
|
3
3
|
export { type AcpAccountProbe, askAcpAccount, type ProbeTimer } from './accounts/acp-account.ts';
|
|
4
|
+
export { chooseAccount, type AccountChoiceInput, type AccountUnusable, type ChosenAccount } from './accounts/choose.ts';
|
|
4
5
|
export { AccountError, type AccountLaunches, storedAccount, launchEnv } from './accounts/launch.ts';
|
|
5
6
|
export { ProviderAccountsService, type ProviderAccountsOptions } from './accounts/service.ts';
|
|
6
7
|
export type { ShadowHomeReport } from './accounts/shadow-home.ts';
|
package/dist/providers/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export {} from './acp-agent-state.js';
|
|
3
3
|
export { ACP_CAPABILITIES, createAcpProvider } from './acp-provider.js';
|
|
4
4
|
export { askAcpAccount } from './accounts/acp-account.js';
|
|
5
|
+
export { chooseAccount } from './accounts/choose.js';
|
|
5
6
|
export { AccountError, storedAccount, launchEnv } from './accounts/launch.js';
|
|
6
7
|
export { ProviderAccountsService } from './accounts/service.js';
|
|
7
8
|
export { variablesProblem, keychainService } from './accounts/variables.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -93,8 +93,8 @@
|
|
|
93
93
|
"test": "bun --conditions=source test"
|
|
94
94
|
},
|
|
95
95
|
"dependencies": {
|
|
96
|
-
"@adecore/agent-contracts": "0.
|
|
97
|
-
"@adecore/intelligent-ui": "0.
|
|
96
|
+
"@adecore/agent-contracts": "0.48.0",
|
|
97
|
+
"@adecore/intelligent-ui": "0.48.0",
|
|
98
98
|
"zod": "^4.6.7"
|
|
99
99
|
},
|
|
100
100
|
"devDependencies": {
|
package/src/chat/chat-core.ts
CHANGED
|
@@ -117,6 +117,11 @@ export interface ChatCoreHooks {
|
|
|
117
117
|
admit?(chatId: string, cwd: string): Promise<void>;
|
|
118
118
|
/* The model a new chat starts on when the host already knows; the explicit one still wins. */
|
|
119
119
|
openingSelection?(chatId: string, kind: AgentKind): ModelSelection | undefined;
|
|
120
|
+
/*
|
|
121
|
+
* The account a new chat starts under, given the one its start named (undefined for none);
|
|
122
|
+
* undefined leaves it to the order of the accounts. Throws an `AccountError` to refuse the start.
|
|
123
|
+
*/
|
|
124
|
+
openingAccount?(chatId: string, kind: AgentKind, named: string | undefined): string | undefined;
|
|
120
125
|
/* The mode a chat runs in, which a host may narrow whatever its record or its client says. */
|
|
121
126
|
runtimeModeFor?(chatId: string, mode: RuntimeMode): RuntimeMode;
|
|
122
127
|
/* What the agent is told once at the start of every process of this chat, in place of `instructions`; null for nothing. */
|
|
@@ -387,6 +392,11 @@ export class ChatCore {
|
|
|
387
392
|
}
|
|
388
393
|
|
|
389
394
|
/* Throws for an account a CLI of this kind cannot start under on this machine. */
|
|
395
|
+
/* The account a start names, after the host had its say; with no account named, the top account of the CLI that is on. */
|
|
396
|
+
openingAccount(chatId: string, kind: AgentKind, named: string | undefined): string | undefined {
|
|
397
|
+
return this.hooks.openingAccount?.(chatId, kind, named) ?? named ?? this.accounts?.defaultFor?.(kind);
|
|
398
|
+
}
|
|
399
|
+
|
|
390
400
|
requireAccount(kind: AgentKind, account: string | undefined): void {
|
|
391
401
|
launchEnv(this.accounts, kind, account, this.env);
|
|
392
402
|
}
|
|
@@ -479,9 +489,9 @@ export class ChatCore {
|
|
|
479
489
|
const cwd = stored?.info.cwd ?? payload.cwd ?? homeFolder(this.env);
|
|
480
490
|
await this.hooks.admit?.(payload.chatId, cwd);
|
|
481
491
|
// A thread on disk keeps its account too. One it lost still opens, and says why on its next turn.
|
|
482
|
-
// A new one without an account takes the
|
|
483
|
-
//
|
|
484
|
-
const requested =
|
|
492
|
+
// A new one without an account takes the host's choice, else the top account of this CLI that is on.
|
|
493
|
+
// It is checked as named, before the default account loses its id, so one that is turned off is refused too.
|
|
494
|
+
const requested = stored ? undefined : this.openingAccount(payload.chatId, kind, payload.account);
|
|
485
495
|
|
|
486
496
|
if (!stored) {
|
|
487
497
|
this.requireAccount(kind, requested);
|
|
@@ -626,7 +636,8 @@ export class ChatCore {
|
|
|
626
636
|
return;
|
|
627
637
|
}
|
|
628
638
|
|
|
629
|
-
// As named, so a switch to the default account is refused while it is turned off.
|
|
639
|
+
// As named, so a switch to the default account is refused while it is turned off, and one the host turned off for this chat too.
|
|
640
|
+
this.hooks.openingAccount?.(session.id, kind, requested);
|
|
630
641
|
this.requireAccount(kind, requested);
|
|
631
642
|
if (holdsConversation(session.info) && !this.canContinue(kind, from, to)) {
|
|
632
643
|
const label = this.accountLabel(kind, to);
|
|
@@ -3,8 +3,6 @@ import type { AgentKind, ChatPreferencesPayload, ModelSelection, RuntimeMode } f
|
|
|
3
3
|
export interface ComposerPreference {
|
|
4
4
|
runtimeMode?: RuntimeMode;
|
|
5
5
|
selection?: ModelSelection;
|
|
6
|
-
// The account a new chat or agent of this CLI starts under when nothing names one.
|
|
7
|
-
account?: string;
|
|
8
6
|
}
|
|
9
7
|
|
|
10
8
|
interface Held {
|
|
@@ -44,11 +42,11 @@ export class ComposerPreferences {
|
|
|
44
42
|
return {};
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
|
|
45
|
+
// The accounts an older client still sends are left out: the order of the accounts decides where a start goes.
|
|
46
|
+
const { runtimeMode, selections } = newest.preference;
|
|
48
47
|
const selection = selections?.[provider];
|
|
49
|
-
const account = accounts?.[provider];
|
|
50
48
|
|
|
51
|
-
return { ...(runtimeMode ? { runtimeMode } : {}), ...(selection ? { selection } : {})
|
|
49
|
+
return { ...(runtimeMode ? { runtimeMode } : {}), ...(selection ? { selection } : {}) };
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
private newest(): Held | null {
|
package/src/host/handlers.ts
CHANGED
|
@@ -150,6 +150,7 @@ export function accountHandlers(accounts: ProviderAccountsService): { [T in Acco
|
|
|
150
150
|
return {
|
|
151
151
|
'accounts.list': () => accounts.snapshot(),
|
|
152
152
|
'accounts.save': (payload) => accounts.save(payload.accounts),
|
|
153
|
+
'accounts.reorder': (payload) => accounts.reorder(payload.kind, payload.ids),
|
|
153
154
|
'accounts.refresh': () => accounts.refresh(),
|
|
154
155
|
'accounts.create': (payload) => accounts.create(payload),
|
|
155
156
|
'accounts.watchLogin': (payload) => {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { orderedAccountIds, type AgentKind, type ProviderAccountMap } from '@adecore/agent-contracts';
|
|
2
|
+
import { AccountError } from './launch.ts';
|
|
3
|
+
|
|
4
|
+
/* Why an account cannot take a start right now, in words that finish "is ...". */
|
|
5
|
+
export type AccountUnusable = 'turned off on this machine' | 'signed out' | 'at its usage limit' | 'not available on this machine';
|
|
6
|
+
|
|
7
|
+
export interface AccountChoiceInput {
|
|
8
|
+
kind: AgentKind;
|
|
9
|
+
accounts: ProviderAccountMap;
|
|
10
|
+
/* The account the start named; undefined for none. */
|
|
11
|
+
named: string | undefined;
|
|
12
|
+
/* The account the place of the start prefers, such as a project's default. */
|
|
13
|
+
preferred: string | undefined;
|
|
14
|
+
/* The accounts the place of the start never offers. */
|
|
15
|
+
off: readonly string[];
|
|
16
|
+
/* Why an account cannot take a start right now, or null when it can. */
|
|
17
|
+
unusable(id: string): AccountUnusable | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ChosenAccount {
|
|
21
|
+
account: string;
|
|
22
|
+
/* The preferred account the start passed over, and why; absent when it took that one or none was preferred. */
|
|
23
|
+
passedOver?: { account: string; reason: AccountUnusable };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* The account a start takes: a named one unless the place turned it off, which refuses the start; else
|
|
28
|
+
* the preferred one while it is usable; else the first usable account in the person's order that the
|
|
29
|
+
* place offers; else the CLI's default account, which the launch refuses as before when it cannot run.
|
|
30
|
+
*/
|
|
31
|
+
export function chooseAccount(input: AccountChoiceInput): ChosenAccount {
|
|
32
|
+
const { kind, accounts, named, preferred, off } = input;
|
|
33
|
+
|
|
34
|
+
if (named !== undefined) {
|
|
35
|
+
if (off.includes(named)) {
|
|
36
|
+
throw new AccountError('account-off-in-project', `The account '${named}' is turned off in this project.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { account: named };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const offered = orderedAccountIds(accounts, kind).filter((id) => !off.includes(id));
|
|
43
|
+
const known = preferred !== undefined && offered.includes(preferred) ? preferred : undefined;
|
|
44
|
+
const reason = known === undefined ? null : input.unusable(known);
|
|
45
|
+
|
|
46
|
+
if (known !== undefined && reason === null) {
|
|
47
|
+
return { account: known };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const next = offered.find((id) => id !== known && input.unusable(id) === null) ?? kind;
|
|
51
|
+
|
|
52
|
+
if (off.includes(next)) {
|
|
53
|
+
throw new AccountError('account-off-in-project', 'No account this project offers can start right now.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return known === undefined || reason === null ? { account: next } : { account: next, passedOver: { account: known, reason } };
|
|
57
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { AgentKind } from '@adecore/agent-contracts';
|
|
|
2
2
|
import { CodedError } from '../../coded-error.ts';
|
|
3
3
|
import type { Env } from './accounts.ts';
|
|
4
4
|
|
|
5
|
-
export class AccountError extends CodedError<'account-unavailable' | 'account-incompatible'> {}
|
|
5
|
+
export class AccountError extends CodedError<'account-unavailable' | 'account-incompatible' | 'account-off-in-project'> {}
|
|
6
6
|
|
|
7
7
|
/*
|
|
8
8
|
* The accounts as a terminal or a chat starts a CLI under one. Absent, or the CLI's own kind, is the
|
|
@@ -21,6 +21,8 @@ export interface AccountLaunches {
|
|
|
21
21
|
labelOf(account: string): string;
|
|
22
22
|
/* A CLI started under this account a moment ago, which is what keeps its plan read. */
|
|
23
23
|
launched?(kind: AgentKind, account: string | undefined): void;
|
|
24
|
+
/* The account a start of this CLI takes when nothing names one: the top one in the person's order that is on. */
|
|
25
|
+
defaultFor?(kind: AgentKind): string;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export function isDefaultAccountOf(kind: AgentKind, account: string | undefined): boolean {
|
|
@@ -3,6 +3,7 @@ import { mkdir } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import {
|
|
5
5
|
KNOWN_AGENT_KINDS,
|
|
6
|
+
topAccountOf,
|
|
6
7
|
type AgentKind,
|
|
7
8
|
type ProviderAccount,
|
|
8
9
|
type ProviderAccountCreatePayload,
|
|
@@ -34,7 +35,16 @@ import {
|
|
|
34
35
|
import { AccountError, isDefaultAccountOf, type AccountLaunches } from './launch.ts';
|
|
35
36
|
import { prepareShadowHome, type ShadowHomeReport } from './shadow-home.ts';
|
|
36
37
|
import { ASKED_KINDS, askAccountAs, type AskAccount } from './status.ts';
|
|
37
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
acceptAccounts,
|
|
40
|
+
accountsPath,
|
|
41
|
+
InvalidAccountError,
|
|
42
|
+
readAccounts,
|
|
43
|
+
reorderedAccounts,
|
|
44
|
+
wireAccounts,
|
|
45
|
+
writeAccounts,
|
|
46
|
+
type StoredAccounts
|
|
47
|
+
} from './store.ts';
|
|
38
48
|
import { DEFAULT_ACCOUNTS_HOST, platformSecrets, secretKey, SecretsUnavailableError, type AccountsHost, type SecretStore } from './variables.ts';
|
|
39
49
|
|
|
40
50
|
/* Who is signed in changes by hand and rarely; often enough to notice, rarely enough not to start CLIs all day. */
|
|
@@ -343,6 +353,15 @@ export class ProviderAccountsService implements AccountLaunches {
|
|
|
343
353
|
return this.writes.run(() => this.commit(accounts, this.folderExists));
|
|
344
354
|
}
|
|
345
355
|
|
|
356
|
+
defaultFor(kind: AgentKind): string {
|
|
357
|
+
return topAccountOf(this.accounts, kind);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* Puts the accounts of one CLI in this order; an id of another CLI or one this machine does not have refuses it. */
|
|
361
|
+
reorder(kind: AgentKind, ids: readonly string[]): Promise<ProviderAccounts> {
|
|
362
|
+
return this.writes.run(() => this.commit(reorderedAccounts(this.accounts, kind, ids), this.folderExists, true));
|
|
363
|
+
}
|
|
364
|
+
|
|
346
365
|
/*
|
|
347
366
|
* An account in a folder of its own under `<home>/accounts`, signed in by nobody yet. A
|
|
348
367
|
* Codex account is a shadow home over the CLI's own folder, so its threads go on under the other.
|
|
@@ -437,8 +456,8 @@ export class ProviderAccountsService implements AccountLaunches {
|
|
|
437
456
|
return { kinds: this.kinds(), get: (kind) => this.providers.get(kind) };
|
|
438
457
|
}
|
|
439
458
|
|
|
440
|
-
private async commit(accounts: ProviderAccountMap, isDirectory: (path: string) => boolean): Promise<ProviderAccounts> {
|
|
441
|
-
const stored = acceptAccounts(accounts, this.stored, this.accountProviders(), this.env, isDirectory, this.host);
|
|
459
|
+
private async commit(accounts: ProviderAccountMap, isDirectory: (path: string) => boolean, takeOrder = false): Promise<ProviderAccounts> {
|
|
460
|
+
const stored = acceptAccounts(accounts, this.stored, this.accountProviders(), this.env, isDirectory, this.host, takeOrder);
|
|
442
461
|
const { writes, removals } = this.planSecrets(stored);
|
|
443
462
|
|
|
444
463
|
for (const [key, value] of writes) {
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
orderedAccountIds,
|
|
5
|
+
ProviderAccountIdSchema,
|
|
6
|
+
ProviderAccountSchema,
|
|
7
|
+
type AgentKind,
|
|
8
|
+
type ProviderAccount,
|
|
9
|
+
type ProviderAccountMap
|
|
10
|
+
} from '@adecore/agent-contracts';
|
|
4
11
|
import { CodedError } from '../../coded-error.ts';
|
|
5
12
|
import { isNotFound, writeAtomic } from '../../fs.ts';
|
|
6
13
|
import type { ChatProvider } from '../provider.ts';
|
|
@@ -146,7 +153,9 @@ export function acceptAccounts(
|
|
|
146
153
|
providers: AccountProviders,
|
|
147
154
|
env: Env,
|
|
148
155
|
isDirectory: (path: string) => boolean,
|
|
149
|
-
host: AccountsHost = DEFAULT_ACCOUNTS_HOST
|
|
156
|
+
host: AccountsHost = DEFAULT_ACCOUNTS_HOST,
|
|
157
|
+
// Only `accounts.reorder` takes the order it is given.
|
|
158
|
+
takeOrder = false
|
|
150
159
|
): StoredAccounts {
|
|
151
160
|
const stored: StoredAccounts = {};
|
|
152
161
|
const reserved = { host, folderVariables: folderVariablesOf(providers) };
|
|
@@ -173,8 +182,47 @@ export function acceptAccounts(
|
|
|
173
182
|
throw new InvalidAccountError(`${id}: ${problem}`);
|
|
174
183
|
}
|
|
175
184
|
|
|
176
|
-
stored[id] = account;
|
|
185
|
+
stored[id] = takeOrder ? account : withKeptOrder(account, before[id]);
|
|
177
186
|
}
|
|
178
187
|
|
|
179
188
|
return stored;
|
|
180
189
|
}
|
|
190
|
+
|
|
191
|
+
/*
|
|
192
|
+
* Only `accounts.reorder` moves an account, so a save keeps the place the file has: an older client
|
|
193
|
+
* strips `order`, and a newer one may send the order it read before a reorder elsewhere.
|
|
194
|
+
*/
|
|
195
|
+
function withKeptOrder(account: ProviderAccount, before: unknown): ProviderAccount {
|
|
196
|
+
const { order: _sent, ...rest } = account;
|
|
197
|
+
const order = typeof before === 'object' && before !== null ? (before as { order?: unknown }).order : undefined;
|
|
198
|
+
|
|
199
|
+
return typeof order === 'number' ? { ...rest, order } : rest;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* The map with the accounts of one CLI in this order; one of that CLI not named, such as one made meanwhile, follows in the order it had. */
|
|
203
|
+
export function reorderedAccounts(accounts: ProviderAccountMap, kind: AgentKind, ids: readonly string[]): ProviderAccountMap {
|
|
204
|
+
if (new Set(ids).size !== ids.length) {
|
|
205
|
+
throw new InvalidAccountError('an account is named twice in the order');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const id of ids) {
|
|
209
|
+
const account = accounts[id];
|
|
210
|
+
|
|
211
|
+
if (account === undefined) {
|
|
212
|
+
throw new InvalidAccountError(`${id}: no account on this machine has that id`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (account.kind !== kind) {
|
|
216
|
+
throw new InvalidAccountError(`${id}: it is an account of ${account.kind}, not of ${kind}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const order = [...ids, ...orderedAccountIds(accounts, kind).filter((id) => !ids.includes(id))];
|
|
221
|
+
const reordered: ProviderAccountMap = { ...accounts };
|
|
222
|
+
|
|
223
|
+
for (const [place, id] of order.entries()) {
|
|
224
|
+
reordered[id] = { ...accounts[id], order: place } as ProviderAccount;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return reordered;
|
|
228
|
+
}
|
package/src/providers/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export { type AcpCatalogStore } from './acp-agent-state.ts';
|
|
3
3
|
export { ACP_CAPABILITIES, type AcpProviderSpec, createAcpProvider } from './acp-provider.ts';
|
|
4
4
|
export { type AcpAccountProbe, askAcpAccount, type ProbeTimer } from './accounts/acp-account.ts';
|
|
5
|
+
export { chooseAccount, type AccountChoiceInput, type AccountUnusable, type ChosenAccount } from './accounts/choose.ts';
|
|
5
6
|
export { AccountError, type AccountLaunches, storedAccount, launchEnv } from './accounts/launch.ts';
|
|
6
7
|
export { ProviderAccountsService, type ProviderAccountsOptions } from './accounts/service.ts';
|
|
7
8
|
export type { ShadowHomeReport } from './accounts/shadow-home.ts';
|