@fastagent-sh/fastagent 0.23.0 → 0.24.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/agent.d.ts +10 -0
- package/dist/channels/kit/preview-kit.d.ts +6 -1
- package/dist/channels/kit/preview-kit.js +21 -4
- package/dist/channels/slack/preview.js +2 -1
- package/dist/cli/commands/info.js +7 -7
- package/dist/cli/commands/login.js +2 -2
- package/dist/cli/commands/models.js +3 -3
- package/dist/cli/invoke-stream.js +14 -0
- package/dist/cli/shared.d.ts +2 -1
- package/dist/cli/shared.js +6 -7
- package/dist/deploy/preflight.js +19 -1
- package/dist/dev-supervisor.js +2 -2
- package/dist/engines/pi/agent-models.d.ts +14 -1
- package/dist/engines/pi/agent-models.js +35 -8
- package/dist/engines/pi/agent-session-factory.d.ts +13 -13
- package/dist/engines/pi/agent-session-factory.js +137 -159
- package/dist/engines/pi/auth.js +6 -94
- package/dist/engines/pi/create.d.ts +16 -9
- package/dist/engines/pi/create.js +71 -38
- package/dist/engines/pi/invoke-session.js +82 -16
- package/dist/engines/pi/locked-file.d.ts +9 -0
- package/dist/engines/pi/locked-file.js +102 -0
- package/dist/engines/pi/login.d.ts +7 -1
- package/dist/engines/pi/login.js +22 -6
- package/dist/engines/pi/machine.d.ts +23 -0
- package/dist/engines/pi/machine.js +24 -2
- package/dist/engines/pi/models.d.ts +47 -42
- package/dist/engines/pi/models.js +154 -67
- package/dist/engines/pi/open.d.ts +29 -12
- package/dist/engines/pi/open.js +52 -21
- package/dist/engines/pi/openai-account-models.d.ts +35 -0
- package/dist/engines/pi/openai-account-models.js +116 -0
- package/dist/engines/pi/report.d.ts +3 -0
- package/dist/engines/pi/report.js +11 -0
- package/dist/engines/pi/session-builder.js +6 -22
- package/dist/engines/pi/session-control.d.ts +4 -4
- package/dist/engines/pi/session-control.js +72 -40
- package/dist/engines/pi/session-settings.d.ts +9 -0
- package/dist/engines/pi/session-settings.js +17 -0
- package/dist/engines/pi/session-store.d.ts +2 -2
- package/dist/engines/pi/tool-context.d.ts +3 -5
- package/dist/engines/pi/tool-context.js +4 -16
- package/dist/engines/pi/tool.d.ts +32 -21
- package/dist/engines/pi/tool.js +21 -25
- package/dist/engines/pi/turn-kit.d.ts +18 -9
- package/dist/engines/pi/turn-kit.js +104 -27
- package/dist/pi.d.ts +1 -1
- package/dist/pi.js +1 -1
- package/dist/session.d.ts +60 -8
- package/dist/session.js +5 -1
- package/package.json +5 -5
- package/dist/engines/pi/search-tools.d.ts +0 -4
- package/dist/engines/pi/search-tools.js +0 -109
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Agent } from "../../agent.ts";
|
|
2
2
|
import { type FastagentConfig } from "./config.ts";
|
|
3
|
-
import type { AgentCommand, SessionControl } from "../../session.ts";
|
|
4
|
-
import { type PiAssembly } from "./create.ts";
|
|
3
|
+
import type { AgentCommand, ModelDescriptor, SessionControl } from "../../session.ts";
|
|
4
|
+
import { type IndirectTool, type PiAssembly } from "./create.ts";
|
|
5
5
|
import type { SessionObserver } from "./turn-kit.ts";
|
|
6
6
|
import { type LoadedDefinition } from "./definition.ts";
|
|
7
7
|
import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
@@ -72,10 +72,10 @@ export interface AgentAssembly {
|
|
|
72
72
|
stateRoot: string;
|
|
73
73
|
/** The credential store and model registry every turn and every report reads ({@link agentModels}). */
|
|
74
74
|
models: AgentModels;
|
|
75
|
-
/** The full mounted tool surface (all coding tools + config.tools + discovered tools
|
|
75
|
+
/** The full mounted tool surface (all coding tools + config.tools + discovered tools/). */
|
|
76
76
|
tools: MountedTool[];
|
|
77
77
|
toolNames: string[];
|
|
78
|
-
|
|
78
|
+
indirectTools: IndirectTool[];
|
|
79
79
|
toolCollisions: ToolCollision[];
|
|
80
80
|
/** Env vars the mounted tools declared, by tool name — already asserted present by this function. */
|
|
81
81
|
toolSecrets: Map<string, DeclaredSecret[]>;
|
|
@@ -96,15 +96,19 @@ export declare function assembleFront(front: AgentAssembly, extra?: {
|
|
|
96
96
|
definition: LoadedDefinition;
|
|
97
97
|
}>;
|
|
98
98
|
/**
|
|
99
|
-
* The
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
99
|
+
* The models `createPiAgentFromDir(dir, { authPath })` could run now, as a picker shows them (spec, name, the thinking
|
|
100
|
+
* levels a session on it accepts, context window), sorted by spec: the agent's registry (pi's built-ins, its
|
|
101
|
+
* `models.json`, and what its `extensions/` declare) filtered to the providers whose credentials are configured. The
|
|
102
|
+
* directory needs no model set, and the same placement and credential layers as the opener apply, so a listed spec
|
|
103
|
+
* authenticates there. Configuration is checked, not validity: no OAuth token is refreshed and no provider is called.
|
|
104
|
+
* A Sign in with ChatGPT on `openai` lists the account's own models (openai-account-models.ts), not pi's built-ins.
|
|
103
105
|
*
|
|
104
106
|
* `warn` reaches the credential store. An unreadable or corrupt credentials file otherwise reads as "nothing
|
|
105
|
-
* configured", so a client that must not show that as an empty list passes a sink that throws.
|
|
107
|
+
* configured", so a client that must not show that as an empty list passes a sink that throws. A ChatGPT sign-in that
|
|
108
|
+
* carries no catalog is a normal, recoverable state (signed in before the catalog was read), not that: it is logged,
|
|
109
|
+
* never sent to `warn`, so a throwing sink does not fail the whole listing for it.
|
|
106
110
|
*/
|
|
107
|
-
export declare function availableModelsFromDir(dir: string, options?: FastagentAuthOptions & CredentialSourceOptions): Promise<
|
|
111
|
+
export declare function availableModelsFromDir(dir: string, options?: FastagentAuthOptions & CredentialSourceOptions): Promise<ModelDescriptor[]>;
|
|
108
112
|
/**
|
|
109
113
|
* Refresh the agent's model catalog (`<agent dir>/models-store.json`) with the credentials
|
|
110
114
|
* `createPiAgentFromDir(dir, { authPath })` would use, so a model released after the installed pi appears in
|
|
@@ -122,6 +126,19 @@ export declare function refreshModelCatalog(dir: string, options?: FastagentAuth
|
|
|
122
126
|
export declare function refreshModelCatalogOver(dir: string, options: FastagentAuthOptions & CredentialSourceOptions & {
|
|
123
127
|
signal?: AbortSignal;
|
|
124
128
|
}, catalogBaseUrl?: string): Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* Refresh the MACHINE's model catalog (`~/.fastagent/models-store.json`), which every agent here reads under its own,
|
|
131
|
+
* so one refresh serves them all and no file is written into any agent. Credentials resolve as for
|
|
132
|
+
* {@link refreshModelCatalog} without a directory: `credentialStore`, else `authPath`, else the global credentials file;
|
|
133
|
+
* the environment applies either way. Rejects for the same reasons. The file never ships with a deploy.
|
|
134
|
+
*/
|
|
135
|
+
export declare function refreshMachineModelCatalog(options?: FastagentAuthOptions & CredentialSourceOptions & {
|
|
136
|
+
signal?: AbortSignal;
|
|
137
|
+
}): Promise<void>;
|
|
138
|
+
/** {@link refreshMachineModelCatalog} against another catalog server. Not public: a test seam. */
|
|
139
|
+
export declare function refreshMachineModelCatalogOver(options: FastagentAuthOptions & CredentialSourceOptions & {
|
|
140
|
+
signal?: AbortSignal;
|
|
141
|
+
}, catalogBaseUrl?: string): Promise<void>;
|
|
125
142
|
/**
|
|
126
143
|
* "Point at a directory → agent": resolve the placement (`dir` may be either end — the workspace or the agent dir
|
|
127
144
|
* itself), load the config, resolve model and tools, then L2.
|
|
@@ -153,7 +170,7 @@ export declare function createPiAgentFromDir(dir: string, options?: CreatePiAgen
|
|
|
153
170
|
http?: HttpSurface;
|
|
154
171
|
/** Non-default, active-by-default tool names in effect: config.tools + discovered tools/. */
|
|
155
172
|
toolNames: string[];
|
|
156
|
-
/**
|
|
157
|
-
|
|
173
|
+
/** Mounted authored tools the model is not given up front, with how each is reached. */
|
|
174
|
+
indirectTools: IndirectTool[];
|
|
158
175
|
toolCollisions: ToolCollision[];
|
|
159
176
|
}>;
|
package/dist/engines/pi/open.js
CHANGED
|
@@ -6,6 +6,7 @@ import { mkdir } from "node:fs/promises";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { loadConfig, resolveModelSpec } from "./config.js";
|
|
8
8
|
import { AGENT_MODEL_CATALOG_FILE, resolveSessionsDir, resolveStateRoot, resolvePlacement } from "../../paths.js";
|
|
9
|
+
import { describeModels } from "./session-settings.js";
|
|
9
10
|
import { agentOf, assemblePiFromDefinition, resolveAgentTools } from "./create.js";
|
|
10
11
|
import { createPiSessionControl } from "./session-control.js";
|
|
11
12
|
import { withWakeTool } from "./wake-tool.js";
|
|
@@ -14,8 +15,10 @@ import { loadAgentSkills } from "./definition.js";
|
|
|
14
15
|
import { servedExtensionCommands } from "./agent-session-factory.js";
|
|
15
16
|
import { reportFindingsIfChanged } from "./report.js";
|
|
16
17
|
import { readMachine, withMachine } from "./machine.js";
|
|
17
|
-
import { createPiModelRuntime, refreshCatalog } from "./models.js";
|
|
18
|
+
import { createPiModelRuntime, globalCatalogPath, machineModelRuntime, refreshCatalog } from "./models.js";
|
|
18
19
|
import { agentModels } from "./agent-models.js";
|
|
20
|
+
import { OPENAI_PROVIDER, missingAccountModels } from "./openai-account-models.js";
|
|
21
|
+
import { log } from "../../log.js";
|
|
19
22
|
import { piSessionRecordStore } from "./session-store.js";
|
|
20
23
|
import { gateSecrets } from "../../secrets-gate.js";
|
|
21
24
|
/**
|
|
@@ -36,9 +39,11 @@ export async function agentCommands(agentDir, workspace, served) {
|
|
|
36
39
|
reportFindingsIfChanged(own.dir, own);
|
|
37
40
|
const machine = await readMachine(workspace);
|
|
38
41
|
const { extensionPaths } = served;
|
|
39
|
-
const extensionCommands =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
const extensionCommands = await servedExtensionCommands({
|
|
43
|
+
cwd: workspace,
|
|
44
|
+
modelRuntime: await served.modelRuntime(),
|
|
45
|
+
extensionPaths,
|
|
46
|
+
});
|
|
42
47
|
// pi dispatches an extension command before it expands a template, so a template it shadows never runs.
|
|
43
48
|
const shadowed = new Set(extensionCommands.map((command) => command.invocationName));
|
|
44
49
|
return [
|
|
@@ -69,7 +74,7 @@ export async function resolveAgentAssembly(dir, options = {}) {
|
|
|
69
74
|
if (!modelSpec) {
|
|
70
75
|
throw new Error(`missing model: set --model, "model" in fastagent.config.ts, or FASTAGENT_MODEL (e.g. "openai-codex/gpt-5.5")`);
|
|
71
76
|
}
|
|
72
|
-
const { tools, toolNames,
|
|
77
|
+
const { tools, toolNames, indirectTools, toolCollisions, toolFailures, toolSecrets } = await resolveAgentTools(config, agentDir, workspace);
|
|
73
78
|
// THE serving-path gate for tool declarations, in the order that costs the fewest round trips. A file that could
|
|
74
79
|
// not be imported declares nothing, so its `secrets:` are missing from `toolSecrets` — gating secrets first would
|
|
75
80
|
// report an incomplete set, and fixing the file could then reveal more missing values. Refuse the broken file
|
|
@@ -79,7 +84,7 @@ export async function resolveAgentAssembly(dir, options = {}) {
|
|
|
79
84
|
// definition and must survive both faults; every path through this function is about to run ALL of the tools, so
|
|
80
85
|
// no `owner`.
|
|
81
86
|
//
|
|
82
|
-
// DEFERRED tools gate too, deliberately:
|
|
87
|
+
// DEFERRED tools gate too, deliberately: native discovery can activate one mid-turn, so "registered"
|
|
83
88
|
// means "may run in this process" — letting it start would put the empty-credential failure back
|
|
84
89
|
// inside a turn. An author who does not want that opts out per tool by not declaring.
|
|
85
90
|
gateSecrets({ declared: toolSecrets, failures: [] });
|
|
@@ -94,10 +99,10 @@ export async function resolveAgentAssembly(dir, options = {}) {
|
|
|
94
99
|
workspace,
|
|
95
100
|
stateRoot,
|
|
96
101
|
// Project-level by default (under `<agentDir>/.secrets`), with the global file behind it per provider.
|
|
97
|
-
models: agentModels(agentDir, options),
|
|
102
|
+
models: agentModels(agentDir, options, { cwd: workspace }),
|
|
98
103
|
tools,
|
|
99
104
|
toolNames,
|
|
100
|
-
|
|
105
|
+
indirectTools,
|
|
101
106
|
toolCollisions,
|
|
102
107
|
toolSecrets,
|
|
103
108
|
};
|
|
@@ -118,18 +123,26 @@ export function assembleFront(front, extra = {}) {
|
|
|
118
123
|
});
|
|
119
124
|
}
|
|
120
125
|
/**
|
|
121
|
-
* The
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
126
|
+
* The models `createPiAgentFromDir(dir, { authPath })` could run now, as a picker shows them (spec, name, the thinking
|
|
127
|
+
* levels a session on it accepts, context window), sorted by spec: the agent's registry (pi's built-ins, its
|
|
128
|
+
* `models.json`, and what its `extensions/` declare) filtered to the providers whose credentials are configured. The
|
|
129
|
+
* directory needs no model set, and the same placement and credential layers as the opener apply, so a listed spec
|
|
130
|
+
* authenticates there. Configuration is checked, not validity: no OAuth token is refreshed and no provider is called.
|
|
131
|
+
* A Sign in with ChatGPT on `openai` lists the account's own models (openai-account-models.ts), not pi's built-ins.
|
|
125
132
|
*
|
|
126
133
|
* `warn` reaches the credential store. An unreadable or corrupt credentials file otherwise reads as "nothing
|
|
127
|
-
* configured", so a client that must not show that as an empty list passes a sink that throws.
|
|
134
|
+
* configured", so a client that must not show that as an empty list passes a sink that throws. A ChatGPT sign-in that
|
|
135
|
+
* carries no catalog is a normal, recoverable state (signed in before the catalog was read), not that: it is logged,
|
|
136
|
+
* never sent to `warn`, so a throwing sink does not fail the whole listing for it.
|
|
128
137
|
*/
|
|
129
138
|
export async function availableModelsFromDir(dir, options = {}) {
|
|
130
|
-
const { agentDir } = resolvePlacement(dir);
|
|
131
|
-
const
|
|
132
|
-
|
|
139
|
+
const { agentDir, workspace } = resolvePlacement(dir);
|
|
140
|
+
const environment = agentModels(agentDir, options, { cwd: workspace });
|
|
141
|
+
const available = await (await environment.runtime()).getAvailable();
|
|
142
|
+
const missing = missingAccountModels(await environment.credentials.read(OPENAI_PROVIDER));
|
|
143
|
+
if (missing)
|
|
144
|
+
log.warn(missing);
|
|
145
|
+
return describeModels(available);
|
|
133
146
|
}
|
|
134
147
|
/**
|
|
135
148
|
* Refresh the agent's model catalog (`<agent dir>/models-store.json`) with the credentials
|
|
@@ -148,10 +161,28 @@ export function refreshModelCatalog(dir, options = {}) {
|
|
|
148
161
|
export async function refreshModelCatalogOver(dir, options, catalogBaseUrl) {
|
|
149
162
|
const { agentDir } = resolvePlacement(dir);
|
|
150
163
|
const { credentials } = agentModels(agentDir, options);
|
|
151
|
-
await refreshCatalog((catalogFile) => createPiModelRuntime({
|
|
164
|
+
await refreshCatalog(join(agentDir, AGENT_MODEL_CATALOG_FILE), (catalogFile) => createPiModelRuntime({
|
|
152
165
|
agentDir,
|
|
153
166
|
credentials,
|
|
154
|
-
|
|
167
|
+
catalogFile,
|
|
168
|
+
...(catalogBaseUrl ? { catalogBaseUrl } : {}),
|
|
169
|
+
}), options.signal ? { signal: options.signal } : {});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Refresh the MACHINE's model catalog (`~/.fastagent/models-store.json`), which every agent here reads under its own,
|
|
173
|
+
* so one refresh serves them all and no file is written into any agent. Credentials resolve as for
|
|
174
|
+
* {@link refreshModelCatalog} without a directory: `credentialStore`, else `authPath`, else the global credentials file;
|
|
175
|
+
* the environment applies either way. Rejects for the same reasons. The file never ships with a deploy.
|
|
176
|
+
*/
|
|
177
|
+
export function refreshMachineModelCatalog(options = {}) {
|
|
178
|
+
return refreshMachineModelCatalogOver(options);
|
|
179
|
+
}
|
|
180
|
+
/** {@link refreshMachineModelCatalog} against another catalog server. Not public: a test seam. */
|
|
181
|
+
export async function refreshMachineModelCatalogOver(options, catalogBaseUrl) {
|
|
182
|
+
const { credentials } = agentModels(undefined, options);
|
|
183
|
+
await refreshCatalog(globalCatalogPath(), (catalogFile) => machineModelRuntime({
|
|
184
|
+
credentials,
|
|
185
|
+
catalogFile,
|
|
155
186
|
...(catalogBaseUrl ? { catalogBaseUrl } : {}),
|
|
156
187
|
}), options.signal ? { signal: options.signal } : {});
|
|
157
188
|
}
|
|
@@ -161,7 +192,7 @@ export async function refreshModelCatalogOver(dir, options, catalogBaseUrl) {
|
|
|
161
192
|
*/
|
|
162
193
|
export async function createPiAgentFromDir(dir, options = {}) {
|
|
163
194
|
const front = await resolveAgentAssembly(dir, options);
|
|
164
|
-
const { config, configPath, modelSpec, agentDir, workspace, stateRoot, models, tools, toolNames,
|
|
195
|
+
const { config, configPath, modelSpec, agentDir, workspace, stateRoot, models, tools, toolNames, indirectTools, toolCollisions, } = front;
|
|
165
196
|
// Every serve mounts the built-in `wake` tool: the agent's own follow-up work is a default capability, and a serve
|
|
166
197
|
// is where the poller that honors it runs. A one-shot `invoke` has no poller, so nothing there would fire it.
|
|
167
198
|
const mountedTools = withWakeTool(tools, stateRoot, !!options.serving);
|
|
@@ -201,7 +232,7 @@ export async function createPiAgentFromDir(dir, options = {}) {
|
|
|
201
232
|
// the prompt would not expand — a second reading of "which skills exist" is how those two come to disagree.
|
|
202
233
|
commands: () => agentCommands(agentDir, workspace, {
|
|
203
234
|
extensionPaths: assembly.extensionPaths,
|
|
204
|
-
modelRuntime:
|
|
235
|
+
modelRuntime: assembly.createModelRuntime,
|
|
205
236
|
}),
|
|
206
237
|
// The caller tap's boundary-event half: state_changed/compaction_* originate in the hub and never cross the
|
|
207
238
|
// data plane's observer seam.
|
|
@@ -233,7 +264,7 @@ export async function createPiAgentFromDir(dir, options = {}) {
|
|
|
233
264
|
sessionsDir,
|
|
234
265
|
models,
|
|
235
266
|
toolNames,
|
|
236
|
-
|
|
267
|
+
indirectTools,
|
|
237
268
|
toolCollisions,
|
|
238
269
|
};
|
|
239
270
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The models a Sign in with ChatGPT can run on the `openai` provider: the ACCOUNT's catalog, which OpenAI publishes at
|
|
3
|
+
* `GET /v1/models` for the same access token, not pi's built-in list (most of which such an account is refused).
|
|
4
|
+
*
|
|
5
|
+
* pi lists a provider's models through `filterModels`, which is synchronous, so the catalog is read when there is a
|
|
6
|
+
* token to read it with (login and every token refresh) and kept on the credential, like the account it belongs to:
|
|
7
|
+
* a new account brings its own list, and nothing per-account lands in the definition's model catalog. pi's `openai`
|
|
8
|
+
* provider does not do this itself (checked against pi 0.99.2), so {@link withAccountModels} wraps it. An API key on the same
|
|
9
|
+
* provider keeps the built-in list.
|
|
10
|
+
*/
|
|
11
|
+
import type { Credential, Provider } from "@earendil-works/pi-ai";
|
|
12
|
+
import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
export declare const OPENAI_PROVIDER = "openai";
|
|
14
|
+
/**
|
|
15
|
+
* Why the stored `openai` credential lists no models, when that is because its sign-in carries no catalog: the one
|
|
16
|
+
* wording a listing reports instead of falling back to every built-in model.
|
|
17
|
+
*/
|
|
18
|
+
export declare function missingAccountModels(credential: Credential | undefined): string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* `provider` with the account catalog: read at login and at each token refresh, and the only models a ChatGPT
|
|
21
|
+
* sign-in lists. Anything without OAuth is returned as is.
|
|
22
|
+
*/
|
|
23
|
+
export declare function withAccountModels(provider: Provider): Provider;
|
|
24
|
+
/**
|
|
25
|
+
* Put {@link withAccountModels} on a runtime's `openai`, over the provider the runtime composed (pi's remote catalog,
|
|
26
|
+
* the agent's models.json, and an extension's `registerProvider("openai", …)` included), so none of them is lost. pi
|
|
27
|
+
* composes a native provider with models.json once more; that second pass is idempotent today, and
|
|
28
|
+
* openai-account-models.test.ts holds it to that. Call it inside `withModelRegistration`, and again after extensions
|
|
29
|
+
* load: pi's `registerProvider` drops a native provider, and a refresh without the wrapper would store a credential
|
|
30
|
+
* without its catalog. Repeating it is harmless.
|
|
31
|
+
*
|
|
32
|
+
* Known ceiling: an extension that registers `openai` later than its load (from an event handler) drops the wrapper
|
|
33
|
+
* for that runtime.
|
|
34
|
+
*/
|
|
35
|
+
export declare function registerAccountModels(runtime: ModelRuntime): void;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { log } from "../../log.js";
|
|
2
|
+
export const OPENAI_PROVIDER = "openai";
|
|
3
|
+
const ACCOUNT_MODELS_URL = "https://api.openai.com/v1/models";
|
|
4
|
+
/** Where the catalog is kept on the stored credential: the model ids the account lists, in OpenAI's order. */
|
|
5
|
+
const ACCOUNT_MODELS_FIELD = "accountModels";
|
|
6
|
+
/** A token refresh is bounded at 15s by pi; the catalog read must leave it room to finish and be stored. */
|
|
7
|
+
const CATALOG_TIMEOUT_MS = 5_000;
|
|
8
|
+
/** The slugs the account lists for a picker (`visibility: "list"`), in the server's order. */
|
|
9
|
+
async function fetchAccountModels(access, signal) {
|
|
10
|
+
const timeout = AbortSignal.timeout(CATALOG_TIMEOUT_MS);
|
|
11
|
+
const response = await fetch(ACCOUNT_MODELS_URL, {
|
|
12
|
+
headers: { accept: "application/json", authorization: `Bearer ${access}` },
|
|
13
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
14
|
+
});
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
const body = await response.text().catch(() => "");
|
|
17
|
+
throw new Error(`GET ${ACCOUNT_MODELS_URL} answered ${response.status}: ${body || response.statusText}`);
|
|
18
|
+
}
|
|
19
|
+
const models = (await response.json())?.models;
|
|
20
|
+
if (!Array.isArray(models))
|
|
21
|
+
throw new Error(`GET ${ACCOUNT_MODELS_URL} answered without a "models" array`);
|
|
22
|
+
return models.flatMap((entry) => entry?.visibility === "list" && typeof entry.slug === "string" ? [entry.slug] : []);
|
|
23
|
+
}
|
|
24
|
+
/** The catalog a ChatGPT sign-in carries, or undefined when it carries none (signed in before it was read). */
|
|
25
|
+
function accountModels(credential) {
|
|
26
|
+
const ids = credential[ACCOUNT_MODELS_FIELD];
|
|
27
|
+
return Array.isArray(ids) && ids.every((id) => typeof id === "string") ? ids : undefined;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Why the stored `openai` credential lists no models, when that is because its sign-in carries no catalog: the one
|
|
31
|
+
* wording a listing reports instead of falling back to every built-in model.
|
|
32
|
+
*/
|
|
33
|
+
export function missingAccountModels(credential) {
|
|
34
|
+
if (credential?.type !== "oauth" || accountModels(credential))
|
|
35
|
+
return undefined;
|
|
36
|
+
return ("[fastagent] the ChatGPT sign-in for openai carries no model catalog, so it lists no models: it is read at " +
|
|
37
|
+
"login and on each token refresh. Run `fastagent login` to read it now");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `provider` with the account catalog: read at login and at each token refresh, and the only models a ChatGPT
|
|
41
|
+
* sign-in lists. Anything without OAuth is returned as is.
|
|
42
|
+
*/
|
|
43
|
+
export function withAccountModels(provider) {
|
|
44
|
+
const oauth = provider.auth.oauth;
|
|
45
|
+
if (!oauth)
|
|
46
|
+
return provider;
|
|
47
|
+
const wrapped = {
|
|
48
|
+
...oauth,
|
|
49
|
+
async login(interaction, options) {
|
|
50
|
+
const credential = await oauth.login(interaction, options);
|
|
51
|
+
try {
|
|
52
|
+
return {
|
|
53
|
+
...credential,
|
|
54
|
+
[ACCOUNT_MODELS_FIELD]: await fetchAccountModels(credential.access, interaction.signal),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
// The sign-in itself worked and inference needs nothing more: keep it, and say the list is empty until the
|
|
59
|
+
// next token refresh reads the catalog.
|
|
60
|
+
interaction.notify({
|
|
61
|
+
type: "info",
|
|
62
|
+
message: `could not read this ChatGPT account's model catalog (${error.message}); it lists no models until the next token refresh or login`,
|
|
63
|
+
});
|
|
64
|
+
return credential;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
async refresh(credential, signal) {
|
|
68
|
+
const next = await oauth.refresh(credential, signal);
|
|
69
|
+
try {
|
|
70
|
+
return { ...next, [ACCOUNT_MODELS_FIELD]: await fetchAccountModels(next.access, signal) };
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// NEVER fail here: the refresh token was just rotated, and a throw would leave the new one unstored and the
|
|
74
|
+
// old one spent. The previous catalog stands.
|
|
75
|
+
log.warn(`[fastagent] could not refresh the ChatGPT account's model catalog: ${error.message}`);
|
|
76
|
+
const previous = accountModels(credential);
|
|
77
|
+
return previous ? { ...next, [ACCOUNT_MODELS_FIELD]: previous } : next;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
...provider,
|
|
83
|
+
auth: { ...provider.auth, oauth: wrapped },
|
|
84
|
+
filterModels: (models, credential) => {
|
|
85
|
+
const base = provider.filterModels?.(models, credential) ?? models;
|
|
86
|
+
if (credential?.type !== "oauth")
|
|
87
|
+
return base;
|
|
88
|
+
const listed = new Set(accountModels(credential) ?? []);
|
|
89
|
+
return base.filter((model) => listed.has(model.id));
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Providers this module built, so a second registration leaves them as they are instead of wrapping them again. */
|
|
94
|
+
const wrappedProviders = new WeakSet();
|
|
95
|
+
/**
|
|
96
|
+
* Put {@link withAccountModels} on a runtime's `openai`, over the provider the runtime composed (pi's remote catalog,
|
|
97
|
+
* the agent's models.json, and an extension's `registerProvider("openai", …)` included), so none of them is lost. pi
|
|
98
|
+
* composes a native provider with models.json once more; that second pass is idempotent today, and
|
|
99
|
+
* openai-account-models.test.ts holds it to that. Call it inside `withModelRegistration`, and again after extensions
|
|
100
|
+
* load: pi's `registerProvider` drops a native provider, and a refresh without the wrapper would store a credential
|
|
101
|
+
* without its catalog. Repeating it is harmless.
|
|
102
|
+
*
|
|
103
|
+
* Known ceiling: an extension that registers `openai` later than its load (from an event handler) drops the wrapper
|
|
104
|
+
* for that runtime.
|
|
105
|
+
*/
|
|
106
|
+
export function registerAccountModels(runtime) {
|
|
107
|
+
const registered = runtime.getRegisteredNativeProvider(OPENAI_PROVIDER);
|
|
108
|
+
if (registered && wrappedProviders.has(registered))
|
|
109
|
+
return;
|
|
110
|
+
const provider = runtime.getProvider(OPENAI_PROVIDER);
|
|
111
|
+
if (!provider)
|
|
112
|
+
return;
|
|
113
|
+
const wrapped = withAccountModels(provider);
|
|
114
|
+
wrappedProviders.add(wrapped);
|
|
115
|
+
runtime.registerNativeProvider(wrapped);
|
|
116
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { SkillDiagnostic } from "@earendil-works/pi-agent-core";
|
|
6
6
|
import type { SkillCollision } from "./definition.ts";
|
|
7
7
|
import type { ToolCollision } from "./tool.ts";
|
|
8
|
+
import type { IndirectTool } from "./create.ts";
|
|
8
9
|
type Findings = {
|
|
9
10
|
collisions: SkillCollision[];
|
|
10
11
|
diagnostics: SkillDiagnostic[];
|
|
@@ -12,5 +13,7 @@ type Findings = {
|
|
|
12
13
|
/** THE door for definition findings: warns only when this dir's set CHANGED since the last report. */
|
|
13
14
|
export declare function reportFindingsIfChanged(dir: string, def: Findings): void;
|
|
14
15
|
export declare function reportDefinitionWarnings(collisions: SkillCollision[], diagnostics: SkillDiagnostic[]): void;
|
|
16
|
+
/** The report line for the mounted tools the model is not given up front: each name with its way in. */
|
|
17
|
+
export declare function describeIndirectTools(tools: readonly IndirectTool[]): string;
|
|
15
18
|
export declare function reportToolCollisions(collisions: ToolCollision[]): void;
|
|
16
19
|
export {};
|
|
@@ -23,6 +23,17 @@ export function reportDefinitionWarnings(collisions, diagnostics) {
|
|
|
23
23
|
log.warn(`[fastagent] ${d.code}: ${d.message} (${d.path})`);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
const REACH = {
|
|
27
|
+
tool_search: "loaded via tool_search",
|
|
28
|
+
codemode: "codemode scripts only",
|
|
29
|
+
hidden: "hidden",
|
|
30
|
+
inactive: "inactive until an authored loader activates it",
|
|
31
|
+
unreachable: "unreachable: pi's settings disable the built-in extension it needs",
|
|
32
|
+
};
|
|
33
|
+
/** The report line for the mounted tools the model is not given up front: each name with its way in. */
|
|
34
|
+
export function describeIndirectTools(tools) {
|
|
35
|
+
return tools.map((tool) => `${tool.name} (${REACH[tool.reach]})`).join(", ");
|
|
36
|
+
}
|
|
26
37
|
export function reportToolCollisions(collisions) {
|
|
27
38
|
for (const c of collisions) {
|
|
28
39
|
log.warn(`[fastagent] tool "${c.name}" (${c.source}) dropped — a default/config tool already uses that name`);
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
|
-
import { SessionManager, SettingsManager, createAgentSessionRuntime,
|
|
8
|
-
import { bindPiSession,
|
|
9
|
-
import { readMachine } from "./machine.js";
|
|
7
|
+
import { SessionManager, SettingsManager, createAgentSessionRuntime, getAgentDir, } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { bindPiSession, definitionServices } from "./agent-session-factory.js";
|
|
10
9
|
import { resolveModel } from "./config.js";
|
|
11
10
|
import { canonicalPath } from "./definition.js";
|
|
12
11
|
import { reportToolCollisions } from "./report.js";
|
|
@@ -42,29 +41,14 @@ export async function buildAgentSessionRuntime(dir, options = {}, sessionManager
|
|
|
42
41
|
const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
|
|
43
42
|
const { modelSpec, assembly, definition } = await assemblyFor(cwd);
|
|
44
43
|
// Per session, NOT memoized with the assembly.
|
|
45
|
-
const
|
|
46
|
-
const loaded = await
|
|
47
|
-
cwd,
|
|
48
|
-
// fastagent's models + auth hub replaces pi's default (~/.pi-backed) one — the auth unification point.
|
|
49
|
-
modelRuntime,
|
|
50
|
-
// PACKAGELESS, for the loader: fastagent never installs a pi package (machine.ts). Handed pi's own settings, the
|
|
51
|
-
// loader resolves `packages` itself — installing a missing one, and failing chat's start when that fails.
|
|
52
|
-
settingsManager: machine.settingsManager(),
|
|
53
|
-
resourceLoaderOptions: definitionResourceLoaderOptions({
|
|
54
|
-
systemPrompt: () => definition.systemPrompt,
|
|
55
|
-
skills: () => definition.skills,
|
|
56
|
-
machine,
|
|
57
|
-
extensionPaths: assembly.extensionPaths,
|
|
58
|
-
}),
|
|
59
|
-
});
|
|
44
|
+
const modelRuntime = await assembly.createModelRuntime();
|
|
45
|
+
const loaded = await definitionServices({ cwd, modelRuntime, definition, extensionPaths: assembly.extensionPaths });
|
|
60
46
|
// ...while the SESSION keeps pi's own file-backed settings, so `/settings` in the TUI still saves. pi persists by
|
|
61
47
|
// re-reading the file under its lock and writing only the fields that changed, so `packages` there is untouched.
|
|
62
48
|
const services = { ...loaded, settingsManager: SettingsManager.create(cwd, loaded.agentDir) };
|
|
63
|
-
reportExtensionErrors(services);
|
|
64
49
|
// AFTER the services, because an extension may be what defines the model.
|
|
65
50
|
const model = resolveModel(modelRuntime, modelSpec);
|
|
66
|
-
//
|
|
67
|
-
// the documented divergence.
|
|
51
|
+
// Serving and chat restore the same native transcript declarations.
|
|
68
52
|
const result = await bindPiSession({
|
|
69
53
|
services,
|
|
70
54
|
sessionManager,
|
|
@@ -74,9 +58,9 @@ export async function buildAgentSessionRuntime(dir, options = {}, sessionManager
|
|
|
74
58
|
// is the definition's (`thinkingLevel` in fastagent.config.ts), like serving's.
|
|
75
59
|
thinkingLevel: assembly.thinkingLevel,
|
|
76
60
|
tools: assembly.tools,
|
|
61
|
+
excludedToolNames: assembly.excludedToolNames,
|
|
77
62
|
// A tool must see one spelling of the workspace, including when opened through a symlink.
|
|
78
63
|
cwd: rootCwd,
|
|
79
|
-
recordActivations: false,
|
|
80
64
|
});
|
|
81
65
|
return { ...result, services, diagnostics: services.diagnostics };
|
|
82
66
|
};
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
16
16
|
import { type Models } from "@earendil-works/pi-ai";
|
|
17
17
|
import { type AgentCommand, type SessionControl, type SessionEvent } from "../../session.ts";
|
|
18
|
-
import type
|
|
18
|
+
import { type SessionObserver, type Lease } from "./turn-kit.ts";
|
|
19
19
|
import type { AnyModel } from "./models.ts";
|
|
20
20
|
import type { PiAgentSessionFactory } from "./invoke-session.ts";
|
|
21
21
|
import type { PiSessionRecordStore } from "./session-store.ts";
|
|
@@ -25,9 +25,9 @@ export declare const SUBSCRIBER_BUFFER_CAP = 10000;
|
|
|
25
25
|
export declare const SUBSCRIBER_BUFFER_BYTES: number;
|
|
26
26
|
/** What the plane's writes (`update` / `compact` / `fork` / `delete`) need — the SAME instances the
|
|
27
27
|
* agent assembly uses: the lease (a write must not race a run), the model registry (validation +
|
|
28
|
-
* allowedModels), and the session factory (compaction is a model call).
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* allowedModels), and the session factory (compaction is a model call). Two writes create a record:
|
|
29
|
+
* fork's `into`, and `update()` on an id that has none (through the store's `openOrCreate`, like an
|
|
30
|
+
* invoke). Every other write needs a record that exists. */
|
|
31
31
|
export interface PiBoundaryWiring {
|
|
32
32
|
lease: Lease;
|
|
33
33
|
models: Models;
|