@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.
- package/README.md +59 -277
- package/dist/adapters/embeddings/openai.d.ts +45 -0
- package/dist/adapters/embeddings/openai.js +109 -0
- package/dist/agents/agent.d.ts +22 -1
- package/dist/agents/agent.js +97 -11
- package/dist/agents/engine.d.ts +28 -0
- package/dist/agents/engine.js +194 -8
- package/dist/capabilities/index.d.ts +5 -1
- package/dist/capabilities/index.js +23 -0
- package/dist/capabilities/runtime.d.ts +8 -0
- package/dist/core/budget.d.ts +55 -0
- package/dist/core/budget.js +56 -0
- package/dist/core/content-parts.d.ts +8 -0
- package/dist/core/events.d.ts +68 -2
- package/dist/core/events.js +2 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/documents/index.d.ts +14 -0
- package/dist/documents/parsers/text.d.ts +16 -0
- package/dist/documents/parsers/text.js +54 -2
- package/dist/entries/guardrails.d.ts +14 -0
- package/dist/entries/guardrails.js +14 -0
- package/dist/entries/knowledge.d.ts +9 -0
- package/dist/entries/knowledge.js +8 -0
- package/dist/graphql/resolvers.d.ts +4 -0
- package/dist/graphql/resolvers.js +6 -0
- package/dist/graphql/schema.d.ts +1 -1
- package/dist/graphql/schema.js +44 -0
- package/dist/guardrails/index.d.ts +115 -0
- package/dist/guardrails/index.js +108 -0
- package/dist/guardrails/moderation.d.ts +53 -0
- package/dist/guardrails/moderation.js +75 -0
- package/dist/guardrails/pii.d.ts +75 -0
- package/dist/guardrails/pii.js +193 -0
- package/dist/knowledge/index.d.ts +1 -0
- package/dist/knowledge/index.js +1 -0
- package/dist/knowledge/navigate.d.ts +89 -0
- package/dist/knowledge/navigate.js +107 -0
- package/dist/knowledge/retrieval.d.ts +73 -5
- package/dist/knowledge/retrieval.js +82 -28
- package/dist/models/streaming.d.ts +22 -1
- package/dist/models/streaming.js +5 -1
- package/dist/security/checklist.js +9 -0
- package/dist/security/findings.js +18 -9
- package/dist/skills/catalogue.d.ts +49 -0
- package/dist/skills/catalogue.js +61 -0
- package/dist/skills/index.d.ts +1 -0
- package/dist/skills/index.js +1 -0
- package/dist/telemetry/spans.js +12 -0
- package/dist/toolkit/files.d.ts +125 -0
- package/dist/toolkit/files.js +320 -0
- package/dist/toolkit/index.d.ts +4 -0
- package/dist/toolkit/index.js +2 -0
- package/dist/toolkit/sandbox.d.ts +119 -0
- package/dist/toolkit/sandbox.js +239 -0
- package/dist/toolkit/web.d.ts +13 -0
- package/dist/toolkit/web.js +7 -1
- package/dist/tools/budget.d.ts +28 -0
- package/dist/tools/budget.js +35 -0
- package/dist/tools/credentials.d.ts +57 -0
- package/dist/tools/credentials.js +54 -0
- package/dist/tools/define.d.ts +31 -0
- package/dist/tools/define.js +23 -0
- package/dist/tools/find.d.ts +109 -0
- package/dist/tools/find.js +210 -0
- package/dist/tools/index.d.ts +14 -2
- package/dist/tools/index.js +4 -0
- package/dist/tools/library/fs.d.ts +24 -0
- package/dist/tools/library/fs.js +102 -0
- package/dist/tools/library/index.d.ts +29 -2
- package/dist/tools/library/index.js +40 -0
- package/dist/tools/library/shell.d.ts +45 -0
- package/dist/tools/library/shell.js +70 -0
- package/dist/tools/meta-tools.js +8 -0
- package/dist/tools/registry.d.ts +113 -0
- package/dist/tools/registry.js +180 -4
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,310 +1,92 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/@retinue/agentkit)
|
|
6
|
+
[](https://github.com/Rise-Experts/retinue/blob/main/LICENSE)
|
|
7
|
+
[](https://www.npmjs.com/package/@retinue/agentkit#provenance)
|
|
75
8
|
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
15
|
+
## Install
|
|
85
16
|
|
|
86
17
|
```bash
|
|
87
|
-
npm
|
|
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
|
-
|
|
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
|
-
##
|
|
126
|
-
|
|
127
|
-
Eight booleans, declared and cross-checked against the wiring — REQ-043 (#197).
|
|
24
|
+
## Your first agent
|
|
128
25
|
|
|
129
26
|
```ts
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
167
|
-
|
|
44
|
+
console.log(result.text); // the assistant's reply
|
|
45
|
+
console.log(result.outcome); // "completed" | "failed" | "cancelled" | …
|
|
168
46
|
```
|
|
169
47
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
198
|
-
|
|
|
199
|
-
|
|
|
200
|
-
|
|
|
201
|
-
|
|
|
202
|
-
|
|
|
203
|
-
|
|
|
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
|
-
##
|
|
65
|
+
## Composing it yourself
|
|
209
66
|
|
|
210
|
-
|
|
211
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
##
|
|
79
|
+
## Documentation
|
|
302
80
|
|
|
303
|
-
|
|
304
|
-
|
|
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](
|
|
89
|
+
MIT — see [LICENSE](https://github.com/Rise-Experts/retinue/blob/main/LICENSE).
|
|
309
90
|
|
|
310
|
-
Copyright (c) 2026 Azeem Sarwar and
|
|
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
|
package/dist/agents/agent.d.ts
CHANGED
|
@@ -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;
|