@fastagent-sh/fastagent 0.17.1 → 0.19.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 +25 -7
- package/dist/agent.d.ts +12 -1
- package/dist/agent.js +1 -1
- package/dist/atomic-write.d.ts +19 -0
- package/dist/atomic-write.js +48 -0
- package/dist/{host/node.d.ts → channel.d.ts} +24 -28
- package/dist/channel.js +1 -0
- package/dist/channels/agentcore-service.d.ts +54 -0
- package/dist/channels/agentcore-service.js +111 -0
- package/dist/channels/agentcore.d.ts +12 -2
- package/dist/channels/agentcore.js +6 -4
- package/dist/channels/busy.d.ts +1 -1
- package/dist/channels/busy.js +1 -1
- package/dist/channels/control.d.ts +16 -6
- package/dist/channels/control.js +106 -4
- package/dist/{engines/pi/channel.d.ts → channels/discover.d.ts} +2 -2
- package/dist/{engines/pi/channel.js → channels/discover.js} +8 -12
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +1 -1
- package/dist/channels/feishu/context-buffer.js +4 -4
- package/dist/channels/feishu/feishu-api.d.ts +4 -2
- package/dist/channels/feishu/feishu-api.js +1 -1
- package/dist/channels/feishu/feishu.d.ts +1 -1
- package/dist/channels/feishu/feishu.js +45 -15
- package/dist/channels/feishu/invoke-turn.d.ts +9 -3
- package/dist/channels/feishu/invoke-turn.js +152 -34
- package/dist/channels/feishu/parse.js +6 -0
- package/dist/channels/feishu/preview.d.ts +1 -1
- package/dist/channels/feishu/preview.js +7 -3
- package/dist/channels/feishu/ws-ingress.d.ts +1 -1
- package/dist/channels/github/github.d.ts +1 -1
- package/dist/channels/http.d.ts +11 -7
- package/dist/channels/http.js +16 -111
- package/dist/channels/{context-buffer.js → kit/context-buffer.js} +2 -2
- package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +18 -4
- package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +23 -8
- package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
- package/dist/channels/{seen.js → kit/seen.js} +1 -1
- package/dist/channels/kit/state.d.ts +6 -0
- package/dist/channels/{state.js → kit/state.js} +4 -24
- package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
- package/dist/channels/{stop-command.js → kit/stop-command.js} +2 -2
- package/dist/channels/{tasks.js → kit/tasks.js} +1 -1
- package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +7 -0
- package/dist/channels/{thread-participants.js → kit/thread-participants.js} +4 -1
- package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
- package/dist/channels/{turn-store.js → kit/turn-store.js} +1 -1
- package/dist/channels/lark/lark.d.ts +1 -1
- package/dist/channels/serve.d.ts +73 -0
- package/dist/channels/serve.js +243 -0
- package/dist/channels/slack/bot-auth.js +3 -14
- package/dist/channels/slack/context-buffer.d.ts +2 -2
- package/dist/channels/slack/context-buffer.js +2 -2
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +9 -6
- package/dist/channels/slack/parse.js +2 -2
- package/dist/channels/slack/preview.d.ts +1 -1
- package/dist/channels/slack/preview.js +11 -3
- package/dist/channels/slack/slack-api.js +1 -1
- package/dist/channels/slack/slack.d.ts +1 -1
- package/dist/channels/slack/slack.js +8 -12
- package/dist/channels/slack/welcomed.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +2 -2
- package/dist/channels/telegram/context-buffer.js +2 -2
- package/dist/channels/telegram/invoke-turn.d.ts +1 -1
- package/dist/channels/telegram/invoke-turn.js +2 -2
- package/dist/channels/telegram/parse.js +2 -2
- package/dist/channels/telegram/preview.d.ts +1 -1
- package/dist/channels/telegram/preview.js +6 -2
- package/dist/channels/telegram/telegram.d.ts +1 -1
- package/dist/channels/telegram/telegram.js +3 -3
- package/dist/channels/telegram/turn-store.d.ts +2 -2
- package/dist/channels/telegram/turn-store.js +2 -2
- package/dist/cli/commands/add.js +1 -1
- package/dist/cli/commands/deploy.js +13 -5
- package/dist/cli/commands/dev.js +26 -14
- package/dist/cli/commands/fire.js +2 -2
- package/dist/cli/commands/info.js +28 -5
- package/dist/cli/commands/invoke.js +1 -1
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +64 -95
- package/dist/cli/commands/tool.js +5 -7
- package/dist/cli/program.js +1 -1
- package/dist/cli/serve.d.ts +18 -57
- package/dist/cli/serve.js +57 -243
- package/dist/cli/shared.d.ts +5 -3
- package/dist/cli/shared.js +14 -7
- package/dist/collect.d.ts +30 -4
- package/dist/collect.js +39 -6
- package/dist/core.d.ts +3 -5
- package/dist/core.js +7 -5
- package/dist/deploy/preflight.d.ts +8 -2
- package/dist/deploy/preflight.js +22 -4
- package/dist/deploy/secrets.d.ts +3 -0
- package/dist/deploy/secrets.js +6 -0
- package/dist/dev-supervisor.js +16 -4
- package/dist/engines/pi/agent-session-factory.d.ts +104 -0
- package/dist/engines/pi/agent-session-factory.js +314 -0
- package/dist/engines/pi/config.d.ts +4 -4
- package/dist/engines/pi/config.js +1 -1
- package/dist/engines/pi/create.d.ts +56 -42
- package/dist/engines/pi/create.js +150 -75
- package/dist/engines/pi/definition.d.ts +41 -7
- package/dist/engines/pi/definition.js +115 -11
- package/dist/engines/pi/invoke-session.d.ts +35 -0
- package/dist/engines/pi/invoke-session.js +357 -0
- package/dist/engines/pi/login.js +7 -4
- package/dist/engines/pi/models.d.ts +59 -11
- package/dist/engines/pi/models.js +64 -9
- package/dist/engines/pi/open.d.ts +9 -6
- package/dist/engines/pi/open.js +20 -10
- package/dist/engines/pi/report.d.ts +0 -4
- package/dist/engines/pi/report.js +0 -7
- package/dist/engines/pi/service.d.ts +23 -0
- package/dist/engines/pi/service.js +19 -0
- package/dist/engines/pi/session-builder.js +88 -90
- package/dist/engines/pi/session-control.d.ts +11 -9
- package/dist/engines/pi/session-control.js +114 -77
- package/dist/engines/pi/session-inheritance.d.ts +44 -0
- package/dist/engines/pi/session-inheritance.js +242 -0
- package/dist/engines/pi/session-settings.d.ts +14 -2
- package/dist/engines/pi/session-settings.js +18 -0
- package/dist/engines/pi/session-store.d.ts +70 -0
- package/dist/engines/pi/session-store.js +313 -0
- package/dist/engines/pi/tool-context.d.ts +18 -8
- package/dist/engines/pi/tool-context.js +23 -1
- package/dist/engines/pi/tool.d.ts +5 -5
- package/dist/engines/pi/tool.js +1 -1
- package/dist/engines/pi/{invoke.d.ts → turn-kit.d.ts} +46 -45
- package/dist/engines/pi/turn-kit.js +188 -0
- package/dist/engines/pi/wake-tool.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/log.d.ts +3 -0
- package/dist/log.js +5 -1
- package/dist/node.d.ts +16 -0
- package/dist/node.js +20 -0
- package/dist/paths.d.ts +13 -0
- package/dist/paths.js +16 -0
- package/dist/pi.d.ts +15 -8
- package/dist/pi.js +7 -6
- package/dist/scaffold/templates/fastagent.config.mjs +2 -0
- package/dist/scaffold/templates/persona.md +2 -2
- package/dist/schedule/state.js +4 -9
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +1 -1
- package/dist/service.d.ts +158 -0
- package/dist/service.js +354 -0
- package/dist/session-remote.d.ts +1 -1
- package/dist/session-remote.js +10 -2
- package/dist/session.d.ts +4 -2
- package/dist/session.js +1 -1
- package/package.json +14 -5
- package/dist/channels/state.d.ts +0 -16
- package/dist/engines/pi/harness.d.ts +0 -112
- package/dist/engines/pi/harness.js +0 -153
- package/dist/engines/pi/invoke.js +0 -618
- package/dist/engines/pi/read-image.d.ts +0 -4
- package/dist/engines/pi/read-image.js +0 -62
- package/dist/engines/pi/sessions.d.ts +0 -44
- package/dist/engines/pi/sessions.js +0 -182
- package/dist/host/node.js +0 -52
- /package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +0 -0
- /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
- /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
- /package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +0 -0
- /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
- /package/dist/channels/{text.js → kit/text.js} +0 -0
- /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
- /package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +0 -0
package/dist/deploy/preflight.js
CHANGED
|
@@ -15,14 +15,15 @@ import ignore from "ignore";
|
|
|
15
15
|
import { classifyBind } from "../bind.js";
|
|
16
16
|
import { resolveAuthPath } from "../engines/pi/config.js";
|
|
17
17
|
import { resolveSecretsDir, resolveStateRoot } from "../paths.js";
|
|
18
|
-
import { inspectChannels } from "../
|
|
18
|
+
import { inspectChannels } from "../channels/discover.js";
|
|
19
19
|
import { discoverScheduleFiles } from "../schedule/discover.js";
|
|
20
|
-
import {
|
|
20
|
+
import { createPiModelRuntime, modelCredentialCarry, probeAuthSource } from "../engines/pi/models.js";
|
|
21
21
|
import { CHANNEL_KINDS } from "../scaffold/add-channel.js";
|
|
22
22
|
import { exists } from "../paths.js";
|
|
23
23
|
import { detectRuntime, readPackageJson } from "../runtime.js";
|
|
24
24
|
import { fastagentVersion } from "../version.js";
|
|
25
25
|
import { isGeneratedDockerfile, isGeneratedDockerignore } from "./container.js";
|
|
26
|
+
import { isEnvKey } from "./secrets.js";
|
|
26
27
|
/**
|
|
27
28
|
* "Would docker's packer drop this path?" — built from a `.dockerignore`'s text via the `ignore`
|
|
28
29
|
* matcher (the same library the workspace ignore files use), so `!` negation and last-match-wins are
|
|
@@ -126,9 +127,25 @@ export async function preflightDeploy(input) {
|
|
|
126
127
|
});
|
|
127
128
|
}
|
|
128
129
|
// Probe auth from the SAME project-level file the opener/login use — not the global default, which would
|
|
129
|
-
// miss a `fastagent login` credential and falsely report "none configured".
|
|
130
|
+
// miss a `fastagent login` credential and falsely report "none configured". Through the AGENT's model
|
|
131
|
+
// surface too (its models.json travels into the image), so a custom endpoint is not read as an unknown
|
|
132
|
+
// provider — this probe feeds the gate that decides whether `--run` may proceed.
|
|
130
133
|
const authPath = resolveAuthPath(agentDir, authPathFlag);
|
|
131
|
-
const
|
|
134
|
+
const models = await createPiModelRuntime({ agentDir, authPath });
|
|
135
|
+
let modelAuth = modelSpec ? await probeAuthSource(models, modelSpec) : undefined;
|
|
136
|
+
let modelKeyInDefinition = false;
|
|
137
|
+
// probeAuthSource answers "is it authenticated here", which is not the deploy question ("how does the
|
|
138
|
+
// credential REACH the host"). It reports every models.json endpoint as "configured API key" — not an
|
|
139
|
+
// env-var name — so without this the gate below sees no credential and stops the deploy with two
|
|
140
|
+
// remedies that are both wrong for such an agent: `fastagent login` cannot serve a custom provider,
|
|
141
|
+
// and the key is already in the environment.
|
|
142
|
+
if (modelSpec && !isEnvKey(modelAuth)) {
|
|
143
|
+
const carry = modelCredentialCarry(models, modelSpec);
|
|
144
|
+
if (carry.envVar)
|
|
145
|
+
modelAuth = carry.envVar;
|
|
146
|
+
else
|
|
147
|
+
modelKeyInDefinition = carry.inDefinition;
|
|
148
|
+
}
|
|
132
149
|
// Container facts (shared by every host) + the warnings that follow. The facts describe the AGENT —
|
|
133
150
|
// its package.json/runtime/lockfile drive the image's install step — never the workspace's (the bake
|
|
134
151
|
// ships the whole tree, but the workspace's own manifest belongs to its own deploy).
|
|
@@ -410,6 +427,7 @@ export async function preflightDeploy(input) {
|
|
|
410
427
|
longConnectionChannels,
|
|
411
428
|
hasTimeTriggers,
|
|
412
429
|
modelAuth,
|
|
430
|
+
modelKeyInDefinition,
|
|
413
431
|
authPath,
|
|
414
432
|
container,
|
|
415
433
|
port,
|
package/dist/deploy/secrets.d.ts
CHANGED
|
@@ -38,6 +38,9 @@ export declare function deploymentSecrets(modelAuth: string | undefined, channel
|
|
|
38
38
|
*/
|
|
39
39
|
export declare function assembleSecrets(input: {
|
|
40
40
|
modelAuth: string | undefined;
|
|
41
|
+
/** The definition carries the model key itself (a models.json literal `apiKey` / `!command`): there is
|
|
42
|
+
* no value to carry and no gate to raise — see {@link modelCredentialCarry}. */
|
|
43
|
+
modelKeyInDefinition?: boolean;
|
|
41
44
|
authFile: Buffer | undefined;
|
|
42
45
|
channels: ChannelKind[];
|
|
43
46
|
longConnectionChannels?: string[];
|
package/dist/deploy/secrets.js
CHANGED
|
@@ -65,6 +65,12 @@ export function assembleSecrets(input) {
|
|
|
65
65
|
else if (input.authFile) {
|
|
66
66
|
secrets.FASTAGENT_AUTH_SEED = input.authFile.toString("base64");
|
|
67
67
|
}
|
|
68
|
+
else if (input.modelKeyInDefinition) {
|
|
69
|
+
// The definition authenticates itself (models.json literal key, or a command run on the host), so it
|
|
70
|
+
// travels in the image with everything else. Gating here would be the worst kind of wrong: both
|
|
71
|
+
// remedies we print are impossible for such an agent — `fastagent login` has no flow for a custom
|
|
72
|
+
// provider, and there is no provider env key to set.
|
|
73
|
+
}
|
|
68
74
|
else {
|
|
69
75
|
needsModelCredential = true; // no env key, no auth.json — `fastagent login` remediation
|
|
70
76
|
}
|
package/dist/dev-supervisor.js
CHANGED
|
@@ -13,15 +13,21 @@
|
|
|
13
13
|
import { spawn } from "node:child_process";
|
|
14
14
|
import { relative, sep } from "node:path";
|
|
15
15
|
import { watch as watchTree } from "chokidar";
|
|
16
|
-
import { AGENT_CONFIG_NAMES, resolveStateRoot } from "./paths.js";
|
|
17
|
-
import { isUnderDir } from "./
|
|
16
|
+
import { AGENT_CONFIG_NAMES, AGENT_MODELS_FILE, resolveStateRoot } from "./paths.js";
|
|
17
|
+
import { isUnderDir } from "./paths.js";
|
|
18
18
|
import { dotEnvPath } from "./env.js";
|
|
19
19
|
import { log } from "./log.js";
|
|
20
20
|
import { installProxyFetch } from "./proxy.js";
|
|
21
21
|
import { openExternalUrl } from "./open-url.js";
|
|
22
22
|
import { announceWebhooks, startCloudflareTunnel } from "./tunnel.js";
|
|
23
23
|
/** What the dev watcher restarts on (agent-dir-relative): the process-bound code inputs only. */
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The agent-dir directories loaded ONCE per worker: a restart is their only re-read. One list, so
|
|
26
|
+
* the watcher and the line printed at startup cannot disagree — a directory watched but unannounced
|
|
27
|
+
* looks broken, and one announced but unwatched silently strands the author mid-edit.
|
|
28
|
+
*/
|
|
29
|
+
const CODE_INPUT_DIRS = ["tools", "channels", "schedules", "extensions"];
|
|
30
|
+
const WATCHED_HINT = `${CODE_INPUT_DIRS.map((dir) => `${dir}/`).join(", ")}, package.json, fastagent.config.*, models.json, .secrets/.env`;
|
|
25
31
|
/**
|
|
26
32
|
* chokidar `ignored` matcher for the narrow watch scope (true = ignore), rooted at the AGENT DIR. When
|
|
27
33
|
* the agent sits INSIDE the workspace that means the surrounding tree never triggers a restart at all;
|
|
@@ -49,8 +55,14 @@ export function devWatchIgnored(root, envFile) {
|
|
|
49
55
|
return false;
|
|
50
56
|
if (rel === "package.json")
|
|
51
57
|
return false;
|
|
58
|
+
// models.json is read ONCE per worker (the model hub is built during assembly), so an edit needs a
|
|
59
|
+
// restart like any other code input. It is also the one input whose breakage KILLS the worker — a
|
|
60
|
+
// malformed file fails assembly — so leaving it unwatched would strand the author: the fix that
|
|
61
|
+
// repairs it would not be the edit that restarts.
|
|
62
|
+
if (rel === AGENT_MODELS_FILE)
|
|
63
|
+
return false;
|
|
52
64
|
const segments = rel.split(sep);
|
|
53
|
-
if (segments[0]
|
|
65
|
+
if (CODE_INPUT_DIRS.includes(segments[0]))
|
|
54
66
|
return false;
|
|
55
67
|
// The `.env` restarts too (credentials are process-bound). Keep it AND its ancestor directories
|
|
56
68
|
// un-pruned so chokidar can descend to it; every sibling inside them (auth.json, .env.example)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ExecutionEnv, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
3
|
+
import { type AgentSessionServices, type CreateAgentSessionServicesOptions, type ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { PiAgentSessionFactory } from "./invoke-session.ts";
|
|
5
|
+
import type { PiSessionRecordStore } from "./session-store.ts";
|
|
6
|
+
import { type MountedTool } from "./tool.ts";
|
|
7
|
+
/** pi's Model with the API-shape generic erased; fastagent only passes models through. */
|
|
8
|
+
type AnyModel = Model<any>;
|
|
9
|
+
export interface PiAgentSessionFactoryOptions {
|
|
10
|
+
/** Where conversations live. Continuity = same store + same session id. */
|
|
11
|
+
sessions: PiSessionRecordStore;
|
|
12
|
+
/**
|
|
13
|
+
* The model to run and the hub that authenticates it, resolved on FIRST USE and kept.
|
|
14
|
+
*
|
|
15
|
+
* A thunk because building a `ModelRuntime` is async while assembling an agent is not: the L1
|
|
16
|
+
* surface hands back an `Agent` synchronously, so the credential read that a runtime performs
|
|
17
|
+
* belongs on the first turn rather than in the caller's constructor. The two travel together
|
|
18
|
+
* because a model must be resolved against the runtime that holds its provider's auth.
|
|
19
|
+
*/
|
|
20
|
+
engine: () => Promise<{
|
|
21
|
+
modelRuntime: ModelRuntime;
|
|
22
|
+
model: AnyModel;
|
|
23
|
+
}>;
|
|
24
|
+
thinkingLevel?: ThinkingLevel;
|
|
25
|
+
tools?: MountedTool[];
|
|
26
|
+
/** Final assembled prompt, or a factory re-evaluated per turn. */
|
|
27
|
+
systemPrompt?: string | (() => string);
|
|
28
|
+
skills?: Skill[];
|
|
29
|
+
/** Per-turn source for the prompt+skills PAIR; supersedes the two above. This is what keeps
|
|
30
|
+
* "the directory is the agent, LIVE" true on a shared `services`: the ResourceLoader is built
|
|
31
|
+
* once, but what it serves is re-read here. */
|
|
32
|
+
live?: () => Promise<{
|
|
33
|
+
systemPrompt?: string;
|
|
34
|
+
skills?: Skill[];
|
|
35
|
+
}>;
|
|
36
|
+
/** The agent's working directory — what fastagent-defined tools see as `cwd`. */
|
|
37
|
+
cwd: string;
|
|
38
|
+
/**
|
|
39
|
+
* Where pi looks for ITS settings (retry budget, compaction thresholds, default thinking level).
|
|
40
|
+
*
|
|
41
|
+
* Deliberately NOT pi's machine-global `~/.pi/agent`: a served agent must behave the same on the
|
|
42
|
+
* author's laptop and in a container, and reading the operator's personal pi configuration is the
|
|
43
|
+
* artifact losing to the machine. Point it at a definition-scoped path; a missing directory simply
|
|
44
|
+
* means pi's own defaults, which is the intended baseline.
|
|
45
|
+
*/
|
|
46
|
+
agentDir?: string;
|
|
47
|
+
/**
|
|
48
|
+
* The definition's own extension entry points, for ANNOUNCING that serving does not run them.
|
|
49
|
+
*
|
|
50
|
+
* pi's extension machinery is built for one process serving one session: `bindCore()` copies the
|
|
51
|
+
* session's actions into a runtime the pi source itself calls "the shared runtime", extension
|
|
52
|
+
* modules are cached per assembly, and `session_start`/`session_shutdown` are a matched pair.
|
|
53
|
+
* Serving breaks every one of those assumptions — concurrent turns for unrelated conversations —
|
|
54
|
+
* and the failure is silent cross-talk: with two turns in flight, an extension calling
|
|
55
|
+
* `pi.sendMessage()` can deliver into the other conversation.
|
|
56
|
+
*
|
|
57
|
+
* Loading them anyway would be a correctness bug dressed as a feature, so serving does not, and
|
|
58
|
+
* warns when a definition ships some. `chat` runs them fully: one session, one runtime, which is
|
|
59
|
+
* exactly the shape pi is built for.
|
|
60
|
+
*
|
|
61
|
+
* Isolating them per session is mechanically possible — pi's uncached loader path builds a fresh
|
|
62
|
+
* module (jiti with `moduleCache: false`) and takes the runtime as an argument — but that function
|
|
63
|
+
* is not exported and the deep path is blocked by the package's `exports`. Reopening this needs
|
|
64
|
+
* that entry point upstream, not a workaround here.
|
|
65
|
+
*/
|
|
66
|
+
extensionPaths?: string[];
|
|
67
|
+
/** Built-ins omitted by an explicit lower-level tool list. */
|
|
68
|
+
excludedToolNames?: readonly string[];
|
|
69
|
+
/** Filesystem/process environment: definition loading, and the turn context for tools that read one. */
|
|
70
|
+
env: ExecutionEnv;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Announce extensions pi failed to load. pi collects them into `LoadExtensionsResult.errors` and
|
|
74
|
+
* carries on with the rest — sound for a TUI that shows them, silent for a server that never looks.
|
|
75
|
+
* A definition running without the extension it ships is exactly the "quietly missing" failure this
|
|
76
|
+
* exists to remove. CHAT calls it, once per built services — serving does not load extensions at
|
|
77
|
+
* all, and announces that instead (see PiAgentSessionFactoryOptions.extensionPaths).
|
|
78
|
+
*/
|
|
79
|
+
export declare function reportExtensionErrors(services: AgentSessionServices): void;
|
|
80
|
+
/** What pi is allowed to discover, minus the parts each assembly fills in itself. */
|
|
81
|
+
type DefinitionLoaderOptions = NonNullable<CreateAgentSessionServicesOptions["resourceLoaderOptions"]>;
|
|
82
|
+
/**
|
|
83
|
+
* The resource posture a fastagent definition asks pi for — ONE definition of it, for both
|
|
84
|
+
* assemblies. Serving (`piAgentSessionFactory`) and chat (`buildAgentSessionRuntime`) build
|
|
85
|
+
* different sessions on top, but what pi is allowed to DISCOVER is not one of the differences:
|
|
86
|
+
* everything comes from the definition, nothing from the machine that happens to be running it.
|
|
87
|
+
*
|
|
88
|
+
* Two copies of this drifted once already: `additionalExtensionPaths` was added to both, and only
|
|
89
|
+
* one of them also passed the resulting tool names through pi's `tools` allowlist — so extensions
|
|
90
|
+
* worked when served and vanished in chat. A difference between the two has to be visible AS a
|
|
91
|
+
* difference, which is what the parameters are for: serving reads a prompt and skills that change
|
|
92
|
+
* per turn and passes NO extension paths (it does not run them — see
|
|
93
|
+
* {@link PiAgentSessionFactoryOptions.extensionPaths}); chat reads a fixed assembly and passes its
|
|
94
|
+
* own. Both are arguments now, rather than two files that happen to disagree.
|
|
95
|
+
*/
|
|
96
|
+
export declare function definitionResourceLoaderOptions(source: {
|
|
97
|
+
systemPrompt: () => string | undefined;
|
|
98
|
+
skills: () => Skill[];
|
|
99
|
+
/** Omitted by serving, which does not run them. */
|
|
100
|
+
extensionPaths?: readonly string[];
|
|
101
|
+
}): DefinitionLoaderOptions;
|
|
102
|
+
/** Open-or-create the record, then bind a fresh session to it. One call per invoke. */
|
|
103
|
+
export declare function piAgentSessionFactory(options: PiAgentSessionFactoryOptions): PiAgentSessionFactory;
|
|
104
|
+
export {};
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AgentSession L0's engine binding: fastagent's assembled agent — model, prompt, skills, tools —
|
|
3
|
+
* bound to one durable record, per invoke.
|
|
4
|
+
*
|
|
5
|
+
* The chat path builds the same pi class from the same assembly (session-builder.ts) but keeps ONE
|
|
6
|
+
* resident session. What is specific here is the posture: many sessions, one turn each, nothing in
|
|
7
|
+
* memory between turns.
|
|
8
|
+
*
|
|
9
|
+
* Shared once, rebuilt per turn:
|
|
10
|
+
* - `services` (ResourceLoader, settings, model runtime) is built lazily and reused — it is the
|
|
11
|
+
* expensive half, and it holds nothing session-specific;
|
|
12
|
+
* - the `AgentSession` and its tool bindings are per turn, because a tool's `execute` closes over the
|
|
13
|
+
* session it runs in and this posture has several in flight at once.
|
|
14
|
+
*/
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { createAgentSessionFromServices, createAgentSessionServices, } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { log } from "../../log.js";
|
|
18
|
+
import { isDeferredTool } from "./tool.js";
|
|
19
|
+
import { activePath, resolveSessionSettings } from "./session-settings.js";
|
|
20
|
+
import { DEFAULT_THINKING_LEVEL } from "./models.js";
|
|
21
|
+
import { additiveActivation, agentSessionManager, turnContext } from "./tool-context.js";
|
|
22
|
+
/**
|
|
23
|
+
* The session custom-entry type recording ONE activation delta: `{ names }` — exactly the deferred
|
|
24
|
+
* tools a loader activated in that call.
|
|
25
|
+
*
|
|
26
|
+
* A DEDICATED record, not pi's own `active_tools_change`: that one is a full SNAPSHOT of everything
|
|
27
|
+
* active at the moment, so replaying it would keep a tool active in old sessions after the author
|
|
28
|
+
* flips it to `deferred` — the session never discovered it. A delta carries only what was actually
|
|
29
|
+
* found, and is layered onto whatever the workspace mounts TODAY.
|
|
30
|
+
*/
|
|
31
|
+
const TOOL_ACTIVATION_ENTRY = "fastagent:tool-activation";
|
|
32
|
+
/** Every deferred tool this session has ever discovered, oldest first. */
|
|
33
|
+
function recordedActivations(session) {
|
|
34
|
+
const names = [];
|
|
35
|
+
for (const entry of session.sessionManager.getBranch()) {
|
|
36
|
+
const record = entry;
|
|
37
|
+
if (record.type !== "custom" || record.customType !== TOOL_ACTIVATION_ENTRY)
|
|
38
|
+
continue;
|
|
39
|
+
if (Array.isArray(record.data?.names)) {
|
|
40
|
+
for (const name of record.data.names)
|
|
41
|
+
if (typeof name === "string")
|
|
42
|
+
names.push(name);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return names;
|
|
46
|
+
}
|
|
47
|
+
/** Warned once per session+missing set: a fresh session is built per invoke and channel sessions run
|
|
48
|
+
* for weeks, so an un-deduped warn would repeat every turn and dilute its own signal. */
|
|
49
|
+
const warnedDroppedActivations = new Set();
|
|
50
|
+
/**
|
|
51
|
+
* The turn's {@link ToolActivation} over a live session — the same bridge chat uses, so one
|
|
52
|
+
* built-in `search_tools` serves both.
|
|
53
|
+
*
|
|
54
|
+
* Activations are PERSISTED as deltas, so a tool discovered in one turn stays callable in the next.
|
|
55
|
+
* pi's own chat session does not do this (it has no place to put the record); a served session does,
|
|
56
|
+
* because the alternative is an agent that re-discovers the same capability every single turn.
|
|
57
|
+
*/
|
|
58
|
+
function sessionToolActivation(session) {
|
|
59
|
+
// Serialize activations: the read-modify-write below is only race-free while nothing awaits
|
|
60
|
+
// between read and write, and parallel tool calls in one batch would otherwise double-stamp.
|
|
61
|
+
let chain = Promise.resolve([]);
|
|
62
|
+
return {
|
|
63
|
+
active: () => session.getActiveToolNames(),
|
|
64
|
+
registered: () => session.getAllTools().map((t) => ({ name: t.name, description: t.description ?? "" })),
|
|
65
|
+
activate(names) {
|
|
66
|
+
const run = async () => {
|
|
67
|
+
const current = session.getActiveToolNames();
|
|
68
|
+
const added = additiveActivation(session.getAllTools().map((t) => t.name), current, names);
|
|
69
|
+
if (added.length > 0) {
|
|
70
|
+
session.setActiveToolsByName([...current, ...added]);
|
|
71
|
+
session.sessionManager.appendCustomEntry(TOOL_ACTIVATION_ENTRY, { names: added });
|
|
72
|
+
}
|
|
73
|
+
return added;
|
|
74
|
+
};
|
|
75
|
+
const result = chain.then(run, run);
|
|
76
|
+
chain = result.catch(() => []);
|
|
77
|
+
return result;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* fastagent's tools as pi tool definitions, bound to ONE session.
|
|
83
|
+
*
|
|
84
|
+
* `bound` is filled after the session exists — pi needs the definitions to build the session, and a
|
|
85
|
+
* tool needs the session to reach the turn context. A tool that somehow runs before that binding
|
|
86
|
+
* throws rather than executing outside the turn: a broken lifecycle must not look like a normal
|
|
87
|
+
* out-of-turn call.
|
|
88
|
+
*/
|
|
89
|
+
function toolDefinitions(tools, cwd, env, sessionId, bound) {
|
|
90
|
+
return tools.map((tool) => ({
|
|
91
|
+
name: tool.name,
|
|
92
|
+
label: tool.name,
|
|
93
|
+
description: tool.description ?? "",
|
|
94
|
+
parameters: tool.parameters,
|
|
95
|
+
// An activating tool (the built-in loader) declares "sequential" so pi serializes its batch;
|
|
96
|
+
// without it pi's outer active-set diff double-stamps parallel calls.
|
|
97
|
+
executionMode: tool.executionMode,
|
|
98
|
+
execute: (id, params, signal) => {
|
|
99
|
+
const session = bound.session;
|
|
100
|
+
if (!session)
|
|
101
|
+
throw new Error("tool executed before its session was bound (lifecycle invariant broken)");
|
|
102
|
+
return turnContext.run({ cwd, sessionManager: agentSessionManager(session, sessionId), tools: sessionToolActivation(session) },
|
|
103
|
+
// Lower-level MountedTools may consume the fifth-argument env. Directory coding tools are
|
|
104
|
+
// cwd-bound and ignore it; authored tools read FastAgent's turnContext instead.
|
|
105
|
+
() => tool.execute(id, params, signal, undefined, { env }));
|
|
106
|
+
},
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Announce extensions pi failed to load. pi collects them into `LoadExtensionsResult.errors` and
|
|
111
|
+
* carries on with the rest — sound for a TUI that shows them, silent for a server that never looks.
|
|
112
|
+
* A definition running without the extension it ships is exactly the "quietly missing" failure this
|
|
113
|
+
* exists to remove. CHAT calls it, once per built services — serving does not load extensions at
|
|
114
|
+
* all, and announces that instead (see PiAgentSessionFactoryOptions.extensionPaths).
|
|
115
|
+
*/
|
|
116
|
+
export function reportExtensionErrors(services) {
|
|
117
|
+
for (const { path, error } of services.resourceLoader.getExtensions().errors) {
|
|
118
|
+
log.warn(`[fastagent] extension ${path} failed to load: ${error}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The resource posture a fastagent definition asks pi for — ONE definition of it, for both
|
|
123
|
+
* assemblies. Serving (`piAgentSessionFactory`) and chat (`buildAgentSessionRuntime`) build
|
|
124
|
+
* different sessions on top, but what pi is allowed to DISCOVER is not one of the differences:
|
|
125
|
+
* everything comes from the definition, nothing from the machine that happens to be running it.
|
|
126
|
+
*
|
|
127
|
+
* Two copies of this drifted once already: `additionalExtensionPaths` was added to both, and only
|
|
128
|
+
* one of them also passed the resulting tool names through pi's `tools` allowlist — so extensions
|
|
129
|
+
* worked when served and vanished in chat. A difference between the two has to be visible AS a
|
|
130
|
+
* difference, which is what the parameters are for: serving reads a prompt and skills that change
|
|
131
|
+
* per turn and passes NO extension paths (it does not run them — see
|
|
132
|
+
* {@link PiAgentSessionFactoryOptions.extensionPaths}); chat reads a fixed assembly and passes its
|
|
133
|
+
* own. Both are arguments now, rather than two files that happen to disagree.
|
|
134
|
+
*/
|
|
135
|
+
export function definitionResourceLoaderOptions(source) {
|
|
136
|
+
return {
|
|
137
|
+
// Definition-only, like dev/start: pi's machine-global discovery (the operator's own ~/.pi
|
|
138
|
+
// extensions, slash commands, global AGENTS.md, APPEND_SYSTEM.md) stays out, so the agent that
|
|
139
|
+
// runs is the artifact, not the artifact plus whoever's laptop it is.
|
|
140
|
+
noExtensions: true,
|
|
141
|
+
// ...except the definition's OWN extensions/: pi honours additionalExtensionPaths even under
|
|
142
|
+
// noExtensions, which is exactly the split wanted here — the artifact travels with its
|
|
143
|
+
// extensions, the machine's stay out.
|
|
144
|
+
...(source.extensionPaths?.length ? { additionalExtensionPaths: [...source.extensionPaths] } : {}),
|
|
145
|
+
noPromptTemplates: true,
|
|
146
|
+
noContextFiles: true,
|
|
147
|
+
// A SPACE, not "", when the assembly has no prompt: pi treats an empty custom prompt as absent
|
|
148
|
+
// and substitutes its own coding-assistant identity, which an L1 agent
|
|
149
|
+
// (`createPiAgent({ model, tools })`) never asked for. pi appends its own working-directory line
|
|
150
|
+
// either way — that is engine behaviour this binding does not fight.
|
|
151
|
+
systemPromptOverride: () => source.systemPrompt() ?? " ",
|
|
152
|
+
appendSystemPromptOverride: () => [],
|
|
153
|
+
skillsOverride: (base) => ({
|
|
154
|
+
skills: toPiSkills(source.skills()),
|
|
155
|
+
diagnostics: base.diagnostics,
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** Open-or-create the record, then bind a fresh session to it. One call per invoke. */
|
|
160
|
+
export function piAgentSessionFactory(options) {
|
|
161
|
+
const { sessions, thinkingLevel, cwd, env } = options;
|
|
162
|
+
const extensionPaths = options.extensionPaths ?? [];
|
|
163
|
+
const excludedToolNames = options.excludedToolNames ?? [];
|
|
164
|
+
if (extensionPaths.length > 0) {
|
|
165
|
+
log.warn(`[fastagent] ${extensionPaths.length} extension(s) in the definition are NOT loaded when serving ` +
|
|
166
|
+
"(they run in `fastagent chat`): pi's extension runtime is shared across sessions, and serving " +
|
|
167
|
+
"runs concurrent turns for different conversations. See docs/configuration.md#extensions.");
|
|
168
|
+
}
|
|
169
|
+
const tools = options.tools ?? [];
|
|
170
|
+
const deferred = tools.filter(isDeferredTool).map((t) => t.name);
|
|
171
|
+
// What the shared ResourceLoader serves, refreshed per turn before the session is built.
|
|
172
|
+
let prompt = typeof options.systemPrompt === "function" ? options.systemPrompt() : options.systemPrompt;
|
|
173
|
+
let skills = options.skills ?? [];
|
|
174
|
+
let services;
|
|
175
|
+
let engine;
|
|
176
|
+
const buildServices = async (modelRuntime) => createAgentSessionServices({
|
|
177
|
+
cwd,
|
|
178
|
+
agentDir: options.agentDir ?? join(cwd, ".fastagent", "pi"),
|
|
179
|
+
modelRuntime,
|
|
180
|
+
// No extensionPaths: serving does not run them (see PiAgentSessionFactoryOptions), which is
|
|
181
|
+
// the one resource question the two assemblies answer differently. The accessors read the
|
|
182
|
+
// CURRENT prompt/skills — serving refreshes both per turn, so a snapshot taken here would
|
|
183
|
+
// serve a stale definition after the first edit.
|
|
184
|
+
resourceLoaderOptions: definitionResourceLoaderOptions({
|
|
185
|
+
systemPrompt: () => prompt,
|
|
186
|
+
skills: () => skills,
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
return async (sessionId, inherit) => {
|
|
190
|
+
const fresh = options.live ? await options.live() : undefined;
|
|
191
|
+
const nextPrompt = fresh
|
|
192
|
+
? fresh.systemPrompt
|
|
193
|
+
: typeof options.systemPrompt === "function"
|
|
194
|
+
? options.systemPrompt()
|
|
195
|
+
: prompt;
|
|
196
|
+
const nextSkills = fresh ? (fresh.skills ?? []) : skills;
|
|
197
|
+
engine ??= options.engine();
|
|
198
|
+
const { modelRuntime, model } = await engine;
|
|
199
|
+
if (services === undefined) {
|
|
200
|
+
prompt = nextPrompt;
|
|
201
|
+
skills = nextSkills;
|
|
202
|
+
services = buildServices(modelRuntime); // assigned before any await: concurrent turns share it
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
// The ResourceLoader reads the overrides once and caches, so a re-read of the definition only
|
|
206
|
+
// reaches the model after a reload. Reload only when the definition ACTUALLY changed — an
|
|
207
|
+
// author edits persona.md far less often than the agent takes a turn, and a reload costs ~5ms
|
|
208
|
+
// against ~0.6ms to bind a session.
|
|
209
|
+
//
|
|
210
|
+
// "Changed" is measured against the LOADER, not against what this factory last wrote. Serving
|
|
211
|
+
// is concurrent across sessions, and a shared variable makes the check lie: one turn writes
|
|
212
|
+
// its new prompt, awaits before reloading, and the next turn sees that value already present,
|
|
213
|
+
// concludes nothing changed, and skips the reload — so the edit reaches neither the loader nor
|
|
214
|
+
// any error. Asking the loader what it is actually serving cannot go stale that way. The cost
|
|
215
|
+
// of losing the race is one redundant reload, not a swallowed edit.
|
|
216
|
+
//
|
|
217
|
+
// Skill CONTENT is not part of this — pi reads a skill from its file at invocation time, so
|
|
218
|
+
// only the declared set matters.
|
|
219
|
+
//
|
|
220
|
+
// What this deliberately does NOT provide is a per-turn snapshot. The definition is an AGENT
|
|
221
|
+
// property, not a session one: two turns running either side of an edit each get a definition
|
|
222
|
+
// that genuinely existed, and the product promise — an edit is live on the next turn — holds
|
|
223
|
+
// for both. Pinning a snapshot per turn would cost either a loader per turn or a queue in
|
|
224
|
+
// front of every bind, to buy a guarantee nothing asks for.
|
|
225
|
+
const loader = (await services).resourceLoader;
|
|
226
|
+
const definitionChanged = loader.getSystemPrompt() !== nextPrompt || loadedSkillSet(loader.getSkills().skills) !== skillSet(nextSkills);
|
|
227
|
+
if (definitionChanged) {
|
|
228
|
+
prompt = nextPrompt;
|
|
229
|
+
skills = nextSkills;
|
|
230
|
+
await loader.reload();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const sessionManager = await sessions.openOrCreate(sessionId, inherit);
|
|
234
|
+
// What the session RUNS on: the boundary plane records model/thinking overrides as entries, and
|
|
235
|
+
// pi does not read them back — a binding that ignored them would silently run every turn on the
|
|
236
|
+
// assembly default, and `state()` would report a setting no turn uses.
|
|
237
|
+
// The SAME read the control plane performs, including its integrity check: a record whose chain
|
|
238
|
+
// is broken must not run on assembly defaults while `state()` rejects it — one of the two planes
|
|
239
|
+
// would be lying. A throw here becomes this turn's `failed` event, which is where the fault has
|
|
240
|
+
// a channel to be reported through.
|
|
241
|
+
const settings = resolveSessionSettings(activePath(sessionManager), modelRuntime, {
|
|
242
|
+
model,
|
|
243
|
+
thinkingLevel: thinkingLevel ?? DEFAULT_THINKING_LEVEL,
|
|
244
|
+
});
|
|
245
|
+
const bound = {};
|
|
246
|
+
const { session } = await createAgentSessionFromServices({
|
|
247
|
+
services: await services,
|
|
248
|
+
sessionManager,
|
|
249
|
+
model: settings.model,
|
|
250
|
+
thinkingLevel: settings.thinkingLevel,
|
|
251
|
+
// pi would otherwise mount its built-ins on top of fastagent's copies, offering duplicate names.
|
|
252
|
+
// Lower-level callers with an explicit list also rely on omitted built-ins staying omitted.
|
|
253
|
+
noTools: "builtin",
|
|
254
|
+
...(excludedToolNames.length > 0 ? { excludeTools: [...excludedToolNames] } : {}),
|
|
255
|
+
customTools: toolDefinitions(tools, cwd, env, sessionId, bound),
|
|
256
|
+
});
|
|
257
|
+
bound.session = session;
|
|
258
|
+
// An extension handler that throws is otherwise dropped: pi fans errors out to registered
|
|
259
|
+
// listeners and has none by default, which on a server means a broken extension looks like an
|
|
260
|
+
// extension that simply did nothing.
|
|
261
|
+
//
|
|
262
|
+
// Deferral, then restoration: pi starts every mounted tool active, so narrow by SUBTRACTING the
|
|
263
|
+
// deferred names (robust to pi mounting tools of its own, unlike an exact-set replacement), then
|
|
264
|
+
// add back what THIS session has already discovered.
|
|
265
|
+
if (deferred.length > 0) {
|
|
266
|
+
const active = session.getActiveToolNames();
|
|
267
|
+
const mounted = new Set(session.getAllTools().map((tool) => tool.name));
|
|
268
|
+
const recorded = recordedActivations(session);
|
|
269
|
+
// A recorded name that is no longer mounted is dropped rather than replayed: pi's setter
|
|
270
|
+
// THROWS on an unknown name, so replaying one would brick every future turn of this session.
|
|
271
|
+
const restored = recorded.filter((name) => mounted.has(name));
|
|
272
|
+
const dropped = recorded.filter((name) => !mounted.has(name));
|
|
273
|
+
if (dropped.length > 0) {
|
|
274
|
+
const key = `${sessionId}\u0000${[...new Set(dropped)].sort().join(",")}`;
|
|
275
|
+
const emit = warnedDroppedActivations.has(key) ? log.debug : log.warn;
|
|
276
|
+
warnedDroppedActivations.add(key);
|
|
277
|
+
emit(`[fastagent] session ${sessionId}: dropping recorded activation(s) no longer mounted: ${[...new Set(dropped)].join(", ")}`);
|
|
278
|
+
}
|
|
279
|
+
const next = [...new Set([...active.filter((name) => !deferred.includes(name)), ...restored])];
|
|
280
|
+
if (next.length !== active.length || next.some((name) => !active.includes(name))) {
|
|
281
|
+
session.setActiveToolsByName(next);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return session;
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/** What a reload has to notice: the declared set, not the files behind it. */
|
|
288
|
+
function skillSet(skills) {
|
|
289
|
+
return skills.map((s) => `${s.name}\u0000${s.filePath}\u0000${s.description}`).join("\u0001");
|
|
290
|
+
}
|
|
291
|
+
/** The same signature, read back off the loader. */
|
|
292
|
+
function loadedSkillSet(skills) {
|
|
293
|
+
return skills.map((s) => `${s.name}\u0000${s.filePath ?? ""}\u0000${s.description}`).join("\u0001");
|
|
294
|
+
}
|
|
295
|
+
/** fastagent's Skill (content inline) as pi's (read from filePath at invocation time). */
|
|
296
|
+
function toPiSkills(skills) {
|
|
297
|
+
return skills.map((skill) => {
|
|
298
|
+
const baseDir = dirname(skill.filePath);
|
|
299
|
+
return {
|
|
300
|
+
name: skill.name,
|
|
301
|
+
description: skill.description,
|
|
302
|
+
filePath: skill.filePath,
|
|
303
|
+
baseDir,
|
|
304
|
+
sourceInfo: {
|
|
305
|
+
path: skill.filePath,
|
|
306
|
+
source: "fastagent",
|
|
307
|
+
scope: "project",
|
|
308
|
+
origin: "top-level",
|
|
309
|
+
baseDir,
|
|
310
|
+
},
|
|
311
|
+
disableModelInvocation: skill.disableModelInvocation ?? false,
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { FastagentTool } from "./tool.ts";
|
|
3
3
|
import type { Models } from "@earendil-works/pi-ai";
|
|
4
|
-
import type { AnyModel } from "./
|
|
4
|
+
import type { AnyModel } from "./models.ts";
|
|
5
5
|
export interface FastagentConfig {
|
|
6
6
|
/** "provider/modelId". Precedence: CLI --model > FASTAGENT_MODEL > config. */
|
|
7
7
|
model?: string;
|
|
@@ -9,8 +9,8 @@ export interface FastagentConfig {
|
|
|
9
9
|
* "xhigh" | "max"). Unset = pi's default. Authors tune thinking in the pi TUI while vibing — this
|
|
10
10
|
* is the serving-side counterpart (fidelity). Levels a model doesn't support are clamped by pi. */
|
|
11
11
|
thinkingLevel?: ThinkingLevel;
|
|
12
|
-
/** Extra custom tools, appended after pi
|
|
13
|
-
* plus the optional `deferred` marker (see defineTool). */
|
|
12
|
+
/** Extra custom tools, appended after the pi coding tools — never replaces them. `FastagentTool`
|
|
13
|
+
* = AgentTool plus the optional `deferred` marker (see defineTool). */
|
|
14
14
|
tools?: FastagentTool[];
|
|
15
15
|
/** `host` is the bind address: unset (or `0.0.0.0`) binds all interfaces — what containers need;
|
|
16
16
|
* `127.0.0.1` keeps the serve (including `/control/*`) off the LAN. Precedence: `--bind` > this. */
|
|
@@ -60,7 +60,7 @@ export declare function loadConfig(dir: string): Promise<LoadedConfig>;
|
|
|
60
60
|
* lookups then miss visibly (an unknown-provider error / a login-required hint), never a mangled id
|
|
61
61
|
* (`slice(0, indexOf("/"))` silently drops the last char when "/" is absent). */
|
|
62
62
|
export declare function providerOf(spec: string): string;
|
|
63
|
-
/** Resolve "provider/modelId" → a pi Model from `models`, so
|
|
63
|
+
/** Resolve "provider/modelId" → a pi Model from `models`, so auth resolves from the same collection. */
|
|
64
64
|
export declare function resolveModel(models: Models, spec: string): AnyModel;
|
|
65
65
|
/** All registered "provider/modelId" specs in `models`, sorted — the list behind `fastagent models`. */
|
|
66
66
|
export declare function listModels(models: Models): string[];
|
|
@@ -151,7 +151,7 @@ export function providerOf(spec) {
|
|
|
151
151
|
const slash = spec.indexOf("/");
|
|
152
152
|
return slash > 0 ? spec.slice(0, slash) : spec;
|
|
153
153
|
}
|
|
154
|
-
/** Resolve "provider/modelId" → a pi Model from `models`, so
|
|
154
|
+
/** Resolve "provider/modelId" → a pi Model from `models`, so auth resolves from the same collection. */
|
|
155
155
|
export function resolveModel(models, spec) {
|
|
156
156
|
const slash = spec.indexOf("/");
|
|
157
157
|
if (slash < 1 || slash === spec.length - 1) {
|