tjs-lang 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/CONTEXT.md +594 -0
- package/LICENSE +190 -0
- package/README.md +220 -0
- package/bin/benchmarks.ts +351 -0
- package/bin/dev.ts +205 -0
- package/bin/docs.js +170 -0
- package/bin/install-cursor.sh +71 -0
- package/bin/install-vscode.sh +71 -0
- package/bin/select-local-models.d.ts +1 -0
- package/bin/select-local-models.js +28 -0
- package/bin/select-local-models.ts +31 -0
- package/demo/autocomplete.test.ts +232 -0
- package/demo/docs.json +186 -0
- package/demo/examples.test.ts +598 -0
- package/demo/index.html +91 -0
- package/demo/src/autocomplete.ts +482 -0
- package/demo/src/capabilities.ts +859 -0
- package/demo/src/demo-nav.ts +2097 -0
- package/demo/src/examples.test.ts +161 -0
- package/demo/src/examples.ts +476 -0
- package/demo/src/imports.test.ts +196 -0
- package/demo/src/imports.ts +421 -0
- package/demo/src/index.ts +639 -0
- package/demo/src/module-store.ts +635 -0
- package/demo/src/module-sw.ts +132 -0
- package/demo/src/playground.ts +949 -0
- package/demo/src/service-host.ts +389 -0
- package/demo/src/settings.ts +440 -0
- package/demo/src/style.ts +280 -0
- package/demo/src/tjs-playground.ts +1605 -0
- package/demo/src/ts-examples.ts +478 -0
- package/demo/src/ts-playground.ts +1092 -0
- package/demo/static/favicon.svg +30 -0
- package/demo/static/photo-1.jpg +0 -0
- package/demo/static/photo-2.jpg +0 -0
- package/demo/static/texts/ai-history.txt +9 -0
- package/demo/static/texts/coffee-origins.txt +9 -0
- package/demo/static/texts/renewable-energy.txt +9 -0
- package/dist/index.js +256 -0
- package/dist/index.js.map +37 -0
- package/dist/tjs-batteries.js +4 -0
- package/dist/tjs-batteries.js.map +15 -0
- package/dist/tjs-full.js +256 -0
- package/dist/tjs-full.js.map +37 -0
- package/dist/tjs-transpiler.js +220 -0
- package/dist/tjs-transpiler.js.map +21 -0
- package/dist/tjs-vm.js +4 -0
- package/dist/tjs-vm.js.map +14 -0
- package/docs/CNAME +1 -0
- package/docs/favicon.svg +30 -0
- package/docs/index.html +91 -0
- package/docs/index.js +10468 -0
- package/docs/index.js.map +92 -0
- package/docs/photo-1.jpg +0 -0
- package/docs/photo-1.webp +0 -0
- package/docs/photo-2.jpg +0 -0
- package/docs/photo-2.webp +0 -0
- package/docs/texts/ai-history.txt +9 -0
- package/docs/texts/coffee-origins.txt +9 -0
- package/docs/texts/renewable-energy.txt +9 -0
- package/docs/tjs-lang.svg +31 -0
- package/docs/tosijs-agent.svg +31 -0
- package/editors/README.md +325 -0
- package/editors/ace/ajs-mode.js +328 -0
- package/editors/ace/ajs-mode.ts +269 -0
- package/editors/ajs-syntax.ts +212 -0
- package/editors/build-grammars.ts +510 -0
- package/editors/codemirror/ajs-language.js +287 -0
- package/editors/codemirror/ajs-language.ts +1447 -0
- package/editors/codemirror/autocomplete.test.ts +531 -0
- package/editors/codemirror/component.ts +404 -0
- package/editors/monaco/ajs-monarch.js +243 -0
- package/editors/monaco/ajs-monarch.ts +225 -0
- package/editors/tjs-syntax.ts +115 -0
- package/editors/vscode/language-configuration.json +37 -0
- package/editors/vscode/package.json +65 -0
- package/editors/vscode/syntaxes/ajs-injection.tmLanguage.json +107 -0
- package/editors/vscode/syntaxes/ajs.tmLanguage.json +252 -0
- package/editors/vscode/syntaxes/tjs.tmLanguage.json +333 -0
- package/package.json +83 -0
- package/src/cli/commands/check.ts +41 -0
- package/src/cli/commands/convert.ts +133 -0
- package/src/cli/commands/emit.ts +260 -0
- package/src/cli/commands/run.ts +68 -0
- package/src/cli/commands/test.ts +194 -0
- package/src/cli/commands/types.ts +20 -0
- package/src/cli/create-app.ts +236 -0
- package/src/cli/playground.ts +250 -0
- package/src/cli/tjs.ts +166 -0
- package/src/cli/tjsx.ts +160 -0
- package/tjs-lang.svg +31 -0
package/CONTEXT.md
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
<!--{"section": "ajs", "group": "docs", "order": 1, "navTitle": "Technical Context"}-->
|
|
2
|
+
|
|
3
|
+
# tjs-lang Technical Context
|
|
4
|
+
|
|
5
|
+
**Note:** This document provides a technical deep-dive into tjs-lang's architecture and security model. For a general overview, installation instructions, and usage examples, please refer to the main [README.md](./README.md).
|
|
6
|
+
|
|
7
|
+
**tjs-lang** is a secure, environment-agnostic runtime for executing AI agents and logic chains defined as JSON ASTs.
|
|
8
|
+
|
|
9
|
+
**Bundle Size:** ~17KB gzipped. Expressions are evaluated via lightweight AST nodes at runtime, eliminating the need for a parser library (the previous JSEP-based approach was ~50KB gzipped).
|
|
10
|
+
|
|
11
|
+
## 1. Architecture
|
|
12
|
+
|
|
13
|
+
### The Builder (`TypedBuilder`)
|
|
14
|
+
|
|
15
|
+
A fluent TypeScript API that generates a portable JSON AST. It uses a `Proxy` to dynamically infer methods from the registered Atoms, providing a strongly-typed developer experience.
|
|
16
|
+
|
|
17
|
+
It is important to understand that the builder is only for constructing the AST; it does not contain any of the actual implementation logic for the atoms. All execution is handled by the Runtime.
|
|
18
|
+
|
|
19
|
+
**Usage Pattern:**
|
|
20
|
+
|
|
21
|
+
- All logic chains **must** start with `Agent.take()` to define the input schema for the agent.
|
|
22
|
+
- Subsequent atom calls are chained together fluently (e.g., `.varSet(...)`, `.httpFetch(...)`). This creates an implicit `seq` (sequence) of operations.
|
|
23
|
+
- The chain is terminated by calling `.toJSON()` to produce the serializable AST.
|
|
24
|
+
|
|
25
|
+
You can access the builder via `Agent` (for core atoms) or `vm.Agent` (the recommended way to access both core and any custom atoms registered with the VM instance).
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { Agent, s, AgentVM } from 'tjs-lang'
|
|
29
|
+
|
|
30
|
+
// Global Builder (Core Atoms)
|
|
31
|
+
const logic = Agent.take(s.object({ input: s.string }))
|
|
32
|
+
.varSet({ key: 'sum', value: { $expr: 'binary', op: '+', left: { $expr: 'literal', value: 1 }, right: { $expr: 'literal', value: 1 } } })
|
|
33
|
+
|
|
34
|
+
// VM Builder (Custom Atoms)
|
|
35
|
+
const vm = new AgentVM({ myAtom })
|
|
36
|
+
const customLogic = vm.Agent
|
|
37
|
+
.myAtom({ ... })
|
|
38
|
+
.varSet({ ... })
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### The Runtime (`AgentVM`)
|
|
42
|
+
|
|
43
|
+
A stateless Virtual Machine that executes the AST. The runtime contains all the actual implementation logic for the atoms.
|
|
44
|
+
|
|
45
|
+
- **Sandboxed:** No `eval()`. Math/Logic is evaluated safely via AST expression nodes.
|
|
46
|
+
- **Resource Limited:** Enforces `fuel` (gas) limits and execution timeouts per atom.
|
|
47
|
+
- **Capability-Based:** All IO (Network, DB, AI) must be injected via `capabilities` object.
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { AgentVM } from 'tjs-lang'
|
|
51
|
+
const vm = new AgentVM()
|
|
52
|
+
const { result, fuelUsed } = await vm.run(ast, args, {
|
|
53
|
+
capabilities,
|
|
54
|
+
fuel: 1000,
|
|
55
|
+
})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 2. Expression Syntax (ExprNode)
|
|
59
|
+
|
|
60
|
+
Expressions use AST expression nodes (`$expr`) for safe, sandboxed evaluation. Conditions in `if` and `while` atoms use expression strings that are parsed at transpile time.
|
|
61
|
+
|
|
62
|
+
For security, expressions are sandboxed and cannot directly access the agent's state. Use the `vars` parameter to explicitly pass variables from state into the expression scope.
|
|
63
|
+
|
|
64
|
+
### ExprNode Types
|
|
65
|
+
|
|
66
|
+
- **literal:** `{ $expr: 'literal', value: 5 }` - A constant value
|
|
67
|
+
- **ident:** `{ $expr: 'ident', name: 'x' }` - A variable reference
|
|
68
|
+
- **member:** `{ $expr: 'member', object: {...}, property: 'foo' }` - Property access
|
|
69
|
+
- **binary:** `{ $expr: 'binary', op: '+', left: {...}, right: {...} }` - Binary operations
|
|
70
|
+
- **unary:** `{ $expr: 'unary', op: '-', argument: {...} }` - Unary operations
|
|
71
|
+
- **conditional:** `{ $expr: 'conditional', test: {...}, consequent: {...}, alternate: {...} }` - Ternary
|
|
72
|
+
|
|
73
|
+
### Supported Operators
|
|
74
|
+
|
|
75
|
+
- **Binary ops:** `+`, `-`, `*`, `/`, `%`, `**`
|
|
76
|
+
- **Logic:** `&&`, `||`
|
|
77
|
+
- **Comparison:** `==`, `!=`, `>`, `<`, `>=`, `<=`
|
|
78
|
+
- **Member Access:** `obj.prop`, `arr[0]`
|
|
79
|
+
|
|
80
|
+
### Security
|
|
81
|
+
|
|
82
|
+
- **Forbidden:** Function calls, `new`, `this`, global access
|
|
83
|
+
- **Blocked:** Prototype access (`__proto__`, `constructor`)
|
|
84
|
+
|
|
85
|
+
### Fuel Consumption
|
|
86
|
+
|
|
87
|
+
Each expression node evaluation consumes **0.01 fuel**. This prevents deeply nested or recursive expressions from running unchecked. A simple `a + b` costs ~0.03 fuel (two identifiers + one binary op), while complex nested expressions accumulate cost proportionally.
|
|
88
|
+
|
|
89
|
+
## 3. Security Model
|
|
90
|
+
|
|
91
|
+
- **Capabilities:** The VM has no default IO. You must provide `fetch`, `store`, etc., allowing you to mock, proxy, or limit access.
|
|
92
|
+
- **Fuel:** Every atom consumes "fuel". Execution aborts if fuel reaches 0.
|
|
93
|
+
- **Execution Timeout:** The VM enforces a global timeout based on fuel budget (see below).
|
|
94
|
+
- **Atom Timeouts:** Individual atoms have a default timeout (1s) to prevent hangs.
|
|
95
|
+
- **State Isolation:** Each run creates a fresh context. Scopes (loops/maps) use prototype inheritance to isolate local variables.
|
|
96
|
+
|
|
97
|
+
### Execution Timeout
|
|
98
|
+
|
|
99
|
+
The VM enforces a hard timeout on execution to prevent hung agents—safeguarding against code that effectively halts by waiting on slow or non-responsive IO.
|
|
100
|
+
|
|
101
|
+
**How it works:**
|
|
102
|
+
|
|
103
|
+
1. **Automatic Safety Net:** By default, timeout = `fuel × 10ms`. So 1000 fuel = 10 seconds. _For IO-heavy agents with low fuel costs, explicitly set `timeoutMs` to prevent premature timeouts._
|
|
104
|
+
2. **Explicit SLA:** Pass `timeoutMs` to enforce a strict time limit regardless of fuel.
|
|
105
|
+
3. **External Cancellation:** Pass an `AbortSignal` to integrate with external controllers (user cancellation, HTTP timeouts, etc.).
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// Default: 1000 fuel = 10 second timeout
|
|
109
|
+
await vm.run(ast, args, { fuel: 1000 })
|
|
110
|
+
|
|
111
|
+
// Explicit timeout: 5 seconds regardless of fuel
|
|
112
|
+
await vm.run(ast, args, { fuel: 10000, timeoutMs: 5000 })
|
|
113
|
+
|
|
114
|
+
// External abort signal
|
|
115
|
+
const controller = new AbortController()
|
|
116
|
+
setTimeout(() => controller.abort(), 3000) // Cancel after 3s
|
|
117
|
+
await vm.run(ast, args, { signal: controller.signal })
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Resource Cleanup:** When a timeout occurs, the VM passes the abort signal to the currently executing atom via `ctx.signal`. Loop atoms (`while`, `map`, `filter`, `reduce`, `find`) check the signal between iterations. `httpFetch` passes the signal to `fetch` for immediate request cancellation.
|
|
121
|
+
|
|
122
|
+
**Timeout vs Fuel:**
|
|
123
|
+
|
|
124
|
+
- **Fuel** protects against CPU-bound abuse (tight loops burning compute)
|
|
125
|
+
- **Timeout** protects against IO-bound abuse (slow network calls, hung promises)
|
|
126
|
+
|
|
127
|
+
Both work together to ensure the VM cannot be held hostage by untrusted code.
|
|
128
|
+
|
|
129
|
+
**Trust Boundary:** The sandbox protects against malicious _agents_ (untrusted AST), not malicious _atom implementations_. Atoms are registered by the host and are trusted to:
|
|
130
|
+
|
|
131
|
+
1. Be non-blocking (no synchronous CPU-heavy work)
|
|
132
|
+
2. Respect `ctx.signal` for cancellation
|
|
133
|
+
3. Clean up resources when aborted
|
|
134
|
+
|
|
135
|
+
If you write custom atoms, ensure they check `ctx.signal?.aborted` in loops and pass `ctx.signal` to any async operations like `fetch`.
|
|
136
|
+
|
|
137
|
+
### Cost Overrides
|
|
138
|
+
|
|
139
|
+
Default atom costs are guesses. Override them per-run to match your deployment reality:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
await vm.run(ast, args, {
|
|
143
|
+
costOverrides: {
|
|
144
|
+
// Static: fixed cost per invocation
|
|
145
|
+
httpFetch: 50,
|
|
146
|
+
llmPredict: 500,
|
|
147
|
+
|
|
148
|
+
// Dynamic: cost based on input
|
|
149
|
+
storeSet: (input) => JSON.stringify(input.value).length * 0.001,
|
|
150
|
+
llmPredict: (input) => (input.model?.includes('gpt-4') ? 1000 : 100),
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Use cases:
|
|
156
|
+
|
|
157
|
+
- **API rate limits:** Make external API calls expensive to stay under quota
|
|
158
|
+
- **Metered billing:** Reflect actual dollar costs in fuel consumption
|
|
159
|
+
- **Resource protection:** Make database writes cost more than reads
|
|
160
|
+
- **Testing:** Set all costs to 0 to focus on logic, not budgeting
|
|
161
|
+
|
|
162
|
+
### Request Context
|
|
163
|
+
|
|
164
|
+
The `context` option passes request-scoped metadata to atoms. Unlike `args` (agent input) or `capabilities` (IO implementations), context carries ambient data like auth, permissions, and request tracing.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
await vm.run(ast, args, {
|
|
168
|
+
context: {
|
|
169
|
+
userId: 'user-123',
|
|
170
|
+
role: 'admin',
|
|
171
|
+
permissions: ['read:data', 'write:data', 'fetch:external'],
|
|
172
|
+
requestId: 'req-abc-123',
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Atoms access it via `ctx.context`:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const secureFetch = defineAtom(
|
|
181
|
+
'secureFetch',
|
|
182
|
+
s.object({ url: s.string }),
|
|
183
|
+
s.any,
|
|
184
|
+
async (input, ctx) => {
|
|
185
|
+
// Check permissions
|
|
186
|
+
if (!ctx.context?.permissions?.includes('fetch:external')) {
|
|
187
|
+
throw new Error('Not authorized for external fetch')
|
|
188
|
+
}
|
|
189
|
+
return ctx.capabilities.fetch(input.url)
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Design rationale:**
|
|
195
|
+
|
|
196
|
+
- **Immutable:** Context is read-only; agents cannot modify their own permissions
|
|
197
|
+
- **Separate from args:** Auth data doesn't pollute the agent's input schema
|
|
198
|
+
- **Separate from capabilities:** Same capability implementation, different authorization
|
|
199
|
+
- **Composable:** Works with cost overrides for user-tier-based fuel costs
|
|
200
|
+
|
|
201
|
+
**Production patterns:**
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// Firebase/Express integration
|
|
205
|
+
app.post('/run-agent', async (req, res) => {
|
|
206
|
+
const ast = req.body.ast
|
|
207
|
+
const args = req.body.args
|
|
208
|
+
|
|
209
|
+
// Extract auth from request
|
|
210
|
+
const user = await verifyToken(req.headers.authorization)
|
|
211
|
+
|
|
212
|
+
const result = await vm.run(ast, args, {
|
|
213
|
+
context: {
|
|
214
|
+
userId: user.id,
|
|
215
|
+
role: user.role,
|
|
216
|
+
permissions: user.permissions,
|
|
217
|
+
requestId: req.id,
|
|
218
|
+
},
|
|
219
|
+
// User-tier-based costs
|
|
220
|
+
costOverrides: {
|
|
221
|
+
llmPredict: user.tier === 'premium' ? 10 : 100,
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
res.json(result)
|
|
226
|
+
})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## 4. Stored Procedures
|
|
230
|
+
|
|
231
|
+
The procedure store provides a built-in mechanism for storing ASTs as callable tokens. This enables function-pointer-like patterns where behavior can be passed as data.
|
|
232
|
+
|
|
233
|
+
### Storage Model
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// Module-level storage in runtime.ts
|
|
237
|
+
const procedureStore = new Map<
|
|
238
|
+
string,
|
|
239
|
+
{
|
|
240
|
+
ast: any
|
|
241
|
+
createdAt: number
|
|
242
|
+
expiresAt: number
|
|
243
|
+
}
|
|
244
|
+
>()
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Constants:**
|
|
248
|
+
|
|
249
|
+
- `PROCEDURE_TOKEN_PREFIX`: `'proc_'` - All tokens start with this prefix
|
|
250
|
+
- `DEFAULT_PROCEDURE_TTL`: 3,600,000ms (1 hour)
|
|
251
|
+
- `DEFAULT_MAX_AST_SIZE`: 102,400 bytes (100KB)
|
|
252
|
+
|
|
253
|
+
### Token Resolution
|
|
254
|
+
|
|
255
|
+
Tokens can be used anywhere an AST is accepted:
|
|
256
|
+
|
|
257
|
+
1. **`vm.run(token, args)`** - Direct execution via VM
|
|
258
|
+
2. **`agentRun({ agentId: token, input })`** - Execution from within an agent
|
|
259
|
+
3. **`agentRun({ agentId: ast, input })`** - Raw AST also accepted (no storage needed)
|
|
260
|
+
|
|
261
|
+
Resolution happens at runtime. If a string starts with `proc_`, the VM looks it up in the store. Expired or missing tokens throw clear errors.
|
|
262
|
+
|
|
263
|
+
### Fuel Costs
|
|
264
|
+
|
|
265
|
+
| Atom | Cost | Notes |
|
|
266
|
+
| ------------------------ | ---- | ------------------------------- |
|
|
267
|
+
| `storeProcedure` | 1.0 | Plus 0.001 per byte of AST |
|
|
268
|
+
| `releaseProcedure` | 0.5 | Constant |
|
|
269
|
+
| `clearExpiredProcedures` | 0.5 | Plus 0.01 per procedure scanned |
|
|
270
|
+
|
|
271
|
+
### Security Considerations
|
|
272
|
+
|
|
273
|
+
**Memory bounds:** The `maxSize` parameter prevents storing arbitrarily large ASTs. Default 100KB is generous for most agents.
|
|
274
|
+
|
|
275
|
+
**Expiry:** TTL prevents memory leaks from abandoned procedures. The store is in-memory, so procedures don't survive process restarts.
|
|
276
|
+
|
|
277
|
+
**No capability escalation:** Stored procedures inherit the capabilities of the calling context, not the storing context. A malicious agent cannot store a procedure that later executes with elevated privileges.
|
|
278
|
+
|
|
279
|
+
**Token predictability:** Tokens are UUIDs, not sequential. They cannot be enumerated or guessed.
|
|
280
|
+
|
|
281
|
+
### Use Cases
|
|
282
|
+
|
|
283
|
+
**Dynamic dispatch (strategy pattern):**
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
const strategies = ajs`
|
|
287
|
+
function dispatch({ strategyToken, data }) {
|
|
288
|
+
let result = agentRun({ agentId: strategyToken, input: { data } })
|
|
289
|
+
return result
|
|
290
|
+
}
|
|
291
|
+
`
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Worker pool:**
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
const orchestrator = ajs`
|
|
298
|
+
function orchestrate({ workers, tasks }) {
|
|
299
|
+
let results = []
|
|
300
|
+
for (let i = 0; i < tasks.length; i = i + 1) {
|
|
301
|
+
let workerToken = workers[i % workers.length]
|
|
302
|
+
let r = agentRun({ agentId: workerToken, input: tasks[i] })
|
|
303
|
+
results.push(r)
|
|
304
|
+
}
|
|
305
|
+
return { results }
|
|
306
|
+
}
|
|
307
|
+
`
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Callback registration:**
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
const registerCallback = ajs`
|
|
314
|
+
function register({ handler }) {
|
|
315
|
+
let token = storeProcedure({ ast: handler, ttl: 300000 })
|
|
316
|
+
return { callbackId: token }
|
|
317
|
+
}
|
|
318
|
+
`
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## 5. Production Considerations
|
|
322
|
+
|
|
323
|
+
### Recursive Agent Fuel
|
|
324
|
+
|
|
325
|
+
When an agent calls sub-agents via `agentRun`, each sub-agent gets its own fuel budget (passed via the capability). Fuel is **not shared** across the call tree by default.
|
|
326
|
+
|
|
327
|
+
**Why:** The `agentRun` atom delegates to `ctx.capabilities.agent.run`, which the host implements. This gives operators full control over sub-agent resource allocation.
|
|
328
|
+
|
|
329
|
+
**Patterns for shared fuel:**
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
// Option 1: Pass remaining fuel to children
|
|
333
|
+
const sharedFuel = { current: 1000 }
|
|
334
|
+
|
|
335
|
+
const caps = {
|
|
336
|
+
agent: {
|
|
337
|
+
run: async (agentId, input) => {
|
|
338
|
+
if (sharedFuel.current <= 0) throw new Error('Out of shared fuel')
|
|
339
|
+
const result = await vm.run(agents[agentId], input, {
|
|
340
|
+
fuel: sharedFuel.current,
|
|
341
|
+
capabilities: caps,
|
|
342
|
+
})
|
|
343
|
+
sharedFuel.current -= result.fuelUsed
|
|
344
|
+
return result.result
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Option 2: Fixed budget per recursion depth
|
|
350
|
+
const caps = {
|
|
351
|
+
agent: {
|
|
352
|
+
run: async (agentId, input) => {
|
|
353
|
+
// Each child gets 10% of parent's budget
|
|
354
|
+
return vm.run(agents[agentId], input, {
|
|
355
|
+
fuel: 100, // Fixed small budget
|
|
356
|
+
capabilities: caps,
|
|
357
|
+
})
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Streaming and Long-Running Agents
|
|
364
|
+
|
|
365
|
+
The VM returns results only after complete execution. For long-running agents:
|
|
366
|
+
|
|
367
|
+
- Use `timeoutMs` to enforce SLAs
|
|
368
|
+
- Use `AbortSignal` for user-initiated cancellation
|
|
369
|
+
- Use `trace: true` for post-hoc debugging
|
|
370
|
+
|
|
371
|
+
**For real-time streaming**, implement a custom atom that emits intermediate results:
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
const streamingAtom = defineAtom(
|
|
375
|
+
'streamChunk',
|
|
376
|
+
s.object({ data: s.any }),
|
|
377
|
+
s.null,
|
|
378
|
+
async ({ data }, ctx) => {
|
|
379
|
+
// ctx.context contains your streaming callback
|
|
380
|
+
await ctx.context?.onChunk?.(data)
|
|
381
|
+
return null
|
|
382
|
+
}
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
// Usage
|
|
386
|
+
await vm.run(ast, args, {
|
|
387
|
+
context: {
|
|
388
|
+
onChunk: (data) => res.write(JSON.stringify(data) + '\n'),
|
|
389
|
+
},
|
|
390
|
+
})
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Condition String Syntax
|
|
394
|
+
|
|
395
|
+
The condition parser in `if`/`while` atoms supports a subset of expression syntax:
|
|
396
|
+
|
|
397
|
+
| Supported | Example |
|
|
398
|
+
| ------------- | --------------------------------- |
|
|
399
|
+
| Comparisons | `a > b`, `x == 'hello'`, `n != 0` |
|
|
400
|
+
| Logical | `a && b`, `a \|\| b`, `!a` |
|
|
401
|
+
| Arithmetic | `a + b * c`, `(a + b) / c` |
|
|
402
|
+
| Member access | `obj.foo.bar` |
|
|
403
|
+
| Literals | `42`, `"string"`, `true`, `null` |
|
|
404
|
+
|
|
405
|
+
| **Unsupported** | Alternative |
|
|
406
|
+
| ---------------------- | ---------------------------------- |
|
|
407
|
+
| Ternary `a ? b : c` | Use nested `if` atoms |
|
|
408
|
+
| Array index `a[0]` | Use ExprNode with `computed: true` |
|
|
409
|
+
| Function calls `fn(x)` | Use atoms |
|
|
410
|
+
| Chained `a > b > c` | Use `a > b && b > c` |
|
|
411
|
+
|
|
412
|
+
Unsupported syntax now throws a clear error at build time with suggestions.
|
|
413
|
+
|
|
414
|
+
### State Semantics
|
|
415
|
+
|
|
416
|
+
Agents are **not transactional**. If an atom fails mid-execution:
|
|
417
|
+
|
|
418
|
+
- Previous state changes persist
|
|
419
|
+
- No automatic rollback
|
|
420
|
+
- Error is captured in monadic flow (`ctx.error`)
|
|
421
|
+
|
|
422
|
+
This is by design—agents are stateful pipelines, not database transactions. If you need atomicity, implement checkpoint/restore in your capabilities:
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
const caps = {
|
|
426
|
+
store: {
|
|
427
|
+
set: async (key, value) => {
|
|
428
|
+
await db.runTransaction(async (tx) => {
|
|
429
|
+
await tx.set(key, value)
|
|
430
|
+
})
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Error Handling Granularity
|
|
437
|
+
|
|
438
|
+
The `try/catch` atom catches all errors in the try block. There's no selective catch by error type.
|
|
439
|
+
|
|
440
|
+
**Pattern for error type handling:**
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
Agent.take(s.object({})).try({
|
|
444
|
+
try: (b) => b.httpFetch({ url: '...' }).as('response'),
|
|
445
|
+
catch: (b) =>
|
|
446
|
+
b
|
|
447
|
+
.varSet({ key: 'errorType', value: 'unknown' })
|
|
448
|
+
// Check error message patterns
|
|
449
|
+
.if(
|
|
450
|
+
'msg.includes("timeout")',
|
|
451
|
+
{ msg: 'error.message' },
|
|
452
|
+
(then) => then.varSet({ key: 'errorType', value: 'timeout' }),
|
|
453
|
+
(el) =>
|
|
454
|
+
el.if('msg.includes("404")', { msg: 'error.message' }, (then) =>
|
|
455
|
+
then.varSet({ key: 'errorType', value: 'not_found' })
|
|
456
|
+
)
|
|
457
|
+
),
|
|
458
|
+
})
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## 9. Test Coverage
|
|
462
|
+
|
|
463
|
+
**Summary (as of January 2025):**
|
|
464
|
+
|
|
465
|
+
| Metric | Value |
|
|
466
|
+
| --------- | ------ |
|
|
467
|
+
| Tests | 508 |
|
|
468
|
+
| Functions | 84.77% |
|
|
469
|
+
| Lines | 80.36% |
|
|
470
|
+
|
|
471
|
+
### Coverage by Component
|
|
472
|
+
|
|
473
|
+
**Core Runtime (security-critical):**
|
|
474
|
+
|
|
475
|
+
| File | Functions | Lines | Notes |
|
|
476
|
+
| ------------------------- | --------- | ------- | ---------------------------- |
|
|
477
|
+
| `src/runtime.ts` | 100% | 100% | Re-exports |
|
|
478
|
+
| `src/vm.ts` | 100% | 100% | Re-exports |
|
|
479
|
+
| `src/vm/runtime.ts` | 84% | **98%** | Atoms, expression eval, fuel |
|
|
480
|
+
| `src/vm/vm.ts` | 90% | 94% | VM entry point |
|
|
481
|
+
| `src/transpiler/index.ts` | 100% | 100% | AJS transpiler |
|
|
482
|
+
| `src/builder.ts` | 92% | 90% | Fluent builder |
|
|
483
|
+
|
|
484
|
+
**Language/Transpiler:**
|
|
485
|
+
|
|
486
|
+
| File | Functions | Lines | Notes |
|
|
487
|
+
| -------------------------- | --------- | ----- | ------------------------------- |
|
|
488
|
+
| `src/lang/emitters/ast.ts` | 94% | 83% | TJS → AST |
|
|
489
|
+
| `src/lang/parser.ts` | 92% | 82% | TJS parser |
|
|
490
|
+
| `src/lang/inference.ts` | 57% | 60% | Type inference (lower priority) |
|
|
491
|
+
|
|
492
|
+
**Test Categories:**
|
|
493
|
+
|
|
494
|
+
| Category | Tests | Coverage |
|
|
495
|
+
| -------------------------- | ----- | ---------------------------------------------------- |
|
|
496
|
+
| Security (malicious actor) | 10 | Prototype access, SSRF, ReDoS, path traversal |
|
|
497
|
+
| Runtime core | 25+ | Fuel, timeout, tracing, expressions |
|
|
498
|
+
| Stress/Memory | 6 | Large arrays, deep nesting, memory pressure |
|
|
499
|
+
| Capability failures | 10 | Network errors, store failures, partial capabilities |
|
|
500
|
+
| Allocation fuel | 4 | Proportional charging for strings/arrays |
|
|
501
|
+
| Transpiler | 50+ | Language features, edge cases |
|
|
502
|
+
| Use cases | 100+ | RAG, orchestration, client-server patterns |
|
|
503
|
+
|
|
504
|
+
### Running Tests
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
# Full suite
|
|
508
|
+
bun test
|
|
509
|
+
|
|
510
|
+
# Fast (skip LLM and benchmarks)
|
|
511
|
+
SKIP_LLM_TESTS=1 AGENT99_TESTS_SKIP_BENCHMARKS=1 bun test
|
|
512
|
+
|
|
513
|
+
# With coverage
|
|
514
|
+
bun test --coverage
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
## 10. Dependencies
|
|
518
|
+
|
|
519
|
+
### Runtime Dependencies
|
|
520
|
+
|
|
521
|
+
These ship with the library and affect bundle size and security posture.
|
|
522
|
+
|
|
523
|
+
| Package | Version | Size | Purpose | Risk |
|
|
524
|
+
| --------------- | ------- | ----- | --------------------------------------- | ---------------------------------------------------- |
|
|
525
|
+
| `acorn` | ^8.15.0 | ~30KB | JavaScript parser for AJS transpilation | **Low** - Mature, widely audited, Mozilla-maintained |
|
|
526
|
+
| `tosijs-schema` | ^1.2.0 | ~5KB | JSON Schema validation | **Low** - Our library, 96.6% coverage, zero deps |
|
|
527
|
+
| `@codemirror/*` | various | ~50KB | Editor syntax highlighting (optional) | **Low** - Only loaded for editor integration |
|
|
528
|
+
|
|
529
|
+
**Total runtime footprint:** ~33KB gzipped (core), ~83KB with editor support.
|
|
530
|
+
|
|
531
|
+
### tosijs-schema 1.2.0 Coverage
|
|
532
|
+
|
|
533
|
+
Our validation dependency maintains comprehensive test coverage:
|
|
534
|
+
|
|
535
|
+
- **98.25% function coverage, 96.62% line coverage** (146 tests, 349 assertions)
|
|
536
|
+
- **Edge cases tested:** NaN, Infinity, -0, sparse arrays, unicode, deeply nested structures
|
|
537
|
+
- **Complex unions:** nested unions, discriminated unions, union of arrays, union with null/undefined
|
|
538
|
+
- **Format validators:** email, ipv4 boundaries, url protocols, datetime variants
|
|
539
|
+
- **Strict mode:** `validate(data, schema, { strict: true })` validates every item
|
|
540
|
+
|
|
541
|
+
Because tosijs-schema schemas are JSON data (not code), library coverage extends to user-defined schemas—unlike Zod where user schema compositions are untested code.
|
|
542
|
+
|
|
543
|
+
### Development Dependencies
|
|
544
|
+
|
|
545
|
+
Not shipped to users. Used for building, testing, and development.
|
|
546
|
+
|
|
547
|
+
| Package | Purpose | Notes |
|
|
548
|
+
| ---------------------- | ----------------------------- | ----------------------- |
|
|
549
|
+
| `typescript` | Type checking and compilation | Standard |
|
|
550
|
+
| `bun` | Runtime, bundler, test runner | Fast, modern |
|
|
551
|
+
| `eslint` / `prettier` | Code quality | Standard |
|
|
552
|
+
| `acorn-walk` | AST traversal for transpiler | Only used at build time |
|
|
553
|
+
| `codemirror` | Editor components for demo | Demo only |
|
|
554
|
+
| `tosijs` / `tosijs-ui` | Demo UI framework | Demo only |
|
|
555
|
+
| `happy-dom` | DOM mocking for tests | Test only |
|
|
556
|
+
| `vitest` | Alternative test runner | Optional |
|
|
557
|
+
|
|
558
|
+
### Dependency Risk Assessment
|
|
559
|
+
|
|
560
|
+
**Supply Chain:**
|
|
561
|
+
|
|
562
|
+
| Risk | Mitigation |
|
|
563
|
+
| ------------------------ | -------------------------------------------------------- |
|
|
564
|
+
| Acorn compromise | Mature project (10+ years), Mozilla backing, widely used |
|
|
565
|
+
| tosijs-schema compromise | We control this library |
|
|
566
|
+
| Transitive dependencies | Minimal—acorn has 0 deps, tosijs-schema has 0 deps |
|
|
567
|
+
|
|
568
|
+
**Version Pinning:**
|
|
569
|
+
|
|
570
|
+
- Production dependencies use caret (`^`) for patch updates
|
|
571
|
+
- Consider using exact versions or lockfile for production deployments
|
|
572
|
+
|
|
573
|
+
**Audit:**
|
|
574
|
+
|
|
575
|
+
```bash
|
|
576
|
+
# Check for known vulnerabilities
|
|
577
|
+
bun audit
|
|
578
|
+
# or
|
|
579
|
+
npm audit
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### What We Don't Depend On
|
|
583
|
+
|
|
584
|
+
Notably absent from our dependency tree:
|
|
585
|
+
|
|
586
|
+
| Common Dependency | Why We Don't Use It |
|
|
587
|
+
| ----------------- | --------------------------------------- |
|
|
588
|
+
| lodash | Native JS methods suffice |
|
|
589
|
+
| axios | Native fetch + capability injection |
|
|
590
|
+
| moment/dayjs | Built-in Date wrapper in expressions |
|
|
591
|
+
| zod/yup | tosijs-schema is lighter and sufficient |
|
|
592
|
+
| jsep | Replaced with acorn + custom AST nodes |
|
|
593
|
+
|
|
594
|
+
This minimal dependency approach reduces supply chain risk and bundle size.
|