stageflow 0.36.0 → 0.40.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 (288) hide show
  1. package/README.md +61 -26
  2. package/dist/agent/claudeAdapter.js +21 -1
  3. package/dist/agent/claudeTools.d.ts +13 -0
  4. package/dist/agent/claudeTools.js +98 -0
  5. package/dist/agent/piAdapter.d.ts +9 -1
  6. package/dist/agent/piAdapter.js +59 -18
  7. package/dist/agent/piIsolatedMcp.js +14 -2
  8. package/dist/agent/piIsolatedMcpProbe.d.ts +5 -1
  9. package/dist/agent/piIsolatedMcpProbe.js +16 -30
  10. package/dist/agent/port.d.ts +5 -0
  11. package/dist/agent/sandboxPiTools.d.ts +99 -0
  12. package/dist/agent/sandboxPiTools.js +109 -0
  13. package/dist/agents/agentId.d.ts +8 -0
  14. package/dist/agents/agentId.js +27 -0
  15. package/dist/agents/approvalBroker.d.ts +16 -0
  16. package/dist/agents/approvalBroker.js +45 -0
  17. package/dist/agents/authorChat.d.ts +19 -0
  18. package/dist/agents/authorChat.js +63 -0
  19. package/dist/agents/chatTurn.d.ts +23 -0
  20. package/dist/agents/chatTurn.js +77 -0
  21. package/dist/agents/communications/agentmail.d.ts +19 -0
  22. package/dist/agents/communications/agentmail.js +46 -0
  23. package/dist/agents/communications/agentmailListener.d.ts +59 -0
  24. package/dist/agents/communications/agentmailListener.js +183 -0
  25. package/dist/agents/communications/allowlist.d.ts +10 -0
  26. package/dist/agents/communications/allowlist.js +36 -0
  27. package/dist/agents/communications/channelConnections.d.ts +3 -0
  28. package/dist/agents/communications/channelConnections.js +51 -0
  29. package/dist/agents/communications/delivery.d.ts +53 -0
  30. package/dist/agents/communications/delivery.js +142 -0
  31. package/dist/agents/communications/email.d.ts +26 -0
  32. package/dist/agents/communications/email.js +20 -0
  33. package/dist/agents/communications/identity.d.ts +13 -0
  34. package/dist/agents/communications/identity.js +18 -0
  35. package/dist/agents/communications/imessageListener.d.ts +41 -0
  36. package/dist/agents/communications/imessageListener.js +92 -0
  37. package/dist/agents/communications/inbound.d.ts +63 -0
  38. package/dist/agents/communications/inbound.js +143 -0
  39. package/dist/agents/communications/listener.d.ts +36 -0
  40. package/dist/agents/communications/listener.js +38 -0
  41. package/dist/agents/communications/photon.d.ts +19 -0
  42. package/dist/agents/communications/photon.js +38 -0
  43. package/dist/agents/communications/port.d.ts +27 -0
  44. package/dist/agents/communications/port.js +6 -0
  45. package/dist/agents/communications/slack.d.ts +28 -0
  46. package/dist/agents/communications/slack.js +65 -0
  47. package/dist/agents/communications/supervisor.d.ts +36 -0
  48. package/dist/agents/communications/supervisor.js +96 -0
  49. package/dist/agents/communications/telegram.d.ts +19 -0
  50. package/dist/agents/communications/telegram.js +52 -0
  51. package/dist/agents/definitionSummary.d.ts +13 -0
  52. package/dist/agents/definitionSummary.js +91 -0
  53. package/dist/agents/delegate.d.ts +3 -0
  54. package/dist/agents/delegate.js +8 -0
  55. package/dist/agents/errors.d.ts +10 -0
  56. package/dist/agents/errors.js +32 -0
  57. package/dist/agents/guardrails.d.ts +24 -0
  58. package/dist/agents/guardrails.js +39 -0
  59. package/dist/agents/historyFit.d.ts +10 -0
  60. package/dist/agents/historyFit.js +26 -0
  61. package/dist/agents/hostContext.d.ts +54 -0
  62. package/dist/agents/hostContext.js +156 -0
  63. package/dist/agents/ledgers.d.ts +29 -0
  64. package/dist/agents/ledgers.js +31 -0
  65. package/dist/agents/lineDiff.d.ts +12 -0
  66. package/dist/agents/lineDiff.js +46 -0
  67. package/dist/agents/ndjson.d.ts +9 -0
  68. package/dist/agents/ndjson.js +47 -0
  69. package/dist/agents/otlpExport.d.ts +9 -0
  70. package/dist/agents/otlpExport.js +58 -0
  71. package/dist/agents/otlpSink.d.ts +7 -0
  72. package/dist/agents/otlpSink.js +23 -0
  73. package/dist/agents/policy.d.ts +31 -0
  74. package/dist/agents/policy.js +84 -0
  75. package/dist/agents/projectRoot.d.ts +5 -0
  76. package/dist/agents/projectRoot.js +15 -0
  77. package/dist/agents/runtime/piRuntime.d.ts +52 -0
  78. package/dist/agents/runtime/piRuntime.js +474 -0
  79. package/dist/agents/runtime/types.d.ts +77 -0
  80. package/dist/agents/runtime/types.js +26 -0
  81. package/dist/agents/sandbox.d.ts +17 -0
  82. package/dist/agents/sandbox.js +10 -0
  83. package/dist/agents/scheduleSource.d.ts +32 -0
  84. package/dist/agents/scheduleSource.js +100 -0
  85. package/dist/agents/service.d.ts +298 -0
  86. package/dist/agents/service.js +1105 -0
  87. package/dist/agents/sessions.d.ts +38 -0
  88. package/dist/agents/sessions.js +321 -0
  89. package/dist/agents/store.d.ts +277 -0
  90. package/dist/agents/store.js +643 -0
  91. package/dist/agents/trace.d.ts +27 -0
  92. package/dist/agents/trace.js +29 -0
  93. package/dist/agents/turnLifecycle.d.ts +72 -0
  94. package/dist/agents/turnLifecycle.js +157 -0
  95. package/dist/agents/webChat/publicChat.d.ts +36 -0
  96. package/dist/agents/webChat/publicChat.js +290 -0
  97. package/dist/agents/webChat/rateLimit.d.ts +15 -0
  98. package/dist/agents/webChat/rateLimit.js +33 -0
  99. package/dist/agents/webChat/slug.d.ts +2 -0
  100. package/dist/agents/webChat/slug.js +9 -0
  101. package/dist/channels/providers/agentmail.js +14 -1
  102. package/dist/channels/providers/photonImessage.js +36 -0
  103. package/dist/channels/providers/slack.js +2 -1
  104. package/dist/channels/providers/telegram.js +2 -1
  105. package/dist/channels/types.d.ts +27 -0
  106. package/dist/cli/agentsCommand.d.ts +14 -0
  107. package/dist/cli/agentsCommand.js +291 -0
  108. package/dist/cli.js +9 -0
  109. package/dist/config/browseCatalog.js +4 -11
  110. package/dist/config/draftPackage.d.ts +1 -0
  111. package/dist/config/draftPackage.js +4 -0
  112. package/dist/config/hostConfig.js +9 -0
  113. package/dist/config/loadAgent.d.ts +266 -0
  114. package/dist/config/loadAgent.js +374 -0
  115. package/dist/config/loadPipeline.js +5 -2
  116. package/dist/config/loadTask.js +34 -1
  117. package/dist/config/mcpHostCatalog.d.ts +50 -0
  118. package/dist/config/mcpHostCatalog.js +405 -0
  119. package/dist/config/mcpOAuth.d.ts +37 -0
  120. package/dist/config/mcpOAuth.js +263 -0
  121. package/dist/config/mcpRedaction.d.ts +42 -0
  122. package/dist/config/mcpRedaction.js +314 -0
  123. package/dist/config/mcpToolCache.d.ts +20 -0
  124. package/dist/config/mcpToolCache.js +43 -0
  125. package/dist/config/mergePipelineIncludes.d.ts +2 -0
  126. package/dist/config/mergePipelineIncludes.js +25 -0
  127. package/dist/config/resolveStageMcpServers.d.ts +35 -3
  128. package/dist/config/resolveStageMcpServers.js +124 -9
  129. package/dist/config/validateCatalog.d.ts +1 -1
  130. package/dist/config/validateCatalog.js +22 -1
  131. package/dist/mcp/agentTools.d.ts +8 -0
  132. package/dist/mcp/agentTools.js +132 -0
  133. package/dist/mcp/catalogTools.js +17 -10
  134. package/dist/mcp/deps.d.ts +6 -0
  135. package/dist/mcp/pipelineAuthorSupport.d.ts +19 -0
  136. package/dist/mcp/pipelineAuthorSupport.js +10 -0
  137. package/dist/mcp/pipelineAuthorTools.d.ts +8 -0
  138. package/dist/mcp/pipelineAuthorTools.js +109 -0
  139. package/dist/mcp/server.js +5 -0
  140. package/dist/mcp/tools.js +4 -0
  141. package/dist/mcp/triggerTools.js +32 -0
  142. package/dist/memory/access.d.ts +36 -0
  143. package/dist/memory/access.js +116 -0
  144. package/dist/memory/agentIssues.d.ts +3 -0
  145. package/dist/memory/agentIssues.js +23 -0
  146. package/dist/memory/bashDeny.d.ts +6 -0
  147. package/dist/memory/bashDeny.js +35 -0
  148. package/dist/memory/host.d.ts +51 -0
  149. package/dist/memory/host.js +394 -0
  150. package/dist/memory/ids.d.ts +15 -0
  151. package/dist/memory/ids.js +31 -0
  152. package/dist/memory/worker.d.ts +1 -0
  153. package/dist/memory/worker.js +84 -0
  154. package/dist/models/catalog.d.ts +113 -0
  155. package/dist/models/catalog.js +238 -0
  156. package/dist/models/response.d.ts +29 -0
  157. package/dist/models/response.js +25 -0
  158. package/dist/operatorAgent/agentAuthorContext.d.ts +17 -0
  159. package/dist/operatorAgent/agentAuthorContext.js +32 -0
  160. package/dist/operatorAgent/fakeHost.js +2 -0
  161. package/dist/operatorAgent/host.js +6 -3
  162. package/dist/operatorAgent/index.d.ts +4 -0
  163. package/dist/operatorAgent/index.js +4 -0
  164. package/dist/operatorAgent/piModel.js +2 -0
  165. package/dist/operatorAgent/profiles/agentAuthor.d.ts +11 -0
  166. package/dist/operatorAgent/profiles/agentAuthor.js +192 -0
  167. package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -0
  168. package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +29 -0
  169. package/dist/operatorAgent/profiles/workshopAuthor.js +2 -2
  170. package/dist/operatorAgent/profiles/workshopAuthorTools.js +3 -3
  171. package/dist/operatorAgent/proposals.d.ts +1 -0
  172. package/dist/operatorAgent/proposals.js +23 -2
  173. package/dist/package-meta.d.ts +1 -1
  174. package/dist/package-meta.js +1 -1
  175. package/dist/preflight/pipelinePreflight.js +1 -0
  176. package/dist/runstore/createStore.d.ts +2 -0
  177. package/dist/runstore/createStore.js +8 -0
  178. package/dist/runstore/overviewSummary.d.ts +56 -0
  179. package/dist/runstore/overviewSummary.js +134 -0
  180. package/dist/runstore/port.d.ts +3 -0
  181. package/dist/runstore/sqlite/SqliteRunStore.d.ts +2 -0
  182. package/dist/runstore/sqlite/SqliteRunStore.js +18 -0
  183. package/dist/runstore/sqlite/migrations/013-agents.d.ts +12 -0
  184. package/dist/runstore/sqlite/migrations/013-agents.js +156 -0
  185. package/dist/runstore/sqlite/migrations/014-agent-deliveries.d.ts +11 -0
  186. package/dist/runstore/sqlite/migrations/014-agent-deliveries.js +32 -0
  187. package/dist/runstore/sqlite/migrations/index.js +4 -0
  188. package/dist/runtime/gitCheckoutCapability.d.ts +1 -0
  189. package/dist/runtime/gitCheckoutCapability.js +1 -1
  190. package/dist/runtime/gitWorktreeCheckout.d.ts +22 -0
  191. package/dist/runtime/gitWorktreeCheckout.js +120 -0
  192. package/dist/runtime/sandboxContainer.d.ts +62 -0
  193. package/dist/runtime/sandboxContainer.js +151 -0
  194. package/dist/runtime/sandboxDesk.d.ts +28 -0
  195. package/dist/runtime/sandboxDesk.js +242 -0
  196. package/dist/runtime/sandboxFiles.d.ts +20 -0
  197. package/dist/runtime/sandboxFiles.js +150 -0
  198. package/dist/runtime/stageAttemptBootstrap.js +15 -1
  199. package/dist/runtime/stageEnvironment.d.ts +2 -0
  200. package/dist/runtime/stageEnvironment.js +8 -1
  201. package/dist/runtime/stageProcessLauncher.js +23 -0
  202. package/dist/runtime/stageRoots.d.ts +9 -0
  203. package/dist/runtime/stageRoots.js +14 -0
  204. package/dist/runtime/stageRunner.d.ts +5 -0
  205. package/dist/runtime/stageRunner.js +38 -4
  206. package/dist/runtime/stageSandbox.d.ts +16 -0
  207. package/dist/runtime/stageSandbox.js +54 -0
  208. package/dist/runtime/stageSecrets.d.ts +6 -1
  209. package/dist/runtime/stageSecrets.js +34 -1
  210. package/dist/runtime/stageWorker.js +138 -105
  211. package/dist/runtime/workspaceBinding.d.ts +2 -1
  212. package/dist/runtime/workspaceBinding.js +20 -4
  213. package/dist/server/agentAuthorRoutes.d.ts +5 -0
  214. package/dist/server/agentAuthorRoutes.js +64 -0
  215. package/dist/server/agentRoutes.d.ts +5 -0
  216. package/dist/server/agentRoutes.js +323 -0
  217. package/dist/server/bootstrap.d.ts +8 -0
  218. package/dist/server/bootstrap.js +36 -10
  219. package/dist/server/controlToken.js +8 -0
  220. package/dist/server/http.d.ts +8 -1
  221. package/dist/server/http.js +172 -20
  222. package/dist/server/mcpHost.js +1 -0
  223. package/dist/server/memoryRoutes.d.ts +5 -0
  224. package/dist/server/memoryRoutes.js +146 -0
  225. package/dist/server/projectMcpRoutes.d.ts +1 -0
  226. package/dist/server/projectMcpRoutes.js +241 -3
  227. package/dist/toolkit/guard.d.ts +27 -0
  228. package/dist/toolkit/guard.js +98 -0
  229. package/dist/toolkit/index.d.ts +48 -0
  230. package/dist/toolkit/index.js +82 -0
  231. package/dist/toolkit/meta.d.ts +36 -0
  232. package/dist/toolkit/meta.js +117 -0
  233. package/dist/toolkit/profiles.d.ts +13 -0
  234. package/dist/toolkit/profiles.js +25 -0
  235. package/dist/toolkit/record.d.ts +15 -0
  236. package/dist/toolkit/record.js +17 -0
  237. package/dist/tools/memoryTools.d.ts +95 -0
  238. package/dist/tools/memoryTools.js +179 -0
  239. package/dist/types/pipeline.d.ts +3 -0
  240. package/dist/types/stage.d.ts +2 -0
  241. package/dist/types/task.d.ts +11 -1
  242. package/dist/ui/assets/AssistantRuntimeProvider-CsiPFhCT.js +9 -0
  243. package/dist/ui/assets/MarkdownText-D7tYaIFO.js +29 -0
  244. package/dist/ui/assets/ReadonlyStore-BfXqHykm.js +6 -0
  245. package/dist/ui/assets/Tooltip-DrpoRRNi.js +1 -0
  246. package/dist/ui/assets/chat-BbLukq9W.js +5 -0
  247. package/dist/ui/assets/chat-zIZU_bP1.css +2 -0
  248. package/dist/ui/assets/jsx-runtime-iVoFAvQ_.js +1 -0
  249. package/dist/ui/assets/{index-Clv4sJpU.css → main-BFo79krC.css} +1 -1
  250. package/dist/ui/assets/main-CijYMeM0.js +259 -0
  251. package/dist/ui/chat.html +25 -0
  252. package/dist/ui/index.html +6 -2
  253. package/dist/workshop/modelSettings.d.ts +2 -2
  254. package/dist/workshop/modelSettings.js +5 -11
  255. package/examples/README.md +1 -0
  256. package/examples/channels-smoke/README.md +12 -1
  257. package/examples/github-release/README.md +1 -1
  258. package/examples/memory/README.md +101 -0
  259. package/examples/memory/desk-outage.task.yaml +15 -0
  260. package/examples/memory/desk-refund.task.yaml +15 -0
  261. package/examples/memory/desk-revisit-outage.task.yaml +11 -0
  262. package/examples/memory/desk-revisit.pipeline.yaml +123 -0
  263. package/examples/memory/desk.pipeline.yaml +192 -0
  264. package/examples/memory/file-card.pipeline.yaml +39 -0
  265. package/examples/memory/file-ledger.task.yaml +22 -0
  266. package/examples/memory/file-release.task.yaml +24 -0
  267. package/examples/memory/file-rich.pipeline.yaml +40 -0
  268. package/examples/memory/file-ticket.task.yaml +7 -0
  269. package/examples/memory/file-timeline.task.yaml +28 -0
  270. package/examples/memory/find-card.pipeline.yaml +46 -0
  271. package/examples/memory/find-ticket.task.yaml +7 -0
  272. package/examples/memory/missing.pipeline.yaml +16 -0
  273. package/examples/memory/probe-bash.task.yaml +3 -0
  274. package/examples/memory/recall.pipeline.yaml +24 -0
  275. package/examples/memory/recall.task.yaml +5 -0
  276. package/examples/memory/remember-ada.task.yaml +6 -0
  277. package/examples/memory/remember-grace.task.yaml +6 -0
  278. package/examples/memory/remember.pipeline.yaml +58 -0
  279. package/examples/memory/sandbox-memory-bash.pipeline.yaml +28 -0
  280. package/examples/memory/stageflow.yaml +12 -0
  281. package/examples/memory/update-card.pipeline.yaml +43 -0
  282. package/examples/memory/update-ledger.task.yaml +23 -0
  283. package/examples/memory/update-rich.pipeline.yaml +44 -0
  284. package/examples/memory/update-ticket.task.yaml +7 -0
  285. package/examples/ship-feature/README.md +15 -0
  286. package/examples/stage-mcp/README.md +1 -1
  287. package/package.json +2 -1
  288. package/dist/ui/assets/index-BOhqnPNg.js +0 -260
