@fastagent-sh/fastagent 0.13.0 → 0.14.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 +2 -2
- package/dist/channels/feishu/context-buffer.d.ts +46 -0
- package/dist/channels/feishu/context-buffer.js +133 -0
- package/dist/channels/feishu/crypto.d.ts +4 -2
- package/dist/channels/feishu/crypto.js +4 -2
- package/dist/channels/feishu/feishu-api.d.ts +4 -4
- package/dist/channels/feishu/feishu-api.js +2 -2
- package/dist/channels/feishu/feishu.d.ts +11 -1
- package/dist/channels/feishu/feishu.js +143 -30
- package/dist/channels/feishu/invoke-turn.d.ts +19 -13
- package/dist/channels/feishu/invoke-turn.js +70 -19
- package/dist/channels/feishu/model.d.ts +97 -0
- package/dist/channels/feishu/model.js +9 -0
- package/dist/channels/feishu/normalize.d.ts +22 -0
- package/dist/channels/feishu/normalize.js +132 -0
- package/dist/channels/feishu/owned-threads.d.ts +11 -0
- package/dist/channels/feishu/owned-threads.js +47 -0
- package/dist/channels/feishu/parse.d.ts +20 -102
- package/dist/channels/feishu/parse.js +35 -145
- package/dist/channels/feishu/preview.js +2 -2
- package/dist/channels/feishu/scaffold/channel.ts +10 -3
- package/dist/channels/feishu/seen.js +12 -12
- package/dist/channels/lark/scaffold/channel.ts +10 -3
- package/dist/cli/commands/add.d.ts +9 -0
- package/dist/cli/commands/add.js +142 -0
- package/dist/cli/commands/chat.d.ts +3 -0
- package/dist/cli/commands/chat.js +16 -0
- package/dist/cli/commands/deploy.d.ts +13 -0
- package/dist/cli/commands/deploy.js +338 -0
- package/dist/cli/commands/dev.d.ts +11 -0
- package/dist/cli/commands/dev.js +76 -0
- package/dist/cli/commands/fire.d.ts +7 -0
- package/dist/cli/commands/fire.js +45 -0
- package/dist/cli/commands/info.d.ts +7 -0
- package/dist/cli/commands/info.js +108 -0
- package/dist/cli/commands/init.d.ts +8 -0
- package/dist/cli/commands/init.js +81 -0
- package/dist/cli/commands/invoke.d.ts +7 -0
- package/dist/cli/commands/invoke.js +29 -0
- package/dist/cli/commands/login.d.ts +6 -0
- package/dist/cli/commands/login.js +63 -0
- package/dist/cli/commands/models.d.ts +1 -0
- package/dist/cli/commands/models.js +15 -0
- package/dist/cli/commands/schedule.d.ts +12 -0
- package/dist/cli/commands/schedule.js +89 -0
- package/dist/cli/commands/start.d.ts +10 -0
- package/dist/cli/commands/start.js +90 -0
- package/dist/cli/commands/tool.d.ts +1 -0
- package/dist/cli/commands/tool.js +37 -0
- package/dist/cli/fail.d.ts +19 -0
- package/dist/cli/fail.js +32 -0
- package/dist/cli/kernel.d.ts +89 -0
- package/dist/cli/kernel.js +190 -0
- package/dist/cli/program.d.ts +11 -0
- package/dist/cli/program.js +421 -0
- package/dist/cli/serve.d.ts +28 -0
- package/dist/cli/serve.js +90 -0
- package/dist/cli/shared.d.ts +24 -0
- package/dist/cli/shared.js +116 -0
- package/dist/cli.js +8 -1329
- package/dist/deploy/docker/plan.d.ts +45 -0
- package/dist/deploy/docker/plan.js +139 -0
- package/dist/deploy/docker/run.d.ts +40 -0
- package/dist/deploy/docker/run.js +126 -0
- package/dist/deploy/preflight.js +4 -3
- package/dist/deploy/runner.d.ts +4 -1
- package/dist/deploy/runner.js +1 -0
- package/dist/engines/pi/auth.js +160 -46
- package/dist/engines/pi/chat.js +77 -4
- package/dist/engines/pi/config.d.ts +12 -3
- package/dist/engines/pi/config.js +16 -1
- package/dist/engines/pi/create.d.ts +14 -5
- package/dist/engines/pi/create.js +44 -9
- package/dist/engines/pi/harness.d.ts +16 -1
- package/dist/engines/pi/harness.js +77 -1
- package/dist/engines/pi/invoke.d.ts +1 -1
- package/dist/engines/pi/invoke.js +37 -2
- package/dist/engines/pi/login.js +1 -1
- package/dist/engines/pi/search-tools.d.ts +10 -0
- package/dist/engines/pi/search-tools.js +138 -0
- package/dist/engines/pi/tool-context.d.ts +28 -0
- package/dist/engines/pi/tool-context.js +8 -0
- package/dist/engines/pi/tool.d.ts +32 -1
- package/dist/engines/pi/tool.js +42 -1
- package/dist/engines/pi/workspace.d.ts +4 -1
- package/dist/engines/pi/workspace.js +3 -1
- package/dist/pi.d.ts +2 -1
- package/dist/scaffold/add-channel.js +3 -1
- package/dist/scaffold/templates/fastagent.config.mjs +1 -0
- package/package.json +7 -4
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fastagent deploy <host> [dir]`: generate host artifacts from the resolved definition and print an
|
|
3
|
+
* ordered deploy runbook. Host-scoped (`docker` | `fly` | `railway` — the extension seam). It does NOT
|
|
4
|
+
* run the host CLI by default: fastagent owns the definition-aware artifacts and precise runbook;
|
|
5
|
+
* Docker may opt into a generated ephemeral tunnel, while durable ingress stays operator-owned. The pre-flight
|
|
6
|
+
* (config/model/channels/container facts) is host-neutral; the host branch adds its config + run drive.
|
|
7
|
+
* Read-only on the definition; the only writes are generated artifacts (never clobbered without
|
|
8
|
+
* --force). `--run` drives the target CLI instead of printing.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
11
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
12
|
+
import { registerFeishuWebhook } from "../../channels/feishu/register-webhook.js";
|
|
13
|
+
import { registerTelegramWebhook } from "../../channels/telegram/register-webhook.js";
|
|
14
|
+
import { isGeneratedDockerfile } from "../../deploy/container.js";
|
|
15
|
+
import { composeHasTunnelService, dockerWebhookPaths, isGeneratedCompose, planDockerDeploy, toDockerProjectName, } from "../../deploy/docker/plan.js";
|
|
16
|
+
import { deployDockerRun } from "../../deploy/docker/run.js";
|
|
17
|
+
import { parseFlyAppName, parseFlyMinMachines, parseFlyRegion, planFlyDeploy, toFlyAppName, } from "../../deploy/fly/plan.js";
|
|
18
|
+
import { deployFlyRun } from "../../deploy/fly/run.js";
|
|
19
|
+
import { preflightDeploy } from "../../deploy/preflight.js";
|
|
20
|
+
import { planRailwayDeploy } from "../../deploy/railway/plan.js";
|
|
21
|
+
import { deployRailwayRun } from "../../deploy/railway/run.js";
|
|
22
|
+
import { spawnRunner } from "../../deploy/runner.js";
|
|
23
|
+
import { assembleSecrets } from "../../deploy/secrets.js";
|
|
24
|
+
import { loadDotEnv } from "../../env.js";
|
|
25
|
+
import { loadConfig, resolveAgentDir, resolveAuthPathOverride, resolveModelSpec } from "../../engines/pi/config.js";
|
|
26
|
+
import { installProxyFetch } from "../../proxy.js";
|
|
27
|
+
import { openExternalUrl } from "../../open-url.js";
|
|
28
|
+
import { exists } from "../../scaffold/init.js";
|
|
29
|
+
import { announceWebhooks } from "../../tunnel.js";
|
|
30
|
+
import { failStartup, failUsage } from "../fail.js";
|
|
31
|
+
export async function runDeploy(host, dirArg, opts) {
|
|
32
|
+
const target = resolve(dirArg);
|
|
33
|
+
if (opts.tunnel && host !== "docker") {
|
|
34
|
+
// A flag/host combination the parser cannot see (host is an argument) — usage class, exit 2.
|
|
35
|
+
failUsage(`deploy stopped: --tunnel is supported only by the local Docker target`);
|
|
36
|
+
}
|
|
37
|
+
loadDotEnv(target); // a custom provider/tool may read a key at config load
|
|
38
|
+
installProxyFetch(); // post-deploy channel API calls must honor HTTP(S)_PROXY under Node
|
|
39
|
+
const { config } = await loadConfig(target).catch(failStartup);
|
|
40
|
+
const agentDir = resolveAgentDir(target, config);
|
|
41
|
+
const modelSpec = resolveModelSpec(opts.model, config);
|
|
42
|
+
// The host-neutral pre-flight (model-travel gate, channel discovery, model-auth probe, container facts +
|
|
43
|
+
// their warnings) lives in deploy/preflight.ts — testable in isolation. The CLI prints its messages and
|
|
44
|
+
// stops on its gate; the host branch below adds only the host-specific artifacts + runbook + run drive.
|
|
45
|
+
const pre = await preflightDeploy({
|
|
46
|
+
target,
|
|
47
|
+
agentDir,
|
|
48
|
+
config,
|
|
49
|
+
modelSpec,
|
|
50
|
+
run: !!opts.run,
|
|
51
|
+
force: !!opts.force,
|
|
52
|
+
authPathOverride: resolveAuthPathOverride(opts.authPath),
|
|
53
|
+
}).catch(failStartup);
|
|
54
|
+
if (!pre.ok)
|
|
55
|
+
failStartup(new Error(`deploy stopped: ${pre.gate}`));
|
|
56
|
+
for (const m of pre.messages)
|
|
57
|
+
console.error(`[fastagent] ${m.level}: ${m.text}`);
|
|
58
|
+
const { channels, hasTimeTriggers, modelAuth, authPath, container, port, extraSecrets } = pre;
|
|
59
|
+
// Docker: one app service + loopback port + state volume. `--tunnel` shapes the generated topology
|
|
60
|
+
// with an optional Quick Tunnel service; `--run` alone decides whether Docker receives side effects.
|
|
61
|
+
if (host === "docker") {
|
|
62
|
+
if (opts.stop || opts.scaleToZero === false) {
|
|
63
|
+
console.error(`[fastagent] warn: --stop/--no-scale-to-zero are Fly-only — local Compose stays running`);
|
|
64
|
+
}
|
|
65
|
+
if (opts.intoLinked) {
|
|
66
|
+
console.error(`[fastagent] warn: --into-linked is Railway-only — ignored for local Docker`);
|
|
67
|
+
}
|
|
68
|
+
const projectName = toDockerProjectName(basename(target));
|
|
69
|
+
const dockerPlan = (tunnel) => planDockerDeploy({ projectName, port, modelAuth, channels, tunnel, extraSecrets, ...container });
|
|
70
|
+
const requestedTunnel = !!opts.tunnel;
|
|
71
|
+
let plan = dockerPlan(requestedTunnel);
|
|
72
|
+
// An existing Compose file is authoritative: shape its comparison/runbook from the topology on disk,
|
|
73
|
+
// regardless of the current flag. `--force` is the explicit reset to the requested generated shape.
|
|
74
|
+
const composeFile = join(target, plan.composePath);
|
|
75
|
+
let keptWithoutRequestedTunnel = false;
|
|
76
|
+
if (!opts.force && (await exists(composeFile))) {
|
|
77
|
+
const existingHasTunnel = composeHasTunnelService(await readFile(composeFile, "utf8"));
|
|
78
|
+
plan = dockerPlan(existingHasTunnel);
|
|
79
|
+
keptWithoutRequestedTunnel = requestedTunnel && !existingHasTunnel;
|
|
80
|
+
}
|
|
81
|
+
await writeArtifacts(target, plan.artifacts, {
|
|
82
|
+
force: !!opts.force,
|
|
83
|
+
neverForce: container.kitDir ? [".dockerignore"] : [],
|
|
84
|
+
});
|
|
85
|
+
if (opts.run) {
|
|
86
|
+
return runDeployDocker({
|
|
87
|
+
target,
|
|
88
|
+
agentDir,
|
|
89
|
+
composeFile: plan.composePath,
|
|
90
|
+
port,
|
|
91
|
+
requireTunnel: requestedTunnel,
|
|
92
|
+
modelAuth,
|
|
93
|
+
authPath,
|
|
94
|
+
channels,
|
|
95
|
+
extraSecrets,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (keptWithoutRequestedTunnel) {
|
|
99
|
+
console.error(`[fastagent] warn: --tunnel was requested but kept ${plan.composePath} has no "tunnel" service — ` +
|
|
100
|
+
`edit it, delete it and regenerate, or pass --force`);
|
|
101
|
+
}
|
|
102
|
+
console.log(plan.runbook.join("\n"));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Railway: thin config file, scale-to-zero is a manual dashboard step, the URL is minted (see
|
|
106
|
+
// planRailwayDeploy). --run drives the railway CLI to completion; otherwise print the runbook.
|
|
107
|
+
if (host === "railway") {
|
|
108
|
+
if (opts.stop || opts.scaleToZero === false) {
|
|
109
|
+
console.error(`[fastagent] warn: --stop/--no-scale-to-zero are Fly-only — Railway's App Sleeping is a dashboard toggle ` +
|
|
110
|
+
`(the runbook states the manual step).`);
|
|
111
|
+
}
|
|
112
|
+
// Railway service names are project-scoped (not globally unique like a Fly app); slug the dir
|
|
113
|
+
// basename so a name with spaces/odd chars can't break the `railway add --service <name>` command.
|
|
114
|
+
const serviceName = basename(target)
|
|
115
|
+
.replace(/[^a-zA-Z0-9-]+/g, "-")
|
|
116
|
+
.replace(/^-+|-+$/g, "") || "agent";
|
|
117
|
+
const plan = planRailwayDeploy({ serviceName, modelAuth, channels, extraSecrets, hasTimeTriggers, ...container });
|
|
118
|
+
await writeArtifacts(target, plan.artifacts, {
|
|
119
|
+
force: !!opts.force,
|
|
120
|
+
neverForce: container.kitDir ? [".dockerignore"] : [],
|
|
121
|
+
});
|
|
122
|
+
if (opts.run) {
|
|
123
|
+
return runDeployRailway({
|
|
124
|
+
target,
|
|
125
|
+
name: serviceName,
|
|
126
|
+
modelAuth,
|
|
127
|
+
authPath,
|
|
128
|
+
channels,
|
|
129
|
+
extraSecrets,
|
|
130
|
+
intoLinked: !!opts.intoLinked,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
console.log(plan.runbook.join("\n"));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// host === "fly".
|
|
137
|
+
if (opts.intoLinked) {
|
|
138
|
+
console.error(`[fastagent] warn: --into-linked is railway-only (fly --run is idempotent — it reuses an existing app/volume)`);
|
|
139
|
+
}
|
|
140
|
+
// The replay floor that makes scale-to-zero safe is Telegram-only (its L1 turn store). GitHub turns
|
|
141
|
+
// are fire-and-forget (no replay), so the generated fly.toml keeps one machine running for them —
|
|
142
|
+
// a note, not a warn, since the plan already did the safe thing (definition-aware autostop).
|
|
143
|
+
if (channels.includes("github")) {
|
|
144
|
+
console.error(`[fastagent] note: github turns have no replay — the generated fly.toml uses min_machines_running=1 ` +
|
|
145
|
+
`(no scale-to-zero) so autostop can't drop an in-flight review. Set it to 0 to accept that trade.`);
|
|
146
|
+
}
|
|
147
|
+
// Two consistent modes. KEEP (no --force): an existing fly.toml is authoritative — not rewritten,
|
|
148
|
+
// and the runbook reads its `app=` (Fly app names are globally unique, so the basename guess may be
|
|
149
|
+
// taken and the user renamed it). --force: the template is authoritative — the WHOLE fly.toml resets
|
|
150
|
+
// (app→basename, region→iad, vm→defaults), so we do NOT round-trip `app` and warn that hand edits go.
|
|
151
|
+
// Kit layout: fly.toml lives under the kit (agent/fly.toml) — the host repo's own fly.toml (if any)
|
|
152
|
+
// belongs to the host's product deploy and is never read or written here.
|
|
153
|
+
const flyTomlPath = container.kitDir ? join(target, container.kitDir, "fly.toml") : join(target, "fly.toml");
|
|
154
|
+
const flyTomlExists = await exists(flyTomlPath);
|
|
155
|
+
const keptApp = flyTomlExists && !opts.force ? parseFlyAppName(await readFile(flyTomlPath, "utf8")) : undefined;
|
|
156
|
+
const appName = keptApp ?? toFlyAppName(basename(target));
|
|
157
|
+
if (keptApp)
|
|
158
|
+
console.error(`[fastagent] app: ${keptApp} (from fly.toml)`);
|
|
159
|
+
if (flyTomlExists && opts.force) {
|
|
160
|
+
console.error(`[fastagent] warn: --force resets fly.toml to defaults (app, region, vm) — re-apply any hand edits`);
|
|
161
|
+
}
|
|
162
|
+
// Autostop flags shape the GENERATED fly.toml only. In KEEP mode (fly.toml exists, no --force) it is
|
|
163
|
+
// not rewritten, so the flags would silently do nothing — surface that instead of a confusing no-op.
|
|
164
|
+
if (flyTomlExists && !opts.force && (opts.stop || opts.scaleToZero === false)) {
|
|
165
|
+
console.error(`[fastagent] warn: --stop/--no-scale-to-zero only shape a freshly generated fly.toml — yours exists and ` +
|
|
166
|
+
`was kept. Edit auto_stop_machines/min_machines_running in fly.toml, or pass --force to regenerate.`);
|
|
167
|
+
}
|
|
168
|
+
// KEEP mode + time triggers: the kept fly.toml may still scale to zero — which would sleep through every
|
|
169
|
+
// cron instant / wake-up. The generated plan can't fix a kept file, so surface it instead of the preflight
|
|
170
|
+
// note silently not applying (the author who deployed FIRST and added schedules LATER hits exactly this).
|
|
171
|
+
// Under `--run` this is a GATE (same discipline as the model-travel gate): a full deploy whose schedules
|
|
172
|
+
// silently never fire is worse than a crash-loop — nothing fails visibly when a cron instant passes on a
|
|
173
|
+
// sleeping machine, and unlike github's min=0 there is no legitimate trade to accept here.
|
|
174
|
+
if (flyTomlExists && !opts.force && hasTimeTriggers) {
|
|
175
|
+
const min = parseFlyMinMachines(await readFile(flyTomlPath, "utf8"));
|
|
176
|
+
if ((min ?? 0) === 0) {
|
|
177
|
+
// undefined = the line is absent — Fly's platform default for min_machines_running is 0, so a
|
|
178
|
+
// hand-written fly.toml without the line scales to zero exactly like an explicit 0.
|
|
179
|
+
const msg = `your kept fly.toml scales to zero (min_machines_running = ${min ?? "absent → platform default 0"}), but ` +
|
|
180
|
+
`schedules/self-scheduling need a running machine (no external wake-up). Set min_machines_running = 1, ` +
|
|
181
|
+
`or pass --force to regenerate.`;
|
|
182
|
+
if (opts.run)
|
|
183
|
+
failStartup(new Error(`deploy stopped: ${msg}`));
|
|
184
|
+
console.error(`[fastagent] warn: ${msg}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const plan = planFlyDeploy({
|
|
188
|
+
appName,
|
|
189
|
+
port,
|
|
190
|
+
modelAuth,
|
|
191
|
+
channels,
|
|
192
|
+
extraSecrets,
|
|
193
|
+
hasTimeTriggers,
|
|
194
|
+
...container,
|
|
195
|
+
autostop: opts.stop ? "stop" : "suspend",
|
|
196
|
+
scaleToZero: opts.scaleToZero !== false,
|
|
197
|
+
});
|
|
198
|
+
await writeArtifacts(target, plan.artifacts, {
|
|
199
|
+
force: !!opts.force,
|
|
200
|
+
neverForce: container.kitDir ? [".dockerignore"] : [],
|
|
201
|
+
});
|
|
202
|
+
if (opts.run)
|
|
203
|
+
return runDeployFly({ target, appName, modelAuth, authPath, channels, flyTomlPath, extraSecrets });
|
|
204
|
+
console.log(plan.runbook.join("\n"));
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Write each generated artifact. An existing file is KEPT unless --force — deploy NEVER clobbers a file
|
|
208
|
+
* without it (no silent data loss). A Dockerfile/Compose file WE generated is definition-derived, so a
|
|
209
|
+
* KEPT one that no longer matches what deploy would generate now (config/channel/lockfile/version drift —
|
|
210
|
+
* OR the user's own edits, which we can't distinguish) is flagged stale; --force regenerates it. A
|
|
211
|
+
* hand-written Dockerfile/Compose/ignore or fly.toml's app+region state is simply kept.
|
|
212
|
+
*/
|
|
213
|
+
async function writeArtifacts(target, artifacts, options) {
|
|
214
|
+
for (const a of artifacts) {
|
|
215
|
+
const abs = join(target, a.path);
|
|
216
|
+
// Host-owned paths (the root .dockerignore in the agentDir layout): --force means "MY generated
|
|
217
|
+
// artifact is authoritative", which never licenses clobbering the HOST's file — keep it always.
|
|
218
|
+
if (options.neverForce?.includes(a.path) && (await exists(abs))) {
|
|
219
|
+
console.error(`[fastagent] kept ${a.path} — the host repo's own file (never overwritten, even with --force); ` +
|
|
220
|
+
`see the preflight warnings for what it must contain`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (!options.force && (await exists(abs))) {
|
|
224
|
+
const existing = await readFile(abs, "utf8");
|
|
225
|
+
const generatedDrift = (a.path.endsWith("Dockerfile") && isGeneratedDockerfile(existing)) ||
|
|
226
|
+
(a.path.endsWith("fastagent.compose.yml") && isGeneratedCompose(existing));
|
|
227
|
+
if (generatedDrift && existing !== a.content) {
|
|
228
|
+
console.error(`[fastagent] kept ${a.path} — it no longer matches what deploy would generate (config changed, or ` +
|
|
229
|
+
`you edited it); pass --force to regenerate.`);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
console.error(`[fastagent] kept ${a.path} (exists — pass --force to overwrite)`);
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
await mkdir(dirname(abs), { recursive: true }); // kit-layout artifacts live under agent/
|
|
237
|
+
await writeFile(abs, a.content);
|
|
238
|
+
console.error(`[fastagent] wrote ${a.path}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* `deploy docker --run`: carry local credentials into Compose's child environment, then reconcile the
|
|
243
|
+
* user-owned local topology. Docker owns container/network/volume lifecycle. A Compose tunnel service,
|
|
244
|
+
* when present, yields an ephemeral URL that reuses the same webhook announcer as `dev --tunnel`.
|
|
245
|
+
*/
|
|
246
|
+
async function runDeployDocker(params) {
|
|
247
|
+
const { target, agentDir, composeFile, port, requireTunnel, modelAuth, authPath, channels, extraSecrets } = params;
|
|
248
|
+
const { secrets, missingSecrets, needsModelCredential } = assembleSecrets({
|
|
249
|
+
modelAuth,
|
|
250
|
+
authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
|
|
251
|
+
channels,
|
|
252
|
+
extraSecrets,
|
|
253
|
+
env: process.env,
|
|
254
|
+
});
|
|
255
|
+
const outcome = await deployDockerRun({ composeFile, port, secrets, missingSecrets, needsModelCredential, requireTunnel }, spawnRunner("docker", target), (message) => console.error(`[fastagent] ${message}`));
|
|
256
|
+
if (!outcome.ok)
|
|
257
|
+
failStartup(new Error(`deploy stopped: ${outcome.gate}`));
|
|
258
|
+
const compose = `docker compose -f ${composeFile}`;
|
|
259
|
+
console.error(`[fastagent] running${outcome.url ? ` → ${outcome.url}` : ""}`);
|
|
260
|
+
console.error(`[fastagent] logs: ${compose} logs -f agent`);
|
|
261
|
+
console.error(`[fastagent] stop: ${compose} down (state volume is kept)`);
|
|
262
|
+
if (outcome.tunnelUrl) {
|
|
263
|
+
// Docker Desktop commonly injects a host proxy. The Quick Tunnel hostname may be resolvable only
|
|
264
|
+
// through it, exactly like provider/channel APIs; use the same Node dispatcher as dev/start/login.
|
|
265
|
+
installProxyFetch();
|
|
266
|
+
await announceWebhooks(agentDir, outcome.tunnelUrl, { openUrl: openExternalUrl });
|
|
267
|
+
console.error(`[fastagent] note: Quick Tunnel URLs are ephemeral — after the tunnel container/Docker daemon ` +
|
|
268
|
+
`restarts, re-run this deploy so webhooks receive the new URL`);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const paths = dockerWebhookPaths(channels);
|
|
272
|
+
if (paths.length > 0) {
|
|
273
|
+
console.error(`[fastagent] note: public ingress is operator-owned — configure your tunnel/proxy, then wire the ` +
|
|
274
|
+
`default webhook path(s): ${paths.join(", ")} (or your remapped channel routes)`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* `deploy fly --run`: drive flyctl to completion (idempotent, resumable). Gathers the secret VALUES
|
|
279
|
+
* from the local env — the model key (env auth) or the whole auth.json as a `FASTAGENT_AUTH_SEED` seed
|
|
280
|
+
* (OAuth/stored auth: the deployed box materializes it onto the /data volume on first boot, so a
|
|
281
|
+
* personal deploy runs on the SAME subscription) plus channel secrets — then runs the flyctl steps
|
|
282
|
+
* behind the shared {@link spawnRunner} seam (spawned `fly`, cwd = the workspace so the build context is the agent).
|
|
283
|
+
*/
|
|
284
|
+
async function runDeployFly(params) {
|
|
285
|
+
const { target, appName, modelAuth, authPath, channels, flyTomlPath, extraSecrets } = params;
|
|
286
|
+
const fly = spawnRunner("fly", target);
|
|
287
|
+
// Fail fast if flyctl is absent (spawn ENOENT → 127), with the install link — not a confusing auth gate.
|
|
288
|
+
if ((await fly(["version"], { capture: true })).code === 127) {
|
|
289
|
+
failStartup(new Error(`flyctl not found — install it: https://fly.io/docs/flyctl/install, then re-run`));
|
|
290
|
+
}
|
|
291
|
+
const region = parseFlyRegion(await readFile(flyTomlPath, "utf8")) ?? "iad";
|
|
292
|
+
const { secrets, missingSecrets, needsModelCredential } = assembleSecrets({
|
|
293
|
+
modelAuth,
|
|
294
|
+
authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
|
|
295
|
+
channels,
|
|
296
|
+
extraSecrets,
|
|
297
|
+
env: process.env,
|
|
298
|
+
});
|
|
299
|
+
// Model credential has its OWN remediation (login), distinct from a missing secret's (.env) — gate it
|
|
300
|
+
// here, not through missingSecrets, so the message isn't a contradictory mash of both.
|
|
301
|
+
if (needsModelCredential) {
|
|
302
|
+
failStartup(new Error(`deploy stopped: no model credential — run \`fastagent login\`, or set a provider API key in .env, then re-run`));
|
|
303
|
+
}
|
|
304
|
+
const outcome = await deployFlyRun({ appName, region, secrets, missingSecrets, channels, flyConfig: "fly.toml" }, fly, (m) => console.error(`[fastagent] ${m}`), (baseUrl) => registerTelegramWebhook(baseUrl), (baseUrl, kind) => registerFeishuWebhook(baseUrl, kind));
|
|
305
|
+
if (!outcome.ok)
|
|
306
|
+
failStartup(new Error(`deploy stopped: ${outcome.gate}`));
|
|
307
|
+
console.error(`[fastagent] deployed → https://${appName}.fly.dev`);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* `deploy railway --run`: drive the railway CLI to completion. Mirrors {@link runDeployFly} — same
|
|
311
|
+
* credential carry (env key OR the OAuth auth.json as `FASTAGENT_AUTH_SEED`) via {@link assembleSecrets},
|
|
312
|
+
* same runner seam (spawned `railway`, cwd = the workspace so `railway up`'s upload is the agent). The
|
|
313
|
+
* Railway-specific sequence (linked-check → init/add/volume when fresh → variables → up → domain →
|
|
314
|
+
* webhook) lives in {@link deployRailwayRun}; see there for why Railway differs from Fly.
|
|
315
|
+
*/
|
|
316
|
+
async function runDeployRailway(params) {
|
|
317
|
+
const { target, name, modelAuth, authPath, channels, extraSecrets, intoLinked } = params;
|
|
318
|
+
const railway = spawnRunner("railway", target);
|
|
319
|
+
// Fail fast if the railway CLI is absent (spawn ENOENT → 127), with the install link.
|
|
320
|
+
if ((await railway(["--version"], { capture: true })).code === 127) {
|
|
321
|
+
failStartup(new Error(`railway CLI not found — install it: https://docs.railway.com/guides/cli, then re-run`));
|
|
322
|
+
}
|
|
323
|
+
const { secrets, missingSecrets, needsModelCredential } = assembleSecrets({
|
|
324
|
+
modelAuth,
|
|
325
|
+
authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
|
|
326
|
+
channels,
|
|
327
|
+
extraSecrets,
|
|
328
|
+
env: process.env,
|
|
329
|
+
});
|
|
330
|
+
// Model credential has its OWN remediation (login), distinct from a missing secret's (.env).
|
|
331
|
+
if (needsModelCredential) {
|
|
332
|
+
failStartup(new Error(`deploy stopped: no model credential — run \`fastagent login\`, or set a provider API key in .env, then re-run`));
|
|
333
|
+
}
|
|
334
|
+
const outcome = await deployRailwayRun({ name, mountPath: "/data", secrets, missingSecrets, channels, intoLinked }, railway, (m) => console.error(`[fastagent] ${m}`), (baseUrl) => registerTelegramWebhook(baseUrl), (baseUrl, kind) => registerFeishuWebhook(baseUrl, kind));
|
|
335
|
+
if (!outcome.ok)
|
|
336
|
+
failStartup(new Error(`deploy stopped: ${outcome.gate}`));
|
|
337
|
+
console.error(`[fastagent] deployed → ${outcome.url}`);
|
|
338
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface DevOptions {
|
|
2
|
+
port?: string;
|
|
3
|
+
model?: string;
|
|
4
|
+
authPath?: string;
|
|
5
|
+
/** false ⇔ `--no-watch`. */
|
|
6
|
+
watch?: boolean;
|
|
7
|
+
tunnel?: boolean;
|
|
8
|
+
/** false ⇔ `--no-input`. */
|
|
9
|
+
input?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function runDev(dirArg: string, opts: DevOptions): Promise<void>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fastagent dev`: a SUPERVISOR that spawns a worker (this command with FASTAGENT_DEV_WORKER set) to
|
|
3
|
+
* assemble + serve, restarting it on workspace edits. A fresh process per reload means what is served
|
|
4
|
+
* is always the latest code, including modules a tool/config imports.
|
|
5
|
+
*/
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { runDevSupervisor } from "../../dev-supervisor.js";
|
|
8
|
+
import { loadDotEnv } from "../../env.js";
|
|
9
|
+
import { resolveAuthPathOverride } from "../../engines/pi/config.js";
|
|
10
|
+
import { reportDefinitionWarnings, reportModuleLoadFailures, reportToolCollisions } from "../../engines/pi/report.js";
|
|
11
|
+
import { createPiAgentFromWorkspace } from "../../engines/pi/workspace.js";
|
|
12
|
+
import { log, setLogLevel } from "../../log.js";
|
|
13
|
+
import { logAgentLoop } from "../../observe.js";
|
|
14
|
+
import { installProxyFetch } from "../../proxy.js";
|
|
15
|
+
import { failStartup } from "../fail.js";
|
|
16
|
+
import { maybeTunnel, routesFor, serve, startSchedules } from "../serve.js";
|
|
17
|
+
import { parsePort, reportAuth, resolveFirstRunModel } from "../shared.js";
|
|
18
|
+
export async function runDev(dirArg, opts) {
|
|
19
|
+
const dir = resolve(dirArg);
|
|
20
|
+
setLogLevel("debug"); // dev posture: verbose, includes the debug turn trace (content) — supervisor and worker both
|
|
21
|
+
const isWorker = process.env.FASTAGENT_DEV_WORKER === "1";
|
|
22
|
+
// Pick a model interactively once, in the parent (both watch and --no-watch have a TTY); a spawned
|
|
23
|
+
// watch worker inherits the choice via FASTAGENT_MODEL, so it must not prompt again. Load .env and
|
|
24
|
+
// the proxy FIRST (as invoke/start do): the picker reads FASTAGENT_MODEL and provider keys from
|
|
25
|
+
// .env, and getAuth's OAuth refresh must go through HTTPS_PROXY. The worker re-loads both in serveOnce.
|
|
26
|
+
if (!isWorker) {
|
|
27
|
+
loadDotEnv(dir);
|
|
28
|
+
installProxyFetch();
|
|
29
|
+
await resolveFirstRunModel(dir, opts);
|
|
30
|
+
}
|
|
31
|
+
if (isWorker || opts.watch === false) {
|
|
32
|
+
await serveOnce(dir, opts);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
parsePort(opts.port, "--port", "flag"); // flag-shape check before spawning
|
|
36
|
+
await runDevSupervisor(dir, { tunnel: opts.tunnel ?? false });
|
|
37
|
+
}
|
|
38
|
+
/** Assemble the workspace agent and serve it once (the dev worker; also the --no-watch path). */
|
|
39
|
+
async function serveOnce(dir, opts) {
|
|
40
|
+
const portFlag = parsePort(opts.port, "--port", "flag");
|
|
41
|
+
loadDotEnv(dir);
|
|
42
|
+
installProxyFetch();
|
|
43
|
+
const a = await createPiAgentFromWorkspace(dir, {
|
|
44
|
+
model: opts.model,
|
|
45
|
+
authPath: resolveAuthPathOverride(opts.authPath),
|
|
46
|
+
serving: true, // long-running serve: the scheduler poller runs (wake mounts iff config.selfSchedule)
|
|
47
|
+
}).catch(failStartup);
|
|
48
|
+
log.info(`[fastagent] dir: ${dir}`);
|
|
49
|
+
if (a.agentDir !== dir)
|
|
50
|
+
log.info(`[fastagent] agent: ${a.agentDir}`);
|
|
51
|
+
log.info(`[fastagent] config: ${a.configPath ?? "(zero-config)"}`);
|
|
52
|
+
log.info(`[fastagent] model: ${a.modelSpec}${a.config.thinkingLevel ? ` (thinking: ${a.config.thinkingLevel})` : ""}`);
|
|
53
|
+
await reportAuth(a.modelSpec, a.authPath);
|
|
54
|
+
reportAgentsSkillsTools(a);
|
|
55
|
+
// Trace each turn's agent loop (tool calls + reply) to the log at debug level — shown in dev, gated
|
|
56
|
+
// out in start (level info), keeping end-user content out of production logs. Wired in both postures.
|
|
57
|
+
const traced = logAgentLoop(a.agent);
|
|
58
|
+
const routes = await routesFor(a.agentDir, traced, a.stateRoot).catch(failStartup);
|
|
59
|
+
await startSchedules(a.agentDir, traced, a.stateRoot, a.config.selfSchedule ?? false);
|
|
60
|
+
serve(routes, portFlag ?? a.config.http?.port ?? 8787, (p) => maybeTunnel(a.agentDir, p, opts.tunnel ?? false));
|
|
61
|
+
}
|
|
62
|
+
/** The agents/skills/tools/collisions report lines. */
|
|
63
|
+
function reportAgentsSkillsTools(a) {
|
|
64
|
+
log.info(`[fastagent] context: ${a.definition.contextFiles.map((f) => f.path).join(", ") || "(none)"}`);
|
|
65
|
+
if (a.definition.persona)
|
|
66
|
+
log.info(`[fastagent] persona: persona.md`);
|
|
67
|
+
log.info(`[fastagent] skills: ${a.definition.skills.map((s) => s.name).join(", ") || "(none)"}`);
|
|
68
|
+
if (a.toolNames.length > 0)
|
|
69
|
+
log.info(`[fastagent] tools: ${a.toolNames.join(", ")}`);
|
|
70
|
+
if (a.deferredToolNames.length > 0) {
|
|
71
|
+
log.info(`[fastagent] deferred: ${a.deferredToolNames.join(", ")} (activated via search_tools)`);
|
|
72
|
+
}
|
|
73
|
+
reportToolCollisions(a.toolCollisions);
|
|
74
|
+
reportModuleLoadFailures(a.toolFailures);
|
|
75
|
+
reportDefinitionWarnings(a.definition.collisions, a.definition.diagnostics);
|
|
76
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fastagent fire <name> [dir]`: run ONE schedule's turn immediately — the authoring loop for schedules
|
|
3
|
+
* (like `invoke` is for a prompt). Fires `schedules/<name>.ts` now, without waiting for its cron, using
|
|
4
|
+
* the schedule's stable session (faithful to the served behavior). Does NOT advance the schedule's fire
|
|
5
|
+
* state — a test run must never make the scheduler skip the real next run.
|
|
6
|
+
*/
|
|
7
|
+
import { relative, resolve, sep } from "node:path";
|
|
8
|
+
import { loadDotEnv } from "../../env.js";
|
|
9
|
+
import { loadConfig, resolveAgentDir, resolveAuthPathOverride } from "../../engines/pi/config.js";
|
|
10
|
+
import { reportModuleLoadFailures } from "../../engines/pi/report.js";
|
|
11
|
+
import { createPiAgentFromWorkspace } from "../../engines/pi/workspace.js";
|
|
12
|
+
import { runInvokeStream } from "../../invoke-stream.js";
|
|
13
|
+
import { installProxyFetch } from "../../proxy.js";
|
|
14
|
+
import { loadSchedules } from "../../schedule/discover.js";
|
|
15
|
+
import { scheduleSession } from "../../schedule/scheduler.js";
|
|
16
|
+
import { failStartup } from "../fail.js";
|
|
17
|
+
import { reportAuth, resolveFirstRunModel } from "../shared.js";
|
|
18
|
+
export async function runFire(name, dirArg, opts) {
|
|
19
|
+
const fireDir = resolve(dirArg);
|
|
20
|
+
loadDotEnv(fireDir);
|
|
21
|
+
installProxyFetch();
|
|
22
|
+
await resolveFirstRunModel(fireDir, opts);
|
|
23
|
+
// Schedules are agent surface — discover them where dev/start/`schedule list` do (agentDir), not the
|
|
24
|
+
// run root, so `fire` sees the same set the scheduler serves in the kit layout.
|
|
25
|
+
const { config: fireConfig } = await loadConfig(fireDir).catch(failStartup);
|
|
26
|
+
const fireAgentDir = resolveAgentDir(fireDir, fireConfig);
|
|
27
|
+
const { schedules, failures } = await loadSchedules(fireAgentDir).catch(failStartup);
|
|
28
|
+
reportModuleLoadFailures(failures);
|
|
29
|
+
const schedule = schedules.find((s) => s.name === name);
|
|
30
|
+
if (!schedule) {
|
|
31
|
+
// Name the discovery path in the kit layout: a schedule misplaced at the run root should read as
|
|
32
|
+
// "wrong place", not "broken file".
|
|
33
|
+
const looked = fireAgentDir === fireDir ? "" : ` (looked in ${relative(fireDir, fireAgentDir).split(sep).join("/")}/schedules)`;
|
|
34
|
+
failStartup(new Error(`unknown schedule "${name}"${looked}. available: ${schedules.map((s) => s.name).join(", ") || "(none)"}`));
|
|
35
|
+
}
|
|
36
|
+
const { agent, modelSpec, authPath } = await createPiAgentFromWorkspace(fireDir, {
|
|
37
|
+
model: opts.model,
|
|
38
|
+
authPath: resolveAuthPathOverride(opts.authPath),
|
|
39
|
+
}).catch(failStartup);
|
|
40
|
+
console.error(`[fastagent] fire: ${name} (${modelSpec})`);
|
|
41
|
+
await reportAuth(modelSpec, authPath);
|
|
42
|
+
const exitCode = await runInvokeStream(agent.invoke({ session: scheduleSession(name) }, { text: schedule.prompt }), (text) => process.stdout.write(text), (line) => console.error(line));
|
|
43
|
+
process.stdout.write("\n");
|
|
44
|
+
process.exit(exitCode);
|
|
45
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** `fastagent info [dir] [--json]`: print what the directory ASSEMBLES into, WITHOUT booting a server. Read-only. */
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { loadDotEnv } from "../../env.js";
|
|
4
|
+
import { discoverChannelFiles } from "../../engines/pi/channel.js";
|
|
5
|
+
import { defaultAuthPath, defaultSessionsDir, loadConfig, resolveAgentDir, resolveAuthPathOverride, resolveModelSpec, resolveSessionsDirOverride, resolveStateRoot, } from "../../engines/pi/config.js";
|
|
6
|
+
import { resolveWorkspaceTools } from "../../engines/pi/create.js";
|
|
7
|
+
import { loadAgentDefinition } from "../../engines/pi/definition.js";
|
|
8
|
+
import { reportDefinitionWarnings, reportModuleLoadFailures, reportToolCollisions } from "../../engines/pi/report.js";
|
|
9
|
+
import { log } from "../../log.js";
|
|
10
|
+
import { nextRun } from "../../schedule/cron.js";
|
|
11
|
+
import { loadSchedules } from "../../schedule/discover.js";
|
|
12
|
+
import { failStartup } from "../fail.js";
|
|
13
|
+
export async function runInfo(dirArg, opts) {
|
|
14
|
+
const dir = resolve(dirArg);
|
|
15
|
+
loadDotEnv(dir); // skills/tools may read env at load time
|
|
16
|
+
const { config, path: configPath } = await loadConfig(dir).catch(failStartup);
|
|
17
|
+
const modelSpec = resolveModelSpec(opts.model, config);
|
|
18
|
+
// dir = the run root (cwd, whose AGENTS.md is ② context); the agent's own surface lives in agentDir.
|
|
19
|
+
const agentDir = resolveAgentDir(dir, config);
|
|
20
|
+
const definition = await loadAgentDefinition(agentDir, { cwd: dir }).catch(failStartup);
|
|
21
|
+
// A tool that fails to load, for any reason (a missing dep, a top-level throw, or just not being a
|
|
22
|
+
// tool), is isolated the same way everywhere (G2): info, dev, AND start report it and keep going with
|
|
23
|
+
// the tools that loaded. The `error`/`.catch` below only fires for a whole-load fault (an unreadable
|
|
24
|
+
// tools/ dir), not a single bad file.
|
|
25
|
+
const tools = await resolveWorkspaceTools(config, agentDir, dir)
|
|
26
|
+
.then((r) => ({
|
|
27
|
+
names: r.toolNames,
|
|
28
|
+
deferred: r.deferredToolNames,
|
|
29
|
+
collisions: r.toolCollisions,
|
|
30
|
+
failures: r.toolFailures,
|
|
31
|
+
error: undefined,
|
|
32
|
+
}))
|
|
33
|
+
.catch((e) => ({
|
|
34
|
+
names: [],
|
|
35
|
+
deferred: [],
|
|
36
|
+
collisions: [],
|
|
37
|
+
failures: [],
|
|
38
|
+
error: e.message,
|
|
39
|
+
}));
|
|
40
|
+
const channels = await discoverChannelFiles(agentDir).catch(failStartup);
|
|
41
|
+
// Loaded (imported + validated), not just discovered: info's job is "fix only what it reports", so a
|
|
42
|
+
// broken schedule file (bad cron/tz, failed import) must show up HERE, not first at dev/start — and
|
|
43
|
+
// loading is what makes the next fire instant printable. Consistent with tools (info imports those too).
|
|
44
|
+
const sched = await loadSchedules(agentDir).catch(failStartup);
|
|
45
|
+
const schedules = sched.schedules.map((s) => ({
|
|
46
|
+
name: s.name,
|
|
47
|
+
cron: s.cron,
|
|
48
|
+
tz: s.tz ?? null,
|
|
49
|
+
next: nextRun(s.cron, s.tz, new Date())?.toISOString() ?? null,
|
|
50
|
+
}));
|
|
51
|
+
// The default sessions/auth paths WITHOUT creating anything (info is read-only; dev/start mkdir/login
|
|
52
|
+
// create them, info must not).
|
|
53
|
+
const stateRoot = resolveStateRoot(dir);
|
|
54
|
+
const sessionsDir = resolveSessionsDirOverride(opts.sessionsDir) ?? defaultSessionsDir(stateRoot);
|
|
55
|
+
const authPath = resolveAuthPathOverride(opts.authPath) ?? defaultAuthPath(stateRoot);
|
|
56
|
+
if (opts.json) {
|
|
57
|
+
console.log(JSON.stringify({
|
|
58
|
+
dir,
|
|
59
|
+
agentDir,
|
|
60
|
+
configPath: configPath ?? null,
|
|
61
|
+
model: modelSpec ?? null,
|
|
62
|
+
thinkingLevel: config.thinkingLevel ?? null,
|
|
63
|
+
context: definition.contextFiles.map((f) => f.path),
|
|
64
|
+
persona: definition.persona !== undefined,
|
|
65
|
+
skills: definition.skills.map((skill) => ({ name: skill.name, description: skill.description })),
|
|
66
|
+
tools: tools.names,
|
|
67
|
+
deferredTools: tools.deferred,
|
|
68
|
+
toolError: tools.error ?? null,
|
|
69
|
+
channels,
|
|
70
|
+
schedules,
|
|
71
|
+
scheduleFailures: sched.failures,
|
|
72
|
+
selfSchedule: config.selfSchedule ?? false,
|
|
73
|
+
stateRoot,
|
|
74
|
+
sessionsDir,
|
|
75
|
+
authPath,
|
|
76
|
+
diagnostics: definition.diagnostics,
|
|
77
|
+
skillCollisions: definition.collisions,
|
|
78
|
+
toolCollisions: tools.collisions,
|
|
79
|
+
toolFailures: tools.failures,
|
|
80
|
+
}, null, 2));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.log(`dir: ${dir}`);
|
|
84
|
+
if (agentDir !== dir)
|
|
85
|
+
console.log(`agent: ${agentDir}`);
|
|
86
|
+
console.log(`config: ${configPath ?? "(none)"}`);
|
|
87
|
+
console.log(`model: ${modelSpec ?? "(not set — pass --model, set FASTAGENT_MODEL, or config.model)"}`);
|
|
88
|
+
if (config.thinkingLevel)
|
|
89
|
+
console.log(`thinking: ${config.thinkingLevel}`);
|
|
90
|
+
console.log(`context: ${definition.contextFiles.map((f) => f.path).join(", ") || "(none)"}`);
|
|
91
|
+
console.log(`persona: ${definition.persona ? "persona.md" : "(none)"}`);
|
|
92
|
+
console.log(`skills: ${definition.skills.map((skill) => skill.name).join(", ") || "(none)"}`);
|
|
93
|
+
console.log(`tools: ${tools.error ? "(could not load — see warning below)" : tools.names.join(", ") || "(none)"}`);
|
|
94
|
+
if (tools.deferred.length > 0)
|
|
95
|
+
console.log(`deferred: ${tools.deferred.join(", ")} (activated via search_tools)`);
|
|
96
|
+
console.log(`channels: ${channels.join(", ") || "(none)"}`);
|
|
97
|
+
console.log(`schedules: ${schedules.map((s) => `${s.name} (next ${s.next ?? "never"})`).join(", ") || "(none)"}`);
|
|
98
|
+
console.log(`selfSchedule: ${config.selfSchedule ? "on (mounts the wake tool when serving)" : "off"}`);
|
|
99
|
+
console.log(`state: ${stateRoot}`);
|
|
100
|
+
console.log(`sessions: ${sessionsDir}`);
|
|
101
|
+
console.log(`auth: ${authPath}`);
|
|
102
|
+
reportToolCollisions(tools.collisions);
|
|
103
|
+
reportModuleLoadFailures(tools.failures);
|
|
104
|
+
reportModuleLoadFailures(sched.failures);
|
|
105
|
+
if (tools.error)
|
|
106
|
+
log.warn(`[fastagent] ${tools.error}`);
|
|
107
|
+
reportDefinitionWarnings(definition.collisions, definition.diagnostics);
|
|
108
|
+
}
|