@spendgraph/tools 0.6.0 → 0.8.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 (211) hide show
  1. package/README.md +3 -2
  2. package/dist/builtin/calculate/calculate.d.ts +2 -0
  3. package/dist/builtin/calculate/calculate.js +1 -1
  4. package/dist/builtin/calculate/index.d.ts +2 -1
  5. package/dist/builtin/calculate/index.js +1 -1
  6. package/dist/builtin/code-exec/code-exec.d.ts +14 -0
  7. package/dist/builtin/date/date.d.ts +1 -1
  8. package/dist/builtin/date/date.js +1 -1
  9. package/dist/builtin/date/index.d.ts +1 -0
  10. package/dist/builtin/date/index.js +1 -1
  11. package/dist/builtin/date/shift.d.ts +1 -1
  12. package/dist/builtin/date/shift.js +1 -1
  13. package/dist/builtin/date/words.d.ts +13 -0
  14. package/dist/builtin/date/words.js +1 -0
  15. package/dist/builtin/decline/decline.d.ts +49 -0
  16. package/dist/builtin/decline/decline.js +1 -0
  17. package/dist/builtin/decline/index.d.ts +1 -0
  18. package/dist/builtin/decline/index.js +1 -0
  19. package/dist/builtin/escalate/escalate.d.ts +19 -0
  20. package/dist/builtin/escalate/escalate.js +1 -1
  21. package/dist/builtin/escalate/index.d.ts +1 -1
  22. package/dist/builtin/escalate/index.js +1 -1
  23. package/dist/builtin/extract/extract.d.ts +25 -0
  24. package/dist/builtin/extract/extract.js +1 -0
  25. package/dist/builtin/extract/index.d.ts +3 -0
  26. package/dist/builtin/extract/index.js +1 -0
  27. package/dist/builtin/extract/kinds.d.ts +6 -0
  28. package/dist/builtin/extract/kinds.js +1 -0
  29. package/dist/builtin/extract/readers.d.ts +7 -0
  30. package/dist/builtin/extract/readers.js +9 -0
  31. package/dist/builtin/finish/finish.d.ts +4 -0
  32. package/dist/builtin/finish/finish.js +1 -1
  33. package/dist/builtin/http/allow.d.ts +2 -0
  34. package/dist/builtin/http/allow.js +1 -1
  35. package/dist/builtin/http/hops.d.ts +10 -0
  36. package/dist/builtin/http/hops.js +1 -0
  37. package/dist/builtin/http/http.d.ts +9 -1
  38. package/dist/builtin/http/http.js +1 -1
  39. package/dist/builtin/http/index.d.ts +2 -0
  40. package/dist/builtin/http/index.js +1 -1
  41. package/dist/builtin/http/retry-after.d.ts +14 -0
  42. package/dist/builtin/http/retry-after.js +1 -0
  43. package/dist/builtin/index.d.ts +11 -6
  44. package/dist/builtin/index.js +1 -1
  45. package/dist/builtin/json/index.d.ts +1 -0
  46. package/dist/builtin/json/index.js +1 -1
  47. package/dist/builtin/json/json.js +1 -1
  48. package/dist/builtin/json/parse.d.ts +15 -0
  49. package/dist/builtin/json/parse.js +1 -0
  50. package/dist/builtin/notes/index.d.ts +1 -0
  51. package/dist/builtin/notes/index.js +1 -0
  52. package/dist/builtin/notes/notes.d.ts +45 -0
  53. package/dist/builtin/notes/notes.js +2 -0
  54. package/dist/builtin/openalex/api.d.ts +26 -0
  55. package/dist/builtin/openalex/api.js +1 -0
  56. package/dist/builtin/openalex/index.d.ts +3 -0
  57. package/dist/builtin/openalex/index.js +1 -0
  58. package/dist/builtin/openalex/openalex.d.ts +36 -0
  59. package/dist/builtin/openalex/openalex.js +1 -0
  60. package/dist/builtin/paper-search/index.d.ts +1 -0
  61. package/dist/builtin/paper-search/index.js +1 -0
  62. package/dist/builtin/paper-search/paper-search.d.ts +38 -0
  63. package/dist/builtin/paper-search/paper-search.js +3 -0
  64. package/dist/builtin/polite.d.ts +15 -0
  65. package/dist/builtin/polite.js +1 -0
  66. package/dist/builtin/publish/index.d.ts +1 -0
  67. package/dist/builtin/publish/index.js +1 -0
  68. package/dist/builtin/publish/publish.d.ts +46 -0
  69. package/dist/builtin/publish/publish.js +1 -0
  70. package/dist/builtin/web-search/web-search.d.ts +10 -35
  71. package/dist/builtin/web-search/web-search.js +1 -1
  72. package/dist/builtin/wikidata/api.d.ts +40 -0
  73. package/dist/builtin/wikidata/api.js +1 -0
  74. package/dist/builtin/wikidata/index.d.ts +5 -0
  75. package/dist/builtin/wikidata/index.js +1 -0
  76. package/dist/builtin/wikidata/render.d.ts +13 -0
  77. package/dist/builtin/wikidata/render.js +2 -0
  78. package/dist/builtin/wikidata/wikidata.d.ts +23 -0
  79. package/dist/builtin/wikidata/wikidata.js +1 -0
  80. package/dist/bus/bus.d.ts +23 -1
  81. package/dist/bus/bus.js +1 -1
  82. package/dist/bus/group.d.ts +14 -1
  83. package/dist/bus/group.js +1 -1
  84. package/dist/bus/index.d.ts +4 -1
  85. package/dist/bus/index.js +1 -1
  86. package/dist/bus/invoke.d.ts +1 -1
  87. package/dist/bus/invoke.js +3 -1
  88. package/dist/bus/namespace.d.ts +21 -0
  89. package/dist/bus/namespace.js +1 -0
  90. package/dist/bus/once.d.ts +32 -0
  91. package/dist/bus/once.js +1 -0
  92. package/dist/bus/only.d.ts +29 -0
  93. package/dist/bus/options.d.ts +31 -0
  94. package/dist/bus/output.d.ts +0 -17
  95. package/dist/bus/output.js +2 -2
  96. package/dist/bus/select.d.ts +0 -16
  97. package/dist/bus/select.js +1 -1
  98. package/dist/bus/spent.d.ts +16 -0
  99. package/dist/bus/spent.js +1 -0
  100. package/dist/bus/trace.d.ts +14 -1
  101. package/dist/bus/trace.js +1 -1
  102. package/dist/index.d.ts +6 -4
  103. package/dist/index.js +1 -1
  104. package/dist/internals.d.ts +8 -1
  105. package/dist/internals.js +1 -1
  106. package/dist/memories/client.d.ts +91 -0
  107. package/dist/memories/client.js +0 -0
  108. package/dist/memories/context.d.ts +5 -0
  109. package/dist/memories/context.js +1 -0
  110. package/dist/{builtin → memories}/deep-recall/deep-recall.d.ts +3 -0
  111. package/dist/memories/deep-recall/deep-recall.js +1 -0
  112. package/dist/memories/explore/explore.d.ts +18 -0
  113. package/dist/memories/explore/explore.js +7 -0
  114. package/dist/memories/explore/subgraph.d.ts +12 -0
  115. package/dist/memories/explore/subgraph.js +3 -0
  116. package/dist/memories/index.d.ts +30 -0
  117. package/dist/memories/index.js +1 -0
  118. package/dist/memories/observe/observe.d.ts +7 -0
  119. package/dist/memories/observe/observe.js +1 -0
  120. package/dist/memories/recall/recall.d.ts +42 -0
  121. package/dist/memories/recall/recall.js +1 -0
  122. package/dist/memories/recall/render.d.ts +5 -0
  123. package/dist/memories/recall/render.js +3 -0
  124. package/dist/memories/remember/remember.d.ts +49 -0
  125. package/dist/memories/remember/remember.js +1 -0
  126. package/dist/papers/consensus/consensus.d.ts +12 -0
  127. package/dist/papers/consensus/consensus.js +1 -0
  128. package/dist/papers/consensus/index.d.ts +4 -0
  129. package/dist/papers/consensus/index.js +1 -0
  130. package/dist/papers/consensus/reply.d.ts +24 -0
  131. package/dist/papers/consensus/reply.js +1 -0
  132. package/dist/papers/every.d.ts +6 -0
  133. package/dist/papers/every.js +1 -0
  134. package/dist/papers/index.d.ts +11 -0
  135. package/dist/papers/index.js +1 -0
  136. package/dist/papers/openalex/index.d.ts +2 -0
  137. package/dist/papers/openalex/index.js +1 -0
  138. package/dist/papers/openalex/openalex.d.ts +8 -0
  139. package/dist/papers/openalex/openalex.js +1 -0
  140. package/dist/papers/papers.d.ts +54 -0
  141. package/dist/papers/papers.js +1 -0
  142. package/dist/papers/render.d.ts +2 -0
  143. package/dist/papers/render.js +4 -0
  144. package/dist/papers/semantic-scholar/index.d.ts +4 -0
  145. package/dist/papers/semantic-scholar/index.js +1 -0
  146. package/dist/papers/semantic-scholar/reply.d.ts +29 -0
  147. package/dist/papers/semantic-scholar/reply.js +1 -0
  148. package/dist/papers/semantic-scholar/semantic-scholar.d.ts +8 -0
  149. package/dist/papers/semantic-scholar/semantic-scholar.js +1 -0
  150. package/dist/search/every.d.ts +6 -0
  151. package/dist/search/every.js +4 -0
  152. package/dist/search/index.d.ts +7 -0
  153. package/dist/search/index.js +1 -0
  154. package/dist/search/retry.d.ts +6 -0
  155. package/dist/search/retry.js +1 -0
  156. package/dist/search/search.d.ts +69 -0
  157. package/dist/search/search.js +1 -0
  158. package/dist/search/searxng/index.d.ts +2 -0
  159. package/dist/search/searxng/index.js +1 -0
  160. package/dist/search/searxng/searxng.d.ts +14 -0
  161. package/dist/search/searxng/searxng.js +5 -0
  162. package/dist/search/searxng/text.d.ts +2 -0
  163. package/dist/search/searxng/text.js +3 -0
  164. package/dist/search/sonar/index.d.ts +2 -0
  165. package/dist/search/sonar/index.js +1 -0
  166. package/dist/search/sonar/sonar.d.ts +13 -0
  167. package/dist/search/sonar/sonar.js +1 -0
  168. package/dist/search/tavily/index.d.ts +4 -0
  169. package/dist/search/tavily/index.js +1 -0
  170. package/dist/search/tavily/reply.d.ts +22 -0
  171. package/dist/search/tavily/reply.js +5 -0
  172. package/dist/search/tavily/tavily.d.ts +14 -0
  173. package/dist/search/tavily/tavily.js +1 -0
  174. package/dist/testing/index.d.ts +2 -0
  175. package/dist/testing/index.js +1 -0
  176. package/dist/testing/replay.d.ts +55 -0
  177. package/dist/testing/replay.js +1 -0
  178. package/dist/testing/store.d.ts +35 -0
  179. package/dist/testing/store.js +4 -0
  180. package/dist/tool/tool.d.ts +8 -1
  181. package/dist/tool/tool.js +1 -1
  182. package/dist/types/index.d.ts +1 -1
  183. package/dist/types/result.d.ts +38 -0
  184. package/dist/types/tool.d.ts +74 -0
  185. package/docs/bogus.mdx +85 -0
  186. package/docs/builtins.mdx +217 -0
  187. package/docs/bus.mdx +90 -0
  188. package/docs/declaring.mdx +85 -0
  189. package/docs/effects.mdx +49 -0
  190. package/docs/overview.mdx +76 -0
  191. package/docs/selecting.mdx +79 -0
  192. package/docs/stored.mdx +83 -0
  193. package/docs/turns.mdx +57 -0
  194. package/package.json +28 -9
  195. package/dist/builtin/deep-recall/deep-recall.js +0 -1
  196. package/dist/builtin/write-document/commission.d.ts +0 -56
  197. package/dist/builtin/write-document/commission.js +0 -13
  198. package/dist/builtin/write-document/design.d.ts +0 -49
  199. package/dist/builtin/write-document/design.js +0 -5
  200. package/dist/builtin/write-document/http.d.ts +0 -34
  201. package/dist/builtin/write-document/http.js +0 -1
  202. package/dist/builtin/write-document/index.d.ts +0 -6
  203. package/dist/builtin/write-document/index.js +0 -1
  204. package/dist/builtin/write-document/types.d.ts +0 -33
  205. package/dist/builtin/write-document/write-document.d.ts +0 -69
  206. package/dist/builtin/write-document/write-document.js +0 -1
  207. /package/dist/{builtin/write-document/types.js → bus/only.js} +0 -0
  208. /package/dist/{builtin → memories}/deep-recall/index.d.ts +0 -0
  209. /package/dist/{builtin → memories}/deep-recall/index.js +0 -0
  210. /package/dist/{builtin/web-search → search/sonar}/reply.d.ts +0 -0
  211. /package/dist/{builtin/web-search → search/sonar}/reply.js +0 -0
