@immediately-run/sdk 0.45.3 → 0.47.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 (219) hide show
  1. package/dist/MDXProvider.cjs +1 -5
  2. package/dist/MDXProvider.cjs.map +1 -1
  3. package/dist/MDXProvider.js +1 -5
  4. package/dist/MDXProvider.js.map +1 -1
  5. package/dist/RoutingSpec.cjs.map +1 -1
  6. package/dist/TinkerableContext.cjs.map +1 -1
  7. package/dist/TinkerableContext.js.map +1 -1
  8. package/dist/auth.cjs.map +1 -1
  9. package/dist/auth.js.map +1 -1
  10. package/dist/boot.cjs +2 -9
  11. package/dist/boot.cjs.map +1 -1
  12. package/dist/boot.d.cts +2 -2
  13. package/dist/boot.d.ts +2 -2
  14. package/dist/boot.js +2 -9
  15. package/dist/boot.js.map +1 -1
  16. package/dist/catalog.cjs.map +1 -1
  17. package/dist/catalog.js.map +1 -1
  18. package/dist/components/Admonition.cjs +4 -13
  19. package/dist/components/Admonition.cjs.map +1 -1
  20. package/dist/components/Admonition.js +4 -13
  21. package/dist/components/Admonition.js.map +1 -1
  22. package/dist/components/FileRouter.cjs +11 -3
  23. package/dist/components/FileRouter.cjs.map +1 -1
  24. package/dist/components/FileRouter.js +11 -3
  25. package/dist/components/FileRouter.js.map +1 -1
  26. package/dist/components/HeadingAnchor.cjs.map +1 -1
  27. package/dist/components/HeadingAnchor.js.map +1 -1
  28. package/dist/components/Link.cjs.map +1 -1
  29. package/dist/components/Link.js.map +1 -1
  30. package/dist/components/MainContent.cjs +11 -1
  31. package/dist/components/MainContent.cjs.map +1 -1
  32. package/dist/components/MainContent.js +11 -1
  33. package/dist/components/MainContent.js.map +1 -1
  34. package/dist/components/MountImage.cjs +1 -9
  35. package/dist/components/MountImage.cjs.map +1 -1
  36. package/dist/components/MountImage.js +1 -9
  37. package/dist/components/MountImage.js.map +1 -1
  38. package/dist/components/Routes.cjs +1 -4
  39. package/dist/components/Routes.cjs.map +1 -1
  40. package/dist/components/Routes.d.cts +1 -1
  41. package/dist/components/Routes.d.ts +1 -1
  42. package/dist/components/Routes.js +1 -4
  43. package/dist/components/Routes.js.map +1 -1
  44. package/dist/components/SafeInclude.cjs +1 -4
  45. package/dist/components/SafeInclude.cjs.map +1 -1
  46. package/dist/components/SafeInclude.js +1 -4
  47. package/dist/components/SafeInclude.js.map +1 -1
  48. package/dist/components/WikiLink.cjs +1 -10
  49. package/dist/components/WikiLink.cjs.map +1 -1
  50. package/dist/components/WikiLink.js +1 -10
  51. package/dist/components/WikiLink.js.map +1 -1
  52. package/dist/components/defaults.cjs.map +1 -1
  53. package/dist/components/defaults.d.cts +1 -1
  54. package/dist/components/defaults.d.ts +1 -1
  55. package/dist/components/defaults.js.map +1 -1
  56. package/dist/components/errors.cjs +3 -1
  57. package/dist/components/errors.cjs.map +1 -1
  58. package/dist/components/errors.js +3 -1
  59. package/dist/components/errors.js.map +1 -1
  60. package/dist/contextUtils.cjs.map +1 -1
  61. package/dist/contextUtils.js.map +1 -1
  62. package/dist/contribute.cjs.map +1 -1
  63. package/dist/contribute.js.map +1 -1
  64. package/dist/debug.cjs +2 -1
  65. package/dist/debug.cjs.map +1 -1
  66. package/dist/debug.js +3 -8
  67. package/dist/debug.js.map +1 -1
  68. package/dist/diagnostics.cjs.map +1 -1
  69. package/dist/diagnostics.js.map +1 -1
  70. package/dist/editor.cjs.map +1 -1
  71. package/dist/editor.js.map +1 -1
  72. package/dist/editorContext.cjs.map +1 -1
  73. package/dist/editorContext.js.map +1 -1
  74. package/dist/fs.cjs.map +1 -1
  75. package/dist/fs.js.map +1 -1
  76. package/dist/hostAttention.cjs +57 -0
  77. package/dist/hostAttention.cjs.map +1 -0
  78. package/dist/hostAttention.d.cts +39 -0
  79. package/dist/hostAttention.d.ts +39 -0
  80. package/dist/hostAttention.js +31 -0
  81. package/dist/hostAttention.js.map +1 -0
  82. package/dist/hostTransport.cjs +55 -0
  83. package/dist/hostTransport.cjs.map +1 -0
  84. package/dist/hostTransport.d.cts +12 -0
  85. package/dist/hostTransport.d.ts +12 -0
  86. package/dist/hostTransport.js +30 -0
  87. package/dist/hostTransport.js.map +1 -0
  88. package/dist/index.cjs +4 -0
  89. package/dist/index.cjs.map +1 -1
  90. package/dist/index.d.cts +3 -1
  91. package/dist/index.d.ts +3 -1
  92. package/dist/index.js +2 -0
  93. package/dist/index.js.map +1 -1
  94. package/dist/injectedBundler.cjs.map +1 -1
  95. package/dist/injectedBundler.js.map +1 -1
  96. package/dist/ipc.cjs +1 -4
  97. package/dist/ipc.cjs.map +1 -1
  98. package/dist/ipc.js +1 -4
  99. package/dist/ipc.js.map +1 -1
  100. package/dist/irMarkers.cjs.map +1 -1
  101. package/dist/irMarkers.d.cts +11 -11
  102. package/dist/irMarkers.d.ts +11 -11
  103. package/dist/irMarkers.js.map +1 -1
  104. package/dist/launch.cjs.map +1 -1
  105. package/dist/launch.js.map +1 -1
  106. package/dist/linkSpace.cjs +11 -2
  107. package/dist/linkSpace.cjs.map +1 -1
  108. package/dist/linkSpace.d.cts +23 -0
  109. package/dist/linkSpace.d.ts +23 -0
  110. package/dist/linkSpace.js +11 -2
  111. package/dist/linkSpace.js.map +1 -1
  112. package/dist/llm.cjs +26 -8
  113. package/dist/llm.cjs.map +1 -1
  114. package/dist/llm.d.cts +75 -3
  115. package/dist/llm.d.ts +75 -3
  116. package/dist/llm.js +21 -7
  117. package/dist/llm.js.map +1 -1
  118. package/dist/loading.cjs +18 -14
  119. package/dist/loading.cjs.map +1 -1
  120. package/dist/loading.d.cts +2 -2
  121. package/dist/loading.d.ts +2 -2
  122. package/dist/loading.js +19 -19
  123. package/dist/loading.js.map +1 -1
  124. package/dist/markers.cjs.map +1 -1
  125. package/dist/markers.js.map +1 -1
  126. package/dist/metadataSource.cjs +1 -5
  127. package/dist/metadataSource.cjs.map +1 -1
  128. package/dist/metadataSource.d.cts +1 -1
  129. package/dist/metadataSource.d.ts +1 -1
  130. package/dist/metadataSource.js +1 -5
  131. package/dist/metadataSource.js.map +1 -1
  132. package/dist/moduleCache.cjs +4 -1
  133. package/dist/moduleCache.cjs.map +1 -1
  134. package/dist/moduleCache.d.cts +1 -1
  135. package/dist/moduleCache.d.ts +1 -1
  136. package/dist/moduleCache.js +4 -1
  137. package/dist/moduleCache.js.map +1 -1
  138. package/dist/mountMatch.cjs.map +1 -1
  139. package/dist/mountMatch.js.map +1 -1
  140. package/dist/mounts.cjs +6 -1
  141. package/dist/mounts.cjs.map +1 -1
  142. package/dist/mounts.d.cts +2 -2
  143. package/dist/mounts.d.ts +2 -2
  144. package/dist/mounts.js +6 -1
  145. package/dist/mounts.js.map +1 -1
  146. package/dist/netFetch.cjs +3 -5
  147. package/dist/netFetch.cjs.map +1 -1
  148. package/dist/netFetch.js +3 -5
  149. package/dist/netFetch.js.map +1 -1
  150. package/dist/onFsChange.cjs.map +1 -1
  151. package/dist/onFsChange.js.map +1 -1
  152. package/dist/pathUtils.cjs +9 -12
  153. package/dist/pathUtils.cjs.map +1 -1
  154. package/dist/pathUtils.js +9 -12
  155. package/dist/pathUtils.js.map +1 -1
  156. package/dist/protocolDeadline.cjs +205 -0
  157. package/dist/protocolDeadline.cjs.map +1 -0
  158. package/dist/protocolDeadline.d.cts +146 -0
  159. package/dist/protocolDeadline.d.ts +146 -0
  160. package/dist/protocolDeadline.js +168 -0
  161. package/dist/protocolDeadline.js.map +1 -0
  162. package/dist/protocolStream.cjs +49 -6
  163. package/dist/protocolStream.cjs.map +1 -1
  164. package/dist/protocolStream.d.cts +9 -2
  165. package/dist/protocolStream.d.ts +9 -2
  166. package/dist/protocolStream.js +56 -6
  167. package/dist/protocolStream.js.map +1 -1
  168. package/dist/pushChannel.cjs +13 -9
  169. package/dist/pushChannel.cjs.map +1 -1
  170. package/dist/pushChannel.js +12 -8
  171. package/dist/pushChannel.js.map +1 -1
  172. package/dist/ready.cjs.map +1 -1
  173. package/dist/ready.js.map +1 -1
  174. package/dist/routing.cjs +3 -1
  175. package/dist/routing.cjs.map +1 -1
  176. package/dist/routing.js +3 -1
  177. package/dist/routing.js.map +1 -1
  178. package/dist/safeContent/index.cjs.map +1 -1
  179. package/dist/safeContent/index.js.map +1 -1
  180. package/dist/safeContent/parseSafeMdast.cjs.map +1 -1
  181. package/dist/safeContent/parseSafeMdast.js.map +1 -1
  182. package/dist/safeContent/renderMdast.cjs.map +1 -1
  183. package/dist/safeContent/renderMdast.js.map +1 -1
  184. package/dist/sandboxTypes.cjs.map +1 -1
  185. package/dist/sandboxUtils.cjs +69 -24
  186. package/dist/sandboxUtils.cjs.map +1 -1
  187. package/dist/sandboxUtils.d.cts +37 -4
  188. package/dist/sandboxUtils.d.ts +37 -4
  189. package/dist/sandboxUtils.js +74 -22
  190. package/dist/sandboxUtils.js.map +1 -1
  191. package/dist/scrollToId.cjs.map +1 -1
  192. package/dist/scrollToId.js.map +1 -1
  193. package/dist/secrets.cjs.map +1 -1
  194. package/dist/secrets.js.map +1 -1
  195. package/dist/tasks.cjs +12 -2
  196. package/dist/tasks.cjs.map +1 -1
  197. package/dist/tasks.js +12 -2
  198. package/dist/tasks.js.map +1 -1
  199. package/dist/testing.cjs +1 -3
  200. package/dist/testing.cjs.map +1 -1
  201. package/dist/testing.js +1 -3
  202. package/dist/testing.js.map +1 -1
  203. package/dist/theme.cjs.map +1 -1
  204. package/dist/theme.js.map +1 -1
  205. package/dist/urlUtils.cjs +7 -18
  206. package/dist/urlUtils.cjs.map +1 -1
  207. package/dist/urlUtils.js +7 -18
  208. package/dist/urlUtils.js.map +1 -1
  209. package/dist/vcs.cjs +1 -3
  210. package/dist/vcs.cjs.map +1 -1
  211. package/dist/vcs.js +1 -3
  212. package/dist/vcs.js.map +1 -1
  213. package/dist/version.cjs +1 -1
  214. package/dist/version.cjs.map +1 -1
  215. package/dist/version.d.cts +1 -1
  216. package/dist/version.d.ts +1 -1
  217. package/dist/version.js +1 -1
  218. package/dist/version.js.map +1 -1
  219. package/package.json +11 -5
