@fastagent-sh/fastagent 0.23.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +10 -0
- package/dist/channels/kit/preview-kit.d.ts +6 -1
- package/dist/channels/kit/preview-kit.js +21 -4
- package/dist/channels/slack/preview.js +2 -1
- package/dist/cli/commands/info.js +7 -7
- package/dist/cli/commands/login.js +2 -2
- package/dist/cli/commands/models.js +3 -3
- package/dist/cli/invoke-stream.js +14 -0
- package/dist/cli/shared.d.ts +2 -1
- package/dist/cli/shared.js +6 -7
- package/dist/deploy/preflight.js +19 -1
- package/dist/dev-supervisor.js +2 -2
- package/dist/engines/pi/agent-models.d.ts +14 -1
- package/dist/engines/pi/agent-models.js +35 -8
- package/dist/engines/pi/agent-session-factory.d.ts +13 -13
- package/dist/engines/pi/agent-session-factory.js +137 -159
- package/dist/engines/pi/auth.js +6 -94
- package/dist/engines/pi/create.d.ts +16 -9
- package/dist/engines/pi/create.js +71 -38
- package/dist/engines/pi/invoke-session.js +82 -16
- package/dist/engines/pi/locked-file.d.ts +9 -0
- package/dist/engines/pi/locked-file.js +102 -0
- package/dist/engines/pi/login.d.ts +7 -1
- package/dist/engines/pi/login.js +22 -6
- package/dist/engines/pi/machine.d.ts +23 -0
- package/dist/engines/pi/machine.js +24 -2
- package/dist/engines/pi/models.d.ts +47 -42
- package/dist/engines/pi/models.js +154 -67
- package/dist/engines/pi/open.d.ts +29 -12
- package/dist/engines/pi/open.js +52 -21
- package/dist/engines/pi/openai-account-models.d.ts +35 -0
- package/dist/engines/pi/openai-account-models.js +116 -0
- package/dist/engines/pi/report.d.ts +3 -0
- package/dist/engines/pi/report.js +11 -0
- package/dist/engines/pi/session-builder.js +6 -22
- package/dist/engines/pi/session-control.d.ts +4 -4
- package/dist/engines/pi/session-control.js +72 -40
- package/dist/engines/pi/session-settings.d.ts +9 -0
- package/dist/engines/pi/session-settings.js +17 -0
- package/dist/engines/pi/session-store.d.ts +2 -2
- package/dist/engines/pi/tool-context.d.ts +3 -5
- package/dist/engines/pi/tool-context.js +4 -16
- package/dist/engines/pi/tool.d.ts +32 -21
- package/dist/engines/pi/tool.js +21 -25
- package/dist/engines/pi/turn-kit.d.ts +18 -9
- package/dist/engines/pi/turn-kit.js +104 -27
- package/dist/pi.d.ts +1 -1
- package/dist/pi.js +1 -1
- package/dist/session.d.ts +60 -8
- package/dist/session.js +5 -1
- package/package.json +5 -5
- package/dist/engines/pi/search-tools.d.ts +0 -4
- package/dist/engines/pi/search-tools.js +0 -109
package/dist/agent.d.ts
CHANGED
|
@@ -47,6 +47,16 @@ export type AgentEvent = {
|
|
|
47
47
|
isError: boolean;
|
|
48
48
|
content: Json;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Advisory, non-terminal (engines MAY emit it): the running tool `id`'s current status, one line, replacing the last.
|
|
52
|
+
* It may be a line of the tool's output so far (trust it like `tool_ended.content`), never the whole output;
|
|
53
|
+
* untruncated, so a consumer that shows it clips it.
|
|
54
|
+
*/
|
|
55
|
+
| {
|
|
56
|
+
type: "tool_progress";
|
|
57
|
+
id: string;
|
|
58
|
+
text: string;
|
|
59
|
+
}
|
|
50
60
|
/**
|
|
51
61
|
* Advisory, non-terminal (engines MAY emit it): a transient internal failure scheduled a retry with backoff — the
|
|
52
62
|
* turn is still alive.
|
|
@@ -23,6 +23,8 @@ export declare const THINKING_PLACEHOLDER = "\uD83D\uDCAD Thinking\u2026";
|
|
|
23
23
|
interface ToolLine {
|
|
24
24
|
label: string;
|
|
25
25
|
status: "running" | "ok" | "error";
|
|
26
|
+
/** The call's latest `tool_progress` text, shown while it runs. */
|
|
27
|
+
progress?: string;
|
|
26
28
|
}
|
|
27
29
|
/** The channel-neutral view STATE of one in-flight turn. */
|
|
28
30
|
export interface TurnView {
|
|
@@ -38,7 +40,10 @@ export interface TurnView {
|
|
|
38
40
|
}
|
|
39
41
|
export declare function createTurnView(): TurnView;
|
|
40
42
|
export declare function applyTurnEvent(view: TurnView, e: AgentEvent, now?: number): boolean;
|
|
41
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* The tool-activity block: one `🔧 label …/✓/✗` line per call, in call order. A running call shows its latest
|
|
45
|
+
* progress after the mark (`🔧 Bash npm test … › 42 passed`); a finished one drops it.
|
|
46
|
+
*/
|
|
42
47
|
export declare function toolLines(view: TurnView): string;
|
|
43
48
|
/** The reasoning peek: the most recent tail of the (growing) reasoning, one line, code-point safe. */
|
|
44
49
|
export declare function thinkingLine(view: TurnView, maxTail: number): string;
|
|
@@ -36,6 +36,13 @@ export function applyTurnEvent(view, e, now = Date.now()) {
|
|
|
36
36
|
view.toolById.set(e.id, line);
|
|
37
37
|
return true;
|
|
38
38
|
}
|
|
39
|
+
case "tool_progress": {
|
|
40
|
+
const line = view.toolById.get(e.id);
|
|
41
|
+
if (line?.status !== "running")
|
|
42
|
+
return closedRetry;
|
|
43
|
+
line.progress = e.text;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
39
46
|
case "tool_ended": {
|
|
40
47
|
const line = view.toolById.get(e.id);
|
|
41
48
|
if (line)
|
|
@@ -50,9 +57,17 @@ export function applyTurnEvent(view, e, now = Date.now()) {
|
|
|
50
57
|
}
|
|
51
58
|
}
|
|
52
59
|
const TOOL_MARK = { running: "…", ok: "✓", error: "✗" };
|
|
53
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* The tool-activity block: one `🔧 label …/✓/✗` line per call, in call order. A running call shows its latest
|
|
62
|
+
* progress after the mark (`🔧 Bash npm test … › 42 passed`); a finished one drops it.
|
|
63
|
+
*/
|
|
54
64
|
export function toolLines(view) {
|
|
55
|
-
return view.tools
|
|
65
|
+
return view.tools
|
|
66
|
+
.map((t) => {
|
|
67
|
+
const progress = t.status === "running" && t.progress ? ` › ${clip(t.progress, TOOL_PROGRESS_MAX)}` : "";
|
|
68
|
+
return `🔧 ${t.label} ${TOOL_MARK[t.status]}${progress}`;
|
|
69
|
+
})
|
|
70
|
+
.join("\n");
|
|
56
71
|
}
|
|
57
72
|
/** The reasoning peek: the most recent tail of the (growing) reasoning, one line, code-point safe. */
|
|
58
73
|
export function thinkingLine(view, maxTail) {
|
|
@@ -77,15 +92,17 @@ export function composeTurnBody(parts) {
|
|
|
77
92
|
}
|
|
78
93
|
/** Max length (code points) of a tool's arg preview. */
|
|
79
94
|
const TOOL_ARG_MAX = 48;
|
|
95
|
+
/** Max length (code points) of a running tool's progress line: one status, not its output. */
|
|
96
|
+
const TOOL_PROGRESS_MAX = 64;
|
|
80
97
|
/** Max length (code points) of a humanized tool label. */
|
|
81
98
|
const TOOL_NAME_MAX = 80;
|
|
82
99
|
/**
|
|
83
100
|
* One-line, truncated at code-point boundaries: collapse whitespace so a multi-line command/arg stays on one line, and
|
|
84
101
|
* never tear a surrogate pair mid-emoji.
|
|
85
102
|
*/
|
|
86
|
-
function clip(s) {
|
|
103
|
+
function clip(s, max = TOOL_ARG_MAX) {
|
|
87
104
|
const one = s.replace(/\s+/g, " ").trim();
|
|
88
|
-
return truncateCodePointPrefix(one,
|
|
105
|
+
return truncateCodePointPrefix(one, max);
|
|
89
106
|
}
|
|
90
107
|
/**
|
|
91
108
|
* A compact, human-readable preview of a tool call's args so the live view reads `🔧 read AGENTS.md` rather than just
|
|
@@ -119,7 +119,8 @@ function streamClassicSlackReply(events, api, target, formatError, initialPrevie
|
|
|
119
119
|
let lastSent = "";
|
|
120
120
|
const view = () => composeTurnBody([
|
|
121
121
|
THINKING_PLACEHOLDER,
|
|
122
|
-
|
|
122
|
+
// Tool names, arguments and progress lines are untrusted text, like the answer.
|
|
123
|
+
sanitizeSlackMarkdown(toolLines(turn)),
|
|
123
124
|
turn.retrying ? RETRY_NOTICE : "",
|
|
124
125
|
sanitizeSlackMarkdown(revealedAnswer(turn, CLASSIC_UPDATE_INTERVAL_MS, now())),
|
|
125
126
|
]);
|
|
@@ -8,7 +8,7 @@ import { agentModels } from "../../engines/pi/agent-models.js";
|
|
|
8
8
|
import { resolveSessionsDir, resolveStateRoot } from "../../paths.js";
|
|
9
9
|
import { CODING_TOOL_NAMES, resolveAgentTools } from "../../engines/pi/create.js";
|
|
10
10
|
import { loadAgentDefinition } from "../../engines/pi/definition.js";
|
|
11
|
-
import { reportFindingsIfChanged, reportToolCollisions } from "../../engines/pi/report.js";
|
|
11
|
+
import { describeIndirectTools, reportFindingsIfChanged, reportToolCollisions } from "../../engines/pi/report.js";
|
|
12
12
|
import { allSecrets, describeSecrets, missingSecrets } from "../../declared-secrets.js";
|
|
13
13
|
import { log } from "../../log.js";
|
|
14
14
|
import { reportModuleLoadFailures } from "../../loader.js";
|
|
@@ -32,7 +32,7 @@ export async function runInfo(dirArg, opts) {
|
|
|
32
32
|
const tools = await resolveAgentTools(config, agentDir, workspace)
|
|
33
33
|
.then((r) => ({
|
|
34
34
|
names: r.toolNames,
|
|
35
|
-
|
|
35
|
+
indirect: r.indirectTools,
|
|
36
36
|
collisions: r.toolCollisions,
|
|
37
37
|
failures: r.toolFailures,
|
|
38
38
|
secrets: allSecrets(r.toolSecrets),
|
|
@@ -40,7 +40,7 @@ export async function runInfo(dirArg, opts) {
|
|
|
40
40
|
}))
|
|
41
41
|
.catch((e) => ({
|
|
42
42
|
names: [],
|
|
43
|
-
|
|
43
|
+
indirect: [],
|
|
44
44
|
collisions: [],
|
|
45
45
|
failures: [],
|
|
46
46
|
secrets: [],
|
|
@@ -75,7 +75,7 @@ export async function runInfo(dirArg, opts) {
|
|
|
75
75
|
const sessionsDir = resolveSessionsDir(agentDir);
|
|
76
76
|
// Both layers: "what is this agent's state" is the question `info` answers, and a credential it runs on can live in
|
|
77
77
|
// a file the agent dir does not contain.
|
|
78
|
-
const models = agentModels(agentDir);
|
|
78
|
+
const models = agentModels(agentDir, {}, { cwd: workspace });
|
|
79
79
|
const { auth } = models;
|
|
80
80
|
// RESOLVE the spec, do not just echo it: a spec is only real once its provider/model exist in the agent's own
|
|
81
81
|
// surface (built-ins + its models.json), which is exactly what a custom endpoint changes.
|
|
@@ -107,7 +107,7 @@ export async function runInfo(dirArg, opts) {
|
|
|
107
107
|
persona: definition.persona !== undefined,
|
|
108
108
|
skills: skills.map((skill) => ({ name: skill.name, description: skill.description })),
|
|
109
109
|
tools: tools.names,
|
|
110
|
-
|
|
110
|
+
indirectTools: tools.indirect,
|
|
111
111
|
toolError: tools.error ?? null,
|
|
112
112
|
channels,
|
|
113
113
|
routines,
|
|
@@ -145,8 +145,8 @@ export async function runInfo(dirArg, opts) {
|
|
|
145
145
|
line("persona", definition.persona ? "persona.md" : "(none)");
|
|
146
146
|
line("skills", skills.map((skill) => skill.name).join(", ") || "(none)");
|
|
147
147
|
line("tools", tools.error ? "(could not load — see warning below)" : tools.names.join(", ") || "(none)");
|
|
148
|
-
if (tools.
|
|
149
|
-
line("
|
|
148
|
+
if (tools.indirect.length > 0)
|
|
149
|
+
line("indirect", describeIndirectTools(tools.indirect));
|
|
150
150
|
line("channels", channels.join(", ") || "(none)");
|
|
151
151
|
line("routines", routines.map((r) => `${r.name} (${r.cron === null ? "on demand" : `next ${r.next ?? "never"}`})`).join(", ") ||
|
|
152
152
|
"(none)");
|
|
@@ -9,7 +9,7 @@ import { enterAgentEnv } from "../../env.js";
|
|
|
9
9
|
import { GLOBAL_AUTH_PATH, resolveAuthPath } from "../../engines/pi/auth.js";
|
|
10
10
|
import { agentModels } from "../../engines/pi/agent-models.js";
|
|
11
11
|
import { DEPLOY_HOSTS } from "../../deploy/hosts.js";
|
|
12
|
-
import { findAgentDir, globalHome, placementDeadEnd } from "../../paths.js";
|
|
12
|
+
import { findAgentDir, globalHome, placementDeadEnd, resolvePlacement } from "../../paths.js";
|
|
13
13
|
import { LoginCancelled, loginFlow } from "../../engines/pi/login.js";
|
|
14
14
|
import { environmentAuthSource } from "../../engines/pi/models.js";
|
|
15
15
|
import { loginOnBox } from "../box-login.js";
|
|
@@ -148,7 +148,7 @@ async function stdioLogin(io, provider, opts) {
|
|
|
148
148
|
enterAgentEnv(agentDir);
|
|
149
149
|
// The model environment the serving runtime reads (createPiAgentFromDir), so "logged in" means what the server will
|
|
150
150
|
// use.
|
|
151
|
-
const models = agentModels(agentDir);
|
|
151
|
+
const models = agentModels(agentDir, {}, { cwd: resolvePlacement(agentDir).workspace });
|
|
152
152
|
const { auth } = models;
|
|
153
153
|
if (opts.ifMissing && provider) {
|
|
154
154
|
// The server's own answer (`authStatus`), the one its startup report prints. Known ceiling: nothing is asked of
|
|
@@ -7,8 +7,8 @@ import { join } from "node:path";
|
|
|
7
7
|
import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
|
|
8
8
|
import { formatModelsCommand } from "../models-view.js";
|
|
9
9
|
import { listModels } from "../../engines/pi/config.js";
|
|
10
|
-
import { createPiModelRuntime, globalCatalogPath, machineModelRuntime
|
|
11
|
-
import { refreshModelCatalog } from "../../engines/pi/open.js";
|
|
10
|
+
import { createPiModelRuntime, globalCatalogPath, machineModelRuntime } from "../../engines/pi/models.js";
|
|
11
|
+
import { refreshMachineModelCatalog, refreshModelCatalog } from "../../engines/pi/open.js";
|
|
12
12
|
import { AGENT_MODEL_CATALOG_FILE, findAgentDir, globalHome, placementDeadEnd } from "../../paths.js";
|
|
13
13
|
import { enterAgentEnv } from "../../env.js";
|
|
14
14
|
import { failStartup, placementOrExit } from "../fail.js";
|
|
@@ -29,7 +29,7 @@ export async function runModels(search, opts = {}) {
|
|
|
29
29
|
console.error(`[fastagent] refreshed ${join(agentDir, AGENT_MODEL_CATALOG_FILE)} — commit it: it ships with a deploy`);
|
|
30
30
|
}
|
|
31
31
|
else {
|
|
32
|
-
await
|
|
32
|
+
await refreshMachineModelCatalog().catch(failStartup);
|
|
33
33
|
console.error(`[fastagent] refreshed ${globalCatalogPath()} — every agent on this machine reads it`);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -10,6 +10,9 @@ export async function runInvokeStream(events, out, err) {
|
|
|
10
10
|
toolName.set(event.id, event.name);
|
|
11
11
|
err(`[tool] ${event.name}`);
|
|
12
12
|
break;
|
|
13
|
+
case "tool_progress":
|
|
14
|
+
err(`[tool] ${toolName.get(event.id) ?? event.id} › ${clipStatus(event.text)}`);
|
|
15
|
+
break;
|
|
13
16
|
case "tool_ended":
|
|
14
17
|
if (event.isError)
|
|
15
18
|
err(`[tool] ${toolName.get(event.id) ?? event.id} failed`);
|
|
@@ -28,3 +31,14 @@ export async function runInvokeStream(events, out, err) {
|
|
|
28
31
|
}
|
|
29
32
|
return exitCode;
|
|
30
33
|
}
|
|
34
|
+
/** How much of a status line a terminal gets, in code points. */
|
|
35
|
+
const STATUS_MAX = 120;
|
|
36
|
+
/**
|
|
37
|
+
* A `tool_progress` line clipped to {@link STATUS_MAX} code points. The event is untruncated by contract, and a shell
|
|
38
|
+
* printing one long line (`curl -s` of a JSON body) re-sends that growing line every 100 ms.
|
|
39
|
+
*/
|
|
40
|
+
function clipStatus(text) {
|
|
41
|
+
// Only the first STATUS_MAX + 1 code points decide the result, and they fit in twice as many UTF-16 units.
|
|
42
|
+
const head = [...text.slice(0, (STATUS_MAX + 1) * 2)];
|
|
43
|
+
return head.length > STATUS_MAX ? `${head.slice(0, STATUS_MAX).join("")}…` : head.join("");
|
|
44
|
+
}
|
package/dist/cli/shared.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type LoginIO } from "../engines/pi/login.ts";
|
|
2
2
|
import { type AgentModels } from "../engines/pi/agent-models.ts";
|
|
3
|
+
import { type IndirectTool } from "../engines/pi/create.ts";
|
|
3
4
|
import type { LoadedDefinition } from "../engines/pi/definition.ts";
|
|
4
5
|
import type { ToolCollision } from "../engines/pi/tool.ts";
|
|
5
6
|
import { type ResolvedPlacement } from "../paths.ts";
|
|
@@ -19,7 +20,7 @@ export interface ReportableAssembly {
|
|
|
19
20
|
};
|
|
20
21
|
definition: LoadedDefinition;
|
|
21
22
|
toolNames: string[];
|
|
22
|
-
|
|
23
|
+
indirectTools: IndirectTool[];
|
|
23
24
|
toolCollisions: ToolCollision[];
|
|
24
25
|
}
|
|
25
26
|
/** What `dev` and `start` say about the directory they just opened, in the order they say it. */
|
package/dist/cli/shared.js
CHANGED
|
@@ -13,7 +13,7 @@ import { providerAuthStatuses } from "../engines/pi/models.js";
|
|
|
13
13
|
import { agentModels } from "../engines/pi/agent-models.js";
|
|
14
14
|
import { formatAuthReport } from "./auth-view.js";
|
|
15
15
|
import { CODING_TOOL_NAMES } from "../engines/pi/create.js";
|
|
16
|
-
import { reportFindingsIfChanged, reportToolCollisions } from "../engines/pi/report.js";
|
|
16
|
+
import { describeIndirectTools, reportFindingsIfChanged, reportToolCollisions } from "../engines/pi/report.js";
|
|
17
17
|
import { isDeployedWorkspace } from "../paths.js";
|
|
18
18
|
import { log } from "../log.js";
|
|
19
19
|
import { enterAgentEnv } from "../env.js";
|
|
@@ -24,7 +24,7 @@ import { failStartup, failUsage, placementOrExit } from "./fail.js";
|
|
|
24
24
|
export async function enterAgentCommand(dirArg, opts) {
|
|
25
25
|
const placement = placementOrExit(resolve(dirArg));
|
|
26
26
|
enterAgentEnv(placement.agentDir);
|
|
27
|
-
await resolveFirstRunModel(placement
|
|
27
|
+
await resolveFirstRunModel(placement, opts);
|
|
28
28
|
return placement;
|
|
29
29
|
}
|
|
30
30
|
/** The padded label writer for the STARTUP report (`dev`/`start`, stderr via the log level). */
|
|
@@ -48,9 +48,8 @@ export async function reportAssembly(a, extras = {}) {
|
|
|
48
48
|
reportLine("codingTools", CODING_TOOL_NAMES.join(", "));
|
|
49
49
|
if (a.toolNames.length > 0)
|
|
50
50
|
reportLine("tools", a.toolNames.join(", "));
|
|
51
|
-
if (a.
|
|
52
|
-
reportLine("
|
|
53
|
-
}
|
|
51
|
+
if (a.indirectTools.length > 0)
|
|
52
|
+
reportLine("indirect", describeIndirectTools(a.indirectTools));
|
|
54
53
|
reportToolCollisions(a.toolCollisions);
|
|
55
54
|
for (const [label, value] of extras.afterTools ?? [])
|
|
56
55
|
reportLine(label, value);
|
|
@@ -107,7 +106,7 @@ export async function reportAuth(models, modelSpec) {
|
|
|
107
106
|
/**
|
|
108
107
|
* First-run model resolution for every assembly command (dev/start/invoke/fire/chat/deploy): ONE funnel, no dead ends.
|
|
109
108
|
*/
|
|
110
|
-
async function resolveFirstRunModel(agentDir, options = {}) {
|
|
109
|
+
async function resolveFirstRunModel({ agentDir, workspace }, options = {}) {
|
|
111
110
|
const { config, path: configPath } = await loadConfig(agentDir).catch(failStartup);
|
|
112
111
|
if (resolveModelSpec(options.model, config))
|
|
113
112
|
return; // already set (flag > FASTAGENT_MODEL > config)
|
|
@@ -115,7 +114,7 @@ async function resolveFirstRunModel(agentDir, options = {}) {
|
|
|
115
114
|
return; // --no-input: never prompt (clig) — the opener raises the clear error
|
|
116
115
|
if (!isInteractive())
|
|
117
116
|
return; // CI/deploy: the opener throws the actionable missing-model error
|
|
118
|
-
const environment = agentModels(agentDir);
|
|
117
|
+
const environment = agentModels(agentDir, {}, { cwd: workspace });
|
|
119
118
|
// The picker lists the AGENT's surface: built-ins plus whatever its models.json declares, so a self-hosted endpoint
|
|
120
119
|
// is pickable on first run instead of being invisible until hand-set.
|
|
121
120
|
const models = await environment.runtime().catch(failStartup);
|
package/dist/deploy/preflight.js
CHANGED
|
@@ -13,6 +13,7 @@ import { AGENT_MODEL_CATALOG_FILE, AGENT_MODELS_FILE, exists } from "../paths.js
|
|
|
13
13
|
import { inspectChannels } from "../channels/discover.js";
|
|
14
14
|
import { loadRoutines } from "../schedule/discover.js";
|
|
15
15
|
import { resolveAgentTools } from "../engines/pi/create.js";
|
|
16
|
+
import { agentModels } from "../engines/pi/agent-models.js";
|
|
16
17
|
import { allSecrets } from "../declared-secrets.js";
|
|
17
18
|
import { createPiModelRuntime, definitionKeyOf, environmentAuthSource, literalKeyProviders, isBuiltinProvider, machineModels, globalCatalogPath, inGlobalCatalog, interactiveLoginKind, loginProviders, } from "../engines/pi/models.js";
|
|
18
19
|
import { CHANNEL_KINDS } from "../scaffold/add-channel.js";
|
|
@@ -160,7 +161,15 @@ async function gatherFacts(input, report) {
|
|
|
160
161
|
if (modelSpec)
|
|
161
162
|
await checkGlobalCatalogModel(modelSpec, deployed, report);
|
|
162
163
|
const authPath = resolveAuthPath(agentDir);
|
|
163
|
-
|
|
164
|
+
// A model only the definition's extensions declare (a virtual model, or a provider one registers) is authenticated
|
|
165
|
+
// by that code on the box, per request: a virtual model's credential is whichever physical model it routes to.
|
|
166
|
+
// Asked of the catalog the box builds, extensions included, so deploy judges the model the box will run.
|
|
167
|
+
const fromExtension = modelSpec ? await extensionDeclared(agentDir, workspace, modelSpec, deployed) : false;
|
|
168
|
+
if (fromExtension && modelSpec) {
|
|
169
|
+
report.note(`${modelSpec} is declared by the definition's extensions, which resolve its credentials on the box — deploy ` +
|
|
170
|
+
`cannot check them. Set the keys the models it uses need in ${valueFile}`);
|
|
171
|
+
}
|
|
172
|
+
const route = modelSpec && !fromExtension ? await credentialRoute(agentDir, modelSpec, values) : {};
|
|
164
173
|
const modelAuth = route.envVar;
|
|
165
174
|
const boxLogin = route.boxLogin;
|
|
166
175
|
if (boxLogin !== undefined) {
|
|
@@ -297,6 +306,15 @@ async function credentialRoute(agentDir, spec, values) {
|
|
|
297
306
|
return isEnvKey(fromValues) ? { envVar: fromValues } : {};
|
|
298
307
|
return { boxLogin: provider };
|
|
299
308
|
}
|
|
309
|
+
/** Whether `spec` exists only once the definition's extensions have registered their models. */
|
|
310
|
+
async function extensionDeclared(agentDir, workspace, spec, deployed) {
|
|
311
|
+
const provider = providerOf(spec);
|
|
312
|
+
const id = spec.slice(provider.length + 1);
|
|
313
|
+
if (deployed.getModel(provider, id))
|
|
314
|
+
return false;
|
|
315
|
+
const catalog = await agentModels(agentDir, { credentialStore: new InMemoryCredentialStore() }, { machineLayer: false, cwd: workspace }).runtime();
|
|
316
|
+
return catalog.getModel(provider, id) !== undefined;
|
|
317
|
+
}
|
|
300
318
|
/** Does `provider` offer an interactive login, i.e. can `fastagent login --deployment` authenticate it on the box? */
|
|
301
319
|
function hasLogin(providerId) {
|
|
302
320
|
const provider = loginProviders().find((p) => p.id === providerId);
|
package/dist/dev-supervisor.js
CHANGED
|
@@ -29,8 +29,8 @@ export function devWatchIgnored(root, envFile) {
|
|
|
29
29
|
return false;
|
|
30
30
|
if (rel === "package.json")
|
|
31
31
|
return false;
|
|
32
|
-
// models.json is read ONCE per worker (
|
|
33
|
-
// any other code input.
|
|
32
|
+
// models.json is read ONCE per worker (agentModels' snapshot, which every session's runtime is built from), so an
|
|
33
|
+
// edit needs a restart like any other code input.
|
|
34
34
|
if (rel === AGENT_MODELS_FILE)
|
|
35
35
|
return false;
|
|
36
36
|
// So is the model catalog: a `models --refresh` during `dev` restarts the worker onto the new models.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* login, the model list — so none of them can read other credential layers or another registry than the runtime
|
|
6
6
|
* does. Each used to compose these parts itself, and two of those copies drifted from the rule (#636, #660).
|
|
7
7
|
*/
|
|
8
|
+
import type { ExecutionEnv } from "@earendil-works/pi-agent-core";
|
|
8
9
|
import type { Credential, CredentialStore, Models, Provider } from "@earendil-works/pi-ai";
|
|
9
10
|
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
10
11
|
import { type AuthLayers, type CredentialSourceOptions, type FastagentAuthOptions } from "./auth.ts";
|
|
@@ -30,17 +31,29 @@ export interface AgentModels {
|
|
|
30
31
|
credentials: CredentialStore;
|
|
31
32
|
/**
|
|
32
33
|
* The registry: pi's built-ins, plus (with a directory) the agent's `models.json` and model catalog over the
|
|
33
|
-
* machine's, plus `providers
|
|
34
|
+
* machine's, plus `providers` and extension model declarations. An unbound catalog, built on first use and shared
|
|
35
|
+
* after; session bindings use createRuntime and their own extension instances.
|
|
34
36
|
*/
|
|
35
37
|
runtime(): Promise<ModelRuntime>;
|
|
38
|
+
/** A session-local registry, before its extensions are loaded. Credentials remain shared. */
|
|
39
|
+
createRuntime(): Promise<ModelRuntime>;
|
|
36
40
|
/**
|
|
37
41
|
* What authenticates `provider` here, resolved exactly as a turn resolves it: the ONE answer the startup report and a
|
|
38
42
|
* deployed box's `login --if-missing` both give. `modelId` picks the model to probe with; the provider's first one
|
|
39
43
|
* otherwise (auth is provider-scoped).
|
|
40
44
|
*/
|
|
41
45
|
authStatus(provider: string, modelId?: string): Promise<AuthStatus>;
|
|
46
|
+
/**
|
|
47
|
+
* The definition's extension entry points, discovered once: what this catalog registers models from and what every
|
|
48
|
+
* session of the assembly built over it loads. Empty without a directory.
|
|
49
|
+
*/
|
|
50
|
+
extensionPaths(): Promise<readonly string[]>;
|
|
42
51
|
}
|
|
43
52
|
export interface AgentModelsOptions {
|
|
53
|
+
/** Workspace for extension model registration; defaults to agentDir for standalone definitions. */
|
|
54
|
+
cwd?: string;
|
|
55
|
+
/** Where `extensions/` is listed from; Node's filesystem by default. */
|
|
56
|
+
env?: ExecutionEnv;
|
|
44
57
|
/** Extra providers registered on top of the built-ins (a same id replaces one). */
|
|
45
58
|
providers?: readonly Provider[];
|
|
46
59
|
/**
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { definitionServices } from "./agent-session-factory.js";
|
|
3
|
+
import { loadExtensionPaths } from "./definition.js";
|
|
2
4
|
import { GLOBAL_AUTH_PATH, assertOneCredentialSource, fastagentCredentialStore, resolveAuthLayers, } from "./auth.js";
|
|
3
5
|
import { createPiModelRuntime, environmentAuthSource, modelRuntimeFiles, piModelsOver, probeAuthSource, } from "./models.js";
|
|
4
6
|
export function agentModels(agentDir, source = {}, options = {}) {
|
|
@@ -9,17 +11,40 @@ export function agentModels(agentDir, source = {}, options = {}) {
|
|
|
9
11
|
: agentDir
|
|
10
12
|
? resolveAuthLayers(agentDir, source.authPath)
|
|
11
13
|
: { path: source.authPath ?? GLOBAL_AUTH_PATH };
|
|
14
|
+
// The model files, read once for every runtime this agent builds: a session's and the control plane's.
|
|
15
|
+
let modelFiles;
|
|
16
|
+
const readModelFiles = () => (modelFiles ??= modelRuntimeFiles({
|
|
17
|
+
...(agentDir ? { agentDir } : {}),
|
|
18
|
+
...(machineLayer !== undefined ? { machineLayer } : {}),
|
|
19
|
+
}));
|
|
12
20
|
const store = files
|
|
13
|
-
? agentCredentialStore(files, {
|
|
21
|
+
? agentCredentialStore(files, readModelFiles, { providers, warn: source.warn })
|
|
14
22
|
: undefined;
|
|
15
23
|
const credentials = source.credentialStore ?? store;
|
|
24
|
+
const createRuntime = async () => createPiModelRuntime({
|
|
25
|
+
credentials,
|
|
26
|
+
files: await readModelFiles(),
|
|
27
|
+
...(agentDir ? { agentDir } : {}),
|
|
28
|
+
...(providers ? { providers } : {}),
|
|
29
|
+
...(machineLayer !== undefined ? { machineLayer } : {}),
|
|
30
|
+
});
|
|
31
|
+
const cwd = options.cwd ?? agentDir;
|
|
32
|
+
let discovered;
|
|
33
|
+
const extensionPaths = () => (discovered ??= agentDir
|
|
34
|
+
? loadExtensionPaths(agentDir, { ...(cwd ? { cwd } : {}), ...(options.env ? { env: options.env } : {}) })
|
|
35
|
+
: Promise.resolve([]));
|
|
16
36
|
let registry;
|
|
17
37
|
const runtime = () => {
|
|
18
|
-
registry ??=
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
38
|
+
registry ??= createRuntime().then(async (models) => {
|
|
39
|
+
if (agentDir) {
|
|
40
|
+
await definitionServices({
|
|
41
|
+
cwd: cwd ?? agentDir,
|
|
42
|
+
modelRuntime: models,
|
|
43
|
+
definition: { skills: [] },
|
|
44
|
+
extensionPaths: await extensionPaths(),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return models;
|
|
23
48
|
});
|
|
24
49
|
return registry;
|
|
25
50
|
};
|
|
@@ -27,6 +52,8 @@ export function agentModels(agentDir, source = {}, options = {}) {
|
|
|
27
52
|
...(files ? { auth: files } : {}),
|
|
28
53
|
credentials,
|
|
29
54
|
runtime,
|
|
55
|
+
createRuntime,
|
|
56
|
+
extensionPaths,
|
|
30
57
|
async authStatus(provider, modelId) {
|
|
31
58
|
const models = await runtime();
|
|
32
59
|
const id = modelId ?? models.getProvider(provider)?.getModels()[0]?.id;
|
|
@@ -66,7 +93,7 @@ export function createPiModels(options = {}) {
|
|
|
66
93
|
* there. A deployment never has the global file, so a global login that outranked, say, an `ANTHROPIC_API_KEY` in
|
|
67
94
|
* `.secrets/.env` would run one credential here and a different one deployed.
|
|
68
95
|
*/
|
|
69
|
-
function agentCredentialStore(auth, options) {
|
|
96
|
+
function agentCredentialStore(auth, modelFiles, options) {
|
|
70
97
|
const { warn, providers = [] } = options;
|
|
71
98
|
const fallback = auth.fallback;
|
|
72
99
|
if (fallback === undefined)
|
|
@@ -76,7 +103,7 @@ function agentCredentialStore(auth, options) {
|
|
|
76
103
|
project ??= (async () => {
|
|
77
104
|
const runtime = await ModelRuntime.create({
|
|
78
105
|
credentials: fastagentCredentialStore(auth.path, { warn }),
|
|
79
|
-
...(await
|
|
106
|
+
...(await (await modelFiles()).create()),
|
|
80
107
|
refreshOnCreate: false,
|
|
81
108
|
});
|
|
82
109
|
for (const provider of providers)
|
|
@@ -3,7 +3,7 @@ import type { Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
|
3
3
|
import { type AgentSession, type AgentSessionServices, type CompactionResult, type CreateAgentSessionServicesOptions, type ModelRuntime, type ResolvedCommand, SessionManager, createAgentSessionFromServices } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { PiAgentSessionFactory } from "./invoke-session.ts";
|
|
5
5
|
import type { PiSessionRecordStore } from "./session-store.ts";
|
|
6
|
-
import {
|
|
6
|
+
import type { MountedTool } from "./tool.ts";
|
|
7
7
|
import { type AnyModel } from "./models.ts";
|
|
8
8
|
interface PiSessionDefinition {
|
|
9
9
|
systemPrompt?: string;
|
|
@@ -12,11 +12,12 @@ interface PiSessionDefinition {
|
|
|
12
12
|
export interface PiAgentSessionFactoryOptions {
|
|
13
13
|
/** Where conversations live. */
|
|
14
14
|
sessions: PiSessionRecordStore;
|
|
15
|
-
/**
|
|
15
|
+
/** A fresh model runtime per binding: extension routers and provider registrations belong to this session. */
|
|
16
16
|
engine: () => Promise<{
|
|
17
17
|
modelRuntime: ModelRuntime;
|
|
18
|
-
model: AnyModel;
|
|
19
18
|
}>;
|
|
19
|
+
/** Resolved after extensions register their models. */
|
|
20
|
+
modelSpec: string;
|
|
20
21
|
thinkingLevel?: ThinkingLevel;
|
|
21
22
|
tools?: MountedTool[];
|
|
22
23
|
/** Read once per binding; prompt and skills come from the same definition read. */
|
|
@@ -42,19 +43,11 @@ export interface BindPiSessionOptions {
|
|
|
42
43
|
excludedToolNames?: readonly string[];
|
|
43
44
|
/** The CALLER's session id — what a tool asking which conversation it is in hears. */
|
|
44
45
|
sessionId?: string;
|
|
45
|
-
/** Record each discovered activation on the session, so the next bind restores it. */
|
|
46
|
-
recordActivations: boolean;
|
|
47
46
|
}
|
|
48
47
|
/**
|
|
49
|
-
* Bind ONE pi session to a record:
|
|
50
|
-
* copies kept off, and deferral applied.
|
|
48
|
+
* Bind ONE pi session to a record: authored tools replace same-name built-ins; Pi owns exposure and discovery.
|
|
51
49
|
*/
|
|
52
50
|
export declare function bindPiSession(options: BindPiSessionOptions): ReturnType<typeof createAgentSessionFromServices>;
|
|
53
|
-
/**
|
|
54
|
-
* Announce extensions pi failed to load. pi collects them into `LoadExtensionsResult.errors` and carries on with the
|
|
55
|
-
* rest.
|
|
56
|
-
*/
|
|
57
|
-
export declare function reportExtensionErrors(services: AgentSessionServices): void;
|
|
58
51
|
/** What pi is allowed to discover, minus the parts each assembly fills in itself. */
|
|
59
52
|
type DefinitionLoaderOptions = NonNullable<CreateAgentSessionServicesOptions["resourceLoaderOptions"]>;
|
|
60
53
|
/** The resource posture a fastagent definition asks pi for — ONE definition of it, for both assemblies. */
|
|
@@ -81,9 +74,16 @@ export declare function startCompaction(session: AgentSession, instructions: str
|
|
|
81
74
|
admission: Promise<CompactAdmission>;
|
|
82
75
|
done: Promise<CompactionResult>;
|
|
83
76
|
};
|
|
77
|
+
/** A fresh loader and Pi's native model registration, over a session-local runtime. */
|
|
78
|
+
export declare function definitionServices(options: {
|
|
79
|
+
cwd: string;
|
|
80
|
+
modelRuntime: ModelRuntime;
|
|
81
|
+
definition: PiSessionDefinition;
|
|
82
|
+
extensionPaths: readonly string[];
|
|
83
|
+
}): Promise<AgentSessionServices>;
|
|
84
84
|
/**
|
|
85
85
|
* The `/name` commands a served session dispatches, named the way pi resolves them (a name two extensions share gets
|
|
86
|
-
* a `:N` suffix). Loaded through the same {@link
|
|
86
|
+
* a `:N` suffix). Loaded through the same {@link definitionServices} a turn binds, so the menu and the dispatch cannot
|
|
87
87
|
* disagree. Loading runs the extensions' factories; no session opens, so `session_start` does not fire, and the
|
|
88
88
|
* instances are never bound (any action they call throws).
|
|
89
89
|
*/
|