@adecore/agents 0.47.0 → 0.48.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/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/messages/deliver-notice.d.ts +2 -2
- package/dist/messages/deliver-notice.js +20 -14
- 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/messages/deliver-notice.ts +24 -16
- 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) => {
|
|
@@ -9,13 +9,13 @@ export interface MessageWords {
|
|
|
9
9
|
}
|
|
10
10
|
export interface ChatNoticeTargets {
|
|
11
11
|
chat(id: string): Promise<'idle' | 'running' | 'none'>;
|
|
12
|
-
|
|
12
|
+
wokenBy(id: string): readonly string[];
|
|
13
13
|
}
|
|
14
14
|
export interface ChatDelivery {
|
|
15
15
|
outcome: 'wake' | 'no-chat' | 'in-turn' | 'from-message';
|
|
16
16
|
waiting: number;
|
|
17
17
|
}
|
|
18
|
-
export declare function
|
|
18
|
+
export declare function turnWokenBy(items: readonly ChatItem[], activeTurnId: string | null): readonly string[];
|
|
19
19
|
export declare function chatNoticeTargets(chats: Pick<ChatCore, 'get' | 'hasStored'>): ChatNoticeTargets;
|
|
20
20
|
export declare function deliverToChat(store: Pick<NoticeStore, 'put'>, targets: ChatNoticeTargets, notice: Omit<Notice, 'createdAt'>): Promise<ChatDelivery>;
|
|
21
21
|
export interface NoticeChat {
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
/*
|
|
2
|
-
*
|
|
3
|
-
* read each other from waking each other forever. It rides on the turn,
|
|
4
|
-
* Between turns the last one counts, since work it left running (a
|
|
5
|
-
* write, and a turn the CLI opened by itself takes the step of the
|
|
2
|
+
* Who sent the message that opened the turn a chat is running, empty when no message opened it,
|
|
3
|
+
* which stops two agents that read each other from waking each other forever. It rides on the turn,
|
|
4
|
+
* so it survives a restart. Between turns the last one counts, since work it left running (a
|
|
5
|
+
* background subagent) may still write, and a turn the CLI opened by itself takes the step of the
|
|
6
|
+
* turn before it.
|
|
6
7
|
*/
|
|
7
|
-
export function
|
|
8
|
+
export function turnWokenBy(items, activeTurnId) {
|
|
8
9
|
const from = activeTurnId === null ? items.findLastIndex((item) => item.kind === 'turn') : items.findIndex((item) => item.id === activeTurnId);
|
|
9
10
|
for (let i = from; i >= 0; i--) {
|
|
10
11
|
const turn = items[i];
|
|
11
12
|
if (turn.kind !== 'turn') {
|
|
12
13
|
continue;
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
const senders = turn.messageFrom ?? [];
|
|
16
|
+
if (senders.length > 0) {
|
|
17
|
+
return senders;
|
|
16
18
|
}
|
|
17
19
|
if (turn.origin !== 'agent' || turn.taskIds !== undefined || turn.summaryFor !== undefined) {
|
|
18
|
-
return
|
|
20
|
+
return [];
|
|
19
21
|
}
|
|
20
22
|
}
|
|
21
|
-
return
|
|
23
|
+
return [];
|
|
22
24
|
}
|
|
23
25
|
/* The chats of a core as a message sees them. */
|
|
24
26
|
export function chatNoticeTargets(chats) {
|
|
@@ -31,17 +33,19 @@ export function chatNoticeTargets(chats) {
|
|
|
31
33
|
// A chat nobody has loaded is idle: the turn opens on the thread the host reads back from disk.
|
|
32
34
|
return (await chats.hasStored(id)) ? 'idle' : 'none';
|
|
33
35
|
},
|
|
34
|
-
|
|
36
|
+
wokenBy: (id) => {
|
|
35
37
|
const session = chats.get(id);
|
|
36
|
-
return session
|
|
38
|
+
return session === undefined ? [] : turnWokenBy(session.thread.list(), session.info.activeTurnId);
|
|
37
39
|
}
|
|
38
40
|
};
|
|
39
41
|
}
|
|
40
42
|
/*
|
|
41
43
|
* One message on its way to a chat, and whether the chat owes a turn on it. A chat between turns gets
|
|
42
44
|
* one, as for a settled task, or the message sits there until someone happens to prompt that chat.
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
+
* A turn a message opened wakes nobody with a message of its own, except the sender it answers,
|
|
46
|
+
* once: only while that sender's own turn was opened by no message, so a question and its answer
|
|
47
|
+
* take two steps and the turn the answer opens wakes nobody. A node that runs something else, such
|
|
48
|
+
* as a terminal, is the host's to deliver to before this.
|
|
45
49
|
*/
|
|
46
50
|
export async function deliverToChat(store, targets, notice) {
|
|
47
51
|
const waiting = await store.put(notice);
|
|
@@ -52,7 +56,9 @@ export async function deliverToChat(store, targets, notice) {
|
|
|
52
56
|
if (chat === 'running') {
|
|
53
57
|
return { outcome: 'in-turn', waiting };
|
|
54
58
|
}
|
|
55
|
-
|
|
59
|
+
const wokenBy = targets.wokenBy(notice.from);
|
|
60
|
+
const answer = wokenBy.includes(notice.targetId) && targets.wokenBy(notice.targetId).length === 0;
|
|
61
|
+
if (wokenBy.length > 0 && !answer) {
|
|
56
62
|
return { outcome: 'from-message', waiting };
|
|
57
63
|
}
|
|
58
64
|
return { outcome: 'wake', waiting };
|
|
@@ -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.1",
|
|
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.1",
|
|
97
|
+
"@adecore/intelligent-ui": "0.48.1",
|
|
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) => {
|
|
@@ -18,8 +18,8 @@ export interface MessageWords {
|
|
|
18
18
|
export interface ChatNoticeTargets {
|
|
19
19
|
/* What the host holds under this id: a chat between turns, one that is in a turn, or no chat at all. */
|
|
20
20
|
chat(id: string): Promise<'idle' | 'running' | 'none'>;
|
|
21
|
-
/*
|
|
22
|
-
|
|
21
|
+
/* The senders whose message opened the turn this node is running, empty for a turn no message opened. */
|
|
22
|
+
wokenBy(id: string): readonly string[];
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/*
|
|
@@ -33,12 +33,13 @@ export interface ChatDelivery {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/*
|
|
36
|
-
*
|
|
37
|
-
* read each other from waking each other forever. It rides on the turn,
|
|
38
|
-
* Between turns the last one counts, since work it left running (a
|
|
39
|
-
* write, and a turn the CLI opened by itself takes the step of the
|
|
36
|
+
* Who sent the message that opened the turn a chat is running, empty when no message opened it,
|
|
37
|
+
* which stops two agents that read each other from waking each other forever. It rides on the turn,
|
|
38
|
+
* so it survives a restart. Between turns the last one counts, since work it left running (a
|
|
39
|
+
* background subagent) may still write, and a turn the CLI opened by itself takes the step of the
|
|
40
|
+
* turn before it.
|
|
40
41
|
*/
|
|
41
|
-
export function
|
|
42
|
+
export function turnWokenBy(items: readonly ChatItem[], activeTurnId: string | null): readonly string[] {
|
|
42
43
|
const from = activeTurnId === null ? items.findLastIndex((item) => item.kind === 'turn') : items.findIndex((item) => item.id === activeTurnId);
|
|
43
44
|
|
|
44
45
|
for (let i = from; i >= 0; i--) {
|
|
@@ -48,16 +49,18 @@ export function turnFromMessage(items: readonly ChatItem[], activeTurnId: string
|
|
|
48
49
|
continue;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
const senders = turn.messageFrom ?? [];
|
|
53
|
+
|
|
54
|
+
if (senders.length > 0) {
|
|
55
|
+
return senders;
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
if (turn.origin !== 'agent' || turn.taskIds !== undefined || turn.summaryFor !== undefined) {
|
|
56
|
-
return
|
|
59
|
+
return [];
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
return
|
|
63
|
+
return [];
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
/* The chats of a core as a message sees them. */
|
|
@@ -73,10 +76,10 @@ export function chatNoticeTargets(chats: Pick<ChatCore, 'get' | 'hasStored'>): C
|
|
|
73
76
|
// A chat nobody has loaded is idle: the turn opens on the thread the host reads back from disk.
|
|
74
77
|
return (await chats.hasStored(id)) ? 'idle' : 'none';
|
|
75
78
|
},
|
|
76
|
-
|
|
79
|
+
wokenBy: (id) => {
|
|
77
80
|
const session = chats.get(id);
|
|
78
81
|
|
|
79
|
-
return session
|
|
82
|
+
return session === undefined ? [] : turnWokenBy(session.thread.list(), session.info.activeTurnId);
|
|
80
83
|
}
|
|
81
84
|
};
|
|
82
85
|
}
|
|
@@ -84,8 +87,10 @@ export function chatNoticeTargets(chats: Pick<ChatCore, 'get' | 'hasStored'>): C
|
|
|
84
87
|
/*
|
|
85
88
|
* One message on its way to a chat, and whether the chat owes a turn on it. A chat between turns gets
|
|
86
89
|
* one, as for a settled task, or the message sits there until someone happens to prompt that chat.
|
|
87
|
-
*
|
|
88
|
-
*
|
|
90
|
+
* A turn a message opened wakes nobody with a message of its own, except the sender it answers,
|
|
91
|
+
* once: only while that sender's own turn was opened by no message, so a question and its answer
|
|
92
|
+
* take two steps and the turn the answer opens wakes nobody. A node that runs something else, such
|
|
93
|
+
* as a terminal, is the host's to deliver to before this.
|
|
89
94
|
*/
|
|
90
95
|
export async function deliverToChat(store: Pick<NoticeStore, 'put'>, targets: ChatNoticeTargets, notice: Omit<Notice, 'createdAt'>): Promise<ChatDelivery> {
|
|
91
96
|
const waiting = await store.put(notice);
|
|
@@ -99,7 +104,10 @@ export async function deliverToChat(store: Pick<NoticeStore, 'put'>, targets: Ch
|
|
|
99
104
|
return { outcome: 'in-turn', waiting };
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
|
|
107
|
+
const wokenBy = targets.wokenBy(notice.from);
|
|
108
|
+
const answer = wokenBy.includes(notice.targetId) && targets.wokenBy(notice.targetId).length === 0;
|
|
109
|
+
|
|
110
|
+
if (wokenBy.length > 0 && !answer) {
|
|
103
111
|
return { outcome: 'from-message', waiting };
|
|
104
112
|
}
|
|
105
113
|
|
|
@@ -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';
|