@fastagent-sh/fastagent 0.19.0 → 0.21.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 +5 -1
- 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-protocol.d.ts +112 -0
- package/dist/channels/agentcore-protocol.js +22 -0
- package/dist/channels/agentcore-service.d.ts +12 -15
- package/dist/channels/agentcore-service.js +15 -24
- package/dist/channels/agentcore-state.d.ts +5 -11
- package/dist/channels/agentcore-state.js +4 -1
- package/dist/channels/agentcore.d.ts +9 -83
- package/dist/channels/agentcore.js +101 -93
- package/dist/channels/control.d.ts +50 -12
- package/dist/channels/control.js +251 -157
- package/dist/channels/discover.d.ts +26 -5
- package/dist/channels/discover.js +13 -26
- package/dist/channels/feishu/context-buffer.d.ts +6 -0
- package/dist/channels/feishu/context-buffer.js +0 -38
- package/dist/channels/feishu/crypto.d.ts +0 -2
- package/dist/channels/feishu/crypto.js +3 -8
- package/dist/channels/feishu/feishu-api.js +7 -12
- package/dist/channels/feishu/feishu.js +86 -128
- package/dist/channels/feishu/parse.d.ts +4 -3
- package/dist/channels/feishu/parse.js +5 -4
- package/dist/channels/feishu/register-webhook.d.ts +5 -5
- package/dist/channels/feishu/register-webhook.js +43 -57
- package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
- package/dist/channels/feishu/setup-mode.d.ts +30 -0
- package/dist/channels/feishu/setup-mode.js +26 -0
- package/dist/channels/feishu/shared-api.d.ts +10 -0
- package/dist/channels/feishu/shared-api.js +38 -0
- package/dist/channels/http.d.ts +0 -8
- package/dist/channels/http.js +4 -56
- 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-runner.d.ts +59 -0
- package/dist/channels/kit/turn-runner.js +84 -0
- package/dist/channels/kit/turn-store.d.ts +19 -3
- package/dist/channels/kit/turn-store.js +29 -2
- package/dist/channels/lark/scaffold/lark-send.ts +12 -67
- package/dist/channels/registration.d.ts +36 -1
- package/dist/channels/registration.js +57 -1
- package/dist/channels/secret.d.ts +1 -0
- package/dist/channels/secret.js +16 -0
- package/dist/channels/slack/config-api.d.ts +35 -3
- package/dist/channels/slack/config-api.js +51 -9
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +5 -1
- package/dist/channels/slack/manifest.js +5 -1
- package/dist/channels/slack/onboard.d.ts +5 -5
- package/dist/channels/slack/onboard.js +44 -23
- 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.d.ts +3 -2
- package/dist/channels/slack/register-webhook.js +35 -15
- package/dist/channels/slack/scaffold/channel.ts +3 -10
- package/dist/channels/slack/scaffold/slack-send.ts +18 -141
- package/dist/channels/slack/shared-api.d.ts +10 -0
- package/dist/channels/slack/shared-api.js +34 -0
- package/dist/channels/slack/slack-api.d.ts +20 -2
- package/dist/channels/slack/slack-api.js +100 -73
- package/dist/channels/slack/slack.d.ts +0 -10
- package/dist/channels/slack/slack.js +68 -107
- package/dist/channels/sse.d.ts +4 -0
- package/dist/channels/sse.js +66 -0
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +65 -11
- package/dist/channels/telegram/register-webhook.d.ts +6 -9
- package/dist/channels/telegram/register-webhook.js +44 -42
- 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 +97 -204
- package/dist/channels/wait-health.js +7 -4
- package/dist/cli/add-feishu.js +3 -10
- package/dist/cli/add-slack.js +13 -25
- package/dist/cli/commands/add.d.ts +0 -1
- package/dist/cli/commands/add.js +22 -35
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/chat.js +6 -12
- package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
- package/dist/cli/commands/deploy/agentcore.js +178 -0
- package/dist/cli/commands/deploy/docker.d.ts +2 -0
- package/dist/cli/commands/deploy/docker.js +119 -0
- package/dist/cli/commands/deploy/fly.d.ts +2 -0
- package/dist/cli/commands/deploy/fly.js +131 -0
- package/dist/cli/commands/deploy/railway.d.ts +2 -0
- package/dist/cli/commands/deploy/railway.js +71 -0
- package/dist/cli/commands/deploy/shared.d.ts +114 -0
- package/dist/cli/commands/deploy/shared.js +124 -0
- package/dist/cli/commands/deploy.d.ts +38 -25
- package/dist/cli/commands/deploy.js +101 -631
- package/dist/cli/commands/dev.js +20 -76
- package/dist/cli/commands/fire.js +6 -12
- package/dist/cli/commands/info.js +1 -1
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/invoke.js +4 -11
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +41 -80
- package/dist/cli/commands/tool.js +3 -8
- package/dist/cli/kernel.d.ts +0 -2
- package/dist/cli/kernel.js +0 -2
- package/dist/cli/program.js +7 -12
- package/dist/cli/serve.d.ts +42 -34
- package/dist/cli/serve.js +107 -28
- package/dist/cli/shared.d.ts +52 -27
- package/dist/cli/shared.js +69 -6
- package/dist/deploy/agentcore/forwarder.js +250 -0
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +34 -11
- package/dist/deploy/agentcore/plan.js +52 -259
- package/dist/deploy/agentcore/run.d.ts +9 -24
- package/dist/deploy/agentcore/run.js +35 -43
- 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 +35 -4
- package/dist/deploy/docker/run.js +40 -8
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +14 -22
- package/dist/deploy/fly/run.d.ts +36 -23
- package/dist/deploy/fly/run.js +129 -83
- package/dist/deploy/hosts.d.ts +5 -0
- package/dist/deploy/hosts.js +4 -0
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +27 -23
- package/dist/deploy/railway/plan.d.ts +9 -5
- package/dist/deploy/railway/plan.js +18 -26
- package/dist/deploy/railway/run.d.ts +11 -10
- package/dist/deploy/railway/run.js +44 -43
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +63 -28
- package/dist/dev-supervisor.js +3 -4
- package/dist/engines/pi/agent-session-factory.d.ts +37 -17
- package/dist/engines/pi/agent-session-factory.js +111 -106
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +2 -2
- package/dist/engines/pi/create.d.ts +22 -27
- package/dist/engines/pi/create.js +59 -74
- package/dist/engines/pi/definition.d.ts +1 -1
- package/dist/engines/pi/definition.js +11 -12
- package/dist/engines/pi/invoke-session.js +13 -30
- package/dist/engines/pi/login.js +32 -16
- package/dist/engines/pi/open.d.ts +3 -4
- package/dist/engines/pi/open.js +32 -42
- package/dist/engines/pi/retry-event.d.ts +6 -0
- package/dist/engines/pi/retry-event.js +15 -0
- 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 +26 -140
- package/dist/engines/pi/session-control.d.ts +27 -22
- package/dist/engines/pi/session-control.js +551 -490
- 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 +5 -5
- package/dist/engines/pi/session-settings.js +8 -5
- package/dist/engines/pi/session-store.d.ts +91 -26
- package/dist/engines/pi/session-store.js +413 -82
- package/dist/engines/pi/tool-context.d.ts +24 -11
- package/dist/engines/pi/tool-context.js +29 -4
- package/dist/engines/pi/tool.d.ts +6 -9
- package/dist/engines/pi/tool.js +3 -2
- package/dist/env.js +1 -2
- package/dist/feishu.d.ts +1 -0
- package/dist/feishu.js +1 -0
- package/dist/lark.d.ts +1 -0
- package/dist/lark.js +1 -0
- package/dist/loader.d.ts +51 -7
- package/dist/loader.js +84 -18
- package/dist/log.d.ts +9 -17
- package/dist/log.js +25 -30
- package/dist/paths.d.ts +26 -3
- package/dist/paths.js +43 -5
- package/dist/scaffold/add-channel.d.ts +6 -1
- package/dist/scaffold/add-channel.js +50 -67
- 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 +14 -19
- package/dist/schedule/wake-alarm.js +89 -48
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +10 -7
- package/dist/service.d.ts +24 -24
- package/dist/service.js +33 -77
- package/dist/session-remote.d.ts +12 -7
- package/dist/session-remote.js +185 -118
- package/dist/session.d.ts +227 -93
- package/dist/session.js +61 -23
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +1 -0
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +34 -9
- package/dist/tunnel.js +83 -50
- package/package.json +5 -4
- package/dist/channels/slack/bot-auth.d.ts +0 -15
- package/dist/channels/slack/bot-auth.js +0 -135
package/dist/cli/commands/add.js
CHANGED
|
@@ -8,13 +8,9 @@ import { join, relative, resolve } from "node:path";
|
|
|
8
8
|
import { isCancel, select } from "@clack/prompts";
|
|
9
9
|
import { onboardFeishuCloudApp } from "../add-feishu.js";
|
|
10
10
|
import { dotEnvPath, loadDotEnv } from "../../env.js";
|
|
11
|
-
import { resolveStateRoot } from "../../paths.js";
|
|
12
|
-
import { SECRETS_DIRNAME } from "../../paths.js";
|
|
11
|
+
import { resolveStateRoot, SECRETS_DIRNAME, isUnderDir, displayPath } from "../../paths.js";
|
|
13
12
|
import { detectRuntime, readPackageJson } from "../../runtime.js";
|
|
14
|
-
import {
|
|
15
|
-
import { displayPath } from "../../paths.js";
|
|
16
|
-
import { appendChannelDotEnv, appendChannelEnv, assertChannelReady, channelExists, channelSetup, scaffoldChannel, } from "../../scaffold/add-channel.js";
|
|
17
|
-
import { exists } from "../../paths.js";
|
|
13
|
+
import { appendChannelDotEnv, appendChannelEnv, assertChannelReady, channelExists, channelSetup, scaffoldChannel, scaffoldCompanionTools, } from "../../scaffold/add-channel.js";
|
|
18
14
|
import { vendorSkill } from "../../scaffold/vendor-skill.js";
|
|
19
15
|
import { failStartup, failUsage, placementOrExit } from "../fail.js";
|
|
20
16
|
/** `fastagent add <kind> [dir]`: scaffold `channels/<kind>.ts` — the adapter import plus a starter `on()`. */
|
|
@@ -37,25 +33,10 @@ export async function runAddChannel(channelKind, dirArg, opts) {
|
|
|
37
33
|
const inAgent = (p) => (agentFromCwd === undefined ? p : join(agentFromCwd, p));
|
|
38
34
|
const envPath = dotEnvPath(target);
|
|
39
35
|
const envLabel = isUnderDir(envPath, target) ? inAgent(relative(target, envPath)) : envPath;
|
|
40
|
-
// App creation is not a flag — it is what `add feishu` IS (the scan-to-create flow is the default
|
|
41
|
-
// and only path there). The retired --create-app spelling gets a pointer, not silence.
|
|
42
|
-
if (opts.createApp) {
|
|
43
|
-
if (channelKind === "feishu") {
|
|
44
|
-
console.error(`[fastagent] note: --create-app is retired — \`add feishu\` creates the app by default`);
|
|
45
|
-
}
|
|
46
|
-
else if (channelKind === "lark") {
|
|
47
|
-
failStartup(new Error("--create-app is retired — `add lark` now opens the developer console and guides credential setup by default"));
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
failStartup(new Error("--create-app is retired — app creation is the default behavior of `add feishu`"));
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
36
|
// Preconditions before the write, so a refusal is side-effect-free. slack/feishu/lark are exceptions:
|
|
54
37
|
// their add is scaffold + ONBOARD THE APP, so an existing scaffold skips the write and continues (a
|
|
55
38
|
// failed/cancelled app or OAuth flow must be re-runnable without hand-deleting authored glue).
|
|
56
39
|
const file = join(target, "channels", `${channelKind}.ts`);
|
|
57
|
-
const slackToolFile = join(target, "tools", "slack-send.ts");
|
|
58
|
-
const slackToolExisted = channelKind === "slack" ? await exists(slackToolFile) : false;
|
|
59
40
|
const existsAlready = await channelExists(target, channelKind).catch(failStartup);
|
|
60
41
|
const ingress = await resolveIngress(channelKind, file, existsAlready, opts.ingress);
|
|
61
42
|
const groupBehavior = await resolveGroupBehavior(channelKind, opts.groupBehavior);
|
|
@@ -67,11 +48,13 @@ export async function runAddChannel(channelKind, dirArg, opts) {
|
|
|
67
48
|
}
|
|
68
49
|
else {
|
|
69
50
|
await assertChannelReady(target).catch(failStartup);
|
|
70
|
-
await scaffoldChannel(target, channelKind, { ingress
|
|
51
|
+
await scaffoldChannel(target, channelKind, { ingress }).catch(failStartup);
|
|
71
52
|
console.error(`[fastagent] created ${relative(target, file)}`);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
53
|
+
}
|
|
54
|
+
// Companion tools are the package's, not authored glue: written on every add, so an upgraded
|
|
55
|
+
// package reaches an existing agent by re-running `add <kind>` (`--no-onboard` skips the prompts).
|
|
56
|
+
for (const tool of await scaffoldCompanionTools(target, channelKind).catch(failStartup)) {
|
|
57
|
+
console.error(`[fastagent] wrote ${relative(target, tool)}`);
|
|
75
58
|
}
|
|
76
59
|
if (await appendChannelEnv(target, channelKind, ingress).catch(failStartup)) {
|
|
77
60
|
console.error(`[fastagent] added ${channelKind} env vars to ${inAgent(join(SECRETS_DIRNAME, ".env.example"))}`);
|
|
@@ -97,9 +80,9 @@ export async function runAddChannel(channelKind, dirArg, opts) {
|
|
|
97
80
|
const env = setup.env;
|
|
98
81
|
const steps = channelKind === "slack" && opts.onboard !== false
|
|
99
82
|
? [
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
"invite the app to
|
|
83
|
+
// No line about the Request URL: the `dev --tunnel` line below is the whole instruction, and
|
|
84
|
+
// FastAgent sets that URL itself when the agent first runs.
|
|
85
|
+
"invite the app to each channel it should read",
|
|
103
86
|
"the agent can send messages or files by calling the scaffolded {tools}/slack-send.ts tool",
|
|
104
87
|
]
|
|
105
88
|
: setup.steps;
|
|
@@ -141,7 +124,7 @@ export async function runAddChannel(channelKind, dirArg, opts) {
|
|
|
141
124
|
console.error(` fastagent dev # no public URL or tunnel required`);
|
|
142
125
|
}
|
|
143
126
|
else if (channelKind === "slack") {
|
|
144
|
-
console.error(` fastagent dev --tunnel #
|
|
127
|
+
console.error(` fastagent dev --tunnel # ${opts.onboard === false ? "serve locally + print the URL to paste into the Slack app's Event Subscriptions" : "start the agent — then message it in Slack"}`);
|
|
145
128
|
}
|
|
146
129
|
else if (channelKind === "github") {
|
|
147
130
|
console.error(` fastagent dev --tunnel # serve locally + print the URL for manual GitHub webhook setup`);
|
|
@@ -187,16 +170,20 @@ async function resolveIngress(kind, file, existsAlready, raw) {
|
|
|
187
170
|
}
|
|
188
171
|
const answer = await select({
|
|
189
172
|
message: `How should ${kind === "feishu" ? "Feishu" : "Lark"} deliver events?`,
|
|
173
|
+
// The default must match the non-interactive branch above. Webhook is the cheaper side to be
|
|
174
|
+
// wrong on: its credentials are a superset of websocket's (App ID/Secret plus the Verification
|
|
175
|
+
// Token), and that token has no read API — a websocket app moving to webhook must re-acquire it.
|
|
176
|
+
initialValue: "webhook",
|
|
190
177
|
options: [
|
|
191
|
-
{
|
|
192
|
-
value: "websocket",
|
|
193
|
-
label: "WebSocket long connection",
|
|
194
|
-
hint: "no public URL; requires an always-on process",
|
|
195
|
-
},
|
|
196
178
|
{
|
|
197
179
|
value: "webhook",
|
|
198
180
|
label: "Webhook endpoint",
|
|
199
|
-
hint: "supports scale-to-zero; requires a public HTTPS URL",
|
|
181
|
+
hint: "works on every deploy target; supports scale-to-zero; requires a public HTTPS URL",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
value: "websocket",
|
|
185
|
+
label: "WebSocket long connection",
|
|
186
|
+
hint: "no public URL; needs an always-on process (not on AgentCore)",
|
|
200
187
|
},
|
|
201
188
|
],
|
|
202
189
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type SessionControl, type SessionEntry } from "../../session.ts";
|
|
2
2
|
export interface AttachOptions {
|
|
3
3
|
/** Override the control endpoint (skip control.json discovery) — for a remote serve. */
|
|
4
4
|
url?: string;
|
|
@@ -63,7 +63,7 @@ export interface AttachIo {
|
|
|
63
63
|
warn: (line: string) => void;
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
|
-
* The backfill slice REDUCED TO THE ACTIVE PATH — a session is a tree (a
|
|
66
|
+
* The backfill slice REDUCED TO THE ACTIVE PATH — a session is a tree (a leaf move, or a
|
|
67
67
|
* compaction, leaves sibling branches behind), and printing the slice raw renders the abandoned
|
|
68
68
|
* branch interleaved with the live one as if it were one conversation. Reachability is computed
|
|
69
69
|
* backwards from the leaf; an entry whose parent lies BEFORE the slice is path-connected by
|
|
@@ -106,5 +106,3 @@ settleMs?: number): Promise<{
|
|
|
106
106
|
cursor: string | undefined;
|
|
107
107
|
sawProgress: boolean;
|
|
108
108
|
}>;
|
|
109
|
-
/** The cursor a failed round reached, if it recorded one. */
|
|
110
|
-
export declare function roundCursor(error: unknown): string | undefined;
|
|
@@ -16,7 +16,7 @@ import { loadDotEnv } from "../../env.js";
|
|
|
16
16
|
import { resolveStateRoot } from "../../paths.js";
|
|
17
17
|
import { log, setLogLevel } from "../../log.js";
|
|
18
18
|
import { ABORTED_CODE, SESSION_BUSY_CODE } from "../../agent.js";
|
|
19
|
-
import { NO_ACTIVE_RUN_CODE } from "../../session.js";
|
|
19
|
+
import { NO_ACTIVE_RUN_CODE, } from "../../session.js";
|
|
20
20
|
import { ControlRequestError, connectAgent, connectSessionControl } from "../../session-remote.js";
|
|
21
21
|
import { failStartup, placementOrExit } from "../fail.js";
|
|
22
22
|
/** Read the serving process's local discovery file. */
|
|
@@ -157,7 +157,7 @@ export async function runAttach(sessionArg, dirArg, opts) {
|
|
|
157
157
|
if (!endpoint)
|
|
158
158
|
endpoint = discover(dir); // discovered: the first read shares the budget
|
|
159
159
|
const connected = await connectSessionControl(endpoint);
|
|
160
|
-
return { control: connected, state: await connected.
|
|
160
|
+
return { control: connected, state: await connected.sessions.get(sessionArg).state() };
|
|
161
161
|
}
|
|
162
162
|
catch (error) {
|
|
163
163
|
const decision = decideRound(errorFacts(error), {
|
|
@@ -208,7 +208,7 @@ export async function runAttach(sessionArg, dirArg, opts) {
|
|
|
208
208
|
// ABORTED_CODE: a deliberate stop from ANY client (this attach's /abort, another attach, a
|
|
209
209
|
// Web panel) — the settled line reports it either way; no source discrimination here.
|
|
210
210
|
}
|
|
211
|
-
// session_busy = the LEASE is held — by another run OR a boundary mutation (compact/
|
|
211
|
+
// session_busy = the LEASE is held — by another run OR a boundary mutation (compact/update
|
|
212
212
|
// contend on the same lease); "a run started" would be a guess, and "steer it" bad advice
|
|
213
213
|
// against a compact. State the lease fact, promise nothing.
|
|
214
214
|
if (sawBusy)
|
|
@@ -225,12 +225,14 @@ export async function runAttach(sessionArg, dirArg, opts) {
|
|
|
225
225
|
void answerSlashInput(trimmed, control, (l) => console.log(l));
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
|
-
const
|
|
229
|
-
|
|
228
|
+
const stopping = trimmed === "/abort";
|
|
229
|
+
const label = stopping ? "abort" : "steer";
|
|
230
|
+
const session = control.sessions.get(sessionArg);
|
|
231
|
+
void (stopping ? session.abort() : session.steer({ text: trimmed })).then((result) => {
|
|
230
232
|
if (result.ok) {
|
|
231
|
-
console.log(`[${
|
|
233
|
+
console.log(`[${label} accepted]`);
|
|
232
234
|
}
|
|
233
|
-
else if (
|
|
235
|
+
else if (!stopping && result.error.code === NO_ACTIVE_RUN_CODE) {
|
|
234
236
|
// The invoke stream attach holds IS this run's driver (design: disconnect = cancel), so
|
|
235
237
|
// unlike channel-started runs, this one dies with the attach. Say so up front.
|
|
236
238
|
// Intent, not fact: admission may still fail (session_busy — the lease could be held
|
|
@@ -240,9 +242,9 @@ export async function runAttach(sessionArg, dirArg, opts) {
|
|
|
240
242
|
void startRun(trimmed).catch((error) => console.log(`[prompt failed: ${String(error)}]`));
|
|
241
243
|
}
|
|
242
244
|
else {
|
|
243
|
-
console.log(`[${
|
|
245
|
+
console.log(`[${label} rejected: ${result.error.code} — ${result.error.message}]`);
|
|
244
246
|
}
|
|
245
|
-
}, (error) => console.log(`[${
|
|
247
|
+
}, (error) => console.log(`[${label} failed: ${String(error)}]`));
|
|
246
248
|
});
|
|
247
249
|
// Every round has ONE shape: subscribe → backfill (render the durable record since the cursor)
|
|
248
250
|
// → drain live until the stream drops. Subscribing first + the server's eager registration
|
|
@@ -424,7 +426,7 @@ const isAuthError = (error) => error instanceof ControlRequestError && error.sta
|
|
|
424
426
|
* startup (a serve restart hours in), so the local name must not imply "startup only". */
|
|
425
427
|
const exitWith = failStartup;
|
|
426
428
|
/**
|
|
427
|
-
* The backfill slice REDUCED TO THE ACTIVE PATH — a session is a tree (a
|
|
429
|
+
* The backfill slice REDUCED TO THE ACTIVE PATH — a session is a tree (a leaf move, or a
|
|
428
430
|
* compaction, leaves sibling branches behind), and printing the slice raw renders the abandoned
|
|
429
431
|
* branch interleaved with the live one as if it were one conversation. Reachability is computed
|
|
430
432
|
* backwards from the leaf; an entry whose parent lies BEFORE the slice is path-connected by
|
|
@@ -437,7 +439,7 @@ export function activePathSlice(entries, leafEntryId) {
|
|
|
437
439
|
return entries;
|
|
438
440
|
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
439
441
|
// An append always moves the leaf, so a leaf BEHIND the slice means every entry in it was
|
|
440
|
-
// appended and then abandoned — a
|
|
442
|
+
// appended and then abandoned — a leaf move backwards with no new turn since. Nothing here is on
|
|
441
443
|
// the active path.
|
|
442
444
|
if (!byId.has(leafEntryId))
|
|
443
445
|
return [];
|
|
@@ -509,7 +511,7 @@ settleMs = 300) {
|
|
|
509
511
|
// The round HOLDS its subscription's iterator: one round = one stream, on every path — a
|
|
510
512
|
// backfill failure must close it before propagating, or the caller's retry round would stack a
|
|
511
513
|
// second concurrent stream interleaving the same session's output.
|
|
512
|
-
const iterator = control.
|
|
514
|
+
const iterator = control.sessions.get(session).events()[Symbol.asyncIterator]();
|
|
513
515
|
// Live output is BUFFERED while the replay block prints, then flushed — the drain runs
|
|
514
516
|
// concurrently with the backfill, and interleaving raw deltas into the labeled replay would make
|
|
515
517
|
// both unreadable; the [end of replay] label is only honest if the block is contiguous.
|
|
@@ -554,7 +556,7 @@ settleMs = 300) {
|
|
|
554
556
|
let next = cursor;
|
|
555
557
|
let sawBackfill = false;
|
|
556
558
|
try {
|
|
557
|
-
const backfill = await control.entries(
|
|
559
|
+
const backfill = await control.sessions.get(session).entries(cursor !== undefined ? { since: cursor } : undefined);
|
|
558
560
|
sawBackfill = backfill.entries.length > 0;
|
|
559
561
|
// Advance by APPEND ORDER (the last returned record), never by leafEntryId: `since` is an
|
|
560
562
|
// append-position cursor (design §7), and a leaf that sits before later appends (abandoned
|
|
@@ -579,7 +581,7 @@ settleMs = 300) {
|
|
|
579
581
|
}
|
|
580
582
|
// The protocol's reconnect step the replay cannot cover: status changes while away are
|
|
581
583
|
// LIVE-only events (state_changed before a restart is neither replayed nor re-emitted).
|
|
582
|
-
const now = await control.
|
|
584
|
+
const now = await control.sessions.get(session).state();
|
|
583
585
|
io.println(`[live — ${now.status}${now.activeRunId ? ` (run ${now.activeRunId})` : ""}]`);
|
|
584
586
|
}
|
|
585
587
|
catch (error) {
|
|
@@ -612,7 +614,7 @@ function withCursor(error, cursor) {
|
|
|
612
614
|
return Object.assign(e, { attachCursor: cursor });
|
|
613
615
|
}
|
|
614
616
|
/** The cursor a failed round reached, if it recorded one. */
|
|
615
|
-
|
|
617
|
+
function roundCursor(error) {
|
|
616
618
|
return error.attachCursor;
|
|
617
619
|
}
|
|
618
620
|
/** Render one durable record on replay — the guaranteed kind vocabulary; other kinds are skipped. */
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
/** `fastagent chat [dir]`: open the SAME assembled agent in pi's interactive TUI. */
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { installProxyFetch } from "../../proxy.js";
|
|
5
|
-
import { failStartup, placementOrExit } from "../fail.js";
|
|
6
|
-
import { resolveFirstRunModel } from "../shared.js";
|
|
2
|
+
import { failStartup } from "../fail.js";
|
|
3
|
+
import { enterAgentCommand } from "../shared.js";
|
|
7
4
|
export async function runChat(dirArg, opts) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// other command (the shared session builder injects it — see engines/pi/session-builder.ts), so the
|
|
13
|
-
// credential-annotated catalog and inline login apply here too.
|
|
14
|
-
await resolveFirstRunModel(placement.agentDir, { model: opts.model, authPath: opts.authPath });
|
|
5
|
+
// Chat authenticates through fastagent's credential store like every other command (the shared
|
|
6
|
+
// session builder injects it — see engines/pi/session-builder.ts), so the first-run picker and its
|
|
7
|
+
// inline login apply here too.
|
|
8
|
+
const placement = await enterAgentCommand(dirArg, opts);
|
|
15
9
|
// Run the chat process AT the workspace: pi resolves a session's cwd as `header.cwd ?? process.cwd()`,
|
|
16
10
|
// so aligning process.cwd() with the workspace keeps a cwd-less session on it. Paths are absolute.
|
|
17
11
|
process.chdir(placement.workspace);
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `deploy agentcore`: one CloudFormation stack (runtime + forwarder Lambda + EventBridge schedules);
|
|
3
|
+
* no public URL and no resident process — see deploy/agentcore/plan.ts for the topology decisions.
|
|
4
|
+
*/
|
|
5
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { basename, join } from "node:path";
|
|
8
|
+
import { MAX_WEBHOOK_BODY_BYTES } from "../../../channels/agentcore-limits.js";
|
|
9
|
+
import { FORWARDER_FILE, TEMPLATE_FILE, agentcoreName, isGeneratedAgentcoreTemplate, planAgentcoreDeploy, } from "../../../deploy/agentcore/plan.js";
|
|
10
|
+
import { deployAgentcoreRun } from "../../../deploy/agentcore/run.js";
|
|
11
|
+
import { spawnRunner } from "../../../deploy/runner.js";
|
|
12
|
+
import { exists } from "../../../paths.js";
|
|
13
|
+
import { loadSchedules } from "../../../schedule/discover.js";
|
|
14
|
+
import { failStartup } from "../../fail.js";
|
|
15
|
+
import { carryCredentials, gateOnModelCredential, registrarsFor } from "./shared.js";
|
|
16
|
+
/** A copy/paste-safe POSIX shell argument for the command hints deploy prints. */
|
|
17
|
+
function shellArg(value) {
|
|
18
|
+
return /^[A-Za-z0-9_./:@%+=,-]+$/.test(value) ? value : `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
19
|
+
}
|
|
20
|
+
export const agentcoreHost = {
|
|
21
|
+
isOurs: (path, content) => path.endsWith(TEMPLATE_FILE) && isGeneratedAgentcoreTemplate(content),
|
|
22
|
+
async deploy(ctx) {
|
|
23
|
+
const { opts, agentDir, workspace, config, channels, longConnectionChannels, pre, write } = ctx;
|
|
24
|
+
const { modelAuth, modelKeyInDefinition, authPath, container, extraSecrets } = pre;
|
|
25
|
+
// Long-connection channels are STRUCTURALLY unsupported: the connection is the ingress, and a
|
|
26
|
+
// reclaimed session has nothing to wake it — events in the gap are silently lost. `--run` gates
|
|
27
|
+
// (deploying a channel that can't connect); generate-only warns and prints the runbook.
|
|
28
|
+
if (longConnectionChannels.length > 0) {
|
|
29
|
+
const msg = `long-connection channel (${longConnectionChannels.map((c) => c.name).join(", ")}) cannot run on AgentCore — there is no ` +
|
|
30
|
+
`resident process to hold the connection, and nothing wakes a reclaimed session. Switch the channel ` +
|
|
31
|
+
`to webhook mode (its events then ride the forwarder like every other channel).`;
|
|
32
|
+
if (opts.run)
|
|
33
|
+
failStartup(new Error(`deploy stopped: ${msg}`));
|
|
34
|
+
console.error(`[fastagent] warn: ${msg}`);
|
|
35
|
+
}
|
|
36
|
+
// selfSchedule is fully supported: pending wake-ups are mirrored into one-shot EventBridge
|
|
37
|
+
// schedules via the forwarder (the wake-alarm mechanism — see deploy/agentcore/plan.ts).
|
|
38
|
+
// Schedules feed EventBridge rules — parsed facts (cron/tz), not just file names, so a bad file
|
|
39
|
+
// must surface here (a schedule silently missing its rule would never fire).
|
|
40
|
+
const loaded = await loadSchedules(agentDir).catch(failStartup);
|
|
41
|
+
if (loaded.failures.length > 0) {
|
|
42
|
+
failStartup(new Error(`deploy stopped: cannot load schedules: ${loaded.failures.map((x) => `${x.label}: ${x.message}`).join("; ")}`));
|
|
43
|
+
}
|
|
44
|
+
const acName = agentcoreName(basename(workspace));
|
|
45
|
+
// Every derived AWS name embeds acName; the tightest ceiling is the Lambda function name
|
|
46
|
+
// (`fastagent-<name>-forwarder` ≤ 64 chars). Gate the base instead of silently truncating —
|
|
47
|
+
// truncation would break the redeploy identity (a renamed stack starts blank state).
|
|
48
|
+
if (acName.length > 40) {
|
|
49
|
+
failStartup(new Error(`deploy stopped: the directory name maps to "${acName}" (${acName.length} chars) — AWS resource ` +
|
|
50
|
+
`names derived from it exceed their limits past 40 chars. Deploy from a shorter directory name.`));
|
|
51
|
+
}
|
|
52
|
+
const plan = planAgentcoreDeploy({
|
|
53
|
+
name: acName,
|
|
54
|
+
modelAuth,
|
|
55
|
+
channels,
|
|
56
|
+
extraSecrets,
|
|
57
|
+
schedules: loaded.schedules.map((s) => ({ name: s.name, cron: s.cron, tz: s.tz })),
|
|
58
|
+
selfSchedule: !!config.selfSchedule,
|
|
59
|
+
...container,
|
|
60
|
+
});
|
|
61
|
+
for (const u of plan.untranslatableSchedules) {
|
|
62
|
+
// Same discipline as Fly's kept-toml time-trigger gate: a deploy whose schedule silently never
|
|
63
|
+
// fires is worse than a stopped deploy — nothing fails visibly when the instant passes.
|
|
64
|
+
const msg = `schedule "${u.name}" cannot be expressed as an EventBridge rule — ${u.reason}`;
|
|
65
|
+
if (opts.run)
|
|
66
|
+
failStartup(new Error(`deploy stopped: ${msg}`));
|
|
67
|
+
console.error(`[fastagent] warn: ${msg} — it will NOT fire on this deployment`);
|
|
68
|
+
}
|
|
69
|
+
// Host capability limit, stated at plan time. GitHub's own webhook contract is 25 MiB, but a
|
|
70
|
+
// Lambda Function URL request caps at 6 MB — so on this host a large payload cannot arrive at
|
|
71
|
+
// all. Better a sentence here than an opaque 502 the first time someone pushes a big diff.
|
|
72
|
+
if (channels.some((channel) => channel.name === "github")) {
|
|
73
|
+
console.error(`[fastagent] note: on AgentCore a webhook body is capped at ~${Math.round(MAX_WEBHOOK_BODY_BYTES / (1 << 20))} MiB ` +
|
|
74
|
+
`(the forwarder's Function URL limit); the GitHub channel accepts 25 MiB on a resident host, so the largest ` +
|
|
75
|
+
`payloads are rejected here rather than delivered`);
|
|
76
|
+
}
|
|
77
|
+
// The template IS the topology (EventBridge rules, wake wiring, secrets) — a kept generated
|
|
78
|
+
// template that no longer matches the definition would deploy a stack silently missing the
|
|
79
|
+
// difference (a new schedule with no rule never fires: the exact miss the gate above stops).
|
|
80
|
+
// A hand-written template (marker removed) is the operator's own — kept, never gated.
|
|
81
|
+
const templateArtifact = plan.artifacts.find((a) => a.path.endsWith(TEMPLATE_FILE));
|
|
82
|
+
const templateHome = join(workspace, templateArtifact?.path ?? TEMPLATE_FILE);
|
|
83
|
+
if (!opts.force && templateArtifact && (await exists(templateHome))) {
|
|
84
|
+
const existing = await readFile(templateHome, "utf8");
|
|
85
|
+
if (isGeneratedAgentcoreTemplate(existing) && existing !== templateArtifact.content) {
|
|
86
|
+
const msg = `${templateArtifact.path} no longer matches this definition (channels/schedules/selfSchedule changed) — ` +
|
|
87
|
+
`the kept template would silently drop the difference. Pass --force to regenerate (hand edits are lost), ` +
|
|
88
|
+
`or delete the file.`;
|
|
89
|
+
if (opts.run)
|
|
90
|
+
failStartup(new Error(`deploy stopped: ${msg}`));
|
|
91
|
+
console.error(`[fastagent] warn: ${msg}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
await write(plan.artifacts, {
|
|
95
|
+
force: !!opts.force,
|
|
96
|
+
alwaysWrite: [`${container.agentPrefix}${FORWARDER_FILE}`],
|
|
97
|
+
});
|
|
98
|
+
if (opts.run) {
|
|
99
|
+
return runDeployAgentcore({
|
|
100
|
+
agentDir,
|
|
101
|
+
workspace,
|
|
102
|
+
agentPrefix: container.agentPrefix,
|
|
103
|
+
name: acName,
|
|
104
|
+
modelAuth,
|
|
105
|
+
modelKeyInDefinition,
|
|
106
|
+
authPath,
|
|
107
|
+
channels,
|
|
108
|
+
extraSecrets,
|
|
109
|
+
topology: plan.topology,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
console.log(plan.runbook.join("\n"));
|
|
113
|
+
return;
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* `deploy agentcore --run`: drive aws + docker to completion. Mirrors the fly driver — same
|
|
118
|
+
* credential carry via {@link carryCredentials}, same runner seam (spawned `aws` + `docker`, cwd = the
|
|
119
|
+
* workspace so the build context is the agent). The AgentCore-specific sequence (identity → buildx →
|
|
120
|
+
* ECR → CloudFormation → outputs → webhooks) lives in {@link deployAgentcoreRun}; the params temp
|
|
121
|
+
* file (secret values off argv) is created here — 0600, removed after the run either way.
|
|
122
|
+
*/
|
|
123
|
+
async function runDeployAgentcore(params) {
|
|
124
|
+
const { agentDir, workspace, agentPrefix, name, channels, topology } = params;
|
|
125
|
+
const { secrets, missingSecrets, needsModelCredential } = await carryCredentials(params);
|
|
126
|
+
// The wake-alarm shared secret (container ↔ forwarder). Minted fresh each run — both sides receive
|
|
127
|
+
// the SAME parameter, so rotation is atomic; it never needs to be remembered locally.
|
|
128
|
+
if (topology.wakeAlarms)
|
|
129
|
+
secrets.FASTAGENT_WAKE_SECRET = crypto.randomUUID();
|
|
130
|
+
// The forwarder→runtime ingress secret: what makes an envelope the forwarder's rather than any IAM
|
|
131
|
+
// principal's. Minted fresh each run — both sides receive the SAME parameter, so rotation is atomic.
|
|
132
|
+
if (topology.forwarder)
|
|
133
|
+
secrets.FASTAGENT_INGRESS_SECRET = crypto.randomUUID();
|
|
134
|
+
gateOnModelCredential(needsModelCredential);
|
|
135
|
+
// The params temp dir holds the ONE file carrying secret values (file:// parameter-overrides —
|
|
136
|
+
// never argv); 0700/0600 and removed after the run, success or gate.
|
|
137
|
+
const paramsDir = await mkdtemp(join(tmpdir(), "fastagent-agentcore-"));
|
|
138
|
+
try {
|
|
139
|
+
const outcome = await deployAgentcoreRun({
|
|
140
|
+
name,
|
|
141
|
+
templatePath: `${agentPrefix}${TEMPLATE_FILE}`,
|
|
142
|
+
dockerfilePath: agentPrefix ? `${agentPrefix}Dockerfile` : undefined,
|
|
143
|
+
tag: new Date()
|
|
144
|
+
.toISOString()
|
|
145
|
+
.replace(/[-:.TZ]/g, "")
|
|
146
|
+
.slice(0, 14),
|
|
147
|
+
region: process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION,
|
|
148
|
+
secrets,
|
|
149
|
+
missingSecrets,
|
|
150
|
+
channels,
|
|
151
|
+
topology,
|
|
152
|
+
}, spawnRunner("aws", workspace), spawnRunner("docker", workspace), (m) => console.error(`[fastagent] ${m}`), async (content) => {
|
|
153
|
+
const path = join(paramsDir, "params.json");
|
|
154
|
+
await writeFile(path, content, { mode: 0o600 });
|
|
155
|
+
return path;
|
|
156
|
+
}, async (bytes) => {
|
|
157
|
+
const path = join(paramsDir, "forwarder.zip");
|
|
158
|
+
await writeFile(path, bytes);
|
|
159
|
+
return path;
|
|
160
|
+
}, registrarsFor(agentDir));
|
|
161
|
+
if (!outcome.ok)
|
|
162
|
+
failStartup(new Error(`deploy stopped: ${outcome.gate}`));
|
|
163
|
+
console.error(`[fastagent] deployed → ${outcome.runtimeArn}`);
|
|
164
|
+
if (outcome.url)
|
|
165
|
+
console.error(`[fastagent] webhook ingress → ${outcome.url}`);
|
|
166
|
+
const logsDir = shellArg(workspace);
|
|
167
|
+
console.error(`[fastagent] runtime logs → fastagent logs agentcore ${logsDir} --follow`);
|
|
168
|
+
if (topology.forwarder) {
|
|
169
|
+
console.error(`[fastagent] forwarder logs → fastagent logs agentcore ${logsDir} --source forwarder --follow`);
|
|
170
|
+
}
|
|
171
|
+
console.error(`[fastagent] invoke: aws bedrock-agentcore invoke-agent-runtime --agent-runtime-arn ${outcome.runtimeArn} \\\n` +
|
|
172
|
+
` --runtime-session-id "my-conversation-000000000000000000" \\\n` +
|
|
173
|
+
` --payload '{"kind":"invoke","session":"cli","text":"hello"}' --cli-binary-format raw-in-base64-out /dev/stdout`);
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
await rm(paramsDir, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `deploy docker`: one app service + loopback port + state volume, as a user-owned Compose file.
|
|
3
|
+
* `--tunnel` shapes the generated topology with an optional Quick Tunnel service; `--run` alone
|
|
4
|
+
* decides whether Docker receives side effects.
|
|
5
|
+
*/
|
|
6
|
+
import { basename, join } from "node:path";
|
|
7
|
+
import { webhookPaths } from "../../../deploy/channel-ingress.js";
|
|
8
|
+
import { composeHasTunnelService, isGeneratedCompose, planDockerDeploy, toDockerProjectName, } from "../../../deploy/docker/plan.js";
|
|
9
|
+
import { deployDockerRun } from "../../../deploy/docker/run.js";
|
|
10
|
+
import { spawnRunner } from "../../../deploy/runner.js";
|
|
11
|
+
import { openExternalUrl } from "../../../open-url.js";
|
|
12
|
+
import { readTextIfExists, resolveStateRoot } from "../../../paths.js";
|
|
13
|
+
import { announceWebhooks } from "../../../tunnel.js";
|
|
14
|
+
import { failStartup } from "../../fail.js";
|
|
15
|
+
import { carryCredentials } from "./shared.js";
|
|
16
|
+
export const dockerHost = {
|
|
17
|
+
isOurs: (path, content) => path.endsWith("fastagent.compose.yml") && isGeneratedCompose(content),
|
|
18
|
+
async deploy(ctx) {
|
|
19
|
+
const { opts, agentDir, workspace, channels, webhookChannels, pre, write } = ctx;
|
|
20
|
+
const { modelAuth, modelKeyInDefinition, authPath, container, port, extraSecrets } = pre;
|
|
21
|
+
const hasDeclaredChannels = channels.length > 0;
|
|
22
|
+
const projectName = toDockerProjectName(basename(workspace));
|
|
23
|
+
const dockerPlan = (tunnel) => planDockerDeploy({
|
|
24
|
+
projectName,
|
|
25
|
+
port,
|
|
26
|
+
modelAuth,
|
|
27
|
+
channels,
|
|
28
|
+
tunnel,
|
|
29
|
+
extraSecrets,
|
|
30
|
+
...container,
|
|
31
|
+
});
|
|
32
|
+
const requestedTunnel = !!opts.tunnel && (!hasDeclaredChannels || webhookChannels.length > 0);
|
|
33
|
+
if (opts.tunnel && hasDeclaredChannels && webhookChannels.length === 0) {
|
|
34
|
+
console.error(`[fastagent] note: --tunnel skipped — every channel uses a long connection`);
|
|
35
|
+
}
|
|
36
|
+
let plan = dockerPlan(requestedTunnel);
|
|
37
|
+
// An existing Compose file is authoritative: shape its comparison/runbook from the topology on disk,
|
|
38
|
+
// regardless of the current flag. `--force` is the explicit reset to the requested generated shape.
|
|
39
|
+
const composeFile = join(workspace, plan.composePath);
|
|
40
|
+
let keptWithoutRequestedTunnel = false;
|
|
41
|
+
// Same ownership question as fly.toml: a hand-owned compose file survives --force, so the plan must
|
|
42
|
+
// describe the topology that will actually be there.
|
|
43
|
+
const composeText = await readTextIfExists(composeFile).catch(failStartup);
|
|
44
|
+
if (composeText !== undefined && (!opts.force || !isGeneratedCompose(composeText))) {
|
|
45
|
+
const existingHasTunnel = composeHasTunnelService(composeText);
|
|
46
|
+
plan = dockerPlan(existingHasTunnel);
|
|
47
|
+
keptWithoutRequestedTunnel = requestedTunnel && !existingHasTunnel;
|
|
48
|
+
}
|
|
49
|
+
await write(plan.artifacts, { force: !!opts.force });
|
|
50
|
+
if (opts.run) {
|
|
51
|
+
return runDeployDocker({
|
|
52
|
+
agentDir,
|
|
53
|
+
workspace,
|
|
54
|
+
composeFile: plan.composePath,
|
|
55
|
+
port,
|
|
56
|
+
requireTunnel: requestedTunnel,
|
|
57
|
+
modelAuth,
|
|
58
|
+
modelKeyInDefinition,
|
|
59
|
+
authPath,
|
|
60
|
+
channels,
|
|
61
|
+
extraSecrets,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (keptWithoutRequestedTunnel) {
|
|
65
|
+
console.error(`[fastagent] warn: --tunnel was requested but kept ${plan.composePath} has no "tunnel" service — ` +
|
|
66
|
+
`edit it, delete it and regenerate, or pass --force`);
|
|
67
|
+
}
|
|
68
|
+
console.log(plan.runbook.join("\n"));
|
|
69
|
+
return;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* `deploy docker --run`: carry local credentials into Compose's child environment, then reconcile the
|
|
74
|
+
* user-owned local topology. Docker owns container/network/volume lifecycle. A Compose tunnel service,
|
|
75
|
+
* when present, yields an ephemeral URL that reuses the same webhook announcer as `dev --tunnel`.
|
|
76
|
+
*/
|
|
77
|
+
async function runDeployDocker(params) {
|
|
78
|
+
const { agentDir, workspace, composeFile, port, requireTunnel, channels } = params;
|
|
79
|
+
const { secrets, missingSecrets, needsModelCredential } = await carryCredentials(params);
|
|
80
|
+
const outcome = await deployDockerRun({
|
|
81
|
+
composeFile,
|
|
82
|
+
port,
|
|
83
|
+
secrets,
|
|
84
|
+
missingSecrets,
|
|
85
|
+
needsModelCredential,
|
|
86
|
+
requireTunnel,
|
|
87
|
+
announce: (tunnelUrl) => announceWebhooks(agentDir, tunnelUrl, channels, {
|
|
88
|
+
openUrl: openExternalUrl,
|
|
89
|
+
stateRoot: resolveStateRoot(agentDir),
|
|
90
|
+
}),
|
|
91
|
+
}, spawnRunner("docker", workspace), (message) => console.error(`[fastagent] ${message}`));
|
|
92
|
+
const compose = `docker compose -f ${composeFile}`;
|
|
93
|
+
// Compose reached "up" iff the driver could report where it is. The gates before that point
|
|
94
|
+
// (no Docker CLI, no daemon, missing secrets) must not be preceded by `docker compose logs`
|
|
95
|
+
// instructions for containers that do not exist; the health-check gate names its own logs command.
|
|
96
|
+
const isUp = outcome.ok || outcome.url !== undefined || outcome.tunnelUrl !== undefined;
|
|
97
|
+
if (outcome.url)
|
|
98
|
+
console.error(`[fastagent] running → ${outcome.url}`);
|
|
99
|
+
if (isUp) {
|
|
100
|
+
console.error(`[fastagent] logs: ${compose} logs -f agent`);
|
|
101
|
+
console.error(`[fastagent] stop: ${compose} down (state volume is kept)`);
|
|
102
|
+
}
|
|
103
|
+
// BEFORE failStartup: a registration gate says "re-run this deploy", and a re-run rebuilds the
|
|
104
|
+
// tunnel service — the operator needs to know the URL will be a different one, or they will read
|
|
105
|
+
// the retry as re-registering the same address.
|
|
106
|
+
if (outcome.tunnelUrl) {
|
|
107
|
+
console.error(`[fastagent] note: Quick Tunnel URLs are ephemeral — after the tunnel container/Docker daemon ` +
|
|
108
|
+
`restarts, re-run this deploy so webhooks receive the new URL`);
|
|
109
|
+
}
|
|
110
|
+
if (!outcome.ok)
|
|
111
|
+
failStartup(new Error(`deploy stopped: ${outcome.gate}`));
|
|
112
|
+
if (outcome.tunnelUrl)
|
|
113
|
+
return;
|
|
114
|
+
const paths = webhookPaths(channels);
|
|
115
|
+
if (paths.length > 0) {
|
|
116
|
+
console.error(`[fastagent] note: public ingress is operator-owned — configure your tunnel/proxy, then wire the ` +
|
|
117
|
+
`default webhook path(s): ${paths.join(", ")} (or your remapped channel routes)`);
|
|
118
|
+
}
|
|
119
|
+
}
|