@fastagent-sh/fastagent 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -6
- package/dist/agent.d.ts +20 -0
- package/dist/agent.js +9 -0
- package/dist/channels/agentcore-limits.d.ts +7 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-state.d.ts +83 -0
- package/dist/channels/agentcore-state.js +258 -0
- package/dist/channels/agentcore.d.ts +98 -0
- package/dist/channels/agentcore.js +260 -0
- package/dist/channels/busy.d.ts +23 -0
- package/dist/channels/busy.js +53 -0
- package/dist/channels/context-buffer.d.ts +35 -0
- package/dist/channels/context-buffer.js +103 -0
- package/dist/channels/control.d.ts +28 -0
- package/dist/channels/control.js +214 -0
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +15 -13
- package/dist/channels/feishu/context-buffer.js +62 -69
- package/dist/channels/feishu/feishu-api.d.ts +11 -3
- package/dist/channels/feishu/feishu-api.js +24 -3
- package/dist/channels/feishu/feishu.d.ts +27 -27
- package/dist/channels/feishu/feishu.js +318 -207
- package/dist/channels/feishu/invoke-turn.d.ts +7 -20
- package/dist/channels/feishu/invoke-turn.js +44 -79
- package/dist/channels/feishu/model.d.ts +2 -2
- package/dist/channels/feishu/normalize.d.ts +2 -1
- package/dist/channels/feishu/normalize.js +0 -1
- package/dist/channels/feishu/parse.d.ts +22 -8
- package/dist/channels/feishu/parse.js +24 -7
- package/dist/channels/feishu/preview.d.ts +4 -7
- package/dist/channels/feishu/preview.js +27 -142
- package/dist/channels/feishu/register-app.d.ts +2 -1
- package/dist/channels/feishu/register-webhook.d.ts +2 -1
- package/dist/channels/feishu/scaffold/channel.ts +9 -8
- package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
- package/dist/channels/feishu/setup-mode.d.ts +37 -0
- package/dist/channels/feishu/setup-mode.js +28 -0
- package/dist/channels/feishu/ws-ingress.d.ts +28 -0
- package/dist/channels/feishu/ws-ingress.js +136 -0
- package/dist/channels/github/github.js +16 -7
- package/dist/channels/http.d.ts +14 -0
- package/dist/channels/http.js +36 -3
- package/dist/channels/invoke-turn-kit.d.ts +65 -0
- package/dist/channels/invoke-turn-kit.js +87 -0
- package/dist/channels/lark/lark.d.ts +4 -2
- package/dist/channels/lark/lark.js +4 -1
- package/dist/channels/lark/onboard.d.ts +8 -4
- package/dist/channels/lark/onboard.js +8 -0
- package/dist/channels/lark/scaffold/channel.ts +9 -8
- package/dist/channels/lark/scaffold/lark-send.ts +6 -4
- package/dist/channels/preview-kit.d.ts +115 -0
- package/dist/channels/preview-kit.js +184 -0
- package/dist/channels/seen.d.ts +5 -0
- package/dist/channels/seen.js +35 -0
- package/dist/channels/slack/bot-auth.d.ts +15 -0
- package/dist/channels/slack/bot-auth.js +146 -0
- package/dist/channels/slack/config-api.d.ts +60 -0
- package/dist/channels/slack/config-api.js +149 -0
- package/dist/channels/slack/context-buffer.d.ts +24 -0
- package/dist/channels/slack/context-buffer.js +37 -0
- package/dist/channels/slack/invoke-turn.d.ts +19 -0
- package/dist/channels/slack/invoke-turn.js +63 -0
- package/dist/channels/slack/manifest.d.ts +49 -0
- package/dist/channels/slack/manifest.js +69 -0
- package/dist/channels/slack/model.d.ts +67 -0
- package/dist/channels/slack/model.js +2 -0
- package/dist/channels/slack/onboard.d.ts +41 -0
- package/dist/channels/slack/onboard.js +120 -0
- package/dist/channels/slack/onboarding-state.d.ts +31 -0
- package/dist/channels/slack/onboarding-state.js +69 -0
- package/dist/channels/slack/parse.d.ts +38 -0
- package/dist/channels/slack/parse.js +124 -0
- package/dist/channels/slack/preview.d.ts +23 -0
- package/dist/channels/slack/preview.js +403 -0
- package/dist/channels/slack/reaction.d.ts +24 -0
- package/dist/channels/slack/reaction.js +62 -0
- package/dist/channels/slack/register-webhook.d.ts +10 -0
- package/dist/channels/slack/register-webhook.js +49 -0
- package/dist/channels/slack/scaffold/channel.ts +33 -0
- package/dist/channels/slack/scaffold/slack-send.ts +171 -0
- package/dist/channels/slack/setup-server.d.ts +17 -0
- package/dist/channels/slack/setup-server.js +103 -0
- package/dist/channels/slack/slack-api.d.ts +57 -0
- package/dist/channels/slack/slack-api.js +399 -0
- package/dist/channels/slack/slack.d.ts +51 -0
- package/dist/channels/slack/slack.js +496 -0
- package/dist/channels/slack/welcomed.d.ts +5 -0
- package/dist/channels/slack/welcomed.js +32 -0
- package/dist/channels/state.d.ts +11 -4
- package/dist/channels/state.js +22 -15
- package/dist/channels/stop-command.d.ts +6 -0
- package/dist/channels/stop-command.js +36 -0
- package/dist/channels/tasks.d.ts +7 -0
- package/dist/channels/tasks.js +25 -0
- package/dist/channels/telegram/context-buffer.d.ts +8 -17
- package/dist/channels/telegram/context-buffer.js +6 -85
- package/dist/channels/telegram/invoke-turn.d.ts +5 -22
- package/dist/channels/telegram/invoke-turn.js +11 -58
- package/dist/channels/telegram/parse.d.ts +0 -7
- package/dist/channels/telegram/parse.js +4 -2
- package/dist/channels/telegram/preview.d.ts +4 -7
- package/dist/channels/telegram/preview.js +24 -142
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
- package/dist/channels/telegram/telegram.js +24 -10
- package/dist/channels/{feishu/text.d.ts → text.d.ts} +15 -1
- package/dist/channels/{feishu/text.js → text.js} +15 -1
- package/dist/channels/thread-participants.d.ts +21 -0
- package/dist/channels/thread-participants.js +132 -0
- package/dist/channels/turn-queue.js +8 -1
- package/dist/channels/turn-store.d.ts +1 -1
- package/dist/channels/turn-store.js +2 -3
- package/dist/cli/add-feishu.d.ts +30 -0
- package/dist/cli/add-feishu.js +366 -0
- package/dist/cli/add-slack.d.ts +11 -0
- package/dist/cli/add-slack.js +199 -0
- package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
- package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
- package/dist/cli/commands/add.d.ts +4 -0
- package/dist/cli/commands/add.js +191 -60
- package/dist/cli/commands/attach.d.ts +82 -0
- package/dist/cli/commands/attach.js +563 -0
- package/dist/cli/commands/chat.d.ts +1 -0
- package/dist/cli/commands/chat.js +12 -7
- package/dist/cli/commands/deploy.d.ts +3 -1
- package/dist/cli/commands/deploy.js +384 -82
- package/dist/cli/commands/dev.js +34 -26
- package/dist/cli/commands/fire.js +17 -18
- package/dist/cli/commands/info.js +36 -29
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.js +65 -53
- package/dist/cli/commands/invoke.js +11 -9
- package/dist/cli/commands/login.js +48 -45
- package/dist/cli/commands/models.js +1 -1
- package/dist/cli/commands/schedule.js +6 -8
- package/dist/cli/commands/start.js +102 -39
- package/dist/cli/commands/tool.js +23 -18
- package/dist/cli/fail.d.ts +17 -2
- package/dist/cli/fail.js +25 -1
- package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
- package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
- package/dist/cli/kernel.d.ts +3 -2
- package/dist/cli/models-view.d.ts +21 -0
- package/dist/cli/models-view.js +66 -0
- package/dist/cli/program.js +135 -56
- package/dist/cli/serve.d.ts +60 -20
- package/dist/cli/serve.js +252 -47
- package/dist/cli/shared.d.ts +40 -7
- package/dist/cli/shared.js +180 -38
- package/dist/collect.d.ts +14 -3
- package/dist/collect.js +24 -0
- package/dist/core.d.ts +3 -1
- package/dist/core.js +2 -0
- package/dist/deploy/agentcore/plan.d.ts +117 -0
- package/dist/deploy/agentcore/plan.js +721 -0
- package/dist/deploy/agentcore/run.d.ts +73 -0
- package/dist/deploy/agentcore/run.js +412 -0
- package/dist/deploy/agentcore/zip.d.ts +17 -0
- package/dist/deploy/agentcore/zip.js +68 -0
- package/dist/deploy/container.d.ts +26 -25
- package/dist/deploy/container.js +93 -89
- package/dist/deploy/docker/plan.d.ts +3 -3
- package/dist/deploy/docker/plan.js +20 -23
- package/dist/deploy/fly/plan.d.ts +5 -1
- package/dist/deploy/fly/plan.js +39 -26
- package/dist/deploy/fly/run.d.ts +17 -5
- package/dist/deploy/fly/run.js +52 -7
- package/dist/deploy/preflight.d.ts +20 -8
- package/dist/deploy/preflight.js +267 -71
- package/dist/deploy/railway/plan.d.ts +10 -1
- package/dist/deploy/railway/plan.js +53 -20
- package/dist/deploy/railway/run.d.ts +13 -5
- package/dist/deploy/railway/run.js +22 -7
- package/dist/deploy/runner.d.ts +7 -3
- package/dist/deploy/runner.js +9 -3
- package/dist/deploy/secrets.d.ts +2 -1
- package/dist/deploy/secrets.js +23 -3
- package/dist/dev-supervisor.d.ts +11 -10
- package/dist/dev-supervisor.js +56 -50
- package/dist/engines/pi/auth.d.ts +8 -7
- package/dist/engines/pi/auth.js +12 -10
- package/dist/engines/pi/channel.d.ts +22 -16
- package/dist/engines/pi/channel.js +92 -62
- package/dist/engines/pi/chat.d.ts +4 -16
- package/dist/engines/pi/chat.js +8 -261
- package/dist/engines/pi/config.d.ts +20 -55
- package/dist/engines/pi/config.js +49 -121
- package/dist/engines/pi/create.d.ts +30 -10
- package/dist/engines/pi/create.js +33 -26
- package/dist/engines/pi/definition.d.ts +7 -26
- package/dist/engines/pi/definition.js +8 -54
- package/dist/engines/pi/harness.d.ts +49 -0
- package/dist/engines/pi/harness.js +90 -2
- package/dist/engines/pi/invoke.d.ts +55 -2
- package/dist/engines/pi/invoke.js +304 -19
- package/dist/engines/pi/login.d.ts +12 -1
- package/dist/engines/pi/login.js +16 -4
- package/dist/engines/pi/models.d.ts +59 -13
- package/dist/engines/pi/models.js +62 -24
- package/dist/engines/pi/open.d.ts +114 -0
- package/dist/engines/pi/open.js +127 -0
- package/dist/engines/pi/session-builder.d.ts +16 -0
- package/dist/engines/pi/session-builder.js +308 -0
- package/dist/engines/pi/session-control.d.ts +50 -0
- package/dist/engines/pi/session-control.js +604 -0
- package/dist/engines/pi/sessions.d.ts +17 -2
- package/dist/engines/pi/sessions.js +9 -0
- package/dist/engines/pi/tool-context.d.ts +18 -11
- package/dist/engines/pi/tool-context.js +3 -9
- package/dist/engines/pi/tool.d.ts +6 -8
- package/dist/engines/pi/tool.js +10 -1
- package/dist/engines/pi/wake-tool.d.ts +0 -3
- package/dist/engines/pi/wake-tool.js +9 -7
- package/dist/env.d.ts +16 -4
- package/dist/env.js +43 -5
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/host/node.d.ts +25 -8
- package/dist/host/node.js +5 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/loader.d.ts +2 -2
- package/dist/loader.js +3 -3
- package/dist/log.d.ts +1 -1
- package/dist/log.js +1 -1
- package/dist/observe.js +3 -0
- package/dist/paths.d.ts +138 -0
- package/dist/paths.js +326 -0
- package/dist/pi.d.ts +6 -5
- package/dist/pi.js +4 -3
- package/dist/runtime.d.ts +7 -5
- package/dist/runtime.js +2 -2
- package/dist/scaffold/add-channel.d.ts +26 -9
- package/dist/scaffold/add-channel.js +175 -40
- package/dist/scaffold/init.d.ts +32 -41
- package/dist/scaffold/init.js +161 -185
- package/dist/scaffold/templates/env.example +15 -6
- package/dist/scaffold/templates/fastagent.config.mjs +5 -4
- package/dist/scaffold/templates/gitignore +14 -6
- package/dist/scaffold/templates/persona.md +4 -2
- package/dist/scaffold/templates/secrets.gitignore +5 -0
- package/dist/scaffold/templates.d.ts +1 -7
- package/dist/scaffold/templates.js +3 -25
- package/dist/scaffold/vendor-skill.d.ts +2 -2
- package/dist/scaffold/vendor-skill.js +13 -13
- package/dist/schedule/discover.js +4 -4
- package/dist/schedule/scheduler.d.ts +40 -1
- package/dist/schedule/scheduler.js +89 -56
- package/dist/schedule/state.js +1 -1
- package/dist/schedule/wake-alarm.d.ts +47 -0
- package/dist/schedule/wake-alarm.js +136 -0
- package/dist/schedule/wakeups.d.ts +1 -3
- package/dist/schedule/wakeups.js +19 -1
- package/dist/session-remote.d.ts +53 -0
- package/dist/session-remote.js +336 -0
- package/dist/session.d.ts +265 -0
- package/dist/session.js +37 -0
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +2 -0
- package/dist/tunnel.d.ts +7 -4
- package/dist/tunnel.js +23 -12
- package/package.json +22 -7
- package/dist/channels/feishu/owned-threads.d.ts +0 -11
- package/dist/channels/feishu/owned-threads.js +0 -47
- package/dist/channels/feishu/seen.d.ts +0 -5
- package/dist/channels/feishu/seen.js +0 -47
- package/dist/cli-add-feishu.d.ts +0 -8
- package/dist/cli-add-feishu.js +0 -223
- package/dist/cli-models.d.ts +0 -11
- package/dist/cli-models.js +0 -20
- package/dist/engines/pi/workspace.d.ts +0 -56
- package/dist/engines/pi/workspace.js +0 -72
- package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
- package/dist/scaffold/templates/gitignore.kit +0 -2
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.js +0 -45
|
@@ -7,7 +7,7 @@ import { type FeishuCloudKind } from "./cloud.ts";
|
|
|
7
7
|
*
|
|
8
8
|
* Reports its outcome as a {@link RegistrationOutcome} fact; gating policy belongs to the caller.
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
interface FeishuManualRegistration {
|
|
11
11
|
consoleUrl: string;
|
|
12
12
|
requestUrl: string;
|
|
13
13
|
}
|
|
@@ -20,3 +20,4 @@ export interface RegisterFeishuWebhookOptions {
|
|
|
20
20
|
onManualRegistration?: (info: FeishuManualRegistration) => void;
|
|
21
21
|
}
|
|
22
22
|
export declare function registerFeishuWebhook(baseUrl: string, kind: FeishuCloudKind, opts?: RegisterFeishuWebhookOptions): Promise<RegistrationOutcome>;
|
|
23
|
+
export {};
|
|
@@ -8,8 +8,10 @@ import { feishuChannel } from "@fastagent-sh/fastagent/feishu";
|
|
|
8
8
|
// 2. Permissions: add `im:message.p2p_msg:readonly` (direct messages), `im:message.group_at_msg:readonly`
|
|
9
9
|
// (group @mentions), `im:message:send_as_bot` (reply), `im:resource` (attachments), and the
|
|
10
10
|
// card scope ("Create and update card" — the live preview streams through a card). To answer bare
|
|
11
|
-
// messages in Agent
|
|
12
|
-
// sensitive `im:message.group_msg` scope (tenant-admin approval) and publish a new version.
|
|
11
|
+
// messages in threads the Agent takes part in, and buffer other unsummoned group/thread context, also add the
|
|
12
|
+
// sensitive `im:message.group_msg` scope (tenant-admin approval) and publish a new version. Add a
|
|
13
|
+
// message-read scope (e.g. `im:message:readonly`) too, so a thread's opening ask can carry the
|
|
14
|
+
// message it quotes; without it that quote degrades to a marker in the prompt.
|
|
13
15
|
// 3. Events & Callbacks → subscribe to `im.message.receive_v1`; copy the Verification Token into
|
|
14
16
|
// .env; RECOMMENDED: set an Encrypt Key there and mirror it in FEISHU_ENCRYPT_KEY
|
|
15
17
|
// 4. the event Request URL (https://your.host/feishu) is registered AUTOMATICALLY by
|
|
@@ -23,18 +25,17 @@ export default feishuChannel({
|
|
|
23
25
|
appSecret: process.env.FEISHU_APP_SECRET ?? "",
|
|
24
26
|
verificationToken: process.env.FEISHU_VERIFICATION_TOKEN ?? "", // authenticates inbound events
|
|
25
27
|
encryptKey: process.env.FEISHU_ENCRYPT_KEY || undefined, // optional; when set, plaintext events are refused
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// groupMessageSession: "continuous",
|
|
28
|
+
// No session modes: a chat is one session and a thread is another, and where the answer goes follows
|
|
29
|
+
// from that (docs/design/participant-model.md).
|
|
29
30
|
// Dev/personal bot: surface raw errors to the chat so you (and your AI agent) can act on them. The
|
|
30
31
|
// chat is customer-facing by default — for a public bot, drop this or return a neutral string;
|
|
31
32
|
// full details always go to the server log regardless.
|
|
32
33
|
onError: (failed) => `⚠️ ${failed.details}`,
|
|
33
34
|
// The channel owns transport + format (markdown card) + attachments (image→vision, file→disk) +
|
|
34
35
|
// the live streaming preview. `route` (POLICY) is OPTIONAL — omitted, it uses defaultFeishuRoute:
|
|
35
|
-
// p2p chats always answer; groups answer on @this-bot, plus bare
|
|
36
|
-
//
|
|
37
|
-
// @other-only messages
|
|
36
|
+
// p2p chats always answer; groups answer on @this-bot, plus bare messages in a thread where the
|
|
37
|
+
// Agent takes part and exactly ONE human does. Other human group/thread discussion buffers until
|
|
38
|
+
// that place's next answered turn; @other-only messages buffer rather than triggering the Agent.
|
|
38
39
|
// Override to customise explicit routing, reusing the export:
|
|
39
40
|
// route: (e) => defaultFeishuRoute(e, { botOpenId: "ou_xxx" }) && { session: `user:${e.sender?.sender_id?.open_id}` },
|
|
40
41
|
// route: (e) => defaultFeishuRoute(e, { botOpenId: "ou_xxx" }) && { text: `${feishuEnvelope(e)}\n[extra]` },
|
|
@@ -6,7 +6,7 @@ import { defineTool, z } from "@fastagent-sh/fastagent";
|
|
|
6
6
|
// context line in a chat turn; a scheduled turn has no such line, so the schedule's prompt must name
|
|
7
7
|
// the target chat id. tools/ is auto-discovered.
|
|
8
8
|
|
|
9
|
-
//
|
|
9
|
+
// Embedded copy of the channel transport's discipline: a timeout so a wedged connection can't hang
|
|
10
10
|
// the tool call (and the turn), named errors, and success gated on the body's own code===0.
|
|
11
11
|
// Deliberately NO rate-limit retry — a tool error goes back to the agent, which can decide to retry;
|
|
12
12
|
// fail-fast beats a silently sleeping tool.
|
|
@@ -57,9 +57,11 @@ async function tenantToken(): Promise<string> {
|
|
|
57
57
|
export default defineTool({
|
|
58
58
|
description:
|
|
59
59
|
"Send a message to a Feishu chat: plain `text`, or `markdown` (rendered as a card — headings, " +
|
|
60
|
-
"bold, code blocks, links). Exactly one of the two.
|
|
61
|
-
"
|
|
62
|
-
"
|
|
60
|
+
"bold, code blocks, links). Exactly one of the two. Use it for a turn NO channel is carrying — a " +
|
|
61
|
+
"scheduled or self-scheduled (wake) turn — or to reach a chat OTHER than the one you are " +
|
|
62
|
+
"answering. In a normal chat turn the channel already delivers your reply, so do NOT call this to " +
|
|
63
|
+
"answer (it would post the message twice). chatId comes from the [feishu: chat …] context line in a " +
|
|
64
|
+
"chat turn; a scheduled/woken turn has no context line, so name the destination in your instruction.",
|
|
63
65
|
input: z.object({
|
|
64
66
|
chatId: z.string().describe("target chat id (oc_…)"),
|
|
65
67
|
text: z.string().optional().describe("plain text message to send"),
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Feishu/Lark app-level event subscription choice used by onboarding and scaffolding. */
|
|
2
|
+
export type FeishuSubscriptionMode = "webhook" | "websocket";
|
|
3
|
+
/** Onboarding choice for group visibility. `context` needs the tenant-wide group-message scope; the
|
|
4
|
+
* runtime remains capability-driven because the platform, not channel source, decides which events
|
|
5
|
+
* are delivered. */
|
|
6
|
+
export type FeishuGroupBehavior = "context" | "mentions";
|
|
7
|
+
/** The sensitive tenant scope behind both bare replies in the agent's threads and group context buffering. */
|
|
8
|
+
export declare const FEISHU_GROUP_CONTEXT_SCOPE = "im:message.group_msg";
|
|
9
|
+
/** Reading a QUOTED message by id, so an ask carries what it replies to (participant-model.md §8).
|
|
10
|
+
* INDEPENDENT of the delivery scope above and of the group posture: the read runs in every chat type
|
|
11
|
+
* (a p2p thread's opening ask, any quoted @mention in a group). It is also a softer dependency —
|
|
12
|
+
* without it everything still works, and an unreadable quote degrades to a marker in the prompt.
|
|
13
|
+
* Bundled into the context-aware request only because that path already needs an approval round; a
|
|
14
|
+
* mention-only app wanting referents must add it by hand. */
|
|
15
|
+
export declare const FEISHU_MESSAGE_READ_SCOPE = "im:message:readonly";
|
|
16
|
+
/** A scope the onboarding asks for, plus any BROADER spelling that already satisfies it. One concept,
|
|
17
|
+
* so adding a superset means editing the entry — not every call site that tests a scope. */
|
|
18
|
+
export interface FeishuScopeRequest {
|
|
19
|
+
/** What to add to the app draft when nothing satisfies it. Always counts as satisfying itself. */
|
|
20
|
+
request: string;
|
|
21
|
+
/** EXTRA spellings that also count — supersets. Optional; `request` is implicit. Modelled this way
|
|
22
|
+
* so a request that can never be satisfied (a list omitting its own `request`) cannot be
|
|
23
|
+
* written: onboarding would add the scope, the tenant would grant it, and it would still read as
|
|
24
|
+
* missing forever. */
|
|
25
|
+
supersets?: string[];
|
|
26
|
+
}
|
|
27
|
+
/** Whether `predicate` holds for any spelling that satisfies this request. */
|
|
28
|
+
export declare function scopeSatisfied(entry: FeishuScopeRequest, predicate: (name: string) => boolean): boolean;
|
|
29
|
+
/** Reading a quoted message: `im:message` is the read/write superset, so an app holding it can already
|
|
30
|
+
* do so — checking only the readonly spelling would warn a correctly configured app forever and push
|
|
31
|
+
* its author into a redundant approval round. Exported on its own because the serving-time capability
|
|
32
|
+
* report asks the same question, and a second mechanism there is what this type exists to prevent. */
|
|
33
|
+
export declare const FEISHU_MESSAGE_READ_REQUEST: FeishuScopeRequest;
|
|
34
|
+
/** What `--group-behavior context` REQUESTS in one approval round — not a dependency set. Only the
|
|
35
|
+
* delivery scope is required for the context path; the read scope rides along because it shares the
|
|
36
|
+
* round and its absence merely degrades quoted messages to a marker. */
|
|
37
|
+
export declare const FEISHU_CONTEXT_ONBOARDING_SCOPES: FeishuScopeRequest[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The sensitive tenant scope behind both bare replies in the agent's threads and group context buffering. */
|
|
2
|
+
export const FEISHU_GROUP_CONTEXT_SCOPE = "im:message.group_msg";
|
|
3
|
+
/** Reading a QUOTED message by id, so an ask carries what it replies to (participant-model.md §8).
|
|
4
|
+
* INDEPENDENT of the delivery scope above and of the group posture: the read runs in every chat type
|
|
5
|
+
* (a p2p thread's opening ask, any quoted @mention in a group). It is also a softer dependency —
|
|
6
|
+
* without it everything still works, and an unreadable quote degrades to a marker in the prompt.
|
|
7
|
+
* Bundled into the context-aware request only because that path already needs an approval round; a
|
|
8
|
+
* mention-only app wanting referents must add it by hand. */
|
|
9
|
+
export const FEISHU_MESSAGE_READ_SCOPE = "im:message:readonly";
|
|
10
|
+
/** Whether `predicate` holds for any spelling that satisfies this request. */
|
|
11
|
+
export function scopeSatisfied(entry, predicate) {
|
|
12
|
+
return [entry.request, ...(entry.supersets ?? [])].some(predicate);
|
|
13
|
+
}
|
|
14
|
+
/** Reading a quoted message: `im:message` is the read/write superset, so an app holding it can already
|
|
15
|
+
* do so — checking only the readonly spelling would warn a correctly configured app forever and push
|
|
16
|
+
* its author into a redundant approval round. Exported on its own because the serving-time capability
|
|
17
|
+
* report asks the same question, and a second mechanism there is what this type exists to prevent. */
|
|
18
|
+
export const FEISHU_MESSAGE_READ_REQUEST = {
|
|
19
|
+
request: FEISHU_MESSAGE_READ_SCOPE,
|
|
20
|
+
supersets: ["im:message"],
|
|
21
|
+
};
|
|
22
|
+
/** What `--group-behavior context` REQUESTS in one approval round — not a dependency set. Only the
|
|
23
|
+
* delivery scope is required for the context path; the read scope rides along because it shares the
|
|
24
|
+
* round and its absence merely degrades quoted messages to a marker. */
|
|
25
|
+
export const FEISHU_CONTEXT_ONBOARDING_SCOPES = [
|
|
26
|
+
{ request: FEISHU_GROUP_CONTEXT_SCOPE },
|
|
27
|
+
FEISHU_MESSAGE_READ_REQUEST,
|
|
28
|
+
];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { LongConnection } from "../../host/node.ts";
|
|
2
|
+
import type { FeishuCloudKind } from "./cloud.ts";
|
|
3
|
+
import type { FeishuMessageEvent } from "./parse.ts";
|
|
4
|
+
interface FeishuWsClient {
|
|
5
|
+
start(): Promise<void>;
|
|
6
|
+
close(): void;
|
|
7
|
+
}
|
|
8
|
+
interface FeishuWsClientCallbacks {
|
|
9
|
+
onReady(): void;
|
|
10
|
+
onError(error: Error): void;
|
|
11
|
+
onReconnecting(): void;
|
|
12
|
+
onReconnected(): void;
|
|
13
|
+
onEvent(event: FeishuMessageEvent): void | Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export type CreateFeishuWsClient = (callbacks: FeishuWsClientCallbacks) => FeishuWsClient | Promise<FeishuWsClient>;
|
|
16
|
+
export interface FeishuWsConnectionOptions {
|
|
17
|
+
kind: FeishuCloudKind;
|
|
18
|
+
appId: string;
|
|
19
|
+
appSecret: string;
|
|
20
|
+
domain: string;
|
|
21
|
+
onEvent(event: FeishuMessageEvent): void | Promise<void>;
|
|
22
|
+
/** Internal SDK seam for deterministic tests. */
|
|
23
|
+
createClient?: CreateFeishuWsClient;
|
|
24
|
+
}
|
|
25
|
+
/** Open Feishu/Lark's official-SDK WebSocket connection. The SDK ACKs only after `onEvent` settles;
|
|
26
|
+
* a persistence throw therefore becomes a 500 response frame and the platform re-pushes the event. */
|
|
27
|
+
export declare function connectFeishuWs(options: FeishuWsConnectionOptions, signal: AbortSignal): LongConnection;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { log } from "../../log.js";
|
|
2
|
+
function sdkLogger(label) {
|
|
3
|
+
return {
|
|
4
|
+
error: (...parts) => log.error(`${label} ${parts.map(String).join(" ")}`),
|
|
5
|
+
warn: (...parts) => log.warn(`${label} ${parts.map(String).join(" ")}`),
|
|
6
|
+
info: (...parts) => log.info(`${label} ${parts.map(String).join(" ")}`),
|
|
7
|
+
debug: (...parts) => log.debug(`${label} ${parts.map(String).join(" ")}`),
|
|
8
|
+
trace: (...parts) => log.debug(`${label} ${parts.map(String).join(" ")}`),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
async function productionClient(options, callbacks) {
|
|
12
|
+
// Webhook-only users stay on the lightweight fetch path; load the proprietary-protocol SDK only
|
|
13
|
+
// when a WebSocket connection is actually opened.
|
|
14
|
+
const { EventDispatcher, LoggerLevel, WSClient } = await import("@larksuiteoapi/node-sdk");
|
|
15
|
+
const label = `[${options.kind}:ws]`;
|
|
16
|
+
const logger = sdkLogger(label);
|
|
17
|
+
const eventDispatcher = new EventDispatcher({ logger, loggerLevel: LoggerLevel.warn }).register({
|
|
18
|
+
"im.message.receive_v1": callbacks.onEvent,
|
|
19
|
+
});
|
|
20
|
+
const client = new WSClient({
|
|
21
|
+
appId: options.appId,
|
|
22
|
+
appSecret: options.appSecret,
|
|
23
|
+
domain: options.domain,
|
|
24
|
+
logger,
|
|
25
|
+
loggerLevel: LoggerLevel.warn,
|
|
26
|
+
autoReconnect: true,
|
|
27
|
+
source: "fastagent",
|
|
28
|
+
handshakeTimeoutMs: 15_000,
|
|
29
|
+
onReady: callbacks.onReady,
|
|
30
|
+
onError: callbacks.onError,
|
|
31
|
+
onReconnecting: callbacks.onReconnecting,
|
|
32
|
+
onReconnected: callbacks.onReconnected,
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
start: () => client.start({ eventDispatcher }),
|
|
36
|
+
close: () => client.close(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Open Feishu/Lark's official-SDK WebSocket connection. The SDK ACKs only after `onEvent` settles;
|
|
40
|
+
* a persistence throw therefore becomes a 500 response frame and the platform re-pushes the event. */
|
|
41
|
+
export function connectFeishuWs(options, signal) {
|
|
42
|
+
const label = `[${options.kind}:ws]`;
|
|
43
|
+
// The SDK only logs + returns for this case (no onError), which would leave readiness pending forever.
|
|
44
|
+
if (!/^cli_[0-9a-fA-F]{16}$/.test(options.appId)) {
|
|
45
|
+
throw new Error(`${options.kind} websocket requires an App ID shaped like cli_<16 hex characters>`);
|
|
46
|
+
}
|
|
47
|
+
let readySettled = false;
|
|
48
|
+
let closedSettled = false;
|
|
49
|
+
let resolveReady;
|
|
50
|
+
let rejectReady;
|
|
51
|
+
let resolveClosed;
|
|
52
|
+
let rejectClosed;
|
|
53
|
+
const ready = new Promise((resolve, reject) => {
|
|
54
|
+
resolveReady = resolve;
|
|
55
|
+
rejectReady = reject;
|
|
56
|
+
});
|
|
57
|
+
const closed = new Promise((resolve, reject) => {
|
|
58
|
+
resolveClosed = resolve;
|
|
59
|
+
rejectClosed = reject;
|
|
60
|
+
});
|
|
61
|
+
const fail = (error) => {
|
|
62
|
+
if (signal.aborted || closedSettled)
|
|
63
|
+
return;
|
|
64
|
+
// Settle-then-close: closedSettled first makes a close()-triggered SDK callback re-entry a no-op.
|
|
65
|
+
closedSettled = true;
|
|
66
|
+
// Terminal failure must release the transport here: the abort listener's close() no-ops once
|
|
67
|
+
// closedSettled is set, and only close() destroys SDK-held resources (e.g. its cache sweep timer).
|
|
68
|
+
if (client)
|
|
69
|
+
closeClient(client);
|
|
70
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
71
|
+
if (!readySettled) {
|
|
72
|
+
readySettled = true;
|
|
73
|
+
rejectReady(failure);
|
|
74
|
+
}
|
|
75
|
+
rejectClosed(failure);
|
|
76
|
+
};
|
|
77
|
+
const createClient = options.createClient ?? ((callbacks) => productionClient(options, callbacks));
|
|
78
|
+
let client;
|
|
79
|
+
const callbacks = {
|
|
80
|
+
onReady() {
|
|
81
|
+
if (readySettled || signal.aborted)
|
|
82
|
+
return;
|
|
83
|
+
readySettled = true;
|
|
84
|
+
log.info(`${label} connected`);
|
|
85
|
+
resolveReady();
|
|
86
|
+
},
|
|
87
|
+
onError(error) {
|
|
88
|
+
fail(new Error(`${error.message} — check that Events & Callbacks uses long connection and the published app version includes im.message.receive_v1`));
|
|
89
|
+
},
|
|
90
|
+
onReconnecting() {
|
|
91
|
+
if (!signal.aborted)
|
|
92
|
+
log.warn(`${label} disconnected — reconnecting…`);
|
|
93
|
+
},
|
|
94
|
+
onReconnected() {
|
|
95
|
+
if (!signal.aborted)
|
|
96
|
+
log.info(`${label} reconnected`);
|
|
97
|
+
},
|
|
98
|
+
onEvent: options.onEvent,
|
|
99
|
+
};
|
|
100
|
+
const closeClient = (target) => {
|
|
101
|
+
try {
|
|
102
|
+
target.close();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
log.warn(`${label} close failed: ${String(error)}`);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const close = () => {
|
|
109
|
+
if (closedSettled)
|
|
110
|
+
return;
|
|
111
|
+
closedSettled = true; // before closeClient, so a callback re-entry from close() is a no-op
|
|
112
|
+
if (client)
|
|
113
|
+
closeClient(client);
|
|
114
|
+
if (!readySettled) {
|
|
115
|
+
// Abort before the first connection: `ready` still settles, and resolution here means
|
|
116
|
+
// cancellation, not readiness (the LongConnection contract; serve skips ready-side effects).
|
|
117
|
+
readySettled = true;
|
|
118
|
+
resolveReady();
|
|
119
|
+
}
|
|
120
|
+
resolveClosed();
|
|
121
|
+
};
|
|
122
|
+
if (signal.aborted) {
|
|
123
|
+
close();
|
|
124
|
+
return { ready, closed };
|
|
125
|
+
}
|
|
126
|
+
signal.addEventListener("abort", close, { once: true });
|
|
127
|
+
void Promise.resolve(createClient(callbacks)).then((created) => {
|
|
128
|
+
client = created;
|
|
129
|
+
if (signal.aborted || closedSettled) {
|
|
130
|
+
closeClient(created);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
void created.start().catch(fail);
|
|
134
|
+
}, fail);
|
|
135
|
+
return { ready, closed };
|
|
136
|
+
}
|
|
@@ -7,6 +7,7 @@ import { verify } from "@octokit/webhooks-methods";
|
|
|
7
7
|
import { collect } from "../../collect.js";
|
|
8
8
|
import { log } from "../../log.js";
|
|
9
9
|
import { readBodyCapped } from "../body.js";
|
|
10
|
+
import { beginWork } from "../busy.js";
|
|
10
11
|
import { text } from "../respond.js";
|
|
11
12
|
/** Raw body cap before verification — GitHub caps webhook payloads at 25 MB; reject larger early. */
|
|
12
13
|
const MAX_WEBHOOK_BYTES = 25 << 20;
|
|
@@ -17,12 +18,7 @@ const MAX_WEBHOOK_BYTES = 25 << 20;
|
|
|
17
18
|
* `(ctx) => ({ "POST /gh": githubChannel(opts)(ctx)["POST /webhook"]! })`.
|
|
18
19
|
*/
|
|
19
20
|
export function githubChannel({ secret, on }) {
|
|
20
|
-
|
|
21
|
-
// compute, so an unset secret must fail at construction, never silently run forgeable.
|
|
22
|
-
if (!secret) {
|
|
23
|
-
throw new Error("githubChannel requires a non-empty secret (the GitHub webhook secret, e.g. GITHUB_WEBHOOK_SECRET)");
|
|
24
|
-
}
|
|
25
|
-
return ({ agent }) => ({
|
|
21
|
+
const channel = ({ agent }) => ({
|
|
26
22
|
"POST /webhook": async (req) => {
|
|
27
23
|
if (req.method !== "POST")
|
|
28
24
|
return text("POST only\n", 405);
|
|
@@ -72,9 +68,22 @@ export function githubChannel({ secret, on }) {
|
|
|
72
68
|
const turn = `${event.deliveryId}#${i}`;
|
|
73
69
|
log.info(`[github] turn start: turn=${turn} session=${session} event=${label}`);
|
|
74
70
|
const startedAt = Date.now();
|
|
75
|
-
|
|
71
|
+
// Post-ACK turns are process-wide in-flight work (busy.ts): a serving surface that must not
|
|
72
|
+
// idle mid-turn (the AgentCore /ping's HealthyBusy) has no other way to see them — and github
|
|
73
|
+
// turns have NO replay, so an idle reclaim here loses the review outright.
|
|
74
|
+
const workDone = beginWork();
|
|
75
|
+
void collect(agent.invoke({ session }, { text }))
|
|
76
|
+
.then(() => log.info(`[github] turn done: turn=${turn} session=${session} (${Date.now() - startedAt}ms)`), (error) => log.error(`[github] turn failed: turn=${turn} session=${session} (${Date.now() - startedAt}ms): ${String(error)}`))
|
|
77
|
+
.finally(workDone);
|
|
76
78
|
}
|
|
77
79
|
return new Response(null, { status: 202 });
|
|
78
80
|
},
|
|
79
81
|
});
|
|
82
|
+
return (ctx) => {
|
|
83
|
+
// Validate at activation so deploy may inspect the module shape before secrets are provisioned.
|
|
84
|
+
if (!secret) {
|
|
85
|
+
throw new Error("githubChannel requires a non-empty secret (the GitHub webhook secret, e.g. GITHUB_WEBHOOK_SECRET)");
|
|
86
|
+
}
|
|
87
|
+
return channel(ctx);
|
|
88
|
+
};
|
|
80
89
|
}
|
package/dist/channels/http.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { Agent } from "../agent.ts";
|
|
3
|
+
/** Request body cap (1 MiB) — shared by every Prompt-bearing wire surface (the control plane's
|
|
4
|
+
* dispatch imports it), so the two caps cannot drift apart. */
|
|
5
|
+
export declare const MAX_BODY_BYTES: number;
|
|
6
|
+
/** SSE comment-heartbeat interval, shared by every SSE surface (the control events route imports
|
|
7
|
+
* it, and the remote client sizes its dead-connection watchdog as a multiple of it). */
|
|
8
|
+
export declare const SSE_HEARTBEAT_MS = 30000;
|
|
9
|
+
/** The emitting half of the heartbeat contract (the client watchdog is the other): starts the
|
|
10
|
+
* `: ping` comment interval on an SSE stream controller and returns its stop function — ONE
|
|
11
|
+
* implementation for every SSE surface, so the emission side cannot regress on one route while
|
|
12
|
+
* the shared client watchdog keeps assuming it. Self-stops if the controller is already closed. */
|
|
13
|
+
export declare function sseHeartbeat(controller: ReadableStreamDefaultController<Uint8Array>): () => void;
|
|
14
|
+
/** A valid example request body for the invoke handler — lives HERE, next to the shape check it must
|
|
15
|
+
* satisfy, so the CLI's "try it" hint can't drift from the protocol. */
|
|
16
|
+
export declare const INVOKE_EXAMPLE_BODY = "{\"session\":\"dev\",\"text\":\"hello\"}";
|
|
3
17
|
/**
|
|
4
18
|
* Fetch-shaped invoke handler. Mount it at any route in the host app; it accepts POST only.
|
|
5
19
|
* Returns SSE (`text/event-stream`) with one `data:` line per AgentEvent.
|
package/dist/channels/http.js
CHANGED
|
@@ -6,15 +6,38 @@
|
|
|
6
6
|
* web stream primitives give cancellation (consumer disconnect → cancel() → iterator.return() →
|
|
7
7
|
* invoke cancellation, SPEC MUST 3), backpressure (pull-based), and the body cap natively.
|
|
8
8
|
*
|
|
9
|
-
* `nodeListener` is the thin node:http adapter for the
|
|
9
|
+
* `nodeListener` is the thin node:http adapter for the embedded `fastagent dev/start` server.
|
|
10
10
|
*/
|
|
11
11
|
import { Readable } from "node:stream";
|
|
12
12
|
import { log } from "../log.js";
|
|
13
13
|
import { readBodyCapped } from "./body.js";
|
|
14
14
|
import { text, textHeaders } from "./respond.js";
|
|
15
|
-
/** Request body cap (1 MiB)
|
|
16
|
-
|
|
15
|
+
/** Request body cap (1 MiB) — shared by every Prompt-bearing wire surface (the control plane's
|
|
16
|
+
* dispatch imports it), so the two caps cannot drift apart. */
|
|
17
|
+
export const MAX_BODY_BYTES = 1 << 20;
|
|
17
18
|
const encoder = new TextEncoder();
|
|
19
|
+
/** SSE comment-heartbeat interval, shared by every SSE surface (the control events route imports
|
|
20
|
+
* it, and the remote client sizes its dead-connection watchdog as a multiple of it). */
|
|
21
|
+
export const SSE_HEARTBEAT_MS = 30_000;
|
|
22
|
+
/** The emitting half of the heartbeat contract (the client watchdog is the other): starts the
|
|
23
|
+
* `: ping` comment interval on an SSE stream controller and returns its stop function — ONE
|
|
24
|
+
* implementation for every SSE surface, so the emission side cannot regress on one route while
|
|
25
|
+
* the shared client watchdog keeps assuming it. Self-stops if the controller is already closed. */
|
|
26
|
+
export function sseHeartbeat(controller) {
|
|
27
|
+
const encoder = new TextEncoder();
|
|
28
|
+
const timer = setInterval(() => {
|
|
29
|
+
try {
|
|
30
|
+
controller.enqueue(encoder.encode(": ping\n\n"));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
clearInterval(timer);
|
|
34
|
+
}
|
|
35
|
+
}, SSE_HEARTBEAT_MS);
|
|
36
|
+
return () => clearInterval(timer);
|
|
37
|
+
}
|
|
38
|
+
/** A valid example request body for the invoke handler — lives HERE, next to the shape check it must
|
|
39
|
+
* satisfy, so the CLI's "try it" hint can't drift from the protocol. */
|
|
40
|
+
export const INVOKE_EXAMPLE_BODY = '{"session":"dev","text":"hello"}';
|
|
18
41
|
/**
|
|
19
42
|
* Fetch-shaped invoke handler. Mount it at any route in the host app; it accepts POST only.
|
|
20
43
|
* Returns SSE (`text/event-stream`) with one `data:` line per AgentEvent.
|
|
@@ -37,19 +60,29 @@ export function createInvokeHandler(agent) {
|
|
|
37
60
|
if (typeof session !== "string" || typeof promptText !== "string") {
|
|
38
61
|
return text('need { "session": string, "text": string }\n', 400);
|
|
39
62
|
}
|
|
63
|
+
// ^ the request shape INVOKE_EXAMPLE_BODY (below) must keep satisfying.
|
|
40
64
|
// Take the iterator explicitly so the stream's cancel() (consumer disconnect) can return() it and
|
|
41
65
|
// run invoke's cancellation cleanup (SPEC MUST 3). pull = backpressure: the next event is produced on demand.
|
|
42
66
|
const iterator = agent.invoke({ session }, { text: promptText })[Symbol.asyncIterator]();
|
|
67
|
+
// Heartbeats: a QUIET stream (a long tool call, no events) is normal here — remote consumers
|
|
68
|
+
// distinguish "quiet but alive" from a dead connection by byte arrival, so silence must not
|
|
69
|
+
// look identical to a black hole (SSE comments are ignored by spec-conforming parsers).
|
|
70
|
+
let stopHeartbeat = () => { };
|
|
43
71
|
const stream = new ReadableStream({
|
|
72
|
+
start(controller) {
|
|
73
|
+
stopHeartbeat = sseHeartbeat(controller);
|
|
74
|
+
},
|
|
44
75
|
async pull(controller) {
|
|
45
76
|
const { value, done } = await iterator.next();
|
|
46
77
|
if (done) {
|
|
78
|
+
stopHeartbeat();
|
|
47
79
|
controller.close();
|
|
48
80
|
return;
|
|
49
81
|
}
|
|
50
82
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(value)}\n\n`));
|
|
51
83
|
},
|
|
52
84
|
async cancel() {
|
|
85
|
+
stopHeartbeat();
|
|
53
86
|
await iterator.return?.();
|
|
54
87
|
},
|
|
55
88
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared pieces of the channels' invoke-turn modules (telegram/feishu/slack `invoke-turn.ts`) — the
|
|
3
|
+
* halves that are channel-independent, so a retry-policy or prompt-wording change lands ONCE:
|
|
4
|
+
*
|
|
5
|
+
* - {@link streamTurnWithBusyRetry}: the busy-retry loop around `agent.invoke`, with the
|
|
6
|
+
* `onCompleted` durable-commit point;
|
|
7
|
+
* - the prompt-suffix wording: {@link attachedFilesManifest}, {@link backgroundImagesManifest},
|
|
8
|
+
* {@link missingAttachmentsNote}, {@link attributedFileName}.
|
|
9
|
+
*
|
|
10
|
+
* Attachment RESOLUTION stays per channel — the platform resource models (Bot API file_ids,
|
|
11
|
+
* message-scoped Feishu keys, Slack file objects) are real differences.
|
|
12
|
+
*/
|
|
13
|
+
import { type Agent, type AgentEvent, type Prompt } from "../agent.ts";
|
|
14
|
+
/** How the busy-wait paces: retry the invoke every `delayMs` while the session's lease is held by an
|
|
15
|
+
* EXTERNAL turn (a self-scheduled wake, a concurrent embedder invoke), up to `maxWaitMs` total. The
|
|
16
|
+
* channel's own turns never collide (the turn-queue serializes per session), so a busy reject here is
|
|
17
|
+
* always an outside holder — wait for it like a queued turn, instead of erroring at the user. */
|
|
18
|
+
export interface BusyRetry {
|
|
19
|
+
delayMs: number;
|
|
20
|
+
maxWaitMs: number;
|
|
21
|
+
}
|
|
22
|
+
export declare const DEFAULT_BUSY_RETRY: BusyRetry;
|
|
23
|
+
/**
|
|
24
|
+
* Stream one Agent turn with the shared busy-wait. `onCompleted` (if given) fires on the turn's
|
|
25
|
+
* `completed` event — the durable-commit point: only then does the turn provably live in the session,
|
|
26
|
+
* so a failure or crash at ANY earlier point leaves the caller's pre-ACK state (turn intent, context
|
|
27
|
+
* buffer) intact for replay/the next summon. The caller uses it to remove the turn intent AND commit
|
|
28
|
+
* the context buffer, in that order, so a crash between the two clears cannot replay a
|
|
29
|
+
* context-stripped turn.
|
|
30
|
+
*
|
|
31
|
+
* BUSY-WAIT: a `failed{code: session_busy}` FIRST event means an external turn holds this session's
|
|
32
|
+
* lease and OUR turn never started — replay-safe. Retry (bounded) instead of yielding it: the user
|
|
33
|
+
* sees the channel's "Thinking…" placeholder while waiting (the mirror of the scheduler deferring a
|
|
34
|
+
* wake INTO a busy session), and only an exhausted wait surfaces the busy failure. Only a FIRST-event
|
|
35
|
+
* busy retries — a fail-fast reject is the only shape the engine emits it in, so nothing that started
|
|
36
|
+
* is ever re-run.
|
|
37
|
+
*/
|
|
38
|
+
export declare function streamTurnWithBusyRetry(agent: Agent, session: string, prompt: Prompt, options: {
|
|
39
|
+
label: string;
|
|
40
|
+
onCompleted?: () => void;
|
|
41
|
+
busyRetry?: BusyRetry;
|
|
42
|
+
}): AsyncIterable<AgentEvent>;
|
|
43
|
+
/** What the attached-files manifest renders per file: display name, byte size, absolute local path. */
|
|
44
|
+
export interface ManifestFile {
|
|
45
|
+
name: string;
|
|
46
|
+
size: number;
|
|
47
|
+
path: string;
|
|
48
|
+
}
|
|
49
|
+
/** The downloaded-file manifest appended to the prompt — the agent reads the paths with its tools.
|
|
50
|
+
* Empty input renders nothing. */
|
|
51
|
+
export declare function attachedFilesManifest(files: readonly ManifestFile[]): string;
|
|
52
|
+
/** Decorate a background file's display name with its attribution ("the file Bob sent" resolves),
|
|
53
|
+
* the way the fold attributes text lines. */
|
|
54
|
+
export declare function attributedFileName(name: string, from: string, msg?: string | number): string;
|
|
55
|
+
/** The manifest attributing background vision images folded in from the earlier discussion — images
|
|
56
|
+
* carry no per-image label inline, so position ("appended after N primary") is the attribution.
|
|
57
|
+
* Channels whose image refs carry no attribution (telegram) simply don't render one. */
|
|
58
|
+
export declare function backgroundImagesManifest(primaryCount: number, refs: readonly {
|
|
59
|
+
from: string;
|
|
60
|
+
messageId: string;
|
|
61
|
+
}[]): string;
|
|
62
|
+
/** The prompt note counting EVERY background attachment the turn does not carry (load failures +
|
|
63
|
+
* cap-skipped) — without it, the model holds fold references it silently cannot open and may
|
|
64
|
+
* pretend it read them. Neutral wording (platforms differ on WHY: expired file_ids, deleted files). */
|
|
65
|
+
export declare function missingAttachmentsNote(missing: number): string;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared pieces of the channels' invoke-turn modules (telegram/feishu/slack `invoke-turn.ts`) — the
|
|
3
|
+
* halves that are channel-independent, so a retry-policy or prompt-wording change lands ONCE:
|
|
4
|
+
*
|
|
5
|
+
* - {@link streamTurnWithBusyRetry}: the busy-retry loop around `agent.invoke`, with the
|
|
6
|
+
* `onCompleted` durable-commit point;
|
|
7
|
+
* - the prompt-suffix wording: {@link attachedFilesManifest}, {@link backgroundImagesManifest},
|
|
8
|
+
* {@link missingAttachmentsNote}, {@link attributedFileName}.
|
|
9
|
+
*
|
|
10
|
+
* Attachment RESOLUTION stays per channel — the platform resource models (Bot API file_ids,
|
|
11
|
+
* message-scoped Feishu keys, Slack file objects) are real differences.
|
|
12
|
+
*/
|
|
13
|
+
import { SESSION_BUSY_CODE } from "../agent.js";
|
|
14
|
+
import { log } from "../log.js";
|
|
15
|
+
// Each retry is a lease-check-level reject (tryAcquire runs before harness assembly) — waiting is nearly
|
|
16
|
+
// free, and the loop exits within one delay of the holder finishing. So the cap is sized to outlast a
|
|
17
|
+
// real tool-using wake turn (minutes), not to be short: 10 min. CEILING: a holder that runs longer than
|
|
18
|
+
// this still surfaces the busy error to the user — the bound exists so a stuck lease can't hang a chat
|
|
19
|
+
// turn forever.
|
|
20
|
+
export const DEFAULT_BUSY_RETRY = { delayMs: 5_000, maxWaitMs: 600_000 };
|
|
21
|
+
/**
|
|
22
|
+
* Stream one Agent turn with the shared busy-wait. `onCompleted` (if given) fires on the turn's
|
|
23
|
+
* `completed` event — the durable-commit point: only then does the turn provably live in the session,
|
|
24
|
+
* so a failure or crash at ANY earlier point leaves the caller's pre-ACK state (turn intent, context
|
|
25
|
+
* buffer) intact for replay/the next summon. The caller uses it to remove the turn intent AND commit
|
|
26
|
+
* the context buffer, in that order, so a crash between the two clears cannot replay a
|
|
27
|
+
* context-stripped turn.
|
|
28
|
+
*
|
|
29
|
+
* BUSY-WAIT: a `failed{code: session_busy}` FIRST event means an external turn holds this session's
|
|
30
|
+
* lease and OUR turn never started — replay-safe. Retry (bounded) instead of yielding it: the user
|
|
31
|
+
* sees the channel's "Thinking…" placeholder while waiting (the mirror of the scheduler deferring a
|
|
32
|
+
* wake INTO a busy session), and only an exhausted wait surfaces the busy failure. Only a FIRST-event
|
|
33
|
+
* busy retries — a fail-fast reject is the only shape the engine emits it in, so nothing that started
|
|
34
|
+
* is ever re-run.
|
|
35
|
+
*/
|
|
36
|
+
export async function* streamTurnWithBusyRetry(agent, session, prompt, options) {
|
|
37
|
+
const { label, onCompleted, busyRetry = DEFAULT_BUSY_RETRY } = options;
|
|
38
|
+
const deadline = Date.now() + busyRetry.maxWaitMs;
|
|
39
|
+
for (;;) {
|
|
40
|
+
let retryBusy = false;
|
|
41
|
+
let first = true;
|
|
42
|
+
for await (const e of agent.invoke({ session }, prompt)) {
|
|
43
|
+
if (first && e.type === "failed" && e.code === SESSION_BUSY_CODE && Date.now() + busyRetry.delayMs < deadline) {
|
|
44
|
+
retryBusy = true; // fail-fast reject — the stream ends after this event; wait and re-invoke
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
first = false;
|
|
48
|
+
if (e.type === "completed")
|
|
49
|
+
onCompleted?.(); // the turn is durably in the session — commit point
|
|
50
|
+
yield e;
|
|
51
|
+
}
|
|
52
|
+
if (!retryBusy)
|
|
53
|
+
return;
|
|
54
|
+
log.info(`${label} session ${session} is busy (an external turn holds it) — retrying in ${busyRetry.delayMs}ms`);
|
|
55
|
+
await new Promise((r) => setTimeout(r, busyRetry.delayMs));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** The downloaded-file manifest appended to the prompt — the agent reads the paths with its tools.
|
|
59
|
+
* Empty input renders nothing. */
|
|
60
|
+
export function attachedFilesManifest(files) {
|
|
61
|
+
return files.length
|
|
62
|
+
? `\n\n[attached files — read them with your tools:\n${files.map((f) => `- ${f.name} (${f.size} bytes) → ${f.path}`).join("\n")}\n]`
|
|
63
|
+
: "";
|
|
64
|
+
}
|
|
65
|
+
/** Decorate a background file's display name with its attribution ("the file Bob sent" resolves),
|
|
66
|
+
* the way the fold attributes text lines. */
|
|
67
|
+
export function attributedFileName(name, from, msg) {
|
|
68
|
+
return `${name} (from ${from}${msg !== undefined ? `, msg ${msg}` : ""}, earlier discussion)`;
|
|
69
|
+
}
|
|
70
|
+
/** The manifest attributing background vision images folded in from the earlier discussion — images
|
|
71
|
+
* carry no per-image label inline, so position ("appended after N primary") is the attribution.
|
|
72
|
+
* Channels whose image refs carry no attribution (telegram) simply don't render one. */
|
|
73
|
+
export function backgroundImagesManifest(primaryCount, refs) {
|
|
74
|
+
return refs.length
|
|
75
|
+
? `\n\n[background vision images from earlier discussion — appended after ${primaryCount} primary image(s):\n${refs
|
|
76
|
+
.map((ref, index) => `- vision image ${primaryCount + index + 1}: from ${ref.from}, msg ${ref.messageId}`)
|
|
77
|
+
.join("\n")}\n]`
|
|
78
|
+
: "";
|
|
79
|
+
}
|
|
80
|
+
/** The prompt note counting EVERY background attachment the turn does not carry (load failures +
|
|
81
|
+
* cap-skipped) — without it, the model holds fold references it silently cannot open and may
|
|
82
|
+
* pretend it read them. Neutral wording (platforms differ on WHY: expired file_ids, deleted files). */
|
|
83
|
+
export function missingAttachmentsNote(missing) {
|
|
84
|
+
return missing > 0
|
|
85
|
+
? `\n[note: ${missing} attachment(s) from the earlier discussion are not loaded (no longer available, or older than the most recent few)]`
|
|
86
|
+
: "";
|
|
87
|
+
}
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* this module binds it to Lark's cloud profile and exposes natural Lark-branded public names. Lark's
|
|
4
4
|
* weaker control-plane capabilities live in onboarding/registration, not in a fork of the turn engine.
|
|
5
5
|
*/
|
|
6
|
-
import type { ChannelModule } from "../../host/node.ts";
|
|
7
|
-
import { type FeishuChannelOptions, type FeishuFailure, type FeishuMessage, type FeishuMessageEvent, type FeishuRoute, defaultFeishuRoute } from "../feishu/feishu.ts";
|
|
6
|
+
import type { ChannelModule, LongConnectionChannelModule } from "../../host/node.ts";
|
|
7
|
+
import { type FeishuChannelOptions, type FeishuFailure, type FeishuMessage, type FeishuMessageEvent, type FeishuRoute, type FeishuWebSocketChannelOptions, defaultFeishuRoute } from "../feishu/feishu.ts";
|
|
8
8
|
export type LarkChannelOptions = FeishuChannelOptions;
|
|
9
|
+
export type LarkWebSocketChannelOptions = FeishuWebSocketChannelOptions;
|
|
9
10
|
export type LarkFailure = FeishuFailure;
|
|
10
11
|
export type LarkMessage = FeishuMessage;
|
|
11
12
|
export type LarkMessageEvent = FeishuMessageEvent;
|
|
@@ -13,3 +14,4 @@ export type LarkRoute = FeishuRoute;
|
|
|
13
14
|
export declare const defaultLarkRoute: typeof defaultFeishuRoute;
|
|
14
15
|
export declare function larkEnvelope(event: LarkMessageEvent): string;
|
|
15
16
|
export declare function larkChannel(opts: LarkChannelOptions): ChannelModule;
|
|
17
|
+
export declare function larkWebSocketChannel(opts: LarkWebSocketChannelOptions): LongConnectionChannelModule;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LARK_COMPAT_CLOUD } from "../feishu/cloud.js";
|
|
2
|
-
import { buildFeishuChannel, defaultFeishuRoute, } from "../feishu/feishu.js";
|
|
2
|
+
import { buildFeishuChannel, buildFeishuWebSocketChannel, defaultFeishuRoute, } from "../feishu/feishu.js";
|
|
3
3
|
import { cloudEnvelope } from "../feishu/parse.js";
|
|
4
4
|
export const defaultLarkRoute = defaultFeishuRoute;
|
|
5
5
|
export function larkEnvelope(event) {
|
|
@@ -8,3 +8,6 @@ export function larkEnvelope(event) {
|
|
|
8
8
|
export function larkChannel(opts) {
|
|
9
9
|
return buildFeishuChannel(LARK_COMPAT_CLOUD, opts, larkChannel.name);
|
|
10
10
|
}
|
|
11
|
+
export function larkWebSocketChannel(opts) {
|
|
12
|
+
return buildFeishuWebSocketChannel(LARK_COMPAT_CLOUD, opts, larkWebSocketChannel.name);
|
|
13
|
+
}
|