@retinue/agentkit 0.2.0 → 0.3.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 (204) 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 +19 -0
  107. package/dist/server/bin.js +226 -0
  108. package/dist/server/boot.d.ts +20 -2
  109. package/dist/server/boot.js +29 -3
  110. package/dist/server/cli.d.ts +13 -0
  111. package/dist/server/cli.js +7 -4
  112. package/dist/server/doctor.d.ts +74 -0
  113. package/dist/server/doctor.js +280 -0
  114. package/dist/skills/context.d.ts +61 -0
  115. package/dist/skills/context.js +107 -0
  116. package/dist/skills/index.d.ts +1 -0
  117. package/dist/skills/index.js +1 -0
  118. package/dist/telemetry/spans.js +5 -0
  119. package/dist/testing/conformance/artifact-exports.d.ts +20 -0
  120. package/dist/testing/conformance/artifact-exports.js +172 -0
  121. package/dist/testing/conformance/artifacts.d.ts +11 -0
  122. package/dist/testing/conformance/artifacts.js +316 -0
  123. package/dist/testing/conformance/audio.d.ts +38 -0
  124. package/dist/testing/conformance/audio.js +145 -0
  125. package/dist/testing/conformance/capability.d.ts +23 -0
  126. package/dist/testing/conformance/capability.js +27 -0
  127. package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
  128. package/dist/testing/conformance/checkpoint-store.js +72 -0
  129. package/dist/testing/conformance/connections.d.ts +14 -0
  130. package/dist/testing/conformance/connections.js +171 -0
  131. package/dist/testing/conformance/conversation-store.d.ts +10 -0
  132. package/dist/testing/conformance/conversation-store.js +60 -0
  133. package/dist/testing/conformance/evaluation.d.ts +10 -0
  134. package/dist/testing/conformance/evaluation.js +295 -0
  135. package/dist/testing/conformance/files.d.ts +19 -0
  136. package/dist/testing/conformance/files.js +454 -0
  137. package/dist/testing/conformance/flows.d.ts +16 -0
  138. package/dist/testing/conformance/flows.js +193 -0
  139. package/dist/testing/conformance/graph.d.ts +22 -0
  140. package/dist/testing/conformance/graph.js +500 -0
  141. package/dist/testing/conformance/hitl.d.ts +25 -0
  142. package/dist/testing/conformance/hitl.js +523 -0
  143. package/dist/testing/conformance/index.d.ts +163 -0
  144. package/dist/testing/conformance/index.js +321 -0
  145. package/dist/testing/conformance/invariants.d.ts +23 -0
  146. package/dist/testing/conformance/invariants.js +80 -0
  147. package/dist/testing/conformance/knowledge.d.ts +41 -0
  148. package/dist/testing/conformance/knowledge.js +592 -0
  149. package/dist/testing/conformance/parents.d.ts +50 -0
  150. package/dist/testing/conformance/parents.js +39 -0
  151. package/dist/testing/conformance/rate-limit.d.ts +25 -0
  152. package/dist/testing/conformance/rate-limit.js +71 -0
  153. package/dist/testing/conformance/records.d.ts +36 -0
  154. package/dist/testing/conformance/records.js +400 -0
  155. package/dist/testing/conformance/rollups.d.ts +30 -0
  156. package/dist/testing/conformance/rollups.js +425 -0
  157. package/dist/testing/conformance/run-coordinator.d.ts +27 -0
  158. package/dist/testing/conformance/run-coordinator.js +150 -0
  159. package/dist/testing/conformance/run-event-log.d.ts +9 -0
  160. package/dist/testing/conformance/run-event-log.js +138 -0
  161. package/dist/testing/conformance/run-store.d.ts +12 -0
  162. package/dist/testing/conformance/run-store.js +294 -0
  163. package/dist/testing/conformance/session-state.d.ts +38 -0
  164. package/dist/testing/conformance/session-state.js +198 -0
  165. package/dist/testing/conformance/usage-limits.d.ts +10 -0
  166. package/dist/testing/conformance/usage-limits.js +309 -0
  167. package/dist/testing/conformance.d.ts +8 -0
  168. package/dist/testing/conformance.js +8 -0
  169. package/dist/testing/memory-backend.d.ts +39 -0
  170. package/dist/testing/memory-backend.js +41 -0
  171. package/dist/testing/pglite.d.ts +55 -0
  172. package/dist/testing/pglite.js +78 -0
  173. package/dist/testing/stub-model.d.ts +58 -0
  174. package/dist/testing/stub-model.js +71 -0
  175. package/dist/testing/supabase-storage-double.d.ts +35 -0
  176. package/dist/testing/supabase-storage-double.js +101 -0
  177. package/dist/toolkit/http.d.ts +27 -0
  178. package/dist/toolkit/http.js +48 -0
  179. package/dist/toolkit/index.d.ts +27 -1
  180. package/dist/toolkit/index.js +24 -1
  181. package/dist/toolkit/mime.d.ts +97 -0
  182. package/dist/toolkit/mime.js +233 -0
  183. package/dist/toolkit/ssrf.d.ts +139 -0
  184. package/dist/toolkit/ssrf.js +349 -0
  185. package/dist/toolkit/vendor.d.ts +91 -0
  186. package/dist/toolkit/vendor.js +153 -0
  187. package/dist/tools/credentials.d.ts +265 -6
  188. package/dist/tools/credentials.js +318 -3
  189. package/dist/tools/define.d.ts +14 -0
  190. package/dist/tools/define.js +3 -0
  191. package/dist/tools/index.d.ts +21 -0
  192. package/dist/tools/index.js +7 -0
  193. package/dist/tools/library/audio.d.ts +63 -0
  194. package/dist/tools/library/audio.js +148 -0
  195. package/dist/tools/library/index.d.ts +8 -0
  196. package/dist/tools/library/index.js +7 -0
  197. package/dist/tools/registry.d.ts +33 -0
  198. package/dist/tools/registry.js +72 -2
  199. package/dist/usage/index.d.ts +21 -0
  200. package/dist/usage/index.js +1 -0
  201. package/dist/usage/rate-limit.d.ts +122 -0
  202. package/dist/usage/rate-limit.js +131 -0
  203. package/dist/usage/recorder.js +3 -0
  204. package/package.json +29 -2
