@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.
Files changed (277) hide show
  1. package/README.md +16 -6
  2. package/dist/agent.d.ts +20 -0
  3. package/dist/agent.js +9 -0
  4. package/dist/channels/agentcore-limits.d.ts +7 -0
  5. package/dist/channels/agentcore-limits.js +9 -0
  6. package/dist/channels/agentcore-state.d.ts +83 -0
  7. package/dist/channels/agentcore-state.js +258 -0
  8. package/dist/channels/agentcore.d.ts +98 -0
  9. package/dist/channels/agentcore.js +260 -0
  10. package/dist/channels/busy.d.ts +23 -0
  11. package/dist/channels/busy.js +53 -0
  12. package/dist/channels/context-buffer.d.ts +35 -0
  13. package/dist/channels/context-buffer.js +103 -0
  14. package/dist/channels/control.d.ts +28 -0
  15. package/dist/channels/control.js +214 -0
  16. package/dist/channels/feishu/card.js +1 -1
  17. package/dist/channels/feishu/context-buffer.d.ts +15 -13
  18. package/dist/channels/feishu/context-buffer.js +62 -69
  19. package/dist/channels/feishu/feishu-api.d.ts +11 -3
  20. package/dist/channels/feishu/feishu-api.js +24 -3
  21. package/dist/channels/feishu/feishu.d.ts +27 -27
  22. package/dist/channels/feishu/feishu.js +318 -207
  23. package/dist/channels/feishu/invoke-turn.d.ts +7 -20
  24. package/dist/channels/feishu/invoke-turn.js +44 -79
  25. package/dist/channels/feishu/model.d.ts +2 -2
  26. package/dist/channels/feishu/normalize.d.ts +2 -1
  27. package/dist/channels/feishu/normalize.js +0 -1
  28. package/dist/channels/feishu/parse.d.ts +22 -8
  29. package/dist/channels/feishu/parse.js +24 -7
  30. package/dist/channels/feishu/preview.d.ts +4 -7
  31. package/dist/channels/feishu/preview.js +27 -142
  32. package/dist/channels/feishu/register-app.d.ts +2 -1
  33. package/dist/channels/feishu/register-webhook.d.ts +2 -1
  34. package/dist/channels/feishu/scaffold/channel.ts +9 -8
  35. package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
  36. package/dist/channels/feishu/setup-mode.d.ts +37 -0
  37. package/dist/channels/feishu/setup-mode.js +28 -0
  38. package/dist/channels/feishu/ws-ingress.d.ts +28 -0
  39. package/dist/channels/feishu/ws-ingress.js +136 -0
  40. package/dist/channels/github/github.js +16 -7
  41. package/dist/channels/http.d.ts +14 -0
  42. package/dist/channels/http.js +36 -3
  43. package/dist/channels/invoke-turn-kit.d.ts +65 -0
  44. package/dist/channels/invoke-turn-kit.js +87 -0
  45. package/dist/channels/lark/lark.d.ts +4 -2
  46. package/dist/channels/lark/lark.js +4 -1
  47. package/dist/channels/lark/onboard.d.ts +8 -4
  48. package/dist/channels/lark/onboard.js +8 -0
  49. package/dist/channels/lark/scaffold/channel.ts +9 -8
  50. package/dist/channels/lark/scaffold/lark-send.ts +6 -4
  51. package/dist/channels/preview-kit.d.ts +115 -0
  52. package/dist/channels/preview-kit.js +184 -0
  53. package/dist/channels/seen.d.ts +5 -0
  54. package/dist/channels/seen.js +35 -0
  55. package/dist/channels/slack/bot-auth.d.ts +15 -0
  56. package/dist/channels/slack/bot-auth.js +146 -0
  57. package/dist/channels/slack/config-api.d.ts +60 -0
  58. package/dist/channels/slack/config-api.js +149 -0
  59. package/dist/channels/slack/context-buffer.d.ts +24 -0
  60. package/dist/channels/slack/context-buffer.js +37 -0
  61. package/dist/channels/slack/invoke-turn.d.ts +19 -0
  62. package/dist/channels/slack/invoke-turn.js +63 -0
  63. package/dist/channels/slack/manifest.d.ts +49 -0
  64. package/dist/channels/slack/manifest.js +69 -0
  65. package/dist/channels/slack/model.d.ts +67 -0
  66. package/dist/channels/slack/model.js +2 -0
  67. package/dist/channels/slack/onboard.d.ts +41 -0
  68. package/dist/channels/slack/onboard.js +120 -0
  69. package/dist/channels/slack/onboarding-state.d.ts +31 -0
  70. package/dist/channels/slack/onboarding-state.js +69 -0
  71. package/dist/channels/slack/parse.d.ts +38 -0
  72. package/dist/channels/slack/parse.js +124 -0
  73. package/dist/channels/slack/preview.d.ts +23 -0
  74. package/dist/channels/slack/preview.js +403 -0
  75. package/dist/channels/slack/reaction.d.ts +24 -0
  76. package/dist/channels/slack/reaction.js +62 -0
  77. package/dist/channels/slack/register-webhook.d.ts +10 -0
  78. package/dist/channels/slack/register-webhook.js +49 -0
  79. package/dist/channels/slack/scaffold/channel.ts +33 -0
  80. package/dist/channels/slack/scaffold/slack-send.ts +171 -0
  81. package/dist/channels/slack/setup-server.d.ts +17 -0
  82. package/dist/channels/slack/setup-server.js +103 -0
  83. package/dist/channels/slack/slack-api.d.ts +57 -0
  84. package/dist/channels/slack/slack-api.js +399 -0
  85. package/dist/channels/slack/slack.d.ts +51 -0
  86. package/dist/channels/slack/slack.js +496 -0
  87. package/dist/channels/slack/welcomed.d.ts +5 -0
  88. package/dist/channels/slack/welcomed.js +32 -0
  89. package/dist/channels/state.d.ts +11 -4
  90. package/dist/channels/state.js +22 -15
  91. package/dist/channels/stop-command.d.ts +6 -0
  92. package/dist/channels/stop-command.js +36 -0
  93. package/dist/channels/tasks.d.ts +7 -0
  94. package/dist/channels/tasks.js +25 -0
  95. package/dist/channels/telegram/context-buffer.d.ts +8 -17
  96. package/dist/channels/telegram/context-buffer.js +6 -85
  97. package/dist/channels/telegram/invoke-turn.d.ts +5 -22
  98. package/dist/channels/telegram/invoke-turn.js +11 -58
  99. package/dist/channels/telegram/parse.d.ts +0 -7
  100. package/dist/channels/telegram/parse.js +4 -2
  101. package/dist/channels/telegram/preview.d.ts +4 -7
  102. package/dist/channels/telegram/preview.js +24 -142
  103. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
  104. package/dist/channels/telegram/telegram.js +24 -10
  105. package/dist/channels/{feishu/text.d.ts → text.d.ts} +15 -1
  106. package/dist/channels/{feishu/text.js → text.js} +15 -1
  107. package/dist/channels/thread-participants.d.ts +21 -0
  108. package/dist/channels/thread-participants.js +132 -0
  109. package/dist/channels/turn-queue.js +8 -1
  110. package/dist/channels/turn-store.d.ts +1 -1
  111. package/dist/channels/turn-store.js +2 -3
  112. package/dist/cli/add-feishu.d.ts +30 -0
  113. package/dist/cli/add-feishu.js +366 -0
  114. package/dist/cli/add-slack.d.ts +11 -0
  115. package/dist/cli/add-slack.js +199 -0
  116. package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
  117. package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
  118. package/dist/cli/commands/add.d.ts +4 -0
  119. package/dist/cli/commands/add.js +191 -60
  120. package/dist/cli/commands/attach.d.ts +82 -0
  121. package/dist/cli/commands/attach.js +563 -0
  122. package/dist/cli/commands/chat.d.ts +1 -0
  123. package/dist/cli/commands/chat.js +12 -7
  124. package/dist/cli/commands/deploy.d.ts +3 -1
  125. package/dist/cli/commands/deploy.js +384 -82
  126. package/dist/cli/commands/dev.js +34 -26
  127. package/dist/cli/commands/fire.js +17 -18
  128. package/dist/cli/commands/info.js +36 -29
  129. package/dist/cli/commands/init.d.ts +1 -1
  130. package/dist/cli/commands/init.js +65 -53
  131. package/dist/cli/commands/invoke.js +11 -9
  132. package/dist/cli/commands/login.js +48 -45
  133. package/dist/cli/commands/models.js +1 -1
  134. package/dist/cli/commands/schedule.js +6 -8
  135. package/dist/cli/commands/start.js +102 -39
  136. package/dist/cli/commands/tool.js +23 -18
  137. package/dist/cli/fail.d.ts +17 -2
  138. package/dist/cli/fail.js +25 -1
  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 +3 -2
  142. package/dist/cli/models-view.d.ts +21 -0
  143. package/dist/cli/models-view.js +66 -0
  144. package/dist/cli/program.js +135 -56
  145. package/dist/cli/serve.d.ts +60 -20
  146. package/dist/cli/serve.js +252 -47
  147. package/dist/cli/shared.d.ts +40 -7
  148. package/dist/cli/shared.js +180 -38
  149. package/dist/collect.d.ts +14 -3
  150. package/dist/collect.js +24 -0
  151. package/dist/core.d.ts +3 -1
  152. package/dist/core.js +2 -0
  153. package/dist/deploy/agentcore/plan.d.ts +117 -0
  154. package/dist/deploy/agentcore/plan.js +721 -0
  155. package/dist/deploy/agentcore/run.d.ts +73 -0
  156. package/dist/deploy/agentcore/run.js +412 -0
  157. package/dist/deploy/agentcore/zip.d.ts +17 -0
  158. package/dist/deploy/agentcore/zip.js +68 -0
  159. package/dist/deploy/container.d.ts +26 -25
  160. package/dist/deploy/container.js +93 -89
  161. package/dist/deploy/docker/plan.d.ts +3 -3
  162. package/dist/deploy/docker/plan.js +20 -23
  163. package/dist/deploy/fly/plan.d.ts +5 -1
  164. package/dist/deploy/fly/plan.js +39 -26
  165. package/dist/deploy/fly/run.d.ts +17 -5
  166. package/dist/deploy/fly/run.js +52 -7
  167. package/dist/deploy/preflight.d.ts +20 -8
  168. package/dist/deploy/preflight.js +267 -71
  169. package/dist/deploy/railway/plan.d.ts +10 -1
  170. package/dist/deploy/railway/plan.js +53 -20
  171. package/dist/deploy/railway/run.d.ts +13 -5
  172. package/dist/deploy/railway/run.js +22 -7
  173. package/dist/deploy/runner.d.ts +7 -3
  174. package/dist/deploy/runner.js +9 -3
  175. package/dist/deploy/secrets.d.ts +2 -1
  176. package/dist/deploy/secrets.js +23 -3
  177. package/dist/dev-supervisor.d.ts +11 -10
  178. package/dist/dev-supervisor.js +56 -50
  179. package/dist/engines/pi/auth.d.ts +8 -7
  180. package/dist/engines/pi/auth.js +12 -10
  181. package/dist/engines/pi/channel.d.ts +22 -16
  182. package/dist/engines/pi/channel.js +92 -62
  183. package/dist/engines/pi/chat.d.ts +4 -16
  184. package/dist/engines/pi/chat.js +8 -261
  185. package/dist/engines/pi/config.d.ts +20 -55
  186. package/dist/engines/pi/config.js +49 -121
  187. package/dist/engines/pi/create.d.ts +30 -10
  188. package/dist/engines/pi/create.js +33 -26
  189. package/dist/engines/pi/definition.d.ts +7 -26
  190. package/dist/engines/pi/definition.js +8 -54
  191. package/dist/engines/pi/harness.d.ts +49 -0
  192. package/dist/engines/pi/harness.js +90 -2
  193. package/dist/engines/pi/invoke.d.ts +55 -2
  194. package/dist/engines/pi/invoke.js +304 -19
  195. package/dist/engines/pi/login.d.ts +12 -1
  196. package/dist/engines/pi/login.js +16 -4
  197. package/dist/engines/pi/models.d.ts +59 -13
  198. package/dist/engines/pi/models.js +62 -24
  199. package/dist/engines/pi/open.d.ts +114 -0
  200. package/dist/engines/pi/open.js +127 -0
  201. package/dist/engines/pi/session-builder.d.ts +16 -0
  202. package/dist/engines/pi/session-builder.js +308 -0
  203. package/dist/engines/pi/session-control.d.ts +50 -0
  204. package/dist/engines/pi/session-control.js +604 -0
  205. package/dist/engines/pi/sessions.d.ts +17 -2
  206. package/dist/engines/pi/sessions.js +9 -0
  207. package/dist/engines/pi/tool-context.d.ts +18 -11
  208. package/dist/engines/pi/tool-context.js +3 -9
  209. package/dist/engines/pi/tool.d.ts +6 -8
  210. package/dist/engines/pi/tool.js +10 -1
  211. package/dist/engines/pi/wake-tool.d.ts +0 -3
  212. package/dist/engines/pi/wake-tool.js +9 -7
  213. package/dist/env.d.ts +16 -4
  214. package/dist/env.js +43 -5
  215. package/dist/feishu.d.ts +1 -1
  216. package/dist/feishu.js +1 -1
  217. package/dist/host/node.d.ts +25 -8
  218. package/dist/host/node.js +5 -4
  219. package/dist/index.d.ts +1 -0
  220. package/dist/index.js +1 -0
  221. package/dist/lark.d.ts +1 -1
  222. package/dist/lark.js +1 -1
  223. package/dist/loader.d.ts +2 -2
  224. package/dist/loader.js +3 -3
  225. package/dist/log.d.ts +1 -1
  226. package/dist/log.js +1 -1
  227. package/dist/observe.js +3 -0
  228. package/dist/paths.d.ts +138 -0
  229. package/dist/paths.js +326 -0
  230. package/dist/pi.d.ts +6 -5
  231. package/dist/pi.js +4 -3
  232. package/dist/runtime.d.ts +7 -5
  233. package/dist/runtime.js +2 -2
  234. package/dist/scaffold/add-channel.d.ts +26 -9
  235. package/dist/scaffold/add-channel.js +175 -40
  236. package/dist/scaffold/init.d.ts +32 -41
  237. package/dist/scaffold/init.js +161 -185
  238. package/dist/scaffold/templates/env.example +15 -6
  239. package/dist/scaffold/templates/fastagent.config.mjs +5 -4
  240. package/dist/scaffold/templates/gitignore +14 -6
  241. package/dist/scaffold/templates/persona.md +4 -2
  242. package/dist/scaffold/templates/secrets.gitignore +5 -0
  243. package/dist/scaffold/templates.d.ts +1 -7
  244. package/dist/scaffold/templates.js +3 -25
  245. package/dist/scaffold/vendor-skill.d.ts +2 -2
  246. package/dist/scaffold/vendor-skill.js +13 -13
  247. package/dist/schedule/discover.js +4 -4
  248. package/dist/schedule/scheduler.d.ts +40 -1
  249. package/dist/schedule/scheduler.js +89 -56
  250. package/dist/schedule/state.js +1 -1
  251. package/dist/schedule/wake-alarm.d.ts +47 -0
  252. package/dist/schedule/wake-alarm.js +136 -0
  253. package/dist/schedule/wakeups.d.ts +1 -3
  254. package/dist/schedule/wakeups.js +19 -1
  255. package/dist/session-remote.d.ts +53 -0
  256. package/dist/session-remote.js +336 -0
  257. package/dist/session.d.ts +265 -0
  258. package/dist/session.js +37 -0
  259. package/dist/slack.d.ts +2 -0
  260. package/dist/slack.js +2 -0
  261. package/dist/tunnel.d.ts +7 -4
  262. package/dist/tunnel.js +23 -12
  263. package/package.json +22 -7
  264. package/dist/channels/feishu/owned-threads.d.ts +0 -11
  265. package/dist/channels/feishu/owned-threads.js +0 -47
  266. package/dist/channels/feishu/seen.d.ts +0 -5
  267. package/dist/channels/feishu/seen.js +0 -47
  268. package/dist/cli-add-feishu.d.ts +0 -8
  269. package/dist/cli-add-feishu.js +0 -223
  270. package/dist/cli-models.d.ts +0 -11
  271. package/dist/cli-models.js +0 -20
  272. package/dist/engines/pi/workspace.d.ts +0 -56
  273. package/dist/engines/pi/workspace.js +0 -72
  274. package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
  275. package/dist/scaffold/templates/gitignore.kit +0 -2
  276. package/dist/workspace.d.ts +0 -9
  277. package/dist/workspace.js +0 -45
