tjs-lang 0.8.1 → 0.8.3
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/CLAUDE.md +13 -4
- package/demo/autocomplete.test.ts +37 -0
- package/demo/docs.json +698 -8
- package/demo/examples.test.ts +6 -2
- package/demo/src/autocomplete.ts +40 -2
- package/demo/src/introspection-bridge.ts +140 -0
- package/demo/src/introspection-doc.test.ts +63 -0
- package/demo/src/playground-shared.ts +101 -16
- package/demo/src/playground-test-results.test.ts +112 -0
- package/demo/src/tjs-playground.ts +32 -5
- package/dist/examples/modules/dist/main.d.ts +34 -0
- package/dist/examples/modules/dist/math.d.ts +120 -0
- package/dist/index.js +115 -112
- package/dist/index.js.map +4 -4
- package/dist/src/lang/core.d.ts +1 -1
- package/dist/src/lang/dialect.d.ts +35 -0
- package/dist/src/lang/emitters/ast.d.ts +1 -1
- package/dist/src/lang/emitters/js-tests.d.ts +7 -0
- package/dist/src/lang/emitters/js.d.ts +12 -0
- package/dist/src/lang/index.d.ts +2 -1
- package/dist/src/lang/parser-types.d.ts +17 -0
- package/dist/src/lang/parser.d.ts +18 -0
- package/dist/src/lang/transpiler.d.ts +1 -0
- package/dist/src/lang/types.d.ts +18 -0
- package/dist/src/vm/runtime.d.ts +18 -0
- package/dist/src/vm/vm.d.ts +15 -1
- package/dist/tjs-batteries.js +2 -2
- package/dist/tjs-batteries.js.map +2 -2
- package/dist/tjs-eval.js +43 -43
- package/dist/tjs-eval.js.map +3 -3
- package/dist/tjs-from-ts.js +1 -1
- package/dist/tjs-from-ts.js.map +2 -2
- package/dist/tjs-lang.js +76 -73
- package/dist/tjs-lang.js.map +4 -4
- package/dist/tjs-vm.js +49 -49
- package/dist/tjs-vm.js.map +3 -3
- package/editors/codemirror/ajs-language.ts +130 -44
- package/editors/codemirror/completion-source.test.ts +114 -0
- package/editors/introspect-value.test.ts +61 -0
- package/editors/introspect-value.ts +86 -0
- package/editors/scope-symbols.test.ts +113 -0
- package/editors/scope-symbols.ts +173 -0
- package/llms.txt +1 -0
- package/package.json +21 -1
- package/src/batteries/audit.ts +3 -2
- package/src/cli/commands/check.ts +3 -2
- package/src/cli/commands/emit.ts +4 -2
- package/src/cli/commands/run.ts +6 -2
- package/src/cli/commands/types.ts +2 -2
- package/src/lang/codegen.test.ts +4 -1
- package/src/lang/core.ts +6 -4
- package/src/lang/dialect.test.ts +63 -0
- package/src/lang/dialect.ts +50 -0
- package/src/lang/emitters/ast.ts +145 -2
- package/src/lang/emitters/js-tests.ts +46 -37
- package/src/lang/emitters/js.ts +19 -2
- package/src/lang/features.test.ts +6 -5
- package/src/lang/index.ts +40 -5
- package/src/lang/parser-types.ts +17 -0
- package/src/lang/parser.test.ts +12 -6
- package/src/lang/parser.ts +113 -3
- package/src/lang/predicate-schema.test.ts +97 -0
- package/src/lang/predicate-schema.ts +168 -0
- package/src/lang/predicate.test.ts +184 -0
- package/src/lang/predicate.ts +550 -0
- package/src/lang/subset-invariant.test.ts +90 -0
- package/src/lang/suggest.test.ts +84 -0
- package/src/lang/transpiler.ts +34 -0
- package/src/lang/types.ts +18 -0
- package/src/lang/wasm.test.ts +8 -2
- package/src/linalg/linalg.test.ts +8 -2
- package/src/use-cases/batteries.test.ts +4 -0
- package/src/use-cases/local-helpers.test.ts +219 -0
- package/src/use-cases/timeout-overrides.test.ts +169 -0
- package/src/vm/atom-effects.test.ts +72 -0
- package/src/vm/atoms/batteries.ts +29 -3
- package/src/vm/runtime.ts +140 -4
- package/src/vm/vm.ts +47 -9
|
@@ -51,7 +51,12 @@ export const storeVectorize = defineAtom(
|
|
|
51
51
|
const resolvedText = resolveValue(text, ctx)
|
|
52
52
|
return vectorCap.embed(resolvedText)
|
|
53
53
|
},
|
|
54
|
-
|
|
54
|
+
// Network embedding call; a cold model can exceed the 1s atom default.
|
|
55
|
+
{
|
|
56
|
+
docs: 'Generate embeddings using vector battery',
|
|
57
|
+
cost: 20,
|
|
58
|
+
timeoutMs: 60000,
|
|
59
|
+
}
|
|
55
60
|
)
|
|
56
61
|
|
|
57
62
|
// store.createCollection
|
|
@@ -97,7 +102,12 @@ export const storeVectorAdd = defineAtom(
|
|
|
97
102
|
|
|
98
103
|
return storeCap.vectorAdd(resolvedColl, resolvedDoc)
|
|
99
104
|
},
|
|
100
|
-
|
|
105
|
+
// May embed the doc via the store (network IO); allow for a cold model.
|
|
106
|
+
{
|
|
107
|
+
docs: 'Add a document to a vector store collection',
|
|
108
|
+
cost: 5,
|
|
109
|
+
timeoutMs: 60000,
|
|
110
|
+
}
|
|
101
111
|
)
|
|
102
112
|
|
|
103
113
|
// store.search (Vector Search)
|
|
@@ -167,7 +177,11 @@ export const llmPredictBattery = defineAtom(
|
|
|
167
177
|
resolvedFormat
|
|
168
178
|
)
|
|
169
179
|
},
|
|
170
|
-
{
|
|
180
|
+
{
|
|
181
|
+
docs: 'Generate completion using LLM battery',
|
|
182
|
+
cost: 100,
|
|
183
|
+
timeoutMs: 120000,
|
|
184
|
+
}
|
|
171
185
|
)
|
|
172
186
|
|
|
173
187
|
// Vision battery interface (multimodal)
|
|
@@ -215,3 +229,15 @@ export const llmVision = defineAtom(
|
|
|
215
229
|
},
|
|
216
230
|
{ docs: 'Analyze images using a vision model', timeoutMs: 120000, cost: 150 }
|
|
217
231
|
)
|
|
232
|
+
|
|
233
|
+
// Every battery atom is IO (embedding/LLM/vector-store network calls).
|
|
234
|
+
for (const atom of [
|
|
235
|
+
storeVectorize,
|
|
236
|
+
storeCreateCollection,
|
|
237
|
+
storeVectorAdd,
|
|
238
|
+
storeSearch,
|
|
239
|
+
llmPredictBattery,
|
|
240
|
+
llmVision,
|
|
241
|
+
]) {
|
|
242
|
+
atom.effects = 'io'
|
|
243
|
+
}
|
package/src/vm/runtime.ts
CHANGED
|
@@ -151,6 +151,11 @@ export type CostOverride =
|
|
|
151
151
|
| number
|
|
152
152
|
| ((input: any, ctx: RuntimeContext) => number)
|
|
153
153
|
|
|
154
|
+
/** Timeout override: static number (ms) or dynamic function. 0 disables. */
|
|
155
|
+
export type TimeoutOverride =
|
|
156
|
+
| number
|
|
157
|
+
| ((input: any, ctx: RuntimeContext) => number)
|
|
158
|
+
|
|
154
159
|
export interface RuntimeContext {
|
|
155
160
|
fuel: { current: number }
|
|
156
161
|
args: Record<string, any>
|
|
@@ -165,12 +170,28 @@ export interface RuntimeContext {
|
|
|
165
170
|
warnings?: string[] // Non-fatal warnings (e.g., console.warn)
|
|
166
171
|
signal?: AbortSignal // External abort signal for timeout enforcement
|
|
167
172
|
costOverrides?: Record<string, CostOverride> // Per-atom cost overrides
|
|
173
|
+
timeoutOverrides?: Record<string, TimeoutOverride> // Per-atom timeout overrides (ms, 0 disables)
|
|
168
174
|
context?: Record<string, any> // Immutable request-scoped metadata (auth, permissions, etc.)
|
|
169
175
|
runCodeDepth?: number // Track nested runCode calls to prevent infinite recursion
|
|
176
|
+
localCall?: boolean // Inside a callLocal helper body — return may be a non-object scalar
|
|
177
|
+
helpers?: Record<string, { steps: any[]; paramNames: string[] }> // Local helper bodies, called by name
|
|
178
|
+
callDepth?: number // Helper call nesting depth — guards against host-stack overflow on deep recursion
|
|
170
179
|
}
|
|
171
180
|
|
|
172
181
|
export type AtomExec = (step: any, ctx: RuntimeContext) => Promise<void>
|
|
173
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Effect classification of an atom.
|
|
185
|
+
* - `'pure'`: deterministic, no IO, no capability access, no observable side
|
|
186
|
+
* effects — safe inside a synchronous predicate (see the predicate verifier).
|
|
187
|
+
* - `'io'`: touches `ctx.capabilities` (fetch/store/llm/agent/code), or is
|
|
188
|
+
* nondeterministic (random/uuid), or has side effects (console). Not allowed
|
|
189
|
+
* in a predicate.
|
|
190
|
+
* Defaults to `'pure'`; effectful atoms must opt into `'io'`. The invariant
|
|
191
|
+
* "anything touching ctx.capabilities is tagged 'io'" is guarded by a test.
|
|
192
|
+
*/
|
|
193
|
+
export type AtomEffects = 'pure' | 'io'
|
|
194
|
+
|
|
174
195
|
export interface AtomDef {
|
|
175
196
|
op: OpCode
|
|
176
197
|
inputSchema: any
|
|
@@ -179,6 +200,7 @@ export interface AtomDef {
|
|
|
179
200
|
docs?: string
|
|
180
201
|
timeoutMs?: number
|
|
181
202
|
cost?: number | ((input: any, ctx: RuntimeContext) => number)
|
|
203
|
+
effects?: AtomEffects
|
|
182
204
|
}
|
|
183
205
|
|
|
184
206
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -190,6 +212,8 @@ export interface AtomOptions {
|
|
|
190
212
|
docs?: string
|
|
191
213
|
timeoutMs?: number
|
|
192
214
|
cost?: number | ((input: any, ctx: RuntimeContext) => number)
|
|
215
|
+
/** Effect class — defaults to `'pure'`; effectful atoms set `'io'`. */
|
|
216
|
+
effects?: AtomEffects
|
|
193
217
|
}
|
|
194
218
|
|
|
195
219
|
export interface RunResult {
|
|
@@ -1338,6 +1362,7 @@ export function defineAtom<I extends Record<string, any>, O = any>(
|
|
|
1338
1362
|
docs = '',
|
|
1339
1363
|
timeoutMs = 1000,
|
|
1340
1364
|
cost = 1,
|
|
1365
|
+
effects = 'pure',
|
|
1341
1366
|
} = typeof options === 'string' ? { docs: options } : options
|
|
1342
1367
|
|
|
1343
1368
|
const exec: AtomExec = async (step: any, ctx: RuntimeContext) => {
|
|
@@ -1363,18 +1388,25 @@ export function defineAtom<I extends Record<string, any>, O = any>(
|
|
|
1363
1388
|
return
|
|
1364
1389
|
}
|
|
1365
1390
|
|
|
1366
|
-
// 3. Execution with Timeout
|
|
1391
|
+
// 3. Execution with Timeout (per-atom override > atom default)
|
|
1392
|
+
const overrideTimeout = ctx.timeoutOverrides?.[op]
|
|
1393
|
+
const baseTimeout =
|
|
1394
|
+
overrideTimeout !== undefined ? overrideTimeout : timeoutMs
|
|
1395
|
+
const effectiveTimeout =
|
|
1396
|
+
typeof baseTimeout === 'function'
|
|
1397
|
+
? baseTimeout(inputData, ctx)
|
|
1398
|
+
: baseTimeout
|
|
1367
1399
|
let timer: any
|
|
1368
1400
|
const execute = async () => fn(step as I, ctx)
|
|
1369
1401
|
|
|
1370
1402
|
result =
|
|
1371
|
-
|
|
1403
|
+
effectiveTimeout > 0
|
|
1372
1404
|
? await Promise.race([
|
|
1373
1405
|
execute(),
|
|
1374
1406
|
new Promise<never>((_, reject) => {
|
|
1375
1407
|
timer = setTimeout(
|
|
1376
1408
|
() => reject(new Error(`Atom '${op}' timed out`)),
|
|
1377
|
-
|
|
1409
|
+
effectiveTimeout
|
|
1378
1410
|
)
|
|
1379
1411
|
}),
|
|
1380
1412
|
]).finally(() => clearTimeout(timer))
|
|
@@ -1430,6 +1462,7 @@ export function defineAtom<I extends Record<string, any>, O = any>(
|
|
|
1430
1462
|
docs,
|
|
1431
1463
|
timeoutMs,
|
|
1432
1464
|
cost,
|
|
1465
|
+
effects,
|
|
1433
1466
|
create: (input: I) => ({ op, ...input }),
|
|
1434
1467
|
}
|
|
1435
1468
|
}
|
|
@@ -1562,8 +1595,11 @@ export const ret = defineAtom(
|
|
|
1562
1595
|
if ('value' in step) {
|
|
1563
1596
|
const res = resolveValue(step.value, ctx)
|
|
1564
1597
|
|
|
1565
|
-
// Enforce object returns — agents must return objects for composability
|
|
1598
|
+
// Enforce object returns — agents must return objects for composability.
|
|
1599
|
+
// Helper bodies (callLocal) are exempt: they're internal calls and may
|
|
1600
|
+
// return scalars, arrays, etc. like ordinary functions.
|
|
1566
1601
|
if (
|
|
1602
|
+
!ctx.localCall &&
|
|
1567
1603
|
res !== undefined &&
|
|
1568
1604
|
res !== null &&
|
|
1569
1605
|
!isAgentError(res) &&
|
|
@@ -1786,6 +1822,71 @@ export const scope = defineAtom(
|
|
|
1786
1822
|
{ docs: 'Create new scope', timeoutMs: 0, cost: 0.1 }
|
|
1787
1823
|
)
|
|
1788
1824
|
|
|
1825
|
+
/**
|
|
1826
|
+
* Maximum helper-call nesting depth. Fuel + timeout bound the total *work* a
|
|
1827
|
+
* recursive helper can do, but deeply-nested `await seq.exec` would overflow
|
|
1828
|
+
* the host JS stack (an uncatchable RangeError) before fuel runs out. This cap
|
|
1829
|
+
* converts runaway recursion into a clean monadic error instead.
|
|
1830
|
+
*/
|
|
1831
|
+
export const MAX_CALL_DEPTH = 256
|
|
1832
|
+
|
|
1833
|
+
export const callLocal = defineAtom(
|
|
1834
|
+
'callLocal',
|
|
1835
|
+
s.object({
|
|
1836
|
+
name: s.string,
|
|
1837
|
+
args: s.array(s.any),
|
|
1838
|
+
}),
|
|
1839
|
+
undefined,
|
|
1840
|
+
async ({ name, args }, ctx) => {
|
|
1841
|
+
const helper = ctx.helpers?.[name as string]
|
|
1842
|
+
if (!helper) {
|
|
1843
|
+
ctx.error = new AgentError(`Unknown helper: ${name}`, 'callLocal')
|
|
1844
|
+
return ctx.error
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
const depth = (ctx.callDepth ?? 0) + 1
|
|
1848
|
+
if (depth > MAX_CALL_DEPTH) {
|
|
1849
|
+
ctx.error = new AgentError(
|
|
1850
|
+
`Maximum helper call depth (${MAX_CALL_DEPTH}) exceeded — likely infinite recursion in '${name}'`,
|
|
1851
|
+
'callLocal'
|
|
1852
|
+
)
|
|
1853
|
+
return ctx.error
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// Resolve each argument expression in the caller's scope
|
|
1857
|
+
const resolvedArgs = (args as any[]).map((arg) => resolveValue(arg, ctx))
|
|
1858
|
+
|
|
1859
|
+
// Isolated scope: helpers are top-level sibling functions, not nested
|
|
1860
|
+
// closures, so they see ONLY their params — never the caller's locals.
|
|
1861
|
+
// Capabilities/fuel/resolver/etc. are shared via the spread; state and
|
|
1862
|
+
// consts start fresh. localCall exempts the helper's `return` from the
|
|
1863
|
+
// agent object-return contract (helpers may return scalars/arrays like
|
|
1864
|
+
// ordinary functions). callDepth guards against host-stack overflow.
|
|
1865
|
+
const scopedCtx: RuntimeContext = {
|
|
1866
|
+
...ctx,
|
|
1867
|
+
state: {},
|
|
1868
|
+
consts: new Set(),
|
|
1869
|
+
output: undefined,
|
|
1870
|
+
error: undefined,
|
|
1871
|
+
localCall: true,
|
|
1872
|
+
callDepth: depth,
|
|
1873
|
+
}
|
|
1874
|
+
for (let i = 0; i < helper.paramNames.length; i++) {
|
|
1875
|
+
scopedCtx.state[helper.paramNames[i]] = resolvedArgs[i]
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
await seq.exec({ op: 'seq', steps: helper.steps } as any, scopedCtx)
|
|
1879
|
+
|
|
1880
|
+
// Propagate errors but NOT output — the helper's return becomes this
|
|
1881
|
+
// atom's result, captured into the caller's named result variable by
|
|
1882
|
+
// the standard exec wrapper. Unlike `scope`, it does not bubble up
|
|
1883
|
+
// to ctx.output (so a helper return doesn't exit the caller agent).
|
|
1884
|
+
if (scopedCtx.error) ctx.error = scopedCtx.error
|
|
1885
|
+
return scopedCtx.output
|
|
1886
|
+
},
|
|
1887
|
+
{ docs: 'Invoke a local helper function by name', timeoutMs: 0, cost: 0.1 }
|
|
1888
|
+
)
|
|
1889
|
+
|
|
1789
1890
|
// 3. List (Cost 1)
|
|
1790
1891
|
|
|
1791
1892
|
/*#
|
|
@@ -2991,6 +3092,7 @@ export const coreAtoms = {
|
|
|
2991
3092
|
varsLet,
|
|
2992
3093
|
varsExport,
|
|
2993
3094
|
scope,
|
|
3095
|
+
callLocal,
|
|
2994
3096
|
map,
|
|
2995
3097
|
filter,
|
|
2996
3098
|
reduce,
|
|
@@ -3029,3 +3131,37 @@ export const coreAtoms = {
|
|
|
3029
3131
|
releaseProcedure,
|
|
3030
3132
|
clearExpiredProcedures,
|
|
3031
3133
|
}
|
|
3134
|
+
|
|
3135
|
+
/**
|
|
3136
|
+
* Effectful core atoms — anything that touches `ctx.capabilities`
|
|
3137
|
+
* (fetch/store/llm/agent/code), is nondeterministic (random/uuid), or has
|
|
3138
|
+
* observable side effects (console). Tagged centrally so the list reads as one
|
|
3139
|
+
* audit surface; a test asserts every capability-touching atom is in here.
|
|
3140
|
+
* Everything else defaults to `effects: 'pure'`.
|
|
3141
|
+
*/
|
|
3142
|
+
export const EFFECTFUL_CORE_OPS = [
|
|
3143
|
+
'httpFetch',
|
|
3144
|
+
'storeGet',
|
|
3145
|
+
'storeSet',
|
|
3146
|
+
'storeQuery',
|
|
3147
|
+
'storeVectorSearch',
|
|
3148
|
+
'llmPredict',
|
|
3149
|
+
'agentRun',
|
|
3150
|
+
'transpileCode',
|
|
3151
|
+
'runCode',
|
|
3152
|
+
'random',
|
|
3153
|
+
'uuid',
|
|
3154
|
+
'consoleLog',
|
|
3155
|
+
'consoleWarn',
|
|
3156
|
+
'consoleError',
|
|
3157
|
+
'storeProcedure',
|
|
3158
|
+
'releaseProcedure',
|
|
3159
|
+
'clearExpiredProcedures',
|
|
3160
|
+
'cache',
|
|
3161
|
+
'memoize',
|
|
3162
|
+
] as const
|
|
3163
|
+
|
|
3164
|
+
for (const op of EFFECTFUL_CORE_OPS) {
|
|
3165
|
+
const atom = (coreAtoms as Record<string, AtomDef>)[op]
|
|
3166
|
+
if (atom) atom.effects = 'io'
|
|
3167
|
+
}
|
package/src/vm/vm.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type RunResult,
|
|
5
5
|
type RuntimeContext,
|
|
6
6
|
type CostOverride,
|
|
7
|
+
type TimeoutOverride,
|
|
7
8
|
coreAtoms,
|
|
8
9
|
AgentError,
|
|
9
10
|
isProcedureToken,
|
|
@@ -13,16 +14,49 @@ import { TypedBuilder, type BaseNode, type BuilderType } from '../builder'
|
|
|
13
14
|
import { validate } from 'tosijs-schema'
|
|
14
15
|
import { transpile } from '../lang/core'
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Floor for the run-level default timeout. The actual default is derived from
|
|
19
|
+
* the registered atoms (slowest atom × 2 — see `defaultRunTimeout`), but never
|
|
20
|
+
* drops below this for a VM whose atoms are all fast.
|
|
21
|
+
*/
|
|
22
|
+
const MIN_DEFAULT_RUN_TIMEOUT_MS = 60_000
|
|
18
23
|
|
|
19
24
|
export class AgentVM<M extends Record<string, Atom<any, any>>> {
|
|
20
25
|
readonly atoms: typeof coreAtoms & M
|
|
21
26
|
|
|
27
|
+
private _defaultRunTimeout?: number
|
|
28
|
+
|
|
22
29
|
constructor(customAtoms: M = {} as M) {
|
|
23
30
|
this.atoms = { ...coreAtoms, ...customAtoms }
|
|
24
31
|
}
|
|
25
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Default run-level wall-clock timeout when `run()` is given no explicit
|
|
35
|
+
* `timeoutMs`. Derived as `max(per-atom timeoutMs) × 2` over the registered
|
|
36
|
+
* atoms (with headroom for an agent that chains a couple of slow calls), so
|
|
37
|
+
* the run-level backstop can never be shorter than the slowest single atom's
|
|
38
|
+
* own budget — otherwise that per-atom budget would be dead config (e.g.
|
|
39
|
+
* `llmVision`/`llmPredictBattery` are 120s; a fixed 60s run default would kill
|
|
40
|
+
* them mid-call). Atoms with `timeoutMs: 0` (no timeout, e.g. `seq`) are
|
|
41
|
+
* excluded; the result is floored at {@link MIN_DEFAULT_RUN_TIMEOUT_MS}.
|
|
42
|
+
* Self-adjusting: registering a slower custom atom raises the default.
|
|
43
|
+
*/
|
|
44
|
+
get defaultRunTimeout(): number {
|
|
45
|
+
if (this._defaultRunTimeout === undefined) {
|
|
46
|
+
let slowest = 0
|
|
47
|
+
for (const atom of Object.values(this.atoms)) {
|
|
48
|
+
// undefined timeoutMs means the per-atom default (1000ms); 0 means none.
|
|
49
|
+
const t = (atom as any).timeoutMs ?? 1000
|
|
50
|
+
if (t > 0 && t > slowest) slowest = t
|
|
51
|
+
}
|
|
52
|
+
this._defaultRunTimeout = Math.max(
|
|
53
|
+
MIN_DEFAULT_RUN_TIMEOUT_MS,
|
|
54
|
+
slowest * 2
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
return this._defaultRunTimeout
|
|
58
|
+
}
|
|
59
|
+
|
|
26
60
|
get builder(): BuilderType<typeof coreAtoms & M> {
|
|
27
61
|
return new TypedBuilder(this.atoms) as any
|
|
28
62
|
}
|
|
@@ -77,9 +111,10 @@ export class AgentVM<M extends Record<string, Atom<any, any>>> {
|
|
|
77
111
|
fuel?: number
|
|
78
112
|
capabilities?: Capabilities
|
|
79
113
|
trace?: boolean
|
|
80
|
-
timeoutMs?: number //
|
|
114
|
+
timeoutMs?: number // Wall-clock cap on the whole run (default: slowest atom × 2, min 60s — see defaultRunTimeout)
|
|
81
115
|
signal?: AbortSignal // External abort signal (e.g., from caller)
|
|
82
116
|
costOverrides?: Record<string, CostOverride> // Per-atom fuel cost overrides
|
|
117
|
+
timeoutOverrides?: Record<string, TimeoutOverride> // Per-atom timeout overrides (ms, 0 disables)
|
|
83
118
|
context?: Record<string, any> // Request-scoped metadata (auth, permissions, etc.)
|
|
84
119
|
} = {}
|
|
85
120
|
): Promise<RunResult> {
|
|
@@ -105,9 +140,10 @@ export class AgentVM<M extends Record<string, Atom<any, any>>> {
|
|
|
105
140
|
|
|
106
141
|
const startFuel = options.fuel ?? 1000
|
|
107
142
|
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
143
|
+
// Run-level wall-clock timeout. Agents are typically IO-bound; the default
|
|
144
|
+
// is derived from the registered atoms (slowest × 2) so it always covers the
|
|
145
|
+
// slowest atom's own budget. See `defaultRunTimeout`.
|
|
146
|
+
const timeoutMs = options.timeoutMs ?? this.defaultRunTimeout
|
|
111
147
|
|
|
112
148
|
// Default Capabilities
|
|
113
149
|
const capabilities = options.capabilities ?? {}
|
|
@@ -160,8 +196,10 @@ export class AgentVM<M extends Record<string, Atom<any, any>>> {
|
|
|
160
196
|
output: undefined,
|
|
161
197
|
signal: controller.signal,
|
|
162
198
|
costOverrides: options.costOverrides,
|
|
199
|
+
timeoutOverrides: options.timeoutOverrides,
|
|
163
200
|
context: options.context,
|
|
164
201
|
warnings, // Shared warnings array
|
|
202
|
+
helpers: (ast as any).helpers, // Local helper bodies, called by name via callLocal
|
|
165
203
|
}
|
|
166
204
|
|
|
167
205
|
if (options.trace) {
|
|
@@ -197,7 +235,7 @@ export class AgentVM<M extends Record<string, Atom<any, any>>> {
|
|
|
197
235
|
controller.signal.addEventListener('abort', () => {
|
|
198
236
|
reject(
|
|
199
237
|
new Error(
|
|
200
|
-
`Execution timeout after ${timeoutMs}ms
|
|
238
|
+
`Execution timeout after ${timeoutMs}ms. Pass a higher \`timeoutMs\` to vm.run() or set per-atom \`timeoutOverrides\` for slow IO atoms.`
|
|
201
239
|
)
|
|
202
240
|
)
|
|
203
241
|
})
|
|
@@ -205,7 +243,7 @@ export class AgentVM<M extends Record<string, Atom<any, any>>> {
|
|
|
205
243
|
if (controller.signal.aborted) {
|
|
206
244
|
reject(
|
|
207
245
|
new Error(
|
|
208
|
-
`Execution timeout after ${timeoutMs}ms
|
|
246
|
+
`Execution timeout after ${timeoutMs}ms. Pass a higher \`timeoutMs\` to vm.run() or set per-atom \`timeoutOverrides\` for slow IO atoms.`
|
|
209
247
|
)
|
|
210
248
|
)
|
|
211
249
|
}
|
|
@@ -219,7 +257,7 @@ export class AgentVM<M extends Record<string, Atom<any, any>>> {
|
|
|
219
257
|
controller.signal.aborted
|
|
220
258
|
) {
|
|
221
259
|
ctx.error = new AgentError(
|
|
222
|
-
`Execution timeout after ${timeoutMs}ms
|
|
260
|
+
`Execution timeout after ${timeoutMs}ms. Pass a higher \`timeoutMs\` to vm.run() or set per-atom \`timeoutOverrides\` for slow IO atoms.`,
|
|
223
261
|
'vm.run'
|
|
224
262
|
)
|
|
225
263
|
} else {
|