@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
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* BUFFERED resources come from earlier un-summoned thread/group discussion and degrade per attachment:
|
|
12
12
|
* one expired background file must not block the current ask or hide its still-readable siblings.
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import type { Agent, AgentEvent } from "../../agent.ts";
|
|
15
|
+
import { type BusyRetry } from "../invoke-turn-kit.ts";
|
|
15
16
|
import type { FeishuBufferedRef } from "./context-buffer.ts";
|
|
16
17
|
import type { FeishuApi } from "./feishu-api.ts";
|
|
17
18
|
/** Everything the transport needs to fetch a turn's attachments. */
|
|
@@ -23,7 +24,7 @@ export interface FeishuTurnTransport {
|
|
|
23
24
|
}
|
|
24
25
|
/** An attachment reference: the resource key inside its CARRYING message (the resource API addresses
|
|
25
26
|
* bytes by message_id + key, so the pair travels together through the turn record). */
|
|
26
|
-
|
|
27
|
+
interface FeishuAttachmentInput {
|
|
27
28
|
msg: string;
|
|
28
29
|
key: string;
|
|
29
30
|
name?: string;
|
|
@@ -42,24 +43,10 @@ export interface FeishuTurnAttachments {
|
|
|
42
43
|
skipped: number;
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
|
-
/** How the busy-wait paces: retry the invoke every `delayMs` while the session's lease is held by an
|
|
46
|
-
* EXTERNAL turn (a self-scheduled wake, a concurrent embedder invoke), up to `maxWaitMs` total. The
|
|
47
|
-
* channel's own turns never collide (the turn-queue serializes per session), so a busy reject here is
|
|
48
|
-
* always an outside holder — wait for it like a queued turn, instead of erroring at the user. */
|
|
49
|
-
export interface BusyRetry {
|
|
50
|
-
delayMs: number;
|
|
51
|
-
maxWaitMs: number;
|
|
52
|
-
}
|
|
53
46
|
/**
|
|
54
|
-
* Run one turn: resolve its inputs, then stream agent.invoke
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* moment the turn provably lives in the session.
|
|
58
|
-
*
|
|
59
|
-
* BUSY-WAIT: a `failed{code: session_busy}` FIRST event means an external turn holds this session's
|
|
60
|
-
* lease and OUR turn never started — replay-safe. Retry (bounded) instead of yielding it: the user sees
|
|
61
|
-
* the "Thinking…" preview while waiting, and only an exhausted wait surfaces the busy failure. Only a
|
|
62
|
-
* FIRST-event busy retries — inputs are already resolved, and a fail-fast reject is the only shape the
|
|
63
|
-
* engine emits it in, so nothing that started is ever re-run.
|
|
47
|
+
* Run one turn: resolve its inputs, then stream agent.invoke with the shared busy-wait
|
|
48
|
+
* (invoke-turn-kit — `onCompleted` is the durable-commit point; see streamTurnWithBusyRetry). A
|
|
49
|
+
* primary-input failure surfaces as a `failed` event (never a silent drop).
|
|
64
50
|
*/
|
|
65
51
|
export declare function invokeFeishuTurn(agent: Agent, session: string, text: string, transport: FeishuTurnTransport, attachments: FeishuTurnAttachments, onCompleted?: () => void, busyRetry?: BusyRetry): AsyncIterable<AgentEvent>;
|
|
52
|
+
export {};
|
|
@@ -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 {
|
|
4
|
+
import { REFERENT_MAX_CODE_POINTS, truncateCodePointPrefix } 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.)";
|
|
@@ -28,24 +15,41 @@ async function resolveTurnInputs(t, attachments) {
|
|
|
28
15
|
let referentBlock = "";
|
|
29
16
|
if (attachments.primary.parentId !== undefined) {
|
|
30
17
|
const parentId = attachments.primary.parentId;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
18
|
+
// A referent is CONTEXT, not the ask. Losing it (deleted, restricted, unreadable) must not cost
|
|
19
|
+
// the user their answer — every first message of a thread carries one, so a hard failure here
|
|
20
|
+
// would turn an ordinary platform edge into a lost turn. Degrade visibly instead: the operator
|
|
21
|
+
// gets a warning, and the model is told the quote could not be read rather than being left to
|
|
22
|
+
// guess what "about that" refers to.
|
|
23
|
+
// A deleted or invisible message comes back as an EMPTY item list rather than an error, so the
|
|
24
|
+
// warning belongs on the branch that renders the marker — that is the one the operator must see.
|
|
25
|
+
let failure;
|
|
26
|
+
const parent = await t.api.getMessage(parentId).catch((error) => {
|
|
27
|
+
failure = String(error);
|
|
28
|
+
return undefined;
|
|
38
29
|
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
30
|
+
if (!parent) {
|
|
31
|
+
log.warn(`${t.label} could not read replied-to message ${parentId} (${failure ?? "no such message"}) — the model is told the quote is unreadable`);
|
|
32
|
+
// Fall THROUGH: the resources this turn carries are the ask itself. Returning here would drop
|
|
33
|
+
// the images and files the user explicitly attached along with the referent they merely quoted.
|
|
34
|
+
referentBlock = `\n\n[replied-to message (msg ${parentId}) could not be read]`;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const parsed = parseContent({
|
|
38
|
+
message_type: parent.msg_type ?? "unknown",
|
|
39
|
+
content: parent.body?.content ?? "",
|
|
40
|
+
mentions: parent.mentions,
|
|
41
|
+
});
|
|
42
|
+
// The referent's own resources join the turn as primary inputs, carried by the PARENT message id.
|
|
43
|
+
for (const key of parsed.imageKeys)
|
|
44
|
+
images.push({ msg: parentId, key });
|
|
45
|
+
for (const ref of parsed.fileRefs)
|
|
46
|
+
files.push({ msg: parentId, key: ref.key, name: ref.name });
|
|
47
|
+
// getMessage's sender is `{ id, id_type, sender_type }` — a DIFFERENT shape from the event's
|
|
48
|
+
// sender (`{ sender_id: { open_id } }`), so the label is built here, not via parse.senderLabel.
|
|
49
|
+
const senderId = parent.sender?.id;
|
|
50
|
+
const from = senderId ? `user ${senderId}` : undefined;
|
|
51
|
+
referentBlock = `\n\n[replied-to message (msg ${parentId}${from ? `, from ${from}` : ""}): ${truncateCodePointPrefix(parsed.text, REFERENT_MAX_CODE_POINTS) || "(empty)"}]`;
|
|
52
|
+
}
|
|
49
53
|
}
|
|
50
54
|
// Primary first and fail-fast: these are resources the current user explicitly pointed at.
|
|
51
55
|
const imageRefs = [];
|
|
@@ -84,46 +88,25 @@ async function resolveTurnInputs(t, attachments) {
|
|
|
84
88
|
log.warn(`${t.label} could not load an earlier (buffered) attachment: ${String(result.reason)}`);
|
|
85
89
|
}
|
|
86
90
|
}
|
|
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
|
-
: "";
|
|
91
|
+
const missingNote = missingAttachmentsNote(lost + attachments.buffered.skipped);
|
|
92
|
+
const backgroundImageManifest = backgroundImagesManifest(imageRefs.length, backgroundImages.map(({ ref }) => ref));
|
|
96
93
|
const allFiles = [
|
|
97
94
|
...downloaded,
|
|
98
95
|
...backgroundFiles.map(({ file, ref }) => ({
|
|
99
96
|
...file,
|
|
100
|
-
name:
|
|
97
|
+
name: attributedFileName(file.name, ref.from, ref.messageId),
|
|
101
98
|
})),
|
|
102
99
|
];
|
|
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
100
|
const allImages = [...imageRefs, ...backgroundImages.map(({ image }) => image)];
|
|
107
101
|
return {
|
|
108
102
|
images: allImages.length ? allImages : undefined,
|
|
109
|
-
promptSuffix: `${referentBlock}${
|
|
103
|
+
promptSuffix: `${referentBlock}${missingNote}${backgroundImageManifest}${attachedFilesManifest(allFiles)}`,
|
|
110
104
|
};
|
|
111
105
|
}
|
|
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
106
|
/**
|
|
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.
|
|
107
|
+
* Run one turn: resolve its inputs, then stream agent.invoke with the shared busy-wait
|
|
108
|
+
* (invoke-turn-kit — `onCompleted` is the durable-commit point; see streamTurnWithBusyRetry). A
|
|
109
|
+
* primary-input failure surfaces as a `failed` event (never a silent drop).
|
|
127
110
|
*/
|
|
128
111
|
export async function* invokeFeishuTurn(agent, session, text, transport, attachments, onCompleted, busyRetry = DEFAULT_BUSY_RETRY) {
|
|
129
112
|
let resolved;
|
|
@@ -135,23 +118,5 @@ export async function* invokeFeishuTurn(agent, session, text, transport, attachm
|
|
|
135
118
|
return;
|
|
136
119
|
}
|
|
137
120
|
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
|
-
}
|
|
121
|
+
yield* streamTurnWithBusyRetry(agent, session, prompt, { label: transport.label, onCompleted, busyRetry });
|
|
157
122
|
}
|
|
@@ -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;
|
|
@@ -87,7 +87,6 @@ export interface NormalizedFeishuMessage {
|
|
|
87
87
|
conversation: {
|
|
88
88
|
chatId: string;
|
|
89
89
|
threadId?: string;
|
|
90
|
-
rootId?: string;
|
|
91
90
|
};
|
|
92
91
|
content: {
|
|
93
92
|
text: string;
|
|
@@ -95,3 +94,4 @@ export interface NormalizedFeishuMessage {
|
|
|
95
94
|
resources: FeishuResourceRef[];
|
|
96
95
|
};
|
|
97
96
|
}
|
|
97
|
+
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 {};
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
import type { FeishuCloudKind } from "./cloud.ts";
|
|
8
8
|
import type { FeishuMention, FeishuMessage, FeishuMessageEvent, FeishuRoute, FeishuSender } from "./model.ts";
|
|
9
9
|
export type { FeishuMention, FeishuMessage, FeishuMessageEvent, FeishuRoute, FeishuSender };
|
|
10
|
-
/**
|
|
11
|
-
*
|
|
12
|
-
|
|
10
|
+
/** A resource reduced to what a caller needs to fetch and name it — the kind and carrying message id
|
|
11
|
+
* that {@link decodeFeishuContent} attaches are supplied by the caller's own context. */
|
|
12
|
+
interface FeishuAttachmentRef {
|
|
13
13
|
key: string;
|
|
14
14
|
name?: string;
|
|
15
15
|
}
|
|
@@ -19,14 +19,27 @@ export interface ParsedFeishuContent {
|
|
|
19
19
|
fileRefs: FeishuAttachmentRef[];
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* The decoder projected onto flat per-kind arrays, for callers that resolve a message on their own
|
|
23
|
+
* (the prompt envelope, and the quoted parent whose resources are carried by the PARENT message id).
|
|
24
|
+
* `decodeFeishuContent` stays canonical: this only reshapes what it returns.
|
|
24
25
|
*/
|
|
25
26
|
export declare function parseContent(message: Pick<FeishuMessage, "message_type" | "content" | "mentions">): ParsedFeishuContent;
|
|
26
27
|
/** A stable sender label for attribution. Display names require an additional contacts permission. */
|
|
27
28
|
export declare function senderLabel(sender: FeishuSender | undefined): string | undefined;
|
|
28
|
-
/**
|
|
29
|
-
|
|
29
|
+
/** Whichever id flavour the tenant populates. `sender_id` is a union and which members are filled is
|
|
30
|
+
* app configuration, not an invariant — callers only ever compare these for distinctness. */
|
|
31
|
+
export declare function senderId(sender: FeishuSender | undefined): string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* The place a message lives (the chat, or a thread within it) — the session key (participant model §5),
|
|
34
|
+
* and the key thread participation is recorded under, since that record is a claim about this session.
|
|
35
|
+
*
|
|
36
|
+
* Branded with the channel kind, like Slack's twin, because session ids share ONE namespace across
|
|
37
|
+
* every channel in a deployment: without it a `feishu` and a `lark` chat carrying the same platform id
|
|
38
|
+
* would answer into the same memory. The length bound is the FILENAME the id becomes (sessions.ts
|
|
39
|
+
* percent-encodes it, so each `:` costs three), and the worst case here — brand + a 35-char chat id +
|
|
40
|
+
* a 36-char thread id — encodes to well under 100 bytes against the filesystem's 255.
|
|
41
|
+
*/
|
|
42
|
+
export declare function placeKey(kind: string, message: Pick<FeishuMessage, "chat_id" | "thread_id">): string;
|
|
30
43
|
/** The canonical Feishu-branded prompt envelope. */
|
|
31
44
|
export declare function feishuEnvelope(event: FeishuMessageEvent): string;
|
|
32
45
|
/** Internal compatibility seam: bind the canonical envelope shape to one cloud's branded tag. */
|
|
@@ -36,7 +49,8 @@ export declare function mentionsBot(message: Pick<FeishuMessage, "mentions">, bo
|
|
|
36
49
|
/**
|
|
37
50
|
* Default EXPLICIT-summon policy: ignore non-user senders, always answer p2p, and answer groups only
|
|
38
51
|
* when THIS bot is structurally mentioned. No bot identity means group routing fails closed. The
|
|
39
|
-
* stateful channel wiring may additionally admit unmentioned
|
|
52
|
+
* stateful channel wiring may additionally admit unmentioned messages in a thread it takes part in
|
|
53
|
+
* (docs/design/participant-model.md §3).
|
|
40
54
|
*/
|
|
41
55
|
export declare function defaultFeishuRoute(event: FeishuMessageEvent, options?: {
|
|
42
56
|
botOpenId?: string;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { decodeFeishuContent } from "./normalize.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* The decoder projected onto flat per-kind arrays, for callers that resolve a message on their own
|
|
4
|
+
* (the prompt envelope, and the quoted parent whose resources are carried by the PARENT message id).
|
|
5
|
+
* `decodeFeishuContent` stays canonical: this only reshapes what it returns.
|
|
5
6
|
*/
|
|
6
7
|
export function parseContent(message) {
|
|
7
8
|
const decoded = decodeFeishuContent(message);
|
|
@@ -15,12 +16,27 @@ export function parseContent(message) {
|
|
|
15
16
|
}
|
|
16
17
|
/** A stable sender label for attribution. Display names require an additional contacts permission. */
|
|
17
18
|
export function senderLabel(sender) {
|
|
18
|
-
const id = sender
|
|
19
|
+
const id = senderId(sender);
|
|
19
20
|
return id ? `user ${id}` : undefined;
|
|
20
21
|
}
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
/** Whichever id flavour the tenant populates. `sender_id` is a union and which members are filled is
|
|
23
|
+
* app configuration, not an invariant — callers only ever compare these for distinctness. */
|
|
24
|
+
export function senderId(sender) {
|
|
25
|
+
return sender?.sender_id?.open_id ?? sender?.sender_id?.user_id ?? sender?.sender_id?.union_id;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The place a message lives (the chat, or a thread within it) — the session key (participant model §5),
|
|
29
|
+
* and the key thread participation is recorded under, since that record is a claim about this session.
|
|
30
|
+
*
|
|
31
|
+
* Branded with the channel kind, like Slack's twin, because session ids share ONE namespace across
|
|
32
|
+
* every channel in a deployment: without it a `feishu` and a `lark` chat carrying the same platform id
|
|
33
|
+
* would answer into the same memory. The length bound is the FILENAME the id becomes (sessions.ts
|
|
34
|
+
* percent-encodes it, so each `:` costs three), and the worst case here — brand + a 35-char chat id +
|
|
35
|
+
* a 36-char thread id — encodes to well under 100 bytes against the filesystem's 255.
|
|
36
|
+
*/
|
|
37
|
+
export function placeKey(kind, message) {
|
|
38
|
+
const chat = `${kind}:${message.chat_id}`;
|
|
39
|
+
return message.thread_id ? `${chat}:${message.thread_id}` : chat;
|
|
24
40
|
}
|
|
25
41
|
/** The canonical Feishu-branded prompt envelope. */
|
|
26
42
|
export function feishuEnvelope(event) {
|
|
@@ -52,7 +68,8 @@ export function mentionsBot(message, botOpenId) {
|
|
|
52
68
|
/**
|
|
53
69
|
* Default EXPLICIT-summon policy: ignore non-user senders, always answer p2p, and answer groups only
|
|
54
70
|
* when THIS bot is structurally mentioned. No bot identity means group routing fails closed. The
|
|
55
|
-
* stateful channel wiring may additionally admit unmentioned
|
|
71
|
+
* stateful channel wiring may additionally admit unmentioned messages in a thread it takes part in
|
|
72
|
+
* (docs/design/participant-model.md §3).
|
|
56
73
|
*/
|
|
57
74
|
export function defaultFeishuRoute(event, options) {
|
|
58
75
|
const message = event.message;
|
|
@@ -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. */
|
|
@@ -88,8 +63,9 @@ async function finalize(api, target, preview, text, seq) {
|
|
|
88
63
|
// Settle failed (card expired / rejected) — fall through to delete + fresh send below.
|
|
89
64
|
}
|
|
90
65
|
if (settled) {
|
|
91
|
-
// Threaded continuations must keep reply_in_thread;
|
|
92
|
-
//
|
|
66
|
+
// Threaded continuations must keep reply_in_thread; top-level group replies (target.replyInThread
|
|
67
|
+
// unset — a custom route, or an ask outside a thread) intentionally avoid repeating the quote on
|
|
68
|
+
// every chunk. sendText owns the same distinction for its own chunking.
|
|
93
69
|
// A continuation failure propagates: the card is already authoritative, so deleting it and sending
|
|
94
70
|
// the full answer again would deterministically duplicate every continuation that already landed.
|
|
95
71
|
const continuationTarget = target.replyInThread ? target : { chatId: target.chatId };
|
|
@@ -174,35 +150,16 @@ export async function settleFeishuPreview(api, target, preview, text) {
|
|
|
174
150
|
* than recalling it and posting another reply.
|
|
175
151
|
*/
|
|
176
152
|
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
|
-
};
|
|
153
|
+
// Event → view-state reduction is the shared machine (preview-kit); this renderer owns the reveal
|
|
154
|
+
// policy, the card-budget cap, and delivery below.
|
|
155
|
+
const turn = createTurnView();
|
|
201
156
|
const view = () => {
|
|
202
|
-
const v = [
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
.
|
|
157
|
+
const v = composeTurnBody([
|
|
158
|
+
thinkingLine(turn, THINKING_PREVIEW),
|
|
159
|
+
toolLines(turn),
|
|
160
|
+
turn.retrying ? RETRY_NOTICE : "",
|
|
161
|
+
revealedAnswer(turn, STREAM_THROTTLE_MS),
|
|
162
|
+
]);
|
|
206
163
|
return capBytes(v === "" ? THINKING_PLACEHOLDER : v, CARD_MARKDOWN_MAX_BYTES);
|
|
207
164
|
};
|
|
208
165
|
// The live preview is ONE message: either the queue card/text handed in by the wiring, or a preview
|
|
@@ -243,105 +200,31 @@ export async function streamFeishuReply(events, api, target, formatError, initia
|
|
|
243
200
|
throw e;
|
|
244
201
|
}
|
|
245
202
|
};
|
|
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
|
-
};
|
|
203
|
+
// The shared single-writer pump (preview-kit) serializes snapshots to the one preview — which also
|
|
204
|
+
// guarantees the card's strictly-increasing `sequence` lands in order (no concurrent frames).
|
|
205
|
+
const { touch, finish } = createPreviewPump({
|
|
206
|
+
flush: flushPreview,
|
|
207
|
+
throttleMs: STREAM_THROTTLE_MS,
|
|
208
|
+
onError: (e) => log.warn(`${label} live preview failed (final reply still sends): ${String(e)}`),
|
|
209
|
+
});
|
|
297
210
|
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
211
|
/** Terminal write, whatever tier the preview reached. */
|
|
306
212
|
const settle = async (text) => {
|
|
307
213
|
await finalize(api, target, preview, text, nextSeq);
|
|
308
214
|
};
|
|
309
215
|
try {
|
|
310
216
|
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") {
|
|
217
|
+
if (e.type === "completed") {
|
|
335
218
|
await finish();
|
|
336
219
|
// Settle the preview into the final answer; the persisted card is the answer alone — the
|
|
337
220
|
// process (thinking/tools) was preview-only. Mark finalized BEFORE delivering: the terminal was
|
|
338
221
|
// reached, so a delivery failure here is a plain failure, not an "abnormal exit" (which would
|
|
339
222
|
// wrongly fire the finally's neutral-notice fallback = double delivery + wrong text).
|
|
340
223
|
finalized = true;
|
|
341
|
-
await settle(answer.trim() !== "" ? answer : "(no reply)");
|
|
224
|
+
await settle(turn.answer.trim() !== "" ? turn.answer : "(no reply)");
|
|
342
225
|
return;
|
|
343
226
|
}
|
|
344
|
-
|
|
227
|
+
if (e.type === "failed") {
|
|
345
228
|
await finish();
|
|
346
229
|
// Two audiences: the chat (customer-facing — formatError, neutral by default) and the operator
|
|
347
230
|
// log (dev-facing — the full details, via the throw below + the handler's catch). Same terminal
|
|
@@ -361,6 +244,8 @@ export async function streamFeishuReply(events, api, target, formatError, initia
|
|
|
361
244
|
}
|
|
362
245
|
throw new Error(`agent failed: ${e.details} (retryable=${e.retryable})`);
|
|
363
246
|
}
|
|
247
|
+
if (applyTurnEvent(turn, e))
|
|
248
|
+
touch();
|
|
364
249
|
}
|
|
365
250
|
throw new Error("stream ended without a terminal event"); // violates SPEC MUST 1
|
|
366
251
|
}
|
|
@@ -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 {};
|