@@ -0,0 +1,336 @@
1
+ import { SSE_HEARTBEAT_MS } from "./channels/http.js";
2
+ import { abortFirstIterator } from "./collect.js";
3
+ /** Dead-connection watchdog for SSE reads: the server heartbeats every SSE_HEARTBEAT_MS, so a
4
+ * PENDING READ seeing no bytes (of ANY kind — comments included) for this many missed beats
5
+ * means the connection is a black hole. The stream is aborted and surfaced as an error, so a
6
+ * consumer's failure budget ticks instead of hanging forever. Quiet-but-alive streams (a long
7
+ * tool call) keep heartbeating and never trip this. */
8
+ const SSE_IDLE_LIMIT_MS = 3 * SSE_HEARTBEAT_MS;
9
+ function idleWatchdog(abort) {
10
+ let armedAt;
11
+ let stale = false;
12
+ const timer = setInterval(() => {
13
+ if (armedAt !== undefined && Date.now() - armedAt > SSE_IDLE_LIMIT_MS) {
14
+ stale = true;
15
+ abort.abort();
16
+ }
17
+ }, SSE_HEARTBEAT_MS);
18
+ return {
19
+ arm: () => {
20
+ armedAt ??= Date.now();
21
+ },
22
+ disarm: () => {
23
+ armedAt = undefined;
24
+ },
25
+ stale: () => stale,
26
+ stop: () => clearInterval(timer),
27
+ };
28
+ }
29
+ /** A control request the server answered with a non-2xx status. Carries the STRUCTURED status so a
30
+ * consumer distinguishing auth failure (401 — stale token, unrecoverable) from transient transport
31
+ * trouble branches on `status`, never on message prose. */
32
+ export class ControlRequestError extends Error {
33
+ status;
34
+ constructor(status, body) {
35
+ super(`control request failed: ${status} ${body}`);
36
+ this.status = status;
37
+ }
38
+ }
39
+ /**
40
+ * Connect and return a remote `SessionControl`. Async because `capabilities()` is synchronous in
41
+ * the contract: the static declaration is fetched ONCE here and served from memory — which also
42
+ * makes a wrong URL/token fail at connect time, not on first use.
43
+ */
44
+ export async function connectSessionControl(options) {
45
+ const { url, token, fetchFn = fetch } = options;
46
+ const base = url.replace(/\/$/, "");
47
+ const headers = { authorization: `Bearer ${token}` };
48
+ // Non-streaming requests carry a TIMEOUT: attach's whole reliability model counts failed rounds
49
+ // against a budget ("unreachable for ~Ns"), which a black-hole endpoint (firewall drop, half-dead
50
+ // tunnel) would silently defeat — a hung state()/entries() ticks nothing. The SSE stream stays
51
+ // timeout-free (quiet is normal there; heartbeats cover proxy idling).
52
+ const REQUEST_TIMEOUT_MS = 10_000;
53
+ // The PAYLOAD-bearing calls get a longer budget than the black-hole detector's 10s — in both
54
+ // directions: dispatch may UPLOAD up to the 1 MiB body cap (base64 images in steer/follow_up),
55
+ // and entries may DOWNLOAD a long session's full record (a cursor-less first backfill) — a slow
56
+ // link legitimately needs longer, and cutting a healthy transfer would be indistinguishable from
57
+ // a dead endpoint. capabilities/state stay on the short detector: they are small by contract.
58
+ const PAYLOAD_TIMEOUT_MS = 60_000;
59
+ const get = async (path, timeoutMs = REQUEST_TIMEOUT_MS) => {
60
+ const res = await fetchFn(`${base}${path}`, { headers, signal: AbortSignal.timeout(timeoutMs) });
61
+ if (!res.ok)
62
+ throw new ControlRequestError(res.status, await res.text());
63
+ return (await res.json());
64
+ };
65
+ const capabilities = await get("/control/capabilities");
66
+ return {
67
+ capabilities: () => capabilities,
68
+ state: (session) => get(`/control/state?session=${encodeURIComponent(session)}`),
69
+ entries: (session, opts) => get(`/control/entries?session=${encodeURIComponent(session)}${opts?.since !== undefined ? `&since=${encodeURIComponent(opts.since)}` : ""}`, PAYLOAD_TIMEOUT_MS),
70
+ async dispatch(session, command) {
71
+ const res = await fetchFn(`${base}/control/dispatch`, {
72
+ method: "POST",
73
+ headers: { ...headers, "content-type": "application/json" },
74
+ body: JSON.stringify({ session, command }),
75
+ signal: AbortSignal.timeout(PAYLOAD_TIMEOUT_MS),
76
+ });
77
+ if (!res.ok)
78
+ throw new ControlRequestError(res.status, await res.text());
79
+ return (await res.json());
80
+ },
81
+ events(session) {
82
+ // Each ITERATION opens its own connection (gen/abort created inside asyncIterator), matching
83
+ // the local hub's "every iteration is a fresh subscription" — a shared single-use generator
84
+ // would make the second for-await silently empty, breaking local/remote isomorphism.
85
+ // The abort controller lives OUTSIDE the generator: a consumer's `return()`/`break` while the
86
+ // generator is suspended on a quiet SSE read must abort the fetch FIRST — an async generator's
87
+ // own finally only runs after the pending await settles, which a silent stream never does.
88
+ const openStream = (abort) => (async function* iterate() {
89
+ // Armed BEFORE the fetch: the connect phase (headers never arriving from a black-holed
90
+ // endpoint) is otherwise a window no timeout covers — the same watchdog terminates it,
91
+ // with headers-arrival counting as the first sign of life.
92
+ const watchdog = idleWatchdog(abort);
93
+ watchdog.arm(); // the connect await is a pending read
94
+ try {
95
+ const res = await fetchFn(`${base}/control/events?session=${encodeURIComponent(session)}`, {
96
+ headers,
97
+ signal: abort.signal,
98
+ });
99
+ watchdog.disarm(); // headers arrived
100
+ if (!res.ok) {
101
+ // The error body is a pending read too — a half-dead tunnel serving 4xx headers then
102
+ // black-holing the body must not hang the round outside every budget. Re-armed: the
103
+ // watchdog aborts the read and the round fails with the dead-connection diagnosis.
104
+ watchdog.arm();
105
+ throw new ControlRequestError(res.status, await res.text());
106
+ }
107
+ if (!res.body)
108
+ throw new Error("control events: response has no body");
109
+ let nextSeq = 0;
110
+ for await (const data of sseData(res.body, watchdog)) {
111
+ // Parse discipline, same as the other two wire planes (dispatch parses, invoke
112
+ // classifies drift): a non-JSON or non-envelope payload is PROTOCOL MISMATCH —
113
+ // thrown, so a consumer's failure budget applies — never misdiagnosed as an
114
+ // in-transit gap whose remedy (reconnect) can never fix it.
115
+ let wire;
116
+ try {
117
+ // The ONE envelope type (control.ts's WireEvent) — an inline shape would let the
118
+ // envelope drift server-side while this cast silently kept the old fields.
119
+ wire = JSON.parse(data);
120
+ }
121
+ catch (parseError) {
122
+ throw new Error(`control events: non-JSON data on the stream (${String(parseError)}) — protocol mismatch?`);
123
+ }
124
+ if (typeof wire.seq !== "number" || typeof wire.event !== "object" || wire.event === null) {
125
+ throw new Error("control events: malformed envelope — the endpoint does not speak this protocol version");
126
+ }
127
+ // Envelope checks — consumed HERE. (epoch is not compared: it cannot change within
128
+ // one connection — see the header note.) A gap THROWS like a protocol mismatch: the
129
+ // consumer's failure path (budget, its own io) owns the diagnostic — a library-level
130
+ // log would bypass consumer output discipline, and a silent clean end would be
131
+ // indistinguishable from the server closing normally.
132
+ if (wire.seq !== nextSeq) {
133
+ throw new Error(`control events: sequence gap (expected ${nextSeq}, got ${wire.seq}) — events were lost in transit; resync via entries()`);
134
+ }
135
+ nextSeq = wire.seq + 1;
136
+ yield wire.event;
137
+ }
138
+ }
139
+ catch (error) {
140
+ if (abort.signal.aborted) {
141
+ if (watchdog.stale()) {
142
+ throw new Error(`control events: no bytes for ${SSE_IDLE_LIMIT_MS / 1000}s (heartbeats absent) — dead connection; resync via entries()`);
143
+ }
144
+ return; // the consumer walked away — clean end, not an error
145
+ }
146
+ throw error;
147
+ }
148
+ finally {
149
+ watchdog.stop();
150
+ }
151
+ })();
152
+ return {
153
+ [Symbol.asyncIterator]() {
154
+ const abort = new AbortController();
155
+ // Abort-first cancellation (see abortFirstIterator): aborting the connection unblocks a
156
+ // generator suspended on a quiet stream read.
157
+ return abortFirstIterator(openStream(abort), () => abort.abort());
158
+ },
159
+ };
160
+ },
161
+ };
162
+ }
163
+ /**
164
+ * The remote DATA plane: an `Agent` whose `invoke` drives `POST /control/invoke` on a serving
165
+ * process — paired with {@link connectSessionControl}, a client holds a full remote fastagent
166
+ * instance through the same two contracts local code uses. A REAL Agent, failure discipline
167
+ * included: SPEC MUST 2 forbids iteration throws, so every failure — transport (401/refused/
168
+ * dropped mid-stream), protocol, and the images precheck — becomes a terminal `failed` event
169
+ * (`retryable` from the HTTP status where one exists; network trouble is retryable). Breaking out
170
+ * of iteration disconnects the request, which cancels the run (SPEC cancellation semantics travel
171
+ * the wire). The invoke wire is text-only for now: a prompt with images fails visibly instead of
172
+ * silently dropping them (steer/follow_up on the control plane carry full Prompts).
173
+ */
174
+ export function connectAgent(options) {
175
+ const { url, token, fetchFn = fetch } = options;
176
+ const base = url.replace(/\/$/, "");
177
+ const toFailed = (error) => {
178
+ if (error instanceof ControlRequestError) {
179
+ return { type: "failed", details: error.message, retryable: error.status === 429 || error.status >= 500 };
180
+ }
181
+ return { type: "failed", details: String(error), retryable: true }; // network-class: worth re-sending
182
+ };
183
+ // COMPILE-TIME drift guard (dispatch-wire parity): the invoke body carries exactly text (and
184
+ // rejects images visibly) — a new Prompt field must break THIS line and force a decision
185
+ // (carry it or reject it), never vanish on the wire while the client believes it was sent.
186
+ const _invokeDriftGuard = {};
187
+ void _invokeDriftGuard;
188
+ // Same guard for Scope: the body carries session only — a new Scope field must force a decision.
189
+ const _scopeDriftGuard = {};
190
+ void _scopeDriftGuard;
191
+ return {
192
+ invoke(scope, prompt) {
193
+ const abort = new AbortController();
194
+ const openStream = () => (async function* iterate() {
195
+ if (prompt.images && prompt.images.length > 0) {
196
+ yield {
197
+ type: "failed",
198
+ details: "remote invoke does not carry images yet — send text, or invoke in-process",
199
+ retryable: false,
200
+ };
201
+ return;
202
+ }
203
+ // Exactly-one-terminal discipline across the wire: a drop AFTER the server's terminal
204
+ // must not append a second one (catch included), and a stream that ends WITHOUT one
205
+ // (server died mid-run) must be closed with a failed — never a terminal-less end.
206
+ let terminalSeen = false;
207
+ // Armed BEFORE the fetch — the run's driver must not hang on a black-holed connect
208
+ // either (the connect await is a pending read; headers arriving disarm it).
209
+ const watchdog = idleWatchdog(abort);
210
+ watchdog.arm();
211
+ try {
212
+ const res = await fetchFn(`${base}/control/invoke`, {
213
+ method: "POST",
214
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
215
+ body: JSON.stringify({ session: scope.session, text: prompt.text }),
216
+ signal: abort.signal,
217
+ });
218
+ watchdog.disarm(); // headers arrived
219
+ if (!res.ok) {
220
+ watchdog.arm(); // the error body is a pending read too — see the events() twin
221
+ const failure = toFailed(new ControlRequestError(res.status, await res.text()));
222
+ watchdog.disarm();
223
+ yield failure;
224
+ return;
225
+ }
226
+ if (!res.body) {
227
+ yield { type: "failed", details: "remote invoke: response has no body", retryable: true };
228
+ return;
229
+ }
230
+ for await (const data of sseData(res.body, watchdog)) {
231
+ let event;
232
+ try {
233
+ event = JSON.parse(data);
234
+ }
235
+ catch (parseError) {
236
+ // Protocol drift (version skew, non-SSE middlebox), NOT transport trouble:
237
+ // re-sending the same prompt cannot fix an unparseable stream — retryable: false.
238
+ // (Guarded by terminalSeen: garbage AFTER the terminal must not add a second one.)
239
+ if (!terminalSeen) {
240
+ yield {
241
+ type: "failed",
242
+ details: `remote invoke: unparseable event on the stream (${String(parseError)})`,
243
+ retryable: false,
244
+ };
245
+ }
246
+ return;
247
+ }
248
+ // Shape check, same discipline as the events plane: `data: null` / `data: 42` is
249
+ // valid JSON but protocol drift — it must not TypeError into the catch below and be
250
+ // misclassified as retryable network trouble.
251
+ if (typeof event !== "object" || event === null || typeof event.type !== "string") {
252
+ if (!terminalSeen) {
253
+ yield {
254
+ type: "failed",
255
+ details: "remote invoke: non-event data on the stream — protocol mismatch?",
256
+ retryable: false,
257
+ };
258
+ }
259
+ return;
260
+ }
261
+ if (event.type === "completed" || event.type === "failed")
262
+ terminalSeen = true;
263
+ yield event;
264
+ }
265
+ if (!terminalSeen) {
266
+ yield { type: "failed", details: "remote invoke: stream ended without a terminal", retryable: true };
267
+ }
268
+ }
269
+ catch (error) {
270
+ if (abort.signal.aborted) {
271
+ if (watchdog.stale() && !terminalSeen) {
272
+ yield {
273
+ type: "failed",
274
+ details: `remote invoke: no bytes for ${SSE_IDLE_LIMIT_MS / 1000}s (heartbeats absent) — dead connection`,
275
+ retryable: true,
276
+ };
277
+ }
278
+ return; // the consumer walked away — cancellation, not an error
279
+ }
280
+ if (!terminalSeen)
281
+ yield toFailed(error);
282
+ }
283
+ finally {
284
+ watchdog.stop();
285
+ }
286
+ })();
287
+ // ONE stream per invoke, like a local async generator (which is its own iterator): a second
288
+ // iteration must never re-POST — that would silently start a second run with the same prompt.
289
+ // Abort-first cancellation (see abortFirstIterator): disconnect = cancel the run, even
290
+ // while suspended on a quiet read.
291
+ const iterator = abortFirstIterator(openStream(), () => abort.abort());
292
+ return {
293
+ [Symbol.asyncIterator]() {
294
+ return iterator;
295
+ },
296
+ };
297
+ },
298
+ };
299
+ }
300
+ /** Minimal SSE reader: yields each `data:` payload; ignores comments (heartbeats) and other
301
+ * fields. The explicit reader loop (not for-await) exists for the watchdog: armed strictly
302
+ * around each pending read, so only "we are listening and nothing arrives" counts as idle — a
303
+ * consumer pausing at a yield leaves the watch disarmed (see {@link ReadWatch}). */
304
+ async function* sseData(body, watch) {
305
+ const decoder = new TextDecoder();
306
+ let buffer = "";
307
+ const reader = body.getReader();
308
+ try {
309
+ for (;;) {
310
+ watch?.arm(); // a read is pending — the idle clock may run
311
+ const { done, value } = await reader.read();
312
+ watch?.disarm(); // bytes (ANY bytes — heartbeats included) or a clean end arrived
313
+ if (done)
314
+ return;
315
+ // SSE permits CRLF line endings (proxies/other servers may produce them); normalize AFTER
316
+ // appending so a \r\n split across chunks still collapses once its second half arrives.
317
+ buffer = (buffer + decoder.decode(value, { stream: true })).replace(/\r\n/g, "\n");
318
+ let sep = buffer.indexOf("\n\n");
319
+ while (sep !== -1) {
320
+ const block = buffer.slice(0, sep);
321
+ buffer = buffer.slice(sep + 2);
322
+ const data = block
323
+ .split("\n")
324
+ .filter((line) => line.startsWith("data:"))
325
+ .map((line) => line.slice(5).trimStart())
326
+ .join("\n");
327
+ if (data !== "")
328
+ yield data;
329
+ sep = buffer.indexOf("\n\n");
330
+ }
331
+ }
332
+ }
333
+ finally {
334
+ reader.releaseLock();
335
+ }
336
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Session control plane — the engine-neutral serving extension beside Agent Handler
3
+ * (docs/design/session-control.md). Pure types, zero dependencies; importing any engine
4
+ * implementation here is forbidden, exactly like agent.ts.
5
+ *
6
+ * The plane model: `invoke` is the only data plane (no run exists without an invoke); `dispatch`
7
+ * modulates the run an invoke drives; `state`/`entries`/`events` observe, strictly read-only.
8
+ */
9
+ import type { Json, Prompt } from "./agent.ts";
10
+ export interface SessionControl {
11
+ capabilities(): SessionCapabilities;
12
+ state(session: string): Promise<SessionState>;
13
+ /** `since` is an APPEND-ORDER position cursor: "every record appended after the one with this
14
+ * id", regardless of branch structure. Reconstructing the active path in a branched session is
15
+ * the client's job via `parentId` chains from `leafEntryId`. An unknown cursor falls back to a
16
+ * full backfill (correct, merely larger). */
17
+ entries(session: string, options?: {
18
+ since?: string;
19
+ }): Promise<SessionEntries>;
20
+ events(session: string): AsyncIterable<SessionEvent>;
21
+ dispatch(session: string, command: SessionCommand): Promise<SessionResult>;
22
+ }
23
+ /**
24
+ * Static support declaration, two kinds of flag:
25
+ * - COMMAND GATES (`steering`, `followUp`, `manualCompaction`, `modelSelection`, `thinkingLevel`):
26
+ * clients MUST gate dispatch on them; an unsupported command is rejected before acceptance with
27
+ * {@link UNSUPPORTED_CAPABILITY_CODE}.
28
+ * - OBSERVATION-QUALITY flags (`toolProgress`, `usage`): whether those events/state fields appear
29
+ * at all — nothing to dispatch, nothing to reject.
30
+ * `state`/`entries`/`events` are mandatory (the reconnect contract) and deliberately absent here.
31
+ */
32
+ export interface SessionCapabilities {
33
+ steering: boolean;
34
+ followUp: boolean;
35
+ manualCompaction: boolean;
36
+ modelSelection: false | {
37
+ allowedModels: string[];
38
+ };
39
+ thinkingLevel: false | {
40
+ allowedLevels: string[];
41
+ };
42
+ toolProgress: boolean;
43
+ usage: boolean;
44
+ }
45
+ /** Stable `SessionResult.error.code` for a command the implementation does not support. */
46
+ export declare const UNSUPPORTED_CAPABILITY_CODE = "unsupported_capability";
47
+ /** Stable `SessionResult.error.code` for a run-modulating command (`steer`/`follow_up`/`abort`)
48
+ * dispatched while the session has no active run — and, for `abort`, no in-flight manual
49
+ * compaction either (`abort` is also the door out of a `compacting` state; the outcome then
50
+ * travels as `compaction_finished{aborted}`). `retryable: false` — re-dispatching as-is fails
51
+ * again; re-dispatch only after `state()` shows an active run. */
52
+ export declare const NO_ACTIVE_RUN_CODE = "no_active_run";
53
+ /** Stable `SessionResult.error.code` for a command whose PAYLOAD is invalid for this runtime — an
54
+ * unknown model spec, an unsupported thinking level. Permanent for that payload; a different value
55
+ * may succeed. Rejected before acceptance. */
56
+ export declare const INVALID_COMMAND_CODE = "invalid_command";
57
+ /** Stable `SessionResult.error.code` for a boundary mutation on a session that does not exist.
58
+ * Sessions are created by the DATA plane (`invoke`), never by the control plane — a mutation on an
59
+ * unknown id (a typo, a not-yet-started conversation) must not mint a ghost record. Rejected
60
+ * before acceptance; retry after the session's first turn exists. */
61
+ export declare const NO_SUCH_SESSION_CODE = "no_such_session";
62
+ /** Stable `SessionResult.error.code` for a boundary mutation rejected BEFORE acceptance with
63
+ * nothing durable landed — a failed override append, or compact's admission failing (the harness
64
+ * build, the local preparation). Acceptance sits where the work becomes asynchronous and
65
+ * expensive: the model call — compact is accept-fast (holding the dispatch open for a full model
66
+ * call would make acceptance = outcome), and post-acceptance outcomes travel as
67
+ * `compaction_finished{summary|error|aborted}` events. `retryable: true` throughout: the same
68
+ * command may succeed on retry (the state-dependent "nothing to compact" has its own code,
69
+ * {@link NOTHING_TO_COMPACT_CODE}). */
70
+ export declare const BOUNDARY_COMMAND_FAILED_CODE = "boundary_command_failed";
71
+ /** Stable `SessionResult.error.code` for `compact` on a session with no compactable history yet —
72
+ * a no-op, not a failure, rejected before acceptance. The {@link NO_ACTIVE_RUN_CODE} pattern:
73
+ * `retryable: false` (as-is retry fails now), re-dispatch once the session has grown. */
74
+ export declare const NOTHING_TO_COMPACT_CODE = "nothing_to_compact";
75
+ /** Stable `SessionResult.error.code` for a run command that reached an active run but could not
76
+ * take effect because the run raced to settlement (or the engine refused it). Distinct from
77
+ * {@link NO_ACTIVE_RUN_CODE}: the run existed — and TRANSIENT: the session's next run can be
78
+ * dispatched to. Still pre-acceptance — nothing was queued — and `retryable: false`: the same
79
+ * command as-is fails again. (A run registered without modulation controls is a capability
80
+ * problem, not a run problem, and rejects with {@link UNSUPPORTED_CAPABILITY_CODE}.) */
81
+ export declare const RUN_COMMAND_FAILED_CODE = "run_command_failed";
82
+ /** Six commands; deliberately NO `prompt` — starting work is the data plane's definition. */
83
+ export type SessionCommand = {
84
+ type: "steer";
85
+ prompt: Prompt;
86
+ } | {
87
+ type: "follow_up";
88
+ prompt: Prompt;
89
+ } | {
90
+ type: "abort";
91
+ } | {
92
+ type: "compact";
93
+ instructions?: string;
94
+ } | {
95
+ type: "set_model";
96
+ model: string;
97
+ } | {
98
+ type: "set_thinking";
99
+ level: string;
100
+ };
101
+ /**
102
+ * Acceptance is not outcome: `ok: true` means admitted or applied, never that the run ultimately
103
+ * succeeded (outcomes are `run_settled` events / the invoke terminal). `ok: false` is guaranteed to
104
+ * mean rejection BEFORE acceptance — nothing was queued or applied. ONE exception to "nothing took
105
+ * effect": a rejected `abort` may still have attributed a concurrently-settling run as `aborted`
106
+ * (the intent was live while the run resolved — see the guarantee boundary in the pi
107
+ * implementation); the settlement is the truth. `error.retryable` means "re-dispatching the SAME
108
+ * command as-is may succeed"; a `false` with a state-dependent code (e.g.
109
+ * {@link NO_ACTIVE_RUN_CODE}) invites a re-dispatch only after `state()` shows the condition
110
+ * changed.
111
+ */
112
+ export type SessionResult = {
113
+ ok: true;
114
+ runId?: string;
115
+ } | {
116
+ ok: false;
117
+ error: {
118
+ code: string;
119
+ message: string;
120
+ retryable: boolean;
121
+ };
122
+ };
123
+ export interface SessionState {
124
+ /** `compacting` refers to Phase 2 MANUAL compaction at a session boundary. Automatic overflow
125
+ * compaction happens inside a run's activity window and reports as `running`. */
126
+ status: "idle" | "running" | "compacting";
127
+ activeRunId?: string;
128
+ /** The session's durable overrides (set_model / set_thinking), read from the record — so a
129
+ * reconnecting client sees them without scanning entries. Absent = the assembly default. */
130
+ model?: string;
131
+ thinkingLevel?: string;
132
+ pending: {
133
+ steering: number;
134
+ followUp: number;
135
+ };
136
+ usage?: {
137
+ inputTokens: number;
138
+ outputTokens: number;
139
+ cacheReadTokens?: number;
140
+ cacheWriteTokens?: number;
141
+ cost?: number;
142
+ contextTokens?: number;
143
+ contextWindow?: number;
144
+ };
145
+ leafEntryId?: string;
146
+ }
147
+ export interface SessionEntries {
148
+ entries: SessionEntry[];
149
+ leafEntryId?: string;
150
+ }
151
+ /** A durable append-only session record. `kind` guarantees a minimum vocabulary of
152
+ * "user" | "assistant" | "tool"; engine-specific kinds beyond it MUST be skippable. */
153
+ export interface SessionEntry {
154
+ id: string;
155
+ parentId?: string;
156
+ timestamp: number;
157
+ kind: string;
158
+ data: Json;
159
+ }
160
+ /** Semantic-only: no sequence, no epoch, no session id — in-process the stream is lossless and
161
+ * ordered, and those concerns belong to the transport envelope (design §13). Consumers MUST
162
+ * forward or ignore unknown event types; the vocabulary is additive. */
163
+ export interface SessionEvent<TType extends string = string, TData extends Json = Json> {
164
+ type: TType;
165
+ timestamp: number;
166
+ /** Present on run-scoped events. */
167
+ runId?: string;
168
+ data: TData;
169
+ }
170
+ export type RunStartedEvent = SessionEvent<"run_started", Record<never, never>> & {
171
+ runId: string;
172
+ };
173
+ export type RunSettledEvent = SessionEvent<"run_settled", {
174
+ status: "completed" | "failed" | "aborted";
175
+ error?: {
176
+ code?: string;
177
+ message: string;
178
+ retryable: boolean;
179
+ };
180
+ }> & {
181
+ runId: string;
182
+ };
183
+ export type MessageStartedEvent = SessionEvent<"message_started", Record<never, never>> & {
184
+ runId: string;
185
+ };
186
+ export type MessageDeltaEvent = SessionEvent<"message_delta", {
187
+ channel: "text" | "thinking";
188
+ delta: string;
189
+ }> & {
190
+ runId: string;
191
+ };
192
+ export type MessageFinishedEvent = SessionEvent<"message_finished", Record<never, never>> & {
193
+ runId: string;
194
+ };
195
+ export type ToolStartedEvent = SessionEvent<"tool_started", {
196
+ id: string;
197
+ name: string;
198
+ args: Json;
199
+ }> & {
200
+ runId: string;
201
+ };
202
+ /** Replace semantics: `partialResult` is the accumulated snapshot so far, not a delta. */
203
+ export type ToolProgressEvent = SessionEvent<"tool_progress", {
204
+ id: string;
205
+ name: string;
206
+ partialResult: Json;
207
+ }> & {
208
+ runId: string;
209
+ };
210
+ export type ToolFinishedEvent = SessionEvent<"tool_finished", {
211
+ id: string;
212
+ isError: boolean;
213
+ content: Json;
214
+ }> & {
215
+ runId: string;
216
+ };
217
+ /** Normalized live queue depths for the active run (L1). */
218
+ export type QueueChangedEvent = SessionEvent<"queue_changed", {
219
+ steering: number;
220
+ followUp: number;
221
+ }> & {
222
+ runId: string;
223
+ };
224
+ /** A boundary mutation changed durable session state (L2; no runId — boundary mutations happen
225
+ * between runs). */
226
+ export type StateChangedEvent = SessionEvent<"state_changed", {
227
+ model?: string;
228
+ thinkingLevel?: string;
229
+ }>;
230
+ /** Manual compaction bounds (L2): every `compaction_started` is closed by exactly one
231
+ * `compaction_finished` — `summary` on success, `error` on failure, `aborted: true` on a
232
+ * deliberate stop (run/compaction symmetry with `run_settled{status: "aborted"}`: a client's own
233
+ * abort is not a failure). In the failure and aborted cases nothing durable landed. Automatic
234
+ * overflow compaction stays inside its run's activity window and does not emit these. */
235
+ export type CompactionStartedEvent = SessionEvent<"compaction_started", Record<never, never>>;
236
+ export type CompactionFinishedEvent = SessionEvent<"compaction_finished", {
237
+ summary?: string;
238
+ error?: string;
239
+ aborted?: boolean;
240
+ }>;
241
+ /** A transient provider failure scheduled a summarization retry backoff (auto-compaction /
242
+ * branch summaries inside a run — `runId` present — or a manual `compact` at a boundary — no
243
+ * `runId`). Explains a quiet gap that would otherwise read as a hang. Deliberately unclosed:
244
+ * the next event (message_*, `run_settled`, `compaction_finished`) is the closure, and the
245
+ * engine's `retry_finished` carries no outcome to forward. */
246
+ export type RetryScheduledEvent = SessionEvent<"retry_scheduled", {
247
+ operation: "compaction" | "branch_summary";
248
+ attempt: number;
249
+ maxAttempts: number;
250
+ delayMs: number;
251
+ error: string;
252
+ }>;
253
+ /**
254
+ * The serving process failed outside a normal run outcome (fail visibly). Emitted by TRANSPORT
255
+ * adapters (design §13) when they lose the backend before ending a remote stream — an in-process
256
+ * embedding cannot produce it (a dead process has no one left to emit), so it is deliberately NOT
257
+ * part of {@link KnownSessionEvent}: a local L0 client would be handling a signal that cannot occur.
258
+ */
259
+ export type ServingErrorEvent = SessionEvent<"serving_error", {
260
+ message: string;
261
+ }>;
262
+ /** Every event the in-process observation plane emits today: L0, L1 `queue_changed`, and the L2
263
+ * events (`state_changed`, `compaction_*`, `retry_scheduled`). Remaining L2 events (turn_*) are
264
+ * future vocabulary; {@link ServingErrorEvent} arrives with the transport adapter. */
265
+ export type KnownSessionEvent = RunStartedEvent | RunSettledEvent | MessageStartedEvent | MessageDeltaEvent | MessageFinishedEvent | ToolStartedEvent | ToolProgressEvent | ToolFinishedEvent | QueueChangedEvent | StateChangedEvent | CompactionStartedEvent | CompactionFinishedEvent | RetryScheduledEvent;
@@ -0,0 +1,37 @@
1
+ /** Stable `SessionResult.error.code` for a command the implementation does not support. */
2
+ export const UNSUPPORTED_CAPABILITY_CODE = "unsupported_capability";
3
+ /** Stable `SessionResult.error.code` for a run-modulating command (`steer`/`follow_up`/`abort`)
4
+ * dispatched while the session has no active run — and, for `abort`, no in-flight manual
5
+ * compaction either (`abort` is also the door out of a `compacting` state; the outcome then
6
+ * travels as `compaction_finished{aborted}`). `retryable: false` — re-dispatching as-is fails
7
+ * again; re-dispatch only after `state()` shows an active run. */
8
+ export const NO_ACTIVE_RUN_CODE = "no_active_run";
9
+ /** Stable `SessionResult.error.code` for a command whose PAYLOAD is invalid for this runtime — an
10
+ * unknown model spec, an unsupported thinking level. Permanent for that payload; a different value
11
+ * may succeed. Rejected before acceptance. */
12
+ export const INVALID_COMMAND_CODE = "invalid_command";
13
+ /** Stable `SessionResult.error.code` for a boundary mutation on a session that does not exist.
14
+ * Sessions are created by the DATA plane (`invoke`), never by the control plane — a mutation on an
15
+ * unknown id (a typo, a not-yet-started conversation) must not mint a ghost record. Rejected
16
+ * before acceptance; retry after the session's first turn exists. */
17
+ export const NO_SUCH_SESSION_CODE = "no_such_session";
18
+ /** Stable `SessionResult.error.code` for a boundary mutation rejected BEFORE acceptance with
19
+ * nothing durable landed — a failed override append, or compact's admission failing (the harness
20
+ * build, the local preparation). Acceptance sits where the work becomes asynchronous and
21
+ * expensive: the model call — compact is accept-fast (holding the dispatch open for a full model
22
+ * call would make acceptance = outcome), and post-acceptance outcomes travel as
23
+ * `compaction_finished{summary|error|aborted}` events. `retryable: true` throughout: the same
24
+ * command may succeed on retry (the state-dependent "nothing to compact" has its own code,
25
+ * {@link NOTHING_TO_COMPACT_CODE}). */
26
+ export const BOUNDARY_COMMAND_FAILED_CODE = "boundary_command_failed";
27
+ /** Stable `SessionResult.error.code` for `compact` on a session with no compactable history yet —
28
+ * a no-op, not a failure, rejected before acceptance. The {@link NO_ACTIVE_RUN_CODE} pattern:
29
+ * `retryable: false` (as-is retry fails now), re-dispatch once the session has grown. */
30
+ export const NOTHING_TO_COMPACT_CODE = "nothing_to_compact";
31
+ /** Stable `SessionResult.error.code` for a run command that reached an active run but could not
32
+ * take effect because the run raced to settlement (or the engine refused it). Distinct from
33
+ * {@link NO_ACTIVE_RUN_CODE}: the run existed — and TRANSIENT: the session's next run can be
34
+ * dispatched to. Still pre-acceptance — nothing was queued — and `retryable: false`: the same
35
+ * command as-is fails again. (A run registered without modulation controls is a capability
36
+ * problem, not a run problem, and rejects with {@link UNSUPPORTED_CAPABILITY_CODE}.) */
37
+ export const RUN_COMMAND_FAILED_CODE = "run_command_failed";
@@ -0,0 +1,2 @@
1
+ /** `@fastagent-sh/fastagent/slack` — the first-party Slack Events API bot-channel surface. */
2
+ export { slackChannel, defaultSlackRoute, slackEnvelope, verifySlackSignature, type SlackChannelOptions, type SlackEventEnvelope, type SlackFile, type SlackMessageEvent, type SlackRendering, type SlackRoute, type SlackFailure, } from "./channels/slack/slack.ts";
package/dist/slack.js ADDED
@@ -0,0 +1,2 @@
1
+ /** `@fastagent-sh/fastagent/slack` — the first-party Slack Events API bot-channel surface. */
2
+ export { slackChannel, defaultSlackRoute, slackEnvelope, verifySlackSignature, } from "./channels/slack/slack.js";