@fastagent-sh/fastagent 0.19.0 → 0.20.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/atomic-write.d.ts +11 -7
- package/dist/atomic-write.js +16 -11
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-service.d.ts +12 -11
- package/dist/channels/agentcore-service.js +14 -18
- package/dist/channels/agentcore.d.ts +11 -19
- package/dist/channels/agentcore.js +14 -7
- package/dist/channels/control.d.ts +50 -12
- package/dist/channels/control.js +239 -96
- package/dist/channels/discover.d.ts +26 -4
- package/dist/channels/discover.js +12 -23
- package/dist/channels/feishu/feishu-api.js +5 -8
- package/dist/channels/feishu/feishu.js +35 -18
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/kit/context-buffer.d.ts +9 -0
- package/dist/channels/kit/context-buffer.js +11 -0
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/kit/stop-command.js +3 -3
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +10 -4
- package/dist/channels/kit/turn-store.d.ts +19 -3
- package/dist/channels/kit/turn-store.js +29 -2
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +5 -1
- package/dist/channels/slack/onboard.js +5 -5
- package/dist/channels/slack/onboarding-state.d.ts +7 -3
- package/dist/channels/slack/onboarding-state.js +13 -20
- package/dist/channels/slack/register-webhook.js +1 -1
- package/dist/channels/slack/scaffold/slack-send.ts +2 -17
- package/dist/channels/slack/slack-api.js +22 -54
- package/dist/channels/slack/slack.js +13 -13
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +65 -11
- package/dist/channels/telegram/scaffold/channel.ts +7 -3
- package/dist/channels/telegram/telegram-api.js +5 -6
- package/dist/channels/telegram/telegram.d.ts +2 -2
- package/dist/channels/telegram/telegram.js +57 -69
- package/dist/cli/add-slack.js +6 -5
- package/dist/cli/commands/add.js +1 -5
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/deploy.d.ts +50 -5
- package/dist/cli/commands/deploy.js +183 -134
- package/dist/cli/commands/dev.js +4 -31
- package/dist/cli/commands/info.js +1 -2
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/start.js +31 -41
- package/dist/cli/serve.d.ts +12 -21
- package/dist/cli/serve.js +20 -25
- package/dist/cli/shared.d.ts +40 -10
- package/dist/cli/shared.js +51 -4
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +12 -10
- package/dist/deploy/agentcore/plan.js +39 -31
- package/dist/deploy/agentcore/run.d.ts +5 -4
- package/dist/deploy/agentcore/run.js +13 -36
- package/dist/deploy/channel-ingress.d.ts +73 -0
- package/dist/deploy/channel-ingress.js +101 -0
- package/dist/deploy/docker/plan.d.ts +3 -7
- package/dist/deploy/docker/plan.js +4 -15
- package/dist/deploy/docker/run.d.ts +18 -1
- package/dist/deploy/docker/run.js +10 -0
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +7 -22
- package/dist/deploy/fly/run.d.ts +9 -23
- package/dist/deploy/fly/run.js +32 -68
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +26 -22
- package/dist/deploy/railway/plan.d.ts +4 -5
- package/dist/deploy/railway/plan.js +11 -26
- package/dist/deploy/railway/run.d.ts +9 -9
- package/dist/deploy/railway/run.js +40 -40
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +64 -12
- package/dist/dev-supervisor.js +3 -4
- package/dist/engines/pi/agent-session-factory.js +20 -39
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +1 -1
- package/dist/engines/pi/create.d.ts +14 -2
- package/dist/engines/pi/create.js +2 -2
- package/dist/engines/pi/definition.d.ts +1 -1
- package/dist/engines/pi/definition.js +3 -4
- package/dist/engines/pi/login.js +32 -16
- package/dist/engines/pi/open.d.ts +1 -2
- package/dist/engines/pi/open.js +1 -1
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +1 -1
- package/dist/engines/pi/service.js +8 -0
- package/dist/engines/pi/session-builder.js +6 -33
- package/dist/engines/pi/session-control.d.ts +19 -6
- package/dist/engines/pi/session-control.js +565 -470
- package/dist/engines/pi/session-inheritance.d.ts +8 -22
- package/dist/engines/pi/session-inheritance.js +95 -76
- package/dist/engines/pi/session-markers.d.ts +48 -0
- package/dist/engines/pi/session-markers.js +59 -0
- package/dist/engines/pi/session-settings.d.ts +4 -4
- package/dist/engines/pi/session-settings.js +7 -4
- package/dist/engines/pi/session-store.d.ts +83 -13
- package/dist/engines/pi/session-store.js +408 -55
- package/dist/engines/pi/tool-context.d.ts +21 -8
- package/dist/engines/pi/tool-context.js +29 -4
- package/dist/engines/pi/tool.js +2 -2
- package/dist/env.js +1 -2
- package/dist/loader.d.ts +49 -7
- package/dist/loader.js +79 -18
- package/dist/paths.d.ts +19 -0
- package/dist/paths.js +27 -2
- package/dist/scaffold/add-channel.js +2 -2
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/wake-alarm.d.ts +13 -7
- package/dist/schedule/wake-alarm.js +88 -46
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +10 -7
- package/dist/service.d.ts +1 -2
- package/dist/service.js +27 -5
- package/dist/session-remote.d.ts +7 -2
- package/dist/session-remote.js +162 -92
- package/dist/session.d.ts +226 -92
- package/dist/session.js +61 -23
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +18 -3
- package/dist/tunnel.js +32 -40
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* --force). `--run` drives the target CLI instead of printing.
|
|
9
9
|
*/
|
|
10
10
|
import { MAX_WEBHOOK_BODY_BYTES } from "../../channels/agentcore-limits.js";
|
|
11
|
-
import {
|
|
11
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { basename, dirname, join, resolve } from "node:path";
|
|
14
14
|
import { registerFeishuWebhook } from "../../channels/feishu/register-webhook.js";
|
|
@@ -17,8 +17,9 @@ import { registerSlackWebhook } from "../../channels/slack/register-webhook.js";
|
|
|
17
17
|
import { registerTelegramWebhook } from "../../channels/telegram/register-webhook.js";
|
|
18
18
|
import { FORWARDER_FILE, TEMPLATE_FILE, agentcoreName, isGeneratedAgentcoreTemplate, planAgentcoreDeploy, } from "../../deploy/agentcore/plan.js";
|
|
19
19
|
import { deployAgentcoreRun } from "../../deploy/agentcore/run.js";
|
|
20
|
+
import { webhookPaths } from "../../deploy/channel-ingress.js";
|
|
20
21
|
import { isGeneratedDockerfile, isGeneratedDockerignore } from "../../deploy/container.js";
|
|
21
|
-
import { composeHasTunnelService,
|
|
22
|
+
import { composeHasTunnelService, isGeneratedCompose, planDockerDeploy, toDockerProjectName, } from "../../deploy/docker/plan.js";
|
|
22
23
|
import { deployDockerRun } from "../../deploy/docker/run.js";
|
|
23
24
|
import { isGeneratedFlyToml, parseFlyAppName, parseFlyMinMachines, parseFlyRegion, planFlyDeploy, toFlyAppName, } from "../../deploy/fly/plan.js";
|
|
24
25
|
import { deployFlyRun } from "../../deploy/fly/run.js";
|
|
@@ -29,18 +30,120 @@ import { spawnRunner } from "../../deploy/runner.js";
|
|
|
29
30
|
import { assembleSecrets } from "../../deploy/secrets.js";
|
|
30
31
|
import { loadDotEnv } from "../../env.js";
|
|
31
32
|
import { loadConfig, resolveModelSpec } from "../../engines/pi/config.js";
|
|
32
|
-
import { resolveStateRoot } from "../../paths.js";
|
|
33
|
+
import { resolveStateRoot, exists } from "../../paths.js";
|
|
33
34
|
import { loadSchedules } from "../../schedule/discover.js";
|
|
34
35
|
import { installProxyFetch } from "../../proxy.js";
|
|
35
36
|
import { openExternalUrl } from "../../open-url.js";
|
|
36
|
-
import { exists } from "../../paths.js";
|
|
37
37
|
import { announceWebhooks } from "../../tunnel.js";
|
|
38
38
|
import { failStartup, failUsage, placementOrExit } from "../fail.js";
|
|
39
39
|
import { resolveFirstRunModel } from "../shared.js";
|
|
40
|
+
/** The deploy targets, as a value: {@link HOST_ONLY_FLAGS} is checked against this list by a test, and
|
|
41
|
+
* a type alone cannot be enumerated at runtime. NOT the only copy — `cli/program.ts` repeats it as the
|
|
42
|
+
* `<host>` argument's `choices`, because that file may not import a command module at load time (its
|
|
43
|
+
* per-command imports are lazy). Adding a host means editing both; the type there rides in on a
|
|
44
|
+
* `import type`, which costs nothing at runtime. */
|
|
45
|
+
export const DEPLOY_HOSTS = ["docker", "fly", "railway", "agentcore"];
|
|
46
|
+
/** The registrars every host driver gets. One wiring: a channel's credentials are the channel's, not
|
|
47
|
+
* the host's, so which of them can run end-to-end never varies by deployment target. */
|
|
48
|
+
function registrarsFor(agentDir) {
|
|
49
|
+
return {
|
|
50
|
+
telegram: (baseUrl) => registerTelegramWebhook(baseUrl),
|
|
51
|
+
slack: (baseUrl) => registerSlackWebhook(baseUrl, { stateRoot: resolveStateRoot(agentDir) }),
|
|
52
|
+
feishu: (baseUrl, kind) => registerFeishuWebhook(baseUrl, kind),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
40
55
|
/** A copy/paste-safe POSIX shell argument for the command hints deploy prints. */
|
|
41
56
|
function shellArg(value) {
|
|
42
57
|
return /^[A-Za-z0-9_./:@%+=,-]+$/.test(value) ? value : `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
43
58
|
}
|
|
59
|
+
/** Infers `Owner` from the literal's own `owner`, so each row carries its exhaustiveness itself. The
|
|
60
|
+
* alternative — annotating the array as a tuple of `HostOnlyFlag<"fly"> | …` — put the whole guarantee
|
|
61
|
+
* in one annotation that a third rule invites relaxing to `HostOnlyFlag<DeployHost>[]`, where
|
|
62
|
+
* `Exclude<DeployHost, DeployHost>` collapses to `never` and every `instead` type-checks empty. */
|
|
63
|
+
const hostOnlyFlag = (rule) => rule;
|
|
64
|
+
/**
|
|
65
|
+
* ONE table for "this flag belongs to that host", because the fact is symmetric and was not stored
|
|
66
|
+
* that way: each host branch stated the OTHER hosts' flags in its own words, so the same sentence
|
|
67
|
+
* existed three times per flag and had already drifted (`Railway-only` twice, `railway-only` once).
|
|
68
|
+
*
|
|
69
|
+
* Every row is a flag that only WARNS elsewhere. `--tunnel` is host-only too and is deliberately not
|
|
70
|
+
* here: it is a usage GATE (`failUsage`, exit 2) raised in `runDeploy`, not a warning — moving it in
|
|
71
|
+
* would downgrade a refusal to a line of advice.
|
|
72
|
+
*
|
|
73
|
+
* Exported for the exhaustiveness test: the type stops a missing `instead` line, and the test stops it
|
|
74
|
+
* from being typed away.
|
|
75
|
+
*/
|
|
76
|
+
export const HOST_ONLY_FLAGS = [
|
|
77
|
+
hostOnlyFlag({
|
|
78
|
+
flag: "--stop/--no-scale-to-zero",
|
|
79
|
+
owner: "fly",
|
|
80
|
+
passed: (opts) => opts.stop === true || opts.scaleToZero === false,
|
|
81
|
+
instead: {
|
|
82
|
+
docker: "local Compose stays running",
|
|
83
|
+
railway: "Railway's App Sleeping is a dashboard toggle (the runbook states the manual step)",
|
|
84
|
+
agentcore: "AgentCore's idle/lifetime policy lives in the template's LifecycleConfiguration",
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
hostOnlyFlag({
|
|
88
|
+
flag: "--into-linked",
|
|
89
|
+
owner: "railway",
|
|
90
|
+
passed: (opts) => opts.intoLinked === true,
|
|
91
|
+
instead: {
|
|
92
|
+
docker: "ignored for local Docker",
|
|
93
|
+
agentcore: "ignored for AgentCore",
|
|
94
|
+
fly: "fly --run is idempotent — it reuses an existing app/volume",
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
];
|
|
98
|
+
/** Exported for its own test: nothing covered these six sentences, which is how three of them came to
|
|
99
|
+
* disagree about the spelling of a host name. */
|
|
100
|
+
export function warnHostOnlyFlags(host, opts) {
|
|
101
|
+
for (const rule of HOST_ONLY_FLAGS) {
|
|
102
|
+
if (host === rule.owner || !rule.passed(opts))
|
|
103
|
+
continue;
|
|
104
|
+
// The `continue` above is exactly the key set `instead` is typed over, but TS cannot narrow a
|
|
105
|
+
// union member out through a comparison against a per-rule literal, so the read needs a cast —
|
|
106
|
+
// and a cast is how a hole would reach the operator as the word "undefined". Unreachable while
|
|
107
|
+
// the rows stay exhaustive, which is what the table's own test asserts.
|
|
108
|
+
const instead = rule.instead[host];
|
|
109
|
+
// THROWN, not `failStartup`ed, though every other failure in this file exits through that: an
|
|
110
|
+
// incomplete table is a bug, and fail.ts prints a plain Error's message WITHOUT its stack (it
|
|
111
|
+
// reserves that shape for user-fixable problems) while exiting the process — which in the test
|
|
112
|
+
// that covers this table would kill the worker instead of reporting which row is short.
|
|
113
|
+
if (instead === undefined) {
|
|
114
|
+
throw new Error(`deploy: HOST_ONLY_FLAGS has no "instead" line for ${host} on ${rule.flag}`);
|
|
115
|
+
}
|
|
116
|
+
// A colon, not "is": one row names a single flag and the other names a pair, and no verb agrees
|
|
117
|
+
// with both (the hand-written copies this replaced said "is" and "are" respectively).
|
|
118
|
+
console.error(`[fastagent] warn: ${rule.flag}: ${rule.owner}-only — ${instead}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The `--run` credential carry, for every host: the local model credential (an env key, or the whole
|
|
123
|
+
* auth.json as a `FASTAGENT_AUTH_SEED`) plus channel secrets, read through the one environment that
|
|
124
|
+
* accounts for Slack's rotated bot token. Four drivers assembled this identically; a fifth would have
|
|
125
|
+
* had to remember the `deployEnvironment` wrapper, which is invisible when forgotten (a rotated token
|
|
126
|
+
* silently deploys stale).
|
|
127
|
+
*/
|
|
128
|
+
async function carryCredentials(params) {
|
|
129
|
+
const { agentDir, modelAuth, modelKeyInDefinition, authPath, channels, extraSecrets } = params;
|
|
130
|
+
return assembleSecrets({
|
|
131
|
+
modelAuth,
|
|
132
|
+
modelKeyInDefinition,
|
|
133
|
+
authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
|
|
134
|
+
channels,
|
|
135
|
+
extraSecrets,
|
|
136
|
+
env: deployEnvironment(agentDir, channels),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/** Gate a `--run` that has no model credential to carry. Its remediation is `fastagent login`, which
|
|
140
|
+
* is why it is separate from `missingSecrets` (a `.env` fix) rather than folded into it. Docker states
|
|
141
|
+
* the same gate inside its driver, where it belongs after the daemon check. */
|
|
142
|
+
function gateOnModelCredential(needsModelCredential) {
|
|
143
|
+
if (!needsModelCredential)
|
|
144
|
+
return;
|
|
145
|
+
failStartup(new Error(`deploy stopped: no model credential — run \`fastagent login\`, or set a provider API key in .env, then re-run`));
|
|
146
|
+
}
|
|
44
147
|
export async function runDeploy(host, dirArg, opts) {
|
|
45
148
|
// ONE deploy semantic: bake the WORKSPACE (WYSIWYG). Artifacts land under the agent dir
|
|
46
149
|
// (`fastagent/`) plus the one workspace-root `.dockerignore` the packers require; host CLIs run
|
|
@@ -76,30 +179,26 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
76
179
|
failStartup(new Error(`deploy stopped: ${pre.gate}`));
|
|
77
180
|
for (const m of pre.messages)
|
|
78
181
|
console.error(`[fastagent] ${m.level}: ${m.text}`);
|
|
79
|
-
const { channels,
|
|
80
|
-
const
|
|
182
|
+
const { channels, hasTimeTriggers, modelAuth, modelKeyInDefinition, authPath, container, port, extraSecrets } = pre;
|
|
183
|
+
const webhookChannels = channels.filter((channel) => channel.ingress === "webhook");
|
|
184
|
+
const longConnectionChannels = channels.filter((channel) => channel.ingress === "long-connection");
|
|
185
|
+
const hasDeclaredChannels = channels.length > 0;
|
|
186
|
+
warnHostOnlyFlags(host, opts);
|
|
81
187
|
// Docker: one app service + loopback port + state volume. `--tunnel` shapes the generated topology
|
|
82
188
|
// with an optional Quick Tunnel service; `--run` alone decides whether Docker receives side effects.
|
|
83
189
|
if (host === "docker") {
|
|
84
|
-
if (opts.stop || opts.scaleToZero === false) {
|
|
85
|
-
console.error(`[fastagent] warn: --stop/--no-scale-to-zero are Fly-only — local Compose stays running`);
|
|
86
|
-
}
|
|
87
|
-
if (opts.intoLinked) {
|
|
88
|
-
console.error(`[fastagent] warn: --into-linked is Railway-only — ignored for local Docker`);
|
|
89
|
-
}
|
|
90
190
|
const projectName = toDockerProjectName(basename(workspace));
|
|
91
191
|
const dockerPlan = (tunnel) => planDockerDeploy({
|
|
92
192
|
projectName,
|
|
93
193
|
port,
|
|
94
194
|
modelAuth,
|
|
95
195
|
channels,
|
|
96
|
-
longConnectionChannels,
|
|
97
196
|
tunnel,
|
|
98
197
|
extraSecrets,
|
|
99
198
|
...container,
|
|
100
199
|
});
|
|
101
|
-
const requestedTunnel = !!opts.tunnel && (!hasDeclaredChannels ||
|
|
102
|
-
if (opts.tunnel && hasDeclaredChannels &&
|
|
200
|
+
const requestedTunnel = !!opts.tunnel && (!hasDeclaredChannels || webhookChannels.length > 0);
|
|
201
|
+
if (opts.tunnel && hasDeclaredChannels && webhookChannels.length === 0) {
|
|
103
202
|
console.error(`[fastagent] note: --tunnel skipped — every channel uses a long connection`);
|
|
104
203
|
}
|
|
105
204
|
let plan = dockerPlan(requestedTunnel);
|
|
@@ -129,7 +228,6 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
129
228
|
modelKeyInDefinition,
|
|
130
229
|
authPath,
|
|
131
230
|
channels,
|
|
132
|
-
longConnectionChannels,
|
|
133
231
|
extraSecrets,
|
|
134
232
|
});
|
|
135
233
|
}
|
|
@@ -143,10 +241,6 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
143
241
|
// Railway: thin config file, scale-to-zero is a manual dashboard step, the URL is minted (see
|
|
144
242
|
// planRailwayDeploy). --run drives the railway CLI to completion; otherwise print the runbook.
|
|
145
243
|
if (host === "railway") {
|
|
146
|
-
if (opts.stop || opts.scaleToZero === false) {
|
|
147
|
-
console.error(`[fastagent] warn: --stop/--no-scale-to-zero are Fly-only — Railway's App Sleeping is a dashboard toggle ` +
|
|
148
|
-
`(the runbook states the manual step).`);
|
|
149
|
-
}
|
|
150
244
|
// Railway service names are project-scoped (not globally unique like a Fly app); slug the dir
|
|
151
245
|
// basename so a name with spaces/odd chars can't break the `railway add --service <name>` command.
|
|
152
246
|
const serviceName = basename(workspace)
|
|
@@ -156,7 +250,6 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
156
250
|
serviceName,
|
|
157
251
|
modelAuth,
|
|
158
252
|
channels,
|
|
159
|
-
longConnectionChannels,
|
|
160
253
|
extraSecrets,
|
|
161
254
|
hasTimeTriggers,
|
|
162
255
|
...container,
|
|
@@ -180,7 +273,6 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
180
273
|
modelKeyInDefinition,
|
|
181
274
|
authPath,
|
|
182
275
|
channels,
|
|
183
|
-
longConnectionChannels,
|
|
184
276
|
extraSecrets,
|
|
185
277
|
intoLinked: !!opts.intoLinked,
|
|
186
278
|
dockerfilePath: dockerfilePathVar(pre.container.agentPrefix),
|
|
@@ -192,18 +284,11 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
192
284
|
// AgentCore: one CloudFormation stack (runtime + forwarder Lambda + EventBridge schedules); no
|
|
193
285
|
// public URL and no resident process — see deploy/agentcore/plan.ts for the topology decisions.
|
|
194
286
|
if (host === "agentcore") {
|
|
195
|
-
if (opts.stop || opts.scaleToZero === false) {
|
|
196
|
-
console.error(`[fastagent] warn: --stop/--no-scale-to-zero are Fly-only — AgentCore's idle/lifetime policy lives in ` +
|
|
197
|
-
`the template's LifecycleConfiguration.`);
|
|
198
|
-
}
|
|
199
|
-
if (opts.intoLinked) {
|
|
200
|
-
console.error(`[fastagent] warn: --into-linked is Railway-only — ignored for AgentCore`);
|
|
201
|
-
}
|
|
202
287
|
// Long-connection channels are STRUCTURALLY unsupported: the connection is the ingress, and a
|
|
203
288
|
// reclaimed session has nothing to wake it — events in the gap are silently lost. `--run` gates
|
|
204
289
|
// (deploying a channel that can't connect); generate-only warns and prints the runbook.
|
|
205
290
|
if (longConnectionChannels.length > 0) {
|
|
206
|
-
const msg = `long-connection channel (${longConnectionChannels.join(", ")}) cannot run on AgentCore — there is no ` +
|
|
291
|
+
const msg = `long-connection channel (${longConnectionChannels.map((c) => c.name).join(", ")}) cannot run on AgentCore — there is no ` +
|
|
207
292
|
`resident process to hold the connection, and nothing wakes a reclaimed session. Switch the channel ` +
|
|
208
293
|
`to webhook mode (its events then ride the forwarder like every other channel).`;
|
|
209
294
|
if (opts.run)
|
|
@@ -230,7 +315,6 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
230
315
|
name: acName,
|
|
231
316
|
modelAuth,
|
|
232
317
|
channels,
|
|
233
|
-
routeChannels,
|
|
234
318
|
extraSecrets,
|
|
235
319
|
schedules: loaded.schedules.map((s) => ({ name: s.name, cron: s.cron, tz: s.tz })),
|
|
236
320
|
selfSchedule: !!config.selfSchedule,
|
|
@@ -247,7 +331,7 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
247
331
|
// Host capability limit, stated at plan time. GitHub's own webhook contract is 25 MiB, but a
|
|
248
332
|
// Lambda Function URL request caps at 6 MB — so on this host a large payload cannot arrive at
|
|
249
333
|
// all. Better a sentence here than an opaque 502 the first time someone pushes a big diff.
|
|
250
|
-
if (channels.
|
|
334
|
+
if (channels.some((channel) => channel.name === "github")) {
|
|
251
335
|
console.error(`[fastagent] note: on AgentCore a webhook body is capped at ~${Math.round(MAX_WEBHOOK_BODY_BYTES / (1 << 20))} MiB ` +
|
|
252
336
|
`(the forwarder's Function URL limit); the GitHub channel accepts 25 MiB on a resident host, so the largest ` +
|
|
253
337
|
`payloads are rejected here rather than delivered`);
|
|
@@ -287,20 +371,17 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
287
371
|
selfSchedule: !!config.selfSchedule,
|
|
288
372
|
// Same predicate the template uses for the forwarder resource — kept in one expression so
|
|
289
373
|
// the run path and the topology cannot disagree about whether a forwarder exists.
|
|
290
|
-
needsForwarder:
|
|
374
|
+
needsForwarder: webhookChannels.length > 0 || loaded.schedules.length > 0 || !!config.selfSchedule,
|
|
291
375
|
});
|
|
292
376
|
}
|
|
293
377
|
console.log(plan.runbook.join("\n"));
|
|
294
378
|
return;
|
|
295
379
|
}
|
|
296
380
|
// host === "fly".
|
|
297
|
-
if (opts.intoLinked) {
|
|
298
|
-
console.error(`[fastagent] warn: --into-linked is railway-only (fly --run is idempotent — it reuses an existing app/volume)`);
|
|
299
|
-
}
|
|
300
381
|
// The replay floor that makes scale-to-zero safe is Telegram-only (its L1 turn store). GitHub turns
|
|
301
382
|
// are fire-and-forget (no replay), so the generated fly.toml keeps one machine running for them —
|
|
302
383
|
// a note, not a warn, since the plan already did the safe thing (definition-aware autostop).
|
|
303
|
-
if (channels.
|
|
384
|
+
if (channels.some((channel) => channel.name === "github")) {
|
|
304
385
|
console.error(`[fastagent] note: github turns have no replay — the generated fly.toml uses min_machines_running=1 ` +
|
|
305
386
|
`(no scale-to-zero) so autostop can't drop an in-flight review. Set it to 0 to accept that trade.`);
|
|
306
387
|
}
|
|
@@ -344,7 +425,7 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
344
425
|
// hand-written fly.toml without the line scales to zero exactly like an explicit 0.
|
|
345
426
|
const reason = hasTimeTriggers
|
|
346
427
|
? `schedules/self-scheduling need a running machine (no external wake-up)`
|
|
347
|
-
: `long-connection channel (${longConnectionChannels.join(", ")}) needs an always-on outbound connection`;
|
|
428
|
+
: `long-connection channel (${longConnectionChannels.map((c) => c.name).join(", ")}) needs an always-on outbound connection`;
|
|
348
429
|
const msg = `your kept fly.toml scales to zero (min_machines_running = ${min ?? "absent → platform default 0"}), but ` +
|
|
349
430
|
`${reason}. Set min_machines_running = 1, or pass --force to regenerate.`;
|
|
350
431
|
if (opts.run)
|
|
@@ -357,7 +438,6 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
357
438
|
port,
|
|
358
439
|
modelAuth,
|
|
359
440
|
channels,
|
|
360
|
-
longConnectionChannels,
|
|
361
441
|
extraSecrets,
|
|
362
442
|
hasTimeTriggers,
|
|
363
443
|
...container,
|
|
@@ -377,30 +457,13 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
377
457
|
modelKeyInDefinition,
|
|
378
458
|
authPath,
|
|
379
459
|
channels,
|
|
380
|
-
longConnectionChannels,
|
|
381
460
|
flyTomlPath,
|
|
382
461
|
extraSecrets,
|
|
383
462
|
});
|
|
384
463
|
}
|
|
385
464
|
console.log(plan.runbook.join("\n"));
|
|
386
465
|
}
|
|
387
|
-
/**
|
|
388
|
-
* Write each generated artifact, under ONE ownership rule: **deploy only ever overwrites its own
|
|
389
|
-
* output.** Each generated file opens with a marker, so a file on disk is either OURS (a previous
|
|
390
|
-
* `deploy` wrote it) or the author's — and `--force` means "my generated artifact is authoritative",
|
|
391
|
-
* which never licenses clobbering a file we did not write. To hand a path back to fastagent, delete it.
|
|
392
|
-
*
|
|
393
|
-
* That single rule replaced a second mechanism (a per-host list of paths exempt from `--force`), which
|
|
394
|
-
* only described one shape: with the agent inside the workspace, the root `.dockerignore` is the
|
|
395
|
-
* WORKSPACE's file and was listed; with the agent flat, the list was empty — so `--force` would have
|
|
396
|
-
* overwritten a hand-written root `Dockerfile` in a repository `init --flat` had explicitly adopted.
|
|
397
|
-
* Ownership is a property of the file, not of where the agent sits.
|
|
398
|
-
*
|
|
399
|
-
* A KEPT file of OURS that no longer matches what deploy would generate now (config/channel/lockfile/
|
|
400
|
-
* version drift — or the author's edits, which we cannot tell apart) is flagged stale; `--force`
|
|
401
|
-
* regenerates that one.
|
|
402
|
-
*/
|
|
403
|
-
/** Did fastagent generate the file at `path`? ONE table for all five artifact kinds: a kind missing from
|
|
466
|
+
/** Did fastagent generate the file at `path`? ONE table for every artifact kind: a kind missing from
|
|
404
467
|
* it is permanently classified as the author's, which silently turns `--force` into a no-op for that
|
|
405
468
|
* file (`fly.toml` and `railway.json` were, until the marker predicates below existed). */
|
|
406
469
|
function isOurArtifact(path, content) {
|
|
@@ -419,10 +482,24 @@ function isOurArtifact(path, content) {
|
|
|
419
482
|
return false;
|
|
420
483
|
}
|
|
421
484
|
/**
|
|
422
|
-
* Write
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
485
|
+
* Write each generated artifact under `target`, under ONE ownership rule: **deploy only ever overwrites
|
|
486
|
+
* its own output.** Each generated file opens with a marker, so a file on disk is either OURS (a previous
|
|
487
|
+
* `deploy` wrote it) or the author's — and `--force` means "my generated artifact is authoritative",
|
|
488
|
+
* which never licenses clobbering a file we did not write. To hand a path back to fastagent, delete it.
|
|
489
|
+
*
|
|
490
|
+
* That single rule replaced a second mechanism (a per-host list of paths exempt from `--force`), which
|
|
491
|
+
* only described one shape: with the agent inside the workspace, the root `.dockerignore` is the
|
|
492
|
+
* WORKSPACE's file and was listed; with the agent flat, the list was empty — so `--force` would have
|
|
493
|
+
* overwritten a hand-written root `Dockerfile` in a repository `init --flat` had explicitly adopted.
|
|
494
|
+
* Ownership is a property of the file, not of where the agent sits.
|
|
495
|
+
*
|
|
496
|
+
* A KEPT file of OURS that no longer matches what deploy would generate now (config/channel/lockfile/
|
|
497
|
+
* version drift — or the author's edits, which we cannot tell apart) is flagged stale; `--force`
|
|
498
|
+
* regenerates that one.
|
|
499
|
+
*
|
|
500
|
+
* Exported for its own test — this is a four-branch state machine over (exists, ours, force) that used
|
|
501
|
+
* to be proven by spawning the CLI eight times, which is command LOGIC re-run through a subprocess
|
|
502
|
+
* (see vitest.config.ts) and the suite's slowest test.
|
|
426
503
|
*/
|
|
427
504
|
export async function writeArtifacts(target, artifacts, options) {
|
|
428
505
|
for (const a of artifacts) {
|
|
@@ -461,7 +538,7 @@ export async function writeArtifacts(target, artifacts, options) {
|
|
|
461
538
|
}
|
|
462
539
|
}
|
|
463
540
|
function deployEnvironment(agentDir, channels) {
|
|
464
|
-
if (!channels.
|
|
541
|
+
if (!channels.some((channel) => channel.name === "slack"))
|
|
465
542
|
return process.env;
|
|
466
543
|
const latest = readSlackBotAuthEnv(join(resolveStateRoot(agentDir), "channels", "slack", "bot-auth.json"));
|
|
467
544
|
return { ...process.env, ...latest };
|
|
@@ -472,37 +549,43 @@ function deployEnvironment(agentDir, channels) {
|
|
|
472
549
|
* when present, yields an ephemeral URL that reuses the same webhook announcer as `dev --tunnel`.
|
|
473
550
|
*/
|
|
474
551
|
async function runDeployDocker(params) {
|
|
475
|
-
const { agentDir, workspace, composeFile, port, requireTunnel,
|
|
476
|
-
const { secrets, missingSecrets, needsModelCredential } =
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const outcome = await deployDockerRun({ composeFile, port, secrets, missingSecrets, needsModelCredential, requireTunnel }, spawnRunner("docker", workspace), (message) => console.error(`[fastagent] ${message}`));
|
|
486
|
-
if (!outcome.ok)
|
|
487
|
-
failStartup(new Error(`deploy stopped: ${outcome.gate}`));
|
|
488
|
-
const compose = `docker compose -f ${composeFile}`;
|
|
489
|
-
console.error(`[fastagent] running${outcome.url ? ` → ${outcome.url}` : ""}`);
|
|
490
|
-
console.error(`[fastagent] logs: ${compose} logs -f agent`);
|
|
491
|
-
console.error(`[fastagent] stop: ${compose} down (state volume is kept)`);
|
|
492
|
-
if (outcome.tunnelUrl) {
|
|
493
|
-
// Docker Desktop commonly injects a host proxy. The Quick Tunnel hostname may be resolvable only
|
|
494
|
-
// through it, exactly like provider/channel APIs; use the same Node dispatcher as dev/start/login.
|
|
495
|
-
installProxyFetch();
|
|
496
|
-
await announceWebhooks(agentDir, outcome.tunnelUrl, {
|
|
552
|
+
const { agentDir, workspace, composeFile, port, requireTunnel, channels } = params;
|
|
553
|
+
const { secrets, missingSecrets, needsModelCredential } = await carryCredentials(params);
|
|
554
|
+
const outcome = await deployDockerRun({
|
|
555
|
+
composeFile,
|
|
556
|
+
port,
|
|
557
|
+
secrets,
|
|
558
|
+
missingSecrets,
|
|
559
|
+
needsModelCredential,
|
|
560
|
+
requireTunnel,
|
|
561
|
+
announce: (tunnelUrl) => announceWebhooks(agentDir, tunnelUrl, channels, {
|
|
497
562
|
openUrl: openExternalUrl,
|
|
498
|
-
routeChannels: channels.filter((kind) => !longConnectionChannels.includes(kind)),
|
|
499
563
|
stateRoot: resolveStateRoot(agentDir),
|
|
500
|
-
})
|
|
564
|
+
}),
|
|
565
|
+
}, spawnRunner("docker", workspace), (message) => console.error(`[fastagent] ${message}`));
|
|
566
|
+
const compose = `docker compose -f ${composeFile}`;
|
|
567
|
+
// Compose reached "up" iff the driver could report where it is. The gates before that point
|
|
568
|
+
// (no Docker CLI, no daemon, missing secrets) must not be preceded by `docker compose logs`
|
|
569
|
+
// instructions for containers that do not exist; the health-check gate names its own logs command.
|
|
570
|
+
const isUp = outcome.ok || outcome.url !== undefined || outcome.tunnelUrl !== undefined;
|
|
571
|
+
if (outcome.url)
|
|
572
|
+
console.error(`[fastagent] running → ${outcome.url}`);
|
|
573
|
+
if (isUp) {
|
|
574
|
+
console.error(`[fastagent] logs: ${compose} logs -f agent`);
|
|
575
|
+
console.error(`[fastagent] stop: ${compose} down (state volume is kept)`);
|
|
576
|
+
}
|
|
577
|
+
// BEFORE failStartup: a registration gate says "re-run this deploy", and a re-run rebuilds the
|
|
578
|
+
// tunnel service — the operator needs to know the URL will be a different one, or they will read
|
|
579
|
+
// the retry as re-registering the same address.
|
|
580
|
+
if (outcome.tunnelUrl) {
|
|
501
581
|
console.error(`[fastagent] note: Quick Tunnel URLs are ephemeral — after the tunnel container/Docker daemon ` +
|
|
502
582
|
`restarts, re-run this deploy so webhooks receive the new URL`);
|
|
503
|
-
return;
|
|
504
583
|
}
|
|
505
|
-
|
|
584
|
+
if (!outcome.ok)
|
|
585
|
+
failStartup(new Error(`deploy stopped: ${outcome.gate}`));
|
|
586
|
+
if (outcome.tunnelUrl)
|
|
587
|
+
return;
|
|
588
|
+
const paths = webhookPaths(channels);
|
|
506
589
|
if (paths.length > 0) {
|
|
507
590
|
console.error(`[fastagent] note: public ingress is operator-owned — configure your tunnel/proxy, then wire the ` +
|
|
508
591
|
`default webhook path(s): ${paths.join(", ")} (or your remapped channel routes)`);
|
|
@@ -516,37 +599,24 @@ async function runDeployDocker(params) {
|
|
|
516
599
|
* behind the shared {@link spawnRunner} seam (spawned `fly`, cwd = the workspace so the build context is the whole workspace).
|
|
517
600
|
*/
|
|
518
601
|
async function runDeployFly(params) {
|
|
519
|
-
const { agentDir, workspace, agentPrefix, appName,
|
|
602
|
+
const { agentDir, workspace, agentPrefix, appName, channels, flyTomlPath } = params;
|
|
520
603
|
const fly = spawnRunner("fly", workspace);
|
|
521
604
|
// Fail fast if flyctl is absent (spawn ENOENT → 127), with the install link — not a confusing auth gate.
|
|
522
605
|
if ((await fly(["version"], { capture: true })).code === 127) {
|
|
523
606
|
failStartup(new Error(`flyctl not found — install it: https://fly.io/docs/flyctl/install, then re-run`));
|
|
524
607
|
}
|
|
525
608
|
const region = parseFlyRegion(await readFile(flyTomlPath, "utf8")) ?? "iad";
|
|
526
|
-
const { secrets, missingSecrets, needsModelCredential } =
|
|
527
|
-
|
|
528
|
-
modelKeyInDefinition,
|
|
529
|
-
authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
|
|
530
|
-
channels,
|
|
531
|
-
longConnectionChannels,
|
|
532
|
-
extraSecrets,
|
|
533
|
-
env: deployEnvironment(agentDir, channels),
|
|
534
|
-
});
|
|
535
|
-
// Model credential has its OWN remediation (login), distinct from a missing secret's (.env) — gate it
|
|
536
|
-
// here, not through missingSecrets, so the message isn't a contradictory mash of both.
|
|
537
|
-
if (needsModelCredential) {
|
|
538
|
-
failStartup(new Error(`deploy stopped: no model credential — run \`fastagent login\`, or set a provider API key in .env, then re-run`));
|
|
539
|
-
}
|
|
609
|
+
const { secrets, missingSecrets, needsModelCredential } = await carryCredentials(params);
|
|
610
|
+
gateOnModelCredential(needsModelCredential);
|
|
540
611
|
const outcome = await deployFlyRun({
|
|
541
612
|
appName,
|
|
542
613
|
region,
|
|
543
614
|
secrets,
|
|
544
615
|
missingSecrets,
|
|
545
616
|
channels,
|
|
546
|
-
longConnectionChannels,
|
|
547
617
|
flyConfig: `${agentPrefix}fly.toml`,
|
|
548
618
|
dockerfile: `${agentPrefix}Dockerfile`,
|
|
549
|
-
}, fly, (m) => console.error(`[fastagent] ${m}`), (
|
|
619
|
+
}, fly, (m) => console.error(`[fastagent] ${m}`), registrarsFor(agentDir));
|
|
550
620
|
if (!outcome.ok)
|
|
551
621
|
failStartup(new Error(`deploy stopped: ${outcome.gate}`));
|
|
552
622
|
console.error(`[fastagent] deployed → https://${appName}.fly.dev`);
|
|
@@ -559,15 +629,8 @@ async function runDeployFly(params) {
|
|
|
559
629
|
* file (secret values off argv) is created here — 0600, removed after the run either way.
|
|
560
630
|
*/
|
|
561
631
|
async function runDeployAgentcore(params) {
|
|
562
|
-
const { agentDir, workspace, agentPrefix, name,
|
|
563
|
-
const { secrets, missingSecrets, needsModelCredential } =
|
|
564
|
-
modelAuth,
|
|
565
|
-
modelKeyInDefinition,
|
|
566
|
-
authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
|
|
567
|
-
channels,
|
|
568
|
-
extraSecrets,
|
|
569
|
-
env: deployEnvironment(agentDir, channels),
|
|
570
|
-
});
|
|
632
|
+
const { agentDir, workspace, agentPrefix, name, channels, selfSchedule } = params;
|
|
633
|
+
const { secrets, missingSecrets, needsModelCredential } = await carryCredentials(params);
|
|
571
634
|
// The wake-alarm shared secret (container ↔ forwarder). Minted fresh each run — both sides receive
|
|
572
635
|
// the SAME parameter, so rotation is atomic; it never needs to be remembered locally.
|
|
573
636
|
if (selfSchedule)
|
|
@@ -576,9 +639,7 @@ async function runDeployAgentcore(params) {
|
|
|
576
639
|
// principal's. Minted fresh each run — both sides receive the SAME parameter, so rotation is atomic.
|
|
577
640
|
if (params.needsForwarder)
|
|
578
641
|
secrets.FASTAGENT_INGRESS_SECRET = crypto.randomUUID();
|
|
579
|
-
|
|
580
|
-
failStartup(new Error(`deploy stopped: no model credential — run \`fastagent login\`, or set a provider API key in .env, then re-run`));
|
|
581
|
-
}
|
|
642
|
+
gateOnModelCredential(needsModelCredential);
|
|
582
643
|
// The params temp dir holds the ONE file carrying secret values (file:// parameter-overrides —
|
|
583
644
|
// never argv); 0700/0600 and removed after the run, success or gate.
|
|
584
645
|
const paramsDir = await mkdtemp(join(tmpdir(), "fastagent-agentcore-"));
|
|
@@ -601,13 +662,12 @@ async function runDeployAgentcore(params) {
|
|
|
601
662
|
}, spawnRunner("aws", workspace), spawnRunner("docker", workspace), (m) => console.error(`[fastagent] ${m}`), async (content) => {
|
|
602
663
|
const path = join(paramsDir, "params.json");
|
|
603
664
|
await writeFile(path, content, { mode: 0o600 });
|
|
604
|
-
await chmod(path, 0o600);
|
|
605
665
|
return path;
|
|
606
666
|
}, async (bytes) => {
|
|
607
667
|
const path = join(paramsDir, "forwarder.zip");
|
|
608
668
|
await writeFile(path, bytes);
|
|
609
669
|
return path;
|
|
610
|
-
}, (
|
|
670
|
+
}, registrarsFor(agentDir));
|
|
611
671
|
if (!outcome.ok)
|
|
612
672
|
failStartup(new Error(`deploy stopped: ${outcome.gate}`));
|
|
613
673
|
console.error(`[fastagent] deployed → ${outcome.runtimeArn}`);
|
|
@@ -634,26 +694,15 @@ async function runDeployAgentcore(params) {
|
|
|
634
694
|
* webhook) lives in {@link deployRailwayRun}; see there for why Railway differs from Fly.
|
|
635
695
|
*/
|
|
636
696
|
async function runDeployRailway(params) {
|
|
637
|
-
const { agentDir, workspace, name,
|
|
697
|
+
const { agentDir, workspace, name, channels, intoLinked, dockerfilePath } = params;
|
|
638
698
|
const railway = spawnRunner("railway", workspace);
|
|
639
699
|
// Fail fast if the railway CLI is absent (spawn ENOENT → 127), with the install link.
|
|
640
700
|
if ((await railway(["--version"], { capture: true })).code === 127) {
|
|
641
701
|
failStartup(new Error(`railway CLI not found — install it: https://docs.railway.com/guides/cli, then re-run`));
|
|
642
702
|
}
|
|
643
|
-
const { secrets, missingSecrets, needsModelCredential } =
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
|
|
647
|
-
channels,
|
|
648
|
-
longConnectionChannels,
|
|
649
|
-
extraSecrets,
|
|
650
|
-
env: deployEnvironment(agentDir, channels),
|
|
651
|
-
});
|
|
652
|
-
// Model credential has its OWN remediation (login), distinct from a missing secret's (.env).
|
|
653
|
-
if (needsModelCredential) {
|
|
654
|
-
failStartup(new Error(`deploy stopped: no model credential — run \`fastagent login\`, or set a provider API key in .env, then re-run`));
|
|
655
|
-
}
|
|
656
|
-
const outcome = await deployRailwayRun({ name, mountPath: "/data", secrets, missingSecrets, channels, longConnectionChannels, intoLinked, dockerfilePath }, railway, (m) => console.error(`[fastagent] ${m}`), (baseUrl) => registerTelegramWebhook(baseUrl), (baseUrl, kind) => registerFeishuWebhook(baseUrl, kind), (baseUrl) => registerSlackWebhook(baseUrl, { stateRoot: resolveStateRoot(agentDir) }));
|
|
703
|
+
const { secrets, missingSecrets, needsModelCredential } = await carryCredentials(params);
|
|
704
|
+
gateOnModelCredential(needsModelCredential);
|
|
705
|
+
const outcome = await deployRailwayRun({ name, mountPath: "/data", secrets, missingSecrets, channels, intoLinked, dockerfilePath }, railway, (m) => console.error(`[fastagent] ${m}`), registrarsFor(agentDir));
|
|
657
706
|
if (!outcome.ok)
|
|
658
707
|
failStartup(new Error(`deploy stopped: ${outcome.gate}`));
|
|
659
708
|
console.error(`[fastagent] deployed → ${outcome.url}`);
|
package/dist/cli/commands/dev.js
CHANGED
|
@@ -6,19 +6,15 @@
|
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
7
|
import { runDevSupervisor } from "../../dev-supervisor.js";
|
|
8
8
|
import { loadDotEnv } from "../../env.js";
|
|
9
|
-
import {
|
|
10
|
-
import { reportModuleLoadFailures } from "../../log.js";
|
|
11
|
-
import { CODING_TOOL_NAMES } from "../../engines/pi/create.js";
|
|
9
|
+
import { setLogLevel } from "../../log.js";
|
|
12
10
|
import { createPiAgentFromDir } from "../../engines/pi/open.js";
|
|
13
11
|
import { mountAgentService } from "../../service.js";
|
|
14
|
-
import { setLogLevel } from "../../log.js";
|
|
15
12
|
import { logAgentLoop } from "../../observe.js";
|
|
16
13
|
import { installProxyFetch } from "../../proxy.js";
|
|
17
|
-
import { workspaceHint } from "../../paths.js";
|
|
18
14
|
import { bindAddress } from "../../bind.js";
|
|
19
15
|
import { failStartup, placementOrExit } from "../fail.js";
|
|
20
16
|
import { SHUTDOWN_GRACE_MS, assertTunnelBindable, maybeTunnel, reportServing, serve } from "../serve.js";
|
|
21
|
-
import { parseBind, parsePort,
|
|
17
|
+
import { parseBind, parsePort, reportAssembly, resolveFirstRunModel } from "../shared.js";
|
|
22
18
|
export async function runDev(dirArg, opts) {
|
|
23
19
|
const dir = resolve(dirArg);
|
|
24
20
|
const placement = placementOrExit(dir);
|
|
@@ -54,15 +50,8 @@ async function serveOnce(dir, opts) {
|
|
|
54
50
|
authPath: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by the opener (one owner)
|
|
55
51
|
serving: true, // long-running serve: the scheduler poller runs (wake mounts iff config.selfSchedule)
|
|
56
52
|
}).catch(failStartup);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
reportWorkspaceHint(workspaceHint(a));
|
|
60
|
-
reportLine("config", a.configPath ?? "(none)");
|
|
61
|
-
reportLine("model", `${a.modelSpec}${a.config.thinkingLevel ? ` (thinking: ${a.config.thinkingLevel})` : ""}`);
|
|
62
|
-
await reportAuth(a.agentDir, a.modelSpec, a.authPath);
|
|
63
|
-
reportAgentsSkillsTools(a);
|
|
64
|
-
// Trace each turn's agent loop (tool calls + reply) to the log at debug level — shown in dev, gated
|
|
65
|
-
// out in start (level info), keeping end-user content out of production logs. Wired in both postures.
|
|
53
|
+
// The same report `start` prints; `config:` is dev's own extra (see reportAssembly on the asymmetry).
|
|
54
|
+
await reportAssembly(a, { beforeModel: [["config", a.configPath ?? "(none)"]] });
|
|
66
55
|
// `http.host` enters here the way the flag enters `parseBind` — through `bindAddress`, so a
|
|
67
56
|
// configured `localhost` is an ADDRESS by the time anything binds, renders or dials it.
|
|
68
57
|
const configured = a.config.http?.host;
|
|
@@ -89,19 +78,3 @@ async function serveOnce(dir, opts) {
|
|
|
89
78
|
onShutdown: () => service.close(),
|
|
90
79
|
});
|
|
91
80
|
}
|
|
92
|
-
/** The agents/skills/tools/collisions report lines. */
|
|
93
|
-
function reportAgentsSkillsTools(a) {
|
|
94
|
-
reportLine("context", a.definition.contextFiles.map((f) => f.path).join(", ") || "(none)");
|
|
95
|
-
if (a.definition.persona)
|
|
96
|
-
reportLine("persona", "persona.md");
|
|
97
|
-
reportLine("skills", a.definition.skills.map((s) => s.name).join(", ") || "(none)");
|
|
98
|
-
reportLine("codingTools", CODING_TOOL_NAMES.join(", "));
|
|
99
|
-
if (a.toolNames.length > 0)
|
|
100
|
-
reportLine("tools", a.toolNames.join(", "));
|
|
101
|
-
if (a.deferredToolNames.length > 0) {
|
|
102
|
-
reportLine("deferred", `${a.deferredToolNames.join(", ")} (activated via search_tools)`);
|
|
103
|
-
}
|
|
104
|
-
reportToolCollisions(a.toolCollisions);
|
|
105
|
-
reportModuleLoadFailures(a.toolFailures);
|
|
106
|
-
reportFindingsIfChanged(a.definition.dir, a.definition);
|
|
107
|
-
}
|
|
@@ -8,8 +8,7 @@ import { resolveStateRoot, workspaceHint } from "../../paths.js";
|
|
|
8
8
|
import { CODING_TOOL_NAMES, resolveAgentTools } from "../../engines/pi/create.js";
|
|
9
9
|
import { loadAgentDefinition } from "../../engines/pi/definition.js";
|
|
10
10
|
import { reportFindingsIfChanged, reportToolCollisions } from "../../engines/pi/report.js";
|
|
11
|
-
import { reportModuleLoadFailures } from "../../log.js";
|
|
12
|
-
import { log } from "../../log.js";
|
|
11
|
+
import { reportModuleLoadFailures, log } from "../../log.js";
|
|
13
12
|
import { nextRun } from "../../schedule/cron.js";
|
|
14
13
|
import { loadSchedules } from "../../schedule/discover.js";
|
|
15
14
|
import { failStartup, placementOrExit } from "../fail.js";
|
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { basename, join, resolve } from "node:path";
|
|
10
|
-
import { DEFAULT_AGENT_DIRNAME, SECRETS_DIRNAME, agentsAt } from "../../paths.js";
|
|
10
|
+
import { DEFAULT_AGENT_DIRNAME, SECRETS_DIRNAME, agentsAt, displayPath } from "../../paths.js";
|
|
11
11
|
import { detectRuntime, readPackageJson } from "../../runtime.js";
|
|
12
12
|
import { agentDirName, agentDirNameError, scaffoldAgent } from "../../scaffold/init.js";
|
|
13
|
-
import { displayPath } from "../../paths.js";
|
|
14
13
|
import { failStartup, failUsage } from "../fail.js";
|
|
15
14
|
export async function runInit(dirArg, opts) {
|
|
16
15
|
const dir = resolve(dirArg);
|