@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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
9
|
import { detectRuntime } from "../runtime.js";
|
|
10
|
-
import { SECRETS_DIRNAME, assertInsideAgentDir, exists } from "../paths.js";
|
|
10
|
+
import { SECRETS_DIRNAME, assertInsideAgentDir, ensureSecretsDir, exists } from "../paths.js";
|
|
11
11
|
import { baseTemplate, channelBundleFiles, channelTemplate } from "./templates.js";
|
|
12
12
|
import { dotEnvPath, envExamplePath, parseEnvContent } from "../env.js";
|
|
13
13
|
const CHANNEL_SCAFFOLDS = {
|
|
@@ -44,28 +44,12 @@ const CHANNEL_SCAFFOLDS = {
|
|
|
44
44
|
},
|
|
45
45
|
slack: {
|
|
46
46
|
env: [
|
|
47
|
-
{ name: "SLACK_BOT_TOKEN", hint: "Slack app →
|
|
48
|
-
{
|
|
49
|
-
name: "SLACK_BOT_REFRESH_TOKEN",
|
|
50
|
-
hint: "Slack OAuth bot refresh token (required when token rotation is enabled)",
|
|
51
|
-
required: false,
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
name: "SLACK_BOT_TOKEN_EXPIRES_AT",
|
|
55
|
-
hint: "Slack rotating bot access-token expiry (epoch milliseconds)",
|
|
56
|
-
required: false,
|
|
57
|
-
},
|
|
58
|
-
{ name: "SLACK_CLIENT_ID", hint: "Slack app OAuth client ID (for bot-token rotation)", required: false },
|
|
59
|
-
{
|
|
60
|
-
name: "SLACK_CLIENT_SECRET",
|
|
61
|
-
hint: "Slack app OAuth client secret (for bot-token rotation)",
|
|
62
|
-
required: false,
|
|
63
|
-
},
|
|
47
|
+
{ name: "SLACK_BOT_TOKEN", hint: "Slack app → OAuth & Permissions → Bot User OAuth Token", required: true },
|
|
64
48
|
{ name: "SLACK_SIGNING_SECRET", hint: "Slack app → Basic Information → App Credentials", required: true },
|
|
65
49
|
],
|
|
66
50
|
steps: [
|
|
67
51
|
"Slack Bot Token Scopes: app_mentions:read, assistant:write, chat:write, im:history, files:read, files:write, channels:history, groups:history, mpim:history",
|
|
68
|
-
"enable Agents (agent_view) and token rotation; subscribe app_home_opened, app_context_changed, app_mention, message.im, message.channels, message.groups, message.mpim; set Request URL to <public-url>/slack",
|
|
52
|
+
"enable Agents (agent_view) and leave token rotation OFF (it cannot be turned off again); subscribe app_home_opened, app_context_changed, app_mention, message.im, message.channels, message.groups, message.mpim; set Request URL to <public-url>/slack",
|
|
69
53
|
"reinstall the app after changing scopes, then invite it to each channel it should read",
|
|
70
54
|
"the agent can send messages or files by calling the scaffolded {tools}/slack-send.ts tool",
|
|
71
55
|
],
|
|
@@ -172,7 +156,7 @@ export function channelSetup(kind, ingress = "webhook", groupBehavior) {
|
|
|
172
156
|
env: setup.env,
|
|
173
157
|
steps: [
|
|
174
158
|
"Slack Bot Token Scopes: app_mentions:read, assistant:write, chat:write, im:history, files:read, files:write (no channel/group/mpim history scopes)",
|
|
175
|
-
"enable Agents (agent_view) and token rotation; subscribe app_home_opened, app_context_changed, app_mention, and message.im; set Request URL to <public-url>/slack",
|
|
159
|
+
"enable Agents (agent_view) and leave token rotation OFF (it cannot be turned off again); subscribe app_home_opened, app_context_changed, app_mention, and message.im; set Request URL to <public-url>/slack",
|
|
176
160
|
"group behavior: mention-only — bare thread replies and unsummoned group context remain disabled",
|
|
177
161
|
...setup.steps.slice(2),
|
|
178
162
|
],
|
|
@@ -229,7 +213,7 @@ function mentionsEnvName(content, name) {
|
|
|
229
213
|
export async function appendChannelDotEnv(dir, kind, generated, overwrite = [], ingress = "webhook") {
|
|
230
214
|
const file = dotEnvPath(dir);
|
|
231
215
|
const secretsDir = dirname(file);
|
|
232
|
-
await
|
|
216
|
+
await ensureSecretsDir(secretsDir);
|
|
233
217
|
// THE one exception to "fastagent has no opinion about git": the directory it writes secrets into
|
|
234
218
|
// carries its own `.gitignore`. `init` writes it, and so does this — the reachable case where it is
|
|
235
219
|
// missing (a hand-made agent) is exactly the one where the next line mints an unrecoverable app
|
|
@@ -340,55 +324,54 @@ export async function scaffoldChannel(dir, kind, options = {}) {
|
|
|
340
324
|
throw new Error(`${file} already exists — edit it, or remove it to re-scaffold`);
|
|
341
325
|
}
|
|
342
326
|
await mkdir(channelsDir, { recursive: true });
|
|
343
|
-
|
|
344
|
-
|
|
327
|
+
let content = channelTemplate(kind, "channel.ts");
|
|
328
|
+
if ((kind === "feishu" || kind === "lark") && options.ingress === "websocket") {
|
|
329
|
+
const factory = `${kind}Channel`;
|
|
330
|
+
const wsFactory = `${kind}WebSocketChannel`;
|
|
331
|
+
let configured = content
|
|
332
|
+
.replace(`import { ${factory} }`, `import { ${wsFactory} }`)
|
|
333
|
+
.replace(`export default ${factory}({`, `export default ${wsFactory}({`);
|
|
334
|
+
if (configured === content)
|
|
335
|
+
throw new Error(`${kind} channel template has no factory anchors`);
|
|
336
|
+
const prefix = kind === "feishu" ? "FEISHU" : "LARK";
|
|
337
|
+
const exportAt = configured.indexOf("export default");
|
|
338
|
+
const importEnd = configured.indexOf("\n\n");
|
|
339
|
+
if (exportAt < 0 || importEnd < 0)
|
|
340
|
+
throw new Error(`${kind} channel template header anchors are missing`);
|
|
341
|
+
const brand = kind === "feishu" ? "Feishu" : "Lark";
|
|
342
|
+
configured =
|
|
343
|
+
`${configured.slice(0, importEnd)}\n\n` +
|
|
344
|
+
`// ${brand} WebSocket long connection: the process connects OUT to the platform, so no public URL,\n` +
|
|
345
|
+
`// Verification Token, Encrypt Key, or --tunnel is needed. In Events & Callbacks choose long\n` +
|
|
346
|
+
`// connection, subscribe im.message.receive_v1, then publish the app version. Keep one process\n` +
|
|
347
|
+
`// running in production: scale-to-zero/App Sleeping would disconnect ingress.\n` +
|
|
348
|
+
configured.slice(exportAt);
|
|
349
|
+
configured = configured
|
|
350
|
+
.split("\n")
|
|
351
|
+
.filter((line) => !line.includes(`verificationToken: process.env.${prefix}_VERIFICATION_TOKEN`) &&
|
|
352
|
+
!line.includes(`encryptKey: process.env.${prefix}_ENCRYPT_KEY`))
|
|
353
|
+
.join("\n");
|
|
354
|
+
content = configured;
|
|
355
|
+
}
|
|
356
|
+
await writeFile(file, content, { flag: "wx" });
|
|
357
|
+
return file;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* The bundle's companion tools (every `.ts` beside `channel.ts` → `tools/<name>`). Unlike the channel
|
|
361
|
+
* file they are the package's, not authored glue, so they are written on EVERY add: re-running
|
|
362
|
+
* `add <kind>` is how an upgraded tool reaches an existing agent.
|
|
363
|
+
*/
|
|
364
|
+
export async function scaffoldCompanionTools(dir, kind) {
|
|
365
|
+
const written = [];
|
|
345
366
|
for (const name of channelBundleFiles(kind)) {
|
|
346
|
-
|
|
347
|
-
if (name === "channel.ts") {
|
|
348
|
-
if ((kind === "feishu" || kind === "lark") && options.ingress === "websocket") {
|
|
349
|
-
const factory = `${kind}Channel`;
|
|
350
|
-
const wsFactory = `${kind}WebSocketChannel`;
|
|
351
|
-
let configured = content
|
|
352
|
-
.replace(`import { ${factory} }`, `import { ${wsFactory} }`)
|
|
353
|
-
.replace(`export default ${factory}({`, `export default ${wsFactory}({`);
|
|
354
|
-
if (configured === content)
|
|
355
|
-
throw new Error(`${kind} channel template has no factory anchors`);
|
|
356
|
-
const prefix = kind === "feishu" ? "FEISHU" : "LARK";
|
|
357
|
-
const exportAt = configured.indexOf("export default");
|
|
358
|
-
const importEnd = configured.indexOf("\n\n");
|
|
359
|
-
if (exportAt < 0 || importEnd < 0)
|
|
360
|
-
throw new Error(`${kind} channel template header anchors are missing`);
|
|
361
|
-
const brand = kind === "feishu" ? "Feishu" : "Lark";
|
|
362
|
-
configured =
|
|
363
|
-
`${configured.slice(0, importEnd)}\n\n` +
|
|
364
|
-
`// ${brand} WebSocket long connection: the process connects OUT to the platform, so no public URL,\n` +
|
|
365
|
-
`// Verification Token, Encrypt Key, or --tunnel is needed. In Events & Callbacks choose long\n` +
|
|
366
|
-
`// connection, subscribe im.message.receive_v1, then publish the app version. Keep one process\n` +
|
|
367
|
-
`// running in production: scale-to-zero/App Sleeping would disconnect ingress.\n` +
|
|
368
|
-
configured.slice(exportAt);
|
|
369
|
-
configured = configured
|
|
370
|
-
.split("\n")
|
|
371
|
-
.filter((line) => !line.includes(`verificationToken: process.env.${prefix}_VERIFICATION_TOKEN`) &&
|
|
372
|
-
!line.includes(`encryptKey: process.env.${prefix}_ENCRYPT_KEY`))
|
|
373
|
-
.join("\n");
|
|
374
|
-
content = configured;
|
|
375
|
-
}
|
|
376
|
-
if (kind === "slack" && options.groupBehavior === "mentions") {
|
|
377
|
-
const configured = content.replace('groupBehavior: "context"', 'groupBehavior: "mentions"');
|
|
378
|
-
if (configured === content)
|
|
379
|
-
throw new Error("slack channel template has no groupBehavior anchor");
|
|
380
|
-
content = configured;
|
|
381
|
-
}
|
|
382
|
-
await writeFile(file, content, { flag: "wx" });
|
|
367
|
+
if (name === "channel.ts")
|
|
383
368
|
continue;
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
await writeFile(toolFile, content, { flag: "wx" });
|
|
389
|
-
}
|
|
369
|
+
const file = join(dir, "tools", name);
|
|
370
|
+
await mkdir(dirname(file), { recursive: true });
|
|
371
|
+
await writeFile(file, channelTemplate(kind, name));
|
|
372
|
+
written.push(file);
|
|
390
373
|
}
|
|
391
|
-
return
|
|
374
|
+
return written;
|
|
392
375
|
}
|
|
393
376
|
/**
|
|
394
377
|
* Verify the AGENT DIR is ready to host a channel: an ESM package.json that declares
|
package/dist/scaffold/init.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { lstat, mkdir, readdir, rm, rmdir, writeFile } from "node:fs/promises";
|
|
29
29
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
30
|
-
import { AGENT_CONFIG_NAMES, DEFAULT_AGENT_DIRNAME, SECRETS_DIRNAME, agentDefinitionOwner, agentsAt, displayPath, exists, } from "../paths.js";
|
|
30
|
+
import { AGENT_CONFIG_NAMES, DEFAULT_AGENT_DIRNAME, SECRETS_DIRNAME, agentDefinitionOwner, agentsAt, displayPath, ensureSecretsDir, exists, } from "../paths.js";
|
|
31
31
|
import { baseTemplate, packageJson, toPackageName } from "./templates.js";
|
|
32
32
|
import { fastagentVersion } from "../version.js";
|
|
33
33
|
/** The agent directory name for a raw `--agent-dir` value: the default when unset, and `./bot` read as
|
|
@@ -191,7 +191,12 @@ export async function scaffoldAgent(dir, options = {}) {
|
|
|
191
191
|
try {
|
|
192
192
|
for (const file of files) {
|
|
193
193
|
const abs = join(dir, file.rel);
|
|
194
|
-
|
|
194
|
+
// The secrets dir carries a mode; every other directory here is ordinary. `init` is usually the
|
|
195
|
+
// FIRST of the four writers to create it, and whoever creates it decides the mode for the rest.
|
|
196
|
+
if (basename(dirname(abs)) === SECRETS_DIRNAME)
|
|
197
|
+
await ensureSecretsDir(dirname(abs));
|
|
198
|
+
else
|
|
199
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
195
200
|
try {
|
|
196
201
|
await writeFile(abs, file.content, { flag: "wx" });
|
|
197
202
|
created.push(file.rel);
|
|
@@ -3,8 +3,6 @@ import { defineTool, z } from "@fastagent-sh/fastagent";
|
|
|
3
3
|
// A code tool: filename (fetch-url.ts) is the tool name. tools/ is auto-discovered,
|
|
4
4
|
// so it needs no registration in fastagent.config. Test it without a model:
|
|
5
5
|
// fastagent tool fetch-url '{"url":"https://example.com"}'
|
|
6
|
-
// Serving this behind a public channel? The URL then comes from untrusted users — add an
|
|
7
|
-
// allowlist or block private-network addresses (localhost, 169.254.169.254, …) to prevent SSRF.
|
|
8
6
|
const MAX_TEXT = 20_000; // keep a huge page from flooding the model's context
|
|
9
7
|
|
|
10
8
|
export default defineTool({
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
* named from its filename. This is the FILE producer of scheduled invocations (the author's, declarative,
|
|
5
5
|
* git-tracked, deploy-guaranteed); the agent's `wake` tool is the second producer.
|
|
6
6
|
*/
|
|
7
|
-
import { readdir } from "node:fs/promises";
|
|
8
7
|
import { join } from "node:path";
|
|
9
|
-
import {
|
|
8
|
+
import { loadModuleDir, moduleInventory } from "../loader.js";
|
|
10
9
|
import { assertInsideAgentDir } from "../paths.js";
|
|
11
10
|
import { cronError } from "./cron.js";
|
|
12
11
|
/** Schedule file basenames under `<dir>/schedules/` — an existence probe listed WITHOUT importing
|
|
@@ -14,19 +13,8 @@ import { cronError } from "./cron.js";
|
|
|
14
13
|
* since it also reports broken files and next instants). */
|
|
15
14
|
export async function discoverScheduleFiles(dir) {
|
|
16
15
|
await assertInsideAgentDir(dir, "schedules");
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
names = await readdir(join(dir, "schedules"));
|
|
20
|
-
}
|
|
21
|
-
catch (error) {
|
|
22
|
-
if (error.code === "ENOENT")
|
|
23
|
-
return [];
|
|
24
|
-
throw error;
|
|
25
|
-
}
|
|
26
|
-
return names
|
|
27
|
-
.filter(isModuleFile)
|
|
28
|
-
.map((n) => n.replace(/\.(ts|js|mjs)$/, ""))
|
|
29
|
-
.sort();
|
|
16
|
+
const entries = await moduleInventory(join(dir, "schedules"));
|
|
17
|
+
return entries.map((entry) => entry.name);
|
|
30
18
|
}
|
|
31
19
|
/**
|
|
32
20
|
* Discover schedules in `<dir>/schedules/`: each file default-exports a `defineSchedule({...})`, named
|
|
@@ -1,16 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
export declare const WAKE_ALARM_PATH = "/__fastagent/wake-alarm";
|
|
4
|
-
/** One desired alarm: mirror of a pending wake-up (id names the EventBridge schedule; at = fireAt). */
|
|
5
|
-
export interface WakeAlarm {
|
|
6
|
-
id: string;
|
|
7
|
-
at: string;
|
|
8
|
-
}
|
|
9
|
-
/** The wire shape the sink POSTs to {@link WAKE_ALARM_PATH} (the forwarder validates `secret`). */
|
|
10
|
-
export interface WakeAlarmRequest {
|
|
11
|
-
secret: string;
|
|
12
|
-
alarms: WakeAlarm[];
|
|
13
|
-
}
|
|
1
|
+
import { type WakeAlarm } from "../channels/agentcore-protocol.ts";
|
|
2
|
+
import { type Wakeup } from "./wakeups.ts";
|
|
14
3
|
/**
|
|
15
4
|
* Persist the forwarder URL the adapter saw in an envelope (write-if-changed — envelopes arrive on
|
|
16
5
|
* every turn, the file should not churn). Durable under <stateRoot>/schedule/ so a freshly booted
|
|
@@ -31,6 +20,17 @@ export declare function toAlarms(pending: Wakeup[], now: Date): WakeAlarm[];
|
|
|
31
20
|
* when `FASTAGENT_AGENTCORE=1` + `FASTAGENT_WAKE_SECRET` are present). Fire-and-forget by contract:
|
|
32
21
|
* failures are logged, never thrown — a broken alarm degrades to the pre-alarm behavior (the wake
|
|
33
22
|
* still fires on the next time the box happens to be awake), it must never break the store write.
|
|
23
|
+
*
|
|
24
|
+
* A single-flight RECONCILER, not a per-mutation delivery: a notification only marks the desired
|
|
25
|
+
* state dirty, and the loop re-derives it from the store before every attempt. So a mutation during
|
|
26
|
+
* a retry needs no ordering rule — there is one desired state, never two snapshots to rank — and the
|
|
27
|
+
* {@link DUE_MARGIN_MS} filter is re-applied with a fresh clock, which a captured payload could not
|
|
28
|
+
* do: entries that became due mid-retry would keep being POSTed with a past `at()`, a rejection no
|
|
29
|
+
* number of retries could clear.
|
|
30
|
+
*
|
|
31
|
+
* Assumes ONE sink per process over ONE state root (what `start` builds): the loop keeps the root of
|
|
32
|
+
* the call that started it, and its dirty flag is shared, so a second root would fold into the first
|
|
33
|
+
* one's pass.
|
|
34
34
|
*/
|
|
35
35
|
export declare function createWakeAlarmSink(options: {
|
|
36
36
|
secret: string;
|
|
@@ -39,9 +39,4 @@ export declare function createWakeAlarmSink(options: {
|
|
|
39
39
|
now?: () => Date;
|
|
40
40
|
/** Injectable retry pause (tests); defaults to exponential-ish backoff off RETRY_BASE_MS. */
|
|
41
41
|
delay?: (ms: number) => Promise<void>;
|
|
42
|
-
}): (stateRoot: string
|
|
43
|
-
/**
|
|
44
|
-
* One boot-time reconcile: pending wake-ups may exist while their alarms were lost (a deploy
|
|
45
|
-
* replaced the forwarder, a sink call failed) — re-mirror the current set once at start.
|
|
46
|
-
*/
|
|
47
|
-
export declare function reconcileWakeAlarms(stateRoot: string, sink: (stateRoot: string, pending: Wakeup[]) => void): void;
|
|
42
|
+
}): (stateRoot: string) => void;
|
|
@@ -17,17 +17,17 @@
|
|
|
17
17
|
* Function URL at cold start), and the adapter persists it here; every wake write happens inside a
|
|
18
18
|
* turn, and every ingress turn arrived through an envelope, so the URL is always known by then.
|
|
19
19
|
*
|
|
20
|
-
* Reconciliation is DECLARATIVE (the
|
|
20
|
+
* Reconciliation is DECLARATIVE (the sink is told only THAT the store changed and reads the full
|
|
21
|
+
* pending set back per attempt, so no retry ever carries a stale view) and deletion is lazy: a
|
|
21
22
|
* cancelled wake-up's alarm still fires its poke, finds nothing due, and self-deletes — a harmless
|
|
22
23
|
* wasted wake-up of the box, traded for never needing list/delete choreography.
|
|
23
24
|
*/
|
|
24
25
|
import { readFileSync } from "node:fs";
|
|
26
|
+
import { RESERVED_PATHS } from "../channels/agentcore-protocol.js";
|
|
25
27
|
import { beginWork } from "../channels/busy.js";
|
|
26
28
|
import { log } from "../log.js";
|
|
27
29
|
import { scheduleFile, writeScheduleFile } from "./state.js";
|
|
28
30
|
import { listWakeups } from "./wakeups.js";
|
|
29
|
-
/** The forwarder's reserved wake-alarm path — never forwarded to channel routes. */
|
|
30
|
-
export const WAKE_ALARM_PATH = "/__fastagent/wake-alarm";
|
|
31
31
|
const URL_FILE = "wake-alarm-url";
|
|
32
32
|
/**
|
|
33
33
|
* Persist the forwarder URL the adapter saw in an envelope (write-if-changed — envelopes arrive on
|
|
@@ -71,66 +71,107 @@ export function toAlarms(pending, now) {
|
|
|
71
71
|
* when `FASTAGENT_AGENTCORE=1` + `FASTAGENT_WAKE_SECRET` are present). Fire-and-forget by contract:
|
|
72
72
|
* failures are logged, never thrown — a broken alarm degrades to the pre-alarm behavior (the wake
|
|
73
73
|
* still fires on the next time the box happens to be awake), it must never break the store write.
|
|
74
|
+
*
|
|
75
|
+
* A single-flight RECONCILER, not a per-mutation delivery: a notification only marks the desired
|
|
76
|
+
* state dirty, and the loop re-derives it from the store before every attempt. So a mutation during
|
|
77
|
+
* a retry needs no ordering rule — there is one desired state, never two snapshots to rank — and the
|
|
78
|
+
* {@link DUE_MARGIN_MS} filter is re-applied with a fresh clock, which a captured payload could not
|
|
79
|
+
* do: entries that became due mid-retry would keep being POSTed with a past `at()`, a rejection no
|
|
80
|
+
* number of retries could clear.
|
|
81
|
+
*
|
|
82
|
+
* Assumes ONE sink per process over ONE state root (what `start` builds): the loop keeps the root of
|
|
83
|
+
* the call that started it, and its dirty flag is shared, so a second root would fold into the first
|
|
84
|
+
* one's pass.
|
|
74
85
|
*/
|
|
75
86
|
export function createWakeAlarmSink(options) {
|
|
76
87
|
const { secret, fetchImpl = fetch, now = () => new Date() } = options;
|
|
77
88
|
const delay = options.delay ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
89
|
+
let running = false;
|
|
90
|
+
let dirty = false;
|
|
91
|
+
/** One POST of the CURRENT desired set. Returns false to retry, true when there is nothing left
|
|
92
|
+
* to do (converged, or nothing this loop can act on). */
|
|
93
|
+
async function attemptOnce(stateRoot, attempt) {
|
|
94
|
+
const alarms = toAlarms(listWakeups(stateRoot), now());
|
|
95
|
+
// Nothing future to mirror: converged. Deletion is lazy by design — alarms already mirrored for
|
|
96
|
+
// cancelled wake-ups fire, find nothing, self-delete — so an empty set is never POSTed.
|
|
97
|
+
if (alarms.length === 0)
|
|
98
|
+
return true;
|
|
99
|
+
const url = readWakeAlarmUrl(stateRoot);
|
|
100
|
+
if (!url) {
|
|
101
|
+
// Before the first envelope, or an unreadable state mount (readWakeAlarmUrl folds both into
|
|
102
|
+
// undefined). The wake itself is stored; the next store mutation or boot re-mirrors it.
|
|
103
|
+
log.warn("[schedule] wake alarm skipped — forwarder URL unavailable (not seen yet, or unreadable)");
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
const body = { secret, alarms };
|
|
107
|
+
try {
|
|
108
|
+
const res = await fetchImpl(`${url.replace(/\/$/, "")}${RESERVED_PATHS.wakeAlarm}`, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { "content-type": "application/json" },
|
|
111
|
+
body: JSON.stringify(body),
|
|
112
|
+
signal: AbortSignal.timeout(SYNC_TIMEOUT_MS),
|
|
113
|
+
});
|
|
114
|
+
if (res.ok)
|
|
115
|
+
return true;
|
|
116
|
+
log.warn(`[schedule] wake alarm sync attempt ${attempt}/${MAX_SYNC_ATTEMPTS} failed: HTTP ${res.status}`);
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
log.warn(`[schedule] wake alarm sync attempt ${attempt}/${MAX_SYNC_ATTEMPTS} failed: ${String(e)}`);
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
async function reconcile(stateRoot) {
|
|
82
124
|
// Counted as in-flight work: the retry loop is exactly the window where the box must not be
|
|
83
125
|
// reclaimed — idle away mid-retry and a pending wake has no alarm until the next boot.
|
|
84
126
|
const workDone = beginWork();
|
|
127
|
+
// Consecutive failures ACROSS passes, not within one. A mid-retry mutation restarts the attempt
|
|
128
|
+
// sequence (the backoff and the log's `n/N` are about the new desired set), but it must not renew
|
|
129
|
+
// the budget: a store mutating faster than the backoff would keep the loop alive forever, and the
|
|
130
|
+
// one loud line saying the forwarder is down — the per-attempt warns are filterable — would never
|
|
131
|
+
// be reached. This counter is the thing that survives to reach it.
|
|
132
|
+
let failures = 0;
|
|
85
133
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
134
|
+
while (dirty && failures < MAX_SYNC_ATTEMPTS) {
|
|
135
|
+
dirty = false;
|
|
136
|
+
for (let attempt = 1; attempt <= MAX_SYNC_ATTEMPTS; attempt++) {
|
|
137
|
+
if (await attemptOnce(stateRoot, attempt)) {
|
|
138
|
+
failures = 0;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
failures++;
|
|
142
|
+
if (attempt === MAX_SYNC_ATTEMPTS || failures >= MAX_SYNC_ATTEMPTS)
|
|
143
|
+
break;
|
|
144
|
+
await delay(RETRY_BASE_MS * attempt);
|
|
145
|
+
// A mutation landed mid-retry: the desired state moved, so this budget is spent on a set
|
|
146
|
+
// that no longer exists. Restart the attempt count against the new one.
|
|
147
|
+
if (dirty)
|
|
148
|
+
break;
|
|
99
149
|
}
|
|
100
|
-
catch (e) {
|
|
101
|
-
log.warn(`[schedule] wake alarm sync attempt ${attempt}/${MAX_SYNC_ATTEMPTS} failed: ${String(e)}`);
|
|
102
|
-
}
|
|
103
|
-
await delay(RETRY_BASE_MS * attempt);
|
|
104
150
|
}
|
|
105
|
-
|
|
106
|
-
`
|
|
151
|
+
if (failures >= MAX_SYNC_ATTEMPTS) {
|
|
152
|
+
log.error(`[schedule] wake alarm sync FAILED after ${MAX_SYNC_ATTEMPTS} attempts — pending wake-ups have no ` +
|
|
153
|
+
`external alarm until the next store change or boot re-mirrors them`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
// The END of the error path: nothing awaits this loop, so an escape is an unhandled rejection
|
|
158
|
+
// that kills the container. `listWakeups` throws by design on an unreadable store (state.ts),
|
|
159
|
+
// exactly the fault a wake alarm cannot fix — report it and leave the store write untouched,
|
|
160
|
+
// which is this sink's whole contract. The next mutation or boot re-runs the mirror.
|
|
161
|
+
log.error(`[schedule] wake alarm reconcile failed (alarms are stale until the next store change): ${String(e)}`);
|
|
107
162
|
}
|
|
108
163
|
finally {
|
|
164
|
+
running = false;
|
|
109
165
|
workDone();
|
|
110
166
|
}
|
|
111
167
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
log.warn("[schedule] wake alarm skipped — forwarder URL not seen yet (it arrives with the first envelope)");
|
|
168
|
+
// Single-flight: a save arriving while the loop runs only marks it dirty, so a burst coalesces
|
|
169
|
+
// into one more pass instead of one concurrent loop each.
|
|
170
|
+
return (stateRoot) => {
|
|
171
|
+
dirty = true;
|
|
172
|
+
if (running)
|
|
118
173
|
return;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (alarms.length === 0)
|
|
122
|
-
return; // nothing future to mirror (deletion is lazy by design)
|
|
123
|
-
const token = Symbol("wake-alarm-sync");
|
|
124
|
-
latest = token;
|
|
125
|
-
void sync(url, { secret, alarms }, token);
|
|
174
|
+
running = true;
|
|
175
|
+
void reconcile(stateRoot);
|
|
126
176
|
};
|
|
127
177
|
}
|
|
128
|
-
/**
|
|
129
|
-
* One boot-time reconcile: pending wake-ups may exist while their alarms were lost (a deploy
|
|
130
|
-
* replaced the forwarder, a sink call failed) — re-mirror the current set once at start.
|
|
131
|
-
*/
|
|
132
|
-
export function reconcileWakeAlarms(stateRoot, sink) {
|
|
133
|
-
const pending = listWakeups(stateRoot);
|
|
134
|
-
if (pending.length > 0)
|
|
135
|
-
sink(stateRoot, pending);
|
|
136
|
-
}
|
|
@@ -26,7 +26,7 @@ export declare const MAX_PENDING_WAKEUPS = 20;
|
|
|
26
26
|
* wake into an active conversation fires in the first gap between the user's turns; this generous
|
|
27
27
|
* ceiling (~1h) only gives up on a pathologically stuck session (then logs, operator-visible). */
|
|
28
28
|
export declare const MAX_WAKE_ATTEMPTS = 120;
|
|
29
|
-
export declare function setWakeupsSink(sink: ((stateRoot: string
|
|
29
|
+
export declare function setWakeupsSink(sink: ((stateRoot: string) => void) | undefined): void;
|
|
30
30
|
/** The current pending wake-ups (`fastagent schedule list` uses this). */
|
|
31
31
|
export declare function listWakeups(stateRoot: string): Wakeup[];
|
|
32
32
|
export type AddWakeupResult = {
|
package/dist/schedule/wakeups.js
CHANGED
|
@@ -48,12 +48,15 @@ function isWakeup(e) {
|
|
|
48
48
|
(typeof w.cron === "string" && cronError(w.cron, typeof w.tz === "string" ? w.tz : undefined) === undefined)));
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
* The wake-ALARM sink: notified after EVERY wakeups-store mutation
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
51
|
+
* The wake-ALARM sink: notified after EVERY wakeups-store mutation. A notification, not a delivery
|
|
52
|
+
* — it carries no set, because the sink reconciles by re-reading the store, and handing it a set
|
|
53
|
+
* captured here would go stale in its retries. The AgentCore deployment registers one
|
|
54
|
+
* (schedule/wake-alarm.ts) that mirrors pending wake-ups into one-shot EventBridge schedules — the
|
|
55
|
+
* external clock that makes `wake` reliable on a host with no resident process. Neutral seam: this
|
|
56
|
+
* module knows only "someone wants to observe changes"; a resident host registers nothing and
|
|
57
|
+
* behaves exactly as before. The sink is fire-and-forget and must own its errors. The guard below
|
|
58
|
+
* catches a SYNCHRONOUS throw so a broken alarm never corrupts a store write; an async sink returns
|
|
59
|
+
* before it does any work, so its rejections are ITS to terminate — nothing here can see them.
|
|
57
60
|
*/
|
|
58
61
|
let wakeupsSink;
|
|
59
62
|
export function setWakeupsSink(sink) {
|
|
@@ -81,7 +84,7 @@ function load(stateRoot) {
|
|
|
81
84
|
function save(stateRoot, wakeups) {
|
|
82
85
|
writeScheduleFile(scheduleFile(stateRoot, "wakeups"), wakeups);
|
|
83
86
|
try {
|
|
84
|
-
wakeupsSink?.(stateRoot
|
|
87
|
+
wakeupsSink?.(stateRoot);
|
|
85
88
|
}
|
|
86
89
|
catch (e) {
|
|
87
90
|
log.error(`[schedule] wake-alarm sink failed (store write is unaffected): ${String(e)}`);
|
package/dist/service.d.ts
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The product, as one call: an agent directory becomes a live service.
|
|
3
|
+
*
|
|
4
|
+
* That phrase is the promise on the README, and until this existed only the CLI could keep it. The
|
|
5
|
+
* assembly parts live here too — `routesFor`, `mountSessionControl`, `startSchedules` — because a
|
|
6
|
+
* public entry may not reach into `cli/`: that directory decides process-level things (`fail.ts`
|
|
7
|
+
* calls `process.exit`) which a library mounted inside someone's app does not get to decide.
|
|
8
|
+
* Everything else was parts: assemble the agent, discover channels, mount the control plane, start
|
|
9
|
+
* schedules, open long connections, compose a router. An embedder had to know that list and get its
|
|
10
|
+
* order right, and getting it wrong is silent: a plane that 404s while advertising itself, a
|
|
11
|
+
* schedule that never fires.
|
|
12
|
+
*
|
|
13
|
+
* So the assembly lives here, and `dev`/`start` are callers. AgentCore is the one exception, and a
|
|
14
|
+
* substantive one: its channels load lazily after a state-snapshot restore, so it cannot use an
|
|
15
|
+
* assembly that discovers them eagerly (cli/commands/start.ts says so at the branch).
|
|
16
|
+
*/
|
|
1
17
|
import type { Agent } from "./agent.ts";
|
|
18
|
+
import { type PrefixMount } from "./channels/serve.ts";
|
|
2
19
|
import { type LoadedLongConnectionChannel } from "./channels/discover.ts";
|
|
3
20
|
import type { SessionControl } from "./session.ts";
|
|
4
21
|
import type { ChannelHandler, Routes } from "./channel.ts";
|
|
5
|
-
import { type PrefixMount } from "./channels/serve.ts";
|
|
6
22
|
import type { LoadedSchedule } from "./schedule/schedule.ts";
|
|
7
23
|
export interface ServingSurface {
|
|
8
24
|
routes: Routes;
|
|
@@ -12,7 +28,6 @@ export interface ServingSurface {
|
|
|
12
28
|
/** Route-channel basenames; the tunnel registers only this subset. */
|
|
13
29
|
routeChannels: string[];
|
|
14
30
|
builtinInvoke: boolean;
|
|
15
|
-
/** Marks the built-in health route ready after every long-connection channel first connects. */
|
|
16
31
|
/** Flip health between 200 and 503. Two-way on purpose: a long connection that dies after coming
|
|
17
32
|
* up leaves the surface serving something it no longer has, and a load balancer should hear it. */
|
|
18
33
|
setReady(value: boolean): void;
|
|
@@ -36,20 +51,17 @@ export declare function routesFor(agentDir: string, agent: Agent, stateRoot: str
|
|
|
36
51
|
* load after this ran against an empty base.
|
|
37
52
|
*/
|
|
38
53
|
export declare function assertNoControlPlaneCollision(channelRoutes: Routes, plane: PrefixMount): void;
|
|
39
|
-
export declare function mountSessionControl(routes: Routes, control: SessionControl | undefined,
|
|
40
|
-
tunnel?: boolean;
|
|
54
|
+
export declare function mountSessionControl(routes: Routes, control: SessionControl | undefined, options?: {
|
|
41
55
|
agent?: Agent;
|
|
42
|
-
host?: string;
|
|
43
56
|
}): {
|
|
44
57
|
routes: Routes;
|
|
45
58
|
mounts: PrefixMount[];
|
|
46
|
-
/** The plane's bearer token and prefix — how
|
|
59
|
+
/** The plane's bearer token and prefix — how a caller distributes access (the CLI writes it to
|
|
60
|
+
* `<stateRoot>/control.json` for local discovery; an embedder hands it out itself). */
|
|
47
61
|
control?: {
|
|
48
62
|
token: string;
|
|
49
63
|
prefix: string;
|
|
50
64
|
};
|
|
51
|
-
/** Write the local discovery file; returns its removal. Installs no signal handlers. */
|
|
52
|
-
announce: (boundPort: number) => () => void;
|
|
53
65
|
};
|
|
54
66
|
/**
|
|
55
67
|
* Load and start the agent's `schedules/` — a time-trigger firing the agent on each cron. Starts iff
|
|
@@ -89,20 +101,14 @@ export interface AgentService {
|
|
|
89
101
|
* fails to come up, after closing the service: a host must not report itself serving while a
|
|
90
102
|
* declared channel is dead, and health answers 503 until this resolves. */
|
|
91
103
|
ready: Promise<void>;
|
|
92
|
-
/** The control plane's bearer token and prefix, when `sessionControl` is on — how
|
|
93
|
-
*
|
|
104
|
+
/** The control plane's bearer token and prefix, when `sessionControl` is on — how a caller hands
|
|
105
|
+
* access to a client. The CLI writes it to `<stateRoot>/control.json` for `fastagent attach`; an
|
|
106
|
+
* embedder mounted inside a larger app has no port of its own to describe and distributes it
|
|
107
|
+
* itself. */
|
|
94
108
|
control?: {
|
|
95
109
|
token: string;
|
|
96
110
|
prefix: string;
|
|
97
111
|
};
|
|
98
|
-
/** Write `<stateRoot>/control.json` so a LOCAL client (`fastagent attach`) can find the plane,
|
|
99
|
-
* once the port is known. Optional: an embedder mounted inside a larger app has no port of its
|
|
100
|
-
* own to describe and uses {@link AgentService.control} instead.
|
|
101
|
-
*
|
|
102
|
-
* Removed by `close()`. Not by an `exit` handler: installing one is a decision about the whole
|
|
103
|
-
* process, which a mounted library does not get to make. A hard exit therefore leaves the file
|
|
104
|
-
* behind — advisory, overwritten by the next boot, and the client's own error stays honest. */
|
|
105
|
-
announce(boundPort: number): void;
|
|
106
112
|
/** Stop long connections and schedules. Idempotent; also runs when `options.signal` aborts. */
|
|
107
113
|
close(): Promise<void>;
|
|
108
114
|
}
|
|
@@ -112,12 +118,6 @@ export interface MountAgentServiceOptions {
|
|
|
112
118
|
* must get the SAME one, which is why this is a hook rather than the caller's own call. `dev`
|
|
113
119
|
* passes `logAgentLoop`. */
|
|
114
120
|
wrapAgent?: (agent: Agent) => Agent;
|
|
115
|
-
/** Passed through to the control plane mount: `--tunnel` widens its warning, `host` names the
|
|
116
|
-
* bind address in the discovery file. */
|
|
117
|
-
control?: {
|
|
118
|
-
tunnel?: boolean;
|
|
119
|
-
host?: string;
|
|
120
|
-
};
|
|
121
121
|
/** Aborting this closes the service, exactly like calling {@link AgentService.close}. */
|
|
122
122
|
signal?: AbortSignal;
|
|
123
123
|
/** Called when a long connection ends on its own — a dropped socket-mode channel, say. The CLI
|