@fastagent-sh/fastagent 0.14.0 → 0.15.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 +3 -3
- package/dist/agent.d.ts +20 -0
- package/dist/agent.js +9 -0
- package/dist/channels/context-buffer.d.ts +30 -0
- package/dist/channels/context-buffer.js +98 -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 +8 -11
- package/dist/channels/feishu/context-buffer.js +6 -67
- package/dist/channels/feishu/feishu-api.d.ts +11 -3
- package/dist/channels/feishu/feishu-api.js +20 -2
- package/dist/channels/feishu/feishu.d.ts +28 -16
- package/dist/channels/feishu/feishu.js +210 -188
- package/dist/channels/feishu/invoke-turn.d.ts +7 -20
- package/dist/channels/feishu/invoke-turn.js +10 -62
- package/dist/channels/feishu/model.d.ts +2 -1
- package/dist/channels/feishu/normalize.d.ts +2 -1
- package/dist/channels/feishu/owned-threads.d.ts +2 -6
- package/dist/channels/feishu/parse.d.ts +1 -1
- package/dist/channels/feishu/preview.d.ts +4 -7
- package/dist/channels/feishu/preview.js +24 -140
- package/dist/channels/feishu/register-app.d.ts +2 -1
- package/dist/channels/feishu/register-webhook.d.ts +2 -1
- package/dist/channels/feishu/setup-mode.d.ts +8 -0
- package/dist/channels/feishu/setup-mode.js +2 -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 +8 -6
- package/dist/channels/http.d.ts +14 -0
- package/dist/channels/http.js +35 -2
- 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/preview-kit.d.ts +109 -0
- package/dist/channels/preview-kit.js +183 -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/owned-threads.d.ts +6 -0
- package/dist/channels/slack/owned-threads.js +43 -0
- package/dist/channels/slack/parse.d.ts +23 -0
- package/dist/channels/slack/parse.js +81 -0
- package/dist/channels/slack/preview.d.ts +24 -0
- package/dist/channels/slack/preview.js +359 -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 +77 -0
- package/dist/channels/slack/slack-api.js +415 -0
- package/dist/channels/slack/slack.d.ts +58 -0
- package/dist/channels/slack/slack.js +451 -0
- package/dist/channels/slack/welcomed.d.ts +5 -0
- package/dist/channels/slack/welcomed.js +32 -0
- package/dist/channels/state.js +3 -3
- package/dist/channels/stop-command.d.ts +6 -0
- package/dist/channels/stop-command.js +36 -0
- package/dist/channels/tasks.d.ts +13 -0
- package/dist/channels/tasks.js +10 -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/preview.d.ts +4 -7
- package/dist/channels/telegram/preview.js +24 -142
- package/dist/channels/telegram/telegram.js +23 -9
- package/dist/channels/{feishu/text.d.ts → text.d.ts} +1 -1
- package/dist/channels/{feishu/text.js → text.js} +1 -1
- package/dist/channels/turn-queue.js +1 -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 +27 -0
- package/dist/{cli-add-feishu.js → cli/add-feishu.js} +167 -44
- package/dist/cli/add-slack.d.ts +10 -0
- package/dist/cli/add-slack.js +204 -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 +152 -20
- package/dist/cli/commands/attach.d.ts +82 -0
- package/dist/cli/commands/attach.js +559 -0
- package/dist/cli/commands/chat.d.ts +1 -0
- package/dist/cli/commands/chat.js +6 -1
- package/dist/cli/commands/deploy.d.ts +2 -0
- package/dist/cli/commands/deploy.js +78 -22
- package/dist/cli/commands/dev.js +11 -5
- package/dist/cli/commands/fire.js +3 -3
- package/dist/cli/commands/info.js +2 -2
- package/dist/cli/commands/invoke.js +2 -3
- package/dist/cli/commands/login.js +14 -25
- package/dist/cli/commands/models.js +1 -1
- package/dist/cli/commands/start.js +15 -8
- package/dist/cli/commands/tool.js +2 -1
- package/dist/cli/fail.d.ts +0 -2
- package/dist/cli/fail.js +1 -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 +79 -21
- package/dist/cli/serve.d.ts +38 -18
- package/dist/cli/serve.js +197 -39
- package/dist/cli/shared.d.ts +26 -6
- package/dist/cli/shared.js +163 -33
- 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/docker/plan.d.ts +2 -2
- package/dist/deploy/docker/plan.js +8 -6
- package/dist/deploy/fly/plan.d.ts +3 -1
- package/dist/deploy/fly/plan.js +12 -7
- package/dist/deploy/fly/run.d.ts +5 -4
- package/dist/deploy/fly/run.js +16 -5
- package/dist/deploy/preflight.d.ts +10 -4
- package/dist/deploy/preflight.js +53 -9
- package/dist/deploy/railway/plan.d.ts +3 -1
- package/dist/deploy/railway/plan.js +12 -4
- package/dist/deploy/railway/run.d.ts +5 -4
- package/dist/deploy/railway/run.js +15 -5
- package/dist/deploy/runner.d.ts +2 -1
- package/dist/deploy/secrets.d.ts +2 -1
- package/dist/deploy/secrets.js +23 -3
- package/dist/dev-supervisor.d.ts +0 -2
- package/dist/dev-supervisor.js +7 -3
- package/dist/engines/pi/channel.d.ts +22 -16
- package/dist/engines/pi/channel.js +90 -60
- package/dist/engines/pi/chat.d.ts +4 -16
- package/dist/engines/pi/chat.js +8 -261
- package/dist/engines/pi/config.d.ts +14 -9
- package/dist/engines/pi/config.js +33 -18
- package/dist/engines/pi/create.d.ts +22 -2
- package/dist/engines/pi/create.js +20 -14
- 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 +11 -0
- package/dist/engines/pi/login.js +16 -4
- package/dist/engines/pi/models.d.ts +56 -10
- package/dist/engines/pi/models.js +61 -23
- 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 +6 -1
- package/dist/engines/pi/wake-tool.d.ts +0 -3
- package/dist/engines/pi/wake-tool.js +9 -7
- package/dist/engines/pi/workspace.d.ts +52 -0
- package/dist/engines/pi/workspace.js +72 -15
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/host/node.d.ts +23 -6
- 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/observe.js +3 -0
- package/dist/pi.d.ts +5 -4
- package/dist/pi.js +2 -1
- package/dist/scaffold/add-channel.d.ts +19 -6
- package/dist/scaffold/add-channel.js +126 -17
- package/dist/scaffold/templates/fastagent.config.mjs +4 -3
- package/dist/schedule/wakeups.d.ts +0 -3
- package/dist/schedule/wakeups.js +1 -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 +21 -10
- package/package.json +19 -7
- 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-models.d.ts +0 -11
- package/dist/cli-models.js +0 -20
|
@@ -1,20 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Run one turn (the IO half of canonical Feishu→Agent translation): assemble its inputs — resolve the reply
|
|
3
|
-
* referent (a summon that replies to an earlier message names it only by `parent_id`; the content is
|
|
4
|
-
* NOT in the event, so it is fetched here) and the attachments (vision images inline, files to disk) —
|
|
5
|
-
* and stream `agent.invoke` with the assembled prompt. Split from parse.ts (which is pure) because this
|
|
6
|
-
* half touches the Open API + disk; split from feishu.ts so the factory keeps only wiring and the
|
|
7
|
-
* per-turn lifecycle.
|
|
8
|
-
*
|
|
9
|
-
* Inputs have two tiers. PRIMARY is the summoning message plus the message it explicitly replied to;
|
|
10
|
-
* any load failure there aborts visibly so the Agent never runs without an input the user pointed at.
|
|
11
|
-
* BUFFERED resources come from earlier un-summoned thread/group discussion and degrade per attachment:
|
|
12
|
-
* one expired background file must not block the current ask or hide its still-readable siblings.
|
|
13
|
-
*/
|
|
14
|
-
import { SESSION_BUSY_CODE } from "../../agent.js";
|
|
15
1
|
import { log } from "../../log.js";
|
|
2
|
+
import { DEFAULT_BUSY_RETRY, attachedFilesManifest, attributedFileName, backgroundImagesManifest, missingAttachmentsNote, streamTurnWithBusyRetry, } from "../invoke-turn-kit.js";
|
|
16
3
|
import { parseContent } from "./parse.js";
|
|
17
|
-
import { codePointPrefix } from "
|
|
4
|
+
import { codePointPrefix } from "../text.js";
|
|
18
5
|
/** Appended to the prompt (not the system prompt): the channel renders the reply in a card, and the
|
|
19
6
|
* card's markdown element is the natural fit for LLM output — steer away from HTML/plain. */
|
|
20
7
|
const MARKDOWN_INSTRUCTION = "\n\n(Format your reply in standard Markdown — it is rendered in a Feishu/Lark card.)";
|
|
@@ -84,46 +71,25 @@ async function resolveTurnInputs(t, attachments) {
|
|
|
84
71
|
log.warn(`${t.label} could not load an earlier (buffered) attachment: ${String(result.reason)}`);
|
|
85
72
|
}
|
|
86
73
|
}
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
? `\n[note: ${missing} attachment(s) from the earlier discussion are not loaded (expired, or older than the most recent few)]`
|
|
90
|
-
: "";
|
|
91
|
-
const backgroundImageManifest = backgroundImages.length
|
|
92
|
-
? `\n\n[background vision images from earlier discussion — appended after ${imageRefs.length} primary image(s):\n${backgroundImages
|
|
93
|
-
.map(({ ref }, index) => `- vision image ${imageRefs.length + index + 1}: from ${ref.from}, msg ${ref.messageId}`)
|
|
94
|
-
.join("\n")}\n]`
|
|
95
|
-
: "";
|
|
74
|
+
const missingNote = missingAttachmentsNote(lost + attachments.buffered.skipped);
|
|
75
|
+
const backgroundImageManifest = backgroundImagesManifest(imageRefs.length, backgroundImages.map(({ ref }) => ref));
|
|
96
76
|
const allFiles = [
|
|
97
77
|
...downloaded,
|
|
98
78
|
...backgroundFiles.map(({ file, ref }) => ({
|
|
99
79
|
...file,
|
|
100
|
-
name:
|
|
80
|
+
name: attributedFileName(file.name, ref.from, ref.messageId),
|
|
101
81
|
})),
|
|
102
82
|
];
|
|
103
|
-
const manifest = allFiles.length
|
|
104
|
-
? `\n\n[attached files — read them with your tools:\n${allFiles.map((file) => `- ${file.name} (${file.size} bytes) → ${file.path}`).join("\n")}\n]`
|
|
105
|
-
: "";
|
|
106
83
|
const allImages = [...imageRefs, ...backgroundImages.map(({ image }) => image)];
|
|
107
84
|
return {
|
|
108
85
|
images: allImages.length ? allImages : undefined,
|
|
109
|
-
promptSuffix: `${referentBlock}${
|
|
86
|
+
promptSuffix: `${referentBlock}${missingNote}${backgroundImageManifest}${attachedFilesManifest(allFiles)}`,
|
|
110
87
|
};
|
|
111
88
|
}
|
|
112
|
-
// Each retry is a lease-check-level reject (tryAcquire runs before harness assembly) — waiting is nearly
|
|
113
|
-
// free, and the loop exits within one delay of the holder finishing. The cap is sized to outlast a real
|
|
114
|
-
// tool-using wake turn (minutes); a holder that runs longer still surfaces the busy error to the user.
|
|
115
|
-
const DEFAULT_BUSY_RETRY = { delayMs: 5_000, maxWaitMs: 600_000 };
|
|
116
89
|
/**
|
|
117
|
-
* Run one turn: resolve its inputs, then stream agent.invoke
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* moment the turn provably lives in the session.
|
|
121
|
-
*
|
|
122
|
-
* BUSY-WAIT: a `failed{code: session_busy}` FIRST event means an external turn holds this session's
|
|
123
|
-
* lease and OUR turn never started — replay-safe. Retry (bounded) instead of yielding it: the user sees
|
|
124
|
-
* the "Thinking…" preview while waiting, and only an exhausted wait surfaces the busy failure. Only a
|
|
125
|
-
* FIRST-event busy retries — inputs are already resolved, and a fail-fast reject is the only shape the
|
|
126
|
-
* engine emits it in, so nothing that started is ever re-run.
|
|
90
|
+
* Run one turn: resolve its inputs, then stream agent.invoke with the shared busy-wait
|
|
91
|
+
* (invoke-turn-kit — `onCompleted` is the durable-commit point; see streamTurnWithBusyRetry). A
|
|
92
|
+
* primary-input failure surfaces as a `failed` event (never a silent drop).
|
|
127
93
|
*/
|
|
128
94
|
export async function* invokeFeishuTurn(agent, session, text, transport, attachments, onCompleted, busyRetry = DEFAULT_BUSY_RETRY) {
|
|
129
95
|
let resolved;
|
|
@@ -135,23 +101,5 @@ export async function* invokeFeishuTurn(agent, session, text, transport, attachm
|
|
|
135
101
|
return;
|
|
136
102
|
}
|
|
137
103
|
const prompt = { text: `${text}${resolved.promptSuffix}${MARKDOWN_INSTRUCTION}`, images: resolved.images };
|
|
138
|
-
|
|
139
|
-
for (;;) {
|
|
140
|
-
let retryBusy = false;
|
|
141
|
-
let first = true;
|
|
142
|
-
for await (const e of agent.invoke({ session }, prompt)) {
|
|
143
|
-
if (first && e.type === "failed" && e.code === SESSION_BUSY_CODE && Date.now() + busyRetry.delayMs < deadline) {
|
|
144
|
-
retryBusy = true; // fail-fast reject — the stream ends after this event; wait and re-invoke
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
first = false;
|
|
148
|
-
if (e.type === "completed")
|
|
149
|
-
onCompleted?.(); // the turn is durably in the session — commit point
|
|
150
|
-
yield e;
|
|
151
|
-
}
|
|
152
|
-
if (!retryBusy)
|
|
153
|
-
return;
|
|
154
|
-
log.info(`${transport.label} session ${session} is busy (an external turn holds it) — retrying in ${busyRetry.delayMs}ms`);
|
|
155
|
-
await new Promise((r) => setTimeout(r, busyRetry.delayMs));
|
|
156
|
-
}
|
|
104
|
+
yield* streamTurnWithBusyRetry(agent, session, prompt, { label: transport.label, onCompleted, busyRetry });
|
|
157
105
|
}
|
|
@@ -76,7 +76,7 @@ export type FeishuResourceKind = "image" | "file" | "audio" | "video";
|
|
|
76
76
|
* A resource locator. User-sent resources are scoped by BOTH their carrying message id and resource
|
|
77
77
|
* key; a bare file_key/image_key is insufficient for the message-resource download API.
|
|
78
78
|
*/
|
|
79
|
-
|
|
79
|
+
interface FeishuResourceRef {
|
|
80
80
|
kind: FeishuResourceKind;
|
|
81
81
|
messageId: string;
|
|
82
82
|
key: string;
|
|
@@ -95,3 +95,4 @@ export interface NormalizedFeishuMessage {
|
|
|
95
95
|
resources: FeishuResourceRef[];
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
|
+
export {};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* converts resource keys into message-scoped locators before the turn engine sees them.
|
|
5
5
|
*/
|
|
6
6
|
import type { FeishuMessage, FeishuMessageEvent, FeishuResourceKind, NormalizedFeishuMessage } from "./model.ts";
|
|
7
|
-
|
|
7
|
+
interface DecodedFeishuResource {
|
|
8
8
|
kind: FeishuResourceKind;
|
|
9
9
|
key: string;
|
|
10
10
|
name?: string;
|
|
@@ -20,3 +20,4 @@ export interface DecodedFeishuContent {
|
|
|
20
20
|
export declare function decodeFeishuContent(message: Pick<FeishuMessage, "message_type" | "content" | "mentions">): DecodedFeishuContent;
|
|
21
21
|
/** Normalize one verified message event. Returns null only when its required identity is absent. */
|
|
22
22
|
export declare function normalizeFeishuMessage(event: FeishuMessageEvent): NormalizedFeishuMessage | null;
|
|
23
|
+
export {};
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
rootId: string;
|
|
3
|
-
chatId: string;
|
|
4
|
-
createdAt: number;
|
|
5
|
-
}
|
|
6
|
-
export interface OwnedFeishuThreads {
|
|
1
|
+
interface OwnedFeishuThreads {
|
|
7
2
|
has(chatId: string, rootId: string): boolean;
|
|
8
3
|
/** Idempotent, synchronous, pre-ACK persistence. A failed write throws so the platform redelivers. */
|
|
9
4
|
add(chatId: string, rootId: string): void;
|
|
10
5
|
}
|
|
11
6
|
export declare function createOwnedFeishuThreads(path: string, label: string, now?: () => number): OwnedFeishuThreads;
|
|
7
|
+
export {};
|
|
@@ -9,7 +9,7 @@ import type { FeishuMention, FeishuMessage, FeishuMessageEvent, FeishuRoute, Fei
|
|
|
9
9
|
export type { FeishuMention, FeishuMessage, FeishuMessageEvent, FeishuRoute, FeishuSender };
|
|
10
10
|
/** Legacy compatibility shape returned by {@link parseContent}. New internal code consumes normalized
|
|
11
11
|
* resource refs, which retain resource kind + carrying message id. */
|
|
12
|
-
|
|
12
|
+
interface FeishuAttachmentRef {
|
|
13
13
|
key: string;
|
|
14
14
|
name?: string;
|
|
15
15
|
}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import type { AgentEvent } from "../../agent.ts";
|
|
2
2
|
import { type FeishuApi, type FeishuTarget } from "./feishu-api.ts";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
/** The customer-facing default: neutral, no leaked internals; differentiate only on whether to retry. */
|
|
9
|
-
export declare function defaultErrorMessage(failed: FeishuFailure): string;
|
|
3
|
+
import { type ChannelFailure, defaultErrorMessage } from "../preview-kit.ts";
|
|
4
|
+
/** A terminal failure, as the channel hands it to `onError` — the shared channel shape. */
|
|
5
|
+
export type FeishuFailure = ChannelFailure;
|
|
6
|
+
export { defaultErrorMessage };
|
|
10
7
|
/** A visible preview mounted into the chat. Exported only for the channel wiring: a queued turn mounts
|
|
11
8
|
* one before execution, then hands the exact entity/message to {@link streamFeishuReply} for takeover. */
|
|
12
9
|
export type MountedFeishuPreview = {
|
|
@@ -23,41 +23,16 @@ import { setTimeout as sleep } from "node:timers/promises";
|
|
|
23
23
|
import { log } from "../../log.js";
|
|
24
24
|
import { ANSWER_ELEMENT_ID, CARD_MARKDOWN_MAX_BYTES, cardEntityContent, finalCardJson, streamingCardJson, } from "./card.js";
|
|
25
25
|
import { chunkFeishuText, isCardStreamingClosed } from "./feishu-api.js";
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
export
|
|
29
|
-
return failed.retryable ? "⚠️ Temporary problem — please try again." : "⚠️ Sorry, something went wrong.";
|
|
30
|
-
}
|
|
26
|
+
import { RETRY_NOTICE, THINKING_PLACEHOLDER, applyTurnEvent, composeTurnBody, createPreviewPump, createTurnView, defaultErrorMessage, revealedAnswer, thinkingLine, toolLines, } from "../preview-kit.js";
|
|
27
|
+
import { truncateUtf8 } from "../text.js";
|
|
28
|
+
export { defaultErrorMessage };
|
|
31
29
|
/** How often (ms) to push a live-preview snapshot; tool events still flush on the next loop. Cardkit
|
|
32
30
|
* allows 10 QPS per card entity (50 per app), but one snapshot a second reads smoothly (the client
|
|
33
31
|
* animates between snapshots).
|
|
34
32
|
* Doubles as the answer-preview aging window (see answerView). */
|
|
35
33
|
const STREAM_THROTTLE_MS = 1000;
|
|
36
|
-
/** Max length of a tool's arg preview in the live view. */
|
|
37
|
-
const TOOL_ARG_MAX = 48;
|
|
38
34
|
/** How much of the (growing) reasoning to peek at in the live view — the most recent tail. */
|
|
39
35
|
const THINKING_PREVIEW = 280;
|
|
40
|
-
/** The placeholder shown before any reasoning/tool/text arrives. */
|
|
41
|
-
const THINKING_PLACEHOLDER = "💭 Thinking…";
|
|
42
|
-
/** One-line, truncated: collapse whitespace so a multi-line command/arg stays on one line. */
|
|
43
|
-
function clip(s) {
|
|
44
|
-
const one = s.replace(/\s+/g, " ").trim();
|
|
45
|
-
return truncateCodePointPrefix(one, TOOL_ARG_MAX);
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* A compact, human-readable preview of a tool call's args so the live view reads `🔧 read AGENTS.md`
|
|
49
|
-
* rather than just `🔧 read`. Generic (the channel knows no tool schemas): show the salient value — the
|
|
50
|
-
* first primitive field, conventionally the subject (path / command / query / url) — else compact JSON.
|
|
51
|
-
*/
|
|
52
|
-
function summarizeArgs(args) {
|
|
53
|
-
if (args === null || typeof args !== "object" || Array.isArray(args))
|
|
54
|
-
return clip(String(args));
|
|
55
|
-
const values = Object.values(args);
|
|
56
|
-
const primary = values.find((v) => typeof v === "string" || typeof v === "number");
|
|
57
|
-
if (primary !== undefined)
|
|
58
|
-
return clip(String(primary));
|
|
59
|
-
return values.length > 0 ? clip(JSON.stringify(args)) : "";
|
|
60
|
-
}
|
|
61
36
|
/** Cap a live view to the card budget, PREFIX-STABLE: the streaming client animates only when the old
|
|
62
37
|
* text is a prefix of the new, so an over-budget view freezes at its head rather than sliding a tail
|
|
63
38
|
* window (which would redraw the whole card every frame). The full answer still lands at settle. */
|
|
@@ -174,35 +149,16 @@ export async function settleFeishuPreview(api, target, preview, text) {
|
|
|
174
149
|
* than recalling it and posting another reply.
|
|
175
150
|
*/
|
|
176
151
|
export async function streamFeishuReply(events, api, target, formatError, initialPreview, label = "[feishu]") {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
let answer = "";
|
|
181
|
-
let answerPreviewSince;
|
|
182
|
-
const mark = { running: "…", ok: "✓", error: "✗" };
|
|
183
|
-
const toolView = () => tools.map((t) => `🔧 ${t.label} ${mark[t.status]}`).join("\n");
|
|
184
|
-
// Reasoning is process, not the answer: shown (capped to its tail) in the live preview only, never
|
|
185
|
-
// in the settled final card (which is `answer` alone).
|
|
186
|
-
const thinkingView = () => {
|
|
187
|
-
const t = thinking.replace(/\s+/g, " ").trim();
|
|
188
|
-
if (t === "")
|
|
189
|
-
return "";
|
|
190
|
-
return `💭 ${truncateCodePointSuffix(t, THINKING_PREVIEW)}`;
|
|
191
|
-
};
|
|
192
|
-
// The answer is hidden until its first delta has aged one STREAM_THROTTLE_MS: the pump's leading-edge
|
|
193
|
-
// flush would otherwise turn the very first content delta (often a lone character) into its own frame
|
|
194
|
-
// — the short-reply flicker. Aging is anchored at delta ARRIVAL (set in the event loop, not here) so
|
|
195
|
-
// an in-flight update can't skew the clock; a turn completing within the window settles directly.
|
|
196
|
-
const answerView = () => {
|
|
197
|
-
if (answer.trim() === "" || answerPreviewSince === undefined)
|
|
198
|
-
return "";
|
|
199
|
-
return Date.now() - answerPreviewSince >= STREAM_THROTTLE_MS ? answer : "";
|
|
200
|
-
};
|
|
152
|
+
// Event → view-state reduction is the shared machine (preview-kit); this renderer owns the reveal
|
|
153
|
+
// policy, the card-budget cap, and delivery below.
|
|
154
|
+
const turn = createTurnView();
|
|
201
155
|
const view = () => {
|
|
202
|
-
const v = [
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
.
|
|
156
|
+
const v = composeTurnBody([
|
|
157
|
+
thinkingLine(turn, THINKING_PREVIEW),
|
|
158
|
+
toolLines(turn),
|
|
159
|
+
turn.retrying ? RETRY_NOTICE : "",
|
|
160
|
+
revealedAnswer(turn, STREAM_THROTTLE_MS),
|
|
161
|
+
]);
|
|
206
162
|
return capBytes(v === "" ? THINKING_PLACEHOLDER : v, CARD_MARKDOWN_MAX_BYTES);
|
|
207
163
|
};
|
|
208
164
|
// The live preview is ONE message: either the queue card/text handed in by the wiring, or a preview
|
|
@@ -243,105 +199,31 @@ export async function streamFeishuReply(events, api, target, formatError, initia
|
|
|
243
199
|
throw e;
|
|
244
200
|
}
|
|
245
201
|
};
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
let previewErrLogged = false;
|
|
254
|
-
let pumpDone;
|
|
255
|
-
let wakeThrottle; // set while the pump is mid-throttle; finish() cuts it short
|
|
256
|
-
const runPump = async () => {
|
|
257
|
-
pumping = true;
|
|
258
|
-
try {
|
|
259
|
-
while (dirty && !stopped) {
|
|
260
|
-
dirty = false;
|
|
261
|
-
try {
|
|
262
|
-
await flushPreview();
|
|
263
|
-
}
|
|
264
|
-
catch (e) {
|
|
265
|
-
// Best-effort preview (the final write is authoritative), but a failing update must be visible —
|
|
266
|
-
// log once per turn so a never-rendering preview is diagnosable, not silent.
|
|
267
|
-
if (!previewErrLogged) {
|
|
268
|
-
previewErrLogged = true;
|
|
269
|
-
log.warn(`${label} live preview failed (final reply still sends): ${String(e)}`);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
if (dirty && !stopped) {
|
|
273
|
-
// Pace + coalesce a burst into one snapshot. Interruptible: finish() cuts this short so the
|
|
274
|
-
// final write is not delayed by up to STREAM_THROTTLE_MS after the turn completes.
|
|
275
|
-
await new Promise((resolve) => {
|
|
276
|
-
const t = setTimeout(resolve, STREAM_THROTTLE_MS);
|
|
277
|
-
wakeThrottle = () => {
|
|
278
|
-
clearTimeout(t);
|
|
279
|
-
resolve();
|
|
280
|
-
};
|
|
281
|
-
});
|
|
282
|
-
wakeThrottle = undefined;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
finally {
|
|
287
|
-
pumping = false;
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
// Mark the preview dirty and ensure the single writer is running (an update already in flight picks
|
|
291
|
-
// up the new state on its next loop). Synchronous — callers never await a network write.
|
|
292
|
-
const touch = () => {
|
|
293
|
-
dirty = true;
|
|
294
|
-
if (!pumping)
|
|
295
|
-
pumpDone = runPump();
|
|
296
|
-
};
|
|
202
|
+
// The shared single-writer pump (preview-kit) serializes snapshots to the one preview — which also
|
|
203
|
+
// guarantees the card's strictly-increasing `sequence` lands in order (no concurrent frames).
|
|
204
|
+
const { touch, finish } = createPreviewPump({
|
|
205
|
+
flush: flushPreview,
|
|
206
|
+
throttleMs: STREAM_THROTTLE_MS,
|
|
207
|
+
onError: (e) => log.warn(`${label} live preview failed (final reply still sends): ${String(e)}`),
|
|
208
|
+
});
|
|
297
209
|
touch(); // mount the "💭 Thinking…" preview immediately
|
|
298
|
-
// Stop the pump and await any in-flight update, so the final write below is strictly the LAST one to
|
|
299
|
-
// the preview (no stale frame landing after the answer).
|
|
300
|
-
const finish = async () => {
|
|
301
|
-
stopped = true;
|
|
302
|
-
wakeThrottle?.(); // cut an in-flight throttle so the final write is not delayed up to STREAM_THROTTLE_MS
|
|
303
|
-
await pumpDone?.catch(() => { });
|
|
304
|
-
};
|
|
305
210
|
/** Terminal write, whatever tier the preview reached. */
|
|
306
211
|
const settle = async (text) => {
|
|
307
212
|
await finalize(api, target, preview, text, nextSeq);
|
|
308
213
|
};
|
|
309
214
|
try {
|
|
310
215
|
for await (const e of events) {
|
|
311
|
-
if (e.type === "
|
|
312
|
-
answer += e.delta;
|
|
313
|
-
if (answerPreviewSince === undefined && answer.trim() !== "")
|
|
314
|
-
answerPreviewSince = Date.now();
|
|
315
|
-
touch();
|
|
316
|
-
}
|
|
317
|
-
else if (e.type === "thinking") {
|
|
318
|
-
thinking += e.delta;
|
|
319
|
-
touch();
|
|
320
|
-
}
|
|
321
|
-
else if (e.type === "tool_started") {
|
|
322
|
-
const arg = summarizeArgs(e.args);
|
|
323
|
-
toolIndexById.set(e.id, tools.length);
|
|
324
|
-
tools.push({ label: arg ? `${e.name} ${arg}` : e.name, status: "running" });
|
|
325
|
-
touch();
|
|
326
|
-
}
|
|
327
|
-
else if (e.type === "tool_ended") {
|
|
328
|
-
const i = toolIndexById.get(e.id);
|
|
329
|
-
const t = i === undefined ? undefined : tools[i];
|
|
330
|
-
if (t)
|
|
331
|
-
t.status = e.isError ? "error" : "ok";
|
|
332
|
-
touch();
|
|
333
|
-
}
|
|
334
|
-
else if (e.type === "completed") {
|
|
216
|
+
if (e.type === "completed") {
|
|
335
217
|
await finish();
|
|
336
218
|
// Settle the preview into the final answer; the persisted card is the answer alone — the
|
|
337
219
|
// process (thinking/tools) was preview-only. Mark finalized BEFORE delivering: the terminal was
|
|
338
220
|
// reached, so a delivery failure here is a plain failure, not an "abnormal exit" (which would
|
|
339
221
|
// wrongly fire the finally's neutral-notice fallback = double delivery + wrong text).
|
|
340
222
|
finalized = true;
|
|
341
|
-
await settle(answer.trim() !== "" ? answer : "(no reply)");
|
|
223
|
+
await settle(turn.answer.trim() !== "" ? turn.answer : "(no reply)");
|
|
342
224
|
return;
|
|
343
225
|
}
|
|
344
|
-
|
|
226
|
+
if (e.type === "failed") {
|
|
345
227
|
await finish();
|
|
346
228
|
// Two audiences: the chat (customer-facing — formatError, neutral by default) and the operator
|
|
347
229
|
// log (dev-facing — the full details, via the throw below + the handler's catch). Same terminal
|
|
@@ -361,6 +243,8 @@ export async function streamFeishuReply(events, api, target, formatError, initia
|
|
|
361
243
|
}
|
|
362
244
|
throw new Error(`agent failed: ${e.details} (retryable=${e.retryable})`);
|
|
363
245
|
}
|
|
246
|
+
if (applyTurnEvent(turn, e))
|
|
247
|
+
touch();
|
|
364
248
|
}
|
|
365
249
|
throw new Error("stream ended without a terminal event"); // violates SPEC MUST 1
|
|
366
250
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* encoding (JSON → gzip → base64url) follow the official SDKs (provenance: node-sdk
|
|
22
22
|
* scene/registration); item names unknown to the platform catalog are silently dropped by the page.
|
|
23
23
|
*/
|
|
24
|
-
|
|
24
|
+
interface FeishuAppAddons {
|
|
25
25
|
scopes?: {
|
|
26
26
|
tenant?: string[];
|
|
27
27
|
user?: string[];
|
|
@@ -68,3 +68,4 @@ export interface RegisteredFeishuApp {
|
|
|
68
68
|
* abort, or a transport failure — every rejection is a plain Error whose message says what to do.
|
|
69
69
|
*/
|
|
70
70
|
export declare function registerFeishuApp(options: RegisterFeishuAppOptions): Promise<RegisteredFeishuApp>;
|
|
71
|
+
export {};
|
|
@@ -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 {};
|
|
@@ -0,0 +1,8 @@
|
|
|
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 managed-thread bare replies and group context buffering. */
|
|
8
|
+
export declare const FEISHU_GROUP_CONTEXT_SCOPE = "im:message.group_msg";
|
|
@@ -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
|
+
}
|
|
@@ -17,12 +17,7 @@ const MAX_WEBHOOK_BYTES = 25 << 20;
|
|
|
17
17
|
* `(ctx) => ({ "POST /gh": githubChannel(opts)(ctx)["POST /webhook"]! })`.
|
|
18
18
|
*/
|
|
19
19
|
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 }) => ({
|
|
20
|
+
const channel = ({ agent }) => ({
|
|
26
21
|
"POST /webhook": async (req) => {
|
|
27
22
|
if (req.method !== "POST")
|
|
28
23
|
return text("POST only\n", 405);
|
|
@@ -77,4 +72,11 @@ export function githubChannel({ secret, on }) {
|
|
|
77
72
|
return new Response(null, { status: 202 });
|
|
78
73
|
},
|
|
79
74
|
});
|
|
75
|
+
return (ctx) => {
|
|
76
|
+
// Validate at activation so deploy may inspect the module shape before secrets are provisioned.
|
|
77
|
+
if (!secret) {
|
|
78
|
+
throw new Error("githubChannel requires a non-empty secret (the GitHub webhook secret, e.g. GITHUB_WEBHOOK_SECRET)");
|
|
79
|
+
}
|
|
80
|
+
return channel(ctx);
|
|
81
|
+
};
|
|
80
82
|
}
|
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.
|