@fastagent-sh/fastagent 0.25.0 → 0.26.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/README.md +3 -2
- package/dist/agent-api.d.ts +10 -2
- package/dist/agent-api.js +10 -2
- package/dist/authoring.d.ts +12 -8
- package/dist/authoring.js +14 -7
- package/dist/cli/add-feishu.js +1 -1
- package/dist/cli/add-slack.js +1 -1
- package/dist/cli/box-login.d.ts +3 -2
- package/dist/cli/commands/add.js +2 -1
- package/dist/cli/commands/deploy.js +1 -1
- package/dist/cli/commands/env.js +5 -4
- package/dist/cli/commands/init.js +3 -3
- package/dist/cli/commands/invoke.js +2 -1
- package/dist/cli/commands/login.js +6 -4
- package/dist/cli/login-relay.d.ts +4 -3
- package/dist/cli/login-relay.js +1 -1
- package/dist/cli/shared.js +5 -3
- package/dist/content/git.d.ts +1 -1
- package/dist/content/git.js +7 -4
- package/dist/content/resolve.d.ts +1 -1
- package/dist/content/resolve.js +2 -2
- package/dist/deploy/preflight.js +1 -1
- package/dist/dev-supervisor.js +1 -1
- package/dist/env.d.ts +0 -13
- package/dist/env.js +1 -44
- package/dist/environment/mise.d.ts +8 -7
- package/dist/environment/mise.js +24 -20
- package/dist/git.d.ts +2 -2
- package/dist/git.js +5 -5
- package/dist/harnesses/models/environment.d.ts +7 -2
- package/dist/harnesses/models/environment.js +16 -10
- package/dist/harnesses/models/host.d.ts +30 -0
- package/dist/harnesses/models/host.js +97 -0
- package/dist/harnesses/models/login.d.ts +38 -73
- package/dist/harnesses/models/login.js +50 -50
- package/dist/harnesses/models/models.d.ts +4 -3
- package/dist/harnesses/models/models.js +51 -12
- package/dist/harnesses/pi-durable/agent.js +2 -0
- package/dist/harnesses/pi-durable/open.d.ts +7 -0
- package/dist/harnesses/pi-durable/open.js +4 -3
- package/dist/harnesses/pi-durable/serve.d.ts +11 -1
- package/dist/harnesses/pi-durable/serve.js +42 -12
- package/dist/harnesses/pi-durable/wake.d.ts +8 -1
- package/dist/harnesses/pi-durable/wake.js +10 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -0
- package/dist/scaffold/add-channel.js +1 -1
- package/dist/sign-in-api.d.ts +147 -0
- package/dist/sign-in-api.js +9 -0
- package/dist/tunnel.js +2 -1
- package/dist/value-file.d.ts +19 -0
- package/dist/value-file.js +61 -5
- package/package.json +1 -1
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Signing in to a MODEL PROVIDER, for any front end: {@link
|
|
2
|
+
* Signing in to a MODEL PROVIDER, for any front end: {@link loginOver} runs one provider's flow over pi-ai's own
|
|
3
3
|
* `AuthInteraction` and persists the credential through the same {@link fastagentCredentialStore} the runtime reads
|
|
4
|
-
* (one writer, one lock/corruption semantics); {@link
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* (one writer, one lock/corruption semantics); {@link loginOptionsOver} is the list a picker offers. An embedding host
|
|
5
|
+
* reaches both through host.ts (`signIn`, `signInOptions`); `fastagent login` is a front end of its own
|
|
6
|
+
* ({@link loginFlow}: terminal menus, then {@link loginOver}); `login --deployment` drives the same flow on a deployed
|
|
7
|
+
* box over a relay (cli/login-relay.ts).
|
|
7
8
|
*/
|
|
8
9
|
import { InMemoryCredentialStore, } from "@earendil-works/pi-ai";
|
|
9
10
|
import { randomUUID } from "node:crypto";
|
|
10
11
|
import { mkdir, readFile } from "node:fs/promises";
|
|
11
12
|
import { dirname, join } from "node:path";
|
|
12
13
|
import { writeFileAtomic } from "../../atomic-write.js";
|
|
14
|
+
import { SignInCancelled } from "../../sign-in-api.js";
|
|
13
15
|
import { assertOneCredentialSource, fastagentCredentialStore } from "./credentials.js";
|
|
14
16
|
import { UNNAMED_AUTH_SOURCE, interactiveAuth, loginProviders, modelsOver, probeApiKey } from "./models.js";
|
|
15
|
-
/** The user backed out of a prompt/menu, or the flow was aborted — a decision, not a failure. */
|
|
16
|
-
export class LoginCancelled extends Error {
|
|
17
|
-
}
|
|
18
17
|
/** A sign-in that cannot start as asked: an unknown provider, or a choice a caller without menus did not make. */
|
|
19
18
|
export class LoginRefused extends Error {
|
|
20
19
|
/** The sign-ins it could be instead, for a caller to choose from. */
|
|
@@ -26,17 +25,13 @@ export class LoginRefused extends Error {
|
|
|
26
25
|
}
|
|
27
26
|
const ACCOUNT_LABEL = "Sign in with an account";
|
|
28
27
|
const API_KEY_LABEL = "Sign in with an API key";
|
|
29
|
-
/**
|
|
30
|
-
* Every sign-in of pi's built-in providers, sorted by name as pi's `/login` lists them, with what authenticates each
|
|
31
|
-
* one here: a credential `credentials` (a credentials file, or the caller's own store) holds, else this process's
|
|
32
|
-
* environment, which a stored credential outranks.
|
|
33
|
-
*/
|
|
34
|
-
export function loginOptions(credentials) {
|
|
35
|
-
return loginOptionsOver(credentials);
|
|
36
|
-
}
|
|
37
28
|
/** A credentials file path names fastagent's store over it; a store is used as given. */
|
|
38
29
|
const storeOf = (credentials) => typeof credentials === "string" ? fastagentCredentialStore(credentials) : credentials;
|
|
39
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Every sign-in of pi's built-in providers plus `providers` (a same id replaces one), sorted by name as pi's `/login`
|
|
32
|
+
* lists them, with what authenticates each one here: a credential `credentials` (a credentials file, or a store) holds,
|
|
33
|
+
* else this process's environment, which a stored credential outranks.
|
|
34
|
+
*/
|
|
40
35
|
export async function loginOptionsOver(credentials, providers) {
|
|
41
36
|
// ONE read of the store: `list` is metadata only, and reading per provider would repeat a corrupt file's warning for
|
|
42
37
|
// every provider.
|
|
@@ -49,21 +44,17 @@ export async function loginOptionsOver(credentials, providers) {
|
|
|
49
44
|
if (methods.length === 0)
|
|
50
45
|
continue;
|
|
51
46
|
const stored = held.get(provider.id);
|
|
52
|
-
let
|
|
47
|
+
let ambient;
|
|
53
48
|
let problem;
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
try {
|
|
50
|
+
const found = await environment.checkAuth(provider.id);
|
|
51
|
+
if (found !== undefined)
|
|
52
|
+
ambient = { type: found.type, source: found.source ?? UNNAMED_AUTH_SOURCE };
|
|
56
53
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const found = await environment.checkAuth(provider.id);
|
|
60
|
-
if (found !== undefined)
|
|
61
|
-
status = { type: found.type, source: found.source ?? UNNAMED_AUTH_SOURCE };
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
problem = error.message;
|
|
65
|
-
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
problem = error.message;
|
|
66
56
|
}
|
|
57
|
+
const status = stored === undefined ? ambient : { type: stored, source: stored === "oauth" ? "OAuth" : "stored credential" };
|
|
67
58
|
for (const method of methods) {
|
|
68
59
|
offered.push({
|
|
69
60
|
provider: provider.id,
|
|
@@ -73,6 +64,7 @@ export async function loginOptionsOver(credentials, providers) {
|
|
|
73
64
|
subscription: method === "oauth" && provider.auth.oauth?.isSubscription === true,
|
|
74
65
|
external: interactiveAuth(provider, method) === undefined,
|
|
75
66
|
...(status ? { status } : {}),
|
|
67
|
+
...(ambient ? { environment: ambient.source } : {}),
|
|
76
68
|
...(problem ? { problem } : {}),
|
|
77
69
|
});
|
|
78
70
|
}
|
|
@@ -87,11 +79,10 @@ function credentialKind(type, subscription) {
|
|
|
87
79
|
}
|
|
88
80
|
/** An option's status as pi's picker writes it after the provider's name. */
|
|
89
81
|
export function loginStatusText(option) {
|
|
90
|
-
if (option.problem !== undefined)
|
|
91
|
-
return `auth check failed: ${option.problem}`;
|
|
92
82
|
const { status } = option;
|
|
83
|
+
// A stored credential outranks the environment, so a failed check of the environment does not decide it.
|
|
93
84
|
if (status === undefined)
|
|
94
|
-
return "not configured"
|
|
85
|
+
return option.problem === undefined ? "not configured" : `auth check failed: ${option.problem}`;
|
|
95
86
|
if (status.type !== option.method)
|
|
96
87
|
return `${credentialKind(status.type, option.subscription)} configured`;
|
|
97
88
|
if (status.source === "OAuth" || status.source === "stored credential")
|
|
@@ -106,8 +97,8 @@ function anySignal(...signals) {
|
|
|
106
97
|
return present.length === 0 ? undefined : present.length === 1 ? present[0] : AbortSignal.any(present);
|
|
107
98
|
}
|
|
108
99
|
/**
|
|
109
|
-
* Sign in to one of pi's built-in providers and persist the credential to `authPath`, or to
|
|
110
|
-
* `credentialStore`.
|
|
100
|
+
* Sign in to one of pi's built-in providers (plus the definition's) and persist the credential to `authPath`, or to
|
|
101
|
+
* the caller's `credentialStore`.
|
|
111
102
|
*
|
|
112
103
|
* The store is checked FIRST (a no-op write runs the file store's refuse-corrupt and writability checks, or whatever
|
|
113
104
|
* the caller's store does on a write), so a flow never runs toward a credential that could not be saved. An entered API key is verified with one minimal request to pi's
|
|
@@ -116,14 +107,11 @@ function anySignal(...signals) {
|
|
|
116
107
|
* the provider refuse the key, HTTP 401); a key the provider rejects is never stored, and the provider's key flow runs
|
|
117
108
|
* again (the provider and method were not the mistake). Progress and outcome go out through `interaction.notify`.
|
|
118
109
|
*
|
|
119
|
-
* Aborting `interaction.signal` at any point, the verification included, rejects with {@link
|
|
120
|
-
* writes no credential (an OAuth device ID, the installation's identity, is kept once created). Every prompt carries a
|
|
121
|
-
* beating a `manual_code` prompt), and when the
|
|
110
|
+
* Aborting `interaction.signal` at any point, the verification included, rejects with {@link SignInCancelled} and
|
|
111
|
+
* writes no credential (an OAuth device ID, the installation's identity, is kept once created). Every prompt carries a
|
|
112
|
+
* signal that aborts on it, on the prompt's own signal (a callback server beating a `manual_code` prompt), and when the
|
|
113
|
+
* flow settles with a prompt still pending.
|
|
122
114
|
*/
|
|
123
|
-
export function login(request) {
|
|
124
|
-
return loginOver(request);
|
|
125
|
-
}
|
|
126
|
-
/** {@link login}, plus {@link LoginInternals}. */
|
|
127
115
|
export async function loginOver(request, internals = {}) {
|
|
128
116
|
const { providers, verifyWith } = internals;
|
|
129
117
|
const { method, interaction } = request;
|
|
@@ -158,7 +146,7 @@ export async function loginOver(request, internals = {}) {
|
|
|
158
146
|
await trial.modify(provider.id, async () => credential);
|
|
159
147
|
const verdict = await verifyApiKey(models, provider.id, verifyWith, notify, signal);
|
|
160
148
|
if (signal.aborted)
|
|
161
|
-
throw new
|
|
149
|
+
throw new SignInCancelled("cancelled");
|
|
162
150
|
if (verdict === "rejected")
|
|
163
151
|
continue;
|
|
164
152
|
verified = verdict;
|
|
@@ -202,20 +190,32 @@ async function readOrCreateDeviceId(path) {
|
|
|
202
190
|
}
|
|
203
191
|
async function runFlow(flow, interaction, signal) {
|
|
204
192
|
if (signal.aborted)
|
|
205
|
-
throw new
|
|
193
|
+
throw new SignInCancelled("cancelled");
|
|
206
194
|
const done = new AbortController();
|
|
195
|
+
let backedOut;
|
|
207
196
|
let credential;
|
|
208
197
|
try {
|
|
209
198
|
credential = await flow({
|
|
210
199
|
signal,
|
|
211
|
-
prompt: (prompt) =>
|
|
200
|
+
prompt: (prompt) => {
|
|
201
|
+
const asked = anySignal(prompt.signal, signal, done.signal);
|
|
202
|
+
return interaction.prompt({ ...prompt, signal: asked }).catch((error) => {
|
|
203
|
+
// Remembered here: a provider may wrap the rejection it is handed in an error of its own. Only a question
|
|
204
|
+
// still asked: one the flow withdrew (its callback came first) rejects too, and nobody backed out of it.
|
|
205
|
+
if (error instanceof SignInCancelled && !asked?.aborted)
|
|
206
|
+
backedOut = error;
|
|
207
|
+
throw error;
|
|
208
|
+
});
|
|
209
|
+
},
|
|
212
210
|
notify: (event) => interaction.notify(event),
|
|
213
211
|
});
|
|
214
212
|
}
|
|
215
213
|
catch (error) {
|
|
216
214
|
// Whatever the provider made of the abort (its own error, a rejected prompt), it was the caller's decision.
|
|
217
215
|
if (signal.aborted)
|
|
218
|
-
throw new
|
|
216
|
+
throw new SignInCancelled("cancelled");
|
|
217
|
+
if (backedOut !== undefined)
|
|
218
|
+
throw backedOut;
|
|
219
219
|
throw error;
|
|
220
220
|
}
|
|
221
221
|
finally {
|
|
@@ -223,7 +223,7 @@ async function runFlow(flow, interaction, signal) {
|
|
|
223
223
|
}
|
|
224
224
|
// A provider that ignored the abort and finished anyway: the caller still cancelled.
|
|
225
225
|
if (signal.aborted)
|
|
226
|
-
throw new
|
|
226
|
+
throw new SignInCancelled("cancelled");
|
|
227
227
|
return credential;
|
|
228
228
|
}
|
|
229
229
|
/**
|
|
@@ -314,7 +314,7 @@ async function verifyApiKey(models, providerId, verifyWith, notify, signal) {
|
|
|
314
314
|
}
|
|
315
315
|
return probe.state;
|
|
316
316
|
}
|
|
317
|
-
/** {@link LoginIO} as pi-ai's `AuthInteraction`: what {@link
|
|
317
|
+
/** {@link LoginIO} as pi-ai's `AuthInteraction`: what {@link loginOver} drives for the terminal. */
|
|
318
318
|
function terminalInteraction(io, signal) {
|
|
319
319
|
return {
|
|
320
320
|
...(signal ? { signal } : {}),
|
|
@@ -332,7 +332,7 @@ function terminalInteraction(io, signal) {
|
|
|
332
332
|
...(p.signal ? { signal: p.signal } : {}),
|
|
333
333
|
});
|
|
334
334
|
if (v === undefined)
|
|
335
|
-
throw new
|
|
335
|
+
throw new SignInCancelled("cancelled");
|
|
336
336
|
return v;
|
|
337
337
|
},
|
|
338
338
|
notify: (e) => {
|
|
@@ -354,7 +354,7 @@ function terminalInteraction(io, signal) {
|
|
|
354
354
|
}
|
|
355
355
|
/**
|
|
356
356
|
* `fastagent login` in a terminal: ask only what was not given, as pi's `/login` asks it (how to sign in, then the
|
|
357
|
-
* provider; a provider named by its id or its name, then how, when it offers both), then {@link
|
|
357
|
+
* provider; a provider named by its id or its name, then how, when it offers both), then {@link loginOver}.
|
|
358
358
|
*/
|
|
359
359
|
export async function loginFlow(io, options) {
|
|
360
360
|
const offered = await loginOptionsOver(options.statusFrom ?? options.authPath, options.providers);
|
|
@@ -383,7 +383,7 @@ export async function chooseLogin(offered, given, menus) {
|
|
|
383
383
|
const value = await menus.select(message, options);
|
|
384
384
|
const option = from.find((o) => by(o) === value);
|
|
385
385
|
if (option === undefined)
|
|
386
|
-
throw new
|
|
386
|
+
throw new SignInCancelled("nothing selected");
|
|
387
387
|
return option;
|
|
388
388
|
};
|
|
389
389
|
if (given.provider !== undefined) {
|
|
@@ -420,7 +420,7 @@ export async function chooseLogin(offered, given, menus) {
|
|
|
420
420
|
if (way === "radius" && radius)
|
|
421
421
|
return radius;
|
|
422
422
|
if (way !== "oauth" && way !== "api_key")
|
|
423
|
-
throw new
|
|
423
|
+
throw new SignInCancelled("no authentication method selected");
|
|
424
424
|
method = way;
|
|
425
425
|
}
|
|
426
426
|
const candidates = offered.filter((o) => o.method === method);
|
|
@@ -11,10 +11,11 @@ export declare function providerOf(spec: string): string;
|
|
|
11
11
|
/** Every chat model `models` offers, as `"provider/modelId"`, sorted. */
|
|
12
12
|
export declare function listModels(models: Models): string[];
|
|
13
13
|
/**
|
|
14
|
-
* The built-ins plus `providers` (a same id replaces a built-in), over any credential store
|
|
15
|
-
*
|
|
14
|
+
* The built-ins plus `providers` (a same id replaces a built-in), over any credential store, in `environment`: what
|
|
15
|
+
* the agent's environment adds to this process's, read before it by a provider's auth and by each request. With
|
|
16
|
+
* `catalogs`, the lists its refresh brings up to date; without, the ones this pi-ai was built with.
|
|
16
17
|
*/
|
|
17
|
-
export declare function modelsOver(credentials: CredentialStore, providers?: readonly Provider[], catalogs?: Catalogs): Models;
|
|
18
|
+
export declare function modelsOver(credentials: CredentialStore, providers?: readonly Provider[], catalogs?: Catalogs, environment?: Readonly<Record<string, string>>): Models;
|
|
18
19
|
/**
|
|
19
20
|
* Whether `env` alone satisfies a provider, by pi's own check, and with what when the provider says (its API-key
|
|
20
21
|
* variable, or an ambient source such as an AWS profile); undefined when it does not. A provider in code may
|
|
@@ -21,19 +21,62 @@ export function listModels(models) {
|
|
|
21
21
|
return specs.sort();
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
* its
|
|
24
|
+
* pi's own reading of an environment (a blank variable is unset) through `read` instead of this process's: an agent's
|
|
25
|
+
* environment is its own when the process runs several. Files a source points at are this machine's unless
|
|
26
|
+
* `fileExists` says otherwise.
|
|
26
27
|
*/
|
|
27
|
-
|
|
28
|
+
function authContextIn(read, fileExists = defaultProviderAuthContext().fileExists) {
|
|
29
|
+
return {
|
|
30
|
+
env: async (name) => {
|
|
31
|
+
const value = read(name);
|
|
32
|
+
return value?.trim() ? value : undefined;
|
|
33
|
+
},
|
|
34
|
+
fileExists,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `provider` with `environment` given to each request, as its auth reads it: pi resolves a provider's key from the
|
|
39
|
+
* auth context, but a request reads its region, endpoint or ambient key (Bedrock, Azure, Vertex) from the request's
|
|
40
|
+
* own `env`, which it otherwise takes from this process.
|
|
41
|
+
*/
|
|
42
|
+
function inEnvironment(provider, environment) {
|
|
43
|
+
const withEnv = (options) => ({ ...options, env: { ...environment, ...options?.env } });
|
|
44
|
+
const { fetchDeferred, cancelDeferred, generateImages, classify } = provider;
|
|
45
|
+
return {
|
|
46
|
+
...provider,
|
|
47
|
+
stream: (model, context, options) => provider.stream(model, context, withEnv(options)),
|
|
48
|
+
streamSimple: (model, context, options) => provider.streamSimple(model, context, withEnv(options)),
|
|
49
|
+
...(fetchDeferred && {
|
|
50
|
+
fetchDeferred: (model, handle, options) => fetchDeferred.call(provider, model, handle, withEnv(options)),
|
|
51
|
+
}),
|
|
52
|
+
...(cancelDeferred && {
|
|
53
|
+
cancelDeferred: (model, handle, options) => cancelDeferred.call(provider, model, handle, withEnv(options)),
|
|
54
|
+
}),
|
|
55
|
+
...(generateImages && {
|
|
56
|
+
generateImages: (model, context, options) => generateImages.call(provider, model, context, withEnv(options)),
|
|
57
|
+
}),
|
|
58
|
+
...(classify && {
|
|
59
|
+
classify: (model, context, options) => classify.call(provider, model, context, withEnv(options)),
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The built-ins plus `providers` (a same id replaces a built-in), over any credential store, in `environment`: what
|
|
65
|
+
* the agent's environment adds to this process's, read before it by a provider's auth and by each request. With
|
|
66
|
+
* `catalogs`, the lists its refresh brings up to date; without, the ones this pi-ai was built with.
|
|
67
|
+
*/
|
|
68
|
+
export function modelsOver(credentials, providers = [], catalogs, environment = {}) {
|
|
28
69
|
const models = createModels({
|
|
29
70
|
credentials,
|
|
30
|
-
|
|
71
|
+
// As a request reads it (pi's getProviderEnvValue): the request's env first, then this process's.
|
|
72
|
+
authContext: authContextIn((name) => environment[name] || process.env[name]),
|
|
31
73
|
...(catalogs ? { modelsStore: catalogs.fetched } : {}),
|
|
32
74
|
});
|
|
33
|
-
for (const builtIn of builtinProviders())
|
|
34
|
-
models.setProvider(catalogs ? catalogs.over(builtIn) : builtIn);
|
|
75
|
+
for (const builtIn of builtinProviders()) {
|
|
76
|
+
models.setProvider(inEnvironment(catalogs ? catalogs.over(builtIn) : builtIn, environment));
|
|
77
|
+
}
|
|
35
78
|
for (const provider of providers)
|
|
36
|
-
models.setProvider(provider);
|
|
79
|
+
models.setProvider(inEnvironment(provider, environment));
|
|
37
80
|
return models;
|
|
38
81
|
}
|
|
39
82
|
/**
|
|
@@ -51,11 +94,7 @@ export function modelsOver(credentials, providers = [], catalogs) {
|
|
|
51
94
|
export async function environmentAuth(providerId, env, fileExists = defaultProviderAuthContext().fileExists, providers = []) {
|
|
52
95
|
const models = builtinModels({
|
|
53
96
|
credentials: new InMemoryCredentialStore(),
|
|
54
|
-
authContext:
|
|
55
|
-
// pi's own reading of a variable (a blank one is unset), over the given environment.
|
|
56
|
-
env: async (name) => (env[name]?.trim() ? env[name] : undefined),
|
|
57
|
-
fileExists,
|
|
58
|
-
},
|
|
97
|
+
authContext: authContextIn((name) => env[name], fileExists),
|
|
59
98
|
});
|
|
60
99
|
for (const provider of providers)
|
|
61
100
|
models.setProvider(provider);
|
|
@@ -177,6 +177,8 @@ function durableAgent(harness, ids, sessions, lifetime, options) {
|
|
|
177
177
|
provider: model.provider,
|
|
178
178
|
modelId: model.id,
|
|
179
179
|
...(model.name ? { name: model.name } : {}),
|
|
180
|
+
// pi reads a window a model does not declare (a virtual model's, before its first answer) as 0.
|
|
181
|
+
...(model.contextWindow > 0 ? { contextWindow: model.contextWindow } : {}),
|
|
180
182
|
})),
|
|
181
183
|
...(options.model === undefined
|
|
182
184
|
? {}
|
|
@@ -82,7 +82,14 @@ export interface AssembleAgentOptions {
|
|
|
82
82
|
settings?: HarnessSettings;
|
|
83
83
|
/** Where the instance's state is: given by a serve, whose scheduler fires the wake-ups `wake` keeps there. */
|
|
84
84
|
stateRoot?: string;
|
|
85
|
+
/** Whether that scheduler runs now: an app's `openAgent` runs one only while it serves. Default always. */
|
|
86
|
+
wakeTicking?: () => boolean;
|
|
85
87
|
unfinished?: Unfinished;
|
|
88
|
+
/**
|
|
89
|
+
* What the agent's environment adds to this process's, for the commands it runs: its value file's variables and its
|
|
90
|
+
* packages' PATH.
|
|
91
|
+
*/
|
|
92
|
+
environment?: Readonly<Record<string, string>>;
|
|
86
93
|
}
|
|
87
94
|
/** Open the definition as an Agent over `models`: the coding tools, then its own. */
|
|
88
95
|
export declare function assembleAgent(definition: AgentDefinition, options: AssembleAgentOptions): Promise<DurableAgent>;
|
|
@@ -309,8 +309,9 @@ export async function assembleAgent(definition, options) {
|
|
|
309
309
|
}));
|
|
310
310
|
for (const extension of code.extensions)
|
|
311
311
|
registry.install(extension);
|
|
312
|
-
if (options.stateRoot !== undefined && !ownWake)
|
|
313
|
-
registry.install(wakeExtension(options.stateRoot));
|
|
312
|
+
if (options.stateRoot !== undefined && !ownWake) {
|
|
313
|
+
registry.install(wakeExtension(options.stateRoot, options.wakeTicking === undefined ? {} : { ticking: options.wakeTicking }));
|
|
314
|
+
}
|
|
314
315
|
const thinkingLevel = options.thinkingLevel ?? config.thinkingLevel;
|
|
315
316
|
return openDurableAgent({
|
|
316
317
|
storage: options.storage,
|
|
@@ -323,7 +324,7 @@ export async function assembleAgent(definition, options) {
|
|
|
323
324
|
...(options.settings === undefined ? {} : { settings: options.settings }),
|
|
324
325
|
...(options.unfinished === undefined ? {} : { unfinished: options.unfinished }),
|
|
325
326
|
// What the agent runs is not `dev`'s own serving, even when `dev` is what runs the agent.
|
|
326
|
-
env: ({ cwd }) => new NodeExecutionEnv({ cwd: cwd ?? dir, shellEnv: { [DEV_SERVE_ENV]: undefined } }),
|
|
327
|
+
env: ({ cwd }) => new NodeExecutionEnv({ cwd: cwd ?? dir, shellEnv: { ...options.environment, [DEV_SERVE_ENV]: undefined } }),
|
|
327
328
|
commands: () => readCommands(dir),
|
|
328
329
|
});
|
|
329
330
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
2
|
import { type Harness } from "@earendil-works/pi-durable";
|
|
3
3
|
import type { CredentialSourceOptions } from "../models/credentials.ts";
|
|
4
4
|
import { type ModelEnvironment } from "../models/environment.ts";
|
|
5
|
+
import type { CredentialStore } from "../../sign-in-api.ts";
|
|
5
6
|
import { type ResolvedContent } from "../../content/resolve.ts";
|
|
6
7
|
import type { Agent } from "../../agent-api.ts";
|
|
7
8
|
import { type Service } from "../../service.ts";
|
|
@@ -22,6 +23,10 @@ export interface OpenServingOptions extends CredentialSourceOptions {
|
|
|
22
23
|
storage?: ServingStorage;
|
|
23
24
|
/** What becomes of the messages an earlier process left unfinished in the storage. Default `"resume"`. */
|
|
24
25
|
unfinished?: Unfinished;
|
|
26
|
+
/** The instance's state root, over `FASTAGENT_STATE_DIR` and the directory's own. */
|
|
27
|
+
stateRoot?: string;
|
|
28
|
+
/** Whether a clock fires the instance's wake-ups now (`assembleAgent`'s `wakeTicking`). Default always. */
|
|
29
|
+
wakeTicking?: () => boolean;
|
|
25
30
|
}
|
|
26
31
|
type ServingStorage = {
|
|
27
32
|
instance: StorageLocking;
|
|
@@ -70,6 +75,11 @@ export interface OpenAgentOptions {
|
|
|
70
75
|
model?: string;
|
|
71
76
|
/** Where provider credentials are kept, instead of the agent's own `.secrets/auth.json` over the machine's. */
|
|
72
77
|
credentials?: CredentialStore;
|
|
78
|
+
/**
|
|
79
|
+
* Where this instance keeps its sessions, its channels' and schedules' state and its model lists, instead of
|
|
80
|
+
* `FASTAGENT_STATE_DIR` or the agent's `.state/`: a host's own instance of an agent beside `fastagent dev`'s.
|
|
81
|
+
*/
|
|
82
|
+
stateDir?: string;
|
|
73
83
|
}
|
|
74
84
|
/**
|
|
75
85
|
* Open the agent directory `dir` the way `dev` and `start` do before they serve: its definition, its code built with
|
|
@@ -8,11 +8,13 @@ import { join } from "node:path";
|
|
|
8
8
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
9
9
|
import { MemoryStorage } from "@earendil-works/pi-durable";
|
|
10
10
|
import { modelEnvironment } from "../models/environment.js";
|
|
11
|
+
import { piCredentials } from "../models/host.js";
|
|
11
12
|
import { loadContent } from "../../content/file.js";
|
|
12
13
|
import { absentNotice, cloneContent, contentAbsentHere, resolveContent, } from "../../content/resolve.js";
|
|
13
|
-
import {
|
|
14
|
+
import { agentValues } from "../../value-file.js";
|
|
15
|
+
import { prepareEnvironment } from "../../environment/mise.js";
|
|
14
16
|
import { log } from "../../log.js";
|
|
15
|
-
import { resolveStateRoot } from "../../paths.js";
|
|
17
|
+
import { resolveOverridePath, resolveStateRoot } from "../../paths.js";
|
|
16
18
|
import { serveAgent } from "../../service.js";
|
|
17
19
|
import { buildDefinition, assembleAgent, prepareDefinition, } from "./open.js";
|
|
18
20
|
import { openLockedStorage } from "./storage.js";
|
|
@@ -24,12 +26,12 @@ const INSTANCE_HOLDERS = "another `fastagent dev`, `start` or `schedules` comman
|
|
|
24
26
|
* up to date in place where git can do so without touching the agent's work, first; then resolved again as what is now
|
|
25
27
|
* on disk.
|
|
26
28
|
*/
|
|
27
|
-
async function contentForThisRun(dir) {
|
|
29
|
+
async function contentForThisRun(dir, env) {
|
|
28
30
|
const declaration = loadContent(dir);
|
|
29
31
|
for (const entry of resolveContent(dir, declaration)) {
|
|
30
32
|
if (entry.kind !== "github" || !entry.clone)
|
|
31
33
|
continue;
|
|
32
|
-
const done = await cloneContent(entry);
|
|
34
|
+
const done = await cloneContent(entry, env);
|
|
33
35
|
if (done.outcome === "kept") {
|
|
34
36
|
log.warn(`[fastagent] content "${entry.name}": the clone in ${entry.location} is kept as it is, ${done.reason}`);
|
|
35
37
|
}
|
|
@@ -67,27 +69,33 @@ function openStorage(stateRoot, storage) {
|
|
|
67
69
|
export async function openServing(directory, options = {}) {
|
|
68
70
|
const definition = await prepareDefinition(directory);
|
|
69
71
|
const { dir } = definition;
|
|
70
|
-
|
|
72
|
+
// The agent's environment is ONE value every reader below is given, and this process's is never written: a process
|
|
73
|
+
// may run several agents, each with its own values and packages.
|
|
74
|
+
const values = agentValues(dir);
|
|
75
|
+
const code = buildDefinition(definition, { ...process.env, ...values });
|
|
71
76
|
// The commands the agent runs, as `context.json`'s environment declares them, before anything can run one.
|
|
72
|
-
await
|
|
73
|
-
const
|
|
77
|
+
const environment = { ...values, ...(await prepareEnvironment(dir, { ...process.env, ...values })) };
|
|
78
|
+
const env = { ...process.env, ...environment };
|
|
79
|
+
const content = await contentForThisRun(dir, env);
|
|
80
|
+
const stateRoot = options.stateRoot ?? resolveStateRoot(dir, env);
|
|
74
81
|
const models = await modelEnvironment(dir, {
|
|
75
82
|
...(options.authPath === undefined ? {} : { authPath: options.authPath }),
|
|
76
83
|
...(options.credentialStore === undefined ? {} : { credentialStore: options.credentialStore }),
|
|
77
|
-
}, code.providers);
|
|
78
|
-
const stateRoot = resolveStateRoot(dir);
|
|
84
|
+
}, code.providers, { stateRoot, environment });
|
|
79
85
|
const chosen = options.storage ?? { instance: "posix" };
|
|
80
86
|
const storage = await openStorage(stateRoot, chosen);
|
|
81
87
|
const agent = await assembleAgent(definition, {
|
|
82
88
|
storage,
|
|
83
89
|
code,
|
|
84
90
|
content,
|
|
91
|
+
environment,
|
|
85
92
|
models: models.models,
|
|
86
93
|
...(models.catalogProblem === undefined ? {} : { catalogProblem: models.catalogProblem }),
|
|
87
94
|
...(typeof chosen === "object" && "instance" in chosen ? { stateRoot } : {}),
|
|
88
95
|
...(options.model === undefined ? {} : { model: options.model }),
|
|
89
96
|
...(options.thinkingLevel === undefined ? {} : { thinkingLevel: options.thinkingLevel }),
|
|
90
97
|
...(options.unfinished === undefined ? {} : { unfinished: options.unfinished }),
|
|
98
|
+
...(options.wakeTicking === undefined ? {} : { wakeTicking: options.wakeTicking }),
|
|
91
99
|
}).catch(async (error) => {
|
|
92
100
|
// An Agent that did not open does not own the storage, and its lock would refuse this process's next attempt.
|
|
93
101
|
await storage.close(BACKGROUND_CONTEXT);
|
|
@@ -105,15 +113,26 @@ export async function openServing(directory, options = {}) {
|
|
|
105
113
|
schedules: code.schedules,
|
|
106
114
|
};
|
|
107
115
|
}
|
|
116
|
+
/** `stateDir` read as `FASTAGENT_STATE_DIR` is: `~` is the home directory, a relative one resolves against the cwd. */
|
|
117
|
+
function stateDirOf(stateDir) {
|
|
118
|
+
const resolved = resolveOverridePath(stateDir);
|
|
119
|
+
if (resolved === undefined)
|
|
120
|
+
throw new Error("stateDir needs the directory the instance's state is kept in");
|
|
121
|
+
return resolved;
|
|
122
|
+
}
|
|
108
123
|
/**
|
|
109
124
|
* Open the agent directory `dir` the way `dev` and `start` do before they serve: its definition, its code built with
|
|
110
125
|
* the secrets it declares, its environment, its content and the instance's storage, which one process holds at a time.
|
|
111
126
|
* Nothing reaches the agent until `serve`.
|
|
112
127
|
*/
|
|
113
128
|
export async function openAgent(dir, options = {}) {
|
|
129
|
+
// An app may never serve the agent, and then no clock fires a wake-up: `wake` is refused until it does.
|
|
130
|
+
let ticking = false;
|
|
114
131
|
const opened = await openServing(dir, {
|
|
132
|
+
wakeTicking: () => ticking,
|
|
115
133
|
...(options.model === undefined ? {} : { model: options.model }),
|
|
116
|
-
...(options.credentials === undefined ? {} : { credentialStore: options.credentials }),
|
|
134
|
+
...(options.credentials === undefined ? {} : { credentialStore: piCredentials(options.credentials) }),
|
|
135
|
+
...(options.stateDir === undefined ? {} : { stateRoot: stateDirOf(options.stateDir) }),
|
|
117
136
|
});
|
|
118
137
|
const { agent } = opened;
|
|
119
138
|
let served;
|
|
@@ -130,10 +149,21 @@ export async function openAgent(dir, options = {}) {
|
|
|
130
149
|
return Promise.reject(new Error("this agent is closed"));
|
|
131
150
|
if (served !== undefined)
|
|
132
151
|
return Promise.reject(new Error("this agent is already served"));
|
|
133
|
-
const serving = serveAgent(opened, serveOptions)
|
|
152
|
+
const serving = serveAgent(opened, serveOptions).then((service) => ({
|
|
153
|
+
...service,
|
|
154
|
+
close: () => {
|
|
155
|
+
// Its clock stops with it, whichever closes it: a wake-up kept after this is one nothing fires.
|
|
156
|
+
ticking = false;
|
|
157
|
+
return service.close();
|
|
158
|
+
},
|
|
159
|
+
}));
|
|
134
160
|
served = serving;
|
|
161
|
+
serving.then(() => {
|
|
162
|
+
if (served === serving && closing === undefined)
|
|
163
|
+
ticking = true;
|
|
164
|
+
},
|
|
135
165
|
// A serve that failed to start rolled itself back, so the agent may be served again.
|
|
136
|
-
|
|
166
|
+
() => {
|
|
137
167
|
if (served === serving)
|
|
138
168
|
served = undefined;
|
|
139
169
|
});
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { type Extension } from "@earendil-works/pi-durable";
|
|
2
2
|
export declare const WAKE_EXTENSION = "fastagent.wake";
|
|
3
3
|
export declare const WAKE_TOOLS: string[];
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* `ticking` says whether a clock fires the wake-ups kept in `stateRoot` now; a wake-up nothing would fire is refused.
|
|
6
|
+
* Without it, one always does: `dev`, `start` and AgentCore serve whatever they open.
|
|
7
|
+
*/
|
|
8
|
+
export declare function wakeExtension(stateRoot: string, options?: {
|
|
9
|
+
now?: () => Date;
|
|
10
|
+
ticking?: () => boolean;
|
|
11
|
+
}): Extension;
|
|
@@ -12,7 +12,14 @@ export const WAKE_TOOLS = ["wake", "unwake"];
|
|
|
12
12
|
const said = (text) => ({ output: [{ type: "text", text }] });
|
|
13
13
|
const refused = (text) => ({ output: [{ type: "text", text }], isError: true });
|
|
14
14
|
const NOT_A_SESSION = "wake-ups belong to a session, and this conversation is not one (a subagent's): nothing would wake it.";
|
|
15
|
-
|
|
15
|
+
const NO_CLOCK = "nothing fires wake-ups for this agent now: the app that opened it does not serve it, so no clock runs. Do the " +
|
|
16
|
+
"work now, or say it cannot be scheduled.";
|
|
17
|
+
/**
|
|
18
|
+
* `ticking` says whether a clock fires the wake-ups kept in `stateRoot` now; a wake-up nothing would fire is refused.
|
|
19
|
+
* Without it, one always does: `dev`, `start` and AgentCore serve whatever they open.
|
|
20
|
+
*/
|
|
21
|
+
export function wakeExtension(stateRoot, options = {}) {
|
|
22
|
+
const now = options.now ?? (() => new Date());
|
|
16
23
|
return defineExtension({
|
|
17
24
|
name: WAKE_EXTENSION,
|
|
18
25
|
tools: [
|
|
@@ -37,6 +44,8 @@ export function wakeExtension(stateRoot, now = () => new Date()) {
|
|
|
37
44
|
}),
|
|
38
45
|
}),
|
|
39
46
|
execute: async (args, api, context) => {
|
|
47
|
+
if (options.ticking?.() === false)
|
|
48
|
+
return refused(NO_CLOCK);
|
|
40
49
|
if ((args.in === undefined) === (args.cron === undefined)) {
|
|
41
50
|
return refused("pass exactly one of `in` (one-shot) or `cron` (recurring).");
|
|
42
51
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The package's one entry (SPEC.md §8), besides each first-party channel's: the Agent API, opening an agent
|
|
3
|
-
* directory and serving it,
|
|
3
|
+
* directory and serving it, what a host embedding agents needs beside them (creating one, model credentials and
|
|
4
|
+
* sign-in), and what an author writes.
|
|
4
5
|
*/
|
|
5
6
|
export type { Agent, AgentErrorData, AgentInfo, CompactOptions, ContentPart, CreateSessionOptions, EntriesOptions, Entry, ErrorCode, Json, ListOptions, Message, MessageKey, MessageStatus, MessageStreamEvent, ModelRef, Page, SendRequest, Session, SessionChanges, SessionEvent, SessionSettings, SessionStatus, ThinkingLevel, Tokens, Usage, WaitOptions, } from "./agent-api.ts";
|
|
6
7
|
export { AgentError, LIMITS, hasEnded } from "./agent-api.ts";
|
|
7
8
|
export { openAgent, type LocalAgent, type OpenAgentOptions, type ServeAgentOptions, } from "./harnesses/pi-durable/serve.ts";
|
|
8
9
|
export type { Service } from "./service.ts";
|
|
9
|
-
|
|
10
|
-
export type
|
|
10
|
+
export { createAgent, type CreatedAgent } from "./authoring.ts";
|
|
11
|
+
export { SignInCancelled, type Credential, type CredentialStore, type ProviderAuth, type SignInEvent, type SignInInteraction, type SignInMethod, type SignInOption, type SignInPrompt, type SignInRequest, type SignInResult, } from "./sign-in-api.ts";
|
|
12
|
+
export { credentialFile, providerAuth, signIn, signInOptions } from "./harnesses/models/host.ts";
|
|
11
13
|
export { defineAgent, defineSchedule, needs, type AgentConfig, type Needs, type SecretSpec, type SecretValues, } from "./harnesses/pi-durable/define.ts";
|
|
12
14
|
export type { ScheduleConfig } from "./schedule/schedule.ts";
|
|
13
15
|
export { defineChannel, type ChannelContext, type ChannelHandler, type ChannelModule, type ForkSource, type LongConnection, type LongConnectionChannelModule, type Routes, } from "./channel.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { AgentError, LIMITS, hasEnded } from "./agent-api.js";
|
|
2
2
|
export { openAgent, } from "./harnesses/pi-durable/serve.js";
|
|
3
|
+
export { createAgent } from "./authoring.js";
|
|
4
|
+
export { SignInCancelled, } from "./sign-in-api.js";
|
|
5
|
+
export { credentialFile, providerAuth, signIn, signInOptions } from "./harnesses/models/host.js";
|
|
3
6
|
export { defineAgent, defineSchedule, needs, } from "./harnesses/pi-durable/define.js";
|
|
4
7
|
export { defineChannel, } from "./channel.js";
|
|
5
8
|
// What a channel reads a request with, answers with, and compares its platform's secret with (constant time).
|
|
@@ -8,7 +8,7 @@ import { detectRuntime } from "../runtime.js";
|
|
|
8
8
|
import { assertInsideAgentDir, exists } from "../paths.js";
|
|
9
9
|
import { channelBundleFiles, channelTemplate } from "./templates.js";
|
|
10
10
|
import { writeValues } from "../value-file.js";
|
|
11
|
-
import { envExamplePath } from "../
|
|
11
|
+
import { envExamplePath } from "../value-file.js";
|
|
12
12
|
import { FEISHU_AGENT_SCOPES } from "../channels/feishu/setup-mode.js";
|
|
13
13
|
import { slackBotScopes } from "../channels/slack/manifest.js";
|
|
14
14
|
const AGENT_SCOPE_LIST = FEISHU_AGENT_SCOPES.map((entry) => entry.request).join(", ");
|