@@ -0,0 +1,18 @@
1
+ /**
2
+ * What a connection really talks to. Two connections can point at the same AgentMail inbox or the same Photon
3
+ * project; one stream and one agent may serve such a pair, so they share an identity. Other providers are
4
+ * identified by the connection itself.
5
+ */
6
+ export function channelIdentity(connection) {
7
+ if (connection.provider === "agentmail") {
8
+ const config = (connection.config ?? {});
9
+ if (config.inbox)
10
+ return `agentmail:${config.region ?? "us"}:${config.inbox.trim().toLowerCase()}`;
11
+ }
12
+ if (connection.provider === "imessage") {
13
+ const project = connection.credential?.secrets.projectId;
14
+ if (project)
15
+ return `imessage:${project}`;
16
+ }
17
+ return `${connection.provider}:${connection.id}`;
18
+ }
@@ -0,0 +1,41 @@
1
+ import { type ChannelListener, type ListenerDeps } from "./listener.js";
2
+ import type { AgentInboundMessage } from "./port.js";
3
+ /** What this listener reads of a Spectrum message. */
4
+ export type SpectrumInbound = {
5
+ id: string;
6
+ direction?: string;
7
+ content?: {
8
+ type?: string;
9
+ text?: string;
10
+ };
11
+ sender?: {
12
+ id?: string;
13
+ } | undefined;
14
+ space?: {
15
+ id?: string;
16
+ type?: string;
17
+ } | undefined;
18
+ };
19
+ export type SpectrumStream = {
20
+ messages: AsyncIterable<[unknown, SpectrumInbound]>;
21
+ stop(): Promise<void>;
22
+ };
23
+ export type IMessageTransport = {
24
+ open(): Promise<SpectrumStream>;
25
+ };
26
+ export type IMessageListenerConfig = {
27
+ connectionId: string;
28
+ };
29
+ /** The text and sender of a message the agent should answer, or undefined for everything else. */
30
+ export declare function toInbound(connectionId: string, message: SpectrumInbound): AgentInboundMessage | undefined;
31
+ /**
32
+ * Holds a Spectrum (Photon) stream open for an iMessage line. One conversation per sender, so with an
33
+ * always-on agent each person has one session that carries on after a failure. A dropped stream is
34
+ * reopened with backoff.
35
+ */
36
+ export declare function createIMessageListener(config: IMessageListenerConfig, transport: IMessageTransport, deps: ListenerDeps): ChannelListener;
37
+ /** The real transport over the Spectrum SDK; the packages load on first use. */
38
+ export declare function sdkIMessageTransport(credentials: {
39
+ projectId: string;
40
+ projectSecret: string;
41
+ }, importer?: (specifier: string) => Promise<any>): IMessageTransport;
@@ -0,0 +1,92 @@
1
+ import { normalizeSender } from "./allowlist.js";
2
+ import { abortableSleep, reconnectDelay, StateTracker } from "./listener.js";
3
+ /** The text and sender of a message the agent should answer, or undefined for everything else. */
4
+ export function toInbound(connectionId, message) {
5
+ if (message.direction !== undefined && message.direction !== "inbound")
6
+ return undefined;
7
+ if (message.content?.type !== "text")
8
+ return undefined;
9
+ const text = message.content.text?.trim() ?? "";
10
+ const sender = message.sender?.id;
11
+ if (text.length === 0 || !sender)
12
+ return undefined;
13
+ // Group chats are out of scope: replying to one person's handle would answer the wrong audience.
14
+ if (message.space?.type === "group")
15
+ return undefined;
16
+ const from = normalizeSender(sender);
17
+ return { provider: "imessage", connectionId, eventId: `imessage:${message.id}`, threadId: from, text, from, replyTo: from };
18
+ }
19
+ /**
20
+ * Holds a Spectrum (Photon) stream open for an iMessage line. One conversation per sender, so with an
21
+ * always-on agent each person has one session that carries on after a failure. A dropped stream is
22
+ * reopened with backoff.
23
+ */
24
+ export function createIMessageListener(config, transport, deps) {
25
+ const now = deps.now ?? Date.now;
26
+ const sleep = deps.sleep ?? abortableSleep;
27
+ const tracker = new StateTracker(now);
28
+ const abort = new AbortController();
29
+ let loop;
30
+ let stream;
31
+ async function session() {
32
+ stream = await transport.open();
33
+ tracker.set("connected");
34
+ for await (const [, message] of stream.messages) {
35
+ if (abort.signal.aborted)
36
+ return;
37
+ const inbound = toInbound(config.connectionId, message);
38
+ if (!inbound)
39
+ continue;
40
+ tracker.received();
41
+ await deps.onMessage(inbound).catch((error) => deps.log(`imessage ${config.connectionId}: delivery failed: ${error instanceof Error ? error.message : String(error)}`));
42
+ }
43
+ if (!abort.signal.aborted)
44
+ throw new Error("stream ended");
45
+ }
46
+ async function run() {
47
+ let attempt = 0;
48
+ while (!abort.signal.aborted) {
49
+ try {
50
+ await session();
51
+ attempt = 0;
52
+ }
53
+ catch (error) {
54
+ if (abort.signal.aborted)
55
+ break;
56
+ const message = error instanceof Error ? error.message : String(error);
57
+ tracker.set(attempt === 0 ? "reconnecting" : "failed", message);
58
+ deps.log(`imessage ${config.connectionId}: ${message}; retrying`);
59
+ attempt += 1;
60
+ await sleep(reconnectDelay(attempt - 1), abort.signal);
61
+ }
62
+ finally {
63
+ await stream?.stop().catch(() => undefined);
64
+ stream = undefined;
65
+ }
66
+ }
67
+ tracker.set("stopped");
68
+ }
69
+ return {
70
+ async start() {
71
+ loop = run();
72
+ await Promise.resolve();
73
+ },
74
+ async stop() {
75
+ abort.abort();
76
+ await stream?.stop().catch(() => undefined);
77
+ await loop;
78
+ },
79
+ state: () => tracker.get(),
80
+ };
81
+ }
82
+ /** The real transport over the Spectrum SDK; the packages load on first use. */
83
+ export function sdkIMessageTransport(credentials, importer = (s) => import(s)) {
84
+ return {
85
+ async open() {
86
+ const core = await importer("@spectrum-ts/core");
87
+ const module = await importer("@spectrum-ts/imessage");
88
+ const app = await core.Spectrum({ projectId: credentials.projectId, projectSecret: credentials.projectSecret, providers: [module.imessage.config()] });
89
+ return { messages: app.messages, stop: () => app.stop() };
90
+ },
91
+ };
92
+ }
@@ -0,0 +1,63 @@
1
+ import type { ContactInfo } from "../../channels/types.js";
2
+ import { ReplyDelivery } from "./delivery.js";
3
+ import type { AgentChatController } from "../chatTurn.js";
4
+ import type { AgentService } from "../service.js";
5
+ import type { AgentInboundMessage } from "./port.js";
6
+ /** What the router needs from the Host's connections: who a connection is, and how to answer on it. */
7
+ export type InboundConnections = {
8
+ find(connectionId: string): {
9
+ provider: string;
10
+ secret: string | undefined;
11
+ } | undefined;
12
+ reply(connectionId: string, to: string, text: string, operationKey: string): Promise<void>;
13
+ /** How people reach the connection (the number to text), when the provider can tell. */
14
+ contact?(connectionId: string): Promise<ContactInfo | undefined>;
15
+ /** The connection with its secrets, for providers the Host listens on itself (a stream instead of a webhook). */
16
+ resolve?(connectionId: string): ListenerConnection | undefined;
17
+ };
18
+ export type ListenerConnection = {
19
+ id: string;
20
+ provider: string;
21
+ config: unknown;
22
+ secrets: Record<string, string>;
23
+ credential?: {
24
+ secrets: Record<string, string>;
25
+ } | undefined;
26
+ };
27
+ export type InboundResponse = {
28
+ status: number;
29
+ body?: unknown;
30
+ };
31
+ type Headers = Record<string, string | undefined>;
32
+ /**
33
+ * Receives provider webhooks for agents. A request is accepted only when the connection has an
34
+ * inbound secret and the provider's signature verifies; everything else is refused without
35
+ * saying why. Replies go back on the same connection to the sender, once per turn.
36
+ */
37
+ export declare class AgentInboundRouter {
38
+ private readonly options;
39
+ private readonly limiter;
40
+ /** Sends each reply back and keeps track of whether it got through. */
41
+ readonly delivery: ReplyDelivery;
42
+ constructor(options: {
43
+ service: AgentService;
44
+ chat: Pick<AgentChatController, "send">;
45
+ connections: InboundConnections;
46
+ projectRoot: string;
47
+ logError?: (message: string) => void;
48
+ senderLimitPerMinute?: number;
49
+ delivery?: ReplyDelivery;
50
+ });
51
+ handle(connectionId: string, headers: Headers, rawBody: string): Promise<InboundResponse>;
52
+ /**
53
+ * A message from any source (a webhook or a stream the Host listens on): enforce the sender allow-list and
54
+ * rate limit, route it to its session and answer on the same connection.
55
+ */
56
+ deliver(message: AgentInboundMessage): Promise<InboundResponse>;
57
+ private verify;
58
+ private parse;
59
+ /** After a restart: retry the turns it cut off on always-on iMessage agents, answering the same sender once. */
60
+ resumeInterrupted(): Promise<number>;
61
+ private converse;
62
+ }
63
+ export {};
@@ -0,0 +1,143 @@
1
+ import { isAgentError } from "../errors.js";
2
+ import { ReplyDelivery } from "./delivery.js";
3
+ import { SlidingWindowLimiter } from "../webChat/rateLimit.js";
4
+ import { parseAgentMailInbound, verifyAgentMailSignature } from "./agentmail.js";
5
+ import { parseSlackInbound, verifySlackSignature } from "./slack.js";
6
+ import { parseTelegramInbound, verifyTelegramSecret } from "./telegram.js";
7
+ const MAX_REPLY_CHARS = 3500;
8
+ /** Messages one sender may send an agent per minute before the rest are ignored. */
9
+ const SENDER_LIMIT_PER_MINUTE = 20;
10
+ /**
11
+ * Receives provider webhooks for agents. A request is accepted only when the connection has an
12
+ * inbound secret and the provider's signature verifies; everything else is refused without
13
+ * saying why. Replies go back on the same connection to the sender, once per turn.
14
+ */
15
+ export class AgentInboundRouter {
16
+ options;
17
+ limiter = new SlidingWindowLimiter();
18
+ /** Sends each reply back and keeps track of whether it got through. */
19
+ delivery;
20
+ constructor(options) {
21
+ this.options = options;
22
+ this.delivery =
23
+ options.delivery ??
24
+ new ReplyDelivery({
25
+ service: options.service,
26
+ reply: (connectionId, to, text, key) => options.connections.reply(connectionId, to, text, key),
27
+ ...(options.logError ? { log: options.logError } : {}),
28
+ });
29
+ }
30
+ async handle(connectionId, headers, rawBody) {
31
+ const connection = this.options.connections.find(connectionId);
32
+ if (!connection || connection.secret === undefined)
33
+ return { status: 404 };
34
+ const verified = this.verify(connection.provider, connection.secret, connectionId, headers, rawBody);
35
+ if (!verified)
36
+ return { status: 401 };
37
+ let payload;
38
+ try {
39
+ payload = JSON.parse(rawBody);
40
+ }
41
+ catch {
42
+ return { status: 400 };
43
+ }
44
+ if (connection.provider === "slack" && payload?.type === "url_verification") {
45
+ return { status: 200, body: { challenge: payload.challenge } };
46
+ }
47
+ const parsed = this.parse(connection.provider, connectionId, connection.secret, headers, payload);
48
+ if (!parsed)
49
+ return { status: 400 };
50
+ if ("ignored" in parsed)
51
+ return { status: 200, body: { ignored: true } };
52
+ return this.deliver(parsed.message);
53
+ }
54
+ /**
55
+ * A message from any source (a webhook or a stream the Host listens on): enforce the sender allow-list and
56
+ * rate limit, route it to its session and answer on the same connection.
57
+ */
58
+ async deliver(message) {
59
+ const limit = this.limiter.take(`${message.connectionId}\0${message.from ?? ""}`, this.options.senderLimitPerMinute ?? SENDER_LIMIT_PER_MINUTE, Date.now());
60
+ if (!limit.ok) {
61
+ this.options.logError?.(`agent inbound ${message.provider}: sender over the rate limit, message ignored`);
62
+ return { status: 200, body: { ignored: true } };
63
+ }
64
+ let routed;
65
+ try {
66
+ routed = await this.options.service.receive(this.options.projectRoot, message);
67
+ }
68
+ catch (error) {
69
+ // An unbound, disabled or not-allowed sender looks the same as success to the sender; nothing is retried.
70
+ if (isAgentError(error))
71
+ return { status: 200, body: { ignored: true } };
72
+ throw error;
73
+ }
74
+ if (routed.duplicate)
75
+ return { status: 200, body: { duplicate: true } };
76
+ void this.converse(routed, message);
77
+ return { status: 200, body: { ok: true } };
78
+ }
79
+ verify(provider, secret, connectionId, headers, rawBody) {
80
+ if (provider === "slack") {
81
+ return verifySlackSignature({ connectionId, signingSecret: secret }, { ...(headers["x-slack-signature"] ? { signature: headers["x-slack-signature"] } : {}), ...(headers["x-slack-request-timestamp"] ? { requestTimestamp: headers["x-slack-request-timestamp"] } : {}) }, rawBody).ok;
82
+ }
83
+ if (provider === "telegram")
84
+ return verifyTelegramSecret({ connectionId, secretToken: secret }, headers["x-telegram-bot-api-secret-token"]).ok;
85
+ if (provider === "agentmail")
86
+ return verifyAgentMailSignature({ connectionId, webhookSecret: secret }, headers["x-agentmail-signature"], rawBody).ok;
87
+ return false;
88
+ }
89
+ parse(provider, connectionId, secret, _headers, payload) {
90
+ const result = provider === "slack"
91
+ ? parseSlackInbound({ connectionId, signingSecret: secret }, payload)
92
+ : provider === "telegram"
93
+ ? parseTelegramInbound({ connectionId, secretToken: secret }, payload)
94
+ : parseAgentMailInbound({ connectionId, webhookSecret: secret }, payload);
95
+ if (!result.ok)
96
+ return undefined;
97
+ if ("ignored" in result)
98
+ return { ignored: true };
99
+ return { message: result.message };
100
+ }
101
+ /** After a restart: retry the turns it cut off on always-on iMessage agents, answering the same sender once. */
102
+ async resumeInterrupted() {
103
+ const turns = await this.options.service.resumableTurns(this.options.projectRoot);
104
+ for (const turn of turns) {
105
+ const message = { provider: turn.provider, connectionId: turn.connectionId, eventId: `resume:${turn.turnId}`, threadId: turn.threadId, text: turn.text, from: turn.from, replyTo: turn.from };
106
+ void this.converse({ agentId: turn.agentId, sessionId: turn.sessionId }, message, turn.turnId);
107
+ }
108
+ return turns.length;
109
+ }
110
+ async converse(routed, message, resumeTurnId) {
111
+ const { chat, connections, logError } = this.options;
112
+ try {
113
+ const detail = await chat.send({
114
+ projectRoot: this.options.projectRoot,
115
+ agentId: routed.agentId,
116
+ sessionId: routed.sessionId,
117
+ text: message.text,
118
+ author: message.from ?? null,
119
+ source: message.provider,
120
+ ...(resumeTurnId ? { resumeTurnId } : {}),
121
+ });
122
+ const turn = detail.turns.at(-1);
123
+ const reply = [...detail.messages].reverse().find((m) => m.role === "assistant" && m.turnId === turn?.id);
124
+ if (!turn || !reply || !message.replyTo)
125
+ return;
126
+ // The turn only counts as succeeded once this reaches the person; failures are retried or left for a person.
127
+ await this.delivery.deliver({
128
+ turnId: turn.id,
129
+ sessionId: routed.sessionId,
130
+ connectionId: message.connectionId,
131
+ provider: message.provider,
132
+ replyTo: message.replyTo,
133
+ text: reply.text.slice(0, MAX_REPLY_CHARS),
134
+ });
135
+ }
136
+ catch (error) {
137
+ if (isAgentError(error) && error.code === "agent.limit_reached" && message.replyTo) {
138
+ await connections.reply(message.connectionId, message.replyTo, error.message, `agent-limit:${routed.sessionId}:${message.eventId}`).catch(() => undefined);
139
+ }
140
+ logError?.(`agent inbound ${message.provider} turn failed: ${isAgentError(error) ? `${error.code}${error.message !== error.code ? ` (${error.message})` : ""}` : error instanceof Error ? error.message : String(error)}`);
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,36 @@
1
+ import type { AgentInboundMessage } from "./port.js";
2
+ /** A stream the Host holds open to a provider so messages arrive without a public URL. */
3
+ export type ChannelListener = {
4
+ /** Resolves once the first connection attempt has been made; it keeps itself connected until `stop`. */
5
+ start(): Promise<void>;
6
+ stop(): Promise<void>;
7
+ state(): ListenerState;
8
+ };
9
+ export type ListenerState = {
10
+ status: "starting" | "connected" | "reconnecting" | "failed" | "stopped";
11
+ since: string;
12
+ /** Last failure, never a secret. */
13
+ error?: string;
14
+ /** Messages handed to the agents since the listener started. */
15
+ received: number;
16
+ };
17
+ export type ListenerDeps = {
18
+ onMessage(message: AgentInboundMessage): Promise<void>;
19
+ log(message: string): void;
20
+ now?: () => number;
21
+ /** Test seam: waits between reconnect attempts. */
22
+ sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
23
+ };
24
+ export declare const RECONNECT_MIN_MS = 2000;
25
+ export declare const RECONNECT_MAX_MS = 60000;
26
+ export declare function reconnectDelay(attempt: number): number;
27
+ export declare function abortableSleep(ms: number, signal: AbortSignal): Promise<void>;
28
+ /** Tracks a listener's state for the console. */
29
+ export declare class StateTracker {
30
+ private readonly now;
31
+ private current;
32
+ constructor(now?: () => number);
33
+ set(status: ListenerState["status"], error?: string): void;
34
+ received(): void;
35
+ get(): ListenerState;
36
+ }
@@ -0,0 +1,38 @@
1
+ export const RECONNECT_MIN_MS = 2_000;
2
+ export const RECONNECT_MAX_MS = 60_000;
3
+ export function reconnectDelay(attempt) {
4
+ return Math.min(RECONNECT_MAX_MS, RECONNECT_MIN_MS * 2 ** Math.min(attempt, 10));
5
+ }
6
+ export function abortableSleep(ms, signal) {
7
+ return new Promise((resolve) => {
8
+ if (signal.aborted)
9
+ return resolve();
10
+ const timer = setTimeout(done, ms);
11
+ function done() {
12
+ clearTimeout(timer);
13
+ signal.removeEventListener("abort", done);
14
+ resolve();
15
+ }
16
+ signal.addEventListener("abort", done, { once: true });
17
+ });
18
+ }
19
+ /** Tracks a listener's state for the console. */
20
+ export class StateTracker {
21
+ now;
22
+ current;
23
+ constructor(now = Date.now) {
24
+ this.now = now;
25
+ this.current = { status: "starting", since: new Date(now()).toISOString(), received: 0 };
26
+ }
27
+ set(status, error) {
28
+ this.current = { ...this.current, status, since: new Date(this.now()).toISOString(), ...(error !== undefined ? { error } : {}) };
29
+ if (error === undefined && status !== "failed" && status !== "reconnecting")
30
+ delete this.current.error;
31
+ }
32
+ received() {
33
+ this.current = { ...this.current, received: this.current.received + 1 };
34
+ }
35
+ get() {
36
+ return { ...this.current };
37
+ }
38
+ }
@@ -0,0 +1,19 @@
1
+ import type { AgentInboundMessage } from "./port.js";
2
+ export type PhotonInboundConfig = {
3
+ connectionId: string;
4
+ webhookSecret: string;
5
+ };
6
+ export type PhotonParseResult = {
7
+ ok: true;
8
+ message: AgentInboundMessage;
9
+ } | {
10
+ ok: false;
11
+ code: "agent.signature_invalid" | "agent.invalid";
12
+ };
13
+ export declare function verifyPhotonSignature(config: PhotonInboundConfig, signatureHeader: string | undefined, rawBody: string): {
14
+ ok: true;
15
+ } | {
16
+ ok: false;
17
+ code: "agent.signature_invalid";
18
+ };
19
+ export declare function parsePhotonInbound(config: PhotonInboundConfig, payload: unknown): PhotonParseResult;
@@ -0,0 +1,38 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ export function verifyPhotonSignature(config, signatureHeader, rawBody) {
3
+ if (signatureHeader === undefined)
4
+ return { ok: false, code: "agent.signature_invalid" };
5
+ const expected = createHmac("sha256", config.webhookSecret).update(rawBody).digest("hex");
6
+ const left = Buffer.from(expected);
7
+ const right = Buffer.from(signatureHeader);
8
+ if (left.length !== right.length || !timingSafeEqual(left, right)) {
9
+ return { ok: false, code: "agent.signature_invalid" };
10
+ }
11
+ return { ok: true };
12
+ }
13
+ export function parsePhotonInbound(config, payload) {
14
+ if (typeof payload !== "object" || payload === null)
15
+ return { ok: false, code: "agent.invalid" };
16
+ const record = payload;
17
+ const eventId = typeof record.id === "string" ? record.id : undefined;
18
+ const threadId = typeof record.conversation_id === "string"
19
+ ? record.conversation_id
20
+ : typeof record.sender === "string"
21
+ ? record.sender
22
+ : undefined;
23
+ const text = typeof record.text === "string" ? record.text.trim() : "";
24
+ if (eventId === undefined || threadId === undefined || text.length === 0) {
25
+ return { ok: false, code: "agent.invalid" };
26
+ }
27
+ return {
28
+ ok: true,
29
+ message: {
30
+ provider: "photon",
31
+ connectionId: config.connectionId,
32
+ eventId: `photon:${eventId}`,
33
+ threadId,
34
+ text,
35
+ ...(typeof record.sender === "string" ? { from: record.sender } : {}),
36
+ },
37
+ };
38
+ }
@@ -0,0 +1,27 @@
1
+ import type { AgentCommunicationProvider } from "../../config/loadAgent.js";
2
+ export type { AgentCommunicationProvider };
3
+ export type AgentInboundMessage = {
4
+ provider: AgentCommunicationProvider;
5
+ connectionId: string;
6
+ eventId: string;
7
+ threadId: string;
8
+ text: string;
9
+ from?: string;
10
+ /** Where a reply goes on the provider (channel id, chat id, or address). */
11
+ replyTo?: string;
12
+ };
13
+ /** The session a message belongs to. Failures (unbound, disabled) throw AgentError. */
14
+ export type AgentInboundResult = {
15
+ agentId: string;
16
+ sessionId: string;
17
+ duplicate: boolean;
18
+ };
19
+ export type AgentInboundPort = {
20
+ receive(projectRoot: string, message: AgentInboundMessage): Promise<AgentInboundResult>;
21
+ };
22
+ export declare function agentThreadKey(input: {
23
+ provider: AgentCommunicationProvider;
24
+ connectionId: string;
25
+ threadId: string;
26
+ }): string;
27
+ export declare function inboundDedupeKey(message: Pick<AgentInboundMessage, "provider" | "eventId">): string;
@@ -0,0 +1,6 @@
1
+ export function agentThreadKey(input) {
2
+ return `${input.provider}\0${input.connectionId}\0${input.threadId}`;
3
+ }
4
+ export function inboundDedupeKey(message) {
5
+ return `${message.provider}\0${message.eventId}`;
6
+ }
@@ -0,0 +1,28 @@
1
+ import type { AgentInboundMessage } from "./port.js";
2
+ export type SlackInboundConfig = {
3
+ connectionId: string;
4
+ signingSecret: string;
5
+ };
6
+ export type SlackInboundHeaders = {
7
+ signature?: string;
8
+ requestTimestamp?: string;
9
+ };
10
+ export type SlackParseResult = {
11
+ ok: true;
12
+ message: AgentInboundMessage;
13
+ } | {
14
+ ok: true;
15
+ ignored: true;
16
+ reason: "bot_message";
17
+ } | {
18
+ ok: false;
19
+ code: "agent.signature_invalid" | "agent.invalid";
20
+ };
21
+ export declare function verifySlackSignature(config: SlackInboundConfig, headers: SlackInboundHeaders, rawBody: string, nowMs?: number): {
22
+ ok: true;
23
+ } | {
24
+ ok: false;
25
+ code: "agent.signature_invalid";
26
+ };
27
+ export declare function parseSlackInbound(config: SlackInboundConfig, payload: unknown): SlackParseResult;
28
+ export declare function slackThreadKey(config: SlackInboundConfig, threadId: string): string;
@@ -0,0 +1,65 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import { agentThreadKey } from "./port.js";
3
+ export function verifySlackSignature(config, headers, rawBody, nowMs = Date.now()) {
4
+ const signature = headers.signature;
5
+ const timestamp = headers.requestTimestamp;
6
+ if (signature === undefined || timestamp === undefined) {
7
+ return { ok: false, code: "agent.signature_invalid" };
8
+ }
9
+ const ts = Number(timestamp);
10
+ if (!Number.isFinite(ts) || Math.abs(nowMs - ts * 1000) > 5 * 60_000) {
11
+ return { ok: false, code: "agent.signature_invalid" };
12
+ }
13
+ const base = `v0:${timestamp}:${rawBody}`;
14
+ const expected = `v0=${createHmac("sha256", config.signingSecret).update(base).digest("hex")}`;
15
+ const left = Buffer.from(expected);
16
+ const right = Buffer.from(signature);
17
+ if (left.length !== right.length || !timingSafeEqual(left, right)) {
18
+ return { ok: false, code: "agent.signature_invalid" };
19
+ }
20
+ return { ok: true };
21
+ }
22
+ export function parseSlackInbound(config, payload) {
23
+ if (typeof payload !== "object" || payload === null)
24
+ return { ok: false, code: "agent.invalid" };
25
+ const event = payload.event;
26
+ if (typeof event !== "object" || event === null)
27
+ return { ok: false, code: "agent.invalid" };
28
+ const record = event;
29
+ if (record.bot_id !== undefined || record.subtype === "bot_message") {
30
+ return { ok: true, ignored: true, reason: "bot_message" };
31
+ }
32
+ const text = typeof record.text === "string" ? record.text.trim() : "";
33
+ if (text.length === 0)
34
+ return { ok: false, code: "agent.invalid" };
35
+ const channel = typeof record.channel === "string" ? record.channel : undefined;
36
+ const thread = typeof record.thread_ts === "string"
37
+ ? record.thread_ts
38
+ : typeof record.ts === "string"
39
+ ? record.ts
40
+ : undefined;
41
+ const eventId = typeof record.event_ts === "string"
42
+ ? record.event_ts
43
+ : typeof record.ts === "string"
44
+ ? record.ts
45
+ : undefined;
46
+ if (channel === undefined || thread === undefined || eventId === undefined) {
47
+ return { ok: false, code: "agent.invalid" };
48
+ }
49
+ const user = typeof record.user === "string" ? record.user : undefined;
50
+ return {
51
+ ok: true,
52
+ message: {
53
+ provider: "slack",
54
+ connectionId: config.connectionId,
55
+ eventId: `slack:${eventId}`,
56
+ threadId: thread,
57
+ text,
58
+ replyTo: channel,
59
+ ...(user !== undefined ? { from: user } : {}),
60
+ },
61
+ };
62
+ }
63
+ export function slackThreadKey(config, threadId) {
64
+ return agentThreadKey({ provider: "slack", connectionId: config.connectionId, threadId });
65
+ }
@@ -0,0 +1,36 @@
1
+ import type { AgentService } from "../service.js";
2
+ import type { ListenerConnection } from "./inbound.js";
3
+ import type { ChannelListener, ListenerDeps, ListenerState } from "./listener.js";
4
+ import type { AgentInboundMessage } from "./port.js";
5
+ export type ListenerFactory = (connection: ListenerConnection, deps: ListenerDeps) => ChannelListener | undefined;
6
+ /** Listeners the Host can hold open itself. Other providers (Slack, Telegram) stay on webhooks. */
7
+ export declare const DEFAULT_FACTORIES: Record<string, ListenerFactory>;
8
+ /**
9
+ * Keeps one listener per connection that an enabled agent answers on: starts it when the agent is enabled or
10
+ * linked, restarts it when the connection's settings change, stops it when no enabled agent uses it.
11
+ * `reconcile` is cheap and idempotent, so it runs on every agent change and on a timer.
12
+ */
13
+ export declare class ChannelSupervisor {
14
+ private readonly options;
15
+ private readonly running;
16
+ private chain;
17
+ private timer;
18
+ private stopped;
19
+ private readonly warned;
20
+ constructor(options: {
21
+ service: Pick<AgentService, "list" | "onChange">;
22
+ projectRoot: string;
23
+ resolve: (connectionId: string) => ListenerConnection | undefined;
24
+ deliver: (message: AgentInboundMessage) => Promise<unknown>;
25
+ log: (message: string) => void;
26
+ factories?: Record<string, ListenerFactory>;
27
+ intervalMs?: number;
28
+ });
29
+ /** Start following agents: listeners for what is linked now, and again after every change. */
30
+ start(): void;
31
+ reconcile(): Promise<void>;
32
+ private run;
33
+ /** The state of the listener on a connection, or undefined when none runs (webhook providers, no enabled agent). */
34
+ status(connectionId: string): ListenerState | undefined;
35
+ stop(): Promise<void>;
36
+ }