@retinue/agentkit 0.1.0 → 0.2.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 (77) hide show
  1. package/README.md +59 -277
  2. package/dist/adapters/embeddings/openai.d.ts +45 -0
  3. package/dist/adapters/embeddings/openai.js +109 -0
  4. package/dist/agents/agent.d.ts +22 -1
  5. package/dist/agents/agent.js +97 -11
  6. package/dist/agents/engine.d.ts +28 -0
  7. package/dist/agents/engine.js +194 -8
  8. package/dist/capabilities/index.d.ts +5 -1
  9. package/dist/capabilities/index.js +23 -0
  10. package/dist/capabilities/runtime.d.ts +8 -0
  11. package/dist/core/budget.d.ts +55 -0
  12. package/dist/core/budget.js +56 -0
  13. package/dist/core/content-parts.d.ts +8 -0
  14. package/dist/core/events.d.ts +68 -2
  15. package/dist/core/events.js +2 -0
  16. package/dist/core/index.d.ts +1 -0
  17. package/dist/core/index.js +1 -0
  18. package/dist/documents/index.d.ts +14 -0
  19. package/dist/documents/parsers/text.d.ts +16 -0
  20. package/dist/documents/parsers/text.js +54 -2
  21. package/dist/entries/guardrails.d.ts +14 -0
  22. package/dist/entries/guardrails.js +14 -0
  23. package/dist/entries/knowledge.d.ts +9 -0
  24. package/dist/entries/knowledge.js +8 -0
  25. package/dist/graphql/resolvers.d.ts +4 -0
  26. package/dist/graphql/resolvers.js +6 -0
  27. package/dist/graphql/schema.d.ts +1 -1
  28. package/dist/graphql/schema.js +44 -0
  29. package/dist/guardrails/index.d.ts +115 -0
  30. package/dist/guardrails/index.js +108 -0
  31. package/dist/guardrails/moderation.d.ts +53 -0
  32. package/dist/guardrails/moderation.js +75 -0
  33. package/dist/guardrails/pii.d.ts +75 -0
  34. package/dist/guardrails/pii.js +193 -0
  35. package/dist/knowledge/index.d.ts +1 -0
  36. package/dist/knowledge/index.js +1 -0
  37. package/dist/knowledge/navigate.d.ts +89 -0
  38. package/dist/knowledge/navigate.js +107 -0
  39. package/dist/knowledge/retrieval.d.ts +73 -5
  40. package/dist/knowledge/retrieval.js +82 -28
  41. package/dist/models/streaming.d.ts +22 -1
  42. package/dist/models/streaming.js +5 -1
  43. package/dist/security/checklist.js +9 -0
  44. package/dist/security/findings.js +18 -9
  45. package/dist/skills/catalogue.d.ts +49 -0
  46. package/dist/skills/catalogue.js +61 -0
  47. package/dist/skills/index.d.ts +1 -0
  48. package/dist/skills/index.js +1 -0
  49. package/dist/telemetry/spans.js +12 -0
  50. package/dist/toolkit/files.d.ts +125 -0
  51. package/dist/toolkit/files.js +320 -0
  52. package/dist/toolkit/index.d.ts +4 -0
  53. package/dist/toolkit/index.js +2 -0
  54. package/dist/toolkit/sandbox.d.ts +119 -0
  55. package/dist/toolkit/sandbox.js +239 -0
  56. package/dist/toolkit/web.d.ts +13 -0
  57. package/dist/toolkit/web.js +7 -1
  58. package/dist/tools/budget.d.ts +28 -0
  59. package/dist/tools/budget.js +35 -0
  60. package/dist/tools/credentials.d.ts +57 -0
  61. package/dist/tools/credentials.js +54 -0
  62. package/dist/tools/define.d.ts +31 -0
  63. package/dist/tools/define.js +23 -0
  64. package/dist/tools/find.d.ts +109 -0
  65. package/dist/tools/find.js +210 -0
  66. package/dist/tools/index.d.ts +14 -2
  67. package/dist/tools/index.js +4 -0
  68. package/dist/tools/library/fs.d.ts +24 -0
  69. package/dist/tools/library/fs.js +102 -0
  70. package/dist/tools/library/index.d.ts +29 -2
  71. package/dist/tools/library/index.js +40 -0
  72. package/dist/tools/library/shell.d.ts +45 -0
  73. package/dist/tools/library/shell.js +70 -0
  74. package/dist/tools/meta-tools.js +8 -0
  75. package/dist/tools/registry.d.ts +113 -0
  76. package/dist/tools/registry.js +180 -4
  77. package/package.json +5 -1
package/README.md CHANGED
@@ -1,310 +1,92 @@
1
- # @retinue/agentkit
2
-
3
- Server-side half of the reusable AI platform. Implements the specifications in
4
- [`../docs`](../docs).
5
-
6
- ## Status
7
-
8
- Implemented and exercised end to end. 174 source files, ~36,000 lines, 2,143 tests.
9
-
10
- This section said *"Contracts only … there is no execution logic yet"* until 2026-08-24,
11
- which stopped being true a long time before it was corrected — a README that understates
12
- a package this far is worse than none, because the reader concludes it does nothing and
13
- looks elsewhere.
14
-
15
- What exists now: a durable run loop with leases, checkpointing and recovery; a streaming
16
- agent engine with tool calls, approvals, questions and citations; three storage adapter
17
- families held to one conformance suite; usage accounting with quota enforcement; MCP tool
18
- import; and a GraphQL surface. `examples/` is a runnable application over all of it, and
19
- `shareflow/` is the first real integration.
20
-
21
- Verified against real Postgres, Redis and a live model provider, not only in memory — the
22
- distinction matters, and `docs/09` records which claims rest on which.
23
-
24
- ## Modules
1
+ <img src="https://raw.githubusercontent.com/Rise-Experts/retinue/main/brand/retinue-mark.svg" alt="Retinue" width="72" />
25
2
 
