@fastagent-sh/fastagent 0.13.0 → 0.15.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 +5 -5
- package/dist/agent.d.ts +20 -0
- package/dist/agent.js +9 -0
- package/dist/channels/context-buffer.d.ts +30 -0
- package/dist/channels/context-buffer.js +98 -0
- package/dist/channels/control.d.ts +28 -0
- package/dist/channels/control.js +214 -0
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +43 -0
- package/dist/channels/feishu/context-buffer.js +72 -0
- package/dist/channels/feishu/crypto.d.ts +4 -2
- package/dist/channels/feishu/crypto.js +4 -2
- package/dist/channels/feishu/feishu-api.d.ts +15 -7
- package/dist/channels/feishu/feishu-api.js +22 -4
- package/dist/channels/feishu/feishu.d.ts +38 -16
- package/dist/channels/feishu/feishu.js +286 -151
- package/dist/channels/feishu/invoke-turn.d.ts +24 -31
- package/dist/channels/feishu/invoke-turn.js +61 -62
- package/dist/channels/feishu/model.d.ts +98 -0
- package/dist/channels/feishu/model.js +9 -0
- package/dist/channels/feishu/normalize.d.ts +23 -0
- package/dist/channels/feishu/normalize.js +132 -0
- package/dist/channels/feishu/owned-threads.d.ts +7 -0
- package/dist/channels/feishu/owned-threads.js +47 -0
- package/dist/channels/feishu/parse.d.ts +21 -103
- package/dist/channels/feishu/parse.js +35 -145
- package/dist/channels/feishu/preview.d.ts +4 -7
- package/dist/channels/feishu/preview.js +26 -142
- package/dist/channels/feishu/register-app.d.ts +2 -1
- package/dist/channels/feishu/register-webhook.d.ts +2 -1
- package/dist/channels/feishu/scaffold/channel.ts +10 -3
- package/dist/channels/feishu/setup-mode.d.ts +8 -0
- package/dist/channels/feishu/setup-mode.js +2 -0
- package/dist/channels/feishu/ws-ingress.d.ts +28 -0
- package/dist/channels/feishu/ws-ingress.js +136 -0
- package/dist/channels/github/github.js +8 -6
- package/dist/channels/http.d.ts +14 -0
- package/dist/channels/http.js +35 -2
- package/dist/channels/invoke-turn-kit.d.ts +65 -0
- package/dist/channels/invoke-turn-kit.js +87 -0
- package/dist/channels/lark/lark.d.ts +4 -2
- package/dist/channels/lark/lark.js +4 -1
- package/dist/channels/lark/onboard.d.ts +8 -4
- package/dist/channels/lark/onboard.js +8 -0
- package/dist/channels/lark/scaffold/channel.ts +10 -3
- package/dist/channels/preview-kit.d.ts +109 -0
- package/dist/channels/preview-kit.js +183 -0
- package/dist/channels/seen.d.ts +5 -0
- package/dist/channels/seen.js +35 -0
- package/dist/channels/slack/bot-auth.d.ts +15 -0
- package/dist/channels/slack/bot-auth.js +146 -0
- package/dist/channels/slack/config-api.d.ts +60 -0
- package/dist/channels/slack/config-api.js +149 -0
- package/dist/channels/slack/context-buffer.d.ts +24 -0
- package/dist/channels/slack/context-buffer.js +37 -0
- package/dist/channels/slack/invoke-turn.d.ts +19 -0
- package/dist/channels/slack/invoke-turn.js +63 -0
- package/dist/channels/slack/manifest.d.ts +49 -0
- package/dist/channels/slack/manifest.js +69 -0
- package/dist/channels/slack/model.d.ts +67 -0
- package/dist/channels/slack/model.js +2 -0
- package/dist/channels/slack/onboard.d.ts +41 -0
- package/dist/channels/slack/onboard.js +120 -0
- package/dist/channels/slack/onboarding-state.d.ts +31 -0
- package/dist/channels/slack/onboarding-state.js +69 -0
- package/dist/channels/slack/owned-threads.d.ts +6 -0
- package/dist/channels/slack/owned-threads.js +43 -0
- package/dist/channels/slack/parse.d.ts +23 -0
- package/dist/channels/slack/parse.js +81 -0
- package/dist/channels/slack/preview.d.ts +24 -0
- package/dist/channels/slack/preview.js +359 -0
- package/dist/channels/slack/reaction.d.ts +24 -0
- package/dist/channels/slack/reaction.js +62 -0
- package/dist/channels/slack/register-webhook.d.ts +10 -0
- package/dist/channels/slack/register-webhook.js +49 -0
- package/dist/channels/slack/scaffold/channel.ts +33 -0
- package/dist/channels/slack/scaffold/slack-send.ts +171 -0
- package/dist/channels/slack/setup-server.d.ts +17 -0
- package/dist/channels/slack/setup-server.js +103 -0
- package/dist/channels/slack/slack-api.d.ts +77 -0
- package/dist/channels/slack/slack-api.js +415 -0
- package/dist/channels/slack/slack.d.ts +58 -0
- package/dist/channels/slack/slack.js +451 -0
- package/dist/channels/slack/welcomed.d.ts +5 -0
- package/dist/channels/slack/welcomed.js +32 -0
- package/dist/channels/state.js +3 -3
- package/dist/channels/stop-command.d.ts +6 -0
- package/dist/channels/stop-command.js +36 -0
- package/dist/channels/tasks.d.ts +13 -0
- package/dist/channels/tasks.js +10 -0
- package/dist/channels/telegram/context-buffer.d.ts +8 -17
- package/dist/channels/telegram/context-buffer.js +6 -85
- package/dist/channels/telegram/invoke-turn.d.ts +5 -22
- package/dist/channels/telegram/invoke-turn.js +11 -58
- package/dist/channels/telegram/preview.d.ts +4 -7
- package/dist/channels/telegram/preview.js +24 -142
- package/dist/channels/telegram/telegram.js +23 -9
- package/dist/channels/{feishu/text.d.ts → text.d.ts} +1 -1
- package/dist/channels/{feishu/text.js → text.js} +1 -1
- package/dist/channels/turn-queue.js +1 -1
- package/dist/channels/turn-store.d.ts +1 -1
- package/dist/channels/turn-store.js +2 -3
- package/dist/cli/add-feishu.d.ts +27 -0
- package/dist/{cli-add-feishu.js → cli/add-feishu.js} +167 -44
- package/dist/cli/add-slack.d.ts +10 -0
- package/dist/cli/add-slack.js +204 -0
- package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
- package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
- package/dist/cli/commands/add.d.ts +13 -0
- package/dist/cli/commands/add.js +274 -0
- package/dist/cli/commands/attach.d.ts +82 -0
- package/dist/cli/commands/attach.js +559 -0
- package/dist/cli/commands/chat.d.ts +4 -0
- package/dist/cli/commands/chat.js +21 -0
- package/dist/cli/commands/deploy.d.ts +15 -0
- package/dist/cli/commands/deploy.js +394 -0
- package/dist/cli/commands/dev.d.ts +11 -0
- package/dist/cli/commands/dev.js +82 -0
- package/dist/cli/commands/fire.d.ts +7 -0
- package/dist/cli/commands/fire.js +45 -0
- package/dist/cli/commands/info.d.ts +7 -0
- package/dist/cli/commands/info.js +108 -0
- package/dist/cli/commands/init.d.ts +8 -0
- package/dist/cli/commands/init.js +81 -0
- package/dist/cli/commands/invoke.d.ts +7 -0
- package/dist/cli/commands/invoke.js +28 -0
- package/dist/cli/commands/login.d.ts +6 -0
- package/dist/cli/commands/login.js +52 -0
- package/dist/cli/commands/models.d.ts +1 -0
- package/dist/cli/commands/models.js +15 -0
- package/dist/cli/commands/schedule.d.ts +12 -0
- package/dist/cli/commands/schedule.js +89 -0
- package/dist/cli/commands/start.d.ts +10 -0
- package/dist/cli/commands/start.js +97 -0
- package/dist/cli/commands/tool.d.ts +1 -0
- package/dist/cli/commands/tool.js +38 -0
- package/dist/cli/fail.d.ts +17 -0
- package/dist/cli/fail.js +32 -0
- package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
- package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
- package/dist/cli/kernel.d.ts +90 -0
- package/dist/cli/kernel.js +190 -0
- package/dist/cli/models-view.d.ts +21 -0
- package/dist/cli/models-view.js +66 -0
- package/dist/cli/program.d.ts +11 -0
- package/dist/cli/program.js +479 -0
- package/dist/cli/serve.d.ts +48 -0
- package/dist/cli/serve.js +248 -0
- package/dist/cli/shared.d.ts +44 -0
- package/dist/cli/shared.js +246 -0
- package/dist/cli.js +8 -1329
- package/dist/collect.d.ts +14 -3
- package/dist/collect.js +24 -0
- package/dist/core.d.ts +3 -1
- package/dist/core.js +2 -0
- package/dist/deploy/docker/plan.d.ts +45 -0
- package/dist/deploy/docker/plan.js +141 -0
- package/dist/deploy/docker/run.d.ts +40 -0
- package/dist/deploy/docker/run.js +126 -0
- package/dist/deploy/fly/plan.d.ts +3 -1
- package/dist/deploy/fly/plan.js +12 -7
- package/dist/deploy/fly/run.d.ts +5 -4
- package/dist/deploy/fly/run.js +16 -5
- package/dist/deploy/preflight.d.ts +10 -4
- package/dist/deploy/preflight.js +57 -12
- package/dist/deploy/railway/plan.d.ts +3 -1
- package/dist/deploy/railway/plan.js +12 -4
- package/dist/deploy/railway/run.d.ts +5 -4
- package/dist/deploy/railway/run.js +15 -5
- package/dist/deploy/runner.d.ts +6 -2
- package/dist/deploy/runner.js +1 -0
- package/dist/deploy/secrets.d.ts +2 -1
- package/dist/deploy/secrets.js +23 -3
- package/dist/dev-supervisor.d.ts +0 -2
- package/dist/dev-supervisor.js +7 -3
- package/dist/engines/pi/auth.js +160 -46
- package/dist/engines/pi/channel.d.ts +22 -16
- package/dist/engines/pi/channel.js +90 -60
- package/dist/engines/pi/chat.d.ts +4 -16
- package/dist/engines/pi/chat.js +8 -188
- package/dist/engines/pi/config.d.ts +23 -9
- package/dist/engines/pi/config.js +35 -5
- package/dist/engines/pi/create.d.ts +36 -7
- package/dist/engines/pi/create.js +63 -22
- package/dist/engines/pi/harness.d.ts +65 -1
- package/dist/engines/pi/harness.js +166 -2
- package/dist/engines/pi/invoke.d.ts +56 -3
- package/dist/engines/pi/invoke.js +340 -20
- package/dist/engines/pi/login.d.ts +11 -0
- package/dist/engines/pi/login.js +17 -5
- package/dist/engines/pi/models.d.ts +56 -10
- package/dist/engines/pi/models.js +61 -23
- package/dist/engines/pi/search-tools.d.ts +10 -0
- package/dist/engines/pi/search-tools.js +138 -0
- package/dist/engines/pi/session-builder.d.ts +16 -0
- package/dist/engines/pi/session-builder.js +308 -0
- package/dist/engines/pi/session-control.d.ts +50 -0
- package/dist/engines/pi/session-control.js +604 -0
- package/dist/engines/pi/sessions.d.ts +17 -2
- package/dist/engines/pi/sessions.js +9 -0
- package/dist/engines/pi/tool-context.d.ts +46 -11
- package/dist/engines/pi/tool-context.js +11 -9
- package/dist/engines/pi/tool.d.ts +35 -6
- package/dist/engines/pi/tool.js +47 -1
- package/dist/engines/pi/wake-tool.d.ts +0 -3
- package/dist/engines/pi/wake-tool.js +9 -7
- package/dist/engines/pi/workspace.d.ts +56 -1
- package/dist/engines/pi/workspace.js +75 -16
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/host/node.d.ts +23 -6
- package/dist/host/node.js +5 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/observe.js +3 -0
- package/dist/pi.d.ts +6 -4
- package/dist/pi.js +2 -1
- package/dist/scaffold/add-channel.d.ts +19 -6
- package/dist/scaffold/add-channel.js +127 -16
- package/dist/scaffold/templates/fastagent.config.mjs +5 -3
- package/dist/schedule/wakeups.d.ts +0 -3
- package/dist/schedule/wakeups.js +1 -1
- package/dist/session-remote.d.ts +53 -0
- package/dist/session-remote.js +336 -0
- package/dist/session.d.ts +265 -0
- package/dist/session.js +37 -0
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +2 -0
- package/dist/tunnel.d.ts +7 -4
- package/dist/tunnel.js +21 -10
- package/package.json +22 -7
- package/dist/channels/feishu/seen.d.ts +0 -5
- package/dist/channels/feishu/seen.js +0 -47
- package/dist/cli-add-feishu.d.ts +0 -8
- package/dist/cli-models.d.ts +0 -11
- package/dist/cli-models.js +0 -20
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* it into the harness alongside the selected `model`; the two must come from the same collection so
|
|
5
5
|
* the model's provider auth is in scope.
|
|
6
6
|
*/
|
|
7
|
-
import { type Models, type Provider } from "@earendil-works/pi-ai";
|
|
7
|
+
import { type Api, type Model, type Models, type Provider } from "@earendil-works/pi-ai";
|
|
8
|
+
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
8
9
|
import { type FastagentAuthOptions } from "./auth.ts";
|
|
10
|
+
import { type InteractiveLoginKind } from "./login.ts";
|
|
9
11
|
export interface CreatePiModelsOptions extends FastagentAuthOptions {
|
|
10
12
|
/** Credentials file path. Defaults to the global `~/.fastagent/auth.json`; the directory opener passes
|
|
11
13
|
* the project-level `<dir>/.fastagent/auth.json`. */
|
|
@@ -22,16 +24,41 @@ export interface CreatePiModelsOptions extends FastagentAuthOptions {
|
|
|
22
24
|
*/
|
|
23
25
|
export declare function createPiModels(options?: CreatePiModelsOptions): Models;
|
|
24
26
|
/**
|
|
25
|
-
* The
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* to what the user can use and lets them choose (mirroring pi-coding-agent's select-then-persist).
|
|
27
|
+
* The `ModelRuntime`-shaped sibling of {@link createPiModels} — the SAME hub semantics (built-in
|
|
28
|
+
* providers + fastagent's credential store at `authPath`) in the type pi's session services require
|
|
29
|
+
* (`createAgentSessionServices({ modelRuntime })`). Builtins only (`modelsPath: null` — pi's
|
|
30
|
+
* machine-global models.json is definition-foreign) and no availability network, so the model
|
|
31
|
+
* surface equals serving's. No `providers` option: `ModelRuntime` registers providers by config
|
|
32
|
+
* record, not `Provider` instance — accepting the option and dropping it would be a silent no-op;
|
|
33
|
+
* add the mapping when a consumer actually needs it.
|
|
33
34
|
*/
|
|
34
|
-
export declare function
|
|
35
|
+
export declare function createPiModelRuntime(options?: FastagentAuthOptions & {
|
|
36
|
+
authPath?: string;
|
|
37
|
+
}): Promise<ModelRuntime>;
|
|
38
|
+
/** Per-provider auth status for the first-run model picker: usable now (with the source label), not
|
|
39
|
+
* configured, or configured-but-broken (expired token, refresh failure, corrupt store — kept as DATA
|
|
40
|
+
* so the picker can show it instead of silently dropping the provider). Non-ready states carry the
|
|
41
|
+
* provider's {@link InteractiveLoginKind}, so the picker's hint predicts what picking does — an
|
|
42
|
+
* OAuth login, an API-key prompt, or (env-key-only providers) neither. */
|
|
43
|
+
export type ProviderAuthStatus = {
|
|
44
|
+
state: "ready";
|
|
45
|
+
source?: string;
|
|
46
|
+
} | {
|
|
47
|
+
state: "unconfigured";
|
|
48
|
+
login: InteractiveLoginKind;
|
|
49
|
+
} | {
|
|
50
|
+
state: "broken";
|
|
51
|
+
message: string;
|
|
52
|
+
login: InteractiveLoginKind;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Probe every provider's auth once (auth is provider-scoped, so any of its models works as the probe)
|
|
56
|
+
* — the status map behind the first-run model picker (`fastagent dev`/`start`/`invoke` with no model
|
|
57
|
+
* set). The picker shows the FULL catalog annotated with these statuses, so "what fastagent supports"
|
|
58
|
+
* and "what is authenticated on this machine" stay distinguishable; a needs-login choice triggers an
|
|
59
|
+
* inline `loginFlow`. Providers with no models are omitted (nothing to pick).
|
|
60
|
+
*/
|
|
61
|
+
export declare function providerAuthStatuses(models: Models): Promise<Map<string, ProviderAuthStatus>>;
|
|
35
62
|
/**
|
|
36
63
|
* Which source currently satisfies auth for `spec` — a startup diagnostic. Returns the upstream
|
|
37
64
|
* `AuthResult.source` label: `"OAuth"` for a stored OAuth credential (e.g. a logged-in openai-codex),
|
|
@@ -39,3 +66,22 @@ export declare function configuredModelSpecs(models: Models): Promise<string[]>;
|
|
|
39
66
|
* undefined when unconfigured. Reporting-only; never throws.
|
|
40
67
|
*/
|
|
41
68
|
export declare function probeAuthSource(models: Models, spec: string): Promise<string | undefined>;
|
|
69
|
+
/** Verdict of {@link probeApiKey}: `rejected` is DEFINITIVE (the provider answered HTTP 401 — the key
|
|
70
|
+
* is wrong); everything else non-ok is `unknown` — a 403 can be a VALID key without model permission,
|
|
71
|
+
* a 429/5xx/network failure says nothing about the key — so callers must only destroy state on
|
|
72
|
+
* `rejected`. */
|
|
73
|
+
export type KeyProbe = {
|
|
74
|
+
state: "ok";
|
|
75
|
+
} | {
|
|
76
|
+
state: "rejected" | "unknown";
|
|
77
|
+
message: string;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Quick-fail probe for a just-stored API key: one minimal real request through the standard auth
|
|
81
|
+
* resolution path (the same path invokes take), so a mistyped key surfaces at login time, not at the
|
|
82
|
+
* first turn. `complete` reports provider errors as `stopReason: "error"` rather than throwing; the
|
|
83
|
+
* HTTP status arrives via `onResponse` — when a provider path never calls it (SDK transports), fall
|
|
84
|
+
* back to a conservative "401" match in the error text. Short timeout, no retries: feedback speed
|
|
85
|
+
* over transient-failure tolerance (a transient lands on `unknown`, which keeps the key).
|
|
86
|
+
*/
|
|
87
|
+
export declare function probeApiKey(models: Models, model: Model<Api>): Promise<KeyProbe>;
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { defaultProviderAuthContext } from "@earendil-works/pi-ai";
|
|
8
8
|
import { builtinModels } from "@earendil-works/pi-ai/providers/all";
|
|
9
|
-
import {
|
|
9
|
+
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { fastagentCredentialStore } from "./auth.js";
|
|
11
|
+
import { interactiveLoginKind } from "./login.js";
|
|
11
12
|
/**
|
|
12
13
|
* A `Models` with every built-in pi provider, wired to fastagent's auth: stored credentials from the
|
|
13
14
|
* {@link CreatePiModelsOptions.authPath} file (via {@link fastagentCredentialStore}; the global
|
|
@@ -25,38 +26,44 @@ export function createPiModels(options = {}) {
|
|
|
25
26
|
return models;
|
|
26
27
|
}
|
|
27
28
|
/**
|
|
28
|
-
* The
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* to what the user can use and lets them choose (mirroring pi-coding-agent's select-then-persist).
|
|
29
|
+
* The `ModelRuntime`-shaped sibling of {@link createPiModels} — the SAME hub semantics (built-in
|
|
30
|
+
* providers + fastagent's credential store at `authPath`) in the type pi's session services require
|
|
31
|
+
* (`createAgentSessionServices({ modelRuntime })`). Builtins only (`modelsPath: null` — pi's
|
|
32
|
+
* machine-global models.json is definition-foreign) and no availability network, so the model
|
|
33
|
+
* surface equals serving's. No `providers` option: `ModelRuntime` registers providers by config
|
|
34
|
+
* record, not `Provider` instance — accepting the option and dropping it would be a silent no-op;
|
|
35
|
+
* add the mapping when a consumer actually needs it.
|
|
36
36
|
*/
|
|
37
|
-
export
|
|
38
|
-
|
|
37
|
+
export function createPiModelRuntime(options = {}) {
|
|
38
|
+
return ModelRuntime.create({
|
|
39
|
+
credentials: fastagentCredentialStore(options.authPath, { warn: options.warn }),
|
|
40
|
+
modelsPath: null,
|
|
41
|
+
allowModelNetwork: false,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Probe every provider's auth once (auth is provider-scoped, so any of its models works as the probe)
|
|
46
|
+
* — the status map behind the first-run model picker (`fastagent dev`/`start`/`invoke` with no model
|
|
47
|
+
* set). The picker shows the FULL catalog annotated with these statuses, so "what fastagent supports"
|
|
48
|
+
* and "what is authenticated on this machine" stay distinguishable; a needs-login choice triggers an
|
|
49
|
+
* inline `loginFlow`. Providers with no models are omitted (nothing to pick).
|
|
50
|
+
*/
|
|
51
|
+
export async function providerAuthStatuses(models) {
|
|
52
|
+
const statuses = new Map();
|
|
39
53
|
for (const provider of models.getProviders()) {
|
|
40
54
|
const [probe] = provider.getModels();
|
|
41
55
|
if (!probe)
|
|
42
56
|
continue;
|
|
43
|
-
|
|
57
|
+
const login = interactiveLoginKind(provider);
|
|
44
58
|
try {
|
|
45
|
-
|
|
59
|
+
const auth = await models.getAuth(probe);
|
|
60
|
+
statuses.set(provider.id, auth ? { state: "ready", source: auth.source } : { state: "unconfigured", login });
|
|
46
61
|
}
|
|
47
62
|
catch (error) {
|
|
48
|
-
|
|
49
|
-
// from the menu, but SAY so — a silent disappearance is the same fail-visibly gap the caller
|
|
50
|
-
// guards against for the top-level enumeration. `undefined` (plainly unconfigured) stays quiet.
|
|
51
|
-
log.warn(`[fastagent] skipping provider "${provider.id}": auth check failed (${error.message})`);
|
|
52
|
-
continue;
|
|
63
|
+
statuses.set(provider.id, { state: "broken", message: error.message, login });
|
|
53
64
|
}
|
|
54
|
-
if (!usable)
|
|
55
|
-
continue;
|
|
56
|
-
for (const model of provider.getModels())
|
|
57
|
-
specs.push(`${provider.id}/${model.id}`);
|
|
58
65
|
}
|
|
59
|
-
return
|
|
66
|
+
return statuses;
|
|
60
67
|
}
|
|
61
68
|
/**
|
|
62
69
|
* Which source currently satisfies auth for `spec` — a startup diagnostic. Returns the upstream
|
|
@@ -74,3 +81,34 @@ export async function probeAuthSource(models, spec) {
|
|
|
74
81
|
const auth = await models.getAuth(model).catch(() => undefined);
|
|
75
82
|
return auth?.source;
|
|
76
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Quick-fail probe for a just-stored API key: one minimal real request through the standard auth
|
|
86
|
+
* resolution path (the same path invokes take), so a mistyped key surfaces at login time, not at the
|
|
87
|
+
* first turn. `complete` reports provider errors as `stopReason: "error"` rather than throwing; the
|
|
88
|
+
* HTTP status arrives via `onResponse` — when a provider path never calls it (SDK transports), fall
|
|
89
|
+
* back to a conservative "401" match in the error text. Short timeout, no retries: feedback speed
|
|
90
|
+
* over transient-failure tolerance (a transient lands on `unknown`, which keeps the key).
|
|
91
|
+
*/
|
|
92
|
+
export async function probeApiKey(models, model) {
|
|
93
|
+
let status;
|
|
94
|
+
let reply;
|
|
95
|
+
try {
|
|
96
|
+
reply = await models.complete(model, { messages: [{ role: "user", content: "ping", timestamp: Date.now() }] }, {
|
|
97
|
+
maxTokens: 16,
|
|
98
|
+
timeoutMs: 15_000,
|
|
99
|
+
maxRetries: 0,
|
|
100
|
+
onResponse: (r) => {
|
|
101
|
+
status = r.status;
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
// Thrown = before/around the request (auth resolution, transport setup) — not a provider verdict.
|
|
107
|
+
return { state: "unknown", message: error.message };
|
|
108
|
+
}
|
|
109
|
+
if (reply.stopReason !== "error" && reply.stopReason !== "aborted")
|
|
110
|
+
return { state: "ok" };
|
|
111
|
+
const message = reply.errorMessage ?? `stopReason "${reply.stopReason}"`;
|
|
112
|
+
const unauthorized = status === 401 || (status === undefined && /(^|\D)401(\D|$)/.test(message));
|
|
113
|
+
return { state: unauthorized ? "rejected" : "unknown", message };
|
|
114
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
+
/** Mount the built-in loader iff any mounted tool is deferred and the author didn't define their own. */
|
|
3
|
+
export declare function withSearchTool(tools: AgentTool[]): AgentTool[];
|
|
4
|
+
/** Build the `search_tools` loader. Keyword search over the inactive tools' name+description.
|
|
5
|
+
*
|
|
6
|
+
* `executionMode: "sequential"` — pi turns any batch containing a sequential tool serial. Required for
|
|
7
|
+
* correct load-point attribution everywhere an OUTER active-set diff exists: pi wraps SDK customTools
|
|
8
|
+
* (the chat path) in a before/after diff, and two parallel loader calls would both snapshot the
|
|
9
|
+
* pre-activation set and get stamped with the same activation. Custom loader authors must set it too. */
|
|
10
|
+
export declare function makeSearchToolsTool(): AgentTool;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in `search_tools` loader — the discovery surface for deferred tools (defineTool
|
|
3
|
+
* `deferred: true`). A deferred tool's schema is not in the request and the model cannot see it; this
|
|
4
|
+
* loader is how it finds and activates one. Mounted automatically (withSearchTool) only when a
|
|
5
|
+
* deferred tool exists; a workspace tool named `search_tools` wins — the author owns the concept then
|
|
6
|
+
* (same rule as the wake pair).
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { log } from "../../log.js";
|
|
10
|
+
import { defineTool, isDeferredTool, stripDeferredMarker } from "./tool.js";
|
|
11
|
+
/** Mount the built-in loader iff any mounted tool is deferred and the author didn't define their own. */
|
|
12
|
+
export function withSearchTool(tools) {
|
|
13
|
+
if (!tools.some(isDeferredTool))
|
|
14
|
+
return tools;
|
|
15
|
+
const authored = tools.find((t) => t.name === "search_tools");
|
|
16
|
+
if (!authored)
|
|
17
|
+
return [...tools, makeSearchToolsTool()];
|
|
18
|
+
let fixed = authored;
|
|
19
|
+
if (isDeferredTool(fixed)) {
|
|
20
|
+
// A deferred LOADER is a contradiction — it is the only entry point to the deferred tools, so
|
|
21
|
+
// nothing could ever activate it (or, through it, them): every deferred tool would be silently
|
|
22
|
+
// unreachable. Ignore the marker and keep the loader active (fail visibly, keep the capability).
|
|
23
|
+
log.warn("[fastagent] search_tools is marked deferred — ignoring the marker: the loader must stay active, or no deferred tool could ever be activated");
|
|
24
|
+
fixed = stripDeferredMarker(fixed);
|
|
25
|
+
}
|
|
26
|
+
if (fixed.executionMode !== "sequential") {
|
|
27
|
+
// A non-sequential loader silently revives the parallel double-attribution (pi's diff around SDK
|
|
28
|
+
// tools in chat) — enforce the mode rather than hope the author read the docs; warn so they know.
|
|
29
|
+
log.warn('[fastagent] search_tools lacks executionMode: "sequential" — forcing it: parallel loader calls would misattribute activations');
|
|
30
|
+
fixed = { ...fixed, executionMode: "sequential" };
|
|
31
|
+
}
|
|
32
|
+
return fixed === authored ? tools : tools.map((t) => (t === authored ? fixed : t));
|
|
33
|
+
}
|
|
34
|
+
/** Activation cap per search: activation is additive, session-persisted, and has NO deactivate path —
|
|
35
|
+
* without a cap, one broad token ("get", "file") would permanently activate half the catalog and
|
|
36
|
+
* silently spend the entire deferral benefit for the rest of the conversation. Over the cap nothing
|
|
37
|
+
* activates; the model gets the candidates and narrows the query. */
|
|
38
|
+
const MAX_ACTIVATIONS_PER_SEARCH = 5;
|
|
39
|
+
/** Miss-path listing cap — same rationale as the activation cap: a typo query must not pour the whole
|
|
40
|
+
* catalog (the thing deferral keeps OUT of the context) back in as a tool result. */
|
|
41
|
+
const MAX_MISS_LISTING = 10;
|
|
42
|
+
/** Build the `search_tools` loader. Keyword search over the inactive tools' name+description.
|
|
43
|
+
*
|
|
44
|
+
* `executionMode: "sequential"` — pi turns any batch containing a sequential tool serial. Required for
|
|
45
|
+
* correct load-point attribution everywhere an OUTER active-set diff exists: pi wraps SDK customTools
|
|
46
|
+
* (the chat path) in a before/after diff, and two parallel loader calls would both snapshot the
|
|
47
|
+
* pre-activation set and get stamped with the same activation. Custom loader authors must set it too. */
|
|
48
|
+
export function makeSearchToolsTool() {
|
|
49
|
+
return defineTool({
|
|
50
|
+
name: "search_tools",
|
|
51
|
+
executionMode: "sequential",
|
|
52
|
+
description:
|
|
53
|
+
// First line short on purpose: the base prompt's tools list truncates at the first newline, and
|
|
54
|
+
// the discovery guidance below would otherwise flood it (and duplicate its deferred note).
|
|
55
|
+
"Discover and activate additional tools.\n" +
|
|
56
|
+
"Part of this agent's toolset is inactive until needed: search by keywords (e.g. what you are " +
|
|
57
|
+
"trying to do), and matching inactive tools are activated and become callable from that point on " +
|
|
58
|
+
"(if too many match, you get the candidates back — narrow the query, or query an exact tool " +
|
|
59
|
+
"name). ALWAYS search here before concluding a capability is missing.",
|
|
60
|
+
input: z.object({
|
|
61
|
+
query: z.string().min(1).describe("keywords describing the capability you need (e.g. 'weather forecast')"),
|
|
62
|
+
}),
|
|
63
|
+
async execute(input, ctx) {
|
|
64
|
+
if (!ctx.tools)
|
|
65
|
+
return "tool activation is unavailable outside a conversation turn.";
|
|
66
|
+
// Search the WHOLE registered catalog: the loader is the only discovery surface, and in a long
|
|
67
|
+
// conversation the model does not remember what it activated — a "No tools matched" answer for
|
|
68
|
+
// an ALREADY-ACTIVE tool would push it toward the exact wrong conclusion (capability missing).
|
|
69
|
+
// ponytail: naive keyword match (any query token as a case-insensitive substring of
|
|
70
|
+
// name+description) with a hard per-search activation cap above — the two named ceilings are
|
|
71
|
+
// relevance and irreversibility; swap in scoring/embeddings if catalogs outgrow this.
|
|
72
|
+
const tokens = input.query
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.split(/[^a-z0-9]+/)
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
// AND semantics: EVERY token must hit — "adding a word narrows" must actually hold, or the
|
|
77
|
+
// over-cap "narrow the query" instruction sends the model in circles (OR would widen with each
|
|
78
|
+
// word, and a shared prefix like "fetch" could make a whole tool family permanently over-cap).
|
|
79
|
+
const matchesQuery = (t) => {
|
|
80
|
+
const haystack = `${t.name} ${t.description}`.toLowerCase();
|
|
81
|
+
return tokens.every((token) => haystack.includes(token));
|
|
82
|
+
};
|
|
83
|
+
const describe = (t) => `${t.name} — ${t.description.split("\n")[0]}`;
|
|
84
|
+
const active = new Set(ctx.tools.active());
|
|
85
|
+
const registered = ctx.tools.registered();
|
|
86
|
+
// A query with no searchable tokens (all punctuation/symbols) must not match: `every` over an
|
|
87
|
+
// empty token list is vacuously true, and a noise query would otherwise activate the catalog.
|
|
88
|
+
if (tokens.length === 0)
|
|
89
|
+
return `"${input.query}" contains no searchable keywords — describe the capability you need.`;
|
|
90
|
+
// Exact-name shortcut: the guaranteed escape hatch from the cap — a query that IS an INACTIVE
|
|
91
|
+
// registered tool's name addresses that one tool, no keyword scoring in the way. An exact match
|
|
92
|
+
// on an ACTIVE tool falls through to keyword matching: it must not swallow the discovery of
|
|
93
|
+
// other, still-inactive keyword matches.
|
|
94
|
+
const exact = registered.find((t) => t.name.toLowerCase() === input.query.trim().toLowerCase());
|
|
95
|
+
const activeMatches = registered.filter((t) => active.has(t.name) && (t === exact || matchesQuery(t)));
|
|
96
|
+
const inactiveMatches = exact && !active.has(exact.name) ? [exact] : registered.filter((t) => !active.has(t.name) && matchesQuery(t));
|
|
97
|
+
// Same listing cap as every other branch — a wide token can match most of the ACTIVE set too
|
|
98
|
+
// (in chat that includes pi's default tools), and no answer may pour a catalog into the context.
|
|
99
|
+
const listedActive = activeMatches.slice(0, MAX_MISS_LISTING);
|
|
100
|
+
const moreActive = activeMatches.length - listedActive.length;
|
|
101
|
+
const activeNote = activeMatches.length > 0
|
|
102
|
+
? `Already active (call directly): ${listedActive.map(describe).join("; ")}${moreActive > 0 ? ` … and ${moreActive} more` : ""}.`
|
|
103
|
+
: "";
|
|
104
|
+
if (inactiveMatches.length === 0) {
|
|
105
|
+
if (activeNote)
|
|
106
|
+
return activeNote;
|
|
107
|
+
const inactive = registered.filter((t) => !active.has(t.name));
|
|
108
|
+
if (inactive.length === 0)
|
|
109
|
+
return "All tools are already active — nothing to discover.";
|
|
110
|
+
// Cap the miss listing like the activation cap — both guard the same semantic (don't pour the
|
|
111
|
+
// catalog back into the context the deferral exists to protect).
|
|
112
|
+
const listed = inactive.slice(0, MAX_MISS_LISTING);
|
|
113
|
+
const more = inactive.length - listed.length;
|
|
114
|
+
return `No tools matched "${input.query}". Inactive tools: ${listed.map(describe).join("; ")}${more > 0 ? ` … and ${more} more — search with different keywords.` : ""}`;
|
|
115
|
+
}
|
|
116
|
+
if (inactiveMatches.length > MAX_ACTIVATIONS_PER_SEARCH) {
|
|
117
|
+
// Same listing cap as the miss path — an over-cap answer must not pour the catalog into the
|
|
118
|
+
// context either. Names alone suffice: the exact-name escape only needs a name to query.
|
|
119
|
+
const listed = inactiveMatches.slice(0, MAX_MISS_LISTING);
|
|
120
|
+
const more = inactiveMatches.length - listed.length;
|
|
121
|
+
return `${inactiveMatches.length} inactive tools matched "${input.query}" — too many to activate at once (activation is permanent for this conversation). Narrow the query (or query an exact name). Matches: ${listed
|
|
122
|
+
.map((t) => t.name)
|
|
123
|
+
.join(", ")}${more > 0 ? ` … and ${more} more` : ""}.${activeNote ? ` ${activeNote}` : ""}`;
|
|
124
|
+
}
|
|
125
|
+
const activated = await ctx.tools.activate(inactiveMatches.map((t) => t.name));
|
|
126
|
+
// Report what actually happened, not what was attempted: a parallel sibling call may have
|
|
127
|
+
// activated the same matches first, leaving nothing new here — an empty "Activated:" would lie.
|
|
128
|
+
if (activated.length === 0) {
|
|
129
|
+
return `Matched ${inactiveMatches.map((t) => t.name).join(", ")} — already active (possibly activated by a concurrent call). Call them directly.${activeNote ? ` ${activeNote}` : ""}`;
|
|
130
|
+
}
|
|
131
|
+
const raced = inactiveMatches.filter((t) => !activated.includes(t.name));
|
|
132
|
+
return `Activated: ${inactiveMatches
|
|
133
|
+
.filter((t) => activated.includes(t.name))
|
|
134
|
+
.map(describe)
|
|
135
|
+
.join("; ")}.${raced.length > 0 ? ` Already active: ${raced.map((t) => t.name).join(", ")}.` : ""}${activeNote ? ` ${activeNote}` : ""} These tools are callable now.`;
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type AgentSessionRuntime, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export interface BuildSessionRuntimeOptions {
|
|
3
|
+
/** Model spec override (the CLI --model flag). Precedence: this > FASTAGENT_MODEL > config.model. */
|
|
4
|
+
model?: string;
|
|
5
|
+
/** Credentials file override (the CLI --auth-path flag). Precedence: this > FASTAGENT_AUTH_PATH >
|
|
6
|
+
* the workspace default `<stateRoot>/auth.json`. */
|
|
7
|
+
authPath?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Build pi's interactive runtime driven by fastagent's assembled agent (model, prompt, tools,
|
|
11
|
+
* skills, and auth resolved exactly as the serving opener does). Split from the TUI launcher so the
|
|
12
|
+
* assembly — the fidelity-critical part — is inspectable and reusable without launching a TUI.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildWorkspaceSessionRuntime(dir: string, options?: BuildSessionRuntimeOptions,
|
|
15
|
+
/** Session backend. Defaults to pi's project-scoped store; tests inject SessionManager.inMemory(). */
|
|
16
|
+
sessionManager?: SessionManager): Promise<AgentSessionRuntime>;
|