@@ -5,6 +5,26 @@ interface LinkSpace {
5
5
  /** Absolute filesystem path of the enclosing corpus's root (e.g. `/app/content`),
6
6
  * or `null` when the document is not corpus-hosted (default). */
7
7
  corpusRoot: string | null;
8
+ /**
9
+ * True when the filesystem this document resolves against is **chroot'd to the
10
+ * bundle** — i.e. the port the app holds was scoped to the bundle's subtree, so
11
+ * the mount root and the bundle root are the same directory
12
+ * (`BUNDLE_LAYERS_SPEC §9`; the `T2`/`T4` wrapper, R3-319 / BL-2).
13
+ *
14
+ * Under that grant `$fs:` **collapses to the scoped root**: `$fs:/p` and `/p`
15
+ * name the same byte, because there is no longer any "mount-absolute" space
16
+ * outside the bundle for `$fs:` to reach into. Without this flag the resolver
17
+ * would hand back a mount-absolute path that the chroot then re-roots anyway —
18
+ * a link that renders as valid and resolves somewhere the author did not mean.
19
+ *
20
+ * **This is an invariant to CREATE, not one to inherit** (`BUNDLE_LAYERS_SPEC
21
+ * §11`): the shipped resolver reads `{currentFile, corpusRoot}` and nothing
22
+ * else, so `$fs:` is bundle-anchored only if something says so. It lives here,
23
+ * in the resolver, rather than as a rule each caller applies by passing
24
+ * `corpusRoot: '/'` — an invariant the arithmetic carries cannot be forgotten
25
+ * at one call site out of five.
26
+ */
27
+ bundleChrooted?: boolean;
8
28
  }
9
29
  /** Ambient link space. A corpus-rendering app wraps its document tree in
10
30
  * `<LinkSpaceContext value={{ corpusRoot }}>`; nesting a second provider inside a
@@ -39,6 +59,9 @@ type ResolvedLinkTarget =
39
59
  declare function resolveLinkTarget(raw: string, opts?: {
40
60
  currentFile?: string;
41
61
  corpusRoot?: string | null;
62
+ /** See `LinkSpace.bundleChrooted`. Under a bundle-chroot'd grant `$fs:`
63
+ * resolves in the corpus space, because they are the same space. */
64
+ bundleChrooted?: boolean;
42
65
  }): ResolvedLinkTarget;
43
66
 
44
67
  export { FS_PREFIX, type LinkSpace, LinkSpaceContext, type ResolvedLinkTarget, normalizeAbsolute, resolveLinkTarget };
@@ -5,6 +5,26 @@ interface LinkSpace {
5
5
  /** Absolute filesystem path of the enclosing corpus's root (e.g. `/app/content`),
6
6
  * or `null` when the document is not corpus-hosted (default). */
7
7
  corpusRoot: string | null;
8
+ /**
9
+ * True when the filesystem this document resolves against is **chroot'd to the
10
+ * bundle** — i.e. the port the app holds was scoped to the bundle's subtree, so
11
+ * the mount root and the bundle root are the same directory
12
+ * (`BUNDLE_LAYERS_SPEC §9`; the `T2`/`T4` wrapper, R3-319 / BL-2).
13
+ *
14
+ * Under that grant `$fs:` **collapses to the scoped root**: `$fs:/p` and `/p`
15
+ * name the same byte, because there is no longer any "mount-absolute" space
16
+ * outside the bundle for `$fs:` to reach into. Without this flag the resolver
17
+ * would hand back a mount-absolute path that the chroot then re-roots anyway —
18
+ * a link that renders as valid and resolves somewhere the author did not mean.
19
+ *
20
+ * **This is an invariant to CREATE, not one to inherit** (`BUNDLE_LAYERS_SPEC
21
+ * §11`): the shipped resolver reads `{currentFile, corpusRoot}` and nothing
22
+ * else, so `$fs:` is bundle-anchored only if something says so. It lives here,
23
+ * in the resolver, rather than as a rule each caller applies by passing
24
+ * `corpusRoot: '/'` — an invariant the arithmetic carries cannot be forgotten
25
+ * at one call site out of five.
26
+ */
27
+ bundleChrooted?: boolean;
8
28
  }
9
29
  /** Ambient link space. A corpus-rendering app wraps its document tree in
10
30
  * `<LinkSpaceContext value={{ corpusRoot }}>`; nesting a second provider inside a
@@ -39,6 +59,9 @@ type ResolvedLinkTarget =
39
59
  declare function resolveLinkTarget(raw: string, opts?: {
40
60
  currentFile?: string;
41
61
  corpusRoot?: string | null;
62
+ /** See `LinkSpace.bundleChrooted`. Under a bundle-chroot'd grant `$fs:`
63
+ * resolves in the corpus space, because they are the same space. */
64
+ bundleChrooted?: boolean;
42
65
  }): ResolvedLinkTarget;
43
66
 
44
67
  export { FS_PREFIX, type LinkSpace, LinkSpaceContext, type ResolvedLinkTarget, normalizeAbsolute, resolveLinkTarget };
package/dist/linkSpace.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import "./chunk-VHAA22YE.js";
2
2
  import { createContext } from "react";
3
3
  const FS_PREFIX = "$fs:";
