@adecore/agents 0.46.3 → 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 +4 -2
- package/dist/chat/chat-core.js +11 -6
- package/dist/chat/chat-session.d.ts +2 -2
- package/dist/chat/chat-session.js +12 -2
- 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 +3 -2
- package/dist/messages/deliver-notice.js +5 -1
- package/dist/messages/notice-notes.d.ts +6 -2
- package/dist/messages/notice-notes.js +2 -1
- 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 +18 -6
- package/src/chat/chat-session.ts +13 -2
- package/src/chat/composer-preferences.ts +3 -5
- package/src/host/handlers.ts +1 -0
- package/src/messages/deliver-notice.ts +8 -3
- package/src/messages/notice-notes.ts +8 -3
- 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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentKind, type ChatAttachment, type ChatAttachResult, type ChatAttachmentUpload, type ChatBookmark, type ChatCheckpointDiff, type ChatConfigurePayload, type ChatCreatePayload, type ChatEvent, type ChatHistoryResult, type ChatGoalAction, type ChatInfo, type ChatItem, type ChatQueuedMessage, type ChatUiChoicePayload, type ChatUiQueryPayload, type ChatUiLinkPayload, type ChatSkill, type ChatSubagentPayload, type ChatSubagentResult, type ChatTurnItem, type ChatVisual, type ModelSelection, type RuntimeMode } from '@adecore/agent-contracts';
|
|
1
|
+
import { type AgentKind, type ChatAttachment, type ChatAttachResult, type ChatAttachmentUpload, type ChatBookmark, type ChatCheckpointDiff, type ChatConfigurePayload, type ChatCreatePayload, type ChatEvent, type ChatHistoryResult, type ChatGoalAction, type ChatInfo, type ChatItem, type ChatNoteMessage, type ChatQueuedMessage, type ChatUiChoicePayload, type ChatUiQueryPayload, type ChatUiLinkPayload, type ChatSkill, type ChatSubagentPayload, type ChatSubagentResult, type ChatTurnItem, type ChatVisual, type ModelSelection, type RuntimeMode } from '@adecore/agent-contracts';
|
|
2
2
|
import type { AgentEvent, AgentSink } from '../events.ts';
|
|
3
3
|
import type { OutboxOutcome } from '../outbox/outbox-worker.ts';
|
|
4
4
|
import { type AccountLaunches } from '../providers/accounts/launch.ts';
|
|
@@ -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<{
|
|
@@ -180,7 +182,7 @@ export declare class ChatCore {
|
|
|
180
182
|
writeRecord(chatId: string, info: ChatInfo, items: ChatItem[], preambles?: string[]): Promise<void>;
|
|
181
183
|
deleteRecord(chatId: string): Promise<boolean>;
|
|
182
184
|
lastTurn(chatId: string): Promise<ChatTurnItem | null>;
|
|
183
|
-
addNote(chatId: string, level: 'info' | 'warning' | 'error', text: string): Promise<void>;
|
|
185
|
+
addNote(chatId: string, level: 'info' | 'warning' | 'error', text: string, message?: ChatNoteMessage): Promise<void>;
|
|
184
186
|
deliverNote(chatId: string, delivery: {
|
|
185
187
|
noteId: string;
|
|
186
188
|
note: string;
|
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);
|
|
@@ -643,7 +648,7 @@ export class ChatCore {
|
|
|
643
648
|
return stored?.items.findLast((item) => item.kind === 'turn') ?? null;
|
|
644
649
|
}
|
|
645
650
|
/* A line from the host in a chat's thread, loading the chat when nobody has; nothing for a chat that is gone. */
|
|
646
|
-
async addNote(chatId, level, text) {
|
|
651
|
+
async addNote(chatId, level, text, message) {
|
|
647
652
|
await this.loaded(chatId);
|
|
648
653
|
if (!(await this.hasStored(chatId))) {
|
|
649
654
|
return;
|
|
@@ -651,7 +656,7 @@ export class ChatCore {
|
|
|
651
656
|
if (!this.chats.has(chatId)) {
|
|
652
657
|
await this.create({ chatId });
|
|
653
658
|
}
|
|
654
|
-
this.chats.get(chatId)?.addNote(level, text);
|
|
659
|
+
this.chats.get(chatId)?.addNote(level, text, message);
|
|
655
660
|
}
|
|
656
661
|
/*
|
|
657
662
|
* Leaves a note and its preamble in a chat, loading that chat when nobody has; false when it was
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatAttachment, ChatCheckpointDiff, ChatEvent, ChatGoalAction, ChatInfo, ChatItem, ChatQueuedMessage, ChatQuoteSource, ChatSkill, ChatUiChoiceOrigin, ChatUiChoicePayload, ChatUiQueryState, ModelSelection, RuntimeMode } from '@adecore/agent-contracts';
|
|
1
|
+
import type { ChatAttachment, ChatCheckpointDiff, ChatEvent, ChatGoalAction, ChatInfo, ChatItem, ChatNoteMessage, ChatQueuedMessage, ChatQuoteSource, ChatSkill, ChatUiChoiceOrigin, ChatUiChoicePayload, ChatUiQueryState, ModelSelection, RuntimeMode } from '@adecore/agent-contracts';
|
|
2
2
|
import type { ChatProvider } from '../providers/provider.ts';
|
|
3
3
|
import type { LimitsUpdate } from '../usage/limits/normalize.ts';
|
|
4
4
|
import type { BackendLaunch } from './backend.ts';
|
|
@@ -211,7 +211,7 @@ export declare class ChatSession {
|
|
|
211
211
|
preamble: string;
|
|
212
212
|
}): boolean;
|
|
213
213
|
upsertItems(items: readonly ChatItem[]): void;
|
|
214
|
-
addNote(level: 'info' | 'warning' | 'error', text: string): void;
|
|
214
|
+
addNote(level: 'info' | 'warning' | 'error', text: string, message?: ChatNoteMessage): void;
|
|
215
215
|
end(reason: string): void;
|
|
216
216
|
restart(): Promise<void>;
|
|
217
217
|
dropProcess(): Promise<void>;
|
|
@@ -1057,8 +1057,18 @@ export class ChatSession {
|
|
|
1057
1057
|
this.options.persistSoon();
|
|
1058
1058
|
}
|
|
1059
1059
|
/* A line in the thread outside any turn, for something the host has to say about work it gave up on. */
|
|
1060
|
-
addNote(level, text) {
|
|
1061
|
-
this.emit([
|
|
1060
|
+
addNote(level, text, message) {
|
|
1061
|
+
this.emit([
|
|
1062
|
+
this.thread.upsert({
|
|
1063
|
+
id: newId('note'),
|
|
1064
|
+
kind: 'note',
|
|
1065
|
+
createdAt: Date.now(),
|
|
1066
|
+
turnId: null,
|
|
1067
|
+
level,
|
|
1068
|
+
text,
|
|
1069
|
+
...(message === undefined ? {} : { message })
|
|
1070
|
+
})
|
|
1071
|
+
]);
|
|
1062
1072
|
this.options.persist();
|
|
1063
1073
|
}
|
|
1064
1074
|
/*
|
|
@@ -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) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatItem } from '@adecore/agent-contracts';
|
|
1
|
+
import type { ChatItem, ChatNoteMessage } from '@adecore/agent-contracts';
|
|
2
2
|
import type { ChatCore } from '../chat/chat-core.ts';
|
|
3
3
|
import type { Notice, NoticeStore } from './notice-store.ts';
|
|
4
4
|
export interface MessageWords {
|
|
@@ -20,6 +20,7 @@ export declare function chatNoticeTargets(chats: Pick<ChatCore, 'get' | 'hasStor
|
|
|
20
20
|
export declare function deliverToChat(store: Pick<NoticeStore, 'put'>, targets: ChatNoticeTargets, notice: Omit<Notice, 'createdAt'>): Promise<ChatDelivery>;
|
|
21
21
|
export interface NoticeChat {
|
|
22
22
|
has(id: string): Promise<boolean>;
|
|
23
|
-
note(id: string, text: string): Promise<void>;
|
|
23
|
+
note(id: string, text: string, message: ChatNoteMessage): Promise<void>;
|
|
24
24
|
}
|
|
25
|
+
export declare function noticeMessage(notice: Notice): ChatNoteMessage;
|
|
25
26
|
export declare function showNotices(store: Pick<NoticeStore, 'show'>, chat: NoticeChat, words: Pick<MessageWords, 'shown'>, targetId: string): Promise<void>;
|
|
@@ -57,6 +57,10 @@ export async function deliverToChat(store, targets, notice) {
|
|
|
57
57
|
}
|
|
58
58
|
return { outcome: 'wake', waiting };
|
|
59
59
|
}
|
|
60
|
+
/* The message itself on the line in the thread, so a client can draw it as said to the chat. */
|
|
61
|
+
export function noticeMessage(notice) {
|
|
62
|
+
return { from: notice.from, title: notice.fromTitle, text: notice.text };
|
|
63
|
+
}
|
|
60
64
|
/*
|
|
61
65
|
* Shows a person the message in the chat's thread the moment it lands, not when the model gets round
|
|
62
66
|
* to it. An id no chat holds shows and marks nothing, so a chat that opens on it later still has all of it.
|
|
@@ -66,6 +70,6 @@ export async function showNotices(store, chat, words, targetId) {
|
|
|
66
70
|
return;
|
|
67
71
|
}
|
|
68
72
|
for (const notice of await store.show(targetId)) {
|
|
69
|
-
await chat.note(targetId, words.shown(notice));
|
|
73
|
+
await chat.note(targetId, words.shown(notice), noticeMessage(notice));
|
|
70
74
|
}
|
|
71
75
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { ChatNoteMessage } from '@adecore/agent-contracts';
|
|
1
2
|
import type { PromptNotes } from '../chat/chat-session.ts';
|
|
2
|
-
import type
|
|
3
|
+
import { type MessageWords } from './deliver-notice.ts';
|
|
3
4
|
import type { NoticeStore } from './notice-store.ts';
|
|
4
5
|
export declare class NoticeNotes implements PromptNotes {
|
|
5
6
|
private readonly store;
|
|
@@ -12,4 +13,7 @@ export declare class NoticeNotes implements PromptNotes {
|
|
|
12
13
|
};
|
|
13
14
|
reset(): void;
|
|
14
15
|
}
|
|
15
|
-
export declare function unshownNotes(store: Pick<NoticeStore, 'show'>, chatId: string, words: Pick<MessageWords, 'shown'>): Promise<
|
|
16
|
+
export declare function unshownNotes(store: Pick<NoticeStore, 'show'>, chatId: string, words: Pick<MessageWords, 'shown'>): Promise<{
|
|
17
|
+
text: string;
|
|
18
|
+
message: ChatNoteMessage;
|
|
19
|
+
}[]>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { noticeMessage } from './deliver-notice.js';
|
|
1
2
|
/*
|
|
2
3
|
* What another node left for a chat, taken from the queue in front of its next prompt so the model
|
|
3
4
|
* hears it once. Nothing of it is shown: a person read each message in the thread as it landed
|
|
@@ -22,5 +23,5 @@ export class NoticeNotes {
|
|
|
22
23
|
* reload writes none again. The model still hears the messages themselves from the queue.
|
|
23
24
|
*/
|
|
24
25
|
export async function unshownNotes(store, chatId, words) {
|
|
25
|
-
return (await store.show(chatId)).map((notice) => words.shown(notice));
|
|
26
|
+
return (await store.show(chatId)).map((notice) => ({ text: words.shown(notice), message: noticeMessage(notice) }));
|
|
26
27
|
}
|
|
@@ -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
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type ChatGoalAction,
|
|
18
18
|
type ChatInfo,
|
|
19
19
|
type ChatItem,
|
|
20
|
+
type ChatNoteMessage,
|
|
20
21
|
type ChatQueuedMessage,
|
|
21
22
|
type ChatUiChoicePayload,
|
|
22
23
|
type ChatUiQueryPayload,
|
|
@@ -116,6 +117,11 @@ export interface ChatCoreHooks {
|
|
|
116
117
|
admit?(chatId: string, cwd: string): Promise<void>;
|
|
117
118
|
/* The model a new chat starts on when the host already knows; the explicit one still wins. */
|
|
118
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;
|
|
119
125
|
/* The mode a chat runs in, which a host may narrow whatever its record or its client says. */
|
|
120
126
|
runtimeModeFor?(chatId: string, mode: RuntimeMode): RuntimeMode;
|
|
121
127
|
/* What the agent is told once at the start of every process of this chat, in place of `instructions`; null for nothing. */
|
|
@@ -386,6 +392,11 @@ export class ChatCore {
|
|
|
386
392
|
}
|
|
387
393
|
|
|
388
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
|
+
|
|
389
400
|
requireAccount(kind: AgentKind, account: string | undefined): void {
|
|
390
401
|
launchEnv(this.accounts, kind, account, this.env);
|
|
391
402
|
}
|
|
@@ -478,9 +489,9 @@ export class ChatCore {
|
|
|
478
489
|
const cwd = stored?.info.cwd ?? payload.cwd ?? homeFolder(this.env);
|
|
479
490
|
await this.hooks.admit?.(payload.chatId, cwd);
|
|
480
491
|
// A thread on disk keeps its account too. One it lost still opens, and says why on its next turn.
|
|
481
|
-
// A new one without an account takes the
|
|
482
|
-
//
|
|
483
|
-
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);
|
|
484
495
|
|
|
485
496
|
if (!stored) {
|
|
486
497
|
this.requireAccount(kind, requested);
|
|
@@ -625,7 +636,8 @@ export class ChatCore {
|
|
|
625
636
|
return;
|
|
626
637
|
}
|
|
627
638
|
|
|
628
|
-
// 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);
|
|
629
641
|
this.requireAccount(kind, requested);
|
|
630
642
|
if (holdsConversation(session.info) && !this.canContinue(kind, from, to)) {
|
|
631
643
|
const label = this.accountLabel(kind, to);
|
|
@@ -974,7 +986,7 @@ export class ChatCore {
|
|
|
974
986
|
}
|
|
975
987
|
|
|
976
988
|
/* A line from the host in a chat's thread, loading the chat when nobody has; nothing for a chat that is gone. */
|
|
977
|
-
async addNote(chatId: string, level: 'info' | 'warning' | 'error', text: string): Promise<void> {
|
|
989
|
+
async addNote(chatId: string, level: 'info' | 'warning' | 'error', text: string, message?: ChatNoteMessage): Promise<void> {
|
|
978
990
|
await this.loaded(chatId);
|
|
979
991
|
|
|
980
992
|
if (!(await this.hasStored(chatId))) {
|
|
@@ -985,7 +997,7 @@ export class ChatCore {
|
|
|
985
997
|
await this.create({ chatId });
|
|
986
998
|
}
|
|
987
999
|
|
|
988
|
-
this.chats.get(chatId)?.addNote(level, text);
|
|
1000
|
+
this.chats.get(chatId)?.addNote(level, text, message);
|
|
989
1001
|
}
|
|
990
1002
|
|
|
991
1003
|
/*
|
package/src/chat/chat-session.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ChatGoalAction,
|
|
7
7
|
ChatInfo,
|
|
8
8
|
ChatItem,
|
|
9
|
+
ChatNoteMessage,
|
|
9
10
|
ChatQueuedMessage,
|
|
10
11
|
ChatQuoteSource,
|
|
11
12
|
ChatSkill,
|
|
@@ -1470,8 +1471,18 @@ export class ChatSession {
|
|
|
1470
1471
|
}
|
|
1471
1472
|
|
|
1472
1473
|
/* A line in the thread outside any turn, for something the host has to say about work it gave up on. */
|
|
1473
|
-
addNote(level: 'info' | 'warning' | 'error', text: string): void {
|
|
1474
|
-
this.emit([
|
|
1474
|
+
addNote(level: 'info' | 'warning' | 'error', text: string, message?: ChatNoteMessage): void {
|
|
1475
|
+
this.emit([
|
|
1476
|
+
this.thread.upsert({
|
|
1477
|
+
id: newId('note'),
|
|
1478
|
+
kind: 'note',
|
|
1479
|
+
createdAt: Date.now(),
|
|
1480
|
+
turnId: null,
|
|
1481
|
+
level,
|
|
1482
|
+
text,
|
|
1483
|
+
...(message === undefined ? {} : { message })
|
|
1484
|
+
})
|
|
1485
|
+
]);
|
|
1475
1486
|
this.options.persist();
|
|
1476
1487
|
}
|
|
1477
1488
|
|
|
@@ -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) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatItem } from '@adecore/agent-contracts';
|
|
1
|
+
import type { ChatItem, ChatNoteMessage } from '@adecore/agent-contracts';
|
|
2
2
|
import type { ChatCore } from '../chat/chat-core.ts';
|
|
3
3
|
import type { Notice, NoticeStore } from './notice-store.ts';
|
|
4
4
|
|
|
@@ -111,7 +111,12 @@ export interface NoticeChat {
|
|
|
111
111
|
/* Whether the host holds a chat under the id, running or on disk. */
|
|
112
112
|
has(id: string): Promise<boolean>;
|
|
113
113
|
/* Puts a line in that chat's thread, loading the chat when nobody has. */
|
|
114
|
-
note(id: string, text: string): Promise<void>;
|
|
114
|
+
note(id: string, text: string, message: ChatNoteMessage): Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* The message itself on the line in the thread, so a client can draw it as said to the chat. */
|
|
118
|
+
export function noticeMessage(notice: Notice): ChatNoteMessage {
|
|
119
|
+
return { from: notice.from, title: notice.fromTitle, text: notice.text };
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
/*
|
|
@@ -124,6 +129,6 @@ export async function showNotices(store: Pick<NoticeStore, 'show'>, chat: Notice
|
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
for (const notice of await store.show(targetId)) {
|
|
127
|
-
await chat.note(targetId, words.shown(notice));
|
|
132
|
+
await chat.note(targetId, words.shown(notice), noticeMessage(notice));
|
|
128
133
|
}
|
|
129
134
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { ChatNoteMessage } from '@adecore/agent-contracts';
|
|
1
2
|
import type { PromptNotes } from '../chat/chat-session.ts';
|
|
2
|
-
import type
|
|
3
|
+
import { noticeMessage, type MessageWords } from './deliver-notice.ts';
|
|
3
4
|
import type { NoticeStore } from './notice-store.ts';
|
|
4
5
|
|
|
5
6
|
/*
|
|
@@ -29,6 +30,10 @@ export class NoticeNotes implements PromptNotes {
|
|
|
29
30
|
* The lines a chat that just loaded owes a person for what landed while nobody held it, marked so a
|
|
30
31
|
* reload writes none again. The model still hears the messages themselves from the queue.
|
|
31
32
|
*/
|
|
32
|
-
export async function unshownNotes(
|
|
33
|
-
|
|
33
|
+
export async function unshownNotes(
|
|
34
|
+
store: Pick<NoticeStore, 'show'>,
|
|
35
|
+
chatId: string,
|
|
36
|
+
words: Pick<MessageWords, 'shown'>
|
|
37
|
+
): Promise<{ text: string; message: ChatNoteMessage }[]> {
|
|
38
|
+
return (await store.show(chatId)).map((notice) => ({ text: words.shown(notice), message: noticeMessage(notice) }));
|
|
34
39
|
}
|
|
@@ -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';
|