@fastagent-sh/fastagent 0.22.0 → 0.23.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/channels/agentcore-protocol.d.ts +2 -1
- package/dist/channels/agentcore-service.js +10 -18
- package/dist/channels/agentcore.js +6 -3
- package/dist/channels/control.js +23 -0
- package/dist/channels/slack/config-api.d.ts +2 -2
- package/dist/channels/slack/config-api.js +2 -2
- package/dist/channels/slack/onboard.js +2 -2
- package/dist/channels/slack/register-webhook.js +2 -2
- package/dist/channels/slack/slack-api.js +4 -4
- package/dist/cli/add-feishu.js +4 -2
- package/dist/cli/auth-view.d.ts +15 -6
- package/dist/cli/auth-view.js +24 -8
- package/dist/cli/box-login.d.ts +33 -0
- package/dist/cli/box-login.js +162 -0
- package/dist/cli/commands/deploy/agentcore.js +48 -11
- package/dist/cli/commands/deploy/docker.js +24 -13
- package/dist/cli/commands/deploy/fly.js +30 -7
- package/dist/cli/commands/deploy/railway.js +21 -7
- package/dist/cli/commands/deploy/shared.d.ts +18 -17
- package/dist/cli/commands/deploy/shared.js +20 -21
- package/dist/cli/commands/destroy.js +2 -2
- package/dist/cli/commands/info.js +9 -6
- package/dist/cli/commands/invoke.js +2 -2
- package/dist/cli/commands/login.d.ts +7 -1
- package/dist/cli/commands/login.js +123 -10
- package/dist/cli/commands/logs.js +2 -2
- package/dist/cli/commands/models.d.ts +4 -1
- package/dist/cli/commands/models.js +37 -4
- package/dist/cli/commands/routine-run.js +2 -2
- package/dist/cli/commands/start.d.ts +0 -5
- package/dist/cli/commands/start.js +3 -13
- package/dist/cli/kernel.d.ts +2 -0
- package/dist/cli/kernel.js +1 -1
- package/dist/cli/login-relay.d.ts +38 -0
- package/dist/cli/login-relay.js +121 -0
- package/dist/cli/program.js +46 -8
- package/dist/cli/shared.d.ts +4 -4
- package/dist/cli/shared.js +24 -30
- package/dist/deploy/agentcore/plan.d.ts +10 -11
- package/dist/deploy/agentcore/plan.js +8 -14
- package/dist/deploy/agentcore/run.d.ts +8 -1
- package/dist/deploy/agentcore/run.js +20 -14
- package/dist/deploy/agentcore/shell.d.ts +25 -0
- package/dist/deploy/agentcore/shell.js +202 -0
- package/dist/deploy/box-shell.d.ts +43 -0
- package/dist/deploy/box-shell.js +28 -0
- package/dist/deploy/build-context.js +16 -7
- package/dist/deploy/channel-ingress.d.ts +20 -1
- package/dist/deploy/channel-ingress.js +34 -7
- package/dist/deploy/container.d.ts +14 -0
- package/dist/deploy/container.js +45 -1
- package/dist/deploy/docker/plan.d.ts +7 -3
- package/dist/deploy/docker/plan.js +9 -14
- package/dist/deploy/docker/run.d.ts +14 -7
- package/dist/deploy/docker/run.js +58 -35
- package/dist/deploy/fly/plan.d.ts +6 -3
- package/dist/deploy/fly/plan.js +6 -8
- package/dist/deploy/fly/run.d.ts +8 -2
- package/dist/deploy/fly/run.js +15 -25
- package/dist/deploy/preflight.d.ts +9 -7
- package/dist/deploy/preflight.js +74 -20
- package/dist/deploy/railway/plan.d.ts +6 -3
- package/dist/deploy/railway/plan.js +7 -9
- package/dist/deploy/railway/run.d.ts +10 -6
- package/dist/deploy/railway/run.js +29 -13
- package/dist/deploy/runner.d.ts +16 -0
- package/dist/deploy/runner.js +31 -0
- package/dist/deploy/secrets.d.ts +4 -13
- package/dist/deploy/secrets.js +8 -27
- package/dist/dev-supervisor.js +5 -2
- package/dist/engines/pi/agent-models.d.ts +70 -0
- package/dist/engines/pi/agent-models.js +93 -0
- package/dist/engines/pi/auth.d.ts +56 -3
- package/dist/engines/pi/auth.js +63 -16
- package/dist/engines/pi/config.d.ts +0 -25
- package/dist/engines/pi/config.js +3 -37
- package/dist/engines/pi/create.d.ts +22 -16
- package/dist/engines/pi/create.js +28 -28
- package/dist/engines/pi/entry-images.d.ts +14 -0
- package/dist/engines/pi/entry-images.js +23 -0
- package/dist/engines/pi/invoke-session.js +26 -0
- package/dist/engines/pi/login.d.ts +20 -13
- package/dist/engines/pi/login.js +19 -12
- package/dist/engines/pi/models.d.ts +115 -36
- package/dist/engines/pi/models.js +218 -48
- package/dist/engines/pi/open.d.ts +42 -11
- package/dist/engines/pi/open.js +53 -21
- package/dist/engines/pi/service.d.ts +2 -2
- package/dist/engines/pi/service.js +1 -0
- package/dist/engines/pi/session-builder.d.ts +3 -2
- package/dist/engines/pi/session-builder.js +28 -58
- package/dist/engines/pi/session-control.js +14 -3
- package/dist/engines/pi/turn-kit.js +11 -5
- package/dist/open-url.d.ts +15 -1
- package/dist/open-url.js +35 -3
- package/dist/paths.d.ts +4 -2
- package/dist/paths.js +6 -2
- package/dist/pi.d.ts +5 -3
- package/dist/pi.js +3 -2
- package/dist/service.d.ts +5 -8
- package/dist/session-remote.js +21 -0
- package/dist/session.d.ts +39 -6
- package/package.json +2 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { GLOBAL_AUTH_PATH, assertOneCredentialSource, fastagentCredentialStore, resolveAuthLayers, } from "./auth.js";
|
|
3
|
+
import { createPiModelRuntime, environmentAuthSource, modelRuntimeFiles, piModelsOver, probeAuthSource, } from "./models.js";
|
|
4
|
+
export function agentModels(agentDir, source = {}, options = {}) {
|
|
5
|
+
assertOneCredentialSource(source);
|
|
6
|
+
const { providers, machineLayer } = options;
|
|
7
|
+
const files = source.credentialStore
|
|
8
|
+
? undefined
|
|
9
|
+
: agentDir
|
|
10
|
+
? resolveAuthLayers(agentDir, source.authPath)
|
|
11
|
+
: { path: source.authPath ?? GLOBAL_AUTH_PATH };
|
|
12
|
+
const store = files
|
|
13
|
+
? agentCredentialStore(files, { agentDir, providers, machineLayer, warn: source.warn })
|
|
14
|
+
: undefined;
|
|
15
|
+
const credentials = source.credentialStore ?? store;
|
|
16
|
+
let registry;
|
|
17
|
+
const runtime = () => {
|
|
18
|
+
registry ??= createPiModelRuntime({
|
|
19
|
+
credentials,
|
|
20
|
+
...(agentDir ? { agentDir } : {}),
|
|
21
|
+
...(providers ? { providers } : {}),
|
|
22
|
+
...(machineLayer !== undefined ? { machineLayer } : {}),
|
|
23
|
+
});
|
|
24
|
+
return registry;
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
...(files ? { auth: files } : {}),
|
|
28
|
+
credentials,
|
|
29
|
+
runtime,
|
|
30
|
+
async authStatus(provider, modelId) {
|
|
31
|
+
const models = await runtime();
|
|
32
|
+
const id = modelId ?? models.getProvider(provider)?.getModels()[0]?.id;
|
|
33
|
+
let resolved = {};
|
|
34
|
+
if (id !== undefined) {
|
|
35
|
+
try {
|
|
36
|
+
const found = await probeAuthSource(models, `${provider}/${id}`);
|
|
37
|
+
if (found !== undefined)
|
|
38
|
+
resolved = { source: found };
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
// The first line: pi folds the request's stack into the message of a failed refresh.
|
|
42
|
+
resolved = { error: String(error.message).split("\n")[0] };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// `read` never refreshes: this is the kind on file, whatever became of it.
|
|
46
|
+
const stored = (await credentials.read(provider))?.type;
|
|
47
|
+
const fromEnvironment = stored === undefined ? undefined : await environmentAuthSource(provider, process.env);
|
|
48
|
+
const shadowed = resolved.source !== undefined && fromEnvironment !== resolved.source ? fromEnvironment : undefined;
|
|
49
|
+
return {
|
|
50
|
+
...(store ? { path: await store.layerOf(provider) } : {}),
|
|
51
|
+
...resolved,
|
|
52
|
+
...(stored !== undefined ? { stored } : {}),
|
|
53
|
+
...(shadowed !== undefined ? { shadowed } : {}),
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** A `Models` with every built-in pi provider, wired to fastagent's auth. */
|
|
59
|
+
export function createPiModels(options = {}) {
|
|
60
|
+
return piModelsOver(agentModels(undefined, options).credentials, options.providers);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The credential store an agent reads through: its own file, then the user-global one for a provider the PROJECT
|
|
64
|
+
* authenticates no other way (its own auth file, a models.json key, or the environment). "No other way" is answered
|
|
65
|
+
* by pi's own resolution over a registry that has no global layer, built the first time a provider is looked up
|
|
66
|
+
* there. A deployment never has the global file, so a global login that outranked, say, an `ANTHROPIC_API_KEY` in
|
|
67
|
+
* `.secrets/.env` would run one credential here and a different one deployed.
|
|
68
|
+
*/
|
|
69
|
+
function agentCredentialStore(auth, options) {
|
|
70
|
+
const { warn, providers = [] } = options;
|
|
71
|
+
const fallback = auth.fallback;
|
|
72
|
+
if (fallback === undefined)
|
|
73
|
+
return fastagentCredentialStore(auth.path, { warn });
|
|
74
|
+
let project;
|
|
75
|
+
const projectRuntime = () => {
|
|
76
|
+
project ??= (async () => {
|
|
77
|
+
const runtime = await ModelRuntime.create({
|
|
78
|
+
credentials: fastagentCredentialStore(auth.path, { warn }),
|
|
79
|
+
...(await modelRuntimeFiles(options)).create,
|
|
80
|
+
refreshOnCreate: false,
|
|
81
|
+
});
|
|
82
|
+
for (const provider of providers)
|
|
83
|
+
runtime.registerNativeProvider(provider);
|
|
84
|
+
return runtime;
|
|
85
|
+
})();
|
|
86
|
+
return project;
|
|
87
|
+
};
|
|
88
|
+
return fastagentCredentialStore(auth.path, {
|
|
89
|
+
warn,
|
|
90
|
+
fallbackPath: fallback,
|
|
91
|
+
projectAuthenticates: async (providerId) => (await (await projectRuntime()).checkAuth(providerId)) !== undefined,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -8,15 +8,68 @@ export interface FastagentAuthOptions {
|
|
|
8
8
|
/** Sink for non-fatal auth anomalies (unreadable/corrupt file). */
|
|
9
9
|
warn?: (message: string) => void;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Where a caller's model credentials live: fastagent's JSON file (`authPath`, or the default layers when neither is
|
|
13
|
+
* given), or a {@link CredentialStore} the caller owns (an OS keychain, Electron `safeStorage`). A supplied store
|
|
14
|
+
* replaces the FILES: none is read or written, and no global layer applies. Env variables and `models.json` keys still
|
|
15
|
+
* apply, below a stored credential, exactly as with `authPath`. The store owns what the file store does
|
|
16
|
+
* for its file: `modify` for one provider must run one at a time, because an OAuth refresh happens inside it and a
|
|
17
|
+
* rotated refresh token must never be used twice; and a store it cannot parse must not be answered as empty, or the
|
|
18
|
+
* next write replaces every provider's credential.
|
|
19
|
+
*/
|
|
20
|
+
export interface CredentialSourceOptions {
|
|
21
|
+
/** fastagent's credentials file. Mutually exclusive with {@link credentialStore}. */
|
|
22
|
+
authPath?: string;
|
|
23
|
+
/** The caller's own store, read and written instead of any file. Mutually exclusive with {@link authPath}. */
|
|
24
|
+
credentialStore?: CredentialStore;
|
|
25
|
+
}
|
|
26
|
+
/** The one refusal for a caller that names both sources: silently preferring one would hide the other's mistake. */
|
|
27
|
+
export declare function assertOneCredentialSource(options: CredentialSourceOptions): void;
|
|
28
|
+
/** A credential store over fastagent's files, which can also say which file a provider belongs to. */
|
|
29
|
+
export type FastagentCredentialStore = CredentialStore & {
|
|
30
|
+
/**
|
|
31
|
+
* The file this store reads a provider from and writes its refresh to: where it already is, else the primary. The
|
|
32
|
+
* one answer to "which file do I edit", so a report naming a file cannot disagree with the store that reads it.
|
|
33
|
+
*/
|
|
34
|
+
layerOf(providerId: string): Promise<string>;
|
|
35
|
+
};
|
|
11
36
|
/**
|
|
12
37
|
* A read-write `CredentialStore` over the given credentials file (default {@link GLOBAL_AUTH_PATH}; the directory
|
|
13
|
-
* opener passes the project-level `<root>/.secrets/auth.json`), optionally falling back to a second file.
|
|
38
|
+
* opener passes the project-level `<root>/.secrets/auth.json`), optionally falling back to a second file. A path is
|
|
39
|
+
* read like `FASTAGENT_AUTH_PATH` (`~` is the home directory, a relative one resolves against the cwd), so every
|
|
40
|
+
* caller that names a file names the same one.
|
|
14
41
|
*
|
|
15
42
|
* The fallback is PER PROVIDER, not per file: logging one provider into a project must not hide the others a person
|
|
16
43
|
* already has globally. And the layer a credential was READ from is the layer its refresh is written back to —
|
|
17
44
|
* anything else would conjure a second holder of the same OAuth grant, which is the failure this whole area exists
|
|
18
45
|
* to avoid. A provider present in neither layer is new, and new credentials belong to the primary.
|
|
46
|
+
*
|
|
47
|
+
* The fallback also yields to `projectAuthenticates`: a provider the project authenticates some other way (an env key,
|
|
48
|
+
* its own models.json) never reaches it — for reading, listing or writing back.
|
|
19
49
|
*/
|
|
20
|
-
export declare function fastagentCredentialStore(
|
|
50
|
+
export declare function fastagentCredentialStore(path?: string, options?: FastagentAuthOptions & {
|
|
21
51
|
fallbackPath?: string;
|
|
22
|
-
|
|
52
|
+
/** Whether the project authenticates this provider without the fallback; then the fallback is not read for it. */
|
|
53
|
+
projectAuthenticates?: (providerId: string) => Promise<boolean>;
|
|
54
|
+
}): FastagentCredentialStore;
|
|
55
|
+
/** The effective auth file for an agent: override if present, else `<secrets dir>/auth.json`. */
|
|
56
|
+
export declare function resolveAuthPath(dir: string, flag?: string, env?: NodeJS.ProcessEnv): string;
|
|
57
|
+
/**
|
|
58
|
+
* Which credentials files an agent reads: `path` first, then, per provider, `fallback`. One value because it is one
|
|
59
|
+
* decision, read through `agentModels` (agent-models.ts) by everything that reads an agent's credentials.
|
|
60
|
+
*/
|
|
61
|
+
export interface AuthLayers {
|
|
62
|
+
path: string;
|
|
63
|
+
fallback?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The {@link AuthLayers} an agent directory reads: `authPath` option > `FASTAGENT_AUTH_PATH` > its own file, and
|
|
67
|
+
* behind it the user-global store, because a login is a PERSON on a machine and not a project — one `login -g` then
|
|
68
|
+
* serves every agent here.
|
|
69
|
+
*
|
|
70
|
+
* No fallback when a path was named: "use this file" is an instruction, not a preference. BOTH knobs
|
|
71
|
+
* {@link resolveAuthPath} reads count as that instruction, `FASTAGENT_SECRETS_DIR` included — it is what the
|
|
72
|
+
* Fly/Railway/AgentCore artifacts set, and a deployed container must read its mounted credentials and nothing else
|
|
73
|
+
* (the artifact is the truth).
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveAuthLayers(agentDir: string, authPath?: string, env?: NodeJS.ProcessEnv): AuthLayers;
|
package/dist/engines/pi/auth.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth for the pi engine: a read-WRITE {@link CredentialStore} over a fastagent credentials file, consumed by the
|
|
3
|
-
* `Models` collection (models.ts). The write path refuses to overwrite a
|
|
4
|
-
* the other providers' credentials.
|
|
3
|
+
* `Models` collection (models.ts), and which of those files an agent reads. The write path refuses to overwrite a
|
|
4
|
+
* corrupt file, so a torn read never clobbers the other providers' credentials.
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { homedir } from "node:os";
|
|
8
7
|
import { dirname, join, resolve } from "node:path";
|
|
9
|
-
import {
|
|
8
|
+
import { globalHome, SECRETS_DIRNAME, SECRET_FILE_MODE, resolveOverridePath, resolveSecretsDir } from "../../paths.js";
|
|
10
9
|
import { writeFileAtomic } from "../../atomic-write.js";
|
|
11
10
|
import { log } from "../../log.js";
|
|
12
11
|
import lockfile from "proper-lockfile";
|
|
@@ -14,7 +13,13 @@ import lockfile from "proper-lockfile";
|
|
|
14
13
|
* The GLOBAL fastagent credentials file (distinct from pi's `~/.pi`), under the user-global machinery home
|
|
15
14
|
* `~/.fastagent/`.
|
|
16
15
|
*/
|
|
17
|
-
export const GLOBAL_AUTH_PATH = join(
|
|
16
|
+
export const GLOBAL_AUTH_PATH = join(globalHome(), SECRETS_DIRNAME, "auth.json");
|
|
17
|
+
/** The one refusal for a caller that names both sources: silently preferring one would hide the other's mistake. */
|
|
18
|
+
export function assertOneCredentialSource(options) {
|
|
19
|
+
if (options.authPath !== undefined && options.credentialStore !== undefined) {
|
|
20
|
+
throw new Error("pass authPath or credentialStore, not both: a supplied store replaces the credentials file");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
18
23
|
/** A valid stored credential, or undefined — a foreign/old entry reads as not-configured, not a crash. */
|
|
19
24
|
function pick(creds, providerId) {
|
|
20
25
|
const cred = creds[providerId];
|
|
@@ -153,16 +158,25 @@ function parseForWrite(raw, where) {
|
|
|
153
158
|
}
|
|
154
159
|
/**
|
|
155
160
|
* A read-write `CredentialStore` over the given credentials file (default {@link GLOBAL_AUTH_PATH}; the directory
|
|
156
|
-
* opener passes the project-level `<root>/.secrets/auth.json`), optionally falling back to a second file.
|
|
161
|
+
* opener passes the project-level `<root>/.secrets/auth.json`), optionally falling back to a second file. A path is
|
|
162
|
+
* read like `FASTAGENT_AUTH_PATH` (`~` is the home directory, a relative one resolves against the cwd), so every
|
|
163
|
+
* caller that names a file names the same one.
|
|
157
164
|
*
|
|
158
165
|
* The fallback is PER PROVIDER, not per file: logging one provider into a project must not hide the others a person
|
|
159
166
|
* already has globally. And the layer a credential was READ from is the layer its refresh is written back to —
|
|
160
167
|
* anything else would conjure a second holder of the same OAuth grant, which is the failure this whole area exists
|
|
161
168
|
* to avoid. A provider present in neither layer is new, and new credentials belong to the primary.
|
|
169
|
+
*
|
|
170
|
+
* The fallback also yields to `projectAuthenticates`: a provider the project authenticates some other way (an env key,
|
|
171
|
+
* its own models.json) never reaches it — for reading, listing or writing back.
|
|
162
172
|
*/
|
|
163
|
-
export function fastagentCredentialStore(
|
|
173
|
+
export function fastagentCredentialStore(path, options = {}) {
|
|
164
174
|
const warn = options.warn ?? ((message) => log.warn(message));
|
|
165
|
-
const
|
|
175
|
+
const authPath = resolveOverridePath(path) ?? GLOBAL_AUTH_PATH;
|
|
176
|
+
const fallbackPath = resolveOverridePath(options.fallbackPath);
|
|
177
|
+
const fallback = fallbackPath !== undefined && fallbackPath !== authPath ? fallbackPath : undefined;
|
|
178
|
+
/** The fallback, when it may serve this provider at all. */
|
|
179
|
+
const fallbackFor = async (providerId) => fallback === undefined || (await options.projectAuthenticates?.(providerId)) ? undefined : fallback;
|
|
166
180
|
/**
|
|
167
181
|
* The file that owns this provider: where it already is, else the primary.
|
|
168
182
|
*
|
|
@@ -171,18 +185,21 @@ export function fastagentCredentialStore(authPath = GLOBAL_AUTH_PATH, options =
|
|
|
171
185
|
* "one grant, one copy" rule can be lost. Re-checking under the lock means locking both files in a fixed order;
|
|
172
186
|
* worth it only if concurrent logins stop being a rounding error.
|
|
173
187
|
*/
|
|
174
|
-
const owner = (providerId) => {
|
|
175
|
-
|
|
188
|
+
const owner = async (providerId) => {
|
|
189
|
+
const second = await fallbackFor(providerId);
|
|
190
|
+
if (second === undefined)
|
|
176
191
|
return authPath;
|
|
177
192
|
const primary = readCreds(authPath, warn);
|
|
178
193
|
if (primary && pick(primary, providerId))
|
|
179
194
|
return authPath;
|
|
180
|
-
const secondary = readCreds(
|
|
181
|
-
return secondary && pick(secondary, providerId) ?
|
|
195
|
+
const secondary = readCreds(second, warn);
|
|
196
|
+
return secondary && pick(secondary, providerId) ? second : authPath;
|
|
182
197
|
};
|
|
183
198
|
return {
|
|
199
|
+
layerOf: owner,
|
|
184
200
|
async read(providerId) {
|
|
185
|
-
|
|
201
|
+
const second = await fallbackFor(providerId);
|
|
202
|
+
for (const path of second === undefined ? [authPath] : [authPath, second]) {
|
|
186
203
|
const creds = readCreds(path, warn);
|
|
187
204
|
const found = creds && pick(creds, providerId);
|
|
188
205
|
if (found)
|
|
@@ -196,14 +213,16 @@ export function fastagentCredentialStore(authPath = GLOBAL_AUTH_PATH, options =
|
|
|
196
213
|
const infos = new Map();
|
|
197
214
|
for (const path of fallback === undefined ? [authPath] : [fallback, authPath]) {
|
|
198
215
|
for (const [providerId, cred] of Object.entries(readCreds(path, warn) ?? {})) {
|
|
216
|
+
if (path === fallback && (await fallbackFor(providerId)) === undefined)
|
|
217
|
+
continue;
|
|
199
218
|
if (cred && (cred.type === "oauth" || cred.type === "api_key"))
|
|
200
219
|
infos.set(providerId, { providerId, type: cred.type });
|
|
201
220
|
}
|
|
202
221
|
}
|
|
203
222
|
return [...infos.values()];
|
|
204
223
|
},
|
|
205
|
-
modify(providerId, fn) {
|
|
206
|
-
const path = owner(providerId);
|
|
224
|
+
async modify(providerId, fn) {
|
|
225
|
+
const path = await owner(providerId);
|
|
207
226
|
return withLockedAuthFile(path, async (current) => {
|
|
208
227
|
const creds = parseForWrite(current, path); // corrupt → throw → no clobber
|
|
209
228
|
const next = await fn(pick(creds, providerId));
|
|
@@ -214,7 +233,7 @@ export function fastagentCredentialStore(authPath = GLOBAL_AUTH_PATH, options =
|
|
|
214
233
|
});
|
|
215
234
|
},
|
|
216
235
|
async delete(providerId) {
|
|
217
|
-
const path = owner(providerId);
|
|
236
|
+
const path = await owner(providerId);
|
|
218
237
|
// No-op when nothing is stored: do NOT take the lock (which would create the file) on a machine that never
|
|
219
238
|
// stored this provider.
|
|
220
239
|
if (!existsSync(path))
|
|
@@ -229,3 +248,31 @@ export function fastagentCredentialStore(authPath = GLOBAL_AUTH_PATH, options =
|
|
|
229
248
|
},
|
|
230
249
|
};
|
|
231
250
|
}
|
|
251
|
+
// ── Which files an agent reads ───────────────────────────────────────────────
|
|
252
|
+
/**
|
|
253
|
+
* The auth-file override: the SDK's `authPath` option > `FASTAGENT_AUTH_PATH` env > undefined (then
|
|
254
|
+
* `<secrets dir>/auth.json`). The CLI has no flag for it: a second spelling of one environment variable buys nothing,
|
|
255
|
+
* and the variable is what a deployed container reads anyway.
|
|
256
|
+
*/
|
|
257
|
+
function resolveAuthPathOverride(flag, env) {
|
|
258
|
+
return resolveOverridePath(flag ?? env.FASTAGENT_AUTH_PATH);
|
|
259
|
+
}
|
|
260
|
+
/** The effective auth file for an agent: override if present, else `<secrets dir>/auth.json`. */
|
|
261
|
+
export function resolveAuthPath(dir, flag, env = process.env) {
|
|
262
|
+
return resolveAuthPathOverride(flag, env) ?? join(resolveSecretsDir(dir, env), "auth.json");
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* The {@link AuthLayers} an agent directory reads: `authPath` option > `FASTAGENT_AUTH_PATH` > its own file, and
|
|
266
|
+
* behind it the user-global store, because a login is a PERSON on a machine and not a project — one `login -g` then
|
|
267
|
+
* serves every agent here.
|
|
268
|
+
*
|
|
269
|
+
* No fallback when a path was named: "use this file" is an instruction, not a preference. BOTH knobs
|
|
270
|
+
* {@link resolveAuthPath} reads count as that instruction, `FASTAGENT_SECRETS_DIR` included — it is what the
|
|
271
|
+
* Fly/Railway/AgentCore artifacts set, and a deployed container must read its mounted credentials and nothing else
|
|
272
|
+
* (the artifact is the truth).
|
|
273
|
+
*/
|
|
274
|
+
export function resolveAuthLayers(agentDir, authPath, env = process.env) {
|
|
275
|
+
const path = resolveAuthPath(agentDir, authPath, env);
|
|
276
|
+
const named = resolveAuthPathOverride(authPath, env) ?? resolveOverridePath(env.FASTAGENT_SECRETS_DIR);
|
|
277
|
+
return named === undefined ? { path, fallback: GLOBAL_AUTH_PATH } : { path };
|
|
278
|
+
}
|
|
@@ -63,28 +63,3 @@ export declare function rewriteConfigModel(src: string, spec: string): string |
|
|
|
63
63
|
* would refuse to start an agent whose config names a model.
|
|
64
64
|
*/
|
|
65
65
|
export declare function resolveModelSpec(flag: string | undefined, config: FastagentConfig, env?: NodeJS.ProcessEnv): string | undefined;
|
|
66
|
-
/** The default credentials file under a resolved secrets dir ({@link resolveSecretsDir}). */
|
|
67
|
-
export declare function defaultAuthPath(secretsDir: string): string;
|
|
68
|
-
/** The effective auth file for an agent: override if present, else `<secrets dir>/auth.json`. */
|
|
69
|
-
export declare function resolveAuthPath(dir: string, flag?: string, env?: NodeJS.ProcessEnv): string;
|
|
70
|
-
/**
|
|
71
|
-
* Which credentials files an agent reads: `path` first, then, per provider, `fallback` — present only when nothing
|
|
72
|
-
* named the path ({@link resolveAuthFallback}). One value because it is one decision: the opener, the model list, the
|
|
73
|
-
* startup report and the first-run picker all read through it.
|
|
74
|
-
*/
|
|
75
|
-
export interface AuthLayers {
|
|
76
|
-
path: string;
|
|
77
|
-
fallback?: string;
|
|
78
|
-
}
|
|
79
|
-
/** The {@link AuthLayers} an agent directory reads: `authPath` option > `FASTAGENT_AUTH_PATH` > its own file. */
|
|
80
|
-
export declare function resolveAuthLayers(agentDir: string, authPath?: string, env?: NodeJS.ProcessEnv): AuthLayers;
|
|
81
|
-
/**
|
|
82
|
-
* Where a credential the project does not have is read from instead: the user-global store, because a login is a
|
|
83
|
-
* PERSON on a machine and not a project — one `login -g` then serves every agent here. Undefined when an explicit
|
|
84
|
-
* path was named: "use this file" is an instruction, not a preference, so it gets no second layer.
|
|
85
|
-
*
|
|
86
|
-
* BOTH knobs {@link resolveAuthPath} reads count as that instruction, `FASTAGENT_SECRETS_DIR` included — it is what
|
|
87
|
-
* the Fly/Railway/AgentCore artifacts set, and a deployed container must read its mounted credentials and nothing
|
|
88
|
-
* else (the artifact is the truth).
|
|
89
|
-
*/
|
|
90
|
-
export declare function resolveAuthFallback(flag?: string, env?: NodeJS.ProcessEnv): string | undefined;
|
|
@@ -14,11 +14,10 @@ import { existsSync, statSync } from "node:fs";
|
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { pathToFileURL } from "node:url";
|
|
16
16
|
import { THINKING_LEVELS } from "./session-settings.js";
|
|
17
|
-
import { GLOBAL_AUTH_PATH } from "./auth.js";
|
|
18
17
|
import { readSecretDeclaration } from "../../declared-secrets.js";
|
|
19
18
|
import { assertCorsOrigins } from "../../channels/serve.js";
|
|
20
19
|
import { moduleLoadHint } from "../../loader.js";
|
|
21
|
-
import { AGENT_CONFIG_FILE
|
|
20
|
+
import { AGENT_CONFIG_FILE } from "../../paths.js";
|
|
22
21
|
/** Identity function for typing and IDE completion (vite/next-style). */
|
|
23
22
|
export function defineConfig(config) {
|
|
24
23
|
return config;
|
|
@@ -152,7 +151,8 @@ export function resolveModel(models, spec) {
|
|
|
152
151
|
const modelId = spec.slice(slash + 1);
|
|
153
152
|
const model = models.getModel(provider, modelId);
|
|
154
153
|
if (!model) {
|
|
155
|
-
throw new Error(`unknown model "${spec}" (provider "${provider}" / id "${modelId}" not in registry); run \`fastagent models\` to list
|
|
154
|
+
throw new Error(`unknown model "${spec}" (provider "${provider}" / id "${modelId}" not in registry); run \`fastagent models\` to list ` +
|
|
155
|
+
"available specs, or `fastagent models --refresh` in the agent to fetch models newer than the installed pi");
|
|
156
156
|
}
|
|
157
157
|
return model;
|
|
158
158
|
}
|
|
@@ -189,37 +189,3 @@ export function rewriteConfigModel(src, spec) {
|
|
|
189
189
|
export function resolveModelSpec(flag, config, env = process.env) {
|
|
190
190
|
return flag ?? (env.FASTAGENT_MODEL || config.model);
|
|
191
191
|
}
|
|
192
|
-
/**
|
|
193
|
-
* The auth-file override: the SDK's `authPath` option > `FASTAGENT_AUTH_PATH` env > undefined (the opener then falls
|
|
194
|
-
* back to {@link defaultAuthPath} under the {@link resolveSecretsDir} dir). The CLI has no flag for it: a second
|
|
195
|
-
* spelling of one environment variable buys nothing, and the variable is what a deployed container reads anyway.
|
|
196
|
-
*/
|
|
197
|
-
function resolveAuthPathOverride(flag, env = process.env) {
|
|
198
|
-
return resolveOverridePath(flag ?? env.FASTAGENT_AUTH_PATH);
|
|
199
|
-
}
|
|
200
|
-
/** The default credentials file under a resolved secrets dir ({@link resolveSecretsDir}). */
|
|
201
|
-
export function defaultAuthPath(secretsDir) {
|
|
202
|
-
return join(secretsDir, "auth.json");
|
|
203
|
-
}
|
|
204
|
-
/** The effective auth file for an agent: override if present, else `<secrets dir>/auth.json`. */
|
|
205
|
-
export function resolveAuthPath(dir, flag, env = process.env) {
|
|
206
|
-
return resolveAuthPathOverride(flag, env) ?? defaultAuthPath(resolveSecretsDir(dir, env));
|
|
207
|
-
}
|
|
208
|
-
/** The {@link AuthLayers} an agent directory reads: `authPath` option > `FASTAGENT_AUTH_PATH` > its own file. */
|
|
209
|
-
export function resolveAuthLayers(agentDir, authPath, env = process.env) {
|
|
210
|
-
const fallback = resolveAuthFallback(authPath, env);
|
|
211
|
-
return { path: resolveAuthPath(agentDir, authPath, env), ...(fallback !== undefined ? { fallback } : {}) };
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Where a credential the project does not have is read from instead: the user-global store, because a login is a
|
|
215
|
-
* PERSON on a machine and not a project — one `login -g` then serves every agent here. Undefined when an explicit
|
|
216
|
-
* path was named: "use this file" is an instruction, not a preference, so it gets no second layer.
|
|
217
|
-
*
|
|
218
|
-
* BOTH knobs {@link resolveAuthPath} reads count as that instruction, `FASTAGENT_SECRETS_DIR` included — it is what
|
|
219
|
-
* the Fly/Railway/AgentCore artifacts set, and a deployed container must read its mounted credentials and nothing
|
|
220
|
-
* else (the artifact is the truth).
|
|
221
|
-
*/
|
|
222
|
-
export function resolveAuthFallback(flag, env = process.env) {
|
|
223
|
-
const explicit = resolveAuthPathOverride(flag, env) ?? resolveOverridePath(env.FASTAGENT_SECRETS_DIR);
|
|
224
|
-
return explicit === undefined ? GLOBAL_AUTH_PATH : undefined;
|
|
225
|
-
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { ExecutionEnv, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { type ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import type { Provider } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { CredentialStore, Provider } from "@earendil-works/pi-ai";
|
|
4
4
|
import type { Agent } from "../../agent.ts";
|
|
5
|
-
import { type
|
|
6
|
-
import type { FastagentAuthOptions } from "./auth.ts";
|
|
5
|
+
import { type FastagentConfig } from "./config.ts";
|
|
7
6
|
import { type LoadedDefinition } from "./definition.ts";
|
|
8
7
|
import type { ModuleLoadFailure } from "../../loader.ts";
|
|
9
8
|
import { type ToolCollision, type MountedTool } from "./tool.ts";
|
|
10
9
|
import { type DeclaredSecret } from "../../declared-secrets.ts";
|
|
11
10
|
import { type PiAgentSessionFactory } from "./invoke-session.ts";
|
|
11
|
+
import { type PiAgentSessionFactoryOptions } from "./agent-session-factory.ts";
|
|
12
12
|
import { type AnyModel } from "./models.ts";
|
|
13
|
+
import { type AgentModels } from "./agent-models.ts";
|
|
13
14
|
import { type PiSessionRecordStore } from "./session-store.ts";
|
|
14
15
|
import { type Lease, type SessionObserver } from "./turn-kit.ts";
|
|
15
16
|
/**
|
|
@@ -65,6 +66,11 @@ export declare function assembleSystemPrompt(options: AssembleSystemPromptOption
|
|
|
65
66
|
export interface PiAssembly {
|
|
66
67
|
lease: Lease;
|
|
67
68
|
sessionFactory: PiAgentSessionFactory;
|
|
69
|
+
/**
|
|
70
|
+
* The registry alone, with injected providers registered. `chat` resolves its model against it only after its
|
|
71
|
+
* extensions have loaded, because there an extension may register the provider (serving refuses that).
|
|
72
|
+
*/
|
|
73
|
+
modelRuntime: () => Promise<ModelRuntime>;
|
|
68
74
|
/** The registry and configured model, resolved on first use (a credential read is async). */
|
|
69
75
|
engine: () => Promise<{
|
|
70
76
|
modelRuntime: ModelRuntime;
|
|
@@ -72,6 +78,10 @@ export interface PiAssembly {
|
|
|
72
78
|
}>;
|
|
73
79
|
/** The configured reasoning effort — the other half of the pair a session without overrides runs on. */
|
|
74
80
|
thinkingLevel: ThinkingLevel;
|
|
81
|
+
/** The tools every session mounts. */
|
|
82
|
+
tools: MountedTool[];
|
|
83
|
+
/** The prompt and skills a session runs on, read when a session is bound. */
|
|
84
|
+
readDefinition: PiAgentSessionFactoryOptions["readDefinition"];
|
|
75
85
|
/** The extension entry points every session loads, discovered once with the assembly. */
|
|
76
86
|
extensionPaths: readonly string[];
|
|
77
87
|
}
|
|
@@ -98,6 +108,8 @@ export interface CreatePiAgentOptions {
|
|
|
98
108
|
providers?: Provider[];
|
|
99
109
|
/** Credentials file for stored OAuth/API-key auth (default `GLOBAL_AUTH_PATH`). */
|
|
100
110
|
authPath?: string;
|
|
111
|
+
/** The caller's own credential store, in place of any file ({@link CredentialSourceOptions}). */
|
|
112
|
+
credentialStore?: CredentialStore;
|
|
101
113
|
sessions?: PiSessionRecordStore;
|
|
102
114
|
/** Supplies the working directory at L1 (default: process.cwd()), which loads no definition. */
|
|
103
115
|
env?: ExecutionEnv;
|
|
@@ -129,6 +141,8 @@ export interface CreatePiAgentFromDefinitionOptions {
|
|
|
129
141
|
* gets no second layer.
|
|
130
142
|
*/
|
|
131
143
|
authPath?: string;
|
|
144
|
+
/** The caller's own credential store, in place of any file ({@link CredentialSourceOptions}). */
|
|
145
|
+
credentialStore?: CredentialStore;
|
|
132
146
|
sessions?: PiSessionRecordStore;
|
|
133
147
|
/** Filesystem/process environment; see {@link CreatePiAgentOptions.env}. */
|
|
134
148
|
env?: ExecutionEnv;
|
|
@@ -137,20 +151,12 @@ export interface CreatePiAgentFromDefinitionOptions {
|
|
|
137
151
|
observer?: SessionObserver;
|
|
138
152
|
}
|
|
139
153
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* The directory rung assembles from it and `availableModelsFromDir` lists from it, so a layer added here reaches both.
|
|
154
|
+
* L2, as the value: load the directory (base + AGENTS.md + skills + env) and assemble. `models` is the directory's
|
|
155
|
+
* model environment when the caller already built it (the opener, whose report must describe what runs); otherwise it
|
|
156
|
+
* is built here from the credential options.
|
|
144
157
|
*/
|
|
145
|
-
export declare function
|
|
146
|
-
|
|
147
|
-
providers?: Provider[];
|
|
148
|
-
}): Promise<ModelRuntime>;
|
|
149
|
-
/** L2, as the value: load the directory (base + AGENTS.md + skills + env) and assemble. */
|
|
150
|
-
export declare function assemblePiFromDefinition(dir: string,
|
|
151
|
-
/** `auth` is the opener's already-resolved layers, which take the place of `authPath`. */
|
|
152
|
-
options: Omit<CreatePiAgentFromDefinitionOptions, "observer"> & {
|
|
153
|
-
auth?: AuthLayers;
|
|
158
|
+
export declare function assemblePiFromDefinition(dir: string, options: Omit<CreatePiAgentFromDefinitionOptions, "observer"> & {
|
|
159
|
+
models?: AgentModels;
|
|
154
160
|
}): Promise<{
|
|
155
161
|
assembly: PiAssembly;
|
|
156
162
|
definition: LoadedDefinition;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { toUSVString } from "node:util";
|
|
6
6
|
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
|
|
7
7
|
import { createCodingTools, createPowerShellTool, createReadOnlyTools, } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import {
|
|
8
|
+
import { resolveModel } from "./config.js";
|
|
9
9
|
import { isAgentcoreRuntime, isDeployedWorkspace } from "../../paths.js";
|
|
10
10
|
import { loadAgentDefinition, loadExtensionPaths } from "./definition.js";
|
|
11
11
|
import { reportFindingsIfChanged } from "./report.js";
|
|
@@ -14,7 +14,8 @@ import { readSecretDeclaration } from "../../declared-secrets.js";
|
|
|
14
14
|
import { withSearchTool } from "./search-tools.js";
|
|
15
15
|
import { createPiAgentFromSession } from "./invoke-session.js";
|
|
16
16
|
import { piAgentSessionFactory } from "./agent-session-factory.js";
|
|
17
|
-
import { DEFAULT_THINKING_LEVEL
|
|
17
|
+
import { DEFAULT_THINKING_LEVEL } from "./models.js";
|
|
18
|
+
import { agentModels } from "./agent-models.js";
|
|
18
19
|
import { piInMemorySessionRecordStore } from "./session-store.js";
|
|
19
20
|
import { inProcessLease } from "./turn-kit.js";
|
|
20
21
|
// ── §1 tools ─────────────────────────────────────────────────────────────────
|
|
@@ -188,18 +189,20 @@ function assemblePi(opts) {
|
|
|
188
189
|
const lease = opts.lease ?? inProcessLease();
|
|
189
190
|
const sessions = opts.sessions ?? piInMemorySessionRecordStore({ cwd });
|
|
190
191
|
// The model and its runtime resolve on FIRST USE.
|
|
191
|
-
let
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
// The caller's registry when there is one — the directory rung builds it from the agent's own models.json, so a
|
|
195
|
-
// custom endpoint declared there is the one a turn resolves against.
|
|
196
|
-
const modelRuntime = opts.models ?? (await createPiModelRuntime({ auth: opts.auth }));
|
|
192
|
+
let registry;
|
|
193
|
+
const modelRuntime = () => {
|
|
194
|
+
registry ??= opts.models().then((runtime) => {
|
|
197
195
|
// ModelRuntime registers providers by config record, so an injected Provider INSTANCE (a gateway, a self-hosted
|
|
198
196
|
// endpoint, a test fake) goes in through its native seam.
|
|
199
197
|
for (const provider of opts.providers ?? [])
|
|
200
|
-
|
|
201
|
-
return
|
|
202
|
-
})
|
|
198
|
+
runtime.registerNativeProvider(provider);
|
|
199
|
+
return runtime;
|
|
200
|
+
});
|
|
201
|
+
return registry;
|
|
202
|
+
};
|
|
203
|
+
let engine;
|
|
204
|
+
const resolveEngine = () => {
|
|
205
|
+
engine ??= modelRuntime().then((runtime) => ({ modelRuntime: runtime, model: resolveModel(runtime, opts.model) }));
|
|
203
206
|
return engine;
|
|
204
207
|
};
|
|
205
208
|
const sessionFactory = piAgentSessionFactory({
|
|
@@ -217,8 +220,11 @@ function assemblePi(opts) {
|
|
|
217
220
|
return {
|
|
218
221
|
lease,
|
|
219
222
|
sessionFactory,
|
|
223
|
+
modelRuntime,
|
|
220
224
|
engine: resolveEngine,
|
|
221
225
|
thinkingLevel: opts.thinkingLevel ?? DEFAULT_THINKING_LEVEL,
|
|
226
|
+
tools: opts.tools ?? [],
|
|
227
|
+
readDefinition: opts.readDefinition,
|
|
222
228
|
extensionPaths: opts.extensionPaths ?? [],
|
|
223
229
|
};
|
|
224
230
|
}
|
|
@@ -233,7 +239,7 @@ export function createPiAgent(options) {
|
|
|
233
239
|
model: options.model,
|
|
234
240
|
thinkingLevel: options.thinkingLevel,
|
|
235
241
|
providers: options.providers,
|
|
236
|
-
|
|
242
|
+
models: agentModels(undefined, options).runtime,
|
|
237
243
|
readDefinition: () => ({
|
|
238
244
|
systemPrompt: typeof instructions === "function" ? instructions() : instructions,
|
|
239
245
|
skills,
|
|
@@ -246,18 +252,11 @@ export function createPiAgent(options) {
|
|
|
246
252
|
}), options.observer);
|
|
247
253
|
}
|
|
248
254
|
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* The directory rung assembles from it and `availableModelsFromDir` lists from it, so a layer added here reaches both.
|
|
255
|
+
* L2, as the value: load the directory (base + AGENTS.md + skills + env) and assemble. `models` is the directory's
|
|
256
|
+
* model environment when the caller already built it (the opener, whose report must describe what runs); otherwise it
|
|
257
|
+
* is built here from the credential options.
|
|
253
258
|
*/
|
|
254
|
-
export function
|
|
255
|
-
return createPiModelRuntime({ agentDir: dir, ...options });
|
|
256
|
-
}
|
|
257
|
-
/** L2, as the value: load the directory (base + AGENTS.md + skills + env) and assemble. */
|
|
258
|
-
export async function assemblePiFromDefinition(dir,
|
|
259
|
-
/** `auth` is the opener's already-resolved layers, which take the place of `authPath`. */
|
|
260
|
-
options) {
|
|
259
|
+
export async function assemblePiFromDefinition(dir, options) {
|
|
261
260
|
// `dir` = the agent-definition dir (persona.md/skills/); `cwd` (default = dir) is the run root where tools operate
|
|
262
261
|
// and whose ancestors are walked for ② context.
|
|
263
262
|
const cwd = options.cwd ?? dir;
|
|
@@ -271,14 +270,15 @@ options) {
|
|
|
271
270
|
// Boot findings go through the SAME memoized reporter every later reader uses (report.ts, keyed by the resolved
|
|
272
271
|
// dir).
|
|
273
272
|
reportFindingsIfChanged(definition.dir, definition);
|
|
274
|
-
const
|
|
273
|
+
const { providers } = options;
|
|
274
|
+
const models = options.models ?? agentModels(dir, options, providers ? { providers } : {});
|
|
275
|
+
// Built at boot, so a malformed models.json fails the assembly rather than its first turn. The directory's own
|
|
276
|
+
// models.json is what a turn resolves against, layered over the machine's (models.ts).
|
|
277
|
+
await models.runtime();
|
|
275
278
|
const assembly = assemblePi({
|
|
276
279
|
model: options.model,
|
|
277
280
|
thinkingLevel: options.thinkingLevel,
|
|
278
|
-
models:
|
|
279
|
-
auth,
|
|
280
|
-
...(options.providers ? { providers: options.providers } : {}),
|
|
281
|
-
}),
|
|
281
|
+
models: models.runtime,
|
|
282
282
|
// The directory is the agent, LIVE: re-read the definition on every invoke, so AGENTS.md/skills edits (the
|
|
283
283
|
// author's, or the agent's own self-modification) take effect on the next turn with no process restart — restarts
|
|
284
284
|
// are reserved for code (tools/channels/config, module cache).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ImageRef } from "../../agent.ts";
|
|
3
|
+
import type { EntryImage } from "../../session.ts";
|
|
4
|
+
type Message = {
|
|
5
|
+
role: string;
|
|
6
|
+
content?: unknown;
|
|
7
|
+
};
|
|
8
|
+
/** `data.images` of the entry `entryId` holding `message`, absent when it has none. */
|
|
9
|
+
export declare function entryImages(entryId: string, message: Message): {
|
|
10
|
+
images?: EntryImage[];
|
|
11
|
+
};
|
|
12
|
+
/** The image `ref` names in `record`, or undefined when it names none there. The inverse of `entryImages`. */
|
|
13
|
+
export declare function imageAt(record: SessionManager, ref: string): ImageRef | undefined;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** A user prompt's and a tool result's images; no other message publishes any, so no ref can reach them. */
|
|
2
|
+
function publishedImages(message) {
|
|
3
|
+
if (message.role !== "user" && message.role !== "toolResult")
|
|
4
|
+
return [];
|
|
5
|
+
const content = message.content;
|
|
6
|
+
return Array.isArray(content) ? content.filter((b) => b.type === "image") : [];
|
|
7
|
+
}
|
|
8
|
+
/** `data.images` of the entry `entryId` holding `message`, absent when it has none. */
|
|
9
|
+
export function entryImages(entryId, message) {
|
|
10
|
+
const images = publishedImages(message).map((b, i) => ({ ref: `${entryId}:${i}`, mimeType: b.mimeType }));
|
|
11
|
+
return images.length > 0 ? { images } : {};
|
|
12
|
+
}
|
|
13
|
+
/** The image `ref` names in `record`, or undefined when it names none there. The inverse of `entryImages`. */
|
|
14
|
+
export function imageAt(record, ref) {
|
|
15
|
+
const at = ref.lastIndexOf(":");
|
|
16
|
+
const index = ref.slice(at + 1);
|
|
17
|
+
// Canonical digits only, so one image has one ref (`:00` is not `:0`).
|
|
18
|
+
if (at <= 0 || !/^(0|[1-9]\d*)$/.test(index))
|
|
19
|
+
return undefined;
|
|
20
|
+
const entry = record.getEntry(ref.slice(0, at));
|
|
21
|
+
const image = entry?.type === "message" ? publishedImages(entry.message)[Number(index)] : undefined;
|
|
22
|
+
return image ? { data: image.data, mimeType: image.mimeType } : undefined;
|
|
23
|
+
}
|