@fastagent-sh/fastagent 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +11 -0
- package/dist/channels/feishu/feishu-api.d.ts +4 -2
- package/dist/channels/feishu/feishu.js +39 -9
- package/dist/channels/feishu/invoke-turn.d.ts +8 -2
- package/dist/channels/feishu/invoke-turn.js +150 -31
- package/dist/channels/feishu/parse.js +6 -0
- package/dist/channels/http.js +15 -2
- package/dist/channels/invoke-turn-kit.d.ts +5 -2
- package/dist/channels/invoke-turn-kit.js +6 -2
- package/dist/channels/slack/invoke-turn.js +1 -1
- package/dist/channels/slack/slack.js +1 -5
- package/dist/channels/state.d.ts +0 -10
- package/dist/channels/state.js +2 -19
- package/dist/channels/telegram/invoke-turn.js +1 -1
- package/dist/channels/thread-participants.d.ts +7 -0
- package/dist/channels/thread-participants.js +3 -0
- package/dist/cli/commands/deploy.js +13 -5
- package/dist/cli/commands/dev.js +1 -1
- package/dist/cli/commands/fire.js +1 -1
- package/dist/cli/commands/info.js +21 -1
- package/dist/cli/commands/invoke.js +1 -1
- package/dist/cli/commands/start.js +1 -1
- package/dist/cli/shared.d.ts +4 -2
- package/dist/cli/shared.js +12 -5
- package/dist/collect.d.ts +30 -4
- package/dist/collect.js +39 -6
- package/dist/deploy/preflight.d.ts +8 -2
- package/dist/deploy/preflight.js +21 -3
- package/dist/deploy/secrets.d.ts +3 -0
- package/dist/deploy/secrets.js +6 -0
- package/dist/dev-supervisor.js +8 -2
- package/dist/engines/pi/create.d.ts +2 -1
- package/dist/engines/pi/create.js +12 -7
- package/dist/engines/pi/harness.d.ts +6 -3
- package/dist/engines/pi/harness.js +4 -3
- package/dist/engines/pi/invoke-session.d.ts +32 -0
- package/dist/engines/pi/invoke-session.js +171 -0
- package/dist/engines/pi/invoke.d.ts +6 -27
- package/dist/engines/pi/invoke.js +49 -208
- package/dist/engines/pi/models.d.ts +45 -11
- package/dist/engines/pi/models.js +55 -8
- package/dist/engines/pi/session-builder.js +4 -2
- package/dist/engines/pi/session-control.d.ts +2 -1
- package/dist/engines/pi/sessions.d.ts +17 -1
- package/dist/engines/pi/sessions.js +292 -10
- package/dist/engines/pi/turn-kit.d.ts +56 -0
- package/dist/engines/pi/turn-kit.js +161 -0
- package/dist/paths.d.ts +6 -0
- package/dist/paths.js +6 -0
- package/dist/pi.d.ts +3 -2
- package/dist/pi.js +1 -1
- package/dist/scaffold/templates/fastagent.config.mjs +2 -0
- package/dist/session-remote.js +10 -2
- package/package.json +1 -1
|
@@ -76,7 +76,7 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
76
76
|
failStartup(new Error(`deploy stopped: ${pre.gate}`));
|
|
77
77
|
for (const m of pre.messages)
|
|
78
78
|
console.error(`[fastagent] ${m.level}: ${m.text}`);
|
|
79
|
-
const { channels, routeChannels, longConnectionChannels, hasTimeTriggers, modelAuth, authPath, container, port, extraSecrets, } = pre;
|
|
79
|
+
const { channels, routeChannels, longConnectionChannels, hasTimeTriggers, modelAuth, modelKeyInDefinition, authPath, container, port, extraSecrets, } = pre;
|
|
80
80
|
const hasDeclaredChannels = routeChannels.length + longConnectionChannels.length > 0;
|
|
81
81
|
// Docker: one app service + loopback port + state volume. `--tunnel` shapes the generated topology
|
|
82
82
|
// with an optional Quick Tunnel service; `--run` alone decides whether Docker receives side effects.
|
|
@@ -126,6 +126,7 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
126
126
|
port,
|
|
127
127
|
requireTunnel: requestedTunnel,
|
|
128
128
|
modelAuth,
|
|
129
|
+
modelKeyInDefinition,
|
|
129
130
|
authPath,
|
|
130
131
|
channels,
|
|
131
132
|
longConnectionChannels,
|
|
@@ -176,6 +177,7 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
176
177
|
workspace,
|
|
177
178
|
name: serviceName,
|
|
178
179
|
modelAuth,
|
|
180
|
+
modelKeyInDefinition,
|
|
179
181
|
authPath,
|
|
180
182
|
channels,
|
|
181
183
|
longConnectionChannels,
|
|
@@ -278,6 +280,7 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
278
280
|
agentPrefix: container.agentPrefix,
|
|
279
281
|
name: acName,
|
|
280
282
|
modelAuth,
|
|
283
|
+
modelKeyInDefinition,
|
|
281
284
|
authPath,
|
|
282
285
|
channels,
|
|
283
286
|
extraSecrets,
|
|
@@ -371,6 +374,7 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
371
374
|
agentPrefix: container.agentPrefix,
|
|
372
375
|
appName,
|
|
373
376
|
modelAuth,
|
|
377
|
+
modelKeyInDefinition,
|
|
374
378
|
authPath,
|
|
375
379
|
channels,
|
|
376
380
|
longConnectionChannels,
|
|
@@ -468,9 +472,10 @@ function deployEnvironment(agentDir, channels) {
|
|
|
468
472
|
* when present, yields an ephemeral URL that reuses the same webhook announcer as `dev --tunnel`.
|
|
469
473
|
*/
|
|
470
474
|
async function runDeployDocker(params) {
|
|
471
|
-
const { agentDir, workspace, composeFile, port, requireTunnel, modelAuth, authPath, channels, longConnectionChannels, extraSecrets, } = params;
|
|
475
|
+
const { agentDir, workspace, composeFile, port, requireTunnel, modelAuth, modelKeyInDefinition, authPath, channels, longConnectionChannels, extraSecrets, } = params;
|
|
472
476
|
const { secrets, missingSecrets, needsModelCredential } = assembleSecrets({
|
|
473
477
|
modelAuth,
|
|
478
|
+
modelKeyInDefinition,
|
|
474
479
|
authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
|
|
475
480
|
channels,
|
|
476
481
|
longConnectionChannels,
|
|
@@ -511,7 +516,7 @@ async function runDeployDocker(params) {
|
|
|
511
516
|
* behind the shared {@link spawnRunner} seam (spawned `fly`, cwd = the workspace so the build context is the whole workspace).
|
|
512
517
|
*/
|
|
513
518
|
async function runDeployFly(params) {
|
|
514
|
-
const { agentDir, workspace, agentPrefix, appName, modelAuth, authPath, channels, longConnectionChannels, flyTomlPath, extraSecrets, } = params;
|
|
519
|
+
const { agentDir, workspace, agentPrefix, appName, modelAuth, modelKeyInDefinition, authPath, channels, longConnectionChannels, flyTomlPath, extraSecrets, } = params;
|
|
515
520
|
const fly = spawnRunner("fly", workspace);
|
|
516
521
|
// Fail fast if flyctl is absent (spawn ENOENT → 127), with the install link — not a confusing auth gate.
|
|
517
522
|
if ((await fly(["version"], { capture: true })).code === 127) {
|
|
@@ -520,6 +525,7 @@ async function runDeployFly(params) {
|
|
|
520
525
|
const region = parseFlyRegion(await readFile(flyTomlPath, "utf8")) ?? "iad";
|
|
521
526
|
const { secrets, missingSecrets, needsModelCredential } = assembleSecrets({
|
|
522
527
|
modelAuth,
|
|
528
|
+
modelKeyInDefinition,
|
|
523
529
|
authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
|
|
524
530
|
channels,
|
|
525
531
|
longConnectionChannels,
|
|
@@ -553,9 +559,10 @@ async function runDeployFly(params) {
|
|
|
553
559
|
* file (secret values off argv) is created here — 0600, removed after the run either way.
|
|
554
560
|
*/
|
|
555
561
|
async function runDeployAgentcore(params) {
|
|
556
|
-
const { agentDir, workspace, agentPrefix, name, modelAuth, authPath, channels, extraSecrets, selfSchedule } = params;
|
|
562
|
+
const { agentDir, workspace, agentPrefix, name, modelAuth, modelKeyInDefinition, authPath, channels, extraSecrets, selfSchedule, } = params;
|
|
557
563
|
const { secrets, missingSecrets, needsModelCredential } = assembleSecrets({
|
|
558
564
|
modelAuth,
|
|
565
|
+
modelKeyInDefinition,
|
|
559
566
|
authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
|
|
560
567
|
channels,
|
|
561
568
|
extraSecrets,
|
|
@@ -627,7 +634,7 @@ async function runDeployAgentcore(params) {
|
|
|
627
634
|
* webhook) lives in {@link deployRailwayRun}; see there for why Railway differs from Fly.
|
|
628
635
|
*/
|
|
629
636
|
async function runDeployRailway(params) {
|
|
630
|
-
const { agentDir, workspace, name, modelAuth, authPath, channels, longConnectionChannels, extraSecrets, intoLinked, dockerfilePath, } = params;
|
|
637
|
+
const { agentDir, workspace, name, modelAuth, modelKeyInDefinition, authPath, channels, longConnectionChannels, extraSecrets, intoLinked, dockerfilePath, } = params;
|
|
631
638
|
const railway = spawnRunner("railway", workspace);
|
|
632
639
|
// Fail fast if the railway CLI is absent (spawn ENOENT → 127), with the install link.
|
|
633
640
|
if ((await railway(["--version"], { capture: true })).code === 127) {
|
|
@@ -635,6 +642,7 @@ async function runDeployRailway(params) {
|
|
|
635
642
|
}
|
|
636
643
|
const { secrets, missingSecrets, needsModelCredential } = assembleSecrets({
|
|
637
644
|
modelAuth,
|
|
645
|
+
modelKeyInDefinition,
|
|
638
646
|
authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
|
|
639
647
|
channels,
|
|
640
648
|
longConnectionChannels,
|
package/dist/cli/commands/dev.js
CHANGED
|
@@ -56,7 +56,7 @@ async function serveOnce(dir, opts) {
|
|
|
56
56
|
reportWorkspaceHint(workspaceHint(a));
|
|
57
57
|
reportLine("config", a.configPath ?? "(none)");
|
|
58
58
|
reportLine("model", `${a.modelSpec}${a.config.thinkingLevel ? ` (thinking: ${a.config.thinkingLevel})` : ""}`);
|
|
59
|
-
await reportAuth(a.modelSpec, a.authPath);
|
|
59
|
+
await reportAuth(a.agentDir, a.modelSpec, a.authPath);
|
|
60
60
|
reportAgentsSkillsTools(a);
|
|
61
61
|
// Trace each turn's agent loop (tool calls + reply) to the log at debug level — shown in dev, gated
|
|
62
62
|
// out in start (level info), keeping end-user content out of production logs. Wired in both postures.
|
|
@@ -37,7 +37,7 @@ export async function runFire(name, dirArg, opts) {
|
|
|
37
37
|
authPath: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by the opener (one owner)
|
|
38
38
|
}).catch(failStartup);
|
|
39
39
|
console.error(`[fastagent] fire: ${name} (${modelSpec})`);
|
|
40
|
-
await reportAuth(modelSpec, authPath);
|
|
40
|
+
await reportAuth(placement.agentDir, modelSpec, authPath);
|
|
41
41
|
const exitCode = await runInvokeStream(agent.invoke({ session: scheduleSession(name) }, { text: schedule.prompt }), (text) => process.stdout.write(text), (line) => console.error(line));
|
|
42
42
|
process.stdout.write("\n");
|
|
43
43
|
process.exit(exitCode);
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { loadDotEnv } from "../../env.js";
|
|
4
4
|
import { discoverChannelFiles } from "../../engines/pi/channel.js";
|
|
5
|
-
import { defaultSessionsDir, loadConfig, resolveAuthPath, resolveModelSpec, resolveSessionsDirOverride, } from "../../engines/pi/config.js";
|
|
5
|
+
import { defaultSessionsDir, loadConfig, resolveAuthPath, resolveModel, resolveModelSpec, resolveSessionsDirOverride, } from "../../engines/pi/config.js";
|
|
6
|
+
import { createPiModelRuntime } from "../../engines/pi/models.js";
|
|
6
7
|
import { resolveStateRoot, workspaceHint } from "../../paths.js";
|
|
7
8
|
import { resolveAgentTools } from "../../engines/pi/create.js";
|
|
8
9
|
import { loadAgentDefinition } from "../../engines/pi/definition.js";
|
|
@@ -55,12 +56,27 @@ export async function runInfo(dirArg, opts) {
|
|
|
55
56
|
const stateRoot = resolveStateRoot(agentDir);
|
|
56
57
|
const sessionsDir = resolveSessionsDirOverride(opts.sessionsDir) ?? defaultSessionsDir(stateRoot);
|
|
57
58
|
const authPath = resolveAuthPath(agentDir, opts.authPath); // flag > FASTAGENT_AUTH_PATH > default — the one owner
|
|
59
|
+
// RESOLVE the spec, do not just echo it: a spec is only real once its provider/model exist in the
|
|
60
|
+
// agent's own surface (built-ins + its models.json), which is exactly what a custom endpoint changes.
|
|
61
|
+
// Reporting a healthy-looking spec that `dev`/`start` then reject is the failure this pre-empts.
|
|
62
|
+
// Reported as DATA rather than thrown — a broken agent is what `info` is for — and read-only: the
|
|
63
|
+
// runtime reads models.json without creating anything (its catalog cache is written on refresh, and
|
|
64
|
+
// there is none here).
|
|
65
|
+
const modelError = modelSpec
|
|
66
|
+
? await createPiModelRuntime({ agentDir, authPath })
|
|
67
|
+
.then((models) => {
|
|
68
|
+
resolveModel(models, modelSpec);
|
|
69
|
+
return undefined;
|
|
70
|
+
})
|
|
71
|
+
.catch((error) => error.message)
|
|
72
|
+
: undefined;
|
|
58
73
|
if (opts.json) {
|
|
59
74
|
console.log(JSON.stringify({
|
|
60
75
|
agentDir,
|
|
61
76
|
workspace,
|
|
62
77
|
configPath: configPath ?? null,
|
|
63
78
|
model: modelSpec ?? null,
|
|
79
|
+
modelError: modelError ?? null,
|
|
64
80
|
thinkingLevel: config.thinkingLevel ?? null,
|
|
65
81
|
context: definition.contextFiles.map((f) => f.path),
|
|
66
82
|
persona: definition.persona !== undefined,
|
|
@@ -85,6 +101,8 @@ export async function runInfo(dirArg, opts) {
|
|
|
85
101
|
// One padded label writer: hand-spaced labels drifted out of alignment the moment a longer one
|
|
86
102
|
// (agent/workspace/selfSchedule) joined the report.
|
|
87
103
|
const line = (label, value) => console.log(`${`${label}:`.padEnd(13)} ${value}`);
|
|
104
|
+
/** A continuation under the previous line, aligned to the same column (no label, so no bare colon). */
|
|
105
|
+
const cont = (value) => console.log(`${"".padEnd(13)} ${value}`);
|
|
88
106
|
line("agent", agentDir);
|
|
89
107
|
line("workspace", workspace);
|
|
90
108
|
const hint = workspaceHint({ agentDir, workspace });
|
|
@@ -92,6 +110,8 @@ export async function runInfo(dirArg, opts) {
|
|
|
92
110
|
line("hint", hint);
|
|
93
111
|
line("config", configPath ?? "(none)");
|
|
94
112
|
line("model", modelSpec ?? "(not set — pass --model, set FASTAGENT_MODEL, or config.model)");
|
|
113
|
+
if (modelError)
|
|
114
|
+
cont(`⚠ does not resolve: ${modelError}`);
|
|
95
115
|
if (config.thinkingLevel)
|
|
96
116
|
line("thinking", config.thinkingLevel);
|
|
97
117
|
line("context", definition.contextFiles.map((f) => f.path).join(", ") || "(none)");
|
|
@@ -20,7 +20,7 @@ export async function runInvoke(message, dirArg, opts) {
|
|
|
20
20
|
// BOTH directories, like dev/start: from the workspace, `placement.workspace` alone equals the dir you
|
|
21
21
|
// typed, so it cannot tell you which agent actually ran.
|
|
22
22
|
console.error(`[fastagent] invoke: ${placement.agentDir} (workspace ${placement.workspace}, ${modelSpec})`);
|
|
23
|
-
await reportAuth(modelSpec, authPath);
|
|
23
|
+
await reportAuth(placement.agentDir, modelSpec, authPath);
|
|
24
24
|
// Fresh session per invoke (one-shot, no resume). runInvokeStream maps events→IO: reply→stdout,
|
|
25
25
|
// tool/failure→stderr, exit 1 iff the turn failed (so CI can gate on it).
|
|
26
26
|
const exitCode = await runInvokeStream(agent.invoke({ session: randomUUID() }, { text: message }), (text) => process.stdout.write(text), (line) => console.error(line));
|
|
@@ -49,7 +49,7 @@ export async function runStart(dirArg, opts) {
|
|
|
49
49
|
reportLine("workspace", workspace);
|
|
50
50
|
reportWorkspaceHint(workspaceHint({ agentDir, workspace }));
|
|
51
51
|
reportLine("model", `${modelSpec}${config.thinkingLevel ? ` (thinking: ${config.thinkingLevel})` : ""}`);
|
|
52
|
-
await reportAuth(modelSpec, authPath);
|
|
52
|
+
await reportAuth(agentDir, modelSpec, authPath);
|
|
53
53
|
reportLine("context", definition.contextFiles.map((f) => f.path).join(", ") || "(none)");
|
|
54
54
|
if (definition.persona)
|
|
55
55
|
reportLine("persona", "persona.md");
|
package/dist/cli/shared.d.ts
CHANGED
|
@@ -26,8 +26,10 @@ export declare function parsePort(value: string | undefined, source: string, fro
|
|
|
26
26
|
* a node bind failure, or worse, as a "the interface you bound" diagnostic downstream.
|
|
27
27
|
*/
|
|
28
28
|
export declare function parseBind(value: string | undefined): string | undefined;
|
|
29
|
-
/** Report which source provides the model's credentials, surfacing a remediation hint at startup. Non-blocking.
|
|
30
|
-
|
|
29
|
+
/** Report which source provides the model's credentials, surfacing a remediation hint at startup. Non-blocking.
|
|
30
|
+
* Probes through the AGENT's model surface (`agentDir` carries its models.json), so a custom endpoint is
|
|
31
|
+
* reported like any built-in rather than as an unknown provider. */
|
|
32
|
+
export declare function reportAuth(agentDir: string, modelSpec: string, authPath: string): Promise<void>;
|
|
31
33
|
/**
|
|
32
34
|
* First-run model resolution for every assembly command (dev/start/invoke/fire/chat/deploy): ONE
|
|
33
35
|
* funnel, no dead ends. When no model is set (flag/env/config) and we're on a TTY, show the FULL
|
package/dist/cli/shared.js
CHANGED
|
@@ -10,7 +10,7 @@ import { buildModelPickerOptions } from "./models-view.js";
|
|
|
10
10
|
import { fastagentCredentialStore } from "../engines/pi/auth.js";
|
|
11
11
|
import { isValidPort, listModels, loadConfig, providerOf, resolveAuthPath, resolveModel, resolveModelSpec, rewriteConfigModel, } from "../engines/pi/config.js";
|
|
12
12
|
import { LoginCancelled, loginFlow } from "../engines/pi/login.js";
|
|
13
|
-
import { createPiModels, probeApiKey, probeAuthSource, providerAuthStatuses } from "../engines/pi/models.js";
|
|
13
|
+
import { createPiModelRuntime, createPiModels, probeApiKey, probeAuthSource, providerAuthStatuses, } from "../engines/pi/models.js";
|
|
14
14
|
import { formatAuthReport } from "./auth-view.js";
|
|
15
15
|
import { log } from "../log.js";
|
|
16
16
|
import { openExternalUrl } from "../open-url.js";
|
|
@@ -70,10 +70,13 @@ export function parseBind(value) {
|
|
|
70
70
|
failUsage(`invalid --bind "${value}": must be an IP address or "localhost"`);
|
|
71
71
|
return bindAddress(trimmed); // a name never travels past this point — see bind.ts
|
|
72
72
|
}
|
|
73
|
-
/** Report which source provides the model's credentials, surfacing a remediation hint at startup. Non-blocking.
|
|
74
|
-
|
|
73
|
+
/** Report which source provides the model's credentials, surfacing a remediation hint at startup. Non-blocking.
|
|
74
|
+
* Probes through the AGENT's model surface (`agentDir` carries its models.json), so a custom endpoint is
|
|
75
|
+
* reported like any built-in rather than as an unknown provider. */
|
|
76
|
+
export async function reportAuth(agentDir, modelSpec, authPath) {
|
|
75
77
|
const provider = providerOf(modelSpec);
|
|
76
|
-
const
|
|
78
|
+
const models = await createPiModelRuntime({ agentDir, authPath }).catch(failStartup);
|
|
79
|
+
const source = await probeAuthSource(models, modelSpec);
|
|
77
80
|
// Only when nothing satisfies auth do we read the store (refresh-FREE) to tell "nothing stored" from
|
|
78
81
|
// "stored but unusable" — see formatAuthReport for why. store.read warns on a corrupt file itself.
|
|
79
82
|
const stored = source === undefined
|
|
@@ -107,7 +110,9 @@ export async function resolveFirstRunModel(agentDir, options = {}) {
|
|
|
107
110
|
if (!isInteractive())
|
|
108
111
|
return; // CI/deploy: the opener throws the actionable missing-model error
|
|
109
112
|
const authPath = resolveAuthPath(agentDir, options.authPath);
|
|
110
|
-
|
|
113
|
+
// The picker lists the AGENT's surface: built-ins plus whatever its models.json declares, so a
|
|
114
|
+
// self-hosted endpoint is pickable on first run instead of being invisible until hand-set.
|
|
115
|
+
const models = await createPiModelRuntime({ agentDir, authPath }).catch(failStartup);
|
|
111
116
|
const chosen = await pickWithCredentials(models, authPath);
|
|
112
117
|
if (chosen === undefined)
|
|
113
118
|
return; // cancelled (or auth probe failed): the caller raises its clear missing-model error
|
|
@@ -208,6 +213,8 @@ seams = {}) {
|
|
|
208
213
|
* more than keeping a doubtful one.
|
|
209
214
|
*/
|
|
210
215
|
async function verifyApiKeyLogin(provider, authPath, spec) {
|
|
216
|
+
// Built-ins only: `login` itself offers built-in providers (login.ts), and a models.json endpoint
|
|
217
|
+
// authenticates from its own `apiKey` (env/command), so there is no stored credential to verify here.
|
|
211
218
|
const models = createPiModels({ authPath });
|
|
212
219
|
const model = spec ? resolveModel(models, spec) : models.getProvider(provider)?.getModels()[0];
|
|
213
220
|
if (!model) {
|
package/dist/collect.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Stream helpers around the SPEC's two stream disciplines.
|
|
3
|
+
*
|
|
4
|
+
* Caller side: `collect` (buffered consumption, SPEC §7) reduces an AgentEvent stream to a final
|
|
5
|
+
* value, encoding the terminal discipline (failed → throw, missing terminal → error) — streaming
|
|
6
|
+
* consumers for-await themselves.
|
|
7
|
+
*
|
|
8
|
+
* Agent side: the cancellation protocol (SPEC MUST 3), in two halves that only work together —
|
|
9
|
+
* `abortFirstIterator` delivers the consumer's knock, `cancellableStream` is what an engine wraps
|
|
10
|
+
* its turn in to receive it.
|
|
6
11
|
*/
|
|
7
12
|
import type { AgentEvent, Json } from "./agent.ts";
|
|
8
13
|
/**
|
|
@@ -13,8 +18,29 @@ import type { AgentEvent, Json } from "./agent.ts";
|
|
|
13
18
|
* settles the suspension), then delegates to `gen.return`, swallowing its rejection (the
|
|
14
19
|
* generator's own catch/finally already surfaced the outcome). `throw()` tears down identically
|
|
15
20
|
* and rethrows the caller's error deterministically instead of poking a completed generator.
|
|
21
|
+
*
|
|
22
|
+
* Cancellation also SILENCES the stream, and that belongs here rather than in each producer: a
|
|
23
|
+
* generator parked in an await can still reach a `yield` on its way out (an error path that
|
|
24
|
+
* yields a terminal, say), and that yield satisfies the pending `next()` — handing a terminal
|
|
25
|
+
* event to a consumer that already walked away, which SPEC MUST 3 forbids. Deciding it once, at
|
|
26
|
+
* the protocol boundary, is what keeps every producer from having to re-ask "is anyone still
|
|
27
|
+
* listening?" before each yield.
|
|
16
28
|
*/
|
|
17
29
|
export declare function abortFirstIterator<T>(gen: AsyncGenerator<T>, cancel: () => void): AsyncIterator<T>;
|
|
30
|
+
/** What a turn generator gets so a consumer walking away can stop it. */
|
|
31
|
+
export interface CancelHooks {
|
|
32
|
+
/** Publish the door: how to abort the engine work, once there is engine work to abort. */
|
|
33
|
+
onCancelReady: (cancel: () => void) => void;
|
|
34
|
+
/** The latch, for the window where the door is armed but the engine is still idle — knocking then
|
|
35
|
+
* does nothing, so a turn must read this before committing to work no one is waiting for. */
|
|
36
|
+
wasCancelled: () => boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Wrap a turn generator in the cancellation protocol: cancelling the returned stream latches the
|
|
40
|
+
* intent AND knocks on whatever door the generator published, in that order (the latch must be set
|
|
41
|
+
* before the knock, or a turn checking it mid-flight could miss the cancel it just received).
|
|
42
|
+
*/
|
|
43
|
+
export declare function cancellableStream<T>(start: (hooks: CancelHooks) => AsyncGenerator<T>): AsyncIterable<T>;
|
|
18
44
|
/** Exception form of a failed event (thrown by collect). Carries the failed event's fields verbatim, so
|
|
19
45
|
* a buffered consumer can branch on `code` (SPEC §8 failure subdivision) just like a streaming one. */
|
|
20
46
|
export declare class AgentFailure extends Error {
|
package/dist/collect.js
CHANGED
|
@@ -6,22 +6,55 @@
|
|
|
6
6
|
* settles the suspension), then delegates to `gen.return`, swallowing its rejection (the
|
|
7
7
|
* generator's own catch/finally already surfaced the outcome). `throw()` tears down identically
|
|
8
8
|
* and rethrows the caller's error deterministically instead of poking a completed generator.
|
|
9
|
+
*
|
|
10
|
+
* Cancellation also SILENCES the stream, and that belongs here rather than in each producer: a
|
|
11
|
+
* generator parked in an await can still reach a `yield` on its way out (an error path that
|
|
12
|
+
* yields a terminal, say), and that yield satisfies the pending `next()` — handing a terminal
|
|
13
|
+
* event to a consumer that already walked away, which SPEC MUST 3 forbids. Deciding it once, at
|
|
14
|
+
* the protocol boundary, is what keeps every producer from having to re-ask "is anyone still
|
|
15
|
+
* listening?" before each yield.
|
|
9
16
|
*/
|
|
10
17
|
export function abortFirstIterator(gen, cancel) {
|
|
18
|
+
let cancelled = false;
|
|
19
|
+
const teardown = async () => {
|
|
20
|
+
cancelled = true;
|
|
21
|
+
cancel();
|
|
22
|
+
await gen.return(undefined).catch(() => { });
|
|
23
|
+
};
|
|
11
24
|
return {
|
|
12
|
-
next
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
25
|
+
async next() {
|
|
26
|
+
const result = await gen.next();
|
|
27
|
+
return cancelled ? { done: true, value: undefined } : result;
|
|
28
|
+
},
|
|
29
|
+
async return() {
|
|
30
|
+
await teardown();
|
|
16
31
|
return { done: true, value: undefined };
|
|
17
32
|
},
|
|
18
33
|
async throw(error) {
|
|
19
|
-
|
|
20
|
-
await gen.return(undefined).catch(() => { });
|
|
34
|
+
await teardown();
|
|
21
35
|
throw error;
|
|
22
36
|
},
|
|
23
37
|
};
|
|
24
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Wrap a turn generator in the cancellation protocol: cancelling the returned stream latches the
|
|
41
|
+
* intent AND knocks on whatever door the generator published, in that order (the latch must be set
|
|
42
|
+
* before the knock, or a turn checking it mid-flight could miss the cancel it just received).
|
|
43
|
+
*/
|
|
44
|
+
export function cancellableStream(start) {
|
|
45
|
+
let door;
|
|
46
|
+
let cancelled = false;
|
|
47
|
+
const iterator = abortFirstIterator(start({
|
|
48
|
+
onCancelReady: (cancel) => {
|
|
49
|
+
door = cancel;
|
|
50
|
+
},
|
|
51
|
+
wasCancelled: () => cancelled,
|
|
52
|
+
}), () => {
|
|
53
|
+
cancelled = true;
|
|
54
|
+
door?.();
|
|
55
|
+
});
|
|
56
|
+
return { [Symbol.asyncIterator]: () => iterator };
|
|
57
|
+
}
|
|
25
58
|
/** Exception form of a failed event (thrown by collect). Carries the failed event's fields verbatim, so
|
|
26
59
|
* a buffered consumer can branch on `code` (SPEC §8 failure subdivision) just like a streaming one. */
|
|
27
60
|
export class AgentFailure extends Error {
|
|
@@ -19,9 +19,15 @@ interface DeployFacts {
|
|
|
19
19
|
* has no external wake-up, so the deployment must keep one machine running: the fly plan forces
|
|
20
20
|
* `min_machines_running=1`, the railway runbook forbids App Sleeping. */
|
|
21
21
|
hasTimeTriggers: boolean;
|
|
22
|
-
/** What satisfies model auth locally
|
|
23
|
-
*
|
|
22
|
+
/** What satisfies model auth locally — an env-var name, an OAuth/stored label, or undefined. Drives the
|
|
23
|
+
* runbook's secret guidance and `--run`'s credential carry. For a models.json endpoint keyed from the
|
|
24
|
+
* environment this is the VARIABLE NAME (see {@link modelCredentialCarry}), not the display label, so
|
|
25
|
+
* the value carries like any provider key. */
|
|
24
26
|
modelAuth: string | undefined;
|
|
27
|
+
/** The definition itself carries the model key (a models.json literal `apiKey`, or a `!command` run on
|
|
28
|
+
* the host), so there is nothing for `--run` to carry AND nothing to gate: `fastagent login` cannot
|
|
29
|
+
* serve a custom provider, so gating on it would strand a correctly configured agent. */
|
|
30
|
+
modelKeyInDefinition: boolean;
|
|
25
31
|
/** The project-level auth file `--run` reads to carry the credential (probed with the same path). */
|
|
26
32
|
authPath: string;
|
|
27
33
|
/** Container facts shared by the plan and the generated Dockerfile — ONE source, so they can't drift. */
|
package/dist/deploy/preflight.js
CHANGED
|
@@ -17,12 +17,13 @@ import { resolveAuthPath } from "../engines/pi/config.js";
|
|
|
17
17
|
import { resolveSecretsDir, resolveStateRoot } from "../paths.js";
|
|
18
18
|
import { inspectChannels } from "../engines/pi/channel.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,7 +13,7 @@
|
|
|
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";
|
|
16
|
+
import { AGENT_CONFIG_NAMES, AGENT_MODELS_FILE, resolveStateRoot } from "./paths.js";
|
|
17
17
|
import { isUnderDir } from "./engines/pi/definition.js";
|
|
18
18
|
import { dotEnvPath } from "./env.js";
|
|
19
19
|
import { log } from "./log.js";
|
|
@@ -21,7 +21,7 @@ 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
|
-
const WATCHED_HINT = "tools/, channels/, schedules/, package.json, fastagent.config.*, .secrets/.env";
|
|
24
|
+
const WATCHED_HINT = "tools/, channels/, schedules/, package.json, fastagent.config.*, models.json, .secrets/.env";
|
|
25
25
|
/**
|
|
26
26
|
* chokidar `ignored` matcher for the narrow watch scope (true = ignore), rooted at the AGENT DIR. When
|
|
27
27
|
* the agent sits INSIDE the workspace that means the surrounding tree never triggers a restart at all;
|
|
@@ -49,6 +49,12 @@ export function devWatchIgnored(root, envFile) {
|
|
|
49
49
|
return false;
|
|
50
50
|
if (rel === "package.json")
|
|
51
51
|
return false;
|
|
52
|
+
// models.json is read ONCE per worker (the model hub is built during assembly), so an edit needs a
|
|
53
|
+
// restart like any other code input. It is also the one input whose breakage KILLS the worker — a
|
|
54
|
+
// malformed file fails assembly — so leaving it unwatched would strand the author: the fix that
|
|
55
|
+
// repairs it would not be the edit that restarts.
|
|
56
|
+
if (rel === AGENT_MODELS_FILE)
|
|
57
|
+
return false;
|
|
52
58
|
const segments = rel.split(sep);
|
|
53
59
|
if (segments[0] === "tools" || segments[0] === "channels" || segments[0] === "schedules")
|
|
54
60
|
return false;
|
|
@@ -7,7 +7,8 @@ import { type AnyModel, piHarnessFactory } from "./harness.ts";
|
|
|
7
7
|
import { type PiSessionStore } from "./sessions.ts";
|
|
8
8
|
import type { ModuleLoadFailure } from "../../loader.ts";
|
|
9
9
|
import { type ToolCollision, type MountedTool } from "./tool.ts";
|
|
10
|
-
import { type
|
|
10
|
+
import { type SessionObserver } from "./invoke.ts";
|
|
11
|
+
import { type Lease } from "./turn-kit.ts";
|
|
11
12
|
/** pi's core default toolset (read/bash/edit/write). Rooted at the ExecutionEnv's cwd, supplied per
|
|
12
13
|
* turn as the harness tool context — hence no argument here. */
|
|
13
14
|
export declare function piDefaultTools(): MountedTool[];
|
|
@@ -18,12 +18,13 @@ import { defaultAuthPath, resolveModel } from "./config.js";
|
|
|
18
18
|
import { resolveSecretsDir } from "../../paths.js";
|
|
19
19
|
import { loadAgentDefinition } from "./definition.js";
|
|
20
20
|
import { DEFAULT_THINKING_LEVEL, piHarnessFactory } from "./harness.js";
|
|
21
|
-
import { createPiModels } from "./models.js";
|
|
21
|
+
import { createPiModelRuntime, createPiModels } from "./models.js";
|
|
22
22
|
import { reportFindingsIfChanged } from "./report.js";
|
|
23
23
|
import { inMemorySessionStore } from "./sessions.js";
|
|
24
24
|
import { isDeferredTool, loadTools, mergeDiscoveredTools, } from "./tool.js";
|
|
25
25
|
import { withSearchTool } from "./search-tools.js";
|
|
26
|
-
import { createPiAgentFromHarness
|
|
26
|
+
import { createPiAgentFromHarness } from "./invoke.js";
|
|
27
|
+
import { inProcessLease } from "./turn-kit.js";
|
|
27
28
|
// ── §1 tools ─────────────────────────────────────────────────────────────────
|
|
28
29
|
//
|
|
29
30
|
// The full pi toolset is the default for fidelity: authors vibe in local pi with it, so serving with
|
|
@@ -164,7 +165,7 @@ export function assembleSystemPrompt(options) {
|
|
|
164
165
|
* directory) and route through here.
|
|
165
166
|
*/
|
|
166
167
|
function buildPiAgent(opts) {
|
|
167
|
-
const models = createPiModels({ providers: opts.providers, authPath: opts.authPath });
|
|
168
|
+
const models = opts.models ?? createPiModels({ providers: opts.providers, authPath: opts.authPath });
|
|
168
169
|
const env = opts.env ?? new NodeExecutionEnv({ cwd: process.cwd() });
|
|
169
170
|
// Materialized here (not defaulted inside createPiAgentFromHarness) so the exposed parts carry
|
|
170
171
|
// the SAME lease instance the agent runs under — boundary mutations must contend on it.
|
|
@@ -241,13 +242,17 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
241
242
|
// Deferred tools need their loader on every rung (idempotent — the workspace opener already applied
|
|
242
243
|
// it; a caller's own search_tools wins).
|
|
243
244
|
const tools = withSearchTool(options.tools ?? piDefaultTools());
|
|
245
|
+
// Dir-aware default: the same secrets-dir-derived file the opener uses for this dir (the opener
|
|
246
|
+
// passes an explicit authPath, so this only affects direct L2 callers).
|
|
247
|
+
const authPath = options.authPath ?? defaultAuthPath(resolveSecretsDir(dir));
|
|
244
248
|
const agent = buildPiAgent({
|
|
245
249
|
model: options.model,
|
|
246
250
|
thinkingLevel: options.thinkingLevel,
|
|
247
|
-
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
authPath: options.
|
|
251
|
+
// THE directory rung's model surface: built-ins + the agent's own models.json (custom endpoints,
|
|
252
|
+
// which are definition data and travel with the artifact) + any injected Provider instance. This
|
|
253
|
+
// is what makes `dev`/`start`/`invoke` and an embedded L2 caller resolve the same specs.
|
|
254
|
+
models: await createPiModelRuntime({ agentDir: dir, authPath, providers: options.providers }),
|
|
255
|
+
authPath,
|
|
251
256
|
// The directory is the agent, LIVE: re-read the definition on every invoke, so AGENTS.md/skills
|
|
252
257
|
// edits (the author's, or the agent's own self-modification) take effect on the next turn with
|
|
253
258
|
// no process restart — restarts are reserved for code (tools/channels/config, module cache).
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { AgentHarness } from "@earendil-works/pi-agent-core";
|
|
10
10
|
import type { ExecutionEnv, ExecutionToolContext, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
11
11
|
import type { Model, Models } from "@earendil-works/pi-ai";
|
|
12
|
-
import { type PiSessionStore } from "./sessions.ts";
|
|
12
|
+
import { type PiSessionStore, type SessionInheritance } from "./sessions.ts";
|
|
13
13
|
import { type MountedTool } from "./tool.ts";
|
|
14
14
|
import { type OverrideEntryLike } from "./session-settings.ts";
|
|
15
15
|
/**
|
|
@@ -38,7 +38,9 @@ export type AnyModel = Model<any>;
|
|
|
38
38
|
* pi's env-backed default tools read (pi 0.83). Custom tools are context-FREE and stay assignable — a
|
|
39
39
|
* four-parameter `execute` satisfies the five-parameter one, so `defineTool` is untouched by this. */
|
|
40
40
|
type PiHarness = AgentHarness<ExecutionToolContext>;
|
|
41
|
-
export type PiHarnessFactory = (session: string
|
|
41
|
+
export type PiHarnessFactory = (session: string,
|
|
42
|
+
/** Where a NEW session starts from (sessions.ts) — an existing session ignores it. */
|
|
43
|
+
inherit?: SessionInheritance) => PiHarness | Promise<PiHarness>;
|
|
42
44
|
export interface PiHarnessFactoryOptions {
|
|
43
45
|
/** Session persistence. Continuity = same backing store + same session id. */
|
|
44
46
|
sessions: PiSessionStore;
|
|
@@ -107,6 +109,7 @@ export declare function resolveHarnessOverrides(entries: OverrideEntryLike[], mo
|
|
|
107
109
|
thinkingLevel: ThinkingLevel;
|
|
108
110
|
};
|
|
109
111
|
export declare function resolveHarnessActiveToolNames(recorded: string[] | null, tools: MountedTool[], sessionId: string): string[] | undefined;
|
|
110
|
-
/** Open-or-create the session per invoke: existing → open (history via buildContext); missing →
|
|
112
|
+
/** Open-or-create the session per invoke: existing → open (history via buildContext); missing →
|
|
113
|
+
* create, seeded from `inherit` when the scope names a parent. */
|
|
111
114
|
export declare function piHarnessFactory(options: PiHarnessFactoryOptions): PiHarnessFactory;
|
|
112
115
|
export {};
|
|
@@ -114,10 +114,11 @@ export function resolveHarnessActiveToolNames(recorded, tools, sessionId) {
|
|
|
114
114
|
}
|
|
115
115
|
return [...new Set([...initial, ...known])];
|
|
116
116
|
}
|
|
117
|
-
/** Open-or-create the session per invoke: existing → open (history via buildContext); missing →
|
|
117
|
+
/** Open-or-create the session per invoke: existing → open (history via buildContext); missing →
|
|
118
|
+
* create, seeded from `inherit` when the scope names a parent. */
|
|
118
119
|
export function piHarnessFactory(options) {
|
|
119
|
-
return async (sessionId) => {
|
|
120
|
-
const session = await options.sessions.openOrCreate(sessionId);
|
|
120
|
+
return async (sessionId, inherit) => {
|
|
121
|
+
const session = await options.sessions.openOrCreate(sessionId, inherit);
|
|
121
122
|
// One extra entry walk per invoke to collect the activation deltas — negligible against the model
|
|
122
123
|
// call, same trade as L2's per-invoke definition re-read. The walk is over the ACTIVE PATH, not
|
|
123
124
|
// the flat journal: `navigate` moves the leaf, so the tree can hold an abandoned branch whose
|