Skip to content

Commit 84de8e1

Browse files
committed
fix: add AGENTS.md (rules) with cautions and subtle differences with agents-known oRPC/tRPC
Signed-off-by: tunnckoCore <5038030+tunnckoCore@users.noreply.github.com>
1 parent d763a24 commit 84de8e1

File tree

1 file changed

+331
-0
lines changed

1 file changed

+331
-0
lines changed

AGENTS.md

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
# Zagora Library
2+
3+
Zagora enables building type-safe and error-safe procedures that encapsulate business logic with robust validation, error handling, and context management. Agents are callable functions that ensure input/output safety and provide structured error responses.
4+
5+
## Highlights
6+
7+
- 🪶 **Minimal:** Lightweight and focused, built on [StandardSchema](https://standardschema.dev) for seamless validation.
8+
- 🛡️ **Error-Safe:** Eliminates exceptions - always `{ ok, data, error }` for predictable, crash-free execution.
9+
- 🦢 **Graceful:** Functions never throw or disrupt your process, akin to `effect.ts` and `neverthrow`.
10+
- 📝 **Typed Errors:** Define error schemas for strongly-typed error helpers, enhancing handler reliability.
11+
- 🧹 **Clean Error Model:** Three distinct error types - unknown, validation, and user-defined—for clarity.
12+
- 🔒 **Type-Safe:** Full type inference across inputs, outputs, errors, context, optionals, and defaults.
13+
-**Ergonomic:** Pure functions with auto-filled defaults, optional args, and detailed diagnostics.
14+
- 🏠 **Familiar:** Echoes remote-RPC patterns from oRPC and tRPC, but focused on libraries, not apps.
15+
- ⚖️ **Unopinionated:** Zero assumptions - no routers, middlewares, or network dependencies.
16+
- 🎁 **No Unwrapping:** Direct access to results, unlike `neverthrow` - no extra steps required.
17+
18+
## Usage
19+
20+
```ts
21+
import { z } from 'zod';
22+
import { zagora } from 'zagora';
23+
24+
const za = zagora();
25+
26+
const getUser = za
27+
.input(z.tuple([
28+
z.string(),
29+
z.number().default(18),
30+
]))
31+
.output(z.object({ name: z.string(), age: z.number(), email: z.string() }))
32+
.handler(async (_, name, age) => {
33+
// name: string;
34+
// age: number; -- even if not passed!
35+
return { name, age, email: `${name.toLowerCase()}@example.com` };
36+
})
37+
.callable();
38+
39+
const result = await getUser('Charlie');
40+
if (result.ok) {
41+
console.log(result.data);
42+
// ^ { name: 'Charlie', age: 18, email: 'charlie@example.com' }
43+
} else {
44+
console.error(result.error);
45+
// ^ { kind: 'UNKNOWN_ERROR', message, cause }
46+
// or
47+
// ^ { kind: 'VALIDATION_ERROR', message, issues: Schema.Issue[] }
48+
}
49+
50+
// primitive input
51+
const helloUppercased = za
52+
.input(z.string())
53+
.handler((_, str) => str.toUpperCase())
54+
.callable();
55+
56+
const res = helloUppercased('Hello world');
57+
58+
if (res.ok) {
59+
console.log(res);
60+
// ^ { ok: true, data: 'HELLO WORLD', error: undefined }
61+
}
62+
63+
// array input
64+
const uppercase = zagora({ autoCallable: true, disableOptions: true })
65+
.input(z.array(z.string()))
66+
.handler((arrayOfStrings) => {
67+
// NOTE: `x` is typed as string too!
68+
return arrayOfStrings.map((x) => x.toUpperCase());
69+
})
70+
71+
const upRes = uppercase(['foo', 'bar', 'qux']);
72+
if (upRes.ok) {
73+
console.log(upRes);
74+
// ^ { ok: true, data: ['FOO', 'BAR', 'QUX' ] }
75+
}
76+
```
77+
78+
You'll also have access to all the types, utils, and error-related stuff through package exports.
79+
80+
```ts
81+
import {
82+
isValidationError,
83+
isInternalError,
84+
isDefinedError,
85+
isZagoraError,
86+
} from 'zagora/errors';
87+
88+
import * ZagoraTypes from 'zagora/types';
89+
import * zagoraUtils from 'zagora/utils';
90+
```
91+
92+
## Creating procedures
93+
94+
Fluent builder API for chaining methods on a Zagora instance:
95+
96+
```typescript
97+
import { zagora } from 'zagora';
98+
import z from 'zod';
99+
100+
const agent = zagora()
101+
.input(z.object({ name: z.string(), age: z.number().default(20) }))
102+
.output(z.object({ greeting: z.string() }))
103+
.handler(({ context }, input) => ({
104+
greeting: `Hello ${input.name}, you are ${input.age} years old!`
105+
}))
106+
.callable();
107+
108+
const result = agent({ name: 'Alice' });
109+
```
110+
111+
**Important:** the handler signature differs from oRPC/tRPC and Zagora requires `.callable` by default.
112+
113+
- oRPC/tRPC - `.handler(({ input, context }) => {})` - always a single object
114+
- zagora with primitive input (string, object, array) - `.handler(({ context }, input) => {})`
115+
- zagora with tuple schemas (spreaded args) - `.handler(({ context }, name, age) => {})`
116+
- zagora with errprs map - `.errors({ NOT_FOUND: z.object({ id: z.string() })}).handler(({ context, errors }, name, age) => {})`
117+
- zagora without options object - `zagora({ disableOptions: true }).input(z.string()).handle((input) => input)`
118+
119+
## Input and Output Validation
120+
121+
Define schemas for type-safe inputs and outputs using Zod, Valibot, or any Standard Schema V1 compliant library:
122+
123+
- **Input Schema**: Validates arguments before execution.
124+
- **Output Schema**: Ensures return values match expectations.
125+
126+
```typescript
127+
const mathAgent = zagora()
128+
.input(z.tuple([z.number(), z.number()]))
129+
.output(z.number())
130+
.handler((_, a, b) => a + b)
131+
.callable();
132+
133+
const sum = mathAgent(5, 10); // { ok: true, data: 15 }
134+
```
135+
136+
## Error Handling
137+
138+
Define custom errors with schemas for structured error responses:
139+
140+
```typescript
141+
const apiAgent = zagora()
142+
.input(z.object({ id: z.string() }))
143+
.output(z.object({ data: z.any() }))
144+
.errors({
145+
NOT_FOUND: z.object({ message: z.string() }),
146+
UNAUTHORIZED: z.object({ userId: z.string() })
147+
})
148+
.handler(({ errors }, { id }) => {
149+
if (!id) throw errors.UNAUTHORIZED({ userId: 'unknown' });
150+
// ... logic
151+
if (!found) throw errors.NOT_FOUND({ message: 'Item not found' });
152+
return { data: item };
153+
})
154+
.callable();
155+
```
156+
157+
Procedures return `ZagoraResult<TOutput, TErrors>` with `ok: true` for success or `ok: false` with typed errors.
158+
159+
## Context Management
160+
161+
Pass shared data like databases or user info via context:
162+
163+
```typescript
164+
const dbAgent = zagora()
165+
.context({ db: myDatabase })
166+
.input(z.string())
167+
.output(z.any())
168+
.handler(({ context }, query) => {
169+
console.log(context.bar); // => 123
170+
171+
return context.db.query(query);
172+
})
173+
.callable({ context: { bar: 123 }});
174+
```
175+
176+
Override context per call: `agent.callable({ context: { db: testDb } })`
177+
178+
## Caching and Memoization
179+
180+
Add caching to avoid redundant computations:
181+
182+
```typescript
183+
const cache = new Map();
184+
const cachedCall = zagora()
185+
.cache(cache)
186+
.input(z.string())
187+
.output(z.string())
188+
.handler((_, input) => expensiveOperation(input))
189+
.callable();
190+
191+
// first time called
192+
cachedCall('foo');
193+
// second is cache hit
194+
cachedCall('foo');
195+
```
196+
197+
Cache can also be passed at execution-site (server handlers) through `.callable({ cache })`.
198+
199+
## Cleaner API - auto callable and disable options
200+
201+
For simpler procedures and API look, enable auto-callable mode to skip `.callable()` and disable passing options to handler:
202+
203+
```typescript
204+
const simpleProcedure = zagora({ autoCallable: true, disableOptions: true })
205+
.input(z.tuple([z.string(), z.number().default(10)]))
206+
.output(z.string())
207+
.handler((str, num) => input.toUpperCase());
208+
209+
const result = simpleProcedure('hello'); // Direct call
210+
```
211+
212+
## Async procedures
213+
214+
Async handlers for I/O operations:
215+
216+
```typescript
217+
const asyncAgent = zagora()
218+
.input(z.string())
219+
.output(z.object({ result: z.string() }))
220+
.handler(async (_, url) => {
221+
const response = await fetch(url);
222+
return { result: await response.text() };
223+
})
224+
.callable();
225+
```
226+
227+
## Best Practices
228+
229+
- Use descriptive schemas for clarity.
230+
- Define errors for all failure cases.
231+
- Leverage context for dependencies.
232+
- Enable caching for performance-critical agents.
233+
- Test agents with various inputs and error scenarios.
234+
235+
Agents built with Zagora are composable, testable, and maintain type safety throughout the application lifecycle.
236+
237+
## Rules and Special Notes for Zagora usage
238+
239+
The following rules outlines critical points, edge cases, and things to be careful about when using Zagora. These are derived from specially noted sections, examples, and warnings in the documentation.
240+
241+
## Error Handling
242+
243+
### Uppercase Error Keys
244+
- **Caution**: All keys in the error map must be uppercased (e.g., `NOT_FOUND`, not `not_found`). TypeScript will report a type error if not.
245+
- **Why**: These keys represent error "kinds" and are used in `result.error.kind`.
246+
247+
### Error Helper Validation
248+
- **Caution**: If you pass invalid or missing keys to error helpers (e.g., `errors.NOT_FOUND({ invalidKey: 'value' })`), you get a `VALIDATION_ERROR` with a `key` property indicating which error validation failed.
249+
- **Example**: `throw errors.RATE_LIMIT({ retryAfter: 'invalid' })``VALIDATION_ERROR` because `retryAfter` expects a number.
250+
- **Tip**: Use `.strict()` on error schemas to throw on unknown keys: `z.object({...}).strict()`.
251+
252+
### Error Type Guards
253+
- **Caution**: Use `isValidationError`, `isInternalError`, `isDefinedError`, `isZagoraError` to narrow error types safely.
254+
- **Note**: Even syntax errors in handlers return `ZagoraResult` with error, never crashing the process.
255+
256+
## Context Management
257+
258+
### Context Merging
259+
- **Caution**: Initial context (from `.context()`) is deep-merged with runtime context (from `.callable({ context })`).
260+
- **Example**: `.context({ userId: 'default' })` + `.callable({ context: { foo: 'bar' } })` → merged `{ userId: 'default', foo: 'bar' }`.
261+
- **Tip**: Useful for dependency injection; override at execution site (e.g., in server handlers).
262+
263+
## Input/Output Validation
264+
265+
### Tuple Inputs (Multiple Arguments)
266+
- **Caution**: Complex feature; schemas like `z.tuple([z.string(), z.number().default(18)])` spread to handler args with defaults/optionals applied.
267+
- **Example**: Handler receives `(name, age)` where `age` is `number` (not `number | undefined`) due to default.
268+
- **Tip**: Supports per-argument validation and diagnostics; missing required args cause `VALIDATION_ERROR`.
269+
270+
### Default Values
271+
- **Caution**: Defaults work at any schema level (objects, tuples, primitives); handler gets fully populated args.
272+
- **Example**: `z.number().default(10)` → no need to pass; handler sees `number`, not `number | undefined`.
273+
274+
## Async Support
275+
276+
### Async Schemas
277+
- **Caution**: If input/output/error schemas are async (e.g., `z.string().refine(async (val) => ...)`, procedure signature remains sync (`ZagoraResult`), but you **must await** at callsite. TypeScript may warn "may not need await" – ignore and await.
278+
- **Why**: StandardSchema limitation; cannot infer async on type-level.
279+
- **Tip**: ArkType doesn't support async schemas, avoiding this issue.
280+
281+
### Handler Async Behavior
282+
- **Caution**: Sync handler → sync procedure; async handler or Promise-returning → async procedure (`Promise<ZagoraResult>`).
283+
- **Note**: Cache async methods force procedure async.
284+
285+
## Caching/Memoization
286+
287+
### Cache Key Composition
288+
- **Caution**: Cache key includes input, input/output/error schemas, and handler function body. Changes to any invalidate cache.
289+
- **Tip**: Useful for custom strategies; memoization out-of-the-box.
290+
291+
### Cache Failures
292+
- **Caution**: Cache adapter throws → `UNKNOWN_ERROR` with `cause` set to original error; process never crashes.
293+
- **Future**: May change to `CACHE_ERROR`.
294+
- **Tip**: If cache has async methods (e.g., `has` is async), procedure becomes async – **await** despite TypeScript warnings.
295+
296+
### Cache Provision
297+
- **Caution**: Provide cache via `.cache()` (definition) or `.callable({ cache })` (execution). Execution-site useful for routers/server handlers.
298+
299+
## Options and Configuration
300+
301+
### Options Object
302+
- **Caution**: Handlers receive `options` as first param: `{ context, errors }`. Typed and merged.
303+
- **Example**: `handler((options, input) => { const { context, errors } = options; ... })`.
304+
305+
### Disable Options
306+
- **Caution**: `zagora({ disableOptions: true })` omits options; handler starts directly with inputs.
307+
- **Example**: `handler((input) => ...)` instead of `handler((options, input) => ...)`.
308+
309+
### Auto-Callable Mode
310+
- **Caution**: `zagora({ autoCallable: true })` returns procedure directly from `.handler()`; skip `.callable()`.
311+
- **Tip**: Combine with `disableOptions` for cleaner APIs.
312+
313+
## Guarantees and Type Safety
314+
315+
### Never-Throwing
316+
- **Caution**: Procedures never throw; all errors (validation, handler, cache) wrapped in `ZagoraResult`.
317+
- **Example**: `throw new Error('Oops')``result.error.kind === 'UNKNOWN_ERROR'`, `result.error.cause.message === 'Oops'`.
318+
319+
### Type Inference
320+
- **Caution**: Full TS support; `result.ok`, `result.data`, `result.error` are discriminated unions.
321+
- **Note**: Complex type system tested; changes caught by type tests.
322+
323+
## General Tips
324+
325+
- **Motivation Reminder**: Zagora produces "just functions" – no network/router assumptions. Focused on low-level, library-building.
326+
- **Comparison**: Unlike oRPC/tRPC (network-focused, always async, single-object inputs), Zagora supports sync, tuples, no middlewares.
327+
- **Alternatives**: Over plain TS (no runtime validation); over standalone schemas (ergonomic layer, unified validation).
328+
- **Testing**: Inspect `test/types-testing.test.ts` for type guarantees.
329+
- **Edge Cases**: Always test with invalid inputs, async paths, and error scenarios.
330+
331+
By heeding these cautions, you can avoid common pitfalls and leverage Zagora's full potential for type-safe, error-safe procedures.

0 commit comments

Comments
 (0)