4
- const LinkSpaceContext = createContext({ corpusRoot: null });
4
+ const LinkSpaceContext = createContext({
5
+ corpusRoot: null,
6
+ bundleChrooted: false
7
+ });
5
8
  const normalizeAbsolute = (path) => {
6
9
  const out = [];
7
10
  for (const seg of path.split("/")) {
@@ -11,16 +14,22 @@ const normalizeAbsolute = (path) => {
11
14
  }
12
15
  return "/" + out.join("/");
13
16
  };
17
+ function resolveCorpusAbsolute(path, corpusRoot) {
18
+ const inner = normalizeAbsolute(path);
19
+ if (corpusRoot === null || corpusRoot === "/") return { state: "resolved", path: inner };
20
+ return { state: "resolved", path: normalizeAbsolute(corpusRoot + inner) };
21
+ }
14
22
  function resolveLinkTarget(raw, opts = {}) {
15
23
  if (raw.startsWith(FS_PREFIX)) {
16
24
  const rest = raw.slice(FS_PREFIX.length);
17
25
  if (!rest.startsWith("/")) return { state: "invalid" };
26
+ if (opts.bundleChrooted) return resolveCorpusAbsolute(rest, opts.corpusRoot ?? null);
18
27
  return { state: "resolved", path: normalizeAbsolute(rest) };
19
28
  }
20
29
  if (raw.startsWith("/")) {
21
30
  const corpusRoot = opts.corpusRoot ?? null;
22
31
  if (corpusRoot !== null) {
23
- return { state: "resolved", path: normalizeAbsolute(corpusRoot + normalizeAbsolute(raw)) };
32
+ return resolveCorpusAbsolute(raw, corpusRoot);
24
33
  }
25
34
  return { state: "resolved", path: normalizeAbsolute(raw) };
26
35
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/linkSpace.ts"],"sourcesContent":["// Link path spaces (R3-273; REPO_CONTENT_DISPATCH_SPEC §9 decision, 2026-08-17).\n//\n// A document link's target resolves in one of two spaces:\n//\n// - DEFAULT — the enclosing corpus's virtual filesystem. RELATIVE targets resolve\n// against the authoring file (identical in both spaces); ABSOLUTE targets\n// (`/x/y.mdx`) resolve from the corpus root when an enclosing `LinkSpaceContext`\n// declares one, else from the filesystem root. A non-corpus app declares nothing\n// and keeps today's behavior bit-for-bit (its fs root IS its only root).\n//\n// - `$fs:` — the explicit filesystem space: `$fs:/content/x.mdx` resolves from the\n// root of the filesystem the app reads, escaping corpus-relative addressing.\n//\n// `$fs:` changes ADDRESSING, never REACH: resolution is pure path arithmetic and\n// existence is checked against the same in-mount metadata the default space uses —\n// nothing is fetched, and a link can never name what the app cannot already read.\n// A malformed `$fs:` target (anything not mount-absolute — which also catches\n// scheme smuggling like `$fs:javascript:…`) is INVALID and must render as a broken\n// link, never an anchor.\n//\n// Corpus nesting: `LinkSpaceContext` providers nest, and the NEAREST one wins —\n// which is exactly the innermost-enclosing-corpus rule (bundle encapsulation): a\n// document rendered inside a nested corpus resolves against the nested corpus.\n\nimport { createContext } from 'react';\n\nexport const FS_PREFIX = '$fs:';\n\nexport interface LinkSpace {\n /** Absolute filesystem path of the enclosing corpus's root (e.g. `/app/content`),\n * or `null` when the document is not corpus-hosted (default). */\n corpusRoot: string | null;\n}\n\n/** Ambient link space. A corpus-rendering app wraps its document tree in\n * `<LinkSpaceContext value={{ corpusRoot }}>`; nesting a second provider inside a\n * rendered sub-corpus makes the innermost root win. */\nexport const LinkSpaceContext = createContext<LinkSpace>({ corpusRoot: null });\n\n/** Collapse `.`/`..`/empty segments into a clean absolute path. `..` can never\n * climb above the root — a (virtual) root's parent is itself, which is what keeps\n * both the mount space and the corpus space closed under traversal. */\nexport const normalizeAbsolute = (path: string): string => {\n const out: string[] = [];\n for (const seg of path.split('/')) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') out.pop();\n else out.push(seg);\n }\n return '/' + out.join('/');\n};\n\nexport type ResolvedLinkTarget =\n /** Resolved to an absolute filesystem path (existence NOT checked here). */\n | { state: 'resolved'; path: string }\n /** A relative target with no known authoring file — the caller may route\n * optimistically (it cannot check existence or self-ness generically). */\n | { state: 'unresolvable' }\n /** A malformed `$fs:` target (not mount-absolute; includes scheme smuggling).\n * Callers MUST render this broken/inert — never as an anchor. */\n | { state: 'invalid' };\n\n/**\n * Resolve a raw link target (a wikilink target or an in-app href's path half —\n * fragment already split off) to an absolute filesystem path. THE shared resolver:\n * the default `WikiLink`, the markdown `a` override, and safe-content consumers\n * all route through this one function so the two render pipelines cannot drift.\n */\nexport function resolveLinkTarget(\n raw: string,\n opts: { currentFile?: string; corpusRoot?: string | null } = {},\n): ResolvedLinkTarget {\n if (raw.startsWith(FS_PREFIX)) {\n const rest = raw.slice(FS_PREFIX.length);\n // Must be mount-absolute. This single rule also fails `$fs:javascript:…`,\n // `$fs:https://…`, and every other smuggled scheme closed.\n if (!rest.startsWith('/')) return { state: 'invalid' };\n return { state: 'resolved', path: normalizeAbsolute(rest) };\n }\n if (raw.startsWith('/')) {\n const corpusRoot = opts.corpusRoot ?? null;\n if (corpusRoot !== null) {\n // Clamp the corpus-relative half FIRST (the virtual FS is closed — `/../x`\n // stays inside the corpus), THEN anchor it at the corpus root.\n return { state: 'resolved', path: normalizeAbsolute(corpusRoot + normalizeAbsolute(raw)) };\n }\n return { state: 'resolved', path: normalizeAbsolute(raw) };\n }\n // Relative: against the authoring file's directory — the same in both spaces.\n if (!opts.currentFile) return { state: 'unresolvable' };\n const dir = opts.currentFile.slice(0, opts.currentFile.lastIndexOf('/'));\n return { state: 'resolved', path: normalizeAbsolute(`${dir}/${raw}`) };\n}\n"],"mappings":";AAwBA,SAAS,qBAAqB;AAEvB,MAAM,YAAY;AAWlB,MAAM,mBAAmB,cAAyB,EAAE,YAAY,KAAK,CAAC;AAKtE,MAAM,oBAAoB,CAAC,SAAyB;AACzD,QAAM,MAAgB,CAAC;AACvB,aAAW,OAAO,KAAK,MAAM,GAAG,GAAG;AACjC,QAAI,QAAQ,MAAM,QAAQ,IAAK;AAC/B,QAAI,QAAQ,KAAM,KAAI,IAAI;AAAA,QACrB,KAAI,KAAK,GAAG;AAAA,EACnB;AACA,SAAO,MAAM,IAAI,KAAK,GAAG;AAC3B;AAkBO,SAAS,kBACd,KACA,OAA6D,CAAC,GAC1C;AACpB,MAAI,IAAI,WAAW,SAAS,GAAG;AAC7B,UAAM,OAAO,IAAI,MAAM,UAAU,MAAM;AAGvC,QAAI,CAAC,KAAK,WAAW,GAAG,EAAG,QAAO,EAAE,OAAO,UAAU;AACrD,WAAO,EAAE,OAAO,YAAY,MAAM,kBAAkB,IAAI,EAAE;AAAA,EAC5D;AACA,MAAI,IAAI,WAAW,GAAG,GAAG;AACvB,UAAM,aAAa,KAAK,cAAc;AACtC,QAAI,eAAe,MAAM;AAGvB,aAAO,EAAE,OAAO,YAAY,MAAM,kBAAkB,aAAa,kBAAkB,GAAG,CAAC,EAAE;AAAA,IAC3F;AACA,WAAO,EAAE,OAAO,YAAY,MAAM,kBAAkB,GAAG,EAAE;AAAA,EAC3D;AAEA,MAAI,CAAC,KAAK,YAAa,QAAO,EAAE,OAAO,eAAe;AACtD,QAAM,MAAM,KAAK,YAAY,MAAM,GAAG,KAAK,YAAY,YAAY,GAAG,CAAC;AACvE,SAAO,EAAE,OAAO,YAAY,MAAM,kBAAkB,GAAG,GAAG,IAAI,GAAG,EAAE,EAAE;AACvE;","names":[]}
1
+ {"version":3,"sources":["../src/linkSpace.ts"],"sourcesContent":["// Link path spaces (R3-273; REPO_CONTENT_DISPATCH_SPEC §9 decision, 2026-08-17).\n//\n// A document link's target resolves in one of two spaces:\n//\n// - DEFAULT — the enclosing corpus's virtual filesystem. RELATIVE targets resolve\n// against the authoring file (identical in both spaces); ABSOLUTE targets\n// (`/x/y.mdx`) resolve from the corpus root when an enclosing `LinkSpaceContext`\n// declares one, else from the filesystem root. A non-corpus app declares nothing\n// and keeps today's behavior bit-for-bit (its fs root IS its only root).\n//\n// - `$fs:` — the explicit filesystem space: `$fs:/content/x.mdx` resolves from the\n// root of the filesystem the app reads, escaping corpus-relative addressing.\n//\n// `$fs:` changes ADDRESSING, never REACH: resolution is pure path arithmetic and\n// existence is checked against the same in-mount metadata the default space uses —\n// nothing is fetched, and a link can never name what the app cannot already read.\n// A malformed `$fs:` target (anything not mount-absolute — which also catches\n// scheme smuggling like `$fs:javascript:…`) is INVALID and must render as a broken\n// link, never an anchor.\n//\n// Corpus nesting: `LinkSpaceContext` providers nest, and the NEAREST one wins —\n// which is exactly the innermost-enclosing-corpus rule (bundle encapsulation): a\n// document rendered inside a nested corpus resolves against the nested corpus.\n\nimport { createContext } from 'react';\n\nexport const FS_PREFIX = '$fs:';\n\nexport interface LinkSpace {\n /** Absolute filesystem path of the enclosing corpus's root (e.g. `/app/content`),\n * or `null` when the document is not corpus-hosted (default). */\n corpusRoot: string | null;\n /**\n * True when the filesystem this document resolves against is **chroot'd to the\n * bundle** — i.e. the port the app holds was scoped to the bundle's subtree, so\n * the mount root and the bundle root are the same directory\n * (`BUNDLE_LAYERS_SPEC §9`; the `T2`/`T4` wrapper, R3-319 / BL-2).\n *\n * Under that grant `$fs:` **collapses to the scoped root**: `$fs:/p` and `/p`\n * name the same byte, because there is no longer any \"mount-absolute\" space\n * outside the bundle for `$fs:` to reach into. Without this flag the resolver\n * would hand back a mount-absolute path that the chroot then re-roots anyway —\n * a link that renders as valid and resolves somewhere the author did not mean.\n *\n * **This is an invariant to CREATE, not one to inherit** (`BUNDLE_LAYERS_SPEC\n * §11`): the shipped resolver reads `{currentFile, corpusRoot}` and nothing\n * else, so `$fs:` is bundle-anchored only if something says so. It lives here,\n * in the resolver, rather than as a rule each caller applies by passing\n * `corpusRoot: '/'` — an invariant the arithmetic carries cannot be forgotten\n * at one call site out of five.\n */\n bundleChrooted?: boolean;\n}\n\n/** Ambient link space. A corpus-rendering app wraps its document tree in\n * `<LinkSpaceContext value={{ corpusRoot }}>`; nesting a second provider inside a\n * rendered sub-corpus makes the innermost root win. */\nexport const LinkSpaceContext = createContext<LinkSpace>({\n corpusRoot: null,\n bundleChrooted: false,\n});\n\n/** Collapse `.`/`..`/empty segments into a clean absolute path. `..` can never\n * climb above the root — a (virtual) root's parent is itself, which is what keeps\n * both the mount space and the corpus space closed under traversal. */\nexport const normalizeAbsolute = (path: string): string => {\n const out: string[] = [];\n for (const seg of path.split('/')) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') out.pop();\n else out.push(seg);\n }\n return '/' + out.join('/');\n};\n\nexport type ResolvedLinkTarget =\n /** Resolved to an absolute filesystem path (existence NOT checked here). */\n | { state: 'resolved'; path: string }\n /** A relative target with no known authoring file — the caller may route\n * optimistically (it cannot check existence or self-ness generically). */\n | { state: 'unresolvable' }\n /** A malformed `$fs:` target (not mount-absolute; includes scheme smuggling).\n * Callers MUST render this broken/inert — never as an anchor. */\n | { state: 'invalid' };\n\n/** Anchor a corpus-absolute path at `corpusRoot`, clamping the corpus-relative\n * half FIRST so the virtual corpus space stays closed under traversal. Shared by\n * the `/p` branch and — under a bundle chroot — the `$fs:/p` branch, so the two\n * spellings cannot drift into resolving differently. */\nfunction resolveCorpusAbsolute(path: string, corpusRoot: string | null): ResolvedLinkTarget {\n const inner = normalizeAbsolute(path);\n if (corpusRoot === null || corpusRoot === '/') return { state: 'resolved', path: inner };\n return { state: 'resolved', path: normalizeAbsolute(corpusRoot + inner) };\n}\n\n/**\n * Resolve a raw link target (a wikilink target or an in-app href's path half —\n * fragment already split off) to an absolute filesystem path. THE shared resolver:\n * the default `WikiLink`, the markdown `a` override, and safe-content consumers\n * all route through this one function so the two render pipelines cannot drift.\n */\nexport function resolveLinkTarget(\n raw: string,\n opts: {\n currentFile?: string;\n corpusRoot?: string | null;\n /** See `LinkSpace.bundleChrooted`. Under a bundle-chroot'd grant `$fs:`\n * resolves in the corpus space, because they are the same space. */\n bundleChrooted?: boolean;\n } = {},\n): ResolvedLinkTarget {\n if (raw.startsWith(FS_PREFIX)) {\n const rest = raw.slice(FS_PREFIX.length);\n // Must be mount-absolute. This single rule also fails `$fs:javascript:…`,\n // `$fs:https://…`, and every other smuggled scheme closed.\n if (!rest.startsWith('/')) return { state: 'invalid' };\n // Under a bundle chroot the mount root IS the bundle root, so `$fs:` has\n // nowhere outside to name: it takes the corpus-absolute branch below and the\n // two spellings collapse. `normalizeAbsolute` clamps `..` at the root either\n // way, so neither spelling can climb out — the collapse changes WHERE a\n // `$fs:` link points, never whether it can escape.\n if (opts.bundleChrooted) return resolveCorpusAbsolute(rest, opts.corpusRoot ?? null);\n return { state: 'resolved', path: normalizeAbsolute(rest) };\n }\n if (raw.startsWith('/')) {\n const corpusRoot = opts.corpusRoot ?? null;\n if (corpusRoot !== null) {\n // Clamp the corpus-relative half FIRST (the virtual FS is closed — `/../x`\n // stays inside the corpus), THEN anchor it at the corpus root.\n return resolveCorpusAbsolute(raw, corpusRoot);\n }\n return { state: 'resolved', path: normalizeAbsolute(raw) };\n }\n // Relative: against the authoring file's directory — the same in both spaces.\n if (!opts.currentFile) return { state: 'unresolvable' };\n const dir = opts.currentFile.slice(0, opts.currentFile.lastIndexOf('/'));\n return { state: 'resolved', path: normalizeAbsolute(`${dir}/${raw}`) };\n}\n"],"mappings":";AAwBA,SAAS,qBAAqB;AAEvB,MAAM,YAAY;AA+BlB,MAAM,mBAAmB,cAAyB;AAAA,EACvD,YAAY;AAAA,EACZ,gBAAgB;AAClB,CAAC;AAKM,MAAM,oBAAoB,CAAC,SAAyB;AACzD,QAAM,MAAgB,CAAC;AACvB,aAAW,OAAO,KAAK,MAAM,GAAG,GAAG;AACjC,QAAI,QAAQ,MAAM,QAAQ,IAAK;AAC/B,QAAI,QAAQ,KAAM,KAAI,IAAI;AAAA,QACrB,KAAI,KAAK,GAAG;AAAA,EACnB;AACA,SAAO,MAAM,IAAI,KAAK,GAAG;AAC3B;AAgBA,SAAS,sBAAsB,MAAc,YAA+C;AAC1F,QAAM,QAAQ,kBAAkB,IAAI;AACpC,MAAI,eAAe,QAAQ,eAAe,IAAK,QAAO,EAAE,OAAO,YAAY,MAAM,MAAM;AACvF,SAAO,EAAE,OAAO,YAAY,MAAM,kBAAkB,aAAa,KAAK,EAAE;AAC1E;AAQO,SAAS,kBACd,KACA,OAMI,CAAC,GACe;AACpB,MAAI,IAAI,WAAW,SAAS,GAAG;AAC7B,UAAM,OAAO,IAAI,MAAM,UAAU,MAAM;AAGvC,QAAI,CAAC,KAAK,WAAW,GAAG,EAAG,QAAO,EAAE,OAAO,UAAU;AAMrD,QAAI,KAAK,eAAgB,QAAO,sBAAsB,MAAM,KAAK,cAAc,IAAI;AACnF,WAAO,EAAE,OAAO,YAAY,MAAM,kBAAkB,IAAI,EAAE;AAAA,EAC5D;AACA,MAAI,IAAI,WAAW,GAAG,GAAG;AACvB,UAAM,aAAa,KAAK,cAAc;AACtC,QAAI,eAAe,MAAM;AAGvB,aAAO,sBAAsB,KAAK,UAAU;AAAA,IAC9C;AACA,WAAO,EAAE,OAAO,YAAY,MAAM,kBAAkB,GAAG,EAAE;AAAA,EAC3D;AAEA,MAAI,CAAC,KAAK,YAAa,QAAO,EAAE,OAAO,eAAe;AACtD,QAAM,MAAM,KAAK,YAAY,MAAM,GAAG,KAAK,YAAY,YAAY,GAAG,CAAC;AACvE,SAAO,EAAE,OAAO,YAAY,MAAM,kBAAkB,GAAG,GAAG,IAAI,GAAG,EAAE,EAAE;AACvE;","names":[]}
package/dist/llm.cjs CHANGED
@@ -20,8 +20,12 @@ var llm_exports = {};
20
20
  __export(llm_exports, {
21
21
  chat: () => chat,
22
22
  describeChat: () => describeChat,
23
+ describeChatState: () => describeChatState,
24
+ normalizeProviderInfo: () => normalizeProviderInfo,
23
25
  onChatProviderChange: () => onChatProviderChange,
24
- useChatProvider: () => useChatProvider
26
+ onChatProviderStateChange: () => onChatProviderStateChange,
27
+ useChatProvider: () => useChatProvider,
28
+ useChatProviderState: () => useChatProviderState
25
29
  });
26
30
  module.exports = __toCommonJS(llm_exports);
27
31
  var import_catalog = require("./catalog");
@@ -29,26 +33,40 @@ var import_pushChannel = require("./pushChannel");
29
33
  var import_protocol = require("./generated/protocol");
30
34
  function chat(req) {
31
35
  const { signal, ...params } = req;
32
- return (0, import_catalog.invokeStream)(
33
- "llm:chat",
34
- params,
35
- signal
36
- );
36
+ return (0, import_catalog.invokeStream)("llm:chat", params, signal);
37
37
  }
38
+ function normalizeProviderInfo(provider) {
39
+ if (!provider) return null;
40
+ const wire = provider.features;
41
+ return { ...provider, features: { ...wire, reasoning: wire.reasoning === true } };
42
+ }
43
+ let answered = false;
38
44
  const channel = (0, import_pushChannel.createPushChannel)({
39
45
  pushType: import_protocol.LLM_PROVIDER,
40
46
  requestType: import_protocol.REQUEST_LLM_PROVIDER,
41
47
  initial: null,
42
- parse: (msg) => "provider" in msg ? msg.provider : void 0
48
+ parse: (msg) => {
49
+ if (!("provider" in msg)) return void 0;
50
+ answered = true;
51
+ return normalizeProviderInfo(msg.provider ?? null);
52
+ }
43
53
  });
54
+ const stateOf = (provider) => !answered ? { status: "unknown" } : provider ? { status: "configured", provider } : { status: "not-configured" };
44
55
  const describeChat = () => channel.get();
56
+ const describeChatState = () => stateOf(channel.get());
45
57
  const onChatProviderChange = (listener) => channel.onChange(listener);
58
+ const onChatProviderStateChange = (listener) => channel.onChange((p) => listener(stateOf(p)));
46
59
  const useChatProvider = () => channel.use();
60
+ const useChatProviderState = () => stateOf(channel.use());
47
61
  // Annotate the CommonJS export names for ESM import in node:
48
62
  0 && (module.exports = {
49
63
  chat,
50
64
  describeChat,
65
+ describeChatState,
66
+ normalizeProviderInfo,
51
67
  onChatProviderChange,
52
- useChatProvider
68
+ onChatProviderStateChange,
69
+ useChatProvider,
70
+ useChatProviderState
53
71
  });
54
72
  //# sourceMappingURL=llm.cjs.map
package/dist/llm.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// Provider-agnostic LLM chat — the `llm.chat@1` slot (SERVICE_PROVIDERS_SPEC;\n// LLM_AND_AGENTS_SPEC §8 D5).\n//\n// An app calls ONE chat slot and never worries about which provider the user has a\n// key for: the HOST resolves which vendor answers from the key the user holds\n// (`SecretView.boundOrigin`) plus their `preferredImplementation` choice, normalizes\n// the wire format, injects the key host-side at the §6 net:fetch point (the\n// look-at-nothing proxy), and streams normalized deltas back. The app never names a\n// vendor, never sees the key, and needs NO `net:fetch`/`secrets` grant of its own —\n// only the `llm:chat` capability (elevated, app-scoped: a fork earns it by consent).\n//\n// Inert until the host implements `protocol-llm` (the `chat` stream) + the\n// `llm-provider` describe channel; the contract ships here so apps (the file-explorer\n// summarize fork) can be written against it — exactly how `secrets.ts` shipped ahead\n// of `protocol-secrets`.\nimport { invokeStream } from './catalog';\nimport { createPushChannel } from './pushChannel';\nimport { LLM_PROVIDER, REQUEST_LLM_PROVIDER } from './generated/protocol';\n\n/** Who authored a {@link ChatMessage}. */\nexport type ChatRole = 'system' | 'user' | 'assistant' | 'tool';\n\n/** A part of a message. `image` is only honored when the resolved provider\n * advertises `features.vision` (§2.5); `tool-use`/`tool-result` only when it\n * advertises `features.tools` — branch on {@link describeChat} first. */\nexport type ContentPart =\n | { type: 'text'; text: string }\n | { type: 'image'; mimeType: string; data: string } // data: base64, no data: URL prefix\n // A tool call the model emitted on a prior `assistant` turn — replay it in the\n // conversation so a follow-up request carries the agentic history. Pairs with the\n // streamed `tool-call` {@link ChatDelta} that first surfaced it.\n | { type: 'tool-use'; id: string; name: string; input: Record<string, unknown> }\n // The result of executing a `tool-use`, fed back so the model can continue. Carried\n // on a `user`/`tool`-role message; `toolCallId` matches the `tool-use` `id`.\n | { type: 'tool-result'; toolCallId: string; content: string; isError?: boolean };\n\n/** One message in a {@link ChatRequest}: a role plus its content parts. */\nexport interface ChatMessage {\n role: ChatRole;\n content: ContentPart[];\n}\n\n/** A tool the model may call — honored only when `features.tools`. */\nexport interface ToolDef {\n name: string;\n description?: string;\n /** JSON-Schema for the tool's arguments. */\n inputSchema: Record<string, unknown>;\n}\n\n/** A host-brokered chat completion request: the messages plus optional tools,\n * response format, and model hint (each honored per the provider's features). */\nexport interface ChatRequest {\n messages: ChatMessage[];\n /** Honored only when the resolved provider advertises `features.tools`. */\n tools?: ToolDef[];\n /** `'json'` honored only when `features.jsonMode`. Defaults to `'text'`. */\n responseFormat?: 'text' | 'json';\n maxTokens?: number;\n /** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete\n * model on the resolved provider. Omit to take the provider's default. */\n modelHint?: 'fast' | 'smart';\n /** Abort the completion mid-stream. When it fires, the SDK sends the host a cancel\n * frame so the host aborts the upstream provider request and STOPS BILLING the\n * user's key — not merely stops the app-side iterator (LLM_AND_AGENTS_SPEC §3.3\n * \"abort the in-flight LLM request\", R3-224). Not sent over the wire (an\n * `AbortSignal` isn't serializable); handled SDK-side. */\n signal?: AbortSignal;\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n | { type: 'usage'; inputTokens: number; outputTokens: number };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound the host fails the\n * stream into the SP-7 connect-me prompt (the user adds a key) — the generator\n * throws with `code: 'auth-required'`; an un-granted call throws `forbidden`.\n */\nexport function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void> {\n // Peel `signal` out of the request before it becomes wire params — an AbortSignal\n // can't cross the postMessage boundary as data; it drives the SDK-side cancel frame.\n const { signal, ...params } = req;\n return invokeStream<ChatDelta, ChatResult>(\n 'llm:chat',\n params as unknown as Record<string, unknown>,\n signal,\n );\n}\n\n/** The resolved provider's advertised abilities (SERVICE_PROVIDERS_SPEC §2.5) — read\n * to branch/degrade (offer image upload only when `vision`). */\nexport interface ChatFeatures {\n vision: boolean;\n tools: boolean;\n jsonMode: boolean;\n maxContextTokens: number;\n}\n\n/** Info about the provider the host resolved for this app. `null` when no provider\n * is bound (SP-7: prompt the user to add a key before calling {@link chat}). */\nexport interface ChatProviderInfo {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — never a vendor secret or model id. */\n providerId: string;\n /** True for Host-proxied providers (host-vouched, SP-9); false for app-level ones,\n * whose `features` are an untrusted claim. */\n hostVouched: boolean;\n features: ChatFeatures;\n}\n\n// The `llm-provider` describe channel (Recipe A): the host pushes the resolved\n// provider info on change and replays it on register-frame, gated by `llm:chat`.\n// A message with no `provider` key is ignored; an explicit `null` means \"no provider\n// bound\" (distinct from \"not yet answered\", which keeps the `initial` null).\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: LLM_PROVIDER,\n requestType: REQUEST_LLM_PROVIDER,\n initial: null,\n parse: (msg) =>\n 'provider' in msg ? (msg.provider as ChatProviderInfo | null) : undefined,\n});\n\n/** The provider the host resolved for this app (or `null` if none bound). Poll for a\n * one-off read; use {@link onChatProviderChange}/{@link useChatProvider} to react. */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** Subscribe to provider changes (key added/revoked, preference changed). Invoked\n * immediately with the current value, then on every change. Returns unsubscribe. */\nexport const onChatProviderChange = (\n listener: (provider: ChatProviderInfo | null) => void,\n): (() => void) => channel.onChange(listener);\n\n/** React hook returning the resolved chat provider (or `null`), re-rendering on\n * change — gate the summarize affordance on `provider !== null`. */\nexport const useChatProvider = (): ChatProviderInfo | null => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,qBAA6B;AAC7B,yBAAkC;AAClC,sBAAmD;AAiF5C,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,aAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AA0BA,MAAM,cAAU,sCAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QACN,cAAc,MAAO,IAAI,WAAuC;AACpE,CAAC;AAIM,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAIhE,MAAM,uBAAuB,CAClC,aACiB,QAAQ,SAAS,QAAQ;AAIrC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;","names":[]}
1
+ {"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// Provider-agnostic LLM chat — the `llm.chat@1` slot (SERVICE_PROVIDERS_SPEC;\n// LLM_AND_AGENTS_SPEC §8 D5).\n//\n// An app calls ONE chat slot and never worries about which provider the user has a\n// key for: the HOST resolves which vendor answers from the key the user holds\n// (`SecretView.boundOrigin`) plus their `preferredImplementation` choice, normalizes\n// the wire format, injects the key host-side at the §6 net:fetch point (the\n// look-at-nothing proxy), and streams normalized deltas back. The app never names a\n// vendor, never sees the key, and needs NO `net:fetch`/`secrets` grant of its own —\n// only the `llm:chat` capability (elevated, app-scoped: a fork earns it by consent).\n//\n// Inert until the host implements `protocol-llm` (the `chat` stream) + the\n// `llm-provider` describe channel; the contract ships here so apps (the file-explorer\n// summarize fork) can be written against it — exactly how `secrets.ts` shipped ahead\n// of `protocol-secrets`.\nimport { invokeStream } from './catalog';\nimport { createPushChannel } from './pushChannel';\nimport { LLM_PROVIDER, REQUEST_LLM_PROVIDER } from './generated/protocol';\n\n/** Who authored a {@link ChatMessage}. */\nexport type ChatRole = 'system' | 'user' | 'assistant' | 'tool';\n\n/** A part of a message. `image` is only honored when the resolved provider\n * advertises `features.vision` (§2.5); `tool-use`/`tool-result` only when it\n * advertises `features.tools` — branch on {@link describeChat} first. */\nexport type ContentPart =\n | { type: 'text'; text: string }\n | { type: 'image'; mimeType: string; data: string } // data: base64, no data: URL prefix\n // A tool call the model emitted on a prior `assistant` turn — replay it in the\n // conversation so a follow-up request carries the agentic history. Pairs with the\n // streamed `tool-call` {@link ChatDelta} that first surfaced it.\n | { type: 'tool-use'; id: string; name: string; input: Record<string, unknown> }\n // A block of the model's own REASONING from a prior `assistant` turn (R3-335).\n // Honored only when the resolved provider advertises `features.reasoning`.\n //\n // Echo these back. On some providers a reasoning block must be replayed — with its\n // `signature` intact and BEFORE the turn's text/tool-use — for the following turn to\n // be accepted at all; a loop that drops them is quietly lossy across turns in a way\n // that shows up as degraded output rather than an error. Pairs with the streamed\n // `reasoning` {@link ChatDelta}.\n | { type: 'reasoning'; text: string; signature?: string }\n // Reasoning the provider REDACTED: opaque bytes with no readable text, which still\n // have to be echoed back in place to keep the chain valid. Never render it.\n | { type: 'reasoning-redacted'; data: string }\n // The result of executing a `tool-use`, fed back so the model can continue. Carried\n // on a `user`/`tool`-role message; `toolCallId` matches the `tool-use` `id`.\n | { type: 'tool-result'; toolCallId: string; content: string; isError?: boolean };\n\n/** One message in a {@link ChatRequest}: a role plus its content parts. */\nexport interface ChatMessage {\n role: ChatRole;\n content: ContentPart[];\n}\n\n/** A tool the model may call — honored only when `features.tools`. */\nexport interface ToolDef {\n name: string;\n description?: string;\n /** JSON-Schema for the tool's arguments. */\n inputSchema: Record<string, unknown>;\n}\n\n/** A host-brokered chat completion request: the messages plus optional tools,\n * response format, and model hint (each honored per the provider's features). */\nexport interface ChatRequest {\n messages: ChatMessage[];\n /** Honored only when the resolved provider advertises `features.tools`. */\n tools?: ToolDef[];\n /** `'json'` honored only when `features.jsonMode`. Defaults to `'text'`. */\n responseFormat?: 'text' | 'json';\n maxTokens?: number;\n /** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete\n * model on the resolved provider. Omit to take the provider's default. */\n modelHint?: 'fast' | 'smart';\n /** Abort the completion mid-stream. When it fires, the SDK sends the host a cancel\n * frame so the host aborts the upstream provider request and STOPS BILLING the\n * user's key — not merely stops the app-side iterator (LLM_AND_AGENTS_SPEC §3.3\n * \"abort the in-flight LLM request\", R3-224). Not sent over the wire (an\n * `AbortSignal` isn't serializable); handled SDK-side. */\n signal?: AbortSignal;\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n // R3-335 — the model's reasoning as it streams. `reasoning-delta` carries the text\n // incrementally (render it live); the terminal `reasoning` carries the WHOLE block\n // plus the `signature` the provider may require on the echo, and is what a caller\n // should put back into the conversation. A provider without reasoning emits neither.\n | { type: 'reasoning-delta'; text: string }\n | { type: 'reasoning'; text: string; signature?: string }\n | { type: 'reasoning-redacted'; data: string }\n | { type: 'usage'; inputTokens: number; outputTokens: number };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound the host fails the\n * stream into the SP-7 connect-me prompt (the user adds a key) — the generator\n * throws with `code: 'auth-required'`; an un-granted call throws `forbidden`.\n */\nexport function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void> {\n // Peel `signal` out of the request before it becomes wire params — an AbortSignal\n // can't cross the postMessage boundary as data; it drives the SDK-side cancel frame.\n const { signal, ...params } = req;\n return invokeStream<ChatDelta, ChatResult>('llm:chat', params as unknown as Record<string, unknown>, signal);\n}\n\n/** The resolved provider's advertised abilities (SERVICE_PROVIDERS_SPEC §2.5) — read\n * to branch/degrade (offer image upload only when `vision`). */\nexport interface ChatFeatures {\n vision: boolean;\n tools: boolean;\n jsonMode: boolean;\n /** R3-335: the provider emits reasoning blocks. Read it to decide whether to render\n * a thinking surface at all — an empty affordance on a provider that never thinks\n * is worse than none. Normalized to `false` by the channel when a host predating\n * R3-335 omits it, so this is never `undefined` in practice. */\n reasoning: boolean;\n maxContextTokens: number;\n}\n\n/** Info about the provider the host resolved for this app. `null` when no provider\n * is bound (SP-7: prompt the user to add a key before calling {@link chat}). */\nexport interface ChatProviderInfo {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — never a vendor secret or model id. */\n providerId: string;\n /** True for Host-proxied providers (host-vouched, SP-9); false for app-level ones,\n * whose `features` are an untrusted claim. */\n hostVouched: boolean;\n features: ChatFeatures;\n // NOTE (R3-300): `displayName`, `executor` and the resolved per-tier `models` belong\n // here — an app rendering provider state wants all three. They are NOT added yet,\n // deliberately: this interface IS the `llm-provider` channel's declared value, so\n // adding a field is a WIRE change, and the wire is owned by\n // `@immediately-run/sandbox-protocol` (descriptor edit → publish → pin bump on both\n // sides). The protocol snapshot gate enforces exactly that, and it is right to. The\n // enrichment rides R3-307's publish, which already has to touch those descriptors —\n // one publish for two additions rather than two.\n}\n\n/**\n * Whether the host has told us about a provider yet, and if so whether one is bound.\n *\n * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both\n * when no provider is configured AND when the channel has not answered — so an app\n * cannot tell \"you need a key\" from \"ask again in a moment\", and consuming apps\n * rendered a misleading \"connect a key\" banner at users who had one. `unknown` is the\n * state before the host answers; it is not an error and not a prompt to act.\n */\nexport type ChatProviderState =\n | { status: 'unknown' }\n | { status: 'not-configured' }\n | { status: 'configured'; provider: ChatProviderInfo };\n\n// The `llm-provider` describe channel (Recipe A): the host pushes the resolved\n// provider info on change and replays it on register-frame, gated by `llm:chat`.\n// A message with no `provider` key is ignored; an explicit `null` means \"no provider\n// bound\", which is now REPRESENTABLE as distinct from \"not yet answered\".\n// The channel's VALUE stays exactly what the wire carries — `ChatProviderInfo | null` —\n// because the wire did not change here and the protocol snapshot gate reads this type as\n// the channel's shape. The three-state lives BESIDE it: `answered` records whether the host\n// has ever spoken on this channel, which is the one bit `null` cannot carry. Deriving the\n// state rather than widening the channel keeps the wire contract byte-identical, which it\n// is (SDK_PACKAGING_SPEC §9: the wire is additive-only, and this is not a wire change).\n/**\n * Fill in feature flags a host older than the field does not send (R3-335).\n *\n * `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it\n * omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in\n * features` check, which is exactly the kind of difference that produces one wrong\n * branch a year later — so it is normalized here, once, rather than left to every\n * caller. Absent means \"does not reason\": the fail-closed reading.\n *\n * Exported for its own test; not part of the public surface (`index.ts` re-exports\n * this module wholesale, so it is reachable — it is documented as internal rather\n * than hidden behind a lie).\n * @internal\n */\nexport function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null {\n if (!provider) return null;\n // The wire value is whatever the host sent, which may predate `reasoning` — so read\n // it as partial rather than trusting the declared type, and decide the flag explicitly.\n const wire = provider.features as Partial<ChatFeatures>;\n return { ...provider, features: { ...wire, reasoning: wire.reasoning === true } as ChatFeatures };\n}\n\nlet answered = false;\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: LLM_PROVIDER,\n requestType: REQUEST_LLM_PROVIDER,\n initial: null,\n parse: (msg) => {\n if (!('provider' in msg)) return undefined;\n answered = true;\n return normalizeProviderInfo((msg.provider as ChatProviderInfo | null) ?? null);\n },\n});\n\n/** Derive the three-state from the wire value plus whether the host has answered. */\nconst stateOf = (provider: ChatProviderInfo | null): ChatProviderState =>\n !answered ? { status: 'unknown' } : provider ? { status: 'configured', provider } : { status: 'not-configured' };\n\n/**\n * The provider the host resolved for this app, or `null`.\n *\n * Kept for compatibility (`ways_of_working §6`, additive-only): it collapses `unknown`\n * and `not-configured` to `null`. Prefer {@link describeChatState} when the difference\n * matters — which is any time you would render \"connect a key\", because doing that in\n * the `unknown` state is exactly the false banner R3-300 fixes.\n */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** The three-state read: `unknown` before the host answers, then configured or not. */\nexport const describeChatState = (): ChatProviderState => stateOf(channel.get());\n\n/** Subscribe to provider changes (key added/revoked, preference changed). Invoked\n * immediately with the current value, then on every change. Returns unsubscribe. */\nexport const onChatProviderChange = (listener: (provider: ChatProviderInfo | null) => void): (() => void) =>\n channel.onChange(listener);\n\n/** Subscribe to the three-state provider description. */\nexport const onChatProviderStateChange = (listener: (state: ChatProviderState) => void): (() => void) =>\n channel.onChange((p) => listener(stateOf(p)));\n\n/** React hook returning the resolved chat provider (or `null`), re-rendering on\n * change — gate the summarize affordance on `provider !== null`. */\nexport const useChatProvider = (): ChatProviderInfo | null => channel.use();\n\n/**\n * React hook returning the three-state description.\n *\n * Use this to render provider state honestly: show nothing (or a neutral placeholder)\n * while `unknown`, the connect affordance only on `not-configured`, and the provider's\n * name on `configured`.\n */\nexport const useChatProviderState = (): ChatProviderState => stateOf(channel.use());\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,qBAA6B;AAC7B,yBAAkC;AAClC,sBAAmD;AAoG5C,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,aAAO,6BAAoC,YAAY,QAA8C,MAAM;AAC7G;AAyEO,SAAS,sBAAsB,UAA4D;AAChG,MAAI,CAAC,SAAU,QAAO;AAGtB,QAAM,OAAO,SAAS;AACtB,SAAO,EAAE,GAAG,UAAU,UAAU,EAAE,GAAG,MAAM,WAAW,KAAK,cAAc,KAAK,EAAkB;AAClG;AAEA,IAAI,WAAW;AACf,MAAM,cAAU,sCAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AACd,QAAI,EAAE,cAAc,KAAM,QAAO;AACjC,eAAW;AACX,WAAO,sBAAuB,IAAI,YAAwC,IAAI;AAAA,EAChF;AACF,CAAC;AAGD,MAAM,UAAU,CAAC,aACf,CAAC,WAAW,EAAE,QAAQ,UAAU,IAAI,WAAW,EAAE,QAAQ,cAAc,SAAS,IAAI,EAAE,QAAQ,iBAAiB;AAU1G,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAGhE,MAAM,oBAAoB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;AAIxE,MAAM,uBAAuB,CAAC,aACnC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,4BAA4B,CAAC,aACxC,QAAQ,SAAS,CAAC,MAAM,SAAS,QAAQ,CAAC,CAAC,CAAC;AAIvC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;AASnE,MAAM,uBAAuB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;","names":[]}
package/dist/llm.d.cts CHANGED
@@ -15,6 +15,13 @@ type ContentPart = {
15
15
  id: string;
16
16
  name: string;
17
17
  input: Record<string, unknown>;
18
+ } | {
19
+ type: 'reasoning';
20
+ text: string;
21
+ signature?: string;
22
+ } | {
23
+ type: 'reasoning-redacted';
24
+ data: string;
18
25
  } | {
19
26
  type: 'tool-result';
20
27
  toolCallId: string;
@@ -61,6 +68,16 @@ type ChatDelta = {
61
68
  id: string;
62
69
  name: string;
63
70
  input: unknown;
71
+ } | {
72
+ type: 'reasoning-delta';
73
+ text: string;
74
+ } | {
75
+ type: 'reasoning';
76
+ text: string;
77
+ signature?: string;
78
+ } | {
79
+ type: 'reasoning-redacted';
80
+ data: string;
64
81
  } | {
65
82
  type: 'usage';
66
83
  inputTokens: number;
@@ -93,6 +110,11 @@ interface ChatFeatures {
93
110
  vision: boolean;
94
111
  tools: boolean;
95
112
  jsonMode: boolean;
113
+ /** R3-335: the provider emits reasoning blocks. Read it to decide whether to render
114
+ * a thinking surface at all — an empty affordance on a provider that never thinks
115
+ * is worse than none. Normalized to `false` by the channel when a host predating
116
+ * R3-335 omits it, so this is never `undefined` in practice. */
117
+ reasoning: boolean;
96
118
  maxContextTokens: number;
97
119
  }
98
120
  /** Info about the provider the host resolved for this app. `null` when no provider
@@ -105,14 +127,64 @@ interface ChatProviderInfo {
105
127
  hostVouched: boolean;
106
128
  features: ChatFeatures;
107
129
  }
108
- /** The provider the host resolved for this app (or `null` if none bound). Poll for a
109
- * one-off read; use {@link onChatProviderChange}/{@link useChatProvider} to react. */
130
+ /**
131
+ * Whether the host has told us about a provider yet, and if so whether one is bound.
132
+ *
133
+ * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both
134
+ * when no provider is configured AND when the channel has not answered — so an app
135
+ * cannot tell "you need a key" from "ask again in a moment", and consuming apps
136
+ * rendered a misleading "connect a key" banner at users who had one. `unknown` is the
137
+ * state before the host answers; it is not an error and not a prompt to act.
138
+ */
139
+ type ChatProviderState = {
140
+ status: 'unknown';
141
+ } | {
142
+ status: 'not-configured';
143
+ } | {
144
+ status: 'configured';
145
+ provider: ChatProviderInfo;
146
+ };
147
+ /**
148
+ * Fill in feature flags a host older than the field does not send (R3-335).
149
+ *
150
+ * `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it
151
+ * omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in
152
+ * features` check, which is exactly the kind of difference that produces one wrong
153
+ * branch a year later — so it is normalized here, once, rather than left to every
154
+ * caller. Absent means "does not reason": the fail-closed reading.
155
+ *
156
+ * Exported for its own test; not part of the public surface (`index.ts` re-exports
157
+ * this module wholesale, so it is reachable — it is documented as internal rather
158
+ * than hidden behind a lie).
159
+ * @internal
160
+ */
161
+ declare function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null;
162
+ /**
163
+ * The provider the host resolved for this app, or `null`.
164
+ *
165
+ * Kept for compatibility (`ways_of_working §6`, additive-only): it collapses `unknown`
166
+ * and `not-configured` to `null`. Prefer {@link describeChatState} when the difference
167
+ * matters — which is any time you would render "connect a key", because doing that in
168
+ * the `unknown` state is exactly the false banner R3-300 fixes.
169
+ */
110
170
  declare const describeChat: () => ChatProviderInfo | null;
171
+ /** The three-state read: `unknown` before the host answers, then configured or not. */
172
+ declare const describeChatState: () => ChatProviderState;
111
173
  /** Subscribe to provider changes (key added/revoked, preference changed). Invoked
112
174
  * immediately with the current value, then on every change. Returns unsubscribe. */
113
175
  declare const onChatProviderChange: (listener: (provider: ChatProviderInfo | null) => void) => (() => void);
176
+ /** Subscribe to the three-state provider description. */
177
+ declare const onChatProviderStateChange: (listener: (state: ChatProviderState) => void) => (() => void);
114
178
  /** React hook returning the resolved chat provider (or `null`), re-rendering on
115
179
  * change — gate the summarize affordance on `provider !== null`. */
116
180
  declare const useChatProvider: () => ChatProviderInfo | null;
181
+ /**
182
+ * React hook returning the three-state description.
183
+ *
184
+ * Use this to render provider state honestly: show nothing (or a neutral placeholder)
185
+ * while `unknown`, the connect affordance only on `not-configured`, and the provider's
186
+ * name on `configured`.
187
+ */
188
+ declare const useChatProviderState: () => ChatProviderState;
117
189
 
118
- export { type ChatDelta, type ChatFeatures, type ChatMessage, type ChatProviderInfo, type ChatRequest, type ChatResult, type ChatRole, type ChatStopReason, type ContentPart, type ToolDef, chat, describeChat, onChatProviderChange, useChatProvider };
190
+ export { type ChatDelta, type ChatFeatures, type ChatMessage, type ChatProviderInfo, type ChatProviderState, type ChatRequest, type ChatResult, type ChatRole, type ChatStopReason, type ContentPart, type ToolDef, chat, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState };
package/dist/llm.d.ts CHANGED
@@ -15,6 +15,13 @@ type ContentPart = {
15
15
  id: string;
16
16
  name: string;
17
17
  input: Record<string, unknown>;
18
+ } | {
19
+ type: 'reasoning';
20
+ text: string;
21
+ signature?: string;
22
+ } | {
23
+ type: 'reasoning-redacted';
24
+ data: string;
18
25
  } | {
19
26
  type: 'tool-result';
20
27
  toolCallId: string;
@@ -61,6 +68,16 @@ type ChatDelta = {
61
68
  id: string;
62
69
  name: string;
63
70
  input: unknown;
71
+ } | {
72
+ type: 'reasoning-delta';
73
+ text: string;
74
+ } | {
75
+ type: 'reasoning';
76
+ text: string;
77
+ signature?: string;
78
+ } | {
79
+ type: 'reasoning-redacted';
80
+ data: string;
64
81
  } | {
65
82
  type: 'usage';
66
83
  inputTokens: number;
@@ -93,6 +110,11 @@ interface ChatFeatures {
93
110
  vision: boolean;
94
111
  tools: boolean;
95
112
  jsonMode: boolean;
113
+ /** R3-335: the provider emits reasoning blocks. Read it to decide whether to render
114
+ * a thinking surface at all — an empty affordance on a provider that never thinks
115
+ * is worse than none. Normalized to `false` by the channel when a host predating
116
+ * R3-335 omits it, so this is never `undefined` in practice. */
117
+ reasoning: boolean;
96
118
  maxContextTokens: number;
97
119
  }
98
120
  /** Info about the provider the host resolved for this app. `null` when no provider
@@ -105,14 +127,64 @@ interface ChatProviderInfo {
105
127
  hostVouched: boolean;
106
128
  features: ChatFeatures;
107
129
  }
108
- /** The provider the host resolved for this app (or `null` if none bound). Poll for a
109
- * one-off read; use {@link onChatProviderChange}/{@link useChatProvider} to react. */
130
+ /**
131
+ * Whether the host has told us about a provider yet, and if so whether one is bound.
132
+ *
133
+ * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both
134
+ * when no provider is configured AND when the channel has not answered — so an app
135
+ * cannot tell "you need a key" from "ask again in a moment", and consuming apps
136
+ * rendered a misleading "connect a key" banner at users who had one. `unknown` is the
137
+ * state before the host answers; it is not an error and not a prompt to act.
138
+ */
139
+ type ChatProviderState = {
140
+ status: 'unknown';
141
+ } | {
142
+ status: 'not-configured';
143
+ } | {
144
+ status: 'configured';
145
+ provider: ChatProviderInfo;
146
+ };
147
+ /**
148
+ * Fill in feature flags a host older than the field does not send (R3-335).
149
+ *
150
+ * `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it
151
+ * omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in
152
+ * features` check, which is exactly the kind of difference that produces one wrong
153
+ * branch a year later — so it is normalized here, once, rather than left to every
154
+ * caller. Absent means "does not reason": the fail-closed reading.
155
+ *
156
+ * Exported for its own test; not part of the public surface (`index.ts` re-exports
157
+ * this module wholesale, so it is reachable — it is documented as internal rather
158
+ * than hidden behind a lie).
159
+ * @internal
160
+ */
161
+ declare function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null;
162
+ /**
163
+ * The provider the host resolved for this app, or `null`.
164
+ *
165
+ * Kept for compatibility (`ways_of_working §6`, additive-only): it collapses `unknown`
166
+ * and `not-configured` to `null`. Prefer {@link describeChatState} when the difference
167
+ * matters — which is any time you would render "connect a key", because doing that in
168
+ * the `unknown` state is exactly the false banner R3-300 fixes.
169
+ */
110
170
  declare const describeChat: () => ChatProviderInfo | null;
171
+ /** The three-state read: `unknown` before the host answers, then configured or not. */
172
+ declare const describeChatState: () => ChatProviderState;
111
173
  /** Subscribe to provider changes (key added/revoked, preference changed). Invoked
112
174
  * immediately with the current value, then on every change. Returns unsubscribe. */
113
175
  declare const onChatProviderChange: (listener: (provider: ChatProviderInfo | null) => void) => (() => void);
176
+ /** Subscribe to the three-state provider description. */
177
+ declare const onChatProviderStateChange: (listener: (state: ChatProviderState) => void) => (() => void);
114
178
  /** React hook returning the resolved chat provider (or `null`), re-rendering on
115
179
  * change — gate the summarize affordance on `provider !== null`. */
116
180
  declare const useChatProvider: () => ChatProviderInfo | null;
181
+ /**
182
+ * React hook returning the three-state description.
183
+ *
184
+ * Use this to render provider state honestly: show nothing (or a neutral placeholder)
185
+ * while `unknown`, the connect affordance only on `not-configured`, and the provider's
186
+ * name on `configured`.
187
+ */
188
+ declare const useChatProviderState: () => ChatProviderState;
117
189
 
118
- export { type ChatDelta, type ChatFeatures, type ChatMessage, type ChatProviderInfo, type ChatRequest, type ChatResult, type ChatRole, type ChatStopReason, type ContentPart, type ToolDef, chat, describeChat, onChatProviderChange, useChatProvider };
190
+ export { type ChatDelta, type ChatFeatures, type ChatMessage, type ChatProviderInfo, type ChatProviderState, type ChatRequest, type ChatResult, type ChatRole, type ChatStopReason, type ContentPart, type ToolDef, chat, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState };
package/dist/llm.js CHANGED
@@ -4,25 +4,39 @@ import { createPushChannel } from "./pushChannel";
4
4
  import { LLM_PROVIDER, REQUEST_LLM_PROVIDER } from "./generated/protocol";
5
5
  function chat(req) {
6
6
  const { signal, ...params } = req;
7
- return invokeStream(
8
- "llm:chat",
9
- params,
10
- signal
11
- );
7
+ return invokeStream("llm:chat", params, signal);
12
8
  }
9
+ function normalizeProviderInfo(provider) {
10
+ if (!provider) return null;
11
+ const wire = provider.features;
12
+ return { ...provider, features: { ...wire, reasoning: wire.reasoning === true } };
13
+ }
14
+ let answered = false;
13
15
  const channel = createPushChannel({
14
16
  pushType: LLM_PROVIDER,
15
17
  requestType: REQUEST_LLM_PROVIDER,
16
18
  initial: null,
17
- parse: (msg) => "provider" in msg ? msg.provider : void 0
19
+ parse: (msg) => {
20
+ if (!("provider" in msg)) return void 0;
21
+ answered = true;
22
+ return normalizeProviderInfo(msg.provider ?? null);
23
+ }
18
24
  });
25
+ const stateOf = (provider) => !answered ? { status: "unknown" } : provider ? { status: "configured", provider } : { status: "not-configured" };
19
26
  const describeChat = () => channel.get();
27
+ const describeChatState = () => stateOf(channel.get());
20
28
  const onChatProviderChange = (listener) => channel.onChange(listener);
29
+ const onChatProviderStateChange = (listener) => channel.onChange((p) => listener(stateOf(p)));
21
30
  const useChatProvider = () => channel.use();
31
+ const useChatProviderState = () => stateOf(channel.use());
22
32
  export {
23
33
  chat,
24
34
  describeChat,
35
+ describeChatState,
36
+ normalizeProviderInfo,
25
37
  onChatProviderChange,
26
- useChatProvider
38
+ onChatProviderStateChange,
39
+ useChatProvider,
40
+ useChatProviderState
27
41
  };
28
42
  //# sourceMappingURL=llm.js.map
package/dist/llm.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// Provider-agnostic LLM chat — the `llm.chat@1` slot (SERVICE_PROVIDERS_SPEC;\n// LLM_AND_AGENTS_SPEC §8 D5).\n//\n// An app calls ONE chat slot and never worries about which provider the user has a\n// key for: the HOST resolves which vendor answers from the key the user holds\n// (`SecretView.boundOrigin`) plus their `preferredImplementation` choice, normalizes\n// the wire format, injects the key host-side at the §6 net:fetch point (the\n// look-at-nothing proxy), and streams normalized deltas back. The app never names a\n// vendor, never sees the key, and needs NO `net:fetch`/`secrets` grant of its own —\n// only the `llm:chat` capability (elevated, app-scoped: a fork earns it by consent).\n//\n// Inert until the host implements `protocol-llm` (the `chat` stream) + the\n// `llm-provider` describe channel; the contract ships here so apps (the file-explorer\n// summarize fork) can be written against it — exactly how `secrets.ts` shipped ahead\n// of `protocol-secrets`.\nimport { invokeStream } from './catalog';\nimport { createPushChannel } from './pushChannel';\nimport { LLM_PROVIDER, REQUEST_LLM_PROVIDER } from './generated/protocol';\n\n/** Who authored a {@link ChatMessage}. */\nexport type ChatRole = 'system' | 'user' | 'assistant' | 'tool';\n\n/** A part of a message. `image` is only honored when the resolved provider\n * advertises `features.vision` (§2.5); `tool-use`/`tool-result` only when it\n * advertises `features.tools` — branch on {@link describeChat} first. */\nexport type ContentPart =\n | { type: 'text'; text: string }\n | { type: 'image'; mimeType: string; data: string } // data: base64, no data: URL prefix\n // A tool call the model emitted on a prior `assistant` turn — replay it in the\n // conversation so a follow-up request carries the agentic history. Pairs with the\n // streamed `tool-call` {@link ChatDelta} that first surfaced it.\n | { type: 'tool-use'; id: string; name: string; input: Record<string, unknown> }\n // The result of executing a `tool-use`, fed back so the model can continue. Carried\n // on a `user`/`tool`-role message; `toolCallId` matches the `tool-use` `id`.\n | { type: 'tool-result'; toolCallId: string; content: string; isError?: boolean };\n\n/** One message in a {@link ChatRequest}: a role plus its content parts. */\nexport interface ChatMessage {\n role: ChatRole;\n content: ContentPart[];\n}\n\n/** A tool the model may call — honored only when `features.tools`. */\nexport interface ToolDef {\n name: string;\n description?: string;\n /** JSON-Schema for the tool's arguments. */\n inputSchema: Record<string, unknown>;\n}\n\n/** A host-brokered chat completion request: the messages plus optional tools,\n * response format, and model hint (each honored per the provider's features). */\nexport interface ChatRequest {\n messages: ChatMessage[];\n /** Honored only when the resolved provider advertises `features.tools`. */\n tools?: ToolDef[];\n /** `'json'` honored only when `features.jsonMode`. Defaults to `'text'`. */\n responseFormat?: 'text' | 'json';\n maxTokens?: number;\n /** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete\n * model on the resolved provider. Omit to take the provider's default. */\n modelHint?: 'fast' | 'smart';\n /** Abort the completion mid-stream. When it fires, the SDK sends the host a cancel\n * frame so the host aborts the upstream provider request and STOPS BILLING the\n * user's key — not merely stops the app-side iterator (LLM_AND_AGENTS_SPEC §3.3\n * \"abort the in-flight LLM request\", R3-224). Not sent over the wire (an\n * `AbortSignal` isn't serializable); handled SDK-side. */\n signal?: AbortSignal;\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n | { type: 'usage'; inputTokens: number; outputTokens: number };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound the host fails the\n * stream into the SP-7 connect-me prompt (the user adds a key) — the generator\n * throws with `code: 'auth-required'`; an un-granted call throws `forbidden`.\n */\nexport function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void> {\n // Peel `signal` out of the request before it becomes wire params — an AbortSignal\n // can't cross the postMessage boundary as data; it drives the SDK-side cancel frame.\n const { signal, ...params } = req;\n return invokeStream<ChatDelta, ChatResult>(\n 'llm:chat',\n params as unknown as Record<string, unknown>,\n signal,\n );\n}\n\n/** The resolved provider's advertised abilities (SERVICE_PROVIDERS_SPEC §2.5) — read\n * to branch/degrade (offer image upload only when `vision`). */\nexport interface ChatFeatures {\n vision: boolean;\n tools: boolean;\n jsonMode: boolean;\n maxContextTokens: number;\n}\n\n/** Info about the provider the host resolved for this app. `null` when no provider\n * is bound (SP-7: prompt the user to add a key before calling {@link chat}). */\nexport interface ChatProviderInfo {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — never a vendor secret or model id. */\n providerId: string;\n /** True for Host-proxied providers (host-vouched, SP-9); false for app-level ones,\n * whose `features` are an untrusted claim. */\n hostVouched: boolean;\n features: ChatFeatures;\n}\n\n// The `llm-provider` describe channel (Recipe A): the host pushes the resolved\n// provider info on change and replays it on register-frame, gated by `llm:chat`.\n// A message with no `provider` key is ignored; an explicit `null` means \"no provider\n// bound\" (distinct from \"not yet answered\", which keeps the `initial` null).\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: LLM_PROVIDER,\n requestType: REQUEST_LLM_PROVIDER,\n initial: null,\n parse: (msg) =>\n 'provider' in msg ? (msg.provider as ChatProviderInfo | null) : undefined,\n});\n\n/** The provider the host resolved for this app (or `null` if none bound). Poll for a\n * one-off read; use {@link onChatProviderChange}/{@link useChatProvider} to react. */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** Subscribe to provider changes (key added/revoked, preference changed). Invoked\n * immediately with the current value, then on every change. Returns unsubscribe. */\nexport const onChatProviderChange = (\n listener: (provider: ChatProviderInfo | null) => void,\n): (() => void) => channel.onChange(listener);\n\n/** React hook returning the resolved chat provider (or `null`), re-rendering on\n * change — gate the summarize affordance on `provider !== null`. */\nexport const useChatProvider = (): ChatProviderInfo | null => channel.use();\n"],"mappings":";AAeA,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,cAAc,4BAA4B;AAiF5C,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AA0BA,MAAM,UAAU,kBAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QACN,cAAc,MAAO,IAAI,WAAuC;AACpE,CAAC;AAIM,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAIhE,MAAM,uBAAuB,CAClC,aACiB,QAAQ,SAAS,QAAQ;AAIrC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;","names":[]}
1
+ {"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// Provider-agnostic LLM chat — the `llm.chat@1` slot (SERVICE_PROVIDERS_SPEC;\n// LLM_AND_AGENTS_SPEC §8 D5).\n//\n// An app calls ONE chat slot and never worries about which provider the user has a\n// key for: the HOST resolves which vendor answers from the key the user holds\n// (`SecretView.boundOrigin`) plus their `preferredImplementation` choice, normalizes\n// the wire format, injects the key host-side at the §6 net:fetch point (the\n// look-at-nothing proxy), and streams normalized deltas back. The app never names a\n// vendor, never sees the key, and needs NO `net:fetch`/`secrets` grant of its own —\n// only the `llm:chat` capability (elevated, app-scoped: a fork earns it by consent).\n//\n// Inert until the host implements `protocol-llm` (the `chat` stream) + the\n// `llm-provider` describe channel; the contract ships here so apps (the file-explorer\n// summarize fork) can be written against it — exactly how `secrets.ts` shipped ahead\n// of `protocol-secrets`.\nimport { invokeStream } from './catalog';\nimport { createPushChannel } from './pushChannel';\nimport { LLM_PROVIDER, REQUEST_LLM_PROVIDER } from './generated/protocol';\n\n/** Who authored a {@link ChatMessage}. */\nexport type ChatRole = 'system' | 'user' | 'assistant' | 'tool';\n\n/** A part of a message. `image` is only honored when the resolved provider\n * advertises `features.vision` (§2.5); `tool-use`/`tool-result` only when it\n * advertises `features.tools` — branch on {@link describeChat} first. */\nexport type ContentPart =\n | { type: 'text'; text: string }\n | { type: 'image'; mimeType: string; data: string } // data: base64, no data: URL prefix\n // A tool call the model emitted on a prior `assistant` turn — replay it in the\n // conversation so a follow-up request carries the agentic history. Pairs with the\n // streamed `tool-call` {@link ChatDelta} that first surfaced it.\n | { type: 'tool-use'; id: string; name: string; input: Record<string, unknown> }\n // A block of the model's own REASONING from a prior `assistant` turn (R3-335).\n // Honored only when the resolved provider advertises `features.reasoning`.\n //\n // Echo these back. On some providers a reasoning block must be replayed — with its\n // `signature` intact and BEFORE the turn's text/tool-use — for the following turn to\n // be accepted at all; a loop that drops them is quietly lossy across turns in a way\n // that shows up as degraded output rather than an error. Pairs with the streamed\n // `reasoning` {@link ChatDelta}.\n | { type: 'reasoning'; text: string; signature?: string }\n // Reasoning the provider REDACTED: opaque bytes with no readable text, which still\n // have to be echoed back in place to keep the chain valid. Never render it.\n | { type: 'reasoning-redacted'; data: string }\n // The result of executing a `tool-use`, fed back so the model can continue. Carried\n // on a `user`/`tool`-role message; `toolCallId` matches the `tool-use` `id`.\n | { type: 'tool-result'; toolCallId: string; content: string; isError?: boolean };\n\n/** One message in a {@link ChatRequest}: a role plus its content parts. */\nexport interface ChatMessage {\n role: ChatRole;\n content: ContentPart[];\n}\n\n/** A tool the model may call — honored only when `features.tools`. */\nexport interface ToolDef {\n name: string;\n description?: string;\n /** JSON-Schema for the tool's arguments. */\n inputSchema: Record<string, unknown>;\n}\n\n/** A host-brokered chat completion request: the messages plus optional tools,\n * response format, and model hint (each honored per the provider's features). */\nexport interface ChatRequest {\n messages: ChatMessage[];\n /** Honored only when the resolved provider advertises `features.tools`. */\n tools?: ToolDef[];\n /** `'json'` honored only when `features.jsonMode`. Defaults to `'text'`. */\n responseFormat?: 'text' | 'json';\n maxTokens?: number;\n /** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete\n * model on the resolved provider. Omit to take the provider's default. */\n modelHint?: 'fast' | 'smart';\n /** Abort the completion mid-stream. When it fires, the SDK sends the host a cancel\n * frame so the host aborts the upstream provider request and STOPS BILLING the\n * user's key — not merely stops the app-side iterator (LLM_AND_AGENTS_SPEC §3.3\n * \"abort the in-flight LLM request\", R3-224). Not sent over the wire (an\n * `AbortSignal` isn't serializable); handled SDK-side. */\n signal?: AbortSignal;\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n // R3-335 — the model's reasoning as it streams. `reasoning-delta` carries the text\n // incrementally (render it live); the terminal `reasoning` carries the WHOLE block\n // plus the `signature` the provider may require on the echo, and is what a caller\n // should put back into the conversation. A provider without reasoning emits neither.\n | { type: 'reasoning-delta'; text: string }\n | { type: 'reasoning'; text: string; signature?: string }\n | { type: 'reasoning-redacted'; data: string }\n | { type: 'usage'; inputTokens: number; outputTokens: number };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound the host fails the\n * stream into the SP-7 connect-me prompt (the user adds a key) — the generator\n * throws with `code: 'auth-required'`; an un-granted call throws `forbidden`.\n */\nexport function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void> {\n // Peel `signal` out of the request before it becomes wire params — an AbortSignal\n // can't cross the postMessage boundary as data; it drives the SDK-side cancel frame.\n const { signal, ...params } = req;\n return invokeStream<ChatDelta, ChatResult>('llm:chat', params as unknown as Record<string, unknown>, signal);\n}\n\n/** The resolved provider's advertised abilities (SERVICE_PROVIDERS_SPEC §2.5) — read\n * to branch/degrade (offer image upload only when `vision`). */\nexport interface ChatFeatures {\n vision: boolean;\n tools: boolean;\n jsonMode: boolean;\n /** R3-335: the provider emits reasoning blocks. Read it to decide whether to render\n * a thinking surface at all — an empty affordance on a provider that never thinks\n * is worse than none. Normalized to `false` by the channel when a host predating\n * R3-335 omits it, so this is never `undefined` in practice. */\n reasoning: boolean;\n maxContextTokens: number;\n}\n\n/** Info about the provider the host resolved for this app. `null` when no provider\n * is bound (SP-7: prompt the user to add a key before calling {@link chat}). */\nexport interface ChatProviderInfo {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — never a vendor secret or model id. */\n providerId: string;\n /** True for Host-proxied providers (host-vouched, SP-9); false for app-level ones,\n * whose `features` are an untrusted claim. */\n hostVouched: boolean;\n features: ChatFeatures;\n // NOTE (R3-300): `displayName`, `executor` and the resolved per-tier `models` belong\n // here — an app rendering provider state wants all three. They are NOT added yet,\n // deliberately: this interface IS the `llm-provider` channel's declared value, so\n // adding a field is a WIRE change, and the wire is owned by\n // `@immediately-run/sandbox-protocol` (descriptor edit → publish → pin bump on both\n // sides). The protocol snapshot gate enforces exactly that, and it is right to. The\n // enrichment rides R3-307's publish, which already has to touch those descriptors —\n // one publish for two additions rather than two.\n}\n\n/**\n * Whether the host has told us about a provider yet, and if so whether one is bound.\n *\n * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both\n * when no provider is configured AND when the channel has not answered — so an app\n * cannot tell \"you need a key\" from \"ask again in a moment\", and consuming apps\n * rendered a misleading \"connect a key\" banner at users who had one. `unknown` is the\n * state before the host answers; it is not an error and not a prompt to act.\n */\nexport type ChatProviderState =\n | { status: 'unknown' }\n | { status: 'not-configured' }\n | { status: 'configured'; provider: ChatProviderInfo };\n\n// The `llm-provider` describe channel (Recipe A): the host pushes the resolved\n// provider info on change and replays it on register-frame, gated by `llm:chat`.\n// A message with no `provider` key is ignored; an explicit `null` means \"no provider\n// bound\", which is now REPRESENTABLE as distinct from \"not yet answered\".\n// The channel's VALUE stays exactly what the wire carries — `ChatProviderInfo | null` —\n// because the wire did not change here and the protocol snapshot gate reads this type as\n// the channel's shape. The three-state lives BESIDE it: `answered` records whether the host\n// has ever spoken on this channel, which is the one bit `null` cannot carry. Deriving the\n// state rather than widening the channel keeps the wire contract byte-identical, which it\n// is (SDK_PACKAGING_SPEC §9: the wire is additive-only, and this is not a wire change).\n/**\n * Fill in feature flags a host older than the field does not send (R3-335).\n *\n * `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it\n * omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in\n * features` check, which is exactly the kind of difference that produces one wrong\n * branch a year later — so it is normalized here, once, rather than left to every\n * caller. Absent means \"does not reason\": the fail-closed reading.\n *\n * Exported for its own test; not part of the public surface (`index.ts` re-exports\n * this module wholesale, so it is reachable — it is documented as internal rather\n * than hidden behind a lie).\n * @internal\n */\nexport function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null {\n if (!provider) return null;\n // The wire value is whatever the host sent, which may predate `reasoning` — so read\n // it as partial rather than trusting the declared type, and decide the flag explicitly.\n const wire = provider.features as Partial<ChatFeatures>;\n return { ...provider, features: { ...wire, reasoning: wire.reasoning === true } as ChatFeatures };\n}\n\nlet answered = false;\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: LLM_PROVIDER,\n requestType: REQUEST_LLM_PROVIDER,\n initial: null,\n parse: (msg) => {\n if (!('provider' in msg)) return undefined;\n answered = true;\n return normalizeProviderInfo((msg.provider as ChatProviderInfo | null) ?? null);\n },\n});\n\n/** Derive the three-state from the wire value plus whether the host has answered. */\nconst stateOf = (provider: ChatProviderInfo | null): ChatProviderState =>\n !answered ? { status: 'unknown' } : provider ? { status: 'configured', provider } : { status: 'not-configured' };\n\n/**\n * The provider the host resolved for this app, or `null`.\n *\n * Kept for compatibility (`ways_of_working §6`, additive-only): it collapses `unknown`\n * and `not-configured` to `null`. Prefer {@link describeChatState} when the difference\n * matters — which is any time you would render \"connect a key\", because doing that in\n * the `unknown` state is exactly the false banner R3-300 fixes.\n */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** The three-state read: `unknown` before the host answers, then configured or not. */\nexport const describeChatState = (): ChatProviderState => stateOf(channel.get());\n\n/** Subscribe to provider changes (key added/revoked, preference changed). Invoked\n * immediately with the current value, then on every change. Returns unsubscribe. */\nexport const onChatProviderChange = (listener: (provider: ChatProviderInfo | null) => void): (() => void) =>\n channel.onChange(listener);\n\n/** Subscribe to the three-state provider description. */\nexport const onChatProviderStateChange = (listener: (state: ChatProviderState) => void): (() => void) =>\n channel.onChange((p) => listener(stateOf(p)));\n\n/** React hook returning the resolved chat provider (or `null`), re-rendering on\n * change — gate the summarize affordance on `provider !== null`. */\nexport const useChatProvider = (): ChatProviderInfo | null => channel.use();\n\n/**\n * React hook returning the three-state description.\n *\n * Use this to render provider state honestly: show nothing (or a neutral placeholder)\n * while `unknown`, the connect affordance only on `not-configured`, and the provider's\n * name on `configured`.\n */\nexport const useChatProviderState = (): ChatProviderState => stateOf(channel.use());\n"],"mappings":";AAeA,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,cAAc,4BAA4B;AAoG5C,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,SAAO,aAAoC,YAAY,QAA8C,MAAM;AAC7G;AAyEO,SAAS,sBAAsB,UAA4D;AAChG,MAAI,CAAC,SAAU,QAAO;AAGtB,QAAM,OAAO,SAAS;AACtB,SAAO,EAAE,GAAG,UAAU,UAAU,EAAE,GAAG,MAAM,WAAW,KAAK,cAAc,KAAK,EAAkB;AAClG;AAEA,IAAI,WAAW;AACf,MAAM,UAAU,kBAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AACd,QAAI,EAAE,cAAc,KAAM,QAAO;AACjC,eAAW;AACX,WAAO,sBAAuB,IAAI,YAAwC,IAAI;AAAA,EAChF;AACF,CAAC;AAGD,MAAM,UAAU,CAAC,aACf,CAAC,WAAW,EAAE,QAAQ,UAAU,IAAI,WAAW,EAAE,QAAQ,cAAc,SAAS,IAAI,EAAE,QAAQ,iBAAiB;AAU1G,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAGhE,MAAM,oBAAoB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;AAIxE,MAAM,uBAAuB,CAAC,aACnC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,4BAA4B,CAAC,aACxC,QAAQ,SAAS,CAAC,MAAM,SAAS,QAAQ,CAAC,CAAC,CAAC;AAIvC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;AASnE,MAAM,uBAAuB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;","names":[]}