@retinue/agentkit 0.2.0 → 0.3.1

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 (209) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/audio/openai.d.ts +36 -0
  3. package/dist/adapters/audio/openai.js +136 -0
  4. package/dist/adapters/bullmq/lock.d.ts +26 -2
  5. package/dist/adapters/memory/connections.d.ts +10 -0
  6. package/dist/adapters/memory/connections.js +87 -0
  7. package/dist/adapters/memory/graph.d.ts +17 -0
  8. package/dist/adapters/memory/graph.js +267 -0
  9. package/dist/adapters/memory/index.d.ts +3 -0
  10. package/dist/adapters/memory/index.js +3 -0
  11. package/dist/adapters/memory/rate-limit.d.ts +14 -0
  12. package/dist/adapters/memory/rate-limit.js +28 -0
  13. package/dist/adapters/memory/runtime.js +0 -0
  14. package/dist/adapters/postgres/connections.d.ts +15 -0
  15. package/dist/adapters/postgres/connections.js +134 -0
  16. package/dist/adapters/postgres/graph.d.ts +24 -0
  17. package/dist/adapters/postgres/graph.js +325 -0
  18. package/dist/adapters/postgres/index.d.ts +2 -0
  19. package/dist/adapters/postgres/index.js +2 -0
  20. package/dist/adapters/postgres/migrations.js +198 -0
  21. package/dist/adapters/postgres/run-store.js +26 -2
  22. package/dist/adapters/postgres/schema.d.ts +61 -2
  23. package/dist/adapters/postgres/schema.js +95 -10
  24. package/dist/adapters/redis/index.d.ts +1 -0
  25. package/dist/adapters/redis/index.js +1 -0
  26. package/dist/adapters/redis/rate-limit.d.ts +46 -0
  27. package/dist/adapters/redis/rate-limit.js +55 -0
  28. package/dist/adapters/supabase/index.d.ts +15 -0
  29. package/dist/adapters/supabase/index.js +15 -0
  30. package/dist/adapters/supabase/rls.js +33 -0
  31. package/dist/agents/agent.d.ts +78 -1
  32. package/dist/agents/agent.js +118 -5
  33. package/dist/agents/define.js +24 -4
  34. package/dist/agents/engine.d.ts +20 -0
  35. package/dist/agents/engine.js +177 -6
  36. package/dist/audio/index.d.ts +151 -0
  37. package/dist/audio/index.js +183 -0
  38. package/dist/connections/cipher.d.ts +103 -0
  39. package/dist/connections/cipher.js +141 -0
  40. package/dist/connections/index.d.ts +156 -0
  41. package/dist/connections/index.js +72 -0
  42. package/dist/connections/oauth/client.d.ts +84 -0
  43. package/dist/connections/oauth/client.js +141 -0
  44. package/dist/connections/oauth/index.d.ts +166 -0
  45. package/dist/connections/oauth/index.js +226 -0
  46. package/dist/connections/oauth/service.d.ts +90 -0
  47. package/dist/connections/oauth/service.js +132 -0
  48. package/dist/connections/pause.d.ts +68 -0
  49. package/dist/connections/pause.js +97 -0
  50. package/dist/connections/resolver.d.ts +59 -0
  51. package/dist/connections/resolver.js +138 -0
  52. package/dist/connections/resume.d.ts +64 -0
  53. package/dist/connections/resume.js +99 -0
  54. package/dist/connections/vault-cipher.d.ts +95 -0
  55. package/dist/connections/vault-cipher.js +206 -0
  56. package/dist/core/content-parts.d.ts +18 -2
  57. package/dist/core/content-parts.js +1 -0
  58. package/dist/core/context.d.ts +25 -0
  59. package/dist/core/errors.d.ts +1 -1
  60. package/dist/core/errors.js +15 -0
  61. package/dist/core/events.d.ts +44 -2
  62. package/dist/core/events.js +31 -2
  63. package/dist/core/validation.js +19 -0
  64. package/dist/entries/connections.d.ts +10 -0
  65. package/dist/entries/connections.js +10 -0
  66. package/dist/entries/knowledge.d.ts +8 -0
  67. package/dist/entries/knowledge.js +8 -0
  68. package/dist/entries/mcp-server.d.ts +9 -0
  69. package/dist/entries/mcp-server.js +9 -0
  70. package/dist/entries/runtime.d.ts +10 -0
  71. package/dist/entries/runtime.js +8 -0
  72. package/dist/entries/testing.d.ts +28 -0
  73. package/dist/entries/testing.js +28 -0
  74. package/dist/graphql/resolvers.d.ts +3 -1
  75. package/dist/graphql/resolvers.js +14 -0
  76. package/dist/knowledge/communities.d.ts +166 -0
  77. package/dist/knowledge/communities.js +377 -0
  78. package/dist/knowledge/graph-global.d.ts +153 -0
  79. package/dist/knowledge/graph-global.js +246 -0
  80. package/dist/knowledge/graph-retrieval.d.ts +150 -0
  81. package/dist/knowledge/graph-retrieval.js +303 -0
  82. package/dist/knowledge/graph.d.ts +223 -0
  83. package/dist/knowledge/graph.js +369 -0
  84. package/dist/knowledge/index.d.ts +24 -0
  85. package/dist/knowledge/index.js +36 -1
  86. package/dist/knowledge/retrieval.d.ts +26 -1
  87. package/dist/knowledge/retrieval.js +77 -1
  88. package/dist/mcp-server/index.d.ts +168 -0
  89. package/dist/mcp-server/index.js +175 -0
  90. package/dist/models/extraction.d.ts +73 -0
  91. package/dist/models/extraction.js +118 -0
  92. package/dist/models/index.d.ts +40 -1
  93. package/dist/models/index.js +22 -1
  94. package/dist/models/pricing.d.ts +2 -0
  95. package/dist/models/pricing.js +17 -1
  96. package/dist/models/provider-factory.js +9 -7
  97. package/dist/models/streaming.d.ts +89 -0
  98. package/dist/models/streaming.js +179 -2
  99. package/dist/persistence/index.d.ts +230 -0
  100. package/dist/persistence/index.js +2 -0
  101. package/dist/runtime/index.d.ts +1 -1
  102. package/dist/runtime/index.js +12 -0
  103. package/dist/runtime/worker.d.ts +36 -1
  104. package/dist/runtime/worker.js +37 -4
  105. package/dist/security/checklist.js +46 -0
  106. package/dist/server/bin.d.ts +72 -0
  107. package/dist/server/bin.js +304 -0
  108. package/dist/server/boot.d.ts +20 -2
  109. package/dist/server/boot.js +29 -3
  110. package/dist/server/cli-worker.js +6 -3
  111. package/dist/server/cli.d.ts +13 -0
  112. package/dist/server/cli.js +10 -5
  113. package/dist/server/config.d.ts +11 -1
  114. package/dist/server/config.js +18 -0
  115. package/dist/server/doctor.d.ts +85 -0
  116. package/dist/server/doctor.js +280 -0
  117. package/dist/server/pool.d.ts +47 -0
  118. package/dist/server/pool.js +73 -0
  119. package/dist/skills/context.d.ts +61 -0
  120. package/dist/skills/context.js +107 -0
  121. package/dist/skills/index.d.ts +1 -0
  122. package/dist/skills/index.js +1 -0
  123. package/dist/telemetry/spans.js +5 -0
  124. package/dist/testing/conformance/artifact-exports.d.ts +20 -0
  125. package/dist/testing/conformance/artifact-exports.js +172 -0
  126. package/dist/testing/conformance/artifacts.d.ts +11 -0
  127. package/dist/testing/conformance/artifacts.js +316 -0
  128. package/dist/testing/conformance/audio.d.ts +38 -0
  129. package/dist/testing/conformance/audio.js +145 -0
  130. package/dist/testing/conformance/capability.d.ts +23 -0
  131. package/dist/testing/conformance/capability.js +27 -0
  132. package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
  133. package/dist/testing/conformance/checkpoint-store.js +72 -0
  134. package/dist/testing/conformance/connections.d.ts +14 -0
  135. package/dist/testing/conformance/connections.js +171 -0
  136. package/dist/testing/conformance/conversation-store.d.ts +10 -0
  137. package/dist/testing/conformance/conversation-store.js +60 -0
  138. package/dist/testing/conformance/evaluation.d.ts +10 -0
  139. package/dist/testing/conformance/evaluation.js +295 -0
  140. package/dist/testing/conformance/files.d.ts +19 -0
  141. package/dist/testing/conformance/files.js +454 -0
  142. package/dist/testing/conformance/flows.d.ts +16 -0
  143. package/dist/testing/conformance/flows.js +193 -0
  144. package/dist/testing/conformance/graph.d.ts +22 -0
  145. package/dist/testing/conformance/graph.js +500 -0
  146. package/dist/testing/conformance/hitl.d.ts +25 -0
  147. package/dist/testing/conformance/hitl.js +523 -0
  148. package/dist/testing/conformance/index.d.ts +163 -0
  149. package/dist/testing/conformance/index.js +321 -0
  150. package/dist/testing/conformance/invariants.d.ts +23 -0
  151. package/dist/testing/conformance/invariants.js +80 -0
  152. package/dist/testing/conformance/knowledge.d.ts +41 -0
  153. package/dist/testing/conformance/knowledge.js +592 -0
  154. package/dist/testing/conformance/parents.d.ts +50 -0
  155. package/dist/testing/conformance/parents.js +39 -0
  156. package/dist/testing/conformance/rate-limit.d.ts +25 -0
  157. package/dist/testing/conformance/rate-limit.js +71 -0
  158. package/dist/testing/conformance/records.d.ts +36 -0
  159. package/dist/testing/conformance/records.js +400 -0
  160. package/dist/testing/conformance/rollups.d.ts +30 -0
  161. package/dist/testing/conformance/rollups.js +425 -0
  162. package/dist/testing/conformance/run-coordinator.d.ts +27 -0
  163. package/dist/testing/conformance/run-coordinator.js +150 -0
  164. package/dist/testing/conformance/run-event-log.d.ts +9 -0
  165. package/dist/testing/conformance/run-event-log.js +138 -0
  166. package/dist/testing/conformance/run-store.d.ts +12 -0
  167. package/dist/testing/conformance/run-store.js +294 -0
  168. package/dist/testing/conformance/session-state.d.ts +38 -0
  169. package/dist/testing/conformance/session-state.js +198 -0
  170. package/dist/testing/conformance/usage-limits.d.ts +10 -0
  171. package/dist/testing/conformance/usage-limits.js +309 -0
  172. package/dist/testing/conformance.d.ts +8 -0
  173. package/dist/testing/conformance.js +8 -0
  174. package/dist/testing/memory-backend.d.ts +39 -0
  175. package/dist/testing/memory-backend.js +41 -0
  176. package/dist/testing/pglite.d.ts +55 -0
  177. package/dist/testing/pglite.js +78 -0
  178. package/dist/testing/stub-model.d.ts +58 -0
  179. package/dist/testing/stub-model.js +71 -0
  180. package/dist/testing/supabase-storage-double.d.ts +35 -0
  181. package/dist/testing/supabase-storage-double.js +101 -0
  182. package/dist/toolkit/http.d.ts +27 -0
  183. package/dist/toolkit/http.js +48 -0
  184. package/dist/toolkit/index.d.ts +27 -1
  185. package/dist/toolkit/index.js +24 -1
  186. package/dist/toolkit/mime.d.ts +97 -0
  187. package/dist/toolkit/mime.js +233 -0
  188. package/dist/toolkit/ssrf.d.ts +139 -0
  189. package/dist/toolkit/ssrf.js +349 -0
  190. package/dist/toolkit/vendor.d.ts +91 -0
  191. package/dist/toolkit/vendor.js +153 -0
  192. package/dist/tools/credentials.d.ts +265 -6
  193. package/dist/tools/credentials.js +318 -3
  194. package/dist/tools/define.d.ts +14 -0
  195. package/dist/tools/define.js +3 -0
  196. package/dist/tools/index.d.ts +21 -0
  197. package/dist/tools/index.js +7 -0
  198. package/dist/tools/library/audio.d.ts +63 -0
  199. package/dist/tools/library/audio.js +148 -0
  200. package/dist/tools/library/index.d.ts +8 -0
  201. package/dist/tools/library/index.js +7 -0
  202. package/dist/tools/registry.d.ts +33 -0
  203. package/dist/tools/registry.js +72 -2
  204. package/dist/usage/index.d.ts +21 -0
  205. package/dist/usage/index.js +1 -0
  206. package/dist/usage/rate-limit.d.ts +122 -0
  207. package/dist/usage/rate-limit.js +131 -0
  208. package/dist/usage/recorder.js +3 -0
  209. package/package.json +29 -2
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The one place a `pg.Pool` is built for this host — REQ-041 (#190).
3
+ *
4
+ * There were four: `bin.ts` for `migrate`/`doctor`, `cli.ts` for the API host, `cli-worker.ts` for the
5
+ * worker, and the driver `doctor` is handed. Four copies of "how this deployment connects" is three
6
+ * copies too many, and the one that drifts is the one nobody exercises — the worker, whose writes are
7
+ * the ones that must land in the same schema as everything else.
8
+ *
9
+ * **What it adds over `new Pool`.** `RETINUE_DATABASE_SCHEMA` names the schema the platform's tables
10
+ * live in, and honouring it takes two things that must agree:
11
+ *
12
+ * - `pool.on("connect")` sets `search_path` on **every** connection, because `createPgExecutor` runs
13
+ * each query through `pool.query`, which takes a different connection per call. A pooled connection
14
+ * carries whatever `search_path` its last user left, so setting it once at startup means the first
15
+ * few statements land in the right schema and the rest land wherever. That failure is not loud: it
16
+ * is a table created in `public` by a migration that reported success.
17
+ * - `createPoolOpener(pool, …)` sets it again per checkout, which is what the transaction scope uses.
18
+ * Redundant on paper and deliberately kept: the opener is the path that holds `SELECT … FOR UPDATE`
19
+ * across statements, and it must not depend on a listener having fired.
20
+ *
21
+ * **`public` stays on the path** after the named schema. Two things need it: the `vector` type is
22
+ * pinned to `public` so it resolves from any schema (see the note in `migrations.ts`), and ShareFlow's
23
+ * adapters qualify all 70 of their queries as `public.`, which is what lets one pool serve a platform
24
+ * schema and a product schema at once.
25
+ */
26
+ /**
27
+ * The `search_path` for a named schema, or `undefined` to leave the connection's own.
28
+ *
29
+ * Exported because it is the only string in this file that ends up in SQL, and a test that pins it is
30
+ * cheaper than reading two call sites to find out what a deployment actually gets.
31
+ */
32
+ export const searchPathFor = (schema) => schema === undefined || schema === "" ? undefined : `${schema}, public`;
33
+ export const openPostgres = async (settings) => {
34
+ const { Pool } = await import("pg");
35
+ const { createPgExecutor, createPoolOpener } = await import("../entries/adapters-postgres.js");
36
+ const searchPath = searchPathFor(settings.databaseSchema);
37
+ const pool = new Pool({
38
+ connectionString: settings.databaseUrl,
39
+ ...(settings.connectionTimeoutMillis === undefined
40
+ ? {}
41
+ : { connectionTimeoutMillis: settings.connectionTimeoutMillis }),
42
+ });
43
+ if (searchPath !== undefined) {
44
+ /**
45
+ * Queued on the client, not awaited — which is what makes it correct rather than racy.
46
+ *
47
+ * node-postgres queues queries per client in order, so this `SET` is ahead of whatever the borrower
48
+ * runs next on that same connection. An `await` here would have nothing to attach to: `connect` is
49
+ * an event, and the pool hands the client out regardless of what a listener is still doing.
50
+ *
51
+ * A failure is surfaced rather than swallowed, and it is worth being precise about what it can and
52
+ * cannot catch. It means the role may not *use* the schema. It does **not** mean the schema is
53
+ * missing: `SET search_path TO retinue, public` succeeds when `retinue` does not exist, because a
54
+ * missing entry is skipped rather than rejected, and every write then lands in `public` with no
55
+ * error anywhere. Nothing at this layer can see that — which is why `retinue migrate` creates the
56
+ * schema before anything connects, and why this listener is not the guard against it.
57
+ */
58
+ pool.on("connect", (client) => {
59
+ void client.query(`SET search_path TO ${searchPath}`).catch((error) => {
60
+ client.end();
61
+ pool.emit("error", error instanceof Error
62
+ ? new Error(`could not SET search_path TO ${searchPath}: ${error.message}`, { cause: error })
63
+ : new Error(`could not SET search_path TO ${searchPath}`), client);
64
+ });
65
+ });
66
+ }
67
+ return {
68
+ sql: createPgExecutor(pool),
69
+ open: createPoolOpener(pool, searchPath),
70
+ end: () => pool.end(),
71
+ };
72
+ };
73
+ //# sourceMappingURL=pool.js.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Skills in context — task #244, the interpreter for `AgentManifest.skillPolicy`.
3
+ *
4
+ * `skillPolicy` was declared and read by nothing. That was not one missing line: the *subsystem* was complete and
5
+ * unreachable. `SkillResolver.listCatalog` already takes `{ tenantId, assigned, allowTenantSkills }` — the
6
+ * manifest's two fields verbatim — the store had memory and Postgres adapters under a conformance suite, and
7
+ * `ContextKind`/`ContextBudget` already reserved a `skills` bucket for a section nothing produced. Two things
8
+ * were missing: something to emit that section, and an implementation of `load_skill` (see
9
+ * `tools/registry.ts`).
10
+ *
11
+ * ## Why the catalogue and not the bodies
12
+ *
13
+ * A skill body is up to 20,000 characters (`SKILL_LIMITS.instructionsMaxLength`). Twenty-five of those is a
14
+ * context window. So the section lists **names and descriptions**, and the model calls `load_skill` for the one
15
+ * it wants — the same two-tier shape the tool catalogue uses, for the same reason.
16
+ *
17
+ * ## Origin is `platform`, and the interpolated values are neutralised
18
+ *
19
+ * A skill body may instruct the agent — that is what a skill *is* — so this section is `platform` rather than
20
+ * `external`. But a *tenant-authored* skill's name and description are written by a customer, and this section
21
+ * carries them. Wrapping the whole section in an untrusted envelope would say "nothing in here is an
22
+ * instruction", which is false and would break skills; so the section stays `platform` and neutralises the
23
+ * values it interpolates, which is exactly the case `ContextSection.origin`'s own documentation describes.
24
+ */
25
+ import type { ContextProvider } from "../context/index.js";
26
+ import { type SkillResolver } from "./index.js";
27
+ import type { SkillBodyLoader } from "../tools/registry.js";
28
+ /** The provider id a manifest names in `contextProviderIds`. */
29
+ export declare const SKILL_CATALOGUE_PROVIDER_ID = "skill-catalogue";
30
+ /**
31
+ * Strips what could end a delimited block or forge a heading in the rendered prompt.
32
+ *
33
+ * Narrow on purpose: a skill description is prose a customer wrote, and mangling it would make the catalogue
34
+ * unreadable. Newlines go because the section is one line per skill and a description containing a line break
35
+ * could otherwise invent a row; backticks and the sequences that open a fenced block go because the surrounding
36
+ * render uses them structurally.
37
+ */
38
+ export declare const neutralise: (value: string) => string;
39
+ export declare const createSkillCatalogueProvider: (deps: {
40
+ readonly resolver: SkillResolver;
41
+ readonly policy: {
42
+ readonly assigned: readonly string[];
43
+ readonly allowTenantSkills: boolean;
44
+ };
45
+ }) => ContextProvider;
46
+ /**
47
+ * Adapts a `SkillResolver` to the registry's structural `SkillBodyLoader`.
48
+ *
49
+ * The version is not a parameter. `loadBody` pins to an exact version so a mid-run edit cannot change behaviour,
50
+ * and the version the model should get is the one the catalogue it just read advertised — so it is looked up
51
+ * here rather than trusted from the model's arguments. A model naming a version would be a model choosing which
52
+ * revision of an instruction to follow.
53
+ */
54
+ export declare const createSkillBodyLoader: (deps: {
55
+ readonly resolver: SkillResolver;
56
+ readonly policy: {
57
+ readonly assigned: readonly string[];
58
+ readonly allowTenantSkills: boolean;
59
+ };
60
+ }) => SkillBodyLoader;
61
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Skills in context — task #244, the interpreter for `AgentManifest.skillPolicy`.
3
+ *
4
+ * `skillPolicy` was declared and read by nothing. That was not one missing line: the *subsystem* was complete and
5
+ * unreachable. `SkillResolver.listCatalog` already takes `{ tenantId, assigned, allowTenantSkills }` — the
6
+ * manifest's two fields verbatim — the store had memory and Postgres adapters under a conformance suite, and
7
+ * `ContextKind`/`ContextBudget` already reserved a `skills` bucket for a section nothing produced. Two things
8
+ * were missing: something to emit that section, and an implementation of `load_skill` (see
9
+ * `tools/registry.ts`).
10
+ *
11
+ * ## Why the catalogue and not the bodies
12
+ *
13
+ * A skill body is up to 20,000 characters (`SKILL_LIMITS.instructionsMaxLength`). Twenty-five of those is a
14
+ * context window. So the section lists **names and descriptions**, and the model calls `load_skill` for the one
15
+ * it wants — the same two-tier shape the tool catalogue uses, for the same reason.
16
+ *
17
+ * ## Origin is `platform`, and the interpolated values are neutralised
18
+ *
19
+ * A skill body may instruct the agent — that is what a skill *is* — so this section is `platform` rather than
20
+ * `external`. But a *tenant-authored* skill's name and description are written by a customer, and this section
21
+ * carries them. Wrapping the whole section in an untrusted envelope would say "nothing in here is an
22
+ * instruction", which is false and would break skills; so the section stays `platform` and neutralises the
23
+ * values it interpolates, which is exactly the case `ContextSection.origin`'s own documentation describes.
24
+ */
25
+ import { estimateTokens } from "../core/tokens.js";
26
+ import { SKILL_LIMITS } from "./index.js";
27
+ /** The provider id a manifest names in `contextProviderIds`. */
28
+ export const SKILL_CATALOGUE_PROVIDER_ID = "skill-catalogue";
29
+ /**
30
+ * Strips what could end a delimited block or forge a heading in the rendered prompt.
31
+ *
32
+ * Narrow on purpose: a skill description is prose a customer wrote, and mangling it would make the catalogue
33
+ * unreadable. Newlines go because the section is one line per skill and a description containing a line break
34
+ * could otherwise invent a row; backticks and the sequences that open a fenced block go because the surrounding
35
+ * render uses them structurally.
36
+ */
37
+ export const neutralise = (value) => value
38
+ .replace(/[\r\n]+/g, " ")
39
+ .replace(/```+/g, "'''")
40
+ .replace(/[`]/g, "'")
41
+ .trim();
42
+ export const createSkillCatalogueProvider = (deps) => ({
43
+ id: SKILL_CATALOGUE_PROVIDER_ID,
44
+ async provide(context) {
45
+ const entries = await deps.resolver.listCatalog({
46
+ tenantId: context.tenantId,
47
+ assigned: deps.policy.assigned,
48
+ allowTenantSkills: deps.policy.allowTenantSkills,
49
+ });
50
+ // No section at all rather than an empty one. A heading saying "Skills" over nothing tells the model it has
51
+ // a capability and then shows it none, which is worse than silence — and it would still cost tokens.
52
+ if (entries.length === 0)
53
+ return [];
54
+ const lines = entries.map((e) => `- \`${neutralise(e.name)}\` — ${neutralise(e.description)}`);
55
+ const body = [
56
+ `You have ${entries.length} skill${entries.length === 1 ? "" : "s"} available. Each is a set of`,
57
+ "instructions for a particular kind of task. Read the list, and when one applies call",
58
+ "`load_skill` with its name to get the full instructions before you start.",
59
+ `You may load at most ${SKILL_LIMITS.maxLoadedPerRun} in a single run, so choose.`,
60
+ "",
61
+ ...lines,
62
+ ].join("\n");
63
+ return [
64
+ {
65
+ providerId: SKILL_CATALOGUE_PROVIDER_ID,
66
+ title: "Skills",
67
+ body,
68
+ // Above ordinary user context: a skill changes *how* the agent works, so it should survive pruning
69
+ // longer than the material it works on.
70
+ priority: 70,
71
+ estimatedTokens: estimateTokens(body),
72
+ provenance: "skill catalogue",
73
+ sensitivity: "internal",
74
+ // See the header: the section may instruct, and the customer-authored values inside it are neutralised
75
+ // rather than the whole section being disclaimed.
76
+ origin: "platform",
77
+ cacheable: true,
78
+ kind: "skills",
79
+ },
80
+ ];
81
+ },
82
+ });
83
+ /**
84
+ * Adapts a `SkillResolver` to the registry's structural `SkillBodyLoader`.
85
+ *
86
+ * The version is not a parameter. `loadBody` pins to an exact version so a mid-run edit cannot change behaviour,
87
+ * and the version the model should get is the one the catalogue it just read advertised — so it is looked up
88
+ * here rather than trusted from the model's arguments. A model naming a version would be a model choosing which
89
+ * revision of an instruction to follow.
90
+ */
91
+ export const createSkillBodyLoader = (deps) => ({
92
+ async load(context, name) {
93
+ const entries = await deps.resolver.listCatalog({
94
+ tenantId: context.tenantId,
95
+ assigned: deps.policy.assigned,
96
+ allowTenantSkills: deps.policy.allowTenantSkills,
97
+ });
98
+ // Resolved against the catalogue *this* agent may see, so `assigned` and `allowTenantSkills` gate loading
99
+ // and not merely listing. A policy that filtered the list but not the load would be no policy at all.
100
+ const entry = entries.find((e) => e.name === name);
101
+ if (entry === undefined)
102
+ return null;
103
+ const version = await deps.resolver.loadBody({ tenantId: context.tenantId, name, version: entry.version });
104
+ return { name: version.name, version: version.version, instructions: version.instructions };
105
+ },
106
+ });
107
+ //# sourceMappingURL=context.js.map
@@ -64,5 +64,6 @@ export interface SkillResolver {
64
64
  }): Promise<SkillVersion>;
