future-lang 0.3.2 → 0.4.1
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/FUTURE_FOR_LLMS.md +364 -0
- package/package.json +1 -1
- package/runtime/ai.js +20 -9
- package/runtime/assert.js +27 -0
- package/runtime/http.js +54 -8
- package/runtime/index.js +42 -3
- package/runtime/providers/anthropic.js +38 -10
- package/runtime/providers/openai-compat.js +35 -19
- package/src/cli.js +120 -11
- package/src/generator.js +50 -8
- package/src/index.js +2 -0
- package/src/parser.js +55 -24
- package/src/sourcemap.js +69 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# Future — Reference for LLMs
|
|
2
|
+
|
|
3
|
+
Future is a small language that compiles to JavaScript. It does NOT exist in your training data. Read this file before generating Future code.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Reserved words
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
print if else end function return true false null none
|
|
11
|
+
and or not for in while try catch on every stream
|
|
12
|
+
agent use as
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Reserved namespaces (cannot be reassigned or used as function names):
|
|
16
|
+
```
|
|
17
|
+
ai http mqtt tts rag vision home memory schedule system device math assert
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Grammar (simplified BNF)
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
program = statement*
|
|
26
|
+
statement = print | assignment | if | function | return
|
|
27
|
+
| for | while | try | on | every | stream | agent | use | expr_stmt
|
|
28
|
+
|
|
29
|
+
print = "print" expression
|
|
30
|
+
assignment = IDENTIFIER "=" expression
|
|
31
|
+
if = "if" expression block ("else if" expression block)* ("else" block)? "end"
|
|
32
|
+
function = "function" IDENTIFIER "(" params ")" block "end"
|
|
33
|
+
return = "return" expression?
|
|
34
|
+
for = "for" IDENTIFIER "in" expression block "end"
|
|
35
|
+
while = "while" expression block "end"
|
|
36
|
+
try = "try" block "catch" IDENTIFIER block "end"
|
|
37
|
+
on = "on" IDENTIFIER expression block "end"
|
|
38
|
+
every = "every" expression block "end"
|
|
39
|
+
stream = "stream" call_expr block "end"
|
|
40
|
+
agent = "agent" IDENTIFIER ("use" IDENTIFIER)* block "end"
|
|
41
|
+
use = "use" STRING ("as" IDENTIFIER)?
|
|
42
|
+
|
|
43
|
+
block = statement*
|
|
44
|
+
params = (IDENTIFIER ("," IDENTIFIER)*)?
|
|
45
|
+
expression = or_expr
|
|
46
|
+
call_expr = IDENTIFIER "(" args ")"
|
|
47
|
+
| IDENTIFIER "." IDENTIFIER "(" args ")"
|
|
48
|
+
args = (expression ("," expression)*)?
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Syntax rules
|
|
54
|
+
|
|
55
|
+
- Blocks end with `end` — NO curly braces, NO semicolons
|
|
56
|
+
- `#` starts a line comment
|
|
57
|
+
- Strings: `"double"` or `'single'`
|
|
58
|
+
- String interpolation: `"Hello, {name}!"` — any `{identifier}` or `{identifier.prop}`
|
|
59
|
+
- Escape literal brace: `\{`
|
|
60
|
+
- `null` and `none` are the same
|
|
61
|
+
- Commas in lists: required — `[1, 2, 3]`
|
|
62
|
+
- Commas in objects: optional — `{ name: "Alice" age: 30 }` or `{ name: "Alice", age: 30 }`
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Every construct with example
|
|
67
|
+
|
|
68
|
+
### Variables
|
|
69
|
+
```
|
|
70
|
+
name = "Alice"
|
|
71
|
+
age = 30
|
|
72
|
+
ok = true
|
|
73
|
+
data = null
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Print
|
|
77
|
+
```
|
|
78
|
+
print "Hello, {name}!"
|
|
79
|
+
print age
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### If / else if / else
|
|
83
|
+
```
|
|
84
|
+
if score >= 90
|
|
85
|
+
print "A"
|
|
86
|
+
else if score >= 80
|
|
87
|
+
print "B"
|
|
88
|
+
else if score >= 70
|
|
89
|
+
print "C"
|
|
90
|
+
else
|
|
91
|
+
print "F"
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Function
|
|
96
|
+
```
|
|
97
|
+
function add(a, b)
|
|
98
|
+
return a + b
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
result = add(3, 4)
|
|
102
|
+
print result
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### For loop
|
|
106
|
+
```
|
|
107
|
+
fruits = ["apple", "banana", "cherry"]
|
|
108
|
+
for fruit in fruits
|
|
109
|
+
print "I like {fruit}"
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### While loop
|
|
114
|
+
```
|
|
115
|
+
count = 0
|
|
116
|
+
while count < 5
|
|
117
|
+
count = count + 1
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Try / catch
|
|
122
|
+
```
|
|
123
|
+
try
|
|
124
|
+
data = http.get("https://api.example.com/data")
|
|
125
|
+
print data.title
|
|
126
|
+
catch err
|
|
127
|
+
print "Error: {err}"
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Objects and lists
|
|
132
|
+
```
|
|
133
|
+
user = { name: "João" age: 30 city: "Lisbon" }
|
|
134
|
+
scores = [85, 92, 78]
|
|
135
|
+
print user.name
|
|
136
|
+
print scores.length
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### String interpolation
|
|
140
|
+
```
|
|
141
|
+
msg = "Name: {user.name}, Age: {user.age}"
|
|
142
|
+
print msg
|
|
143
|
+
print "Pi is {math.pi}"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Import system
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
# Import all functions from a file by name
|
|
152
|
+
use "./utils.future"
|
|
153
|
+
result = formatName("Alice")
|
|
154
|
+
|
|
155
|
+
# Import as a namespace
|
|
156
|
+
use "./math.future" as m
|
|
157
|
+
result = m.add(10, 20)
|
|
158
|
+
|
|
159
|
+
# Import an npm package as a namespace
|
|
160
|
+
use "date-fns" as df
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Imported `.future` files must contain only top-level function declarations. They compile to ES module exports automatically.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Capability namespaces
|
|
168
|
+
|
|
169
|
+
No `async`/`await` needed — the compiler handles it. Any namespace call switches the program to async mode automatically.
|
|
170
|
+
|
|
171
|
+
### `ai`
|
|
172
|
+
```
|
|
173
|
+
answer = ai.ask("What is the capital of France?")
|
|
174
|
+
reply = ai.chat([{ role: "user" content: "Hello" }])
|
|
175
|
+
embed = ai.embed("text to embed")
|
|
176
|
+
ai.configure("openai", "sk-...")
|
|
177
|
+
ai.configure("ollama")
|
|
178
|
+
|
|
179
|
+
# With options: temperature and max_tokens
|
|
180
|
+
answer = ai.ask("Explain quantum physics", { temperature: 0.2 max_tokens: 200 })
|
|
181
|
+
reply = ai.chat(messages, { model: "gpt-4o" temperature: 0.7 })
|
|
182
|
+
|
|
183
|
+
stream ai.ask("Tell me a story")
|
|
184
|
+
print chunk
|
|
185
|
+
end
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `http`
|
|
189
|
+
```
|
|
190
|
+
data = http.get("https://api.example.com/todos/1")
|
|
191
|
+
print data.title
|
|
192
|
+
|
|
193
|
+
res = http.post("https://api.example.com/items", { name: "Widget" price: 9.99 })
|
|
194
|
+
print res.id
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### `mqtt`
|
|
198
|
+
```
|
|
199
|
+
mqtt.publish("home/light", "on")
|
|
200
|
+
|
|
201
|
+
on mqtt "home/temp"
|
|
202
|
+
print "Temperature: {message}"
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### `tts`
|
|
207
|
+
```
|
|
208
|
+
tts.speak("Hello from Future!")
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### `memory`
|
|
212
|
+
```
|
|
213
|
+
memory.set("key", "value")
|
|
214
|
+
val = memory.get("key")
|
|
215
|
+
memory.delete("key")
|
|
216
|
+
results = memory.search("query")
|
|
217
|
+
memory.forget() # clear all
|
|
218
|
+
memory.forget("prefix") # clear matching keys
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### `schedule`
|
|
222
|
+
```
|
|
223
|
+
every "30m"
|
|
224
|
+
data = http.get("https://api.example.com/stats")
|
|
225
|
+
print data.count
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# schedule.once and schedule.cron also available
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### `http`
|
|
232
|
+
```
|
|
233
|
+
data = http.get("https://api.example.com/todos/1")
|
|
234
|
+
print data.title
|
|
235
|
+
|
|
236
|
+
res = http.post("https://api.example.com/items", { name: "Widget" price: 9.99 })
|
|
237
|
+
print res.id
|
|
238
|
+
|
|
239
|
+
# Global config (call once at the top of your program)
|
|
240
|
+
http.configure({ headers: { Authorization: "Bearer {token}" } timeout: 5000 })
|
|
241
|
+
|
|
242
|
+
# Errors have .status, .code, .url, .body properties
|
|
243
|
+
try
|
|
244
|
+
data = http.get("https://api.example.com/private")
|
|
245
|
+
catch err
|
|
246
|
+
print "Status: {err.status}"
|
|
247
|
+
print "Code: {err.code}"
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### `assert` (use in *.test.future files)
|
|
252
|
+
```
|
|
253
|
+
assert.ok(value)
|
|
254
|
+
assert.equal(actual, expected)
|
|
255
|
+
assert.notEqual(a, b)
|
|
256
|
+
assert.deepEqual(obj1, obj2)
|
|
257
|
+
assert.fail("custom message")
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### `system`, `rag`, `vision`, `home`, `device`
|
|
261
|
+
See [README.md](README.md) for full API tables.
|
|
262
|
+
|
|
263
|
+
### `math`
|
|
264
|
+
```
|
|
265
|
+
print math.round(3.7) # 4
|
|
266
|
+
print math.sqrt(16) # 4
|
|
267
|
+
print math.pi # 3.14159…
|
|
268
|
+
print math.random() # 0–1
|
|
269
|
+
print math.max(1, 5, 3) # 5
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### `len` (built-in, not a namespace)
|
|
273
|
+
```
|
|
274
|
+
print len([1, 2, 3]) # 3
|
|
275
|
+
print len("hello") # 5
|
|
276
|
+
print len({ a: 1 b: 2 }) # 2
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Agents
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
agent support
|
|
285
|
+
use rag
|
|
286
|
+
use memory
|
|
287
|
+
|
|
288
|
+
docs = rag.query(goal)
|
|
289
|
+
memory.set("last", goal)
|
|
290
|
+
return docs
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
answer = support("How do I reset the device?")
|
|
294
|
+
print answer
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`goal` is the implicit parameter. `use` inside an agent declares capabilities (documentation only — no generated code).
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Common mistakes
|
|
302
|
+
|
|
303
|
+
| Wrong | Correct |
|
|
304
|
+
|-------|---------|
|
|
305
|
+
| `if (x > 0) {` | `if x > 0` |
|
|
306
|
+
| `end if` | `end` |
|
|
307
|
+
| `elif` | `else if` |
|
|
308
|
+
| `&&` / `\|\|` / `!` | `and` / `or` / `not` |
|
|
309
|
+
| `x++` | `x = x + 1` |
|
|
310
|
+
| `x += 1` | `x = x + 1` |
|
|
311
|
+
| `// comment` | `# comment` |
|
|
312
|
+
| `import "./utils.js"` | `use "./utils.future"` |
|
|
313
|
+
| `let x = 5` | `x = 5` |
|
|
314
|
+
| `function f() { }` | `function f()` + body + `end` |
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## What compiles to what
|
|
319
|
+
|
|
320
|
+
```future
|
|
321
|
+
x = 1
|
|
322
|
+
```
|
|
323
|
+
```js
|
|
324
|
+
let x;
|
|
325
|
+
x = 1;
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
```future
|
|
329
|
+
if x > 0
|
|
330
|
+
print "positive"
|
|
331
|
+
else if x < 0
|
|
332
|
+
print "negative"
|
|
333
|
+
else
|
|
334
|
+
print "zero"
|
|
335
|
+
end
|
|
336
|
+
```
|
|
337
|
+
```js
|
|
338
|
+
if (x > 0) {
|
|
339
|
+
console.log("positive");
|
|
340
|
+
} else if (x < 0) {
|
|
341
|
+
console.log("negative");
|
|
342
|
+
} else {
|
|
343
|
+
console.log("zero");
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
```future
|
|
348
|
+
answer = ai.ask("Hello?")
|
|
349
|
+
```
|
|
350
|
+
```js
|
|
351
|
+
import { runtime as __rt } from "future-lang/runtime";
|
|
352
|
+
let answer;
|
|
353
|
+
answer = await __rt.ai.ask("Hello?");
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
```future
|
|
357
|
+
use "./utils.future"
|
|
358
|
+
name = formatName("Alice")
|
|
359
|
+
```
|
|
360
|
+
```js
|
|
361
|
+
import { formatName } from "./utils.js";
|
|
362
|
+
let name;
|
|
363
|
+
name = formatName("Alice");
|
|
364
|
+
```
|
package/package.json
CHANGED
package/runtime/ai.js
CHANGED
|
@@ -34,31 +34,42 @@ export function configure(baseUrlOrProvider, apiKey, model) {
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Ask a single question.
|
|
39
|
+
* @param {string} prompt
|
|
40
|
+
* @param {{ temperature?: number, max_tokens?: number, model?: string, system?: string }} [opts]
|
|
41
|
+
* @returns {Promise<string>}
|
|
42
|
+
*/
|
|
43
|
+
export async function ask(prompt, opts = {}) {
|
|
44
|
+
return chat([{ role: 'user', content: String(prompt) }], opts);
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Multi-turn chat.
|
|
49
|
+
* @param {Array<{role,content}>} messages
|
|
50
|
+
* @param {{ temperature?: number, max_tokens?: number, model?: string, system?: string }} [opts]
|
|
51
|
+
* @returns {Promise<string>}
|
|
52
|
+
*/
|
|
53
|
+
export async function chat(messages, opts = {}) {
|
|
44
54
|
const provider = resolveProvider();
|
|
45
55
|
if (!provider) return offlineStub(messages);
|
|
46
|
-
return provider.chat(messages);
|
|
56
|
+
return provider.chat(messages, opts);
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
/**
|
|
50
60
|
* Stream a response chunk-by-chunk.
|
|
51
61
|
* @param {string|Array} promptOrMessages
|
|
52
|
-
* @param {(chunk: string) => void} onChunk
|
|
62
|
+
* @param {(chunk: string) => void} onChunk
|
|
63
|
+
* @param {{ temperature?: number, max_tokens?: number, model?: string }} [opts]
|
|
53
64
|
* @returns {Promise<void>}
|
|
54
65
|
*/
|
|
55
|
-
export async function stream(promptOrMessages, onChunk) {
|
|
66
|
+
export async function stream(promptOrMessages, onChunk, opts = {}) {
|
|
56
67
|
const messages = Array.isArray(promptOrMessages)
|
|
57
68
|
? promptOrMessages
|
|
58
69
|
: [{ role: 'user', content: String(promptOrMessages) }];
|
|
59
70
|
const provider = resolveProvider();
|
|
60
71
|
if (!provider) { onChunk(offlineStub(messages)); return; }
|
|
61
|
-
return provider.stream(messages, onChunk);
|
|
72
|
+
return provider.stream(messages, onChunk, opts);
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
/**
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// runtime/assert.js — Test assertions for `future test`.
|
|
2
|
+
// Wraps node:assert/strict with Future-friendly error messages.
|
|
3
|
+
|
|
4
|
+
import nodeAssert from 'node:assert/strict';
|
|
5
|
+
|
|
6
|
+
function wrap(fn, name) {
|
|
7
|
+
return (...args) => {
|
|
8
|
+
try {
|
|
9
|
+
fn(...args);
|
|
10
|
+
} catch (err) {
|
|
11
|
+
// Re-throw with the assert namespace tag so the test runner can identify it.
|
|
12
|
+
const e = new Error(err.message);
|
|
13
|
+
e.name = 'AssertionError';
|
|
14
|
+
e.namespace = 'assert';
|
|
15
|
+
e.operator = err.operator ?? name;
|
|
16
|
+
e.actual = err.actual;
|
|
17
|
+
e.expected = err.expected;
|
|
18
|
+
throw e;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const ok = wrap((val, msg) => nodeAssert.ok(val, msg), 'ok');
|
|
24
|
+
export const equal = wrap((a, b, msg) => nodeAssert.equal(a, b, msg), 'equal');
|
|
25
|
+
export const notEqual = wrap((a, b, msg) => nodeAssert.notEqual(a, b, msg), 'notEqual');
|
|
26
|
+
export const deepEqual = wrap((a, b, msg) => nodeAssert.deepEqual(a, b, msg), 'deepEqual');
|
|
27
|
+
export const fail = (msg = 'assertion failed') => { throw Object.assign(new Error(msg), { name: 'AssertionError', namespace: 'assert' }); };
|
package/runtime/http.js
CHANGED
|
@@ -1,32 +1,78 @@
|
|
|
1
1
|
// runtime/http.js — consume REST APIs.
|
|
2
2
|
// Uses the global fetch (stable in Node 22). Returns parsed JSON or text.
|
|
3
3
|
|
|
4
|
+
export class HttpError extends Error {
|
|
5
|
+
constructor(status, statusText, url, body) {
|
|
6
|
+
super(`HTTP ${status} ${statusText} — ${url}`);
|
|
7
|
+
this.name = 'HttpError';
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.statusText = statusText;
|
|
10
|
+
this.url = url;
|
|
11
|
+
this.body = body;
|
|
12
|
+
this.namespace = 'http';
|
|
13
|
+
this.code = `HTTP_${status}`;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Global config state — mutated by configure().
|
|
18
|
+
let _config = {
|
|
19
|
+
headers: {},
|
|
20
|
+
timeout: 0,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Set global defaults for all HTTP requests.
|
|
25
|
+
* Useful for Authorization headers, base timeouts, etc.
|
|
26
|
+
* @param {{ headers?: Record<string,string>, timeout?: number }} opts
|
|
27
|
+
*/
|
|
28
|
+
export function configure(opts = {}) {
|
|
29
|
+
if (opts.headers) _config.headers = { ..._config.headers, ...opts.headers };
|
|
30
|
+
if (opts.timeout != null) _config.timeout = opts.timeout;
|
|
31
|
+
}
|
|
32
|
+
|
|
4
33
|
// Default headers — many public APIs (e.g. GitHub) reject requests without a
|
|
5
34
|
// User-Agent. Callers can override any of these.
|
|
6
35
|
const DEFAULT_HEADERS = {
|
|
7
|
-
'user-agent': 'future-lang/0.
|
|
36
|
+
'user-agent': 'future-lang/0.4 (+https://github.com/humolot/future-lang)',
|
|
8
37
|
accept: 'application/json, text/*;q=0.9, */*;q=0.8',
|
|
9
38
|
};
|
|
10
39
|
|
|
11
|
-
async function
|
|
40
|
+
async function parseBody(res) {
|
|
12
41
|
const ct = res.headers.get('content-type') || '';
|
|
13
42
|
return ct.includes('application/json') ? res.json() : res.text();
|
|
14
43
|
}
|
|
15
44
|
|
|
45
|
+
function buildSignal() {
|
|
46
|
+
if (!_config.timeout) return undefined;
|
|
47
|
+
return AbortSignal.timeout(_config.timeout);
|
|
48
|
+
}
|
|
49
|
+
|
|
16
50
|
/** GET a URL. @returns parsed JSON object/array, or text. */
|
|
17
51
|
export async function get(url, headers = {}) {
|
|
18
|
-
const res = await fetch(url, {
|
|
19
|
-
|
|
20
|
-
|
|
52
|
+
const res = await fetch(url, {
|
|
53
|
+
headers: { ...DEFAULT_HEADERS, ..._config.headers, ...headers },
|
|
54
|
+
signal: buildSignal(),
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
let body;
|
|
58
|
+
try { body = await parseBody(res); } catch { body = null; }
|
|
59
|
+
throw new HttpError(res.status, res.statusText, url, body);
|
|
60
|
+
}
|
|
61
|
+
return parseBody(res);
|
|
21
62
|
}
|
|
22
63
|
|
|
23
64
|
/** POST a JSON body to a URL. @returns parsed JSON or text. */
|
|
24
65
|
export async function post(url, body, headers = {}) {
|
|
25
66
|
const res = await fetch(url, {
|
|
26
67
|
method: 'POST',
|
|
27
|
-
headers: { ...DEFAULT_HEADERS, 'content-type': 'application/json', ...headers },
|
|
68
|
+
headers: { ...DEFAULT_HEADERS, 'content-type': 'application/json', ..._config.headers, ...headers },
|
|
28
69
|
body: typeof body === 'string' ? body : JSON.stringify(body),
|
|
70
|
+
signal: buildSignal(),
|
|
29
71
|
});
|
|
30
|
-
if (!res.ok)
|
|
31
|
-
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
let errBody;
|
|
74
|
+
try { errBody = await parseBody(res); } catch { errBody = null; }
|
|
75
|
+
throw new HttpError(res.status, res.statusText, url, errBody);
|
|
76
|
+
}
|
|
77
|
+
return parseBody(res);
|
|
32
78
|
}
|
package/runtime/index.js
CHANGED
|
@@ -16,15 +16,46 @@ import * as schedule from './schedule.js';
|
|
|
16
16
|
import * as system from './system.js';
|
|
17
17
|
import * as device from './device.js';
|
|
18
18
|
import * as math from './math.js';
|
|
19
|
+
import * as assert from './assert.js';
|
|
19
20
|
import readline from 'node:readline';
|
|
20
21
|
|
|
21
22
|
// Canonical ordered list of capability module names.
|
|
22
23
|
const MODULE_NAMES = [
|
|
23
24
|
'ai', 'http', 'mqtt', 'tts', 'rag', 'vision', 'home',
|
|
24
|
-
'memory', 'schedule', 'system', 'device', 'math',
|
|
25
|
+
'memory', 'schedule', 'system', 'device', 'math', 'assert',
|
|
25
26
|
];
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
const _base = { ai, http, mqtt, tts, rag, vision, home, memory, schedule, system, device, math, assert };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* When FUTURE_DEBUG=1, wrap every namespace method with timing/logging.
|
|
32
|
+
* Non-function properties (constants like math.pi) pass through unchanged.
|
|
33
|
+
*/
|
|
34
|
+
function wrapDebug(base) {
|
|
35
|
+
const wrapped = {};
|
|
36
|
+
for (const [ns, mod] of Object.entries(base)) {
|
|
37
|
+
wrapped[ns] = {};
|
|
38
|
+
for (const [key, val] of Object.entries(mod)) {
|
|
39
|
+
if (typeof val !== 'function') { wrapped[ns][key] = val; continue; }
|
|
40
|
+
wrapped[ns][key] = async (...args) => {
|
|
41
|
+
const preview = args.length ? String(JSON.stringify(args[0])).slice(0, 60) : '';
|
|
42
|
+
process.stderr.write(`\x1b[90m[debug] ${ns}.${key}(${preview}) …\x1b[0m\n`);
|
|
43
|
+
const t = Date.now();
|
|
44
|
+
try {
|
|
45
|
+
const result = await val(...args);
|
|
46
|
+
process.stderr.write(`\x1b[90m[debug] ${ns}.${key} ✓ ${Date.now() - t}ms\x1b[0m\n`);
|
|
47
|
+
return result;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
process.stderr.write(`\x1b[31m[debug] ${ns}.${key} ✗ ${Date.now() - t}ms — ${err.message}\x1b[0m\n`);
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return wrapped;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const runtime = process.env.FUTURE_DEBUG === '1' ? wrapDebug(_base) : _base;
|
|
28
59
|
|
|
29
60
|
// input(prompt) — reads a line from stdin (CLI programs).
|
|
30
61
|
runtime.input = async (prompt = '') => {
|
|
@@ -376,6 +407,14 @@ export const manifest = {
|
|
|
376
407
|
pi: { description: 'The mathematical constant π', params: [], returns: 'number', async: false },
|
|
377
408
|
e: { description: "Euler's number", params: [], returns: 'number', async: false },
|
|
378
409
|
},
|
|
410
|
+
|
|
411
|
+
assert: {
|
|
412
|
+
ok: { description: 'Assert that value is truthy', params: [{ name: 'value', type: 'any' }, { name: 'msg', type: 'string', optional: true }], returns: 'void', async: false },
|
|
413
|
+
equal: { description: 'Assert that actual === expected', params: [{ name: 'actual', type: 'any' }, { name: 'expected', type: 'any' }, { name: 'msg', type: 'string', optional: true }], returns: 'void', async: false },
|
|
414
|
+
notEqual: { description: 'Assert that actual !== expected', params: [{ name: 'actual', type: 'any' }, { name: 'expected', type: 'any' }, { name: 'msg', type: 'string', optional: true }], returns: 'void', async: false },
|
|
415
|
+
deepEqual: { description: 'Assert deep structural equality', params: [{ name: 'actual', type: 'any' }, { name: 'expected', type: 'any' }, { name: 'msg', type: 'string', optional: true }], returns: 'void', async: false },
|
|
416
|
+
fail: { description: 'Unconditionally fail the test with a message', params: [{ name: 'msg', type: 'string', optional: true }], returns: 'void', async: false },
|
|
417
|
+
},
|
|
379
418
|
};
|
|
380
419
|
|
|
381
420
|
// --- Introspection API ---
|
|
@@ -395,7 +434,7 @@ runtime.listFunctions = (mod) => {
|
|
|
395
434
|
* Suitable for AI agent discovery or documentation generation.
|
|
396
435
|
*/
|
|
397
436
|
runtime.describe = () => ({
|
|
398
|
-
version: '0.
|
|
437
|
+
version: '0.4.1',
|
|
399
438
|
modules: [...MODULE_NAMES],
|
|
400
439
|
manifest,
|
|
401
440
|
});
|
|
@@ -6,13 +6,26 @@ import { parseSSE, keywordVector } from './util.js';
|
|
|
6
6
|
|
|
7
7
|
const BASE = 'https://api.anthropic.com/v1';
|
|
8
8
|
|
|
9
|
+
export class AiError extends Error {
|
|
10
|
+
constructor(status, provider, body) {
|
|
11
|
+
const msg = body?.error?.message ?? body ?? `HTTP ${status}`;
|
|
12
|
+
super(`[ai:${provider}] ${msg}`);
|
|
13
|
+
this.name = 'AiError';
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.code = `AI_HTTP_${status}`;
|
|
16
|
+
this.namespace = 'ai';
|
|
17
|
+
this.provider = provider;
|
|
18
|
+
this.body = body;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
/**
|
|
10
23
|
* Create an Anthropic provider instance.
|
|
11
24
|
* @param {{ apiKey: string, model?: string }} config
|
|
12
25
|
*/
|
|
13
26
|
export function create(config) {
|
|
14
|
-
const key
|
|
15
|
-
const
|
|
27
|
+
const key = config.apiKey;
|
|
28
|
+
const defaultModel = config.model ?? 'claude-sonnet-4-6';
|
|
16
29
|
|
|
17
30
|
const headers = {
|
|
18
31
|
'content-type': 'application/json',
|
|
@@ -20,28 +33,43 @@ export function create(config) {
|
|
|
20
33
|
'anthropic-version': '2023-06-01',
|
|
21
34
|
};
|
|
22
35
|
|
|
23
|
-
async function chat(messages) {
|
|
36
|
+
async function chat(messages, opts = {}) {
|
|
37
|
+
const model = opts.model ?? defaultModel;
|
|
38
|
+
const max_tokens = opts.max_tokens ?? 1024;
|
|
39
|
+
const body = { model, max_tokens, messages };
|
|
40
|
+
if (opts.temperature != null) body.temperature = opts.temperature;
|
|
41
|
+
if (opts.system) body.system = opts.system;
|
|
42
|
+
|
|
24
43
|
const res = await fetch(`${BASE}/messages`, {
|
|
25
44
|
method: 'POST',
|
|
26
45
|
headers,
|
|
27
|
-
body: JSON.stringify(
|
|
46
|
+
body: JSON.stringify(body),
|
|
28
47
|
});
|
|
29
|
-
if (!res.ok)
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
let errBody;
|
|
50
|
+
try { errBody = await res.json(); } catch { errBody = await res.text(); }
|
|
51
|
+
throw new AiError(res.status, 'anthropic', errBody);
|
|
52
|
+
}
|
|
30
53
|
const data = await res.json();
|
|
31
54
|
return (data.content ?? []).map((b) => b.text ?? '').join('').trim();
|
|
32
55
|
}
|
|
33
56
|
|
|
34
|
-
async function ask(prompt) {
|
|
35
|
-
return chat([{ role: 'user', content: String(prompt) }]);
|
|
57
|
+
async function ask(prompt, opts = {}) {
|
|
58
|
+
return chat([{ role: 'user', content: String(prompt) }], opts);
|
|
36
59
|
}
|
|
37
60
|
|
|
38
|
-
async function stream(messages, onChunk) {
|
|
61
|
+
async function stream(messages, onChunk, opts = {}) {
|
|
62
|
+
const model = opts.model ?? defaultModel;
|
|
63
|
+
const max_tokens = opts.max_tokens ?? 1024;
|
|
64
|
+
const body = { model, max_tokens, messages, stream: true };
|
|
65
|
+
if (opts.temperature != null) body.temperature = opts.temperature;
|
|
66
|
+
|
|
39
67
|
const res = await fetch(`${BASE}/messages`, {
|
|
40
68
|
method: 'POST',
|
|
41
69
|
headers,
|
|
42
|
-
body: JSON.stringify(
|
|
70
|
+
body: JSON.stringify(body),
|
|
43
71
|
});
|
|
44
|
-
if (!res.ok) throw new
|
|
72
|
+
if (!res.ok) throw new AiError(res.status, 'anthropic', `stream HTTP ${res.status}`);
|
|
45
73
|
for await (const { event, data } of parseSSE(res.body)) {
|
|
46
74
|
if (event === 'content_block_delta' || data?.type === 'content_block_delta') {
|
|
47
75
|
onChunk(data.delta?.text ?? '');
|