@@ -0,0 +1,55 @@
1
+ /**
2
+ * A shared PGlite instance with a fresh schema per caller.
3
+ *
4
+ * **Why.** Every test used to boot its own embedded Postgres, and boot is essentially the entire cost:
5
+ * measured on this machine, `new PGlite()` plus a first query is **432ms warm**, while running all
6
+ * eleven migrations is **20ms** — and creating a second schema and migrating into it inside an
7
+ * already-booted instance is also **20ms**. Boot dominates by more than twenty to one.
8
+ *
9
+ * The conformance entrypoints call their store factory once per test case, so between them they were
10
+ * booting roughly 250 embedded databases per run. That is the largest single cost in CI, and it grew
11
+ * with every port added — the `build` job roughly tripled over #98–#111.
12
+ *
13
+ * **Isolation is by schema, not by instance.** Each caller gets `CREATE SCHEMA` and an executor that
14
+ * sets `search_path` on every query, because the connection is shared and another caller's schema may
15
+ * be current by the time a query runs. Two in-process round trips instead of one is nothing next to a
16
+ * 432ms boot.
17
+ *
18
+ * **What this does not isolate**, and the reason some test files deliberately keep their own instance:
19
+ * connection-level state. Roles (`CREATE ROLE app_user`), `SET ROLE`, and session GUCs are properties
20
+ * of the connection, not the schema — so a file that creates a role per test would collide on the
21
+ * second one. The row-level-security tests are exactly that shape and are left alone on purpose.
22
+ *
23
+ * Also note: `information_schema` queries must filter on `table_schema = current_schema()` here. With
24
+ * one schema per instance an unqualified `WHERE table_name = 'runs'` matched one row; with many schemas
25
+ * in one instance it matches one per schema.
26
+ */
27
+ import { PGlite } from "@electric-sql/pglite";
28
+ import type { SqlExecutor } from "../adapters/postgres/sql.js";
29
+ /**
30
+ * An executor pinned to `schema`.
31
+ *
32
+ * `search_path` is set per query rather than once, because the connection is shared: between two
33
+ * queries from this executor, another caller's executor may have pointed the path at its own schema.
34
+ * Setting it once at creation would make every test's isolation depend on execution order.
35
+ *
36
+ * `public` is deliberately **absent** from the path. With it present, `CREATE TABLE IF NOT EXISTS`
37
+ * would see a table of the same name in `public` and skip creating it here — so a stray table in
38
+ * `public` would silently make every later schema share it.
39
+ */
40
+ export declare const schemaExecutor: (db: PGlite, schema: string) => SqlExecutor;
41
+ export type PgliteSchema = {
42
+ readonly sql: SqlExecutor;
43
+ readonly db: PGlite;
44
+ readonly schema: string;
45
+ };
46
+ /** A migrated, isolated schema on the shared instance. Lazily boots the instance on first use. */
47
+ export declare const freshPgliteSchema: () => Promise<PgliteSchema>;
48
+ /**
49
+ * A migrated schema, created lazily on first query so a caller can build stores synchronously.
50
+ *
51
+ * The conformance harnesses call their factory inside each test and expect a usable executor back
52
+ * immediately, so the work has to be deferred to the first query rather than done up front.
53
+ */
54
+ export declare const lazyPgliteSchema: () => SqlExecutor;
55
+ //# sourceMappingURL=pglite.d.ts.map
@@ -0,0 +1,78 @@
1
+ /**
2
+ * A shared PGlite instance with a fresh schema per caller.
3
+ *
4
+ * **Why.** Every test used to boot its own embedded Postgres, and boot is essentially the entire cost:
5
+ * measured on this machine, `new PGlite()` plus a first query is **432ms warm**, while running all
6
+ * eleven migrations is **20ms** — and creating a second schema and migrating into it inside an
7
+ * already-booted instance is also **20ms**. Boot dominates by more than twenty to one.
8
+ *
9
+ * The conformance entrypoints call their store factory once per test case, so between them they were
10
+ * booting roughly 250 embedded databases per run. That is the largest single cost in CI, and it grew
11
+ * with every port added — the `build` job roughly tripled over #98–#111.
12
+ *
13
+ * **Isolation is by schema, not by instance.** Each caller gets `CREATE SCHEMA` and an executor that
14
+ * sets `search_path` on every query, because the connection is shared and another caller's schema may
15
+ * be current by the time a query runs. Two in-process round trips instead of one is nothing next to a
16
+ * 432ms boot.
17
+ *
18
+ * **What this does not isolate**, and the reason some test files deliberately keep their own instance:
19
+ * connection-level state. Roles (`CREATE ROLE app_user`), `SET ROLE`, and session GUCs are properties
20
+ * of the connection, not the schema — so a file that creates a role per test would collide on the
21
+ * second one. The row-level-security tests are exactly that shape and are left alone on purpose.
22
+ *
23
+ * Also note: `information_schema` queries must filter on `table_schema = current_schema()` here. With
24
+ * one schema per instance an unqualified `WHERE table_name = 'runs'` matched one row; with many schemas
25
+ * in one instance it matches one per schema.
26
+ */
27
+ import { PGlite } from "@electric-sql/pglite";
28
+ import { migrate } from "../adapters/postgres/migrations.js";
29
+ /**
30
+ * One instance per module, which under vitest means one per test file — files run in separate workers,
31
+ * so this is not shared across them.
32
+ */
33
+ let shared = null;
34
+ let schemas = 0;
35
+ const instance = () => (shared ??= new PGlite());
36
+ /**
37
+ * An executor pinned to `schema`.
38
+ *
39
+ * `search_path` is set per query rather than once, because the connection is shared: between two
40
+ * queries from this executor, another caller's executor may have pointed the path at its own schema.
41
+ * Setting it once at creation would make every test's isolation depend on execution order.
42
+ *
43
+ * `public` is deliberately **absent** from the path. With it present, `CREATE TABLE IF NOT EXISTS`
44
+ * would see a table of the same name in `public` and skip creating it here — so a stray table in
45
+ * `public` would silently make every later schema share it.
46
+ */
47
+ export const schemaExecutor = (db, schema) => ({
48
+ async query(text, params) {
49
+ await db.exec(`SET search_path TO ${schema}`);
50
+ const result = await db.query(text, params ? [...params] : undefined);
51
+ return result.rows;
52
+ },
53
+ });
54
+ /** A migrated, isolated schema on the shared instance. Lazily boots the instance on first use. */
55
+ export const freshPgliteSchema = async () => {
56
+ const db = instance();
57
+ const schema = `s${(schemas += 1)}`;
58
+ await db.exec(`CREATE SCHEMA ${schema}`);
59
+ const sql = schemaExecutor(db, schema);
60
+ await migrate(sql);
61
+ return { sql, db, schema };
62
+ };
63
+ /**
64
+ * A migrated schema, created lazily on first query so a caller can build stores synchronously.
65
+ *
66
+ * The conformance harnesses call their factory inside each test and expect a usable executor back
67
+ * immediately, so the work has to be deferred to the first query rather than done up front.
68
+ */
69
+ export const lazyPgliteSchema = () => {
70
+ let ready = null;
71
+ const init = () => (ready ??= freshPgliteSchema().then((created) => created.sql));
72
+ return {
73
+ async query(text, params) {
74
+ return (await init()).query(text, params);
75
+ },
76
+ };
77
+ };
78
+ //# sourceMappingURL=pglite.js.map
@@ -0,0 +1,58 @@
1
+ /**
2
+ * A scripted model, for testing an agent without calling a provider — task #253 AC-4.
3
+ *
4
+ * Testing an agent currently means writing a `streamTurn` by hand, and every consumer writes the same one badly:
5
+ * a single `text-delta` and a `finish`, which exercises none of the paths that actually break. The interesting
6
+ * behaviour of this platform — retry, approval gates, tool errors, structured output, the catalogue budget — all
7
+ * live in what the model does *across steps*, and a one-chunk fake cannot express any of it.
8
+ *
9
+ * ## Scripted per turn, not per call
10
+ *
11
+ * A script is a list of **turns**, and each call consumes the next one. That is what lets a test say "the model
12
+ * calls the tool, sees the result, then answers" — three chunks in one turn — separately from "the first turn
13
+ * fails and the second succeeds", which is two turns and is how a retry test is written.
14
+ *
15
+ * Running past the end of the script is an **error**, not a silent empty turn. A test whose agent took one more
16
+ * turn than the author expected should fail loudly, because the alternative is an assertion passing against a
17
+ * turn that produced nothing.
18
+ */
19
+ import { AgentPlatformError } from "../core/errors.js";
20
+ import type { ModelTurnRequest, NeutralStreamChunk, NeutralUsage } from "../models/streaming.js";
21
+ /** What the model does on one turn. */
22
+ export type ScriptedTurn =
23
+ /** Answers with text. */
24
+ {
25
+ readonly say: string;
26
+ readonly usage?: Partial<NeutralUsage>;
27
+ }
28
+ /** Calls tools, then answers — the shape almost every interesting test needs. */
29
+ | {
30
+ readonly call: readonly {
31
+ readonly tool: string;
32
+ readonly input?: unknown;
33
+ readonly id?: string;
34
+ }[];
35
+ readonly then?: string;
36
+ readonly usage?: Partial<NeutralUsage>;
37
+ }
38
+ /**
39
+ * Fails, as a provider would.
40
+ *
41
+ * `retryable` defaults to **true**, because the case worth testing is the retry path — a non-retryable failure
42
+ * is just a thrown error and needs no scripting to produce.
43
+ */
44
+ | {
45
+ readonly fail: string;
46
+ readonly code?: AgentPlatformError["code"];
47
+ readonly retryable?: boolean;
48
+ };
49
+ export type StubModel = {
50
+ /** Pass as `DefaultEngineDeps.streamTurn`. */
51
+ readonly streamTurn: (request: ModelTurnRequest) => AsyncIterable<NeutralStreamChunk>;
52
+ /** Every request the engine made, in order — so a test can assert what the model was *given*. */
53
+ readonly requests: readonly ModelTurnRequest[];
54
+ /** Turns consumed so far. */
55
+ readonly turns: () => number;
56
+ };
57
+ export declare const createStubModel: (script: readonly ScriptedTurn[]) => StubModel;
58
+ //# sourceMappingURL=stub-model.d.ts.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * A scripted model, for testing an agent without calling a provider — task #253 AC-4.
3
+ *
4
+ * Testing an agent currently means writing a `streamTurn` by hand, and every consumer writes the same one badly:
5
+ * a single `text-delta` and a `finish`, which exercises none of the paths that actually break. The interesting
6
+ * behaviour of this platform — retry, approval gates, tool errors, structured output, the catalogue budget — all
7
+ * live in what the model does *across steps*, and a one-chunk fake cannot express any of it.
8
+ *
9
+ * ## Scripted per turn, not per call
10
+ *
11
+ * A script is a list of **turns**, and each call consumes the next one. That is what lets a test say "the model
12
+ * calls the tool, sees the result, then answers" — three chunks in one turn — separately from "the first turn
13
+ * fails and the second succeeds", which is two turns and is how a retry test is written.
14
+ *
15
+ * Running past the end of the script is an **error**, not a silent empty turn. A test whose agent took one more
16
+ * turn than the author expected should fail loudly, because the alternative is an assertion passing against a
17
+ * turn that produced nothing.
18
+ */
19
+ import { AgentPlatformError } from "../core/errors.js";
20
+ const usageOf = (partial) => ({
21
+ inputTokens: 10,
22
+ outputTokens: 5,
23
+ cachedInputTokens: 0,
24
+ ...partial,
25
+ });
26
+ export const createStubModel = (script) => {
27
+ const requests = [];
28
+ let index = 0;
29
+ async function* run(request) {
30
+ requests.push(request);
31
+ const turn = script[index];
32
+ index += 1;
33
+ if (turn === undefined) {
34
+ // Loud, not empty. An agent that took one more turn than the test expected is a finding, and an empty
35
+ // turn would let an assertion pass against a model that said nothing.
36
+ throw new AgentPlatformError({
37
+ code: "internal",
38
+ message: `the stub model's script has ${script.length} turn(s) and the agent asked for turn ${index}. ` +
39
+ "Either the agent is looping, or the script is short — both are worth knowing.",
40
+ retryable: false,
41
+ });
42
+ }
43
+ if ("fail" in turn) {
44
+ throw new AgentPlatformError({
45
+ code: turn.code ?? "provider_error",
46
+ message: turn.fail,
47
+ retryable: turn.retryable ?? true,
48
+ });
49
+ }
50
+ if ("say" in turn) {
51
+ yield { type: "text-delta", id: `t${index}`, text: turn.say };
52
+ yield { type: "finish", usage: usageOf(turn.usage) };
53
+ return;
54
+ }
55
+ for (const [n, call] of turn.call.entries()) {
56
+ const id = call.id ?? `call-${index}-${n}`;
57
+ yield { type: "tool-call", toolCallId: id, toolName: call.tool, input: call.input ?? {} };
58
+ /**
59
+ * No `tool-result` is emitted here, deliberately.
60
+ *
61
+ * The engine runs the tool itself and produces the result — that is the path under test. A stub that
62
+ * emitted its own result would test nothing but the stub, and would hide an unwired tool entirely.
63
+ */
64
+ }
65
+ if (turn.then !== undefined)
66
+ yield { type: "text-delta", id: `t${index}`, text: turn.then };
67
+ yield { type: "finish", usage: usageOf(turn.usage) };
68
+ }
69
+ return { streamTurn: run, requests, turns: () => index };
70
+ };
71
+ //# sourceMappingURL=stub-model.js.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * An in-process Supabase Storage, for the `FileContentStore` adapter (#129).
3
+ *
4
+ * Lives here rather than beside one test because two suites need it: `supabase-conformance.test.ts` is the
5
+ * single conformance entrypoint the matrix generator reads, and `supabase-storage.test.ts` holds the cases
6
+ * only an HTTP store has. A double copied into both would drift, and a drifting double is a suite that
7
+ * proves two different things while appearing to prove one.
8
+ *
9
+ * Deliberately strict about what the adapter must get right: it requires the bearer token, it refuses an
10
+ * upload to an existing path unless `x-upsert` says otherwise, and it 404s an unknown object rather than
11
+ * returning an empty body. A lenient double would let every one of those bugs through to production.
12
+ *
13
+ * It can only prove the adapter is self-consistent about the API it *believes* Supabase has. What it proves
14
+ * well is everything on this side of the wire — URL construction, status handling, tenant prefixing — which
15
+ * is where the tenant boundary lives.
16
+ */
17
+ import type { FileContentStore } from "../persistence/index.js";
18
+ export type RecordedCall = {
19
+ readonly method: string;
20
+ readonly url: string;
21
+ readonly headers: Record<string, string>;
22
+ };
23
+ export declare const STORAGE_BUCKET = "attachments";
24
+ export declare const STORAGE_URL = "https://project.supabase.test";
25
+ export declare const STORAGE_KEY = "service-role-key";
26
+ /** The adapter over the double, plus the double's innards for the assertions that need them. */
27
+ export declare const supabaseStorageDouble: () => {
28
+ readonly store: FileContentStore;
29
+ readonly objects: Map<string, {
30
+ bytes: Uint8Array;
31
+ mediaType: string;
32
+ }>;
33
+ readonly calls: RecordedCall[];
34
+ };
35
+ //# sourceMappingURL=supabase-storage-double.d.ts.map
@@ -0,0 +1,101 @@
1
+ /**
2
+ * An in-process Supabase Storage, for the `FileContentStore` adapter (#129).
3
+ *
4
+ * Lives here rather than beside one test because two suites need it: `supabase-conformance.test.ts` is the
5
+ * single conformance entrypoint the matrix generator reads, and `supabase-storage.test.ts` holds the cases
6
+ * only an HTTP store has. A double copied into both would drift, and a drifting double is a suite that
7
+ * proves two different things while appearing to prove one.
8
+ *
9
+ * Deliberately strict about what the adapter must get right: it requires the bearer token, it refuses an
10
+ * upload to an existing path unless `x-upsert` says otherwise, and it 404s an unknown object rather than
11
+ * returning an empty body. A lenient double would let every one of those bugs through to production.
12
+ *
13
+ * It can only prove the adapter is self-consistent about the API it *believes* Supabase has. What it proves
14
+ * well is everything on this side of the wire — URL construction, status handling, tenant prefixing — which
15
+ * is where the tenant boundary lives.
16
+ */
17
+ import { createSupabaseStorageFileContentStore } from "../adapters/supabase/storage.js";
18
+ export const STORAGE_BUCKET = "attachments";
19
+ export const STORAGE_URL = "https://project.supabase.test";
20
+ export const STORAGE_KEY = "service-role-key";
21
+ const fakeStorage = () => {
22
+ const objects = new Map();
23
+ const calls = [];
24
+ const reply = (status, body) => ({
25
+ status,
26
+ ok: status >= 200 && status < 300,
27
+ async text() {
28
+ return typeof body === "string" ? body : new TextDecoder().decode(body);
29
+ },
30
+ async arrayBuffer() {
31
+ return (typeof body === "string" ? new TextEncoder().encode(body) : body)
32
+ .slice()
33
+ .buffer;
34
+ },
35
+ });
36
+ const fetchImpl = async (url, init) => {
37
+ const method = init?.method ?? "GET";
38
+ const headers = init?.headers ?? {};
39
+ calls.push({ method, url, headers });
40
+ if (headers["Authorization"] !== `Bearer ${STORAGE_KEY}`)
41
+ return reply(401, "missing bearer token");
42
+ const rest = url.slice(`${STORAGE_URL}/storage/v1/`.length);
43
+ if (rest.startsWith(`object/list/${STORAGE_BUCKET}`) && method === "POST") {
44
+ const body = JSON.parse(String(init?.body ?? "{}"));
45
+ const matched = [...objects.entries()]
46
+ .filter(([path]) => path.startsWith(body.prefix))
47
+ .map(([path, value]) => ({
48
+ // The API returns names relative to the searched prefix, which is what the adapter relies on.
49
+ name: path.slice(body.prefix.length),
50
+ metadata: { size: value.bytes.byteLength },
51
+ }))
52
+ .sort((a, b) => a.name.localeCompare(b.name))
53
+ .slice(body.offset, body.offset + body.limit);
54
+ return reply(200, JSON.stringify(matched));
55
+ }
56
+ if (rest.startsWith(`object/sign/${STORAGE_BUCKET}/`) && method === "POST") {
57
+ const path = rest.slice(`object/sign/${STORAGE_BUCKET}/`.length);
58
+ if (!objects.has(path))
59
+ return reply(404, "Object not found");
60
+ const { expiresIn } = JSON.parse(String(init?.body ?? "{}"));
61
+ return reply(200, JSON.stringify({ signedURL: `/object/sign/${STORAGE_BUCKET}/${path}?token=t&exp=${expiresIn}` }));
62
+ }
63
+ if (rest.startsWith(`object/${STORAGE_BUCKET}/`)) {
64
+ const path = rest.slice(`object/${STORAGE_BUCKET}/`.length);
65
+ if (method === "POST") {
66
+ if (objects.has(path) && headers["x-upsert"] !== "true")
67
+ return reply(409, "The resource already exists");
68
+ const body = init?.body;
69
+ if (!(body instanceof Uint8Array))
70
+ return reply(400, "expected a byte body");
71
+ objects.set(path, { bytes: body, mediaType: headers["Content-Type"] ?? "application/octet-stream" });
72
+ return reply(200, JSON.stringify({ Key: `${STORAGE_BUCKET}/${path}` }));
73
+ }
74
+ if (method === "DELETE") {
75
+ if (!objects.has(path))
76
+ return reply(404, "Object not found");
77
+ objects.delete(path);
78
+ return reply(200, JSON.stringify({ message: "Successfully deleted" }));
79
+ }
80
+ const stored = objects.get(path);
81
+ return stored === undefined ? reply(404, "Object not found") : reply(200, stored.bytes);
82
+ }
83
+ return reply(404, `unrouted ${method} ${rest}`);
84
+ };
85
+ return { fetchImpl, objects, calls };
86
+ };
87
+ /** The adapter over the double, plus the double's innards for the assertions that need them. */
88
+ export const supabaseStorageDouble = () => {
89
+ const storage = fakeStorage();
90
+ return {
91
+ store: createSupabaseStorageFileContentStore({
92
+ url: STORAGE_URL,
93
+ serviceKey: STORAGE_KEY,
94
+ bucket: STORAGE_BUCKET,
95
+ fetch: storage.fetchImpl,
96
+ }),
97
+ objects: storage.objects,
98
+ calls: storage.calls,
99
+ };
100
+ };
101
+ //# sourceMappingURL=supabase-storage-double.js.map
@@ -57,6 +57,27 @@ export type HttpFailure = {
57
57
  readonly kind: "forbidden" | "redirected" | "timeout" | "unreachable" | "http-error" | "unreadable";
58
58
  readonly status?: number;
59
59
  readonly reason: string;
60
+ /**
61
+ * `Retry-After`, in milliseconds, when the server sent one.
62
+ *
63
+ * Parsed here because it is the one response header a *failure* still needs to carry: a vendor that answers
64
+ * `429` with `Retry-After: 47` has told you exactly how long to wait, and a default backoff that ignores it
65
+ * either hammers the vendor or waits far too long. `PlatformError.retryAfterMs` already exists and the retry
66
+ * path already reads it — this is the missing link between them.
67
+ *
68
+ * Both wire formats: a delay in seconds, and an HTTP date.
69
+ */
70
+ readonly retryAfterMs?: number;
71
+ /**
72
+ * The vendor's rate-limit headers, and **only** those — see `RATE_LIMIT_HEADERS`.
73
+ *
74
+ * An allowlist rather than every header, for two reasons. A failure travels into error messages and audit
75
+ * records, so carrying `set-cookie` or an auth echo would be a leak with no purpose. And a bounded set says
76
+ * what this field is *for*: `retryAfterMs` answers "how long", and these answer the question X forces —
77
+ * **which** limit was hit. A 15-minute burst limit and a 24-hour cap are both `429`, and treating the second
78
+ * as retryable makes a client sit in backoff until tomorrow.
79
+ */
80
+ readonly headers?: Readonly<Record<string, string>>;
60
81
  };