65
65
  }
66
66
  export * from "./catalogue.js";
67
+ export * from "./context.js";
67
68
  export * from "./resolver.js";
68
69
  //# sourceMappingURL=index.d.ts.map
@@ -28,5 +28,6 @@ export const SKILL_LIMITS = {
28
28
  maxLoadedPerRun: 5,
29
29
  };
30
30
  export * from "./catalogue.js";
31
+ export * from "./context.js";
31
32
  export * from "./resolver.js";
32
33
  //# sourceMappingURL=index.js.map
@@ -36,6 +36,11 @@ export const SPAN_FOR_RUN_EVENT = {
36
36
  "question.answered": "hitl.question",
37
37
  "approval.requested": "hitl.approval",
38
38
  "approval.decided": "hitl.approval",
39
+ // Its own span rather than `hitl.approval`: both pause a run for a person, and the questions they answer are
40
+ // different — "may I do this" versus "I have no access, grant me some" — so a trace that merged them would
41
+ // make "how often do runs stall on a missing connection" unanswerable.
42
+ "connection.requested": "hitl.connection",
43
+ "connection.completed": "hitl.connection",
39
44
  "usage.updated": "run.step",
40
45
  "context.compacted": "context.compact",
41
46
  // Its own span, not `run.step`: a guardrail verdict is the boundary of a decision somebody will need to find
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `ArtifactExportStore` conformance (#134).
3
+ *
4
+ * Almost entirely about `claim`, because that is the method whose contract two adapters can plausibly get
5
+ * differently and where differing is expensive: a claim that is not exclusive renders the same PDF twice and
6
+ * leaves two rows pointing at two identical files, and a caller has no way to tell which is canonical.
7
+ */
8
+ import type { ArtifactId, ConversationId, TenantId } from "../../core/ids.js";
9
+ import type { ArtifactExportStore } from "../../persistence/index.js";
10
+ import { type Fixture, type FixtureOrStore } from "./parents.js";
11
+ /** The parent an adapter with a foreign key needs: an artifact, which itself needs a conversation. */
12
+ export type ExportFixture = Fixture<ArtifactExportStore> & {
13
+ readonly seedArtifact?: (input: {
14
+ readonly tenantId: TenantId;
15
+ readonly artifactId: ArtifactId;
16
+ readonly conversationId: ConversationId;
17
+ }) => Promise<void>;
18
+ };
19
+ export declare function artifactExportStoreConformance(make: () => FixtureOrStore<ArtifactExportStore> | Promise<FixtureOrStore<ArtifactExportStore>>): void;
20
+ //# sourceMappingURL=artifact-exports.d.ts.map
@@ -0,0 +1,172 @@
1
+ /**
2
+ * `ArtifactExportStore` conformance (#134).
3
+ *
4
+ * Almost entirely about `claim`, because that is the method whose contract two adapters can plausibly get
5
+ * differently and where differing is expensive: a claim that is not exclusive renders the same PDF twice and
6
+ * leaves two rows pointing at two identical files, and a caller has no way to tell which is canonical.
7
+ */
8
+ import { describe, expect, it } from "vitest";
9
+ import { asId } from "../../core/ids.js";
10
+ import { openFixture } from "./parents.js";
11
+ const T1 = asId("conf-export-tenant-1");
12
+ const T2 = asId("conf-export-tenant-2");
13
+ const A1 = asId("conf-export-artifact-1");
14
+ const AT = "2026-08-23T12:00:00.000Z";
15
+ const USER = asId("conf-export-user");
16
+ const requested = (overrides = {}) => ({
17
+ id: overrides.id ?? "e1",
18
+ artifactId: A1,
19
+ version: overrides.version ?? 1,
20
+ format: overrides.format ?? "pdf",
21
+ requestedBy: USER,
22
+ createdAt: AT,
23
+ });
24
+ export function artifactExportStoreConformance(make) {
25
+ describe("ArtifactExportStore conformance", () => {
26
+ const open = async () => {
27
+ const fixture = openFixture(await make());
28
+ // Both tenants, because every isolation case below needs the parent to exist on both sides — otherwise
29
+ // a cross-tenant write would fail on the foreign key and look like isolation working.
30
+ for (const tenantId of [T1, T2]) {
31
+ await fixture.seedArtifact?.({
32
+ tenantId,
33
+ artifactId: A1,
34
+ conversationId: asId("conf-export-convo-1"),
35
+ });
36
+ }
37
+ return fixture.store;
38
+ };
39
+ it("claims a slot and records it pending", async () => {
40
+ const store = await open();
41
+ const result = await store.claim({ tenantId: T1, export: requested() });
42
+ expect(result.claimed).toBe(true);
43
+ expect(result.export).toMatchObject({ id: "e1", version: 1, format: "pdf", state: "pending" });
44
+ });
45
+ it("lets exactly one of two racing claims win, and hands the loser the winner's row", async () => {
46
+ // The property the whole port exists for. Both callers then read the same row, which is why the loser
47
+ // gets it rather than an error: its next move is identical either way.
48
+ const store = await open();
49
+ const results = await Promise.all([
50
+ store.claim({ tenantId: T1, export: requested({ id: "race-a" }) }),
51
+ store.claim({ tenantId: T1, export: requested({ id: "race-b" }) }),
52
+ ]);
53
+ expect(results.filter((r) => r.claimed)).toHaveLength(1);
54
+ const winner = results.find((r) => r.claimed).export;
55
+ const loser = results.find((r) => !r.claimed).export;
56
+ expect(loser.id).toBe(winner.id);
57
+ });
58
+ it("treats a different format as a different slot", async () => {
59
+ const store = await open();
60
+ await store.claim({ tenantId: T1, export: requested({ id: "e1", format: "pdf" }) });
61
+ const second = await store.claim({ tenantId: T1, export: requested({ id: "e2", format: "markdown" }) });
62
+ expect(second.claimed).toBe(true);
63
+ });
64
+ it("treats a different version as a different slot", async () => {
65
+ // An export is of a *version*. Sharing one across versions would hand someone last week's document.
66
+ const store = await open();
67
+ await store.claim({ tenantId: T1, export: requested({ id: "e1", version: 1 }) });
68
+ const second = await store.claim({ tenantId: T1, export: requested({ id: "e2", version: 2 }) });
69
+ expect(second.claimed).toBe(true);
70
+ });
71
+ it("does not share a slot across tenants", async () => {
72
+ const store = await open();
73
+ await store.claim({ tenantId: T1, export: requested({ id: "e1" }) });
74
+ const other = await store.claim({ tenantId: T2, export: requested({ id: "e2" }) });
75
+ expect(other.claimed).toBe(true);
76
+ expect(await store.get({ tenantId: T2, id: "e1" })).toBeNull();
77
+ });
78
+ it("records a rendered outcome with its file and checksum", async () => {
79
+ const store = await open();
80
+ await store.claim({ tenantId: T1, export: requested() });
81
+ expect(await store.complete({
82
+ tenantId: T1,
83
+ id: "e1",
84
+ state: "rendered",
85
+ fileId: asId("file-1"),
86
+ byteSize: 2048,
87
+ checksum: "abc",
88
+ at: AT,
89
+ })).toEqual({ recorded: true });
90
+ expect(await store.get({ tenantId: T1, id: "e1" })).toMatchObject({
91
+ state: "rendered",
92
+ fileId: "file-1",
93
+ byteSize: 2048,
94
+ checksum: "abc",
95
+ renderedAt: AT,
96
+ });
97
+ });
98
+ it("records a failure with its reason and message", async () => {
99
+ // AC-4 depends on both surviving storage: the reason drives behaviour and the message is what the user
100
+ // reads, so a store keeping one and dropping the other would half-work.
101
+ const store = await open();
102
+ await store.claim({ tenantId: T1, export: requested() });
103
+ await store.complete({
104
+ tenantId: T1,
105
+ id: "e1",
106
+ state: "failed",
107
+ failureReason: "render-failed",
108
+ failureMessage: "That artifact could not be rendered.",
109
+ at: AT,
110
+ });
111
+ expect(await store.get({ tenantId: T1, id: "e1" })).toMatchObject({
112
+ state: "failed",
113
+ failureReason: "render-failed",
114
+ failureMessage: "That artifact could not be rendered.",
115
+ });
116
+ });
117
+ it("does not complete another tenant's export", async () => {
118
+ const store = await open();
119
+ await store.claim({ tenantId: T1, export: requested() });
120
+ // Reported rather than thrown: it is the same answer a deleted row gives, and a worker must be able to
121
+ // tell "gone" from "broken".
122
+ expect(await store.complete({ tenantId: T2, id: "e1", state: "rendered", fileId: asId("f"), at: AT })).toEqual({ recorded: false });
123
+ expect(await store.get({ tenantId: T1, id: "e1" })).toMatchObject({ state: "pending" });
124
+ });
125
+ it("finds an export by version and format, which is the cache lookup", async () => {
126
+ const store = await open();
127
+ await store.claim({ tenantId: T1, export: requested({ id: "e1" }) });
128
+ expect(await store.find({ tenantId: T1, artifactId: A1, version: 1, format: "pdf" })).toMatchObject({
129
+ id: "e1",
130
+ });
131
+ expect(await store.find({ tenantId: T1, artifactId: A1, version: 1, format: "markdown" })).toBeNull();
132
+ expect(await store.find({ tenantId: T2, artifactId: A1, version: 1, format: "pdf" })).toBeNull();
133
+ });
134
+ it("lists an artifact's exports, paging on a keyset cursor", async () => {
135
+ // Two formats requested together share a timestamp, so a cursor on the timestamp alone would skip or
136
+ // repeat -- which is the normal case here rather than a rare one.
137
+ const store = await open();
138
+ for (const n of [1, 2, 3, 4, 5]) {
139
+ await store.claim({
140
+ tenantId: T1,
141
+ export: { ...requested({ id: `e${n}`, version: n }), createdAt: AT },
142
+ });
143
+ }
144
+ const seen = [];
145
+ let cursor;
146
+ do {
147
+ const page = await store.listByArtifact({
148
+ tenantId: T1,
149
+ artifactId: A1,
150
+ limit: 2,
151
+ ...(cursor === undefined ? {} : { cursor }),
152
+ });
153
+ seen.push(...page.items.map((e) => e.id));
154
+ cursor = page.nextCursor;
155
+ } while (cursor !== undefined);
156
+ expect(seen.sort()).toEqual(["e1", "e2", "e3", "e4", "e5"]);
157
+ });
158
+ it("lists nothing for another tenant", async () => {
159
+ const store = await open();
160
+ await store.claim({ tenantId: T1, export: requested() });
161
+ expect((await store.listByArtifact({ tenantId: T2, artifactId: A1, limit: 10 })).items).toEqual([]);
162
+ });
163
+ it("rejects a non-timestamp `at` rather than storing it", async () => {
164
+ // The lesson from #129: a reference adapter laxer than the real one turns a production write failure
165
+ // into a passing test.
166
+ const store = await open();
167
+ await store.claim({ tenantId: T1, export: requested() });
168
+ await expect(store.complete({ tenantId: T1, id: "e1", state: "rendered", fileId: asId("f"), at: "t" })).rejects.toThrow();
169
+ });
170
+ });
171
+ }
172
+ //# sourceMappingURL=artifact-exports.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `ArtifactStore` conformance (#133) — AC-6.
3
+ *
4
+ * The cases are mostly about **versioning**, because that is the part where two adapters can plausibly
5
+ * disagree and where disagreeing is expensive: an earlier version that stops resolving is a shared link that
6
+ * 404s, and a race that silently collapses two regenerations into one is data loss that looks like success.
7
+ */
8
+ import type { ArtifactStore } from "../../persistence/index.js";
9
+ import { type FixtureOrStore } from "./parents.js";
10
+ export declare function artifactStoreConformance(make: () => FixtureOrStore<ArtifactStore> | Promise<FixtureOrStore<ArtifactStore>>): void;
11
+ //# sourceMappingURL=artifacts.d.ts.map