@fastagent-sh/fastagent 0.20.0 → 0.21.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 +5 -1
- package/dist/channels/agentcore-protocol.d.ts +112 -0
- package/dist/channels/agentcore-protocol.js +22 -0
- package/dist/channels/agentcore-service.d.ts +0 -4
- package/dist/channels/agentcore-service.js +1 -6
- package/dist/channels/agentcore-state.d.ts +5 -11
- package/dist/channels/agentcore-state.js +4 -1
- package/dist/channels/agentcore.d.ts +1 -67
- package/dist/channels/agentcore.js +90 -89
- package/dist/channels/control.d.ts +1 -1
- package/dist/channels/control.js +13 -62
- package/dist/channels/discover.d.ts +0 -1
- package/dist/channels/discover.js +1 -3
- 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 +2 -4
- package/dist/channels/feishu/feishu.js +56 -115
- 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/turn-runner.d.ts +59 -0
- package/dist/channels/kit/turn-runner.js +84 -0
- 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/manifest.js +5 -1
- package/dist/channels/slack/onboard.d.ts +5 -5
- package/dist/channels/slack/onboard.js +41 -20
- package/dist/channels/slack/register-webhook.d.ts +3 -2
- package/dist/channels/slack/register-webhook.js +34 -14
- package/dist/channels/slack/scaffold/channel.ts +3 -10
- package/dist/channels/slack/scaffold/slack-send.ts +18 -126
- 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 +81 -22
- package/dist/channels/slack/slack.d.ts +0 -10
- package/dist/channels/slack/slack.js +60 -99
- package/dist/channels/sse.d.ts +4 -0
- package/dist/channels/sse.js +66 -0
- package/dist/channels/telegram/register-webhook.d.ts +6 -9
- package/dist/channels/telegram/register-webhook.js +44 -42
- package/dist/channels/telegram/telegram.js +47 -142
- package/dist/channels/wait-health.js +7 -4
- package/dist/cli/add-feishu.js +3 -10
- package/dist/cli/add-slack.js +7 -20
- package/dist/cli/commands/add.d.ts +0 -1
- package/dist/cli/commands/add.js +25 -37
- 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 +17 -49
- package/dist/cli/commands/deploy.js +39 -618
- package/dist/cli/commands/dev.js +17 -46
- package/dist/cli/commands/fire.js +6 -12
- package/dist/cli/commands/info.js +2 -1
- package/dist/cli/commands/invoke.js +4 -11
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +17 -46
- 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 +36 -19
- package/dist/cli/serve.js +91 -7
- package/dist/cli/shared.d.ts +13 -18
- package/dist/cli/shared.js +20 -4
- package/dist/deploy/agentcore/forwarder.js +250 -0
- package/dist/deploy/agentcore/plan.d.ts +23 -2
- package/dist/deploy/agentcore/plan.js +29 -244
- package/dist/deploy/agentcore/run.d.ts +4 -20
- package/dist/deploy/agentcore/run.js +22 -7
- package/dist/deploy/channel-ingress.js +1 -1
- package/dist/deploy/docker/run.d.ts +17 -3
- package/dist/deploy/docker/run.js +30 -8
- package/dist/deploy/fly/plan.js +7 -0
- package/dist/deploy/fly/run.d.ts +27 -0
- package/dist/deploy/fly/run.js +102 -20
- package/dist/deploy/hosts.d.ts +5 -0
- package/dist/deploy/hosts.js +4 -0
- package/dist/deploy/preflight.js +3 -3
- package/dist/deploy/railway/plan.d.ts +5 -0
- package/dist/deploy/railway/plan.js +7 -0
- package/dist/deploy/railway/run.d.ts +2 -1
- package/dist/deploy/railway/run.js +5 -4
- package/dist/deploy/secrets.js +0 -17
- package/dist/engines/pi/agent-session-factory.d.ts +37 -17
- package/dist/engines/pi/agent-session-factory.js +109 -85
- package/dist/engines/pi/config.d.ts +1 -1
- package/dist/engines/pi/create.d.ts +22 -39
- package/dist/engines/pi/create.js +58 -73
- package/dist/engines/pi/definition.js +8 -8
- package/dist/engines/pi/invoke-session.js +13 -30
- package/dist/engines/pi/open.d.ts +2 -2
- package/dist/engines/pi/open.js +31 -41
- package/dist/engines/pi/retry-event.d.ts +6 -0
- package/dist/engines/pi/retry-event.js +15 -0
- package/dist/engines/pi/session-builder.js +26 -113
- package/dist/engines/pi/session-control.d.ts +10 -18
- package/dist/engines/pi/session-control.js +47 -81
- package/dist/engines/pi/session-settings.d.ts +1 -1
- package/dist/engines/pi/session-settings.js +1 -1
- package/dist/engines/pi/session-store.d.ts +12 -17
- package/dist/engines/pi/session-store.js +18 -40
- package/dist/engines/pi/tool-context.d.ts +3 -3
- package/dist/engines/pi/tool.d.ts +6 -9
- package/dist/engines/pi/tool.js +1 -0
- 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 +2 -0
- package/dist/loader.js +5 -0
- package/dist/log.d.ts +9 -17
- package/dist/log.js +25 -30
- package/dist/paths.d.ts +7 -3
- package/dist/paths.js +17 -4
- package/dist/scaffold/add-channel.d.ts +6 -1
- package/dist/scaffold/add-channel.js +48 -65
- package/dist/schedule/wake-alarm.d.ts +1 -12
- package/dist/schedule/wake-alarm.js +2 -3
- package/dist/service.d.ts +23 -22
- package/dist/service.js +7 -73
- package/dist/session-remote.d.ts +5 -5
- package/dist/session-remote.js +25 -28
- package/dist/session.d.ts +1 -1
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +1 -0
- package/dist/tunnel.d.ts +16 -6
- package/dist/tunnel.js +53 -12
- package/package.json +5 -4
- package/dist/channels/slack/bot-auth.d.ts +0 -15
- package/dist/channels/slack/bot-auth.js +0 -135
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Every other concern lives in its own module, each owning its invariants:
|
|
10
10
|
* - parse.ts pure message parsing: field extraction, prompt envelope, summon/route policy
|
|
11
11
|
* - invoke-turn.ts run one turn: assemble inputs (resolve attachments) + stream `agent.invoke`
|
|
12
|
-
* - turn-
|
|
12
|
+
* - kit/turn-runner.ts the durable-turn lifecycle (accept → dequeue → execute → end) over the queue + store
|
|
13
13
|
* - turn-store.ts durable turn intent (L1): pre-ACK persist, replay a crash-surviving turn
|
|
14
14
|
* - context-buffer.ts un-summoned group discussion, folded into the next answered turn
|
|
15
15
|
* - preview.ts the live-preview pump ("💭 Thinking…" → edits → final answer) + terminal writes
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
* Authored against the public `@fastagent-sh/fastagent` surface only (the contract + the channel-authoring
|
|
23
23
|
* kit: readBodyCapped / text), so it is exactly what a third-party `fastagent-channel-*` package would write.
|
|
24
24
|
*/
|
|
25
|
-
import { timingSafeEqual } from "node:crypto";
|
|
26
25
|
import { isAbsolute, join } from "node:path";
|
|
27
26
|
import { log } from "../../log.js";
|
|
28
27
|
import { readBodyCapped } from "../body.js";
|
|
29
28
|
import { text } from "../respond.js";
|
|
29
|
+
import { secretEquals } from "../secret.js";
|
|
30
30
|
import { invokeTurn } from "./invoke-turn.js";
|
|
31
31
|
import { collectAttachments, createContextBuffer } from "./context-buffer.js";
|
|
32
32
|
import { attachmentSummary, defaultTelegramRoute, extractFiles, extractImages, fromLabel, messageText, ownFiles, ownImages, pickMessage, telegramEnvelope, telegramStop, } from "./parse.js";
|
|
@@ -34,20 +34,13 @@ import { defaultErrorMessage, streamReply } from "./preview.js";
|
|
|
34
34
|
import { ensureStateHome } from "../kit/state.js";
|
|
35
35
|
import { dispatchStop } from "../kit/stop-command.js";
|
|
36
36
|
import { callApi, editMessageText, sendMessage } from "./telegram-api.js";
|
|
37
|
-
import {
|
|
38
|
-
import { commitAnsweredTurn } from "../kit/turn-store.js";
|
|
37
|
+
import { createTurnRunner } from "../kit/turn-runner.js";
|
|
39
38
|
import { discussionBlock } from "../kit/context-buffer.js";
|
|
40
39
|
import { createTurnStore } from "./turn-store.js";
|
|
41
40
|
// Re-export the public surface authored elsewhere, so `@fastagent-sh/fastagent/telegram` keeps one entry point.
|
|
42
41
|
export { defaultTelegramRoute, telegramEnvelope, telegramStop };
|
|
43
42
|
/** Update body cap — Telegram updates are small JSON; 1 MiB is generous and guards a public endpoint. */
|
|
44
43
|
const MAX_UPDATE_BYTES = 1 << 20;
|
|
45
|
-
/** Constant-time compare so the secret-token check leaks no timing signal. */
|
|
46
|
-
function tokenMatches(header, secret) {
|
|
47
|
-
const a = Buffer.from(header);
|
|
48
|
-
const b = Buffer.from(secret);
|
|
49
|
-
return a.length === b.length && timingSafeEqual(a, b);
|
|
50
|
-
}
|
|
51
44
|
/**
|
|
52
45
|
* Build a Telegram bot channel: policy options in, a {@link ChannelModule} out. The framework (or an
|
|
53
46
|
* embedder) mounts it with the context — `telegramChannel(opts)` in `channels/telegram.ts` is the whole
|
|
@@ -111,152 +104,64 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
111
104
|
// Durable turn intent (L1): persist an accepted turn pre-ACK, remove it when the turn ends; a crash
|
|
112
105
|
// leaves it for replay on the next start. See turn-store.ts for the at-least-once semantics.
|
|
113
106
|
const store = createTurnStore(join(stateHome, "turns.json"));
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
107
|
+
const targetOf = (r) => ({ chatId: r.chatId, threadId: r.threadId, replyTo: r.replyTo });
|
|
108
|
+
// Tell the asker when a turn is dropped at the execution ceiling: the chain's end needs a signal, not
|
|
109
|
+
// just an operator log line. Take over the ⏳ "Queued" notice in place if the turn had one (else send
|
|
110
|
+
// fresh) — leaving it pinned at "Queued" while sending a separate failure would double-post. Best-
|
|
111
|
+
// effort, like the queue notices.
|
|
112
|
+
const notifyDropped = (r) => {
|
|
113
|
+
const body = "⚠️ I couldn’t complete an earlier request — please ask again.";
|
|
114
|
+
const sent = r.previewId !== undefined
|
|
115
|
+
? editMessageText(apiBaseUrl, botToken, targetOf(r), r.previewId, body, { html: false })
|
|
116
|
+
: sendMessage(apiBaseUrl, botToken, targetOf(r), body, { html: false }).then(() => { });
|
|
117
|
+
void sent.catch((e) => log.warn(`[telegram] could not notify a dropped turn (session=${r.session}): ${String(e)}`));
|
|
117
118
|
};
|
|
118
|
-
|
|
119
|
-
// the notice message over (rec.previewId) instead of racing it and orphaning a late-arriving notice.
|
|
120
|
-
const notices = new Map();
|
|
121
|
-
const queue = createTurnQueue({
|
|
119
|
+
const runner = createTurnRunner({
|
|
122
120
|
label: "[telegram]",
|
|
121
|
+
store,
|
|
122
|
+
buffer,
|
|
123
|
+
toStored: ({ previewId: _live, ...intent }) => ({ ...intent, attempts: 0 }),
|
|
124
|
+
fromStored: ({ attempts: _a, ...intent }) => ({ ...intent, previewId: undefined }),
|
|
125
|
+
bufferKey: (rec) => rec.placeKey,
|
|
126
|
+
where: (rec) => `chat=${rec.chatId}${rec.threadId !== undefined ? ` thread=${rec.threadId}` : ""}`,
|
|
123
127
|
// Queue feedback: when this session already has a turn running/queued, a silent wait reads as "the
|
|
124
128
|
// bot ignored me" once the current turn runs long — tell the asker NOW (reply-quoted, so it is
|
|
125
129
|
// clear whose ask is queued). Best-effort and post-ACK: a failed notice is a log line, never a
|
|
126
|
-
// failed update. The turn's live preview then edits this same message in place.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
threadId: rec.threadId,
|
|
131
|
-
replyTo: rec.replyTo,
|
|
132
|
-
};
|
|
133
|
-
notices.set(rec.id, sendMessage(apiBaseUrl, botToken, target, "⏳ Queued — I’ll start once the current task finishes.", {
|
|
130
|
+
// failed update. The turn's live preview then edits this same message in place. The runner holds
|
|
131
|
+
// this same `rec` object, so mutating it here (awaited at dequeue) hands the turn its preview id.
|
|
132
|
+
onQueuedBehind: (rec) => ({
|
|
133
|
+
done: sendMessage(apiBaseUrl, botToken, targetOf(rec), "⏳ Queued — I’ll start once the current task finishes.", {
|
|
134
134
|
html: false,
|
|
135
|
-
}).then(
|
|
136
|
-
// The runner holds this same `rec` object, so mutating it here (gated by the notices await at
|
|
137
|
-
// dequeue below) is what hands the turn its preview message id.
|
|
138
|
-
(id) => {
|
|
135
|
+
}).then((id) => {
|
|
139
136
|
if (id !== undefined)
|
|
140
137
|
rec.previewId = id;
|
|
141
|
-
}, (e) => log.warn(`[telegram] queue notice failed (the turn still runs): ${String(e)}`))
|
|
142
|
-
},
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
await notices.get(rec.id);
|
|
152
|
-
notices.delete(rec.id);
|
|
153
|
-
// Count this execution against the durable record (poison-turn ceiling) before running it again.
|
|
154
|
-
const decision = store.startAttempt(rec.id);
|
|
155
|
-
if (decision === "exceeded") {
|
|
156
|
-
// Started the ceiling's worth of times without finishing (turn-store.ts MAX_TURN_ATTEMPTS) —
|
|
157
|
-
// tell the asker (reusing its ⏳ notice if any), drop it.
|
|
158
|
-
notifyDropped(rec);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
if (decision === "defer") {
|
|
162
|
-
// Couldn't record the attempt (disk failure): skip this cycle. A restart replays it, so no notify
|
|
163
|
-
// (telling the asker to re-ask would double-answer once the deferred turn runs). Two accepted
|
|
164
|
-
// edges of this rare disk-failure corner: (1) the session chain proceeds to the next queued turn,
|
|
165
|
-
// so a deferred turn can replay AFTER its successors — a per-session FIFO reorder; (2) its ⏳ notice
|
|
166
|
-
// (if it was queued behind another turn) now falsely reads "Queued", so delete it best-effort — the
|
|
167
|
-
// eventual replay sends a fresh preview, and leaving it would orphan a stale message above that.
|
|
168
|
-
if (rec.previewId !== undefined) {
|
|
169
|
-
void callApi(apiBaseUrl, botToken, "deleteMessage", {
|
|
170
|
-
chat_id: rec.chatId,
|
|
171
|
-
message_id: rec.previewId,
|
|
172
|
-
}).catch(() => { });
|
|
173
|
-
}
|
|
174
|
-
return;
|
|
138
|
+
}, (e) => log.warn(`[telegram] queue notice failed (the turn still runs): ${String(e)}`)),
|
|
139
|
+
}),
|
|
140
|
+
// Its ⏳ notice (if any) now falsely reads "Queued": delete it best-effort — the eventual replay
|
|
141
|
+
// sends a fresh preview, and leaving it would orphan a stale message above that.
|
|
142
|
+
onDeferred: (rec) => {
|
|
143
|
+
if (rec.previewId !== undefined) {
|
|
144
|
+
void callApi(apiBaseUrl, botToken, "deleteMessage", {
|
|
145
|
+
chat_id: rec.chatId,
|
|
146
|
+
message_id: rec.previewId,
|
|
147
|
+
}).catch(() => { });
|
|
175
148
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const { text: recent, consumed } = buffer.peek(rec.placeKey);
|
|
182
|
-
const prompt = `${discussionBlock(recent)}${rec.baseText}`;
|
|
183
|
-
const buffered = collectAttachments(consumed, {
|
|
149
|
+
},
|
|
150
|
+
notifyDropped,
|
|
151
|
+
execute: (rec, discussion, onCompleted) => streamReply(invokeTurn(agent, rec.session, `${discussionBlock(discussion.text)}${rec.baseText}`, { api: apiBaseUrl, botToken, chatId: rec.chatId, filesDir: join(stateHome, "files") }, {
|
|
152
|
+
primary: { imageFileIds: rec.imageFileIds, fileIds: rec.fileIds },
|
|
153
|
+
buffered: collectAttachments(discussion.consumed, {
|
|
184
154
|
files: new Set(rec.fileIds),
|
|
185
155
|
images: new Set(rec.imageFileIds),
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
chatId: rec.chatId,
|
|
189
|
-
threadId: rec.threadId,
|
|
190
|
-
replyTo: rec.replyTo,
|
|
191
|
-
};
|
|
192
|
-
try {
|
|
193
|
-
await streamReply(invokeTurn(agent, rec.session, prompt, {
|
|
194
|
-
api: apiBaseUrl,
|
|
195
|
-
botToken,
|
|
196
|
-
chatId: rec.chatId,
|
|
197
|
-
filesDir: join(stateHome, "files"),
|
|
198
|
-
}, {
|
|
199
|
-
primary: {
|
|
200
|
-
imageFileIds: rec.imageFileIds,
|
|
201
|
-
fileIds: rec.fileIds,
|
|
202
|
-
},
|
|
203
|
-
buffered,
|
|
204
|
-
}, () => commitAnsweredTurn(store, buffer, { id: rec.id, bufferKey: rec.placeKey, consumed })), apiBaseUrl, botToken, target, formatError, rec.previewId);
|
|
205
|
-
log.info(`[telegram] turn done: turn=${rec.id} session=${rec.session} (${Date.now() - startedAt}ms)`);
|
|
206
|
-
}
|
|
207
|
-
catch (error) {
|
|
208
|
-
log.error(`[telegram] turn failed: turn=${rec.id} session=${rec.session} (${Date.now() - startedAt}ms): ${String(error)}`);
|
|
209
|
-
}
|
|
210
|
-
finally {
|
|
211
|
-
// Fallback removal for the caught-error paths (a `failed` event or a transport throw): those
|
|
212
|
-
// never reach the completed hook above, which is where a completed turn removes its intent in
|
|
213
|
-
// order. Idempotent — a second remove after the completed hook is a no-op. Only an INTERRUPTED run
|
|
214
|
-
// (this finally never runs — a crash or a SIGTERM deploy, no graceful drain) leaves the record for
|
|
215
|
-
// replay; a transport throw is dropped, not retried (safe retry needs an L2 delivery key).
|
|
216
|
-
store.remove(rec.id);
|
|
217
|
-
}
|
|
218
|
-
},
|
|
156
|
+
}),
|
|
157
|
+
}, onCompleted), apiBaseUrl, botToken, targetOf(rec), formatError, rec.previewId),
|
|
219
158
|
});
|
|
220
|
-
|
|
221
|
-
// crash-surviving turn WITHOUT re-persisting (it is already on disk with a bumped attempt count).
|
|
222
|
-
const submit = (rec, persist) => {
|
|
223
|
-
if (persist)
|
|
224
|
-
store.add(toStored(rec)); // pre-ACK: a failed write throws → webhook 500 → redeliver
|
|
225
|
-
queue.accept(rec);
|
|
226
|
-
};
|
|
227
|
-
// Tell the asker when a turn is dropped at the execution ceiling: the chain's end needs a signal, not
|
|
228
|
-
// just an operator log line. Take over the ⏳ "Queued" notice in place if the turn had one (else send
|
|
229
|
-
// fresh) — leaving it pinned at "Queued" while sending a separate failure would double-post. Best-
|
|
230
|
-
// effort, like the queue notices.
|
|
231
|
-
const notifyDropped = (r) => {
|
|
232
|
-
const body = "⚠️ I couldn’t complete an earlier request — please ask again.";
|
|
233
|
-
const target = {
|
|
234
|
-
chatId: r.chatId,
|
|
235
|
-
threadId: r.threadId,
|
|
236
|
-
replyTo: r.replyTo,
|
|
237
|
-
};
|
|
238
|
-
const sent = r.previewId !== undefined
|
|
239
|
-
? editMessageText(apiBaseUrl, botToken, target, r.previewId, body, {
|
|
240
|
-
html: false,
|
|
241
|
-
})
|
|
242
|
-
: sendMessage(apiBaseUrl, botToken, target, body, {
|
|
243
|
-
html: false,
|
|
244
|
-
}).then(() => { });
|
|
245
|
-
void sent.catch((e) => log.warn(`[telegram] could not notify a dropped turn (session=${r.session}): ${String(e)}`));
|
|
246
|
-
};
|
|
247
|
-
// Re-enqueue turns a prior crash left mid-flight (ACKed but unfinished). Synchronous at construction:
|
|
248
|
-
// the queue runs them on the next tick, once this factory returns and the event loop turns. The
|
|
249
|
-
// execution ceiling is enforced per turn at dequeue (run), not here — a never-run turn keeps its budget.
|
|
250
|
-
const recovered = store.recover();
|
|
251
|
-
if (recovered.length > 0)
|
|
252
|
-
log.info(`[telegram] recovering ${recovered.length} unfinished turn(s) from a prior run`);
|
|
253
|
-
for (const { attempts: _a, ...intent } of recovered)
|
|
254
|
-
submit({ ...intent, previewId: undefined }, false);
|
|
159
|
+
runner.recover();
|
|
255
160
|
const handler = async (req) => {
|
|
256
161
|
if (req.method !== "POST")
|
|
257
162
|
return text("POST only\n", 405);
|
|
258
163
|
// Fail closed: a missing/wrong secret token is 401, never routed.
|
|
259
|
-
if (!
|
|
164
|
+
if (!secretEquals(req.headers.get("x-telegram-bot-api-secret-token"), secretToken)) {
|
|
260
165
|
return text("invalid secret token\n", 401);
|
|
261
166
|
}
|
|
262
167
|
const body = await readBodyCapped(req, MAX_UPDATE_BYTES);
|
|
@@ -331,7 +236,7 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
331
236
|
const fileIds = extractFiles(m);
|
|
332
237
|
if (baseText.trim() !== "" || imageFileIds.length > 0 || fileIds.length > 0) {
|
|
333
238
|
// Everything the turn needs, as a plain record; persisted pre-ACK then run serially per session.
|
|
334
|
-
submit({
|
|
239
|
+
runner.submit({
|
|
335
240
|
id: `${update.update_id}`,
|
|
336
241
|
session,
|
|
337
242
|
placeKey,
|
|
@@ -348,7 +253,7 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
348
253
|
// Test/observability seam: await the fire-and-forget turns this handler enqueues. Inert in production
|
|
349
254
|
// (nothing reads it; the runtime never drains — see turn-queue), it lets a test await a turn
|
|
350
255
|
// deterministically instead of polling for side effects to settle.
|
|
351
|
-
handler.turnsIdle = () =>
|
|
256
|
+
handler.turnsIdle = () => runner.idle();
|
|
352
257
|
return { "POST /telegram": handler };
|
|
353
258
|
};
|
|
354
259
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Readiness probe
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Readiness probe for a server THIS process can reach directly — the local port `deploy docker --run`
|
|
3
|
+
* just published, and the live probes' own origins.
|
|
4
|
+
*
|
|
5
|
+
* NOT a probe for a public URL a platform must reach: a freshly minted hostname (a quick tunnel, a
|
|
6
|
+
* fresh deploy) is routinely unreachable from here for a minute or more while the platform reaches it
|
|
7
|
+
* fine, and polling it hard from t+0 makes that worse (#421). The webhook registrars therefore let the
|
|
8
|
+
* platform's own URL verification be the probe, retried, instead of gating on this.
|
|
6
9
|
*/
|
|
7
10
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
8
11
|
/**
|
package/dist/cli/add-feishu.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { readFile } from "node:fs/promises";
|
|
13
13
|
import { isCancel, log as clackLog, password, text as clackText } from "@clack/prompts";
|
|
14
14
|
import { bootstrapFeishuVerificationToken } from "../channels/feishu/bootstrap-token.js";
|
|
15
|
-
import { FEISHU_GROUP_CONTEXT_SCOPE, FEISHU_CONTEXT_ONBOARDING_SCOPES, FEISHU_MESSAGE_READ_SCOPE, scopeSatisfied, } from "../channels/feishu/setup-mode.js";
|
|
15
|
+
import { FEISHU_GROUP_CONTEXT_SCOPE, FEISHU_CONTEXT_ONBOARDING_SCOPES, FEISHU_MESSAGE_READ_SCOPE, feishuAppAddons, scopeSatisfied, } from "../channels/feishu/setup-mode.js";
|
|
16
16
|
import { cloudFor } from "../channels/feishu/cloud.js";
|
|
17
17
|
import { createFeishuApi, isFeishuConfigApiMissing, isTransientFeishuRegistrationError, } from "../channels/feishu/feishu-api.js";
|
|
18
18
|
import { registerFeishuApp } from "../channels/feishu/register-app.js";
|
|
@@ -233,16 +233,9 @@ async function createFeishuAppFlow(target, existing, ingress, groupBehavior) {
|
|
|
233
233
|
const app = await registerFeishuApp({
|
|
234
234
|
name: "{user}'s agent", // the platform expands {user} to the confirming user's name; editable on the page
|
|
235
235
|
desc: "Served by fastagent",
|
|
236
|
-
// The agent template alone is not enough to SERVE
|
|
237
|
-
// context-aware group scope setup) demand application:application:patch, and the app must subscribe
|
|
238
|
-
// the receive event. Addons merge those BASE capabilities onto the confirm page; sensitive group
|
|
236
|
+
// The agent template alone is not enough to SERVE (see feishuAppAddons). Sensitive group
|
|
239
237
|
// permission approval and version publishing remain explicit console work.
|
|
240
|
-
addons:
|
|
241
|
-
...(ingress === "webhook" || (groupBehavior.behavior === "context" && groupBehavior.explicit)
|
|
242
|
-
? { scopes: { tenant: ["application:application:patch"] } }
|
|
243
|
-
: {}),
|
|
244
|
-
events: { items: { tenant: ["im.message.receive_v1"] } },
|
|
245
|
-
},
|
|
238
|
+
addons: feishuAppAddons(),
|
|
246
239
|
onVerificationUrl: ({ url, expiresInS }) => {
|
|
247
240
|
console.error(`\n Opening the confirmation link in your browser (or open it in Feishu / render it as a QR code) — valid for ${Math.round(expiresInS / 60)} minutes:\n\n ${url}\n\n waiting for confirmation… (keep this running — the credentials are delivered here)`);
|
|
248
241
|
openExternalUrl(url); // best-effort, like `login` — the URL above is the fallback
|
package/dist/cli/add-slack.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { isCancel, log as clackLog, password, select, text as clackText } from "@clack/prompts";
|
|
4
|
-
import { waitForHealth } from "../channels/wait-health.js";
|
|
5
4
|
import { dotEnvPath, parseEnvContent } from "../env.js";
|
|
6
5
|
import { openExternalUrl } from "../open-url.js";
|
|
7
6
|
import { installProxyFetch } from "../proxy.js";
|
|
@@ -41,14 +40,7 @@ export async function onboardSlackInternalApp(input) {
|
|
|
41
40
|
return new Map();
|
|
42
41
|
throw error;
|
|
43
42
|
});
|
|
44
|
-
const missingRuntime = [
|
|
45
|
-
"SLACK_BOT_TOKEN",
|
|
46
|
-
"SLACK_BOT_REFRESH_TOKEN",
|
|
47
|
-
"SLACK_BOT_TOKEN_EXPIRES_AT",
|
|
48
|
-
"SLACK_CLIENT_ID",
|
|
49
|
-
"SLACK_CLIENT_SECRET",
|
|
50
|
-
"SLACK_SIGNING_SECRET",
|
|
51
|
-
].filter((name) => !((process.env[name] ?? env.get(name))?.trim() ?? ""));
|
|
43
|
+
const missingRuntime = ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"].filter((name) => !((process.env[name] ?? env.get(name))?.trim() ?? ""));
|
|
52
44
|
if (missingRuntime.length > 0) {
|
|
53
45
|
throw new Error(`Slack app ${state.appId ?? "(unknown)"} is installed but ${dotEnvPath(input.target)} is missing ` +
|
|
54
46
|
`${missingRuntime.join(", ")} — ` +
|
|
@@ -168,20 +160,16 @@ export async function onboardSlackInternalApp(input) {
|
|
|
168
160
|
const redirectUrl = `${tunnel.url}${server.redirectPath}`;
|
|
169
161
|
console.error(`[fastagent] temporary Slack setup tunnel ready → ${tunnel.url}`);
|
|
170
162
|
try {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
163
|
+
// No local readiness probe: Slack challenges requestUrl from ITS network during app creation, and
|
|
164
|
+
// that is the reachability that matters (#421) — this machine often cannot reach a fresh tunnel
|
|
165
|
+
// hostname for a minute. onboardSlackApp retries the create while Slack cannot verify it yet.
|
|
174
166
|
await onboardSlackApp({ stateRoot: input.stateRoot, state, requestUrl, redirectUrl }, {
|
|
175
167
|
note: (message) => clackLog.info(message),
|
|
176
168
|
openUrl: openExternalUrl,
|
|
177
169
|
waitForOAuth: () => server.waitForOAuth(),
|
|
178
|
-
writeRuntimeSecrets: async ({ botToken,
|
|
170
|
+
writeRuntimeSecrets: async ({ botToken, signingSecret }) => {
|
|
179
171
|
const values = {
|
|
180
172
|
...(botToken ? { SLACK_BOT_TOKEN: botToken } : {}),
|
|
181
|
-
...(botRefreshToken ? { SLACK_BOT_REFRESH_TOKEN: botRefreshToken } : {}),
|
|
182
|
-
...(botTokenExpiresAt ? { SLACK_BOT_TOKEN_EXPIRES_AT: String(botTokenExpiresAt) } : {}),
|
|
183
|
-
...(clientId ? { SLACK_CLIENT_ID: clientId } : {}),
|
|
184
|
-
...(clientSecret ? { SLACK_CLIENT_SECRET: clientSecret } : {}),
|
|
185
173
|
...(signingSecret ? { SLACK_SIGNING_SECRET: signingSecret } : {}),
|
|
186
174
|
};
|
|
187
175
|
if (Object.keys(values).length > 0) {
|
|
@@ -189,9 +177,8 @@ export async function onboardSlackInternalApp(input) {
|
|
|
189
177
|
}
|
|
190
178
|
},
|
|
191
179
|
});
|
|
192
|
-
|
|
193
|
-
console.error(`[fastagent]
|
|
194
|
-
"replace the temporary Events API URL automatically");
|
|
180
|
+
// What happened, once. What to do next is `add`'s "next steps" block.
|
|
181
|
+
console.error(`[fastagent] Slack app installed; credentials written to ${dotEnvPath(input.target)}`);
|
|
195
182
|
}
|
|
196
183
|
finally {
|
|
197
184
|
tunnel.close();
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type ChannelKind } from "../../scaffold/add-channel.ts";
|
|
2
2
|
/** `fastagent add <kind> [dir]`: scaffold `channels/<kind>.ts` — the adapter import plus a starter `on()`. */
|
|
3
3
|
export declare function runAddChannel(channelKind: ChannelKind, dirArg: string, opts: {
|
|
4
|
-
createApp?: boolean;
|
|
5
4
|
ingress?: string;
|
|
6
5
|
groupBehavior?: string;
|
|
7
6
|
onboard?: boolean;
|
package/dist/cli/commands/add.js
CHANGED
|
@@ -8,9 +8,9 @@ import { join, relative, resolve } from "node:path";
|
|
|
8
8
|
import { isCancel, select } from "@clack/prompts";
|
|
9
9
|
import { onboardFeishuCloudApp } from "../add-feishu.js";
|
|
10
10
|
import { dotEnvPath, loadDotEnv } from "../../env.js";
|
|
11
|
-
import { resolveStateRoot, SECRETS_DIRNAME, isUnderDir, displayPath
|
|
11
|
+
import { resolveStateRoot, SECRETS_DIRNAME, isUnderDir, displayPath } from "../../paths.js";
|
|
12
12
|
import { detectRuntime, readPackageJson } from "../../runtime.js";
|
|
13
|
-
import { appendChannelDotEnv, appendChannelEnv, assertChannelReady, channelExists, channelSetup, scaffoldChannel, } from "../../scaffold/add-channel.js";
|
|
13
|
+
import { appendChannelDotEnv, appendChannelEnv, assertChannelReady, channelExists, channelSetup, scaffoldChannel, scaffoldCompanionTools, } from "../../scaffold/add-channel.js";
|
|
14
14
|
import { vendorSkill } from "../../scaffold/vendor-skill.js";
|
|
15
15
|
import { failStartup, failUsage, placementOrExit } from "../fail.js";
|
|
16
16
|
/** `fastagent add <kind> [dir]`: scaffold `channels/<kind>.ts` — the adapter import plus a starter `on()`. */
|
|
@@ -33,41 +33,25 @@ export async function runAddChannel(channelKind, dirArg, opts) {
|
|
|
33
33
|
const inAgent = (p) => (agentFromCwd === undefined ? p : join(agentFromCwd, p));
|
|
34
34
|
const envPath = dotEnvPath(target);
|
|
35
35
|
const envLabel = isUnderDir(envPath, target) ? inAgent(relative(target, envPath)) : envPath;
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
if (channelKind === "feishu") {
|
|
40
|
-
console.error(`[fastagent] note: --create-app is retired — \`add feishu\` creates the app by default`);
|
|
41
|
-
}
|
|
42
|
-
else if (channelKind === "lark") {
|
|
43
|
-
failStartup(new Error("--create-app is retired — `add lark` now opens the developer console and guides credential setup by default"));
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
failStartup(new Error("--create-app is retired — app creation is the default behavior of `add feishu`"));
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
// Preconditions before the write, so a refusal is side-effect-free. slack/feishu/lark are exceptions:
|
|
50
|
-
// their add is scaffold + ONBOARD THE APP, so an existing scaffold skips the write and continues (a
|
|
51
|
-
// failed/cancelled app or OAuth flow must be re-runnable without hand-deleting authored glue).
|
|
36
|
+
// An existing channel file is authored glue: kept, never rewritten. The add continues past it — the
|
|
37
|
+
// companion tools below are refreshed, and slack/feishu/lark resume a failed/cancelled app or OAuth
|
|
38
|
+
// flow — so re-running `add <kind>` never requires hand-deleting the glue.
|
|
52
39
|
const file = join(target, "channels", `${channelKind}.ts`);
|
|
53
|
-
const slackToolFile = join(target, "tools", "slack-send.ts");
|
|
54
|
-
const slackToolExisted = channelKind === "slack" ? await exists(slackToolFile) : false;
|
|
55
40
|
const existsAlready = await channelExists(target, channelKind).catch(failStartup);
|
|
56
41
|
const ingress = await resolveIngress(channelKind, file, existsAlready, opts.ingress);
|
|
57
42
|
const groupBehavior = await resolveGroupBehavior(channelKind, opts.groupBehavior);
|
|
58
43
|
if (existsAlready) {
|
|
59
|
-
if (channelKind !== "slack" && channelKind !== "feishu" && channelKind !== "lark") {
|
|
60
|
-
failStartup(new Error(`${relative(target, file)} already exists — edit it, or remove it to re-scaffold`));
|
|
61
|
-
}
|
|
62
44
|
console.error(`[fastagent] ${relative(target, file)} already exists — keeping it`);
|
|
63
45
|
}
|
|
64
46
|
else {
|
|
65
47
|
await assertChannelReady(target).catch(failStartup);
|
|
66
|
-
await scaffoldChannel(target, channelKind, { ingress
|
|
48
|
+
await scaffoldChannel(target, channelKind, { ingress }).catch(failStartup);
|
|
67
49
|
console.error(`[fastagent] created ${relative(target, file)}`);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
50
|
+
}
|
|
51
|
+
// Companion tools are the package's, not authored glue: written on every add, so an upgraded
|
|
52
|
+
// package reaches an existing agent by re-running `add <kind>` (slack: `--no-onboard` skips the prompts).
|
|
53
|
+
for (const tool of await scaffoldCompanionTools(target, channelKind).catch(failStartup)) {
|
|
54
|
+
console.error(`[fastagent] wrote ${relative(target, tool)}`);
|
|
71
55
|
}
|
|
72
56
|
if (await appendChannelEnv(target, channelKind, ingress).catch(failStartup)) {
|
|
73
57
|
console.error(`[fastagent] added ${channelKind} env vars to ${inAgent(join(SECRETS_DIRNAME, ".env.example"))}`);
|
|
@@ -93,9 +77,9 @@ export async function runAddChannel(channelKind, dirArg, opts) {
|
|
|
93
77
|
const env = setup.env;
|
|
94
78
|
const steps = channelKind === "slack" && opts.onboard !== false
|
|
95
79
|
? [
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"invite the app to
|
|
80
|
+
// No line about the Request URL: the `dev --tunnel` line below is the whole instruction, and
|
|
81
|
+
// FastAgent sets that URL itself when the agent first runs.
|
|
82
|
+
"invite the app to each channel it should read",
|
|
99
83
|
"the agent can send messages or files by calling the scaffolded {tools}/slack-send.ts tool",
|
|
100
84
|
]
|
|
101
85
|
: setup.steps;
|
|
@@ -137,7 +121,7 @@ export async function runAddChannel(channelKind, dirArg, opts) {
|
|
|
137
121
|
console.error(` fastagent dev # no public URL or tunnel required`);
|
|
138
122
|
}
|
|
139
123
|
else if (channelKind === "slack") {
|
|
140
|
-
console.error(` fastagent dev --tunnel #
|
|
124
|
+
console.error(` fastagent dev --tunnel # ${opts.onboard === false ? "serve locally + print the URL to paste into the Slack app's Event Subscriptions" : "start the agent — then message it in Slack"}`);
|
|
141
125
|
}
|
|
142
126
|
else if (channelKind === "github") {
|
|
143
127
|
console.error(` fastagent dev --tunnel # serve locally + print the URL for manual GitHub webhook setup`);
|
|
@@ -183,16 +167,20 @@ async function resolveIngress(kind, file, existsAlready, raw) {
|
|
|
183
167
|
}
|
|
184
168
|
const answer = await select({
|
|
185
169
|
message: `How should ${kind === "feishu" ? "Feishu" : "Lark"} deliver events?`,
|
|
170
|
+
// The default must match the non-interactive branch above. Webhook is the cheaper side to be
|
|
171
|
+
// wrong on: its credentials are a superset of websocket's (App ID/Secret plus the Verification
|
|
172
|
+
// Token), and that token has no read API — a websocket app moving to webhook must re-acquire it.
|
|
173
|
+
initialValue: "webhook",
|
|
186
174
|
options: [
|
|
187
|
-
{
|
|
188
|
-
value: "websocket",
|
|
189
|
-
label: "WebSocket long connection",
|
|
190
|
-
hint: "no public URL; requires an always-on process",
|
|
191
|
-
},
|
|
192
175
|
{
|
|
193
176
|
value: "webhook",
|
|
194
177
|
label: "Webhook endpoint",
|
|
195
|
-
hint: "supports scale-to-zero; requires a public HTTPS URL",
|
|
178
|
+
hint: "works on every deploy target; supports scale-to-zero; requires a public HTTPS URL",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
value: "websocket",
|
|
182
|
+
label: "WebSocket long connection",
|
|
183
|
+
hint: "no public URL; needs an always-on process (not on AgentCore)",
|
|
196
184
|
},
|
|
197
185
|
],
|
|
198
186
|
});
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
/** `fastagent chat [dir]`: open the SAME assembled agent in pi's interactive TUI. */
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { installProxyFetch } from "../../proxy.js";
|
|
5
|
-
import { failStartup, placementOrExit } from "../fail.js";
|
|
6
|
-
import { resolveFirstRunModel } from "../shared.js";
|
|
2
|
+
import { failStartup } from "../fail.js";
|
|
3
|
+
import { enterAgentCommand } from "../shared.js";
|
|
7
4
|
export async function runChat(dirArg, opts) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// other command (the shared session builder injects it — see engines/pi/session-builder.ts), so the
|
|
13
|
-
// credential-annotated catalog and inline login apply here too.
|
|
14
|
-
await resolveFirstRunModel(placement.agentDir, { model: opts.model, authPath: opts.authPath });
|
|
5
|
+
// Chat authenticates through fastagent's credential store like every other command (the shared
|
|
6
|
+
// session builder injects it — see engines/pi/session-builder.ts), so the first-run picker and its
|
|
7
|
+
// inline login apply here too.
|
|
8
|
+
const placement = await enterAgentCommand(dirArg, opts);
|
|
15
9
|
// Run the chat process AT the workspace: pi resolves a session's cwd as `header.cwd ?? process.cwd()`,
|
|
16
10
|
// so aligning process.cwd() with the workspace keeps a cwd-less session on it. Paths are absolute.
|
|
17
11
|
process.chdir(placement.workspace);
|