@fastagent-sh/fastagent 0.13.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.
Files changed (238) hide show
  1. package/README.md +5 -5
  2. package/dist/agent.d.ts +20 -0
  3. package/dist/agent.js +9 -0
  4. package/dist/channels/context-buffer.d.ts +30 -0
  5. package/dist/channels/context-buffer.js +98 -0
  6. package/dist/channels/control.d.ts +28 -0
  7. package/dist/channels/control.js +214 -0
  8. package/dist/channels/feishu/card.js +1 -1
  9. package/dist/channels/feishu/context-buffer.d.ts +43 -0
  10. package/dist/channels/feishu/context-buffer.js +72 -0
  11. package/dist/channels/feishu/crypto.d.ts +4 -2
  12. package/dist/channels/feishu/crypto.js +4 -2
  13. package/dist/channels/feishu/feishu-api.d.ts +15 -7
  14. package/dist/channels/feishu/feishu-api.js +22 -4
  15. package/dist/channels/feishu/feishu.d.ts +38 -16
  16. package/dist/channels/feishu/feishu.js +286 -151
  17. package/dist/channels/feishu/invoke-turn.d.ts +24 -31
  18. package/dist/channels/feishu/invoke-turn.js +61 -62
  19. package/dist/channels/feishu/model.d.ts +98 -0
  20. package/dist/channels/feishu/model.js +9 -0
  21. package/dist/channels/feishu/normalize.d.ts +23 -0
  22. package/dist/channels/feishu/normalize.js +132 -0
  23. package/dist/channels/feishu/owned-threads.d.ts +7 -0
  24. package/dist/channels/feishu/owned-threads.js +47 -0
  25. package/dist/channels/feishu/parse.d.ts +21 -103
  26. package/dist/channels/feishu/parse.js +35 -145
  27. package/dist/channels/feishu/preview.d.ts +4 -7
  28. package/dist/channels/feishu/preview.js +26 -142
  29. package/dist/channels/feishu/register-app.d.ts +2 -1
  30. package/dist/channels/feishu/register-webhook.d.ts +2 -1
  31. package/dist/channels/feishu/scaffold/channel.ts +10 -3
  32. package/dist/channels/feishu/setup-mode.d.ts +8 -0
  33. package/dist/channels/feishu/setup-mode.js +2 -0
  34. package/dist/channels/feishu/ws-ingress.d.ts +28 -0
  35. package/dist/channels/feishu/ws-ingress.js +136 -0
  36. package/dist/channels/github/github.js +8 -6
  37. package/dist/channels/http.d.ts +14 -0
  38. package/dist/channels/http.js +35 -2
  39. package/dist/channels/invoke-turn-kit.d.ts +65 -0
  40. package/dist/channels/invoke-turn-kit.js +87 -0
  41. package/dist/channels/lark/lark.d.ts +4 -2
  42. package/dist/channels/lark/lark.js +4 -1
  43. package/dist/channels/lark/onboard.d.ts +8 -4
  44. package/dist/channels/lark/onboard.js +8 -0
  45. package/dist/channels/lark/scaffold/channel.ts +10 -3
  46. package/dist/channels/preview-kit.d.ts +109 -0
  47. package/dist/channels/preview-kit.js +183 -0
  48. package/dist/channels/seen.d.ts +5 -0
  49. package/dist/channels/seen.js +35 -0
  50. package/dist/channels/slack/bot-auth.d.ts +15 -0
  51. package/dist/channels/slack/bot-auth.js +146 -0
  52. package/dist/channels/slack/config-api.d.ts +60 -0
  53. package/dist/channels/slack/config-api.js +149 -0
  54. package/dist/channels/slack/context-buffer.d.ts +24 -0
  55. package/dist/channels/slack/context-buffer.js +37 -0
  56. package/dist/channels/slack/invoke-turn.d.ts +19 -0
  57. package/dist/channels/slack/invoke-turn.js +63 -0
  58. package/dist/channels/slack/manifest.d.ts +49 -0
  59. package/dist/channels/slack/manifest.js +69 -0
  60. package/dist/channels/slack/model.d.ts +67 -0
  61. package/dist/channels/slack/model.js +2 -0
  62. package/dist/channels/slack/onboard.d.ts +41 -0
  63. package/dist/channels/slack/onboard.js +120 -0
  64. package/dist/channels/slack/onboarding-state.d.ts +31 -0
  65. package/dist/channels/slack/onboarding-state.js +69 -0
  66. package/dist/channels/slack/owned-threads.d.ts +6 -0
  67. package/dist/channels/slack/owned-threads.js +43 -0
  68. package/dist/channels/slack/parse.d.ts +23 -0
  69. package/dist/channels/slack/parse.js +81 -0
  70. package/dist/channels/slack/preview.d.ts +24 -0
  71. package/dist/channels/slack/preview.js +359 -0
  72. package/dist/channels/slack/reaction.d.ts +24 -0
  73. package/dist/channels/slack/reaction.js +62 -0
  74. package/dist/channels/slack/register-webhook.d.ts +10 -0
  75. package/dist/channels/slack/register-webhook.js +49 -0
  76. package/dist/channels/slack/scaffold/channel.ts +33 -0
  77. package/dist/channels/slack/scaffold/slack-send.ts +171 -0
  78. package/dist/channels/slack/setup-server.d.ts +17 -0
  79. package/dist/channels/slack/setup-server.js +103 -0
  80. package/dist/channels/slack/slack-api.d.ts +77 -0
  81. package/dist/channels/slack/slack-api.js +415 -0
  82. package/dist/channels/slack/slack.d.ts +58 -0
  83. package/dist/channels/slack/slack.js +451 -0
  84. package/dist/channels/slack/welcomed.d.ts +5 -0
  85. package/dist/channels/slack/welcomed.js +32 -0
  86. package/dist/channels/state.js +3 -3
  87. package/dist/channels/stop-command.d.ts +6 -0
  88. package/dist/channels/stop-command.js +36 -0
  89. package/dist/channels/tasks.d.ts +13 -0
  90. package/dist/channels/tasks.js +10 -0
  91. package/dist/channels/telegram/context-buffer.d.ts +8 -17
  92. package/dist/channels/telegram/context-buffer.js +6 -85
  93. package/dist/channels/telegram/invoke-turn.d.ts +5 -22
  94. package/dist/channels/telegram/invoke-turn.js +11 -58
  95. package/dist/channels/telegram/preview.d.ts +4 -7
  96. package/dist/channels/telegram/preview.js +24 -142
  97. package/dist/channels/telegram/telegram.js +23 -9
  98. package/dist/channels/{feishu/text.d.ts → text.d.ts} +1 -1
  99. package/dist/channels/{feishu/text.js → text.js} +1 -1
  100. package/dist/channels/turn-queue.js +1 -1
  101. package/dist/channels/turn-store.d.ts +1 -1
  102. package/dist/channels/turn-store.js +2 -3
  103. package/dist/cli/add-feishu.d.ts +27 -0
  104. package/dist/{cli-add-feishu.js → cli/add-feishu.js} +167 -44
  105. package/dist/cli/add-slack.d.ts +10 -0
  106. package/dist/cli/add-slack.js +204 -0
  107. package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
  108. package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
  109. package/dist/cli/commands/add.d.ts +13 -0
  110. package/dist/cli/commands/add.js +274 -0
  111. package/dist/cli/commands/attach.d.ts +82 -0
  112. package/dist/cli/commands/attach.js +559 -0
  113. package/dist/cli/commands/chat.d.ts +4 -0
  114. package/dist/cli/commands/chat.js +21 -0
  115. package/dist/cli/commands/deploy.d.ts +15 -0
  116. package/dist/cli/commands/deploy.js +394 -0
  117. package/dist/cli/commands/dev.d.ts +11 -0
  118. package/dist/cli/commands/dev.js +82 -0
  119. package/dist/cli/commands/fire.d.ts +7 -0
  120. package/dist/cli/commands/fire.js +45 -0
  121. package/dist/cli/commands/info.d.ts +7 -0
  122. package/dist/cli/commands/info.js +108 -0
  123. package/dist/cli/commands/init.d.ts +8 -0
  124. package/dist/cli/commands/init.js +81 -0
  125. package/dist/cli/commands/invoke.d.ts +7 -0
  126. package/dist/cli/commands/invoke.js +28 -0
  127. package/dist/cli/commands/login.d.ts +6 -0
  128. package/dist/cli/commands/login.js +52 -0
  129. package/dist/cli/commands/models.d.ts +1 -0
  130. package/dist/cli/commands/models.js +15 -0
  131. package/dist/cli/commands/schedule.d.ts +12 -0
  132. package/dist/cli/commands/schedule.js +89 -0
  133. package/dist/cli/commands/start.d.ts +10 -0
  134. package/dist/cli/commands/start.js +97 -0
  135. package/dist/cli/commands/tool.d.ts +1 -0
  136. package/dist/cli/commands/tool.js +38 -0
  137. package/dist/cli/fail.d.ts +17 -0
  138. package/dist/cli/fail.js +32 -0
  139. package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
  140. package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
  141. package/dist/cli/kernel.d.ts +90 -0
  142. package/dist/cli/kernel.js +190 -0
  143. package/dist/cli/models-view.d.ts +21 -0
  144. package/dist/cli/models-view.js +66 -0
  145. package/dist/cli/program.d.ts +11 -0
  146. package/dist/cli/program.js +479 -0
  147. package/dist/cli/serve.d.ts +48 -0
  148. package/dist/cli/serve.js +248 -0
  149. package/dist/cli/shared.d.ts +44 -0
  150. package/dist/cli/shared.js +246 -0
  151. package/dist/cli.js +8 -1329
  152. package/dist/collect.d.ts +14 -3
  153. package/dist/collect.js +24 -0
  154. package/dist/core.d.ts +3 -1
  155. package/dist/core.js +2 -0
  156. package/dist/deploy/docker/plan.d.ts +45 -0
  157. package/dist/deploy/docker/plan.js +141 -0
  158. package/dist/deploy/docker/run.d.ts +40 -0
  159. package/dist/deploy/docker/run.js +126 -0
  160. package/dist/deploy/fly/plan.d.ts +3 -1
  161. package/dist/deploy/fly/plan.js +12 -7
  162. package/dist/deploy/fly/run.d.ts +5 -4
  163. package/dist/deploy/fly/run.js +16 -5
  164. package/dist/deploy/preflight.d.ts +10 -4
  165. package/dist/deploy/preflight.js +57 -12
  166. package/dist/deploy/railway/plan.d.ts +3 -1
  167. package/dist/deploy/railway/plan.js +12 -4
  168. package/dist/deploy/railway/run.d.ts +5 -4
  169. package/dist/deploy/railway/run.js +15 -5
  170. package/dist/deploy/runner.d.ts +6 -2
  171. package/dist/deploy/runner.js +1 -0
  172. package/dist/deploy/secrets.d.ts +2 -1
  173. package/dist/deploy/secrets.js +23 -3
  174. package/dist/dev-supervisor.d.ts +0 -2
  175. package/dist/dev-supervisor.js +7 -3
  176. package/dist/engines/pi/auth.js +160 -46
  177. package/dist/engines/pi/channel.d.ts +22 -16
  178. package/dist/engines/pi/channel.js +90 -60
  179. package/dist/engines/pi/chat.d.ts +4 -16
  180. package/dist/engines/pi/chat.js +8 -188
  181. package/dist/engines/pi/config.d.ts +23 -9
  182. package/dist/engines/pi/config.js +35 -5
  183. package/dist/engines/pi/create.d.ts +36 -7
  184. package/dist/engines/pi/create.js +63 -22
  185. package/dist/engines/pi/harness.d.ts +65 -1
  186. package/dist/engines/pi/harness.js +166 -2
  187. package/dist/engines/pi/invoke.d.ts +56 -3
  188. package/dist/engines/pi/invoke.js +340 -20
  189. package/dist/engines/pi/login.d.ts +11 -0
  190. package/dist/engines/pi/login.js +17 -5
  191. package/dist/engines/pi/models.d.ts +56 -10
  192. package/dist/engines/pi/models.js +61 -23
  193. package/dist/engines/pi/search-tools.d.ts +10 -0
  194. package/dist/engines/pi/search-tools.js +138 -0
  195. package/dist/engines/pi/session-builder.d.ts +16 -0
  196. package/dist/engines/pi/session-builder.js +308 -0
  197. package/dist/engines/pi/session-control.d.ts +50 -0
  198. package/dist/engines/pi/session-control.js +604 -0
  199. package/dist/engines/pi/sessions.d.ts +17 -2
  200. package/dist/engines/pi/sessions.js +9 -0
  201. package/dist/engines/pi/tool-context.d.ts +46 -11
  202. package/dist/engines/pi/tool-context.js +11 -9
  203. package/dist/engines/pi/tool.d.ts +35 -6
  204. package/dist/engines/pi/tool.js +47 -1
  205. package/dist/engines/pi/wake-tool.d.ts +0 -3
  206. package/dist/engines/pi/wake-tool.js +9 -7
  207. package/dist/engines/pi/workspace.d.ts +56 -1
  208. package/dist/engines/pi/workspace.js +75 -16
  209. package/dist/feishu.d.ts +1 -1
  210. package/dist/feishu.js +1 -1
  211. package/dist/host/node.d.ts +23 -6
  212. package/dist/host/node.js +5 -4
  213. package/dist/index.d.ts +1 -0
  214. package/dist/index.js +1 -0
  215. package/dist/lark.d.ts +1 -1
  216. package/dist/lark.js +1 -1
  217. package/dist/observe.js +3 -0
  218. package/dist/pi.d.ts +6 -4
  219. package/dist/pi.js +2 -1
  220. package/dist/scaffold/add-channel.d.ts +19 -6
  221. package/dist/scaffold/add-channel.js +127 -16
  222. package/dist/scaffold/templates/fastagent.config.mjs +5 -3
  223. package/dist/schedule/wakeups.d.ts +0 -3
  224. package/dist/schedule/wakeups.js +1 -1
  225. package/dist/session-remote.d.ts +53 -0
  226. package/dist/session-remote.js +336 -0
  227. package/dist/session.d.ts +265 -0
  228. package/dist/session.js +37 -0
  229. package/dist/slack.d.ts +2 -0
  230. package/dist/slack.js +2 -0
  231. package/dist/tunnel.d.ts +7 -4
  232. package/dist/tunnel.js +21 -10
  233. package/package.json +22 -7
  234. package/dist/channels/feishu/seen.d.ts +0 -5
  235. package/dist/channels/feishu/seen.js +0 -47
  236. package/dist/cli-add-feishu.d.ts +0 -8
  237. package/dist/cli-models.d.ts +0 -11
  238. package/dist/cli-models.js +0 -20