61
82
  export type HttpSuccess = {
62
83
  readonly ok: true;
@@ -97,6 +118,12 @@ export type HttpRequest = {
97
118
  export type HttpClient = {
98
119
  request(input: HttpRequest): Promise<HttpOutcome>;
99
120
  };
121
+ export declare const rateLimitHeadersOf: (headers: Headers) => {
122
+ headers?: Readonly<Record<string, string>>;
123
+ };
124
+ export declare const retryAfterOf: (headers: Headers) => {
125
+ retryAfterMs?: number;
126
+ };
100
127
  export declare const createHttpClient: (config?: HttpClientConfig) => HttpClient;
101
128
  /**
102
129
  * Read at most `maxBytes`, stopping as they arrive, and abandon the rest.
@@ -56,6 +56,52 @@ export const REQUEST_TIMEOUT_MS = 10_000;
56
56
  * choose which credential to spend and where to send it. `host` forges the request target past the policy check.
57
57
  */
58
58
  const RESERVED_HEADERS = new Set(["authorization", "cookie", "proxy-authorization", "host", "content-length"]);
59
+ /**
60
+ * `Retry-After` as milliseconds, in whichever of its two forms the server used.
61
+ *
62
+ * Returns a spreadable object rather than a number so an absent or unparseable header adds no key at all —
63
+ * `retryAfterMs: undefined` and "no retry hint" are different things to a consumer reading the field.
64
+ *
65
+ * A date in the past yields `0` rather than a negative delay, and an absurd value is capped: a vendor asking
66
+ * for a six-hour wait inside a request path is a vendor to give up on, not to obey.
67
+ */
68
+ const MAX_RETRY_AFTER_MS = 5 * 60_000;
69
+ /**
70
+ * The response headers a failure carries, by prefix.
71
+ *
72
+ * These are the shapes vendors actually use: `x-ratelimit-*` (GitHub, Reddit), `x-rate-limit-*` (X's
73
+ * per-15-minute window), `x-user-limit-24hour-*` / `x-app-limit-24hour-*` (X's daily caps, which are a
74
+ * different limit with a different remedy), and `x-ms-ratelimit-*` (Azure Resource Manager, whose
75
+ * `remaining-subscription-reads` counts *down* rather than reporting a ceiling — the only one of these that
76
+ * lets a client see a throttle approaching instead of discovering it).
77
+ */
78
+ const RATE_LIMIT_HEADERS = [
79
+ "retry-after",
80
+ "x-ratelimit-",
81
+ "x-rate-limit-",
82
+ "x-user-limit-",
83
+ "x-app-limit-",
84
+ "x-ms-ratelimit-",
85
+ ];
86
+ export const rateLimitHeadersOf = (headers) => {
87
+ const kept = {};
88
+ headers.forEach((value, name) => {
89
+ const lower = name.toLowerCase();
90
+ if (RATE_LIMIT_HEADERS.some((prefix) => lower === prefix || lower.startsWith(prefix)))
91
+ kept[lower] = value;
92
+ });
93
+ return Object.keys(kept).length === 0 ? {} : { headers: kept };
94
+ };
95
+ export const retryAfterOf = (headers) => {
96
+ const raw = headers.get("retry-after");
97
+ if (raw === null || raw.trim() === "")
98
+ return {};
99
+ const seconds = Number(raw.trim());
100
+ const ms = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(raw.trim()) - Date.now();
101
+ if (!Number.isFinite(ms))
102
+ return {};
103
+ return { retryAfterMs: Math.min(Math.max(Math.round(ms), 0), MAX_RETRY_AFTER_MS) };
104
+ };
59
105
  export const createHttpClient = (config = {}) => {
60
106
  const policy = config.policy ?? DEFAULT_EGRESS_POLICY;
61
107
  const doFetch = config.fetchImpl ?? fetch;
@@ -124,6 +170,8 @@ export const createHttpClient = (config = {}) => {
124
170
  kind: "http-error",
125
171
  status: response.status,
126
172
  reason: `That URL returned ${response.status}${text.trim() === "" ? "" : `: ${text.slice(0, 500)}`}`,
173
+ ...retryAfterOf(response.headers),
174
+ ...rateLimitHeadersOf(response.headers),
127
175
  };
128
176
  }
129
177
  const body = input.fence === false
@@ -10,8 +10,34 @@
10
10
  * interesting behaviour of an outbound tool — what it refuses, where it stops reading — should be provable
11
11
  * without constructing a run.
12
12
  */
13
- export { DEFAULT_EGRESS_POLICY, MAX_RESPONSE_BYTES, REQUEST_TIMEOUT_MS, createHttpClient, readBounded, } from "./http.js";
13
+ export { DEFAULT_EGRESS_POLICY, MAX_RESPONSE_BYTES, REQUEST_TIMEOUT_MS, createHttpClient, readBounded, rateLimitHeadersOf, retryAfterOf, } from "./http.js";
14
14
  export type { HttpClient, HttpClientConfig, HttpFailure, HttpOutcome, HttpRequest, HttpSuccess } from "./http.js";
15
+ export { createVendorTransport } from "./vendor.js";
16
+ /**
17
+ * RFC 5322 message building, shared — REQ-056 (#240), task #241.
18
+ *
19
+ * It began in `tools-google` and moved here when `tools-email` needed the same encoder. The header rules are
20
+ * the kind that fail *silently* — an unencoded non-ASCII subject arrives as mojibake, a header with a line
21
+ * break becomes two headers and the second copies the mail to somebody — so there is one implementation, and
22
+ * `tools-google` re-exports it rather than keeping a copy.
23
+ */
24
+ export { assertHeaderSafe, boundaryFor, buildMessage, encodeHeader, HeaderInjectionError, headerOf, stripBcc, } from "./mime.js";
25
+ export type { Attachment, OutgoingMessage } from "./mime.js";
26
+ /**
27
+ * The SSRF-hardened fetch, shared — REQ-055 (#237), tasks #238 and #239.
28
+ *
29
+ * It was written for `tools-scrape` and lives here because `tools-browser` needs the *same* implementation
30
+ * rather than a second copy: a browser navigating to `169.254.169.254` is the same hole as a fetch doing it,
31
+ * and two copies of an address classifier is how one of them ends up missing the IPv6-mapped forms.
32
+ *
33
+ * **Not a replacement for `mcp/egress.ts`, which is deliberately stricter.** That one denies *every* IPv6
34
+ * literal unless explicitly allowed, which is right for an MCP endpoint an operator configures once and wrong
35
+ * for scraping the open web, where a great many real sites are v6-only. They differ because the questions
36
+ * differ, and merging them would mean picking one answer for both.
37
+ */
38
+ export { BlockedError, DEFAULT_USER_AGENT, isPrivateAddress, isPrivateV4, isPrivateV6, nodeTransport, refuseUrl, resolvePublicly, safeFetch, systemResolve, } from "./ssrf.js";
39
+ export type { Refusal, Resolve, SafeFetchOptions, SafeResponse, SafeTransport } from "./ssrf.js";
40
+ export type { VendorClassifier, VendorFailure, VendorTransport, VendorTransportConfig } from "./vendor.js";
15
41
  export { DEFAULT_SEARCH_LIMIT, MAX_SNIPPET_CHARS, createFetchJson, createFetchPage, createWebSearch, htmlToText, } from "./web.js";
16
42
  export type { JsonResult, PageResult, SearchHit, SearchOutcome, SearchProvider } from "./web.js";
17
43
  export { MAX_CELL_CHARS, MAX_CSV_ROWS, MAX_SQL_ROWS, createSqlQuery, createSqlSchema, parseCsv, queryJson, } from "./data.js";
@@ -10,7 +10,30 @@
10
10
  * interesting behaviour of an outbound tool — what it refuses, where it stops reading — should be provable
11
11
  * without constructing a run.
12
12
  */
13
- export { DEFAULT_EGRESS_POLICY, MAX_RESPONSE_BYTES, REQUEST_TIMEOUT_MS, createHttpClient, readBounded, } from "./http.js";
13
+ export { DEFAULT_EGRESS_POLICY, MAX_RESPONSE_BYTES, REQUEST_TIMEOUT_MS, createHttpClient, readBounded, rateLimitHeadersOf, retryAfterOf, } from "./http.js";
14
+ export { createVendorTransport } from "./vendor.js";
15
+ /**
16
+ * RFC 5322 message building, shared — REQ-056 (#240), task #241.
17
+ *
18
+ * It began in `tools-google` and moved here when `tools-email` needed the same encoder. The header rules are
19
+ * the kind that fail *silently* — an unencoded non-ASCII subject arrives as mojibake, a header with a line
20
+ * break becomes two headers and the second copies the mail to somebody — so there is one implementation, and
21
+ * `tools-google` re-exports it rather than keeping a copy.
22
+ */
23
+ export { assertHeaderSafe, boundaryFor, buildMessage, encodeHeader, HeaderInjectionError, headerOf, stripBcc, } from "./mime.js";
24
+ /**
25
+ * The SSRF-hardened fetch, shared — REQ-055 (#237), tasks #238 and #239.
26
+ *
27
+ * It was written for `tools-scrape` and lives here because `tools-browser` needs the *same* implementation
28
+ * rather than a second copy: a browser navigating to `169.254.169.254` is the same hole as a fetch doing it,
29
+ * and two copies of an address classifier is how one of them ends up missing the IPv6-mapped forms.
30
+ *
31
+ * **Not a replacement for `mcp/egress.ts`, which is deliberately stricter.** That one denies *every* IPv6
32
+ * literal unless explicitly allowed, which is right for an MCP endpoint an operator configures once and wrong
33
+ * for scraping the open web, where a great many real sites are v6-only. They differ because the questions
34
+ * differ, and merging them would mean picking one answer for both.
35
+ */
36
+ export { BlockedError, DEFAULT_USER_AGENT, isPrivateAddress, isPrivateV4, isPrivateV6, nodeTransport, refuseUrl, resolvePublicly, safeFetch, systemResolve, } from "./ssrf.js";
14
37
  export { DEFAULT_SEARCH_LIMIT, MAX_SNIPPET_CHARS, createFetchJson, createFetchPage, createWebSearch, htmlToText, } from "./web.js";
15
38
  export { MAX_CELL_CHARS, MAX_CSV_ROWS, MAX_SQL_ROWS, createSqlQuery, createSqlSchema, parseCsv, queryJson, } from "./data.js";
16
39
  export { MAX_EXPRESSION_CHARS, calculate, currentTime } from "./compute.js";