@@ -0,0 +1,85 @@
1
+ export const meta = {
2
+ title: "Declaring a tool — spendgraph docs",
3
+ description:
4
+ "Write `as const` and the handler types itself. Every check `tool` makes is something a provider either rejects opaquely or accepts and behaves oddly about.",
5
+ };
6
+
7
+ # Declaring a tool
8
+
9
+ ```ts
10
+ const lookup = tool({
11
+ name: "lookup_contract",
12
+ description: "Finds a contract clause by its reference, such as MSA 2.4.",
13
+ args: [
14
+ { name: "ref", type: "string", required: true },
15
+ { name: "limit", type: "number", required: false },
16
+ { name: "mode", type: "enum", required: false, options: ["fast", "thorough"] },
17
+ ] as const,
18
+ run: ({ ref, limit, mode }) => search(ref, limit ?? 5, mode ?? "fast"),
19
+ });
20
+ ```
21
+
22
+ ## Write `as const` and the handler types itself
23
+
24
+ `ref` arrives a string, `limit` a number, `mode` narrowed to its two options, and the optional ones optional. Rename an argument and the handler stops compiling — rather than being handed `undefined` halfway through an agent loop.
25
+
26
+ Without `as const`, inference falls back to nothing **silently**. That is the failure the package's own typecheck test exists to catch.
27
+
28
+ ## What arrives at the handler
29
+
30
+ Arguments are validated, then filled in and coerced, before `run` sees them:
31
+
32
+ - a **declared default** applies when the argument is absent;
33
+ - a value arrives at the **type its declaration promises**.
34
+
35
+ Providers hand tool arguments over as JSON a model wrote, so `"limit": "7"` is ordinary. It validates — the checker reads it through the same coercion — and it reaches `run` as the number `7`. `"loud": "false"` reaches it as `false`, not as a non-empty string that would make `if (args.loud)` run the branch the model asked it not to.
36
+
37
+ ## `aliases` are matched, never sent
38
+
39
+ The description is what the model reads. `aliases` are what [selection](/docs/tools/selecting) matches on, and they never leave the process:
40
+
41
+ ```ts
42
+ tool({
43
+ name: "issue_refund",
44
+ aliases: ["money back", "chargeback", "reimburse"],
45
+ description: "Reverses a completed order and returns the payment.",
46
+ args,
47
+ run,
48
+ });
49
+ ```
50
+
51
+ A question about money back scores zero against `refund` on wording alone. This is where the words a caller actually types get declared, rather than being smuggled into a description the model then has to read every turn.
52
+
53
+ A blank alias throws at import: it would match every query or none.
54
+
55
+ ## Checked at import, not on the first loop
56
+
57
+ Every check `tool` makes is something a provider either rejects opaquely or accepts and behaves oddly about:
58
+
59
+ | | |
60
+ | --- | --- |
61
+ | a name a provider will not take | letters, digits and underscores, starting with a letter |
62
+ | a blank description | it is the only thing the model reads to decide whether to call it |
63
+ | an enum with no options | nothing can satisfy it |
64
+ | a blank option | the model can legally pick it and you cannot act on it |
65
+ | a duplicate option or argument | one silently shadows the other |
66
+ | a blank alias | it matches every query, or none |
67
+
68
+ They fire at import, not on the first loop in production.
69
+
70
+ <Callout tone="trap" title="Without `as const`, inference gives up quietly">
71
+ Drop it and the handler's argument type falls back to `Record<string, unknown>` — no error, no red squiggle, just every field typed `unknown` from then on. It is checked by a typecheck test in the package for exactly that reason.
72
+ </Callout>
73
+
74
+ ## What a result carries
75
+
76
+ ```ts
77
+ result.status; // "completed" | "failed"
78
+ result.output; // a string is itself, anything else is JSON
79
+ result.error; // why, when it failed
80
+ result.latencyMs;
81
+ ```
82
+
83
+ A tool whose **return value** cannot be serialised — a circular object, a `BigInt` — is still a tool that **ran**. It comes back completed, carrying `[not recordable: …]`, rather than failed: reporting a completed action as a failure invites the agent to do it again.
84
+
85
+ A thrown string keeps its message. Anything can be thrown, and reading only `Error.message` discards the one thing the tool said about itself.
@@ -0,0 +1,49 @@
1
+ export const meta = {
2
+ title: "Effects — spendgraph docs",
3
+ description:
4
+ "Three values, not a taxonomy. There is no default, and unset is not readonly: a default would quietly label every tool written before this existed as safe.",
5
+ };
6
+
7
+ # Effects
8
+
9
+ What running a tool does to the world outside the process.
10
+
11
+ ```ts
12
+ tool({ name: "issue_refund", description: "…", effect: "destructive", args, run });
13
+ ```
14
+
15
+ Without it, `issue_refund` and `lookup_clause` are the same thing to a bus: two names with schemas. Nothing can tell what is safe to retry, safe to run twice, or worth asking a person about first.
16
+
17
+ | | |
18
+ | --- | --- |
19
+ | `readonly` | no effect outside the process |
20
+ | `idempotent` | has an effect, but twice is the same as once |
21
+ | `destructive` | neither, so never retry it on its own |
22
+
23
+ Three values, not a taxonomy — anything finer does not get filled in accurately.
24
+
25
+ ## There is no default, and unset is not `readonly`
26
+
27
+ A default would quietly label every tool written before this existed as **safe**, which is the one mistake that matters here. Unset means nobody has said.
28
+
29
+ ```ts
30
+ bus.effectOf("issue_refund") // "destructive", or undefined if it never said
31
+ bus.destructive() // ["issue_refund"]
32
+ bus.unannotated() // ["send_email"]
33
+ ```
34
+
35
+ `destructive()` returns only the tools that said so, so a gate built on it alone lets an unlabelled one through. **`unannotated()` is how you find those** — assert it is empty in a test and a new tool cannot join the bus unlabelled.
36
+
37
+ <Callout tone="trap" title="`readonly` is not the same as safe">
38
+ Effect says what a call does outside the process; `trust` says whether what it returns was written by somebody outside. A web fetch is `readonly` and is also how an injected instruction arrives. A guard that wants "no destructive call after untrusted content came in" has to gate on `trust: "external"` — the alternative is gating on the tool's *name*, which a rename turns off silently, and in the permissive direction.
39
+ </Callout>
40
+
41
+ ## It survives selection
42
+
43
+ The effect rides along on `declarations()` and survives `select()`, which is where a gate reads it. Every provider shape drops it: no API has a field for it, and an unknown key is either rejected or ignored.
44
+
45
+ A turn answers about the tools **it** was offered — `effectOf` is `undefined` both for a tool that never said and for one this turn never saw. A gate reading it decides what unknown means for it.
46
+
47
+ ## Three builtins leave it unset on purpose
48
+
49
+ `http_request` is a GET or a POST depending on an argument, and one label cannot be true of both. `escalate` runs your `onEscalate`, and paging someone twice is not the same as paging them once. `code_exec` runs whatever the model wrote, which is neither safe nor known — and `undefined` says exactly that, where either label would lie.
@@ -0,0 +1,76 @@
1
+ export const meta = {
2
+ title: "Tools — spendgraph docs",
3
+ description:
4
+ "Declare a tool once, offer the right few, and record what was called. A description and a function in one object, so a rename cannot become a runtime failure.",
5
+ };
6
+
7
+ # Tools
8
+
9
+ Fifty tools is 7,500 tokens spent before the user has typed anything, and a model given fifty picks worse than a model given five.
10
+
11
+ Declare a tool once, offer the right few, and record what was called — because a model that says it checked the ledger and did not is a claim only the steps can disprove.
12
+
13
+ ```sh
14
+ npm install @spendgraph/tools
15
+ ```
16
+
17
+ Two things to learn, and twenty-two builtins you can ignore until you want one.
18
+
19
+ ```ts
20
+ import { tool, toolbus, currentTime } from "@spendgraph/tools";
21
+ ```
22
+
23
+ ## The whole thing
24
+
25
+ ```ts
26
+ const lookup = tool({
27
+ name: "lookup_contract",
28
+ description: "Finds a contract clause by its reference, such as MSA 2.4.",
29
+ args: [
30
+ { name: "ref", type: "string", required: true },
31
+ { name: "limit", type: "number", required: false },
32
+ ] as const,
33
+ run: ({ ref, limit }) => search(ref, limit ?? 5),
34
+ });
35
+
36
+ const bus = toolbus([lookup, currentTime()], { limit: 6 });
37
+
38
+ const turn = bus.trace(question);
39
+ const reply = await llm.call(messages, { tools: turn });
40
+ await turn.invoke(call.name, call.args);
41
+
42
+ turn.record(); // { offeredTools, steps }
43
+ ```
44
+
45
+ ## Why one object
46
+
47
+ A description the model reads and a function you wrote, in **one object**. Splitting them — a schema here, a handler there, matched by string — is how a rename becomes a runtime failure mid-loop.
48
+
49
+ The description is the biggest lever on whether the model calls it correctly, and worth iterating on more than the code is.
50
+
51
+ ## It plugs into a prompt
52
+
53
+ `call` and `invoke` take anything that hands out a turn, and neither package imports the other:
54
+
55
+ ```ts
56
+ await prompt.call(values, ({ messages, turn }) => llm.call(messages, { tools: turn }), {
57
+ tools: bus,
58
+ });
59
+ ```
60
+
61
+ <Callout tone="trap" title="Declaring a tool is not offering it">
62
+ `toolbus()` holds everything your app can do; what the model sees each turn is a shortlist. Selection is never applied for you — hand the model the whole bus and you are paying for every description on every turn, and making it worse at choosing.
63
+ </Callout>
64
+
65
+ ## Where to go next
66
+
67
+ | | |
68
+ | --- | --- |
69
+ | [Declaring a tool](/docs/tools/declaring) | args, `as const`, and what is checked at import |
70
+ | [A stored tool](/docs/tools/stored) | wording in the dashboard, handler in code |
71
+ | [Effects](/docs/tools/effects) | what running one does to the world outside |
72
+ | [The bus](/docs/tools/bus) | holds, picks, converts, runs — and nests |
73
+ | [Selecting](/docs/tools/selecting) | the shortlist, and why it is a shortlist |
74
+ | [A turn](/docs/tools/turns) | offered, called, and what the rollout keeps |
75
+ | [Builtins](/docs/tools/builtins) | thirteen factories, none registered for you |
76
+ | [Bogus tools](/docs/tools/bogus) | twelve mocks for testing the pipeline, not the provider |
@@ -0,0 +1,79 @@
1
+ export const meta = {
2
+ title: "Selecting tools — spendgraph docs",
3
+ description:
4
+ "Fifty tools is 7.5k tokens before the user has said anything. `limit` is an accuracy budget, not a token one, and selection is keyword and usage on purpose.",
5
+ };
6
+
7
+ # Selecting
8
+
9
+ ```ts
10
+ const bus = toolbus([lookup, currentTime()], { limit: 6 });
11
+
12
+ bus.select(query) // the working set for one request
13
+ bus.anthropic(decls?) // declarations, per provider
14
+ bus.openai(decls?)
15
+ bus.markdown(decls?) // for a model with no tools API
16
+ await bus.invoke(name, args)
17
+ bus.missing(["refund"]) // names something expects that this bus lacks
18
+ bus.usage()
19
+ ```
20
+
21
+ Holding tools in one place is what makes selection possible. Fifty tools at roughly 150 tokens of declaration each is **7.5k tokens on every call** before the user has said anything, so past a point you stop sending all of them.
22
+
23
+ ## `limit` is an accuracy budget
24
+
25
+ More tools in front of a model makes it worse at picking the right one **regardless of context length**. Measured shortlists cluster around five to eight. Pinned tools count against it — what is in front of the model is what costs accuracy, whoever put it there.
26
+
27
+ A limit that is `NaN` is refused rather than clamped: `Math.max(1, NaN)` is `NaN`, and every length compares false against it, so the model would be offered **nothing** — the one outcome selection exists to avoid.
28
+
29
+ ## Keyword and usage, deliberately
30
+
31
+ No embedding provider, no extra call, no latency. It is also the baseline that says what embeddings would buy.
32
+
33
+ Relevance comes before usage: at a shortlist of six, three previously-used tools take half the slots and push out the tool the query needs. Usage earns its place as the tiebreaker among equal matches, and as the fallback when nothing matches — the case where an agent would otherwise be handed nothing.
34
+
35
+ Words are found in **any script**. A query with no ASCII letters used to produce no terms at all, so every request fell through to "the first six declared" — the same six for a Japanese question as for a Russian one.
36
+
37
+ ## How a tool scores
38
+
39
+ Whole words, never substrings. A term matching inside a longer word is noise, not signal: `me` sits inside `statement`, and at a few hundred tools that alone puts half the catalogue in every shortlist.
40
+
41
+ Where the word appears decides its weight — **name beats alias beats description**. A tool named `refund` is a stronger answer to a question about refunds than one that merely mentions the word in a paragraph.
42
+
43
+ Then each term is weighted by how **rare** it is across the bus. A word one tool in two thousand uses says far more than one four hundred of them share, and without that every term counts the same and common vocabulary decides the shortlist.
44
+
45
+ ## Aliases reach words a description never used
46
+
47
+ Lexical ranking cannot cross a synonym. A question about money back scores **zero** against a tool called `refund`, whatever its description says. Declare the wording callers actually type:
48
+
49
+ ```ts
50
+ tool({
51
+ name: "issue_refund",
52
+ aliases: ["money back", "chargeback", "reimburse"],
53
+ description: "Reverses a completed order and returns the payment.",
54
+ args,
55
+ run,
56
+ });
57
+ ```
58
+
59
+ They are matched, never sent — every provider shape drops them, exactly as it drops `effect`. Unlike an embedding, a bad match is fixed by editing one line.
60
+
61
+ <Callout tone="trap" title="Pinned tools spend the budget too">
62
+ `limit` counts pinned tools, so a shortlist of 5 with `finish` and `escalate` pinned leaves three slots for the query. A tool that needs a partner is dropped rather than offered alone — an agent told to prefer `recall` and handed only `deep_recall` will reach for the one it does not have.
63
+ </Callout>
64
+
65
+ ## Past a few hundred tools
66
+
67
+ Ranking, not speed, is what gives out first: selection stays near a millisecond at two thousand tools, but a coincidental match in an unrelated domain starts taking slots. [`busgroup`](/docs/tools/bus) gives each domain its own bus and routes between them.
68
+
69
+ ## `missing` catches a silent failure
70
+
71
+ A prompt edited to expect a `refund` tool, deployed against a bus without one, does not error. The agent is never offered it and answers anyway.
72
+
73
+ ```ts
74
+ bus.missing(["refund", "escalate"]); // the names this bus does not have
75
+ ```
76
+
77
+ ## Two tools that describe themselves the same way
78
+
79
+ Two near-identical descriptions are worse than one tool: the model cannot tell them apart and picks wrong more often, with nothing failing. The bus says so at construction, once, however many pairs there are — a line each buries whatever else the process said at startup and teaches people to switch the check off.
@@ -0,0 +1,83 @@
1
+ export const meta = {
2
+ title: "A stored tool — spendgraph docs",
3
+ description:
4
+ "The dashboard owns the description, your code owns the arguments and the handler, and bindTool checks the two against each other before the model ever sees it.",
5
+ };
6
+
7
+ # A stored tool
8
+
9
+ The description is the biggest lever on whether a model calls a tool correctly, and the part you will want to change most often. `bindTool` puts that half in the dashboard and leaves the rest in code.
10
+
11
+ ```ts
12
+ import { bindTool } from "@spendgraph/tools";
13
+ import { Spendgraph } from "@spendgraph/sdk";
14
+
15
+ const lookup = await bindTool(sg, {
16
+ name: "lookup_contract",
17
+ args: [{ name: "ref", type: "string", required: true }] as const,
18
+ run: ({ ref }) => search(ref),
19
+ });
20
+ ```
21
+
22
+ No `description` in that call. That is the point — one source for the string the model reads, and it is the one you can edit without a release.
23
+
24
+ ## Who owns what
25
+
26
+ ```
27
+ DASHBOARD YOUR CODE
28
+ ───────── ─────────
29
+ description ◀── iterate on args ──▶ run() is typed from them
30
+ effect these without run() ──▶ the thing that actually happens
31
+ pinned a deploy
32
+ ```
33
+
34
+ Arguments stay in code because the handler's parameter types are inferred from them. Pull those from the server and every `run` is handed a `Record<string, unknown>`, which gives up the one guarantee [declaring a tool](/docs/tools/declaring) exists to provide.
35
+
36
+ Write `as const` here exactly as you would for `tool()`. Without it inference falls back to nothing, and the drift check below becomes the only thing still catching a rename.
37
+
38
+ ## Drift fails at bind, not mid-loop
39
+
40
+ The stored arguments and the declared ones are compared on the way through, by **name, type and requiredness** — the three a handler is written against.
41
+
42
+ ```ts
43
+ import { ToolDriftError, ToolNotStoredError } from "@spendgraph/tools";
44
+
45
+ try {
46
+ await bindTool(sg, spec);
47
+ } catch (err) {
48
+ if (err instanceof ToolDriftError) err.differences; // every disagreement, in words
49
+ if (err instanceof ToolNotStoredError) err.toolName; // nothing stored under that name
50
+ }
51
+ ```
52
+
53
+ `differences` reads like a review comment rather than a diff:
54
+
55
+ ```
56
+ "ref" is string here and number stored
57
+ "limit" is declared here but not stored
58
+ "mode" is stored but not declared here
59
+ ```
60
+
61
+ A description or a bound that differs is **not** drift — that is the dashboard being edited, which is the whole feature. A renamed argument is a handler about to be passed `undefined` halfway through an agent loop, so it throws.
62
+
63
+ <Callout tone="warn" title="Bind at startup, not per request">
64
+ Every `bindTool` is a round trip to the API. Bind once when the process starts and hold the result, the same as you would a database connection — a bind inside a request handler puts a network hop in front of every model call.
65
+ </Callout>
66
+
67
+ <Callout tone="trap" title="A rename in the dashboard is a handler passed `undefined`">
68
+ The wording is editable without a deploy; the argument names are not. `bindTool` compares them at bind time and refuses — a deploy that fails is recoverable, and the same rename noticed mid-loop is a handler halfway through somebody's refund reading an argument that is not there.
69
+ </Callout>
70
+
71
+ ## When nothing is stored yet
72
+
73
+ `ToolNotStoredError` is the ordinary case of "you have not written this one in the dashboard yet", not an exotic failure. It exists so a 404 from three layers down arrives as a sentence naming the tool.
74
+
75
+ Create the row first — `sg.tools.create` on the [client](/docs/sdk/client), or the Tools page in the dashboard — then bind to it.
76
+
77
+ ## It is an ordinary tool afterwards
78
+
79
+ What comes back is what `tool()` returns. Register it on a bus, select it, invoke it, record it; nothing downstream knows or cares where the wording came from.
80
+
81
+ ```ts
82
+ const bus = toolbus([lookup, calculate()]);
83
+ ```
package/docs/turns.mdx ADDED
@@ -0,0 +1,57 @@
1
+ export const meta = {
2
+ title: "A turn — spendgraph docs",
3
+ description:
4
+ "A turn is selected once and remembers what it offered. Without that a record cannot tell 'chose not to' from 'was never offered', which are opposite bugs.",
5
+ };
6
+
7
+ # A turn
8
+
9
+ ```ts
10
+ const turn = bus.trace(query);
11
+
12
+ const reply = await llm.call(messages, { tools: turn });
13
+ await turn.invoke(call.name, call.args);
14
+
15
+ turn.record(); // { offeredTools, steps }
16
+ ```
17
+
18
+ A turn is **selected once** and remembers what it offered. A trace cannot disagree with itself about what the model saw.
19
+
20
+ ## Offered is recorded, not just called
21
+
22
+ Which tools were offered is recorded as well as which were called. Without it a record cannot tell **"chose not to"** from **"was never offered"** — which are opposite bugs, and look identical in a log that only lists calls.
23
+
24
+ ## What a turn answers
25
+
26
+ | | |
27
+ | --- | --- |
28
+ | `turn.offered` | the shortlist this turn was given |
29
+ | `turn.anthropic()` · `openai()` · `markdown()` | the same list, per provider |
30
+ | `turn.effectOf(name)` | what that tool said it does, or `undefined` |
31
+ | `turn.invoke(name, args)` | runs one and records the step. Never throws |
32
+ | `turn.steps()` | the steps so far, in call order |
33
+ | `turn.record()` | the pair, in the shape `report` takes |
34
+
35
+ ## Never throwing is the point
36
+
37
+ A tool that fails is an **outcome the agent can react to** and a step worth recording, not an exception that ends the run — the same argument `status` and `error` exist on a rollout for.
38
+
39
+ A tool the bus does not have is a failure that says what was offered, since the usual cause is a hallucinated name.
40
+
41
+ <Callout tone="trap" title="`undefined` means two different things">
42
+ A tool that was offered and stayed quiet, and a tool this turn never offered, both answer `undefined`. `offeredTools` is what tells them apart — which is the difference between a model that declined to look something up and a model that could not.
43
+ </Callout>
44
+
45
+ ## Usage drives the tiebreaker
46
+
47
+ `bus.usage()` counts what has actually been called, which is what selection falls back on when a query singles out nothing. A call that never reached the tool — a name that does not exist, arguments that did not validate — is not counted, because it was not a use.
48
+
49
+ ## With a prompt
50
+
51
+ ```ts
52
+ await prompt.call(values, ({ messages, turn }) => llm.call(messages, { tools: turn }), {
53
+ tools: bus,
54
+ });
55
+ ```
56
+
57
+ The prompt hands the turn to your callback and puts `record()` on the rollout. The three packages compose without any of them importing another.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spendgraph/tools",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Declare a tool once, offer the right few, and record what was called.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -28,6 +28,7 @@
28
28
  "types": "./dist/index.d.ts",
