@fastagent-sh/fastagent 0.29.0 → 0.30.1
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 +9 -4
- package/dist/atomic-write.d.ts +4 -5
- package/dist/atomic-write.js +4 -5
- package/dist/authoring.d.ts +32 -15
- package/dist/authoring.js +147 -127
- package/dist/channels/agentcore-protocol.d.ts +8 -3
- package/dist/channels/agentcore-protocol.js +3 -1
- package/dist/channels/agentcore-service.d.ts +1 -0
- package/dist/channels/agentcore-service.js +14 -4
- package/dist/channels/agentcore.d.ts +9 -0
- package/dist/channels/agentcore.js +29 -4
- package/dist/channels/context.d.ts +2 -0
- package/dist/channels/context.js +5 -1
- package/dist/channels/feishu/feishu-api.d.ts +2 -1
- package/dist/channels/feishu/feishu.d.ts +0 -3
- package/dist/channels/feishu/feishu.js +10 -10
- package/dist/channels/feishu/register-app.js +3 -5
- package/dist/channels/feishu/scaffold/channel.ts +32 -34
- package/dist/channels/feishu/scaffold/feishu-send.ts +5 -5
- package/dist/channels/feishu/scaffold/feishu-threads.ts +3 -3
- package/dist/channels/feishu/setup-mode.d.ts +2 -12
- package/dist/channels/feishu/setup-mode.js +2 -22
- package/dist/channels/feishu/setup.d.ts +12 -0
- package/dist/channels/feishu/setup.js +197 -0
- package/dist/channels/kit/abortable.d.ts +8 -0
- package/dist/channels/kit/abortable.js +22 -0
- package/dist/channels/lark/lark.d.ts +0 -3
- package/dist/channels/lark/lark.js +0 -5
- package/dist/channels/lark/scaffold/channel.ts +33 -35
- package/dist/channels/lark/scaffold/lark-send.ts +5 -5
- package/dist/channels/lark/scaffold/lark-threads.ts +3 -3
- package/dist/channels/lark/setup.d.ts +9 -0
- package/dist/channels/lark/setup.js +57 -0
- package/dist/channels/setup.d.ts +112 -0
- package/dist/channels/setup.js +31 -0
- package/dist/channels/slack/history.js +1 -1
- package/dist/channels/slack/manifest.d.ts +17 -7
- package/dist/channels/slack/manifest.js +12 -16
- package/dist/channels/slack/scaffold/channel.ts +44 -30
- package/dist/channels/slack/scaffold/slack-send.ts +8 -8
- package/dist/channels/slack/scaffold/slack-threads.ts +4 -4
- package/dist/channels/slack/setup.d.ts +7 -0
- package/dist/channels/slack/setup.js +119 -0
- package/dist/channels/slack/slack-api.d.ts +2 -0
- package/dist/channels/slack/slack-api.js +4 -0
- package/dist/channels/slack/slack.d.ts +21 -2
- package/dist/channels/slack/slack.js +398 -338
- package/dist/channels/slack/socket.d.ts +57 -0
- package/dist/channels/slack/socket.js +305 -0
- package/dist/channels/telegram/polling.d.ts +27 -0
- package/dist/channels/telegram/polling.js +105 -0
- package/dist/channels/telegram/scaffold/channel.ts +52 -32
- package/dist/channels/telegram/scaffold/telegram-send.ts +9 -9
- package/dist/channels/telegram/setup.d.ts +2 -0
- package/dist/channels/telegram/setup.js +42 -0
- package/dist/channels/telegram/telegram-api.d.ts +27 -1
- package/dist/channels/telegram/telegram-api.js +7 -6
- package/dist/channels/telegram/telegram.d.ts +11 -3
- package/dist/channels/telegram/telegram.js +184 -157
- package/dist/channels/webhook-evidence.d.ts +11 -0
- package/dist/channels/webhook-evidence.js +71 -0
- package/dist/channels/webhooks.d.ts +37 -0
- package/dist/channels/webhooks.js +70 -0
- package/dist/child-env.d.ts +16 -0
- package/dist/child-env.js +30 -0
- package/dist/cli/box-login.js +4 -1
- package/dist/cli/channel-app.d.ts +3 -0
- package/dist/cli/channel-app.js +35 -0
- package/dist/cli/channel-setup.d.ts +38 -0
- package/dist/cli/channel-setup.js +195 -0
- package/dist/cli/commands/add.d.ts +6 -14
- package/dist/cli/commands/add.js +45 -303
- package/dist/cli/commands/auth.js +8 -2
- package/dist/cli/commands/build.d.ts +5 -0
- package/dist/cli/commands/build.js +16 -0
- package/dist/cli/commands/channels.d.ts +19 -0
- package/dist/cli/commands/channels.js +230 -0
- package/dist/cli/commands/content.d.ts +7 -3
- package/dist/cli/commands/content.js +16 -10
- package/dist/cli/commands/deploy/agentcore.js +55 -23
- package/dist/cli/commands/deploy/channel-apps.d.ts +17 -0
- package/dist/cli/commands/deploy/channel-apps.js +30 -0
- package/dist/cli/commands/deploy/docker.js +32 -65
- package/dist/cli/commands/deploy/fly.js +28 -12
- package/dist/cli/commands/deploy/railway.js +38 -13
- package/dist/cli/commands/deploy/secrets-step.d.ts +19 -0
- package/dist/cli/commands/deploy/secrets-step.js +66 -0
- package/dist/cli/commands/deploy/shared.d.ts +16 -5
- package/dist/cli/commands/deploy/shared.js +25 -8
- package/dist/cli/commands/deploy.js +53 -60
- package/dist/cli/commands/dev.d.ts +2 -0
- package/dist/cli/commands/dev.js +15 -3
- package/dist/cli/commands/env.js +1 -1
- package/dist/cli/commands/info.d.ts +2 -0
- package/dist/cli/commands/info.js +60 -25
- package/dist/cli/commands/init.js +3 -1
- package/dist/cli/commands/invoke.d.ts +2 -0
- package/dist/cli/commands/invoke.js +20 -7
- package/dist/cli/commands/login.d.ts +0 -7
- package/dist/cli/commands/login.js +22 -10
- package/dist/cli/commands/logout.js +1 -1
- package/dist/cli/commands/mcp.js +14 -6
- package/dist/cli/commands/models.js +3 -2
- package/dist/cli/commands/schedules.d.ts +4 -2
- package/dist/cli/commands/schedules.js +14 -10
- package/dist/cli/commands/secrets.d.ts +14 -0
- package/dist/cli/commands/secrets.js +213 -0
- package/dist/cli/commands/start.d.ts +14 -14
- package/dist/cli/commands/start.js +46 -60
- package/dist/cli/content-view.d.ts +4 -6
- package/dist/cli/content-view.js +5 -11
- package/dist/cli/first-run-channels.d.ts +3 -0
- package/dist/cli/first-run-channels.js +63 -0
- package/dist/cli/instance.d.ts +41 -0
- package/dist/cli/instance.js +155 -0
- package/dist/cli/invoke-stream.d.ts +5 -2
- package/dist/cli/invoke-stream.js +7 -2
- package/dist/cli/kernel.js +3 -0
- package/dist/cli/program.js +282 -110
- package/dist/cli/serve.d.ts +4 -5
- package/dist/cli/serve.js +4 -7
- package/dist/cli/shared.d.ts +11 -2
- package/dist/cli/shared.js +32 -19
- package/dist/cli/ticket.d.ts +7 -0
- package/dist/cli/ticket.js +106 -30
- package/dist/content/declare.d.ts +31 -37
- package/dist/content/declare.js +59 -60
- package/dist/content/file.d.ts +23 -12
- package/dist/content/file.js +48 -35
- package/dist/content/git.d.ts +0 -10
- package/dist/content/git.js +5 -39
- package/dist/content/resolve.d.ts +18 -22
- package/dist/content/resolve.js +32 -66
- package/dist/content/source.d.ts +7 -8
- package/dist/content/source.js +10 -28
- package/dist/credentials-page.d.ts +26 -0
- package/dist/credentials-page.js +185 -0
- package/dist/declared-secrets.d.ts +10 -3
- package/dist/declared-secrets.js +8 -4
- package/dist/deploy/agentcore/destroy.d.ts +4 -1
- package/dist/deploy/agentcore/destroy.js +25 -1
- package/dist/deploy/agentcore/forwarder.js +1 -1
- package/dist/deploy/agentcore/plan.d.ts +7 -21
- package/dist/deploy/agentcore/plan.js +47 -49
- package/dist/deploy/agentcore/run.d.ts +18 -10
- package/dist/deploy/agentcore/run.js +93 -70
- package/dist/deploy/agentcore/runtime-secrets.d.ts +7 -0
- package/dist/deploy/agentcore/runtime-secrets.js +108 -0
- package/dist/deploy/agentcore/shell.js +12 -19
- package/dist/deploy/agentcore/sigv4.d.ts +7 -0
- package/dist/deploy/agentcore/sigv4.js +15 -0
- package/dist/deploy/agentcore/slr.d.ts +18 -0
- package/dist/deploy/agentcore/slr.js +27 -0
- package/dist/deploy/agentcore/store.d.ts +7 -0
- package/dist/deploy/agentcore/store.js +115 -0
- package/dist/deploy/api.d.ts +14 -14
- package/dist/deploy/api.js +26 -24
- package/dist/deploy/box-shell.d.ts +3 -1
- package/dist/deploy/build-context.d.ts +4 -4
- package/dist/deploy/build-context.js +11 -15
- package/dist/deploy/channel-ingress.d.ts +35 -9
- package/dist/deploy/channel-ingress.js +119 -22
- package/dist/deploy/container.d.ts +35 -10
- package/dist/deploy/container.js +79 -37
- package/dist/deploy/delivered-text.d.ts +13 -0
- package/dist/deploy/delivered-text.js +86 -0
- package/dist/deploy/docker/plan.d.ts +7 -8
- package/dist/deploy/docker/plan.js +31 -52
- package/dist/deploy/docker/run.d.ts +5 -43
- package/dist/deploy/docker/run.js +9 -84
- package/dist/deploy/file-store.d.ts +2 -0
- package/dist/deploy/file-store.js +17 -0
- package/dist/deploy/fly/plan.d.ts +3 -2
- package/dist/deploy/fly/plan.js +20 -9
- package/dist/deploy/fly/run.d.ts +18 -8
- package/dist/deploy/fly/run.js +24 -42
- package/dist/deploy/fly/store.d.ts +12 -0
- package/dist/deploy/fly/store.js +83 -0
- package/dist/deploy/hosts.d.ts +1 -2
- package/dist/deploy/hosts.js +1 -2
- package/dist/deploy/preflight.d.ts +18 -5
- package/dist/deploy/preflight.js +135 -63
- package/dist/deploy/railway/ignore.d.ts +8 -0
- package/dist/deploy/railway/ignore.js +57 -0
- package/dist/deploy/railway/plan.d.ts +1 -2
- package/dist/deploy/railway/plan.js +8 -6
- package/dist/deploy/railway/run.d.ts +21 -15
- package/dist/deploy/railway/run.js +32 -78
- package/dist/deploy/railway/store.d.ts +29 -0
- package/dist/deploy/railway/store.js +143 -0
- package/dist/deploy/runner.d.ts +5 -2
- package/dist/deploy/runner.js +4 -3
- package/dist/deploy/secret-store.d.ts +63 -0
- package/dist/deploy/secret-store.js +98 -0
- package/dist/deploy/secrets.d.ts +29 -46
- package/dist/deploy/secrets.js +49 -122
- package/dist/deploy/workspace.d.ts +6 -12
- package/dist/deploy/workspace.js +18 -94
- package/dist/dev-supervisor.js +6 -8
- package/dist/environment/declare.js +3 -2
- package/dist/environment/mise.d.ts +3 -1
- package/dist/environment/mise.js +4 -2
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/harnesses/models/catalog.d.ts +1 -0
- package/dist/harnesses/models/catalog.js +1 -2
- package/dist/harnesses/models/environment.d.ts +7 -2
- package/dist/harnesses/models/environment.js +9 -5
- package/dist/harnesses/models/host.d.ts +0 -7
- package/dist/harnesses/models/host.js +10 -1
- package/dist/harnesses/models/login.d.ts +0 -8
- package/dist/harnesses/models/login.js +5 -4
- package/dist/harnesses/models/models.d.ts +7 -2
- package/dist/harnesses/models/models.js +28 -4
- package/dist/harnesses/pi-durable/agent.js +59 -38
- package/dist/harnesses/pi-durable/build.d.ts +12 -0
- package/dist/harnesses/pi-durable/build.js +270 -0
- package/dist/harnesses/pi-durable/built.d.ts +24 -0
- package/dist/harnesses/pi-durable/built.js +136 -0
- package/dist/harnesses/pi-durable/config.d.ts +81 -0
- package/dist/harnesses/pi-durable/config.js +266 -0
- package/dist/harnesses/pi-durable/define.d.ts +63 -13
- package/dist/harnesses/pi-durable/define.js +23 -2
- package/dist/harnesses/pi-durable/definition.d.ts +72 -0
- package/dist/harnesses/pi-durable/definition.js +143 -0
- package/dist/harnesses/pi-durable/events.d.ts +4 -3
- package/dist/harnesses/pi-durable/events.js +179 -136
- package/dist/harnesses/pi-durable/execution-env.d.ts +17 -0
- package/dist/harnesses/pi-durable/execution-env.js +11 -0
- package/dist/harnesses/pi-durable/mcp.js +1 -3
- package/dist/harnesses/pi-durable/open.d.ts +35 -37
- package/dist/harnesses/pi-durable/open.js +91 -153
- package/dist/harnesses/pi-durable/reads.d.ts +0 -1
- package/dist/harnesses/pi-durable/reads.js +1 -3
- package/dist/harnesses/pi-durable/registry.d.ts +4 -6
- package/dist/harnesses/pi-durable/registry.js +29 -26
- package/dist/harnesses/pi-durable/release.d.ts +2 -2
- package/dist/harnesses/pi-durable/release.js +10 -7
- package/dist/harnesses/pi-durable/serve.d.ts +91 -15
- package/dist/harnesses/pi-durable/serve.js +109 -47
- package/dist/harnesses/pi-durable/sessions.js +15 -12
- package/dist/harnesses/pi-durable/text-sources.d.ts +30 -0
- package/dist/harnesses/pi-durable/text-sources.js +55 -0
- package/dist/harnesses/pi-durable/text.d.ts +16 -4
- package/dist/harnesses/pi-durable/text.js +115 -59
- package/dist/harnesses/pi-durable/wake.js +8 -8
- package/dist/http/client.js +9 -3
- package/dist/http/contract.d.ts +3 -3
- package/dist/http/server.d.ts +1 -1
- package/dist/http/server.js +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +4 -1
- package/dist/instance-name.d.ts +7 -0
- package/dist/instance-name.js +26 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/mcp/client.js +11 -2
- package/dist/mcp/config.d.ts +70 -18
- package/dist/mcp/config.js +136 -53
- package/dist/mcp/host.d.ts +5 -2
- package/dist/mcp/host.js +14 -6
- package/dist/mcp/sign-in.js +2 -2
- package/dist/mcp-api.d.ts +14 -0
- package/dist/paths.d.ts +22 -8
- package/dist/paths.js +44 -11
- package/dist/runtime-websocket.d.ts +14 -0
- package/dist/runtime-websocket.js +39 -0
- package/dist/scaffold/add-channel.d.ts +9 -16
- package/dist/scaffold/add-channel.js +13 -83
- package/dist/scaffold/templates/env.example +4 -5
- package/dist/scaffold/templates/fastagent.config.ts +20 -14
- package/dist/scaffold/templates/gitignore +9 -6
- package/dist/scaffold/templates.d.ts +6 -2
- package/dist/scaffold/templates.js +7 -2
- package/dist/scaffold/vendor-skill.js +4 -2
- package/dist/service.d.ts +44 -6
- package/dist/service.js +110 -10
- package/dist/serving-command.d.ts +10 -0
- package/dist/serving-command.js +1 -0
- package/dist/slack.d.ts +1 -1
- package/dist/slack.js +1 -1
- package/dist/strip-edges.d.ts +10 -0
- package/dist/strip-edges.js +22 -0
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +0 -1
- package/dist/tunnel.js +2 -8
- package/dist/value-file.d.ts +11 -6
- package/dist/value-file.js +21 -9
- package/dist/version.d.ts +7 -1
- package/dist/version.js +9 -3
- package/package.json +6 -5
- package/dist/channels/lark/credentials-page.d.ts +0 -25
- package/dist/channels/lark/credentials-page.js +0 -206
- package/dist/channels/lark/onboard.d.ts +0 -52
- package/dist/channels/lark/onboard.js +0 -86
- package/dist/channels/slack/config-api.d.ts +0 -69
- package/dist/channels/slack/config-api.js +0 -162
- package/dist/channels/slack/onboard.d.ts +0 -40
- package/dist/channels/slack/onboard.js +0 -136
- package/dist/channels/slack/onboarding-state.d.ts +0 -31
- package/dist/channels/slack/onboarding-state.js +0 -64
- package/dist/channels/slack/register-webhook.d.ts +0 -12
- package/dist/channels/slack/register-webhook.js +0 -67
- package/dist/channels/slack/setup-server.d.ts +0 -32
- package/dist/channels/slack/setup-server.js +0 -246
- package/dist/cli/add-feishu.d.ts +0 -41
- package/dist/cli/add-feishu.js +0 -391
- package/dist/cli/add-slack.d.ts +0 -18
- package/dist/cli/add-slack.js +0 -216
- package/dist/harnesses/pi-durable/parked.d.ts +0 -18
- package/dist/harnesses/pi-durable/parked.js +0 -27
|
@@ -7,46 +7,23 @@ import { dirname, join } from "node:path";
|
|
|
7
7
|
import { detectRuntime } from "../runtime.js";
|
|
8
8
|
import { assertInsideAgentDir, exists } from "../paths.js";
|
|
9
9
|
import { channelBundleFiles, channelTemplate } from "./templates.js";
|
|
10
|
-
import { writeValues } from "../value-file.js";
|
|
11
10
|
import { envExamplePath } from "../value-file.js";
|
|
12
|
-
import { FEISHU_AGENT_SCOPES } from "../channels/feishu/setup-mode.js";
|
|
13
|
-
import { slackBotScopes } from "../channels/slack/manifest.js";
|
|
14
|
-
const AGENT_SCOPE_LIST = FEISHU_AGENT_SCOPES.map((entry) => entry.request).join(", ");
|
|
15
|
-
const FEISHU_PERMISSION_STEP = `before publishing: the app needs ${AGENT_SCOPE_LIST} — \`add feishu\` requests them with the app and names any ` +
|
|
16
|
-
"your tenant withheld (approve those, then publish)";
|
|
17
|
-
/** The setting both clouds read, said the same way in `.env.example` and in `add`'s next steps. */
|
|
18
|
-
const ingressEnv = (prefix) => ({
|
|
19
|
-
name: `${prefix}_INGRESS`,
|
|
20
|
-
hint: "webhook | websocket — unset, `dev` connects by websocket, and `start` and every deployment receive by webhook",
|
|
21
|
-
required: false,
|
|
22
|
-
});
|
|
23
|
-
const LARK_PERMISSION_STEP = `before publishing: add ${AGENT_SCOPE_LIST} — \`add lark\` checks them and opens the permission page for any that ` +
|
|
24
|
-
"are missing";
|
|
25
11
|
const CHANNEL_SCAFFOLDS = {
|
|
12
|
+
// A channel's app is set up by `channels setup`, which asks for these values; `add` only names them.
|
|
26
13
|
telegram: {
|
|
27
14
|
env: [
|
|
28
15
|
{ name: "TELEGRAM_BOT_TOKEN", hint: "from @BotFather → /newbot", required: true },
|
|
29
|
-
{
|
|
30
|
-
name: "TELEGRAM_SECRET_TOKEN",
|
|
31
|
-
hint: "any random string; verifies inbound updates",
|
|
32
|
-
required: true,
|
|
33
|
-
generate: true,
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
steps: [
|
|
37
|
-
"edit {channel} — customise routing with route() (optional; the defaults already work)",
|
|
38
|
-
"the agent can send messages or files back by calling the scaffolded {tools}/telegram-send.ts tool",
|
|
16
|
+
{ name: "TELEGRAM_SECRET_TOKEN", hint: "webhook only: verifies inbound updates", required: true },
|
|
39
17
|
],
|
|
18
|
+
steps: ["the agent can send messages or files back by calling the scaffolded {tools}/telegram-send.ts tool"],
|
|
40
19
|
},
|
|
41
20
|
slack: {
|
|
42
21
|
env: [
|
|
43
|
-
{ name: "SLACK_BOT_TOKEN", hint: "
|
|
44
|
-
{ name: "
|
|
22
|
+
{ name: "SLACK_BOT_TOKEN", hint: "Install App → Bot User OAuth Token", required: true },
|
|
23
|
+
{ name: "SLACK_APP_TOKEN", hint: "connection only: Basic Information → App-Level Tokens", required: true },
|
|
24
|
+
{ name: "SLACK_SIGNING_SECRET", hint: "webhook only: Basic Information → Signing Secret", required: true },
|
|
45
25
|
],
|
|
46
26
|
steps: [
|
|
47
|
-
`Slack Bot Token Scopes: ${slackBotScopes().join(", ")}`,
|
|
48
|
-
"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",
|
|
49
|
-
"reinstall the app after changing scopes, then invite it to each channel it should read",
|
|
50
27
|
"the agent can send messages or files by calling the scaffolded {tools}/slack-send.ts tool",
|
|
51
28
|
"the agent reads the threads of the channel it is asked in via the scaffolded {tools}/slack-threads.ts tool",
|
|
52
29
|
],
|
|
@@ -57,17 +34,17 @@ const CHANNEL_SCAFFOLDS = {
|
|
|
57
34
|
env: [
|
|
58
35
|
{
|
|
59
36
|
name: "FEISHU_APP_ID",
|
|
60
|
-
hint: "
|
|
37
|
+
hint: "`channels setup feishu` creates the app (console → Credentials & Basic Info)",
|
|
61
38
|
required: true,
|
|
62
39
|
},
|
|
63
40
|
{
|
|
64
41
|
name: "FEISHU_APP_SECRET",
|
|
65
|
-
hint: "
|
|
42
|
+
hint: "`channels setup feishu` keeps it (console → Credentials & Basic Info)",
|
|
66
43
|
required: true,
|
|
67
44
|
},
|
|
68
45
|
{
|
|
69
46
|
name: "FEISHU_VERIFICATION_TOKEN",
|
|
70
|
-
hint: "webhook only
|
|
47
|
+
hint: "webhook only: `channels setup feishu` captures it (console → Events & Callbacks)",
|
|
71
48
|
required: true,
|
|
72
49
|
},
|
|
73
50
|
{
|
|
@@ -75,13 +52,8 @@ const CHANNEL_SCAFFOLDS = {
|
|
|
75
52
|
hint: "webhook only; optional but recommended — set one in the console and copy it here",
|
|
76
53
|
required: false,
|
|
77
54
|
},
|
|
78
|
-
ingressEnv("FEISHU"),
|
|
79
55
|
],
|
|
80
56
|
steps: [
|
|
81
|
-
FEISHU_PERMISSION_STEP,
|
|
82
|
-
"PUBLISH the app version in the developer console after permission approval — the switch to webhook mode takes effect on publish (one click, once ever; no API for it)",
|
|
83
|
-
"edit {channel} — routing policy (the header walks through the console setup, for hand-made apps)",
|
|
84
|
-
"for webhook ingress, run `{serve} --tunnel` locally or `deploy --run` to register the selected app's event Request URL",
|
|
85
57
|
"the agent can push messages from scheduled turns via the scaffolded {tools}/feishu-send.ts tool",
|
|
86
58
|
"the agent reads the threads of the group it is asked in via the scaffolded {tools}/feishu-threads.ts tool",
|
|
87
59
|
],
|
|
@@ -92,7 +64,7 @@ const CHANNEL_SCAFFOLDS = {
|
|
|
92
64
|
{ name: "LARK_APP_SECRET", hint: "developer console → Credentials & Basic Info", required: true },
|
|
93
65
|
{
|
|
94
66
|
name: "LARK_VERIFICATION_TOKEN",
|
|
95
|
-
hint: "webhook only
|
|
67
|
+
hint: "webhook only — console → Events & Callbacks; authenticates inbound events",
|
|
96
68
|
required: true,
|
|
97
69
|
},
|
|
98
70
|
{
|
|
@@ -100,12 +72,8 @@ const CHANNEL_SCAFFOLDS = {
|
|
|
100
72
|
hint: "webhook only; optional but recommended — set one in the console and copy it here",
|
|
101
73
|
required: false,
|
|
102
74
|
},
|
|
103
|
-
ingressEnv("LARK"),
|
|
104
75
|
],
|
|
105
76
|
steps: [
|
|
106
|
-
"finish the console setup: enable Bot and add the required permissions + im.message.receive_v1 event listed in {channel} (do not publish yet)",
|
|
107
|
-
LARK_PERMISSION_STEP,
|
|
108
|
-
"for webhook ingress, run `{serve} --tunnel` and keep it running; if auto-registration reports a config-API 404, manually switch Subscription mode to webhook, set its printed https://…/lark Request URL, save, then create + publish a version",
|
|
109
77
|
"the agent can push messages from scheduled turns via the scaffolded {tools}/lark-send.ts tool",
|
|
110
78
|
"the agent reads the threads of the group it is asked in via the scaffolded {tools}/lark-threads.ts tool",
|
|
111
79
|
],
|
|
@@ -113,35 +81,9 @@ const CHANNEL_SCAFFOLDS = {
|
|
|
113
81
|
};
|
|
114
82
|
/** The channel kinds `fastagent add <kind>` can scaffold. */
|
|
115
83
|
export const CHANNEL_KINDS = Object.keys(CHANNEL_SCAFFOLDS);
|
|
116
|
-
/**
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"set for this environment; production has its own app and ingress setting"
|
|
120
|
-
: "run `{serve}` (no tunnel needed: it connects by WebSocket); " +
|
|
121
|
-
"`deploy --run` creates a separate production app for webhook";
|
|
122
|
-
const websocketSetup = (kind, pinned) => {
|
|
123
|
-
const prefix = kind.toUpperCase();
|
|
124
|
-
return {
|
|
125
|
-
env: CHANNEL_SCAFFOLDS[kind].env.filter((entry) => [`${prefix}_APP_ID`, `${prefix}_APP_SECRET`, `${prefix}_INGRESS`].includes(entry.name)),
|
|
126
|
-
steps: [
|
|
127
|
-
kind === "feishu" ? FEISHU_PERMISSION_STEP : LARK_PERMISSION_STEP,
|
|
128
|
-
kind === "feishu"
|
|
129
|
-
? "PUBLISH the app version in the developer console after permission approval — long-connection event subscriptions become active with the published version"
|
|
130
|
-
: "in Events & Callbacks choose long connection, subscribe im.message.receive_v1, then create + publish a version",
|
|
131
|
-
"edit {channel} — routing policy",
|
|
132
|
-
deploymentStep(kind, pinned),
|
|
133
|
-
`the agent can push messages from scheduled turns via the scaffolded {tools}/${kind}-send.ts tool`,
|
|
134
|
-
`the agent reads the threads of the group it is asked in via the scaffolded {tools}/${kind}-threads.ts tool`,
|
|
135
|
-
],
|
|
136
|
-
};
|
|
137
|
-
};
|
|
138
|
-
/** The mode-specific env vars + next-step lines a scaffolded channel needs. */
|
|
139
|
-
export function channelSetup(kind, ingress = "webhook",
|
|
140
|
-
/** Whether `<PREFIX>_INGRESS` is explicitly set in this environment. */
|
|
141
|
-
pinned = false) {
|
|
142
|
-
if (ingress !== "websocket" || (kind !== "feishu" && kind !== "lark"))
|
|
143
|
-
return CHANNEL_SCAFFOLDS[kind];
|
|
144
|
-
return websocketSetup(kind, pinned);
|
|
84
|
+
/** The env vars and next-step lines a scaffolded channel has. */
|
|
85
|
+
export function channelSetup(kind) {
|
|
86
|
+
return CHANNEL_SCAFFOLDS[kind];
|
|
145
87
|
}
|
|
146
88
|
/**
|
|
147
89
|
* Append a channel's env vars (commented placeholders + hints) to `.env.example`, so a developer who copies it to
|
|
@@ -167,18 +109,6 @@ export async function appendChannelEnv(dir, kind) {
|
|
|
167
109
|
await appendFile(file, block);
|
|
168
110
|
return true;
|
|
169
111
|
}
|
|
170
|
-
/**
|
|
171
|
-
* Append generated channel secrets to the selected environment's value file, and a placeholder for each the channel
|
|
172
|
-
* reads that nothing generated. Existing non-empty values are kept.
|
|
173
|
-
*/
|
|
174
|
-
export function appendChannelDotEnv(dir, kind, generated, overwrite = [], ingress = "webhook") {
|
|
175
|
-
const entries = channelSetup(kind, ingress).env.map(({ name, hint }) => ({
|
|
176
|
-
name,
|
|
177
|
-
hint,
|
|
178
|
-
...(generated[name] === undefined ? {} : { value: generated[name] }),
|
|
179
|
-
}));
|
|
180
|
-
return writeValues(dir, `${kind} channel`, entries, overwrite);
|
|
181
|
-
}
|
|
182
112
|
/** The path `add <kind>` scaffolds to. */
|
|
183
113
|
function channelPath(dir, kind) {
|
|
184
114
|
return join(dir, "src", "channels", `${kind}.ts`);
|
|
@@ -9,11 +9,10 @@
|
|
|
9
9
|
# ANTHROPIC_API_KEY=
|
|
10
10
|
|
|
11
11
|
# --- Model selection (overrides fastagent.config) ---
|
|
12
|
-
# Precedence: --model flag > FASTAGENT_MODEL > config
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# editing this line and redeploying.
|
|
12
|
+
# Precedence: --model flag > FASTAGENT_MODEL > config, evaluated in whichever environment is running. This file is
|
|
13
|
+
# `dev`'s alone: `fastagent start` and `fastagent deploy` read .secrets/production/.env and never this one, and a
|
|
14
|
+
# deployment's values live on its target (`fastagent secrets list <host>`). To fix the model for every instance, set
|
|
15
|
+
# `defaults.model` in fastagent.config.ts.
|
|
17
16
|
# FASTAGENT_MODEL=openai/gpt-5.5
|
|
18
17
|
|
|
19
18
|
# --- Serving (fastagent start) ---
|
|
@@ -1,24 +1,30 @@
|
|
|
1
|
-
// fastagent.config.ts — the agent's configuration:
|
|
2
|
-
// SYSTEM.md, its capabilities in skills/ and src/ (tools, extensions, channels, schedules) — never here.
|
|
1
|
+
// fastagent.config.ts — the agent's configuration: what it is, and what each instance of it starts with. Its prompt
|
|
2
|
+
// lives in SYSTEM.md, its capabilities in skills/ and src/ (tools, extensions, channels, schedules) — never here.
|
|
3
3
|
//
|
|
4
|
-
// Model precedence: `--model` flag > FASTAGENT_MODEL env > this default. No model is
|
|
5
|
-
// the model catalog (models you already have credentials for come first; picking one
|
|
6
|
-
// inline) and runs with your choice; set it below to keep it. `fastagent models` lists
|
|
7
|
-
// A self-hosted model (vLLM, Ollama, …) or your own gateway is a provider in code:
|
|
8
|
-
// pi-ai's `createProvider`.
|
|
4
|
+
// Model precedence: `--model` flag > FASTAGENT_MODEL env > an instance's `fastagent.json` > this default. No model is
|
|
5
|
+
// preset: `fastagent dev` shows the model catalog (models you already have credentials for come first; picking one
|
|
6
|
+
// that needs auth logs you in inline) and runs with your choice; set it below to keep it. `fastagent models` lists
|
|
7
|
+
// every "provider/modelId". A self-hosted model (vLLM, Ollama, …) or your own gateway is a provider in code:
|
|
8
|
+
// `providers: [...]`, built with pi-ai's `createProvider`.
|
|
9
9
|
//
|
|
10
10
|
// Every key is optional; the commented lines carry an example. `satisfies` is what makes your editor complete these
|
|
11
11
|
// keys and turns a typo into an error you see while writing. The import is TYPE-only: nothing is loaded at runtime,
|
|
12
|
-
// so the agent opens before `npm install` has run.
|
|
12
|
+
// so the agent opens before `npm install` has run. To let each instance choose values of its own, declare `settings`
|
|
13
|
+
// with `obj({ ... })` from @earendil-works/pi-schema and switch to `defineAgent`.
|
|
13
14
|
import type { AgentConfig } from "@fastagent-sh/fastagent";
|
|
14
15
|
|
|
15
16
|
export default {
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
17
|
+
// Constants (what this agent is; an instance cannot change them): name, description, providers, extensions.
|
|
18
|
+
|
|
19
|
+
// Defaults: what an instance starts with, which its own `fastagent.json` may override.
|
|
20
|
+
defaults: {
|
|
21
|
+
// model: "openai/gpt-5.5",
|
|
22
|
+
// thinkingLevel: "high", // reasoning effort (off|minimal|low|medium|high|xhigh|max)
|
|
23
|
+
http: {
|
|
24
|
+
port: 8787, // the port a serve listens on, for its channels' webhooks and its health check
|
|
25
|
+
// api: false, // serve no Agent API under /v1, whatever key the environment holds; deploy gives none a key
|
|
26
|
+
},
|
|
27
|
+
// deploy: { agentcore: { idleTimeoutSeconds: 180 } }, // `deploy agentcore` only: idle microVM tail, 60–1209600s
|
|
21
28
|
},
|
|
22
29
|
// The commands the agent needs (CLIs, runtimes, apt packages) live in context.json: `fastagent env add gh`.
|
|
23
|
-
// deploy: { agentcore: { idleTimeoutSeconds: 180 } }, // `deploy agentcore` only: idle microVM tail, 60–1209600s
|
|
24
30
|
} satisfies AgentConfig;
|
|
@@ -5,17 +5,20 @@
|
|
|
5
5
|
# DIRECTORY, so a symlinked one (pnpm, a shared store) would show up as untracked.
|
|
6
6
|
node_modules
|
|
7
7
|
|
|
8
|
-
# generated
|
|
8
|
+
# generated: `fastagent build`'s package, and FastAgent's own files, a local instance's state, working directory and
|
|
9
|
+
# clones of the agent's content included (`.fastagent/dev/`, `.fastagent/start/`)
|
|
10
|
+
/dist
|
|
9
11
|
.fastagent
|
|
10
|
-
#
|
|
12
|
+
# the same, where an instance is placed in the agent's directory (`--state`, `--root`), and on a deployed host, where
|
|
13
|
+
# `content/` is a link to the volume (no trailing slash, since a link is not a directory)
|
|
11
14
|
.state
|
|
12
|
-
# where fastagent clones the agent's content (each clone is its repository's, not this one's). No trailing
|
|
13
|
-
# slash: on a deployed host it is a link itself.
|
|
14
15
|
/content
|
|
15
|
-
# the directories of this machine the agent is bound to: this machine's, never the agent's
|
|
16
|
-
/context.local.json
|
|
17
16
|
.cache
|
|
18
17
|
|
|
18
|
+
# the development instance's configuration: its app is one person's, so it is personal. What a team shares goes in
|
|
19
|
+
# `defaults` of fastagent.config.ts, or in a deployment's configuration (`deploy --config`).
|
|
20
|
+
fastagent.json
|
|
21
|
+
|
|
19
22
|
# fastagent reads the agent's env from .secrets/.env (which un-ignores its own .env.example). A bare
|
|
20
23
|
# .env at THIS level is the file habit puts here — `dev` says so when it finds one; this keeps it
|
|
21
24
|
# uncommittable meanwhile.
|
|
@@ -5,12 +5,16 @@ export declare const channelTemplate: (kind: string, name: string) => string;
|
|
|
5
5
|
/** The .ts files in a channel's bundle: `channel.ts` is the channel template; the rest are companion tools. */
|
|
6
6
|
export declare const channelBundleFiles: (kind: string) => string[];
|
|
7
7
|
/**
|
|
8
|
-
* package.json for the complete agent: fastagent at this build's version,
|
|
9
|
-
* against (its tools and extensions are pi-durable code, its providers pi-ai's
|
|
8
|
+
* package.json for the complete agent: fastagent at this build's version, the packages an agent's code is written
|
|
9
|
+
* against (its tools and extensions are pi-durable code, its providers pi-ai's, its tool schemas pi-schema's) at the
|
|
10
|
+
* versions this build runs, and the TypeScript `fastagent build` compiles it with. Its scripts are the framework's
|
|
11
|
+
* three entries, so `npm run dev` and `npm start` work as in any web project; `start` builds first, since it opens the
|
|
12
|
+
* built `dist/` and refuses one the project has moved past.
|
|
10
13
|
*/
|
|
11
14
|
export declare function packageJson(name: string, build: {
|
|
12
15
|
version: string;
|
|
13
16
|
harness: Record<string, string>;
|
|
17
|
+
typescript: string;
|
|
14
18
|
}): string;
|
|
15
19
|
/** Sanitize a directory basename into a valid npm package name (lowercase, safe chars). */
|
|
16
20
|
export declare function toPackageName(dir: string): string;
|
|
@@ -12,15 +12,20 @@ export const channelTemplate = (kind, name) => readFileSync(new URL(name, channe
|
|
|
12
12
|
/** The .ts files in a channel's bundle: `channel.ts` is the channel template; the rest are companion tools. */
|
|
13
13
|
export const channelBundleFiles = (kind) => readdirSync(channelScaffoldDir(kind)).filter((f) => f.endsWith(".ts"));
|
|
14
14
|
/**
|
|
15
|
-
* package.json for the complete agent: fastagent at this build's version,
|
|
16
|
-
* against (its tools and extensions are pi-durable code, its providers pi-ai's
|
|
15
|
+
* package.json for the complete agent: fastagent at this build's version, the packages an agent's code is written
|
|
16
|
+
* against (its tools and extensions are pi-durable code, its providers pi-ai's, its tool schemas pi-schema's) at the
|
|
17
|
+
* versions this build runs, and the TypeScript `fastagent build` compiles it with. Its scripts are the framework's
|
|
18
|
+
* three entries, so `npm run dev` and `npm start` work as in any web project; `start` builds first, since it opens the
|
|
19
|
+
* built `dist/` and refuses one the project has moved past.
|
|
17
20
|
*/
|
|
18
21
|
export function packageJson(name, build) {
|
|
19
22
|
return `${JSON.stringify({
|
|
20
23
|
name,
|
|
21
24
|
private: true,
|
|
22
25
|
type: "module",
|
|
26
|
+
scripts: { dev: "fastagent dev", build: "fastagent build", start: "fastagent build && fastagent start" },
|
|
23
27
|
dependencies: { "@fastagent-sh/fastagent": `^${build.version}`, ...build.harness },
|
|
28
|
+
devDependencies: { typescript: build.typescript },
|
|
24
29
|
}, null, 2)}\n`;
|
|
25
30
|
}
|
|
26
31
|
/** Sanitize a directory basename into a valid npm package name (lowercase, safe chars). */
|
|
@@ -8,11 +8,13 @@ import { randomUUID } from "node:crypto";
|
|
|
8
8
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { readSkills } from "../harnesses/pi-durable/text.js";
|
|
11
|
+
import { programText } from "../harnesses/pi-durable/text-sources.js";
|
|
11
12
|
import { assertInsideAgentDir } from "../paths.js";
|
|
13
|
+
import { stripTrailingSlashes } from "../strip-edges.js";
|
|
12
14
|
/** Derive the destination skill name from a source ref: the last path segment, sans `#ref`. */
|
|
13
15
|
function skillNameFromSource(source) {
|
|
14
16
|
const noRef = source.split("#")[0] ?? source;
|
|
15
|
-
return basename(noRef
|
|
17
|
+
return basename(stripTrailingSlashes(noRef));
|
|
16
18
|
}
|
|
17
19
|
/**
|
|
18
20
|
* The skills/ path must be safe to write through: never follow a symlink that escapes the agent dir (mkdir would), and
|
|
@@ -135,7 +137,7 @@ export async function vendorSkill(agentDir, source, options = {}) {
|
|
|
135
137
|
// would prefix-pollute a sibling `<name>-x` and break on Windows path separators). A skill it refuses is said by
|
|
136
138
|
// that loader, and is missing here.
|
|
137
139
|
const rel = join("skills", name);
|
|
138
|
-
const skill = readSkills(agentDir).find((sk) => relative(agentDir, dirname(sk.filePath)) === rel);
|
|
140
|
+
const skill = readSkills(programText(agentDir).skills).find((sk) => relative(agentDir, dirname(sk.filePath)) === rel);
|
|
139
141
|
return {
|
|
140
142
|
name: skill?.name ?? name,
|
|
141
143
|
description: skill?.description,
|
package/dist/service.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Agent } from "./agent-api.ts";
|
|
2
2
|
import type { ChannelHandler, Routes } from "./channel.ts";
|
|
3
3
|
import { type AnyChannelModule, type LoadedLongConnectionChannel } from "./channels/discover.ts";
|
|
4
|
+
import { type WebhookRegistration } from "./channels/webhooks.ts";
|
|
4
5
|
import { type Scheduler } from "./schedule/scheduler.ts";
|
|
5
6
|
import type { Schedule } from "./schedule/schedule.ts";
|
|
7
|
+
export type { WebhookRegistration } from "./channels/webhooks.ts";
|
|
6
8
|
/** The port a serve binds when neither a flag, `PORT` (start only) nor `http.port` names one. */
|
|
7
9
|
export declare const DEFAULT_HTTP_PORT = 8787;
|
|
8
10
|
/** What a serve answers: its channels' routes, and `GET /health` unless a channel serves it. */
|
|
@@ -14,8 +16,11 @@ interface ServingSurface {
|
|
|
14
16
|
/** Flip health between 200 and 503. */
|
|
15
17
|
setReady(value: boolean): void;
|
|
16
18
|
}
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Mount the channels and the health check: JSON, `{ status, webhooks }`, each webhook channel only by name and outcome
|
|
21
|
+
* (SPEC.md §9.1), since it is public; a step and its reasons are in the log.
|
|
22
|
+
*/
|
|
23
|
+
export declare function routesFor(agent: Agent, stateRoot: string, channels: ReadonlyMap<string, AnyChannelModule>, webhooks?: () => readonly WebhookRegistration[]): ServingSurface;
|
|
19
24
|
/** Start the clock: the definition's schedules, and the wake-ups the agent sets for itself, on every serve. */
|
|
20
25
|
export declare function startSchedules(agent: Agent, stateRoot: string, schedules: readonly Schedule[], options?: {
|
|
21
26
|
localClock?: boolean;
|
|
@@ -24,8 +29,19 @@ export declare function startSchedules(agent: Agent, stateRoot: string, schedule
|
|
|
24
29
|
export interface Service {
|
|
25
30
|
/** For an app's own server to route to. */
|
|
26
31
|
fetch(request: Request): Promise<Response>;
|
|
27
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Or a server of its own, on this runtime's server: the one way to start one. Answers the port it bound. Once the
|
|
34
|
+
* serve is ready it registers the webhook channels at `http.publicUrl`, in the background (`webhooks`).
|
|
35
|
+
*/
|
|
28
36
|
listen(port: number, host?: string): Promise<number>;
|
|
37
|
+
/**
|
|
38
|
+
* Point each webhook channel's platform at `<baseUrl ?? http.publicUrl>/<its path>`, for an app serving `fetch` on its
|
|
39
|
+
* own server, once that server answers and the serve is ready: set on every call. Answers every webhook channel's
|
|
40
|
+
* outcome; rejects only for a `baseUrl` that is not an absolute http(s) URL.
|
|
41
|
+
*/
|
|
42
|
+
registerWebhooks(baseUrl?: string): Promise<WebhookRegistration[]>;
|
|
43
|
+
/** Every webhook channel's outcome, `pending` until its registration ends; empty only with no webhook channel. */
|
|
44
|
+
readonly webhooks: readonly WebhookRegistration[];
|
|
29
45
|
/** Settles when every long connection is up: immediately when there are none. */
|
|
30
46
|
ready: Promise<void>;
|
|
31
47
|
/**
|
|
@@ -52,11 +68,21 @@ export interface ServedAgent extends Service {
|
|
|
52
68
|
/** Whether the Agent API is served, under `/v1`: only with a key (SPEC.md §9.1). */
|
|
53
69
|
api: boolean;
|
|
54
70
|
}
|
|
71
|
+
/** A serve's webhook channels: their outcomes, and the registration that sets them, one at a time. */
|
|
72
|
+
export interface WebhookRegistry {
|
|
73
|
+
/** The outcomes, as one array kept current. */
|
|
74
|
+
list(): readonly WebhookRegistration[];
|
|
75
|
+
register(baseUrl?: string): Promise<WebhookRegistration[]>;
|
|
76
|
+
/** Stop hearing the platforms reach it. */
|
|
77
|
+
dispose(): void;
|
|
78
|
+
}
|
|
79
|
+
/** The webhook channels of an opened agent, to be pointed at `http.publicUrl` or at a URL the host names. */
|
|
80
|
+
export declare function webhooksOf(opened: ServableAgent): WebhookRegistry;
|
|
55
81
|
/**
|
|
56
82
|
* A Service over `handler`: answered in process, or served on a server of its own. `endStreams` ends what would hold
|
|
57
83
|
* the server's close open (a Caller watching events), and `shutdown` stops the rest once the server has.
|
|
58
84
|
*/
|
|
59
|
-
export declare function serviceOver(handler: ChannelHandler, ready: Promise<void>, shutdown: () => Promise<void>, endStreams?: () => Promise<void
|
|
85
|
+
export declare function serviceOver(handler: ChannelHandler, ready: Promise<void>, shutdown: () => Promise<void>, endStreams?: () => Promise<void>, webhooks?: WebhookRegistry): Service;
|
|
60
86
|
/** What {@link serveAgent} needs beyond an opened agent. */
|
|
61
87
|
export interface ServeOptions {
|
|
62
88
|
/** Called when a long connection ends on its own — a dropped socket-mode channel, say. */
|
|
@@ -69,7 +95,20 @@ export interface ServeOptions {
|
|
|
69
95
|
/** What the assembly serves. */
|
|
70
96
|
export interface ServableAgent {
|
|
71
97
|
agent: Agent;
|
|
72
|
-
/** The
|
|
98
|
+
/** The instance's secrets: what its webhook channels are pointed with. */
|
|
99
|
+
secrets: Readonly<Record<string, string>>;
|
|
100
|
+
/** The configuration's part a serve reads: where the platforms reach it, and the URLs a person set there. */
|
|
101
|
+
config: {
|
|
102
|
+
http?: {
|
|
103
|
+
publicUrl?: string;
|
|
104
|
+
};
|
|
105
|
+
channels?: Readonly<Record<string, {
|
|
106
|
+
app?: {
|
|
107
|
+
url?: string;
|
|
108
|
+
};
|
|
109
|
+
}>>;
|
|
110
|
+
};
|
|
111
|
+
/** The project directory the instance's `.secrets/` belong to. */
|
|
73
112
|
agentDir: string;
|
|
74
113
|
/** Where durable state lives (channel state, schedule fires, wake-ups). */
|
|
75
114
|
stateRoot: string;
|
|
@@ -78,4 +117,3 @@ export interface ServableAgent {
|
|
|
78
117
|
}
|
|
79
118
|
/** The assembly itself: channels, schedules and long connections, composed into one handler. */
|
|
80
119
|
export declare function serveAgent(opened: ServableAgent, options?: ServeOptions): Promise<ServedAgent>;
|
|
81
|
-
export {};
|
package/dist/service.js
CHANGED
|
@@ -4,16 +4,19 @@ import * as Effect from "effect/Effect";
|
|
|
4
4
|
import * as Exit from "effect/Exit";
|
|
5
5
|
import * as Fiber from "effect/Fiber";
|
|
6
6
|
import * as Scope from "effect/Scope";
|
|
7
|
-
import { mountChannels } from "./channels/discover.js";
|
|
8
|
-
import { text } from "./channels/respond.js";
|
|
7
|
+
import { channelIngress, mountChannels, } from "./channels/discover.js";
|
|
9
8
|
import { parseRouteKey, router } from "./channels/router.js";
|
|
10
9
|
import { serveHandler, totalFetch } from "./channels/serve.js";
|
|
10
|
+
import { pendingWebhooks, registerOwnWebhooks, webhookKinds, webhookPath, } from "./channels/webhooks.js";
|
|
11
|
+
import { addressedTo, onReached, readReached } from "./channels/webhook-evidence.js";
|
|
12
|
+
import { channelStateDir } from "./channels/context.js";
|
|
11
13
|
import { portCleanup } from "./effect-port.js";
|
|
12
14
|
import { API_PREFIX } from "./http/contract.js";
|
|
13
15
|
import { apiHandler, isApiPath } from "./http/server.js";
|
|
14
16
|
import { log } from "./log.js";
|
|
15
17
|
import { once } from "./once.js";
|
|
16
18
|
import { createScheduler } from "./schedule/scheduler.js";
|
|
19
|
+
import { stripTrailingSlashes } from "./strip-edges.js";
|
|
17
20
|
/** Default wait for a channel's `closed` before reporting it stuck. */
|
|
18
21
|
const CLOSE_DEADLINE_MS = 5_000;
|
|
19
22
|
/** One deadline for all connections; a failed close must not hide a sibling that is still running. */
|
|
@@ -41,8 +44,11 @@ function closeWithin(runs, names, deadlineMs) {
|
|
|
41
44
|
}
|
|
42
45
|
/** The port a serve binds when neither a flag, `PORT` (start only) nor `http.port` names one. */
|
|
43
46
|
export const DEFAULT_HTTP_PORT = 8787;
|
|
44
|
-
/**
|
|
45
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Mount the channels and the health check: JSON, `{ status, webhooks }`, each webhook channel only by name and outcome
|
|
49
|
+
* (SPEC.md §9.1), since it is public; a step and its reasons are in the log.
|
|
50
|
+
*/
|
|
51
|
+
export function routesFor(agent, stateRoot, channels, webhooks = () => []) {
|
|
46
52
|
const { routes, longConnections, routeChannels } = mountChannels(channels, agent, stateRoot);
|
|
47
53
|
for (const key of Object.keys(routes)) {
|
|
48
54
|
if (isApiPath(parseRouteKey(key).path)) {
|
|
@@ -50,7 +56,10 @@ export function routesFor(agent, stateRoot, channels) {
|
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
let ready = longConnections.length === 0;
|
|
53
|
-
const health = () => (
|
|
59
|
+
const health = () => Response.json({
|
|
60
|
+
status: ready ? "ok" : "starting",
|
|
61
|
+
webhooks: webhooks().map(({ channel, outcome }) => ({ channel, outcome })),
|
|
62
|
+
}, { status: ready ? 200 : 503 });
|
|
54
63
|
const probed = Object.keys(routes).some((key) => {
|
|
55
64
|
const entry = parseRouteKey(key);
|
|
56
65
|
return entry.path === "/health" && (entry.method === undefined || entry.method === "GET");
|
|
@@ -78,16 +87,96 @@ export function startSchedules(agent, stateRoot, schedules, options = {}) {
|
|
|
78
87
|
log.info(`[fastagent] schedules: ${schedules.map((s) => s.name).join(", ")}`);
|
|
79
88
|
return scheduler;
|
|
80
89
|
}
|
|
90
|
+
/** The channels whose platform lets only a person set the URL (SPEC.md §8): Slack, and Lark without its config API. */
|
|
91
|
+
const PERSON_SET = ["slack", "lark"];
|
|
92
|
+
/** Nothing to point: a serve with no webhook channel. */
|
|
93
|
+
const NONE = Object.freeze([]);
|
|
94
|
+
const NO_WEBHOOKS = { list: () => NONE, register: async () => [], dispose: () => { } };
|
|
95
|
+
/**
|
|
96
|
+
* The webhook channels among `kinds`, registered with `env`, the instance's own secrets, at `publicUrl` unless a call
|
|
97
|
+
* names another base. Each call waits for the one before it, so two never interleave their platform calls.
|
|
98
|
+
*/
|
|
99
|
+
function webhookRegistry(kinds, env, publicUrl, confirmation, options = {}) {
|
|
100
|
+
// One array, replaced in place: a Service is spread into the objects that extend it, and each keeps reading this.
|
|
101
|
+
const current = pendingWebhooks(kinds);
|
|
102
|
+
const replace = (next) => current.splice(0, current.length, ...next);
|
|
103
|
+
let previous = Promise.resolve();
|
|
104
|
+
let base;
|
|
105
|
+
// A URL only a person sets is confirmed by the platform reaching it: told as it happens, or read from the record.
|
|
106
|
+
const confirmed = (kind, address, at) => {
|
|
107
|
+
const path = webhookPath(kind);
|
|
108
|
+
if (confirmation.appUrl(kind) === `${at}${path}`)
|
|
109
|
+
return true;
|
|
110
|
+
if (address === undefined)
|
|
111
|
+
return false;
|
|
112
|
+
if (addressedTo(address, at, path))
|
|
113
|
+
return true;
|
|
114
|
+
log.warn(`[fastagent] ${kind}: the platform reached this instance at ${address}, not ${at}${path}`);
|
|
115
|
+
return false;
|
|
116
|
+
};
|
|
117
|
+
const confirm = (registrations, at) => registrations.map((entry) => {
|
|
118
|
+
const kind = entry.channel;
|
|
119
|
+
if (at === undefined || entry.outcome !== "manual" || !PERSON_SET.includes(kind))
|
|
120
|
+
return entry;
|
|
121
|
+
if (!confirmed(kind, readReached(confirmation.stateDir(kind)), at))
|
|
122
|
+
return entry;
|
|
123
|
+
return { channel: entry.channel, outcome: "registered", url: `${at}${webhookPath(kind)}` };
|
|
124
|
+
});
|
|
125
|
+
const stops = kinds
|
|
126
|
+
.filter((kind) => PERSON_SET.includes(kind))
|
|
127
|
+
.map((kind) => onReached(confirmation.stateDir(kind), (address) => {
|
|
128
|
+
const index = current.findIndex((entry) => entry.channel === kind);
|
|
129
|
+
const entry = current[index];
|
|
130
|
+
if (base === undefined || entry?.outcome !== "manual" || !confirmed(kind, address, base))
|
|
131
|
+
return;
|
|
132
|
+
current[index] = { channel: kind, outcome: "registered", url: `${base}${webhookPath(kind)}` };
|
|
133
|
+
}));
|
|
134
|
+
return {
|
|
135
|
+
list: () => current,
|
|
136
|
+
register(baseUrl) {
|
|
137
|
+
if (baseUrl !== undefined && !/^https?:\/\/[^/?#\s]+/.test(baseUrl)) {
|
|
138
|
+
return Promise.reject(new Error(`registerWebhooks: ${JSON.stringify(baseUrl)} is not an absolute http(s) URL`));
|
|
139
|
+
}
|
|
140
|
+
const target = baseUrl ?? publicUrl;
|
|
141
|
+
const at = target === undefined ? undefined : stripTrailingSlashes(target);
|
|
142
|
+
const run = previous.then(async () => {
|
|
143
|
+
base = at;
|
|
144
|
+
replace(pendingWebhooks(kinds));
|
|
145
|
+
if (at === undefined && kinds.length > 0) {
|
|
146
|
+
log.info(`[fastagent] webhooks: no http.publicUrl, so nothing points ${kinds.join(", ")} at this instance`);
|
|
147
|
+
}
|
|
148
|
+
replace(confirm(await registerOwnWebhooks(at, kinds, env, options), at));
|
|
149
|
+
return [...current];
|
|
150
|
+
});
|
|
151
|
+
previous = run.catch(() => { });
|
|
152
|
+
return run;
|
|
153
|
+
},
|
|
154
|
+
dispose: () => {
|
|
155
|
+
for (const stop of stops)
|
|
156
|
+
stop();
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/** The webhook channels of an opened agent, to be pointed at `http.publicUrl` or at a URL the host names. */
|
|
161
|
+
export function webhooksOf(opened) {
|
|
162
|
+
const kinds = webhookKinds([...opened.channels].map(([name, module]) => ({ name, ingress: channelIngress(module) })));
|
|
163
|
+
return webhookRegistry(kinds, opened.secrets, opened.config.http?.publicUrl, {
|
|
164
|
+
stateDir: (kind) => channelStateDir(opened.stateRoot, kind),
|
|
165
|
+
appUrl: (kind) => opened.config.channels?.[kind]?.app?.url,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
81
168
|
/**
|
|
82
169
|
* A Service over `handler`: answered in process, or served on a server of its own. `endStreams` ends what would hold
|
|
83
170
|
* the server's close open (a Caller watching events), and `shutdown` stops the rest once the server has.
|
|
84
171
|
*/
|
|
85
|
-
export function serviceOver(handler, ready, shutdown, endStreams = async () => { }) {
|
|
172
|
+
export function serviceOver(handler, ready, shutdown, endStreams = async () => { }, webhooks = NO_WEBHOOKS) {
|
|
86
173
|
let listener;
|
|
87
174
|
let closing;
|
|
88
175
|
const fetch = totalFetch(handler);
|
|
89
176
|
return {
|
|
90
177
|
fetch,
|
|
178
|
+
registerWebhooks: (baseUrl) => closing !== undefined ? Promise.reject(new Error("the service is closed")) : webhooks.register(baseUrl),
|
|
179
|
+
webhooks: webhooks.list(),
|
|
91
180
|
listen(port, host) {
|
|
92
181
|
if (closing !== undefined)
|
|
93
182
|
return Promise.reject(new Error("the service is closed"));
|
|
@@ -95,7 +184,17 @@ export function serviceOver(handler, ready, shutdown, endStreams = async () => {
|
|
|
95
184
|
return Promise.reject(new Error("the service is already listening"));
|
|
96
185
|
const current = serveHandler(handler, { port, ...(host === undefined ? {} : { host }) });
|
|
97
186
|
listener = current;
|
|
98
|
-
return current.listening.
|
|
187
|
+
return current.listening.then((bound) => {
|
|
188
|
+
// After ready: a host may route to the instance only once its health check passes, and a platform verifies
|
|
189
|
+
// the URL as it is set. In the background, so the port is answered without waiting for a platform.
|
|
190
|
+
// A ready that fails is the failure its awaiter reports: nothing is registered for a serve that never was.
|
|
191
|
+
void ready.then(() => closing === undefined
|
|
192
|
+
? webhooks
|
|
193
|
+
.register()
|
|
194
|
+
.catch((error) => log.error(`[fastagent] webhook registration failed: ${String(error)}`))
|
|
195
|
+
: undefined, () => { });
|
|
196
|
+
return bound;
|
|
197
|
+
}, async (error) => {
|
|
99
198
|
// A bind that failed holds nothing once its scope is closed, so the service may listen again, elsewhere.
|
|
100
199
|
if (listener === current)
|
|
101
200
|
listener = undefined;
|
|
@@ -108,7 +207,7 @@ export function serviceOver(handler, ready, shutdown, endStreams = async () => {
|
|
|
108
207
|
closing ??= (async () => {
|
|
109
208
|
// The server first, so nothing new reaches what stops after it; each step runs, and every failure is said.
|
|
110
209
|
const failures = [];
|
|
111
|
-
for (const step of [endStreams, () => listener?.close(), shutdown]) {
|
|
210
|
+
for (const step of [endStreams, () => listener?.close(), shutdown, webhooks.dispose]) {
|
|
112
211
|
await Promise.resolve()
|
|
113
212
|
.then(step)
|
|
114
213
|
.catch((error) => failures.push(error));
|
|
@@ -125,7 +224,8 @@ export function serviceOver(handler, ready, shutdown, endStreams = async () => {
|
|
|
125
224
|
export async function serveAgent(opened, options = {}) {
|
|
126
225
|
const { agent, agentDir, stateRoot, schedules } = opened;
|
|
127
226
|
const closeTimeoutMs = options.closeTimeoutMs ?? CLOSE_DEADLINE_MS;
|
|
128
|
-
const
|
|
227
|
+
const webhooks = webhooksOf(opened);
|
|
228
|
+
const routed = routesFor(agent, stateRoot, opened.channels, webhooks.list);
|
|
129
229
|
// Composed BEFORE anything starts.
|
|
130
230
|
const channels = router(routed.routes);
|
|
131
231
|
const api = options.apiKeys?.length ? apiHandler(agent, { keys: options.apiKeys }) : undefined;
|
|
@@ -200,7 +300,7 @@ export async function serveAgent(opened, options = {}) {
|
|
|
200
300
|
// The rejection is still delivered to callers that await ready.
|
|
201
301
|
ready.catch(() => { });
|
|
202
302
|
return {
|
|
203
|
-
...serviceOver(handler, ready, close, async () => api?.endStreams()),
|
|
303
|
+
...serviceOver(handler, ready, close, async () => api?.endStreams(), webhooks),
|
|
204
304
|
routes: routed.routes,
|
|
205
305
|
agentDir,
|
|
206
306
|
channels: { routes: routed.routeChannels, longConnections: names },
|
|
@@ -8,3 +8,13 @@ export declare function markDevServe(env?: NodeJS.ProcessEnv): void;
|
|
|
8
8
|
/** For a command that is never `dev` (`start`, `deploy`), run from a shell or a tool that a `dev` process spawned. */
|
|
9
9
|
export declare function unmarkDevServe(env?: NodeJS.ProcessEnv): void;
|
|
10
10
|
export declare function servedByDev(env?: NodeJS.ProcessEnv): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Who runs an instance, as an open is told: `dev` serves it, a deployed box (the release manifest's variable) runs it,
|
|
13
|
+
* on AgentCore. The CLI reads its own process's variables for these (`runsAsIn`); an app's instance is none of them.
|
|
14
|
+
*/
|
|
15
|
+
export interface RunsAs {
|
|
16
|
+
dev: boolean;
|
|
17
|
+
deployed: boolean;
|
|
18
|
+
agentcore: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare const NOT_HOSTED: RunsAs;
|
package/dist/serving-command.js
CHANGED