26
- | Module | Specification | Contains |
27
- |---|---|---|
28
- | `core` | [02](../docs/02-core-and-persistence.md) | `ExecutionContext`, branded IDs, typed message parts, error and event contracts |
29
- | `capabilities` | #198, #196 | `createRuntime` — the composition root. The wired set is **derived** from the dependencies supplied, not declared beside them, so there is one statement of intent and one of fact. A capability that is off is **enforced**: `runtime.stores.messages` throws when `history` is off, so no caller has to remember to check |
30
- | `capabilities` *(model)* | #198 | What a runtime does, **declared and cross-checked**. A capability on with nothing wired refuses to construct; so does one wired that nothing declares — the second direction is what catches a feature present, tested and reachable from nothing. Profiles for the two common shapes: a chat assistant, and a headless automation |
31
- | `models` | [03](../docs/03-intelligence-runtime.md) | Model definitions, capabilities, pricing, resolution policy |
32
- | `agents` | [03](../docs/03-intelligence-runtime.md) | `AgentManifest` — declarative, stored, versioned |
33
- | `tools` | [03](../docs/03-intelligence-runtime.md) | Tool descriptors, effect classification, result envelope, meta-tools |
34
- | `skills` | [03](../docs/03-intelligence-runtime.md) | Versioned skills with a compact catalog entry and lazily loaded body |
35
- | `context` | [03](../docs/03-intelligence-runtime.md) | Context providers, section metadata, prompt budgets |
36
- | `runtime` | [04](../docs/04-durable-runtime-and-hitl.md) | Run lifecycle states and execution limits |
37
- | `hitl` | [04](../docs/04-durable-runtime-and-hitl.md) | Durable questions, approvals and idempotency |
38
- | `persistence` | [02](../docs/02-core-and-persistence.md) | Tenant-scoped store ports and infrastructure ports |
39
- | `usage` | [12](../docs/12-usage-and-accounting.md) | Recomputed rollups keyed on tenant, period and principal; quota enforcement at admission across **every** applicable limit — calendar windows, rolling windows and per-model allowances — with a warning below the limit; provider reconciliation that reports rather than corrects |
40
- | `evaluation` | [09](../docs/09-quality-and-release.md) | Deterministic graders for six of seven expectation kinds, a pinned and cached judge for the seventh, and a release comparison that names the cases that moved |
41
- | `mcp` | [10](../docs/10-mcp-integration.md) | Outbound MCP-server connections, tool import with safe-by-default effect classification, per-run catalog snapshots for drift detection, and an HTTP egress policy |
42
- | `files` | [05](../docs/05-knowledge-and-documents.md) | The attachment lifecycle: capped uploads, mediated reads, scheduled deletion, orphan reconciliation; the reference-not-inject context provider and the bounded `read_attachment` step |
43
- | `documents` | [05](../docs/05-knowledge-and-documents.md) | Extraction to structured blocks (headings, tables, lists), bounded parsers for PDF/Markdown/CSV/JSON, OCR and vision ports, confidence flagging, typed failures, and the bounded `read_document` step |
44
- | `artifacts` | [05](../docs/05-knowledge-and-documents.md) | Named, versioned assistant output: content by reference, compare-and-set versioning, required provenance, restore, and conversation-scoped access |
45
- | `export` | [05](../docs/05-knowledge-and-documents.md) | Deterministic PDF and Markdown rendering, one export per version per format, downloads through the mediated file path |
46
- | `knowledge` | [05](../docs/05-knowledge-and-documents.md) | Structure-aware chunking, the batched embedding pipeline, incremental resumable re-indexing, the freshness target, and hybrid rank-fusion retrieval with an honest empty result |
47
- | `citations` | [05](../docs/05-knowledge-and-documents.md) | Per-claim provenance as a durable snapshot, groundedness derived from the citation graph, permission checked at citation time |
48
- | `adapters` | [02](../docs/02-core-and-persistence.md) | Every storage and infrastructure implementation: `memory` (18 files, the reference), `postgres` (26), `supabase` (RLS over the Postgres adapters), `redis`, `bullmq`, `otel`. All three store families are held to the same conformance suite — 29 memory / 28 postgres (1 n/a) / 29 supabase, with no unaccounted cells |
49
- | `authorization` | [11](../docs/11-authorization.md) | The policy port. **Frozen v1.** Tools are filtered before discovery and re-authorized during execution; untrusted text can never widen capability |
50
- | `graphql` | [06](../docs/06-graphql-and-frontend.md) | SDL plus a thin resolver map the host mounts on its own server, so the library takes no GraphQL server dependency |
51
- | `idempotency` | [04](../docs/04-durable-runtime-and-hitl.md) | The idempotency contract. **Frozen v1.** Every external or destructive call carries a key derived from tenant, run and tool-call identity |
52
- | `principal-memory` | [15](../docs/15-user-memory.md) | Per-person memory, scoped to the principal as well as the tenant — enforced in the adapters and by RLS, not by a `WHERE` clause the caller has to remember |
53
- | `retention` | [18](../docs/18-data-retention.md) | Retention windows and the deletion path |
54
- | `security` | [17](../docs/17-security-review.md) | The security review as executable acceptances, each with a revisit date the release gate checks |
55
- | `telemetry` | [16](../docs/16-load-and-resilience.md) | The telemetry port and its OTel adapter |
56
- | `loadtest` | [16](../docs/16-load-and-resilience.md) | Load, soak and failure injection harnesses |
57
- | `worker` | [05](../docs/05-knowledge-and-documents.md) | The export worker |
58
- | `server` | [06](../docs/06-graphql-and-frontend.md) | The reference GraphQL host, SSE endpoint, boot, config, health and the runnable API and worker commands. Reached at the `./server` subpath; `graphql`, `graphql-yoga` and `@whatwg-node/server` are **optional peers**, so a consumer embedding the runtime in their own server installs none of them. Rules **R12** and **R13** keep the dependency one-way |
59
- | `tools/library` | — | The first-party tools (#188), reached at the `./tools` subpath: web fetch and search, HTTP, CSV, JSON, read-only SQL, knowledge search, attachments, time and arithmetic. Envelopes only — rule **R7** forbids I/O here |
60
- | `toolkit` | — | The deterministic functions those tools delegate to, and the only place the outbound HTTP client is built. Separate from `tools/` precisely because it *does* perform I/O |
61
- | `testing` | [09](../docs/09-quality-and-release.md) | The conformance suite every adapter runs, plus PGlite fixtures. Named `testing` and shipped deliberately: an adapter written outside this repository has to be holdable to the same behaviour |
62
-
63
- ## Rules these contracts encode
3
+ # @retinue/agentkit
64
4
 
65
- 1. Every tenant-sensitive operation takes an explicit tenant context. `findById(id)` is
66
- forbidden; ports use `findById({ tenantId, id })`.
67
- 2. `ExecutionContext` identity is constructed by the host application. Model-generated
68
- input can never override it.
69
- 3. Tools are authorization-filtered before discovery **and** re-authorized during
70
- execution.
71
- 4. Every external or destructive tool call carries an idempotency key derived from
72
- tenant, run and tool-call identity.
73
- 5. Untrusted text — tenant-authored skill bodies, MCP tool descriptions — can never
74
- widen capability. Authorization lives in the policy layer, never in the prompt.
5
+ [![npm](https://img.shields.io/npm/v/@retinue/agentkit)](https://www.npmjs.com/package/@retinue/agentkit)
6
+ [![licence](https://img.shields.io/npm/l/@retinue/agentkit)](https://github.com/Rise-Experts/retinue/blob/main/LICENSE)
7
+ [![provenance](https://img.shields.io/badge/provenance-attested-brightgreen)](https://www.npmjs.com/package/@retinue/agentkit#provenance)
75
8
 
76
- ## Scripts
9
+ **A durable AI agent runtime for TypeScript.** Agents that survive a restart, tools that ask before
10
+ they act, and retrieval that cites its sources — behind ports you can replace.
77
11
 
78
- ```bash
79
- npm run typecheck -w @retinue/agentkit
80
- npm test -w @retinue/agentkit
81
- npm run build -w @retinue/agentkit
82
- ```
12
+ For teams building an assistant or an automation that has to be *correct*: a run that crashes resumes
13
+ instead of vanishing, an external write waits for a human, and every token is accounted for.
83
14
 
84
- From the repository root, the checks that gate a change:
15
+ ## Install
85
16
 
86
17
  ```bash
87
- npm run conformance # the adapter matrix, and it fails on an unaccounted cell
88
- npm run check:boundaries # the dependency rules between workspaces
89
- npm run check:reachability # every declared capability is wired, every run event is emitted
90
- npm run security:review # the acceptances in `security`, and their revisit dates
91
- ```
92
-
93
- `check:reachability` exists because the recurring defect in this codebase is not code that
94
- is wrong — it is code that is **correct, tested and unreachable**. Citations, questions,
95
- usage recording, compaction, skills and MCP import were each built, each passing tests,
96
- and each wired to nothing.
97
-
98
- ## Subpaths
99
-
100
- The root is the **semver boundary**: what is exported from it is API, and what is not exported from it cannot be
101
- broken. So it is **five values** — it was 392 (#199).
102
-
103
- ```ts
104
- import { createRuntime, resolveCapabilities, defineAgent, asId, AgentPlatformError } from "@retinue/agentkit";
105
- ```
106
-
107
- Every **type** is still exported from the root, by `export type *`, which emits no import. That is what makes the
108
- cut affordable: a consumer holding an `ExecutionContext` does not have to know which layer defined it, and a type
109
- cannot be broken by being imported. The split is **types by subject, values by consumer**.
110
-
111
- The root's runtime graph now reaches *nothing* — not `ai`, not `zod`. Those are still real dependencies of the
112
- package, because `./runtime` and `./tools` need them, and they stay `dependencies` rather than peers: a consumer
113
- who installs this will use at least one subpath and should not have to install two more things to do it.
114
-
115
- ```ts
116
- import { createDefaultEngine } from "@retinue/agentkit/runtime";
117
- import { defineTool, createStandardToolProvider } from "@retinue/agentkit/tools"; // no peer: uses global fetch
118
- import { createMemoryRunStore } from "@retinue/agentkit/persistence"; // no peer at all
119
- import { createPostgresRunStore } from "@retinue/agentkit/adapters/postgres"; // peer: pg
120
- import { typeDefs, createResolvers } from "@retinue/agentkit/server"; // peer: graphql, graphql-yoga
18
+ npm i @retinue/agentkit
121
19
  ```
122
20
 
123
- `src/entries/README.md` lists them all, including why there is no `./testing` yet.
21
+ Node 20+. Provider SDKs, PostgreSQL, Redis and BullMQ are **optional peers** — install only what you use.
22
+ The package root imports nothing but `ai` and `zod`.
124
23
 
125
- ## Capabilities
126
-
127
- Eight booleans, declared and cross-checked against the wiring — REQ-043 (#197).
24
+ ## Your first agent
128
25
 
129
26
  ```ts
130
- const runtime = createRuntime({
131
- profile: "automation", // or "assistant", or no profile and set them yourself
132
- capabilities: { memory: "on" }, // an override, without restating the rest
133
- floor: { runs }, // what every runtime needs
134
- stores: { usage, principalMemory },
27
+ import { createAgent } from "@retinue/agentkit/providers";
28
+
29
+ const agent = createAgent({
30
+ manifest: {
31
+ id: "assistant",
32
+ name: "Assistant",
33
+ instructions: "You are a helpful assistant. Be concise.",
34
+ // A capability, not a hardcoded model id — swapping providers is config.
35
+ modelPolicy: { role: "smart" },
36
+ },
135
37
  });
136
- ```
137
-
138
- | Capability | Needs | On means |
139
- |---|---|---|
140
- | `history` | `messages` | Prior turns reach the model |
141
- | `memory` | `principalMemory` | Per-person memory is read and written |
142
- | `compaction` | `summaries`, `summarizer` | A long thread is condensed rather than refused |
143
- | `citations` | `citations` | Claims carry provenance |
144
- | `questions` | `interactions` | A run can park on a question and resume |
145
- | `skills` | `skills` | Named instruction blocks load on demand |
146
- | `mcp` | `mcpConnections`, `mcpClient` | Another server's tools are importable |
147
- | `usage` | `usage` | Spend is metered |
148
-
149
- **A declaration that disagrees with the wiring refuses to start**, in both directions and naming every
150
- mismatch at once. Declared on with nothing wired is the obvious half. Wired but *not* declared is the half that
151
- matters more: it is how a declaration drifts into a lie, and this repo has found the same defect six times
152
- (#157, #159, #161, #163, #165, #185) — a capability that existed, passed its tests, and was wired to nothing.
153
38
 
154
- **Off removes the cost.** No store is required, no query is issued, and reading the dependency of an off
155
- capability throws rather than returning undefined — access is the gate, so no caller has to remember to check.
156
-
157
- **Approvals and quotas are not on this list, deliberately.** They have no off switch. An automation that needs no
158
- human approves through a *policy* that records what it approved, which is auditable; a boolean that removed the
159
- gate would remove the record with it. That distinction is the difference between "nobody had to approve this" and
160
- "nobody knows whether anybody approved this".
161
-
162
- ### The minimum viable configuration
163
-
164
- The smallest thing that runs a tool-calling automation — no conversation, no memory, no human in the loop:
39
+ const result = await agent.run({
40
+ conversationId: "conv-1",
41
+ message: "Draft a one-line launch note for our analytics dashboard.",
42
+ });
165
43
 
166
- ```ts
167
- const runtime = createRuntime({ profile: "automation", floor: { runs }, stores: { usage } });
44
+ console.log(result.text); // the assistant's reply
45
+ console.log(result.outcome); // "completed" | "failed" | "cancelled" |
168
46
  ```
169
47
 
170
- One store beyond the floor. A run in this configuration has **no `conversationId`** absent, not invented
171
- (#198): the conversation-scoped capabilities are unavailable rather than operating on a fabricated id, which is
172
- what #164 did with `principalId` and why every per-person figure silently read as a machine's.
173
-
174
- All 256 combinations of the eight are constructed and gate-checked in `capabilities/__tests__/runtime.test.ts`.
175
- That test used to enumerate six hand-picked mixes, on the reasoning that the matrix would "assert that
176
- combinations nobody has thought about work" — which is backwards for a surface of eight independent booleans,
177
- where the combination nobody thought about is the one a customer picks first.
178
-
179
- ## Tools
180
-
181
- Fifteen first-party tools, at `@retinue/agentkit/tools`. **Wiring is the toggle** — a tool exists when its
182
- dependency was supplied and not otherwise, because a separate `enable` flag beside a `sqlQuery` function is how a
183
- deployment ends up with a tool that is enabled and unwired:
48
+ `createAgent` is the batteries-included path: it wires the in-memory stores, the model registry and the
49
+ default engine for you, and reads `ANTHROPIC_API_KEY` from the environment. Swap in PostgreSQL, Redis and
50
+ your own model catalogue when you need them same code above.
184
51
 
185
- ```ts
186
- const tools = createStandardToolProvider({
187
- deps: { authorization, idempotency, approvals },
188
- http: {}, // fetch_url, fetch_json, http_request, http_write
189
- search: braveProvider, // web_search — omitted, and the tool does not exist
190
- sql: { query: readOnlyPool, readOnly: true, schemas: ["app"] },
191
- knowledge: { retriever, authSubjects: (ctx) => [String(ctx.conversationId)] },
192
- });
193
- ```
52
+ ## What you get
194
53
 
195
54
  | | |
196
55
  |---|---|
197
- | `fetch_url`, `fetch_json`, `http_request` | `read`. Egress-policy checked before any request, redirects refused rather than followed, bodies bounded while reading and fenced as untrusted content |
198
- | `http_write` | `external-write`, so approval and an idempotency key are required by the registry. Two tools rather than one with a `method` argument, because effect is classified per *tool* — a single tool could only be gated for every call or none |
199
- | `parse_csv`, `query_json` | `read`, pure. They take text, not a path or a URL: reading is `read_attachment`'s or `fetch_url`'s job, and each should be checked by the thing that should check it |
200
- | `sql_query`, `sql_schema` | `read`, and only honest because `createSqlQuery` demands a `readOnly: true` acknowledgement. The keyword scan inside it is a second line of defence; the connection is the control |
201
- | `search_knowledge` | `read`. `authSubjects` comes from the host, never from tool input a model must not widen its own read scope by asking |
202
- | `read_attachment`, `list_attachments`, `read_document` | `read`, through `FileService` so the entitlement check is not duplicated |
203
- | `now`, `calculate` | `read`, pure. `calculate` is a parser, not `eval`: the expression comes from a model |
204
-
205
- Credentials are configured per host (`headersFor`) and never appear in a tool's input schema. The client refuses
206
- an `authorization` or `cookie` header supplied by a caller rather than forwarding it.
56
+ | **Durable runs** | Leases, checkpoints and recovery. Kill the worker mid-turn; the run resumes rather than disappearing |
57
+ | **Approval gates** | An external write stops and waits for a person. Idempotency keys mean a retry does not fire the side effect twice |
58
+ | **Tools that scale** | A compact catalogue in context, full schemas fetched on demand, authorization re-checked at execution |
59
+ | **Knowledge with citations** | Block-aware chunking, hybrid retrieval fused by rank, and answers that point at the source |
60
+ | **Injection containment** | Untrusted content is wrapped in a nonce-delimited envelope with delimiter forgery neutralised structural, not a detector |
61
+ | **Usage you can bill** | Per-model, per-principal token and cost accounting, with quotas enforced before a run is admitted |
62
+ | **Flows and teams** | Durable multi-step workflows; a team compiles to a flow, and each member's turn is a child run with its own ceiling |
63
+ | **Replaceable everything** | 31 ports, three adapter families, one conformance suite held over all of them |
207
64
 
208
- ## Flows and teams
65
+ ## Composing it yourself
209
66
 
210
- `@retinue/agentkit/flows` REQ-038 ([#187](https://github.com/Rise-Experts/retinue/issues/187)) and REQ-037
211
- ([#186](https://github.com/Rise-Experts/retinue/issues/186)).
212
-
213
- **A team is a kind of flow step, and a team compiles to a flow.** Both issues say they share design, and they are
214
- right: a flow's step and a team's member turn are the same idea, and modelling them separately produces two
215
- overlapping notions of "a step" to keep in agreement forever.
67
+ The root exports five values and every type. Everything else sits behind a documented subpath, so a
68
+ consumer never installs a dependency they do not use:
216
69
 
217
70
  ```ts
218
- import { compileTeam, createFlowRunner } from "@retinue/agentkit/flows";
71
+ import { createRuntime, defineAgent } from "@retinue/agentkit";
72
+ import { createDefaultEngine } from "@retinue/agentkit/runtime";
73
+ import { createPostgresConversationStore } from "@retinue/agentkit/adapters/postgres";
219
74
  ```
220
75
 
221
- ### The interpreter is a pure function
222
-
223
- `advance(definition, execution, outcome)` returns the next execution and **one effect** for the caller to perform.
224
- It performs nothing itself — no agent call, no tool call, no clock read, no store write. Every property these two
225
- REQs ask for is a consequence rather than a separate mechanism:
226
-
227
- | Property | Why it follows |
228
- |---|---|
229
- | Durable resume | The returned execution *is* the position. A host persists it; after a restart it calls `advance` again and gets the same effect. There is no interpreter instance to rebuild |
230
- | Idempotency across a resume | The effect's key is `(executionId, step, attempt)` — all three are in the stored state, so a step that wrote externally and crashed produces the *same* key and the idempotency store answers with the first result |
231
- | Budgets | Checked before the effect is produced, so an over-budget flow performs nothing rather than spending and then noticing |
232
- | Tests | Feeding outcomes to a function needs no agent, no database and no clock. The awkward cases — a crash mid-step, a retry surviving a process death — are testable at all |
233
-
234
- The alternative, an async interpreter that awaits its own effects, is shorter and cannot be made durable without a
235
- checkpoint after every `await` — which is the same state machine with the states implicit.
236
-
237
- ### A definition and an execution are different things
238
-
239
- `FlowExecution.flowVersion` is pinned at start and the definition is read at that version for the execution's whole
240
- life. **Editing a flow does not change one already running.** Proven live: a v2 with a completely different shape
241
- was published while an execution sat parked at a checkpoint, and it still finished through v1.
242
-
243
- The store refuses to overwrite a version at all — `(tenant_id, flow_id, version)` is the primary key with no
244
- `ON CONFLICT` — which is what makes the pin worth having.
245
-
246
- ### Step kinds
247
-
248
- `agent`, `team`, `tool`, `branch`, `wait`, `checkpoint`, `subflow`, `done`. A `checkpoint` uses the **existing**
249
- HITL path, so a parked flow is the same object the assistant surface already answers.
250
-
251
- Two arithmetic details that were bugs first: `done` consumes no budget and is not gated by one, because a budget
252
- stops *work* and finishing is not work. Counting it meant a flow whose ceiling exactly matched its work always
253
- failed on the last step — so `maxSteps: 3` really meant two steps and a marker. A `branch` does count, because a
254
- branch can loop.
255
-
256
- ### Failure is chosen in the definition
257
-
258
- `retry` (bounded, with backoff), `skip`, `escalate`, `fail`. A retry gets a **different** idempotency key, because
259
- reusing it would have the store answer with the *failed* first result — a retry policy that silently does nothing.
260
- A failed step that spent money is still charged, or a retrying flow costs more than its ceiling allows.
261
-
262
- ### Teams
263
-
264
- `sequential` chains one agent step per member, each reading the previous one's output *and* the original brief —
265
- passing only the previous output loses the request by the third member, which is how a chain of agents drifts off
266
- the question. `manager-led` compiles to **one** agent step whose tools include a delegation tool, so the engine's
267
- own turn loop does the iterating and a delegation is a real tool call: authorised, approved, deduplicated and
268
- accounted for unchanged, because it *is* one rather than resembling one.
269
-
270
- A member's tools are an **intersection**, never a union: a member cannot reach a tool the delegating context could
271
- not, and the delegation tool itself is always stripped — a member that could delegate would be a manager.
272
-
273
- ### An agent step is a child run
274
-
275
- Each agent step creates a `Run` of its own and the flow parks on it (#202). A run rather than an inline model
276
- call, because a `Run` is what earns checkpointing, recovery, quota admission and its own usage rows — calling a
277
- model from the runner would be a second turn implementation with none of those, and it would pass a demo.
278
-
279
- Three decisions inside that, each with an appealing wrong answer:
280
-
281
- - **The child has no conversation.** `ConversationRunCoordinator` claims a *conversation's* single run slot, so a
282
- flow inside a conversation whose steps also claimed it would deadlock against the conversation's own turn — the
283
- parent holds the slot and waits for a child that can never get it. A conversation-less run (#198) has no slot to
284
- contend for, and what the member needs travels in the run's `input`.
285
- - **The ceiling is the flow's remainder**, re-derived per step. Handing each member the flow's original budget
286
- would let every one of them spend the whole thing. Visible live: the first member gets 6 steps, the second 5.
287
- - **Two ways to wake up, and only one of them is load-bearing.** `onRunSettled` on the worker is the fast path;
288
- correctness is a *poll* of the child's state at the top of every resume. A crash between the child completing
289
- and the notification being sent loses the message, and a parent that only woke on notifications would sit
290
- forever with nothing looking again.
291
-
292
- `Run.input` and `Run.limits` exist because of this. #198 made a run able to exist without a conversation, but the
293
- only place a request could live was a `Message` — and a message requires a conversation. So the run shape said "no
294
- conversation needed" while the storage said its input still needed one.
295
-
296
- `npm run flow -w @retinue/example-app` drives all of it against Postgres: a flow straight through, one parked for
297
- a person, a reload from storage by something that never held it, the version pin against a published v2, a team
298
- whose members become child runs with shrinking ceilings, a lost notification recovered by the poll, and a failing
299
- member routed into its step's policy.
76
+ Anything reachable only by a deep import is **not** API — and that is enforced against the published
77
+ tarball rather than asserted here.
300
78
 
301
- ## Import convention
79
+ ## Documentation
302
80
 
303
- The package is ESM with `NodeNext` resolution, so relative imports carry an explicit
304
- `.js` extension even in TypeScript sources.
81
+ - [Getting started](https://docs.retinue.riseexperts.de/docs/getting-started/installation) install, first agent, configuration
82
+ - [Core concepts](https://docs.retinue.riseexperts.de/docs/concepts/architecture) agents, tools, durable runtime, retrieval, HITL
83
+ - [Package surface](https://docs.retinue.riseexperts.de/docs/reference/package-surface) — every module, subpath, capability and tool
84
+ - [Versioning and deprecation](https://github.com/Rise-Experts/retinue/blob/main/docs/19-versioning.md) — what semver covers, and how you are told before it changes
85
+ - [Specifications](https://github.com/Rise-Experts/retinue/tree/main/docs) — the design documents, kept as reference
305
86
 
306
87
  ## Licence
307
88
 
308
- MIT — see [LICENSE](./LICENSE).
89
+ MIT — see [LICENSE](https://github.com/Rise-Experts/retinue/blob/main/LICENSE).
309
90
 
310
- Copyright (c) 2026 Azeem Sarwar and Rise Experts.
91
+ Copyright (c) 2026 [Azeem Sarwar](https://github.com/azeem-sarwar) and
92
+ [Rise Experts](https://github.com/Rise-Experts).
@@ -0,0 +1,45 @@
1
+ /**
2
+ * An OpenAI-compatible embedding adapter — REQ-050 (#209), task #219.
3
+ *
4
+ * The `EmbeddingProvider` port has existed since #136 with **no adapter of any kind**, which means the semantic
5
+ * half of hybrid retrieval has never run against a real model in this repository: every test supplies a stub, and
6
+ * a stub measures the stub. That is why this exists before the eval does — a retrieval score computed over
7
+ * hash-based pseudo-vectors is a number about nothing.
8
+ *
9
+ * OpenAI's shape rather than OpenAI specifically: the same request works against Azure OpenAI, Together, a local
10
+ * `llama.cpp` server and anything else that copied the endpoint, so `baseUrl` is the whole configuration story.
11
+ *
12
+ * ## Two properties worth stating
13
+ *
14
+ * **Order is checked, not assumed.** The API returns objects carrying an `index`, and the port's contract is one
15
+ * vector per input *in order*. A provider that returned them out of order — or dropped one — would silently pair
16
+ * every chunk with its neighbour's vector, and retrieval would still work well enough to look fine. So the
17
+ * response is sorted by `index` and the count is verified.
18
+ *
19
+ * **The model reference records a version.** Providers change what a model id returns without renaming it, and a
20
+ * corpus embedded across such a change has two incomparable halves. The port carries the ref per chunk precisely
21
+ * so that is detectable; this passes the caller's version through rather than inventing one.
22
+ */
23
+ import type { EmbeddingProvider } from "../../knowledge/index.js";
24
+ export type OpenAiEmbeddingsConfig = {
25
+ readonly apiKey: string;
26
+ /** Defaults to `text-embedding-3-small`, which is 1536 dimensions — the platform's `EMBEDDING_DIMENSIONS`. */
27
+ readonly modelId?: string;
28
+ /**
29
+ * The version this deployment is recording for these vectors.
30
+ *
31
+ * Not derivable from the API: OpenAI publishes no version for an embedding model, which is exactly the problem
32
+ * the field exists for. A deployment that re-embeds after noticing a change bumps this itself.
33
+ */
34
+ readonly version?: string;
35
+ readonly dimensions?: number;
36
+ readonly baseUrl?: string;
37
+ readonly fetchImpl?: typeof fetch;
38
+ /** Inputs per request. The endpoint accepts many; the ceiling is the request body's size, not a count. */
39
+ readonly batchSize?: number;
40
+ readonly timeoutMs?: number;
41
+ };
42
+ export declare const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small";
43
+ export declare const DEFAULT_EMBEDDING_BATCH = 96;
44
+ export declare const createOpenAiEmbeddings: (config: OpenAiEmbeddingsConfig) => EmbeddingProvider;
45
+ //# sourceMappingURL=openai.d.ts.map
@@ -0,0 +1,109 @@
1
+ /**
2
+ * An OpenAI-compatible embedding adapter — REQ-050 (#209), task #219.
3
+ *
4
+ * The `EmbeddingProvider` port has existed since #136 with **no adapter of any kind**, which means the semantic
5
+ * half of hybrid retrieval has never run against a real model in this repository: every test supplies a stub, and
6
+ * a stub measures the stub. That is why this exists before the eval does — a retrieval score computed over
7
+ * hash-based pseudo-vectors is a number about nothing.
8
+ *
9
+ * OpenAI's shape rather than OpenAI specifically: the same request works against Azure OpenAI, Together, a local
10
+ * `llama.cpp` server and anything else that copied the endpoint, so `baseUrl` is the whole configuration story.
11
+ *
12
+ * ## Two properties worth stating
13
+ *
14
+ * **Order is checked, not assumed.** The API returns objects carrying an `index`, and the port's contract is one
15
+ * vector per input *in order*. A provider that returned them out of order — or dropped one — would silently pair
16
+ * every chunk with its neighbour's vector, and retrieval would still work well enough to look fine. So the
17
+ * response is sorted by `index` and the count is verified.
18
+ *
19
+ * **The model reference records a version.** Providers change what a model id returns without renaming it, and a
20
+ * corpus embedded across such a change has two incomparable halves. The port carries the ref per chunk precisely
21
+ * so that is detectable; this passes the caller's version through rather than inventing one.
22
+ */
23
+ import { AgentPlatformError } from "../../core/errors.js";
24
+ export const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small";
25
+ export const DEFAULT_EMBEDDING_BATCH = 96;
26
+ export const createOpenAiEmbeddings = (config) => {
27
+ const modelId = config.modelId ?? DEFAULT_EMBEDDING_MODEL;
28
+ const model = {
29
+ modelId,
30
+ version: config.version ?? "1",
31
+ dimensions: config.dimensions ?? 1536,
32
+ };
33
+ const doFetch = config.fetchImpl ?? fetch;
34
+ const base = (config.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "");
35
+ const batchSize = config.batchSize ?? DEFAULT_EMBEDDING_BATCH;
36
+ const embedBatch = async (texts) => {
37
+ const controller = new AbortController();
38
+ const timer = setTimeout(() => controller.abort(), config.timeoutMs ?? 60_000);
39
+ let response;
40
+ try {
41
+ response = await doFetch(`${base}/embeddings`, {
42
+ method: "POST",
43
+ signal: controller.signal,
44
+ headers: { authorization: `Bearer ${config.apiKey}`, "content-type": "application/json" },
45
+ body: JSON.stringify({
46
+ model: modelId,
47
+ input: texts,
48
+ // Asked for explicitly, because the 3-series models support shortening and a deployment that pinned
49
+ // 1536 in its column must get 1536 rather than whatever the default becomes.
50
+ ...(model.dimensions === 1536 ? {} : { dimensions: model.dimensions }),
51
+ }),
52
+ });
53
+ }
54
+ catch (error) {
55
+ throw new AgentPlatformError({
56
+ code: error.name === "AbortError" ? "timeout" : "provider_unavailable",
57
+ message: `The embedding endpoint did not respond: ${error.message}`,
58
+ retryable: true,
59
+ });
60
+ }
61
+ finally {
62
+ clearTimeout(timer);
63
+ }
64
+ const payload = (await response.json().catch(() => ({})));
65
+ if (!response.ok) {
66
+ const rateLimited = response.status === 429;
67
+ throw new AgentPlatformError({
68
+ code: rateLimited ? "rate_limited" : response.status >= 500 ? "provider_unavailable" : "provider_error",
69
+ message: `The embedding endpoint returned ${response.status}: ${payload.error?.message ?? "no message"}`,
70
+ // A 5xx and a rate limit are worth retrying; a 400 means the request is wrong and will stay wrong.
71
+ retryable: rateLimited || response.status >= 500,
72
+ });
73
+ }
74
+ const data = [...(payload.data ?? [])].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
75
+ if (data.length !== texts.length) {
76
+ throw new AgentPlatformError({
77
+ code: "provider_error",
78
+ message: `Asked for ${texts.length} embeddings and received ${data.length}. Pairing them would attach every chunk to the wrong vector.`,
79
+ retryable: false,
80
+ });
81
+ }
82
+ return data.map((entry, at) => {
83
+ const vector = entry.embedding;
84
+ if (vector === undefined || vector.length !== model.dimensions) {
85
+ throw new AgentPlatformError({
86
+ code: "provider_error",
87
+ message: `Embedding ${at} has ${vector?.length ?? 0} dimensions; this deployment records ${model.dimensions}.`,
88
+ retryable: false,
89
+ });
90
+ }
91
+ return vector;
92
+ });
93
+ };
94
+ return {
95
+ model,
96
+ async embed(texts) {
97
+ if (texts.length === 0)
98
+ return [];
99
+ const out = [];
100
+ // Sequential batches, deliberately: parallel ones hit the rate limit on a first index of a real corpus,
101
+ // and the failure arrives as a 429 in the middle of a job rather than as a slower job.
102
+ for (let at = 0; at < texts.length; at += batchSize) {
103
+ out.push(...(await embedBatch(texts.slice(at, at + batchSize))));
104
+ }
105
+ return out;
106
+ },
107
+ };
108
+ };
109
+ //# sourceMappingURL=openai.js.map
@@ -15,8 +15,10 @@ import { type ProviderCredentials } from "../models/provider-factory.js";
15
15
  import type { ModelProvider } from "../models/index.js";
16
16
  import type { AuthorizationPolicy } from "../authorization/index.js";
17
17
  import { type ContextProvider } from "../context/index.js";
18
- import { type ToolProvider } from "../tools/index.js";
18
+ import { type ToolProvider, type ToolSearch, type ToolsetResolver } from "../tools/index.js";
19
+ import type { TokenBudget } from "../core/budget.js";
19
20
  import { type AgentEngine, type ProcessOutcome } from "../runtime/index.js";
21
+ import type { Guardrail } from "../guardrails/index.js";
20
22
  import { type ResolvedModelInfo } from "./engine.js";
21
23
  import type { AgentManifest } from "./index.js";
22
24
  import { type AgentManifestInput } from "./define.js";
@@ -38,6 +40,25 @@ export type CreateAgentConfig = {
38
40
  */
39
41
  readonly randomHex?: (bytes: number) => string;
40
42
  readonly authorization?: AuthorizationPolicy;
43
+ /**
44
+ * Checks to run before the model sees a turn and before anything leaves it — REQ-046 (#205), AC-5.
45
+ *
46
+ * Here so a host can add one without composing the runtime by hand: this facade exists to be the short path,
47
+ * and a guardrail that could only be wired through the long one would be a guardrail most deployments never
48
+ * add.
49
+ */
50
+ readonly guardrails?: readonly Guardrail[];
51
+ /**
52
+ * Search over the catalogue, which is what makes `find_tools` exist — REQ-045 (#204), task #210.
53
+ *
54
+ * Absent means no `find_tools`. Wire it with `createToolSearch()` for keyword search, or pass an
55
+ * `EmbeddingProvider` to it for hybrid — and see `tools/find.ts` on why keyword-only is the honest default.
56
+ */
57
+ readonly toolSearch?: ToolSearch;
58
+ /** A ceiling in tokens on the tool list handed to the model — task #210, AC-3. Absent means no ceiling. */
59
+ readonly catalogBudget?: TokenBudget;
60
+ /** A tenant's category switches, applied before authorization — task #210, AC-4. */
61
+ readonly toolsets?: ToolsetResolver;
41
62
  readonly tenantId?: string;
42
63
  /** Test/advanced seam: override how a manifest resolves to a model (e.g. a mock model). */
43
64
  readonly resolveModel?: (manifest: AgentManifest, context: ExecutionContext) => ResolvedModelInfo;