29
29
  "import": "./dist/index.js"
30
30
  },
31
+ "./docs/*": "./docs/*",
31
32
  "./bogus": {
32
33
  "types": "./dist/bogus/index.d.ts",
33
34
  "import": "./dist/bogus/index.js"
@@ -36,6 +37,18 @@
36
37
  "types": "./dist/wire/index.d.ts",
37
38
  "import": "./dist/wire/index.js"
38
39
  },
40
+ "./papers": {
41
+ "types": "./dist/papers/index.d.ts",
42
+ "import": "./dist/papers/index.js"
43
+ },
44
+ "./search": {
45
+ "types": "./dist/search/index.d.ts",
46
+ "import": "./dist/search/index.js"
47
+ },
48
+ "./testing": {
49
+ "types": "./dist/testing/index.d.ts",
50
+ "import": "./dist/testing/index.js"
51
+ },
39
52
  "./internals": {
40
53
  "types": "./dist/internals.d.ts",
41
54
  "import": "./dist/internals.js"
@@ -44,19 +57,19 @@
44
57
  },
45
58
  "files": [
46
59
  "dist",
60
+ "docs",
47
61
  "README.md"
48
62
  ],
49
- "scripts": {
50
- "build": "rm -rf dist && tsc -p tsconfig.json --emitDeclarationOnly && tsc -p tsconfig.json --declaration false --removeComments && node ../../scripts/minify.mjs dist",
51
- "test": "npm run build && vitest run",
52
- "examples": "npm run build && node examples/02-select/01-shortlist.mjs"
53
- },
54
63
  "dependencies": {
55
- "@spendgraph/sdk": "^0.6.0"
64
+ "@spendgraph/sdk": "^0.8.0",
65
+ "jszip": "^3.10.2",
66
+ "unpdf": "^1.8.1"
56
67
  },