@@ -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
- // A non-empty secret is mandatory: verify() against an empty key accepts a signature anyone can
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
  }
@@ -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.
@@ -12,9 +12,32 @@ 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
- const MAX_BODY_BYTES = 1 << 20;
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
+ }
@@ -1,3 +1,4 @@
1
+ import type { FeishuGroupBehavior, FeishuSubscriptionMode } from "../feishu/setup-mode.ts";
1
2
  /**
2
3
  * Guided Lark-international onboarding. The intl cloud cannot complete the BOUND scan-to-create flow,
3
4
  * so a new/partial setup opens its unbound one-click launcher and collects one App-scoped credential
@@ -17,7 +18,7 @@ export interface LarkOnboardIO {
17
18
  hidden?: boolean;
18
19
  }): Promise<string | undefined>;
19
20
  }
20
- export interface LarkBootstrapResult {
21
+ interface LarkBootstrapResult {
21
22
  /** Challenge-captured token: the PATCH also switched Subscription mode to webhook. */
22
23
  token?: string;
23
24
  /** Present only for a definitive config-route 404; tells the user why the manual path is active. */
@@ -26,14 +27,17 @@ export interface LarkBootstrapResult {
26
27
  export interface LarkOnboardOptions {
27
28
  /** Existing active .env values. A complete credential pair is reused (and still validated). */
28
29
  existing?: Readonly<Record<string, string | undefined>>;
30
+ ingress?: FeishuSubscriptionMode;
31
+ groupBehavior?: FeishuGroupBehavior;
29
32
  verifyCredentials(appId: string, appSecret: string): Promise<void>;
30
- bootstrapWebhook(appId: string, appSecret: string): Promise<LarkBootstrapResult>;
33
+ bootstrapWebhook?(appId: string, appSecret: string): Promise<LarkBootstrapResult>;
31
34
  }
32
- export interface LarkOnboardCredentials extends Record<string, string> {
35
+ export interface LarkOnboardCredentials {
33
36
  LARK_APP_ID: string;
34
37
  LARK_APP_SECRET: string;
35
- LARK_VERIFICATION_TOKEN: string;
38
+ LARK_VERIFICATION_TOKEN?: string;
36
39
  }
37
40
  /** Open the stable app console and collect everything the runtime needs. Cancellation is a visible
38
41
  * failure: the scaffold remains and `add lark` is deliberately re-runnable to resume onboarding. */
39
42
  export declare function onboardLarkApp(io: LarkOnboardIO, opts: LarkOnboardOptions): Promise<LarkOnboardCredentials>;
43
+ export {};
@@ -39,7 +39,15 @@ export async function onboardLarkApp(io, opts) {
39
39
  const eventSecurityUrl = larkEventSecurityUrl(appId);
40
40
  io.note(`App ID / Secret verified. Opening Events & Callbacks → Security: ${eventSecurityUrl}`);
41
41
  io.openUrl(eventSecurityUrl);
42
+ if (opts.ingress === "websocket") {
43
+ io.note(opts.groupBehavior === "mentions"
44
+ ? "Choose long connection, subscribe im.message.receive_v1, then create + publish an app version. No Verification Token or Request URL is needed."
45
+ : "Choose long connection and subscribe im.message.receive_v1, but do not publish yet — context-aware group permission setup follows. No Verification Token or Request URL is needed.");
46
+ return { LARK_APP_ID: appId, LARK_APP_SECRET: appSecret };
47
+ }
42
48
  io.note("Trying automatic webhook-mode + Verification-Token bootstrap…");
49
+ if (!opts.bootstrapWebhook)
50
+ throw new Error("Lark webhook onboarding requires bootstrapWebhook");
43
51
  const bootstrap = await opts.bootstrapWebhook(appId, appSecret);
44
52
  if (bootstrap.token) {
45
53
  io.note("Verification Token captured; Subscription mode changed to webhook in the app draft.");
@@ -7,7 +7,9 @@ import { larkChannel } from "@fastagent-sh/fastagent/lark";
7
7
  // 1. create a custom app → enable the BOT capability → copy App ID / App Secret into .env
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
- // card scope ("Create and update card" — the live preview streams through a card)
10
+ // card scope ("Create and update card" — the live preview streams through a card). To answer bare
11
+ // messages in Agent-managed threads 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.
11
13
  // 3. Events & Callbacks → subscribe to `im.message.receive_v1`; copy the Verification Token into
12
14
  // .env; RECOMMENDED: set an Encrypt Key there and mirror it in LARK_ENCRYPT_KEY
13
15
  // 4. run `fastagent dev --tunnel`: it attempts to switch Subscription mode to webhook + register
@@ -19,14 +21,19 @@ export default larkChannel({
19
21
  appSecret: process.env.LARK_APP_SECRET ?? "",
20
22
  verificationToken: process.env.LARK_VERIFICATION_TOKEN ?? "", // authenticates inbound events
21
23
  encryptKey: process.env.LARK_ENCRYPT_KEY || undefined, // optional; when set, plaintext events are refused
24
+ // Direct and group chats default to one Agent session/thread per top-level ask. Opt out independently:
25
+ // directMessageSession: "continuous",
26
+ // groupMessageSession: "continuous",
22
27
  // Dev/personal bot: surface raw errors to the chat so you (and your AI agent) can act on them. The
23
28
  // chat is customer-facing by default — for a public bot, drop this or return a neutral string;
24
29
  // full details always go to the server log regardless.
25
30
  onError: (failed) => `⚠️ ${failed.details}`,
26
31
  // The channel owns transport + format (markdown card) + attachments (image→vision, file→disk) +
27
32
  // the live streaming preview. `route` (POLICY) is OPTIONAL — omitted, it uses defaultLarkRoute:
28
- // p2p chats always answer, groups only on an @mention of this bot (matched by open_id, resolved at
29
- // startup). Override to customise, reusing the export:
33
+ // p2p chats always answer; groups answer on @this-bot, plus bare continuations in Agent-managed
34
+ // threads. Other human group/thread discussion buffers until that place's next answered turn;
35
+ // @other-only messages in managed threads buffer rather than triggering the Agent.
36
+ // Override to customise explicit routing, reusing the export:
30
37
  // route: (e) => defaultLarkRoute(e, { botOpenId: "ou_xxx" }) && { session: `user:${e.sender?.sender_id?.open_id}` },
31
38
  // route: (e) => defaultLarkRoute(e, { botOpenId: "ou_xxx" }) && { text: `${larkEnvelope(e)}\n[extra]` },
32
39
  });
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Channel-neutral live-preview pieces shared by every messaging channel's preview renderer
3
+ * (telegram/preview.ts, feishu/preview.ts, slack/preview.ts): the turn-view REDUCER (the one
4
+ * event → view-state machine every renderer consumes), its line renderers, the terminal-failure
5
+ * shape a channel hands to its `onError`, the customer-facing wording for it, and the serialized
6
+ * single-writer pump. DELIVERY stays per-platform — message edits vs streaming cards vs chat.update,
7
+ * pacing constants, reveal timing, and terminal-write policies are real platform differences — but
8
+ * everything platform-independent lives here, so a new event type or a wording change lands in ONE
9
+ * place instead of one hunk per channel.
10
+ */
11
+ import type { AgentEvent } from "../agent.ts";
12
+ /** A terminal failure, as a channel hands it to its `onError`. */
13
+ export interface ChannelFailure {
14
+ details: string;
15
+ retryable: boolean;
16
+ }
17
+ /** The customer-facing default: neutral, no leaked internals. Differentiate on retryability and always
18
+ * offer a next step (Slack agent-design: never leave the user with a dead-end "something went wrong").
19
+ * The non-retryable branch keeps the "something went wrong" phrase deliberately — it is neutral (we only
20
+ * know a boolean, never the specific limitation) and shared verbatim across channels. */
21
+ export declare function defaultErrorMessage(failed: ChannelFailure): string;
22
+ /** Customer-facing live-preview line for an engine-internal retry backoff (the advisory `retrying`
23
+ * event): neutral, no leaked internals — the reason stays in operator logs. */
24
+ export declare const RETRY_NOTICE = "\u23F3 Temporary problem \u2014 retrying\u2026";
25
+ /** The placeholder shown before any reasoning/tool/text arrives. */
26
+ export declare const THINKING_PLACEHOLDER = "\uD83D\uDCAD Thinking\u2026";
27
+ /** One tool call's line in the live view. */
28
+ interface ToolLine {
29
+ label: string;
30
+ status: "running" | "ok" | "error";
31
+ }
32
+ /**
33
+ * The channel-neutral view STATE of one in-flight turn. Renderers own everything after this state:
34
+ * when to reveal the young answer (age vs timer), how to format (HTML / card markdown / mrkdwn),
35
+ * and how to deliver frames. Terminal events are deliberately NOT view state — completed/failed
36
+ * resolve the preview into a final write, which is each platform's terminal-write policy.
37
+ */
38
+ export interface TurnView {
39
+ thinking: string;
40
+ tools: ToolLine[];
41
+ /** tool-call id → its line, for `tool_ended` status flips (bookkeeping; renderers read `tools`). */
42
+ toolById: Map<string, ToolLine>;
43
+ answer: string;
44
+ /** Arrival time of the first non-empty answer delta; age-based reveal policies read it. */
45
+ answerSince?: number;
46
+ /** An advisory retry backoff is in progress (closed again by any subsequent progress event). */
47
+ retrying: boolean;
48
+ }
49
+ export declare function createTurnView(): TurnView;
50
+ /**
51
+ * Apply one event to the view state. Returns true when the view changed (the caller repaints).
52
+ * This is the ONE place the shared view rules live: tool labels are humanized with a compact arg
53
+ * summary, and any progress event closes an open retry notice (a stale "retrying" line must never
54
+ * outlive actual progress). Terminal events only close the notice — they are the caller's business.
55
+ */
56
+ export declare function applyTurnEvent(view: TurnView, e: AgentEvent): boolean;
57
+ /** The tool-activity block: one `🔧 label …/✓/✗` line per call, in call order. */
58
+ export declare function toolLines(view: TurnView): string;
59
+ /** The reasoning peek: the most recent tail of the (growing) reasoning, one line, code-point safe.
60
+ * Process, not the answer — renderers show it live only, never in the persisted final message. */
61
+ export declare function thinkingLine(view: TurnView, maxTail: number): string;
62
+ /**
63
+ * The shared answer-reveal policy: the answer stays hidden until its first delta has aged one
64
+ * throttle window (`ageMs` — each platform passes its own pacing constant). The pump's leading-edge
65
+ * flush would otherwise turn the very first content delta (often a lone character or unbalanced
66
+ * markup) into its own frame — the short-reply flicker (placeholder → "O" → "OK."). Aging is
67
+ * anchored at delta ARRIVAL (`answerSince`, set by the reducer) so an in-flight write can't skew the
68
+ * clock, and there is deliberately NO timer at the boundary: a young answer surfaces on the next
69
+ * content-driven pass, so a turn completing within the window delivers the final answer only.
70
+ */
71
+ export declare function revealedAnswer(view: TurnView, ageMs: number): string;
72
+ /** Compose body parts (thinking/tools/retry/answer) into one frame: skip empties, blank-line joins. */
73
+ export declare function composeTurnBody(parts: readonly string[]): string;
74
+ /**
75
+ * The serialized live-preview writer shared by the edit/snapshot renderers (telegram, feishu).
76
+ * Events mark the view dirty; the pump repaints to the LATEST view with at most ONE write in
77
+ * flight, paced by `throttleMs`. One-in-flight is the whole point: concurrent writes can land out
78
+ * of order — an older frame over a newer one is the "shows 3-4 steps, blanks, re-fills" flicker —
79
+ * so a single writer keeps frames monotonic (and makes feishu's strictly-increasing card `sequence`
80
+ * correct by construction). NOT used by slack-classic: its pacing lives inside the flush (the 3s
81
+ * chat.update rate slot), not at the frame boundary.
82
+ */
83
+ export interface PreviewPump {
84
+ /** Mark the view dirty and ensure the single writer runs (an in-flight write picks the new state
85
+ * up on its next loop). Synchronous — callers never await a network write. */
86
+ touch(): void;
87
+ /** Stop the pump, cut an in-flight throttle short, and await any in-flight write — so the
88
+ * caller's terminal write is strictly the LAST one (no stale frame landing after the answer). */
89
+ finish(): Promise<void>;
90
+ }
91
+ export declare function createPreviewPump(opts: {
92
+ /** Write the LATEST view. Best-effort — the terminal write is authoritative. */
93
+ flush: () => Promise<void>;
94
+ /** Pace + coalesce a burst into one write; finish() interrupts a throttle in progress. */
95
+ throttleMs: number;
96
+ /** Called for the FIRST failing flush only (the pump keeps running): a never-rendering preview
97
+ * must be diagnosable, not silent — and not a log flood. */
98
+ onError: (error: unknown) => void;
99
+ }): PreviewPump;
100
+ /**
101
+ * A plain-language label for a tool call, following Slack's agent-design guidance to name what a tool
102
+ * does rather than expose a raw identifier ("Create issue", not "create_issue"; "Github: create issue",
103
+ * not "mcp__github__create_issue"). Deliberately generic and engine-neutral: it only reshapes the
104
+ * identifier string — it never invents semantics and never exposes arguments. An `mcp__server__tool`
105
+ * identifier becomes `server: tool`; any other identifier has its separators normalized to spaces and
106
+ * its first letter capitalized.
107
+ */
108
+ export declare function humanizeToolName(name: string): string;
109
+ export {};