57
68
  "devDependencies": {
58
69
  "@locusgraph/client": "^0.8.1",
59
- "@spendgraph/prompt": "^0.6.0",
70
+ "@spendgraph/config": "0.8.0",
71
+ "@spendgraph/prompt": "^0.8.0",
72
+ "pdf-lib": "^1.17.1",
60
73
  "typescript": "^5"
61
74
  },
62
75
  "engines": {
@@ -64,5 +77,11 @@
64
77
  },
65
78
  "publishConfig": {
66
79
  "access": "public"
80
+ },
81
+ "scripts": {
82
+ "build": "rm -rf dist && tsc -p tsconfig.json --emitDeclarationOnly && tsc -p tsconfig.json --declaration false --removeComments && node ../../scripts/minify.mjs dist",
83
+ "examples": "pnpm run build && node examples/02-select/01-shortlist.mjs",
84
+ "test": "vitest run",
85
+ "typecheck": "tsc -p tsconfig.tests.json"
67
86
  }
68
- }
87
+ }
@@ -1 +0,0 @@
1
- import{tool as s}from"../../tool/index.js";const o=20;function l(t){const a=t.maxFacts??o;return s({name:"deep_recall",description:"Searches your own knowledge base and returns an answer with the evidence it rests on. A walker follows linked contexts, so it answers questions spread across several notes \u2014 what do we know about X, what was decided and why. Ask one focused thing. Prefer it over a web search whenever the answer could already be in your records.",effect:"readonly",args:[{name:"question",type:"string",required:!0,maxLength:1e3,description:"One focused question, not a conversation and not several at once."},{name:"context_ids",type:"list",required:!1,description:"Contexts to start the walk from, as type:name \u2014 take them from the contexts of an earlier result. A hint, not a filter; the walk may go elsewhere."}],run:async({question:n,context_ids:r})=>{const e=await t.client.deepRecall({question:n,...t.graphId?{graphId:t.graphId}:{},...r?.length?{contextIds:r}:{}});return{summary:e.summary,facts:e.facts.slice(0,a),truncated:e.facts.length>a,contextsWalked:e.contexts_walked,coverage:e.coverage,fallback:e.fallback,costMicroUsd:Math.round(e.usage.cost_usd*1e6)}}})}export{l as deepRecall};
@@ -1,56 +0,0 @@
1
- import { type AbortSignalLike, type MoaClientConfig } from "./http.js";
2
- import type { CommissionInput, CommissionResult } from "./types.js";
3
- /**
4
- * Moa's task surface — fire, then await.
5
- *
6
- * `create()` hands back an id immediately and `get()` reads it; pass `waitMs`
7
- * (or use {@link pollUntilDone}) to long-poll, so one call is awaited rather
8
- * than a loop tight-polling. `cancel()` is optional so a test stub stays small.
9
- */
10
- export interface CommissionClient {
11
- create(input: CommissionInput): Promise<string>;
12
- get(id: string, opts?: {
13
- waitMs?: number;
14
- signal?: AbortSignalLike;
15
- }): Promise<CommissionResult>;
16
- cancel?(id: string): Promise<void>;
17
- }
18
- /**
19
- * HTTP {@link CommissionClient} over Moa's task API (`POST /tasks` → `GET /tasks/:id`).
20
- *
21
- * The request body carries the ask as prose — Moa's artifact pipeline is
22
- * prompt-driven and `POST /tasks` strips unknown fields, so context, design and
23
- * deliverables are blocks on the task rather than parameters beside it.
24
- */
25
- export declare function createCommissionClient(config?: MoaClientConfig): CommissionClient;
26
- /** The sane ceiling for a chat-bound call. A rendered document wants minutes, not this. */
27
- export declare const INTERACTIVE_TIMEOUT_MS = 30000;
28
- /** Thrown by {@link pollUntilDone} when the caller's abort signal fires. */
29
- export declare class CommissionAbortedError extends Error {
30
- readonly commissionId: string;
31
- constructor(commissionId: string);
32
- }
33
- /** Thrown when the wait ran out. The task itself is still running server-side. */
34
- export declare class CommissionTimeoutError extends Error {
35
- readonly commissionId: string;
36
- readonly timeoutMs: number;
37
- constructor(commissionId: string, timeoutMs: number);
38
- }
39
- export interface PollOptions {
40
- timeoutMs?: number;
41
- pollIntervalMs?: number;
42
- /**
43
- * Stops the wait and throws {@link CommissionAbortedError}, cutting the held
44
- * long-poll with it. The task keeps running — cancelling it is the caller's call.
45
- */
46
- signal?: AbortSignalLike;
47
- /**
48
- * How long to wait before re-reading a done task whose result has not landed
49
- * yet. Moa flips the status slightly before it writes. Default 0.
50
- */
51
- settleMs?: number;
52
- /** Keep settling while a done task carries no artifacts. Set it when files were asked for. */
53
- expectArtifacts?: boolean;
54
- }
55
- /** Wait for a commission to settle, long-polling rather than tight-polling. */
56
- export declare function pollUntilDone(client: CommissionClient, id: string, opts?: PollOptions): Promise<CommissionResult>;
@@ -1,13 +0,0 @@
1
- import{createRequest as w,MoaApiError as h}from"./http.js";const T={queued:"pending",running:"running",succeeded:"done",failed:"failed",canceled:"failed"},M=["md","pdf","docx","pptx"],g=n=>{const t=n?.split(".").pop()?.toLowerCase();return t&&M.includes(t)?t:void 0};function E(n){if(!n?.length)return;const t=n.flatMap(e=>{const s=g(e.name);return!s||!(e.url||e.data)?[]:[{name:e.name??`document.${s}`,format:s,url:e.url,data:e.data,mime:e.contentType,sizeBytes:e.sizeBytes}]});return t.length?t:void 0}function I(n){return`
2
-
3
- <deliverables>
4
- Besides the text answer, save the finished deliverable into the artifacts/ directory of your working dir as: ${n.map(t=>`one .${t} file`).join(", ")}.
5
- </deliverables>`}function x(n){let t=n.task;return n.context&&(t+=`
6
-
7
- <context>
8
- ${n.context}
9
- </context>`),n.design&&(t+=`
10
-
11
- <design>
12
- ${n.design}
13
- </design>`),n.artifacts?.length&&(t+=I(n.artifacts)),t}function _(n={}){const t=w(n);return{async create(e){const s=await t("/tasks",{method:"POST",body:{task:x(e),mode:"striker",...e.sessionId?{sessionId:e.sessionId}:{},...e.newSessionId?{newSessionId:e.newSessionId}:{}}});if(!s.id)throw new h("Moa did not return a task id",502);return s.id},async get(e,s){const c=s?.waitMs&&s.waitMs>0?`?waitMs=${Math.floor(s.waitMs)}`:"",o=await t(`/tasks/${encodeURIComponent(e)}${c}`,{signal:s?.signal}),i=o.status?T[o.status]:void 0;return{id:o.id??e,status:i??"running",output:o.result,error:o.error,costUsd:o.costUsd,numTurns:o.numTurns,sessionId:o.sessionId,artifacts:E(o.artifacts)}},async cancel(e){await t(`/tasks/${encodeURIComponent(e)}`,{method:"DELETE"})}}}const b=3e4,A=2e3,k=3,y=25e3;class m extends Error{commissionId;constructor(t){super(`Commission ${t} was cancelled by the caller.`),this.name="CommissionAbortedError",this.commissionId=t}}class C extends Error{commissionId;timeoutMs;constructor(t,e){super(`Commission ${t} did not complete within ${e/1e3}s. The task may still be running in the background.`),this.name="CommissionTimeoutError",this.commissionId=t,this.timeoutMs=e}}const f=(n,t)=>new Promise(e=>{const s=()=>e();t?.addEventListener("abort",s,{once:!0}),setTimeout(()=>{t?.removeEventListener("abort",s),e()},n)});async function p(n,t,e={}){const s=e.timeoutMs??b,c=e.pollIntervalMs??A,o=e.settleMs??0,i=e.signal,l=Date.now()+s;for(;;){if(i?.aborted)throw new m(t);const u=l-Date.now();if(u<=0)throw new C(t,s);let r;try{r=await n.get(t,{waitMs:Math.min(u,y),signal:i})}catch(d){throw i?.aborted?new m(t):d}if(r.status==="failed")return r;if(r.status==="done"){const d=a=>!a.output?.trim()||e.expectArtifacts===!0&&!a.artifacts?.length;for(let a=0;o>0&&a<k&&d(r);a++)await f(o),r=await n.get(t);return r}await f(Math.min(c,Math.max(0,l-Date.now())),i)}}export{m as CommissionAbortedError,C as CommissionTimeoutError,b as INTERACTIVE_TIMEOUT_MS,_ as createCommissionClient,p as pollUntilDone};
@@ -1,49 +0,0 @@
1
- import type { ArtifactFormat } from "./types.js";
2
- /**
3
- * The house style a commissioned document is built to.
4
- *
5
- * Every field is optional because a caller overrides only what its brand
6
- * actually fixes; the rest falls back to {@link DEFAULT_DESIGN}.
7
- */
8
- export interface DesignGuide {
9
- /** What the style is called, so the document can say which one it followed. */
10
- name?: string;
11
- fonts?: {
12
- heading?: string;
13
- body?: string;
14
- mono?: string;
15
- };
16
- palette?: {
17
- primary?: string;
18
- accent?: string;
19
- text?: string;
20
- muted?: string;
21
- surface?: string;
22
- };
23
- /** Page size, margins, grid, density. */
24
- layout?: string;
25
- /** How a figure earns its place and what it may not do. */
26
- charts?: string;
27
- /** How the writing sounds. */
28
- tone?: string;
29
- /** Fetchable logo, dropped on the cover and nowhere else. */
30
- logoUrl?: string;
31
- /** Hard rules, appended to the defaults rather than replacing them. */
32
- rules?: readonly string[];
33
- }
34
- /**
35
- * What a document looks like when nobody has said.
36
- *
37
- * Chosen to be defensible rather than distinctive: one typeface, one accent,
38
- * real whitespace, and figures that carry a single message.
39
- */
40
- export declare const DEFAULT_DESIGN: DesignGuide;
41
- /**
42
- * A caller's guide over the defaults: named fields win, `rules` accumulate.
43
- *
44
- * Rules append because they are constraints — a brand adding "never use red"
45
- * means it as well as the defaults, not instead of them.
46
- */
47
- export declare function mergeDesign(base: DesignGuide, over?: DesignGuide): DesignGuide;
48
- /** The guide as the block Moa reads, since its artifact pipeline is prompt-driven. */
49
- export declare function renderDesign(guide: DesignGuide, format: ArtifactFormat, notes?: string): string;
@@ -1,5 +0,0 @@
1
- const c={name:"Spendgraph house style",fonts:{heading:"Inter",body:"Inter",mono:"JetBrains Mono"},palette:{primary:"#0F172A",accent:"#2563EB",text:"#111827",muted:"#64748B",surface:"#FFFFFF"},layout:"A4 portrait, 20mm margins, a single text column at 60-75 characters, a 4pt spacing scale, and one heading scale used consistently (28/20/16/12). Whitespace is the design.",charts:"One message per figure, stated in its caption. Label series directly where there are three or fewer. No 3D, no gradients, no gridline heavier than the data, no pie chart past three slices, and axes that start at zero unless the caption says why not.",tone:"Plain and specific. Lead with the finding, then the evidence. No filler adjectives, no throat-clearing, no sentence that survives being deleted.",rules:["A cover carrying the title, date and author, and no other ornament.","Every number traceable to something in the source material; invent nothing.","Tables for anything being compared; prose for anything being argued.","No stock imagery, no clip art, no placeholder text in the finished file.","Page numbers on every page after the cover."]},d={pdf:"Cover, then contents if it runs past four pages, then the body. Figures captioned and referenced from the text. Nothing straddles a page break: no orphan heading, no split table row.",pptx:"16:9. One idea per slide, and the title is that idea as a sentence \u2014 a claim, not a label. At most six lines of body per slide, one figure per slide at most, and everything you cut goes into the speaker notes. Close on decisions and next steps, not on 'thank you'.",docx:"Real Word styles for headings rather than manual bold, a table-of-contents field rather than a typed list, a header row on every table, and captions on figures.",md:"A title heading, ATX headings throughout, tables for comparisons, fenced code with a language tag, and no HTML."};function i(e,t){return e?t?{...e,...t}:e:t}function h(e,t){return t?{...e,...t,fonts:i(e.fonts,t.fonts),palette:i(e.palette,t.palette),rules:[...e.rules??[],...t.rules??[]]}:e}const a=(e,t)=>t?[`${e}: ${t}`]:[];function p(e,t,o){const{fonts:r,palette:n}=e;return[...a("Style",e.name),...a("Typefaces",[r?.heading&&`headings ${r.heading}`,r?.body&&`body ${r.body}`,r?.mono&&`code ${r.mono}`].filter(Boolean).join(", ")||void 0),...a("Palette",[n?.primary&&`primary ${n.primary}`,n?.accent&&`accent ${n.accent}`,n?.text&&`text ${n.text}`,n?.muted&&`muted ${n.muted}`,n?.surface&&`surface ${n.surface}`].filter(Boolean).join(", ")||void 0),...a("Layout",e.layout),...a("Figures",e.charts),...a("Voice",e.tone),...a("Logo",e.logoUrl),`Format craft (.${t}): ${d[t]}`,...e.rules?.length?[`Rules:
2
- ${e.rules.map(s=>`- ${s}`).join(`
3
- `)}`]:[],...o?.trim()?[`Overrides for this document, which win over everything above:
4
- ${o.trim()}`]:[]].join(`
5
- `)}export{c as DEFAULT_DESIGN,h as mergeDesign,p as renderDesign};