future-lang 0.4.0 → 0.4.2
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/ARCHITECTURE.md +47 -13
- package/FUTURE_FOR_LLMS.md +55 -2
- package/MIGRATION.md +209 -1
- package/README.md +103 -11
- package/ROADMAP.md +58 -11
- package/package.json +1 -1
- package/runtime/ai.js +54 -9
- package/runtime/assert.js +27 -0
- package/runtime/http.js +54 -8
- package/runtime/index.js +19 -4
- package/runtime/providers/anthropic.js +70 -11
- package/runtime/providers/openai-compat.js +65 -19
- package/src/cli.js +137 -17
- package/src/generator.js +8 -1
- package/src/parser.js +1 -1
- package/src/sourcemap.js +69 -0
package/ROADMAP.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Future — Roadmap
|
|
2
2
|
|
|
3
|
-
**Version:** 0.
|
|
3
|
+
**Version:** 0.4.1 · **Last updated:** 2026-06-13
|
|
4
4
|
|
|
5
5
|
Status legend: ✅ Done · 🔄 In progress · 📋 Planned · 💡 Idea
|
|
6
6
|
|
|
@@ -18,7 +18,9 @@ Status legend: ✅ Done · 🔄 In progress · 📋 Planned · 💡 Idea
|
|
|
18
18
|
| Multi-line strings | 📋 | Strings must be single-line today |
|
|
19
19
|
| String `+` concatenation | ✅ | Works via binary `+` operator |
|
|
20
20
|
| Integer division / modulo | 📋 | `math.trunc(a / b)` workaround for now |
|
|
21
|
-
| `use "other.future"` imports |
|
|
21
|
+
| `use "other.future"` imports | ✅ | Named + namespace imports; npm packages; recursive deps |
|
|
22
|
+
| `else if` chains | ✅ | One `end` for the whole chain |
|
|
23
|
+
| Reserved namespace protection | ✅ | Compile-time error if reserved name is reassigned |
|
|
22
24
|
| REPL (`future repl`) | 💡 | Interactive shell with introspection |
|
|
23
25
|
|
|
24
26
|
---
|
|
@@ -28,11 +30,14 @@ Status legend: ✅ Done · 🔄 In progress · 📋 Planned · 💡 Idea
|
|
|
28
30
|
| Feature | Status | Notes |
|
|
29
31
|
|---------|--------|-------|
|
|
30
32
|
| `ai.ask(prompt)` | ✅ | Single-turn Q&A |
|
|
33
|
+
| `ai.ask(prompt, opts)` | ✅ | `opts`: `temperature`, `max_tokens`, `model`, `system` |
|
|
31
34
|
| `ai.chat(messages)` | ✅ | Multi-turn conversation |
|
|
35
|
+
| `ai.chat(messages, opts)` | ✅ | Inference options forwarded to provider |
|
|
32
36
|
| `ai.embed(text)` | ✅ | Real embeddings (OpenAI/Ollama) + keyword fallback |
|
|
33
|
-
| `ai.stream(prompt,
|
|
37
|
+
| `ai.stream(prompt, cb, opts?)` | ✅ | Streaming via SSE; opts forwarded |
|
|
34
38
|
| `stream ai.ask() ... end` syntax | ✅ | Language-level streaming with implicit `chunk` variable |
|
|
35
39
|
| `ai.configure(provider, key, model)` | ✅ | Pluggable provider from Future code |
|
|
40
|
+
| Structured `AiError` (status, code, provider) | ✅ | Catchable with rich properties |
|
|
36
41
|
| Provider: Anthropic | ✅ | Native Messages API |
|
|
37
42
|
| Provider: OpenAI | ✅ | Via OpenAI-compat layer |
|
|
38
43
|
| Provider: Ollama | ✅ | Local models, no key needed |
|
|
@@ -205,8 +210,11 @@ Status legend: ✅ Done · 🔄 In progress · 📋 Planned · 💡 Idea
|
|
|
205
210
|
| `len(x)` built-in | ✅ | Arrays, strings, objects — sync, no runtime needed |
|
|
206
211
|
| `math.*` module | ✅ | Full JS Math wrapper |
|
|
207
212
|
| `input(prompt)` built-in | ✅ | stdin (Node.js) / `window.prompt` (browser) |
|
|
213
|
+
| `else if` chains | ✅ | One `end` closes the whole chain |
|
|
214
|
+
| `use "./file.future"` imports | ✅ | Named or namespace imports, recursive deps |
|
|
215
|
+
| `use "npm-pkg" as alias` | ✅ | Import npm packages as namespaces |
|
|
216
|
+
| Reserved namespace protection | ✅ | Compile-time error on redefinition |
|
|
208
217
|
| Multi-line strings | 📋 | Strings must be single-line |
|
|
209
|
-
| `use "other.future"` — module imports | 💡 | No cross-file composition |
|
|
210
218
|
| REPL (`future repl`) | 💡 | Interactive shell with introspection |
|
|
211
219
|
|
|
212
220
|
---
|
|
@@ -230,21 +238,57 @@ Status legend: ✅ Done · 🔄 In progress · 📋 Planned · 💡 Idea
|
|
|
230
238
|
|
|
231
239
|
---
|
|
232
240
|
|
|
241
|
+
## HTTP
|
|
242
|
+
|
|
243
|
+
| Feature | Status | Notes |
|
|
244
|
+
|---------|--------|-------|
|
|
245
|
+
| `http.get(url, headers?)` | ✅ | Parses JSON or returns text |
|
|
246
|
+
| `http.post(url, body, headers?)` | ✅ | JSON body; parses response |
|
|
247
|
+
| `http.configure({ headers, timeout })` | ✅ | Global defaults for all requests |
|
|
248
|
+
| Structured `HttpError` (status, code, url, body) | ✅ | Catchable with rich properties |
|
|
249
|
+
| `http.put / patch / delete` | 📋 | Additional HTTP verbs |
|
|
250
|
+
| Response headers access | 📋 | `res.headers.get("content-type")` |
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Testing
|
|
255
|
+
|
|
256
|
+
| Feature | Status | Notes |
|
|
257
|
+
|---------|--------|-------|
|
|
258
|
+
| `future test` command | ✅ | Finds `*.test.future` / `test/**/*.future`, runs all |
|
|
259
|
+
| `future test <pattern>` | ✅ | Filter by filename substring |
|
|
260
|
+
| `assert` namespace | ✅ | `ok`, `equal`, `notEqual`, `deepEqual`, `fail` |
|
|
261
|
+
| Per-file pass/fail reporting | ✅ | `✓` / `✗` with error message |
|
|
262
|
+
| Exit code 1 on failure | ✅ | Integrates with CI |
|
|
263
|
+
| Test isolation (separate process) | 📋 | Currently runs in same Node process |
|
|
264
|
+
| `assert.throws(fn)` | 📋 | Assert that a function throws |
|
|
265
|
+
| Coverage reporting | 💡 | Line coverage for `.future` files |
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
233
269
|
## Tooling
|
|
234
270
|
|
|
235
271
|
| Feature | Status | Notes |
|
|
236
272
|
|---------|--------|-------|
|
|
237
273
|
| CLI: `future run` | ✅ | |
|
|
238
274
|
| CLI: `future compile` | ✅ | |
|
|
239
|
-
|
|
|
275
|
+
| CLI: `future compile --sourcemap` | ✅ | Emits Source Map v3 `.js.map` |
|
|
276
|
+
| CLI: `future run --debug` | ✅ | Per-call timing via `FUTURE_DEBUG=1` |
|
|
277
|
+
| CLI: `future test` | ✅ | Test runner for `*.test.future` files |
|
|
278
|
+
| CLI: `future new` | ✅ | Project scaffold |
|
|
279
|
+
| CLI: `future check` | ✅ | Syntax-check without running |
|
|
280
|
+
| CLI: `future fmt` | ✅ | Auto-formatter |
|
|
281
|
+
| CLI: `future doctor` | ✅ | Environment health check |
|
|
282
|
+
| CLI: `future playground` | ✅ | Launches browser playground server |
|
|
283
|
+
| Source maps (`.js.map`) | ✅ | VLQ-encoded Source Map v3 |
|
|
284
|
+
| Structured manifest | ✅ | All 13 modules, 50+ functions fully described |
|
|
240
285
|
| Runtime introspection API | ✅ | `runtime.describe()` / `listModules()` / `listFunctions()` |
|
|
241
286
|
| LSP metadata module | ✅ | Completions, hover, signatures |
|
|
242
287
|
| Browser playground | ✅ | `future-playground.html` — 11 examples |
|
|
288
|
+
| `FUTURE_FOR_LLMS.md` | ✅ | BNF grammar + all APIs for AI code assistants |
|
|
289
|
+
| npm publish (`future-lang`) | ✅ | Public — `npm install -g future-lang` |
|
|
243
290
|
| VSCode extension | 📋 | Syntax highlighting, completions, hover |
|
|
244
291
|
| Language Server (LSP) | 📋 | Full editor integration |
|
|
245
|
-
| `future fmt` | 📋 | Auto-formatter |
|
|
246
|
-
| `future check` | 📋 | Lint / type check without running |
|
|
247
|
-
| npm publish (`future-lang`) | 📋 | Public package registry |
|
|
248
292
|
|
|
249
293
|
---
|
|
250
294
|
|
|
@@ -252,12 +296,15 @@ Status legend: ✅ Done · 🔄 In progress · 📋 Planned · 💡 Idea
|
|
|
252
296
|
|
|
253
297
|
| Priority | Item | Why it matters |
|
|
254
298
|
|----------|------|----------------|
|
|
255
|
-
| 🔴 Critical | npm publish (`future-lang`) | Required to ship publicly |
|
|
256
299
|
| 🔴 Critical | VSCode extension (syntax highlighting) | First impression for new users |
|
|
257
|
-
|
|
|
258
|
-
| 🟠 High | `
|
|
300
|
+
| 🔴 Critical | Language Server (LSP) | Completions and hover for all IDEs |
|
|
301
|
+
| 🟠 High | `ai.extract(text, schema)` | Structured output is the #1 AI use case |
|
|
302
|
+
| 🟠 High | Test isolation (separate process) | Prevents test state leakage |
|
|
303
|
+
| 🟠 High | `assert.throws(fn)` | Needed for error-handling tests |
|
|
259
304
|
| 🟡 Medium | Home Assistant REST API | Most HA users don't run MQTT |
|
|
260
305
|
| 🟡 Medium | Persistent memory / device registry | Most programs are stateless today |
|
|
261
306
|
| 🟡 Medium | Agent tool-calling loop (ReAct) | True autonomous agents |
|
|
307
|
+
| 🟡 Medium | `http.put / patch / delete` | Needed for full REST APIs |
|
|
262
308
|
| 🟢 Low | `rag.delete(id)` | Selective document removal |
|
|
263
309
|
| 🟢 Low | REPL | Nice-to-have for exploration |
|
|
310
|
+
| 🟢 Low | Coverage reporting | `.future` line coverage |
|
package/package.json
CHANGED
package/runtime/ai.js
CHANGED
|
@@ -34,31 +34,76 @@ 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);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Like ask(), but returns a structured result object instead of a plain string.
|
|
77
|
+
* Includes the generated text, model used, provider name, and token counts.
|
|
78
|
+
*
|
|
79
|
+
* @param {string|Array} prompt String prompt or messages array
|
|
80
|
+
* @param {{ temperature?: number, max_tokens?: number, model?: string, system?: string }} [opts]
|
|
81
|
+
* @returns {Promise<{ text: string, model: string, provider: string, tokens: { input: number, output: number, total: number } }>}
|
|
82
|
+
*/
|
|
83
|
+
export async function complete(prompt, opts = {}) {
|
|
84
|
+
const messages = Array.isArray(prompt)
|
|
85
|
+
? prompt
|
|
86
|
+
: [{ role: 'user', content: String(prompt) }];
|
|
87
|
+
const provider = resolveProvider();
|
|
88
|
+
if (!provider) {
|
|
89
|
+
return {
|
|
90
|
+
text: offlineStub(messages),
|
|
91
|
+
model: 'none',
|
|
92
|
+
provider: 'offline',
|
|
93
|
+
tokens: { input: 0, output: 0, total: 0 },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (typeof provider.complete === 'function') {
|
|
97
|
+
return provider.complete(messages, opts);
|
|
98
|
+
}
|
|
99
|
+
// Fallback for providers that don't implement complete() yet.
|
|
100
|
+
const text = await provider.chat(messages, opts);
|
|
101
|
+
return {
|
|
102
|
+
text,
|
|
103
|
+
model: opts.model ?? 'unknown',
|
|
104
|
+
provider: provider.name,
|
|
105
|
+
tokens: { input: 0, output: 0, total: 0 },
|
|
106
|
+
};
|
|
62
107
|
}
|
|
63
108
|
|
|
64
109
|
/**
|
|
@@ -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,16 @@ 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
|
-
const _base = { ai, http, mqtt, tts, rag, vision, home, memory, schedule, system, device, math };
|
|
28
|
+
const _base = { ai, http, mqtt, tts, rag, vision, home, memory, schedule, system, device, math, assert };
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* When FUTURE_DEBUG=1, wrap every namespace method with timing/logging.
|
|
@@ -82,10 +83,16 @@ export const manifest = {
|
|
|
82
83
|
},
|
|
83
84
|
ask: {
|
|
84
85
|
description: 'Ask an AI model a question and get a text response',
|
|
85
|
-
params: [{ name: 'prompt', type: 'string' }],
|
|
86
|
+
params: [{ name: 'prompt', type: 'string' }, { name: 'opts', type: 'object', optional: true }],
|
|
86
87
|
returns: 'string',
|
|
87
88
|
async: true,
|
|
88
89
|
},
|
|
90
|
+
complete: {
|
|
91
|
+
description: 'Like ask(), but returns a structured object: { text, model, provider, tokens: { input, output, total } }',
|
|
92
|
+
params: [{ name: 'prompt', type: 'string|array' }, { name: 'opts', type: 'object', optional: true }],
|
|
93
|
+
returns: '{ text: string, model: string, provider: string, tokens: { input: number, output: number, total: number } }',
|
|
94
|
+
async: true,
|
|
95
|
+
},
|
|
89
96
|
chat: {
|
|
90
97
|
description: 'Send a multi-turn message list to an AI model',
|
|
91
98
|
params: [{ name: 'messages', type: 'array' }],
|
|
@@ -406,6 +413,14 @@ export const manifest = {
|
|
|
406
413
|
pi: { description: 'The mathematical constant π', params: [], returns: 'number', async: false },
|
|
407
414
|
e: { description: "Euler's number", params: [], returns: 'number', async: false },
|
|
408
415
|
},
|
|
416
|
+
|
|
417
|
+
assert: {
|
|
418
|
+
ok: { description: 'Assert that value is truthy', params: [{ name: 'value', type: 'any' }, { name: 'msg', type: 'string', optional: true }], returns: 'void', async: false },
|
|
419
|
+
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 },
|
|
420
|
+
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 },
|
|
421
|
+
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 },
|
|
422
|
+
fail: { description: 'Unconditionally fail the test with a message', params: [{ name: 'msg', type: 'string', optional: true }], returns: 'void', async: false },
|
|
423
|
+
},
|
|
409
424
|
};
|
|
410
425
|
|
|
411
426
|
// --- Introspection API ---
|
|
@@ -425,7 +440,7 @@ runtime.listFunctions = (mod) => {
|
|
|
425
440
|
* Suitable for AI agent discovery or documentation generation.
|
|
426
441
|
*/
|
|
427
442
|
runtime.describe = () => ({
|
|
428
|
-
version: '0.4.
|
|
443
|
+
version: '0.4.2',
|
|
429
444
|
modules: [...MODULE_NAMES],
|
|
430
445
|
manifest,
|
|
431
446
|
});
|
|
@@ -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 ?? '');
|
|
@@ -49,11 +77,42 @@ export function create(config) {
|
|
|
49
77
|
}
|
|
50
78
|
}
|
|
51
79
|
|
|
80
|
+
async function complete(messages, opts = {}) {
|
|
81
|
+
const model = opts.model ?? defaultModel;
|
|
82
|
+
const max_tokens = opts.max_tokens ?? 1024;
|
|
83
|
+
const body = { model, max_tokens, messages };
|
|
84
|
+
if (opts.temperature != null) body.temperature = opts.temperature;
|
|
85
|
+
if (opts.system) body.system = opts.system;
|
|
86
|
+
|
|
87
|
+
const res = await fetch(`${BASE}/messages`, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers,
|
|
90
|
+
body: JSON.stringify(body),
|
|
91
|
+
});
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
let errBody;
|
|
94
|
+
try { errBody = await res.json(); } catch { errBody = await res.text(); }
|
|
95
|
+
throw new AiError(res.status, 'anthropic', errBody);
|
|
96
|
+
}
|
|
97
|
+
const data = await res.json();
|
|
98
|
+
const text = (data.content ?? []).map((b) => b.text ?? '').join('').trim();
|
|
99
|
+
return {
|
|
100
|
+
text,
|
|
101
|
+
model: data.model ?? model,
|
|
102
|
+
provider: 'anthropic',
|
|
103
|
+
tokens: {
|
|
104
|
+
input: data.usage?.input_tokens ?? 0,
|
|
105
|
+
output: data.usage?.output_tokens ?? 0,
|
|
106
|
+
total: (data.usage?.input_tokens ?? 0) + (data.usage?.output_tokens ?? 0),
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
52
111
|
async function embed(text) {
|
|
53
112
|
// Anthropic has no public embeddings endpoint — use keyword vector fallback.
|
|
54
113
|
// For production semantic search, configure an OpenAI-compatible provider with an embed model.
|
|
55
114
|
return keywordVector(String(text));
|
|
56
115
|
}
|
|
57
116
|
|
|
58
|
-
return { name: 'anthropic', ask, chat, stream, embed };
|
|
117
|
+
return { name: 'anthropic', ask, chat, complete, stream, embed };
|
|
59
118
|
}
|
|
@@ -7,19 +7,20 @@
|
|
|
7
7
|
// No separate SDK or special casing needed.
|
|
8
8
|
|
|
9
9
|
import { parseSSE, keywordVector } from './util.js';
|
|
10
|
+
import { AiError } from './anthropic.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Well-known provider presets. Used when FUTURE_AI_PROVIDER is set without FUTURE_AI_BASE_URL.
|
|
13
14
|
* Users can always override by setting FUTURE_AI_BASE_URL directly.
|
|
14
15
|
*/
|
|
15
16
|
export const PRESETS = {
|
|
16
|
-
openai: { baseUrl: 'https://api.openai.com/v1',
|
|
17
|
-
ollama: { baseUrl: 'http://localhost:11434/v1',
|
|
18
|
-
openrouter: { baseUrl: 'https://openrouter.ai/api/v1',
|
|
17
|
+
openai: { baseUrl: 'https://api.openai.com/v1', embedModel: 'text-embedding-3-small' },
|
|
18
|
+
ollama: { baseUrl: 'http://localhost:11434/v1', embedModel: 'nomic-embed-text' },
|
|
19
|
+
openrouter: { baseUrl: 'https://openrouter.ai/api/v1', embedModel: null },
|
|
19
20
|
gemini: { baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai', embedModel: 'text-embedding-004' },
|
|
20
|
-
venice: { baseUrl: 'https://api.venice.ai/api/v1',
|
|
21
|
-
groq: { baseUrl: 'https://api.groq.com/openai/v1',
|
|
22
|
-
together: { baseUrl: 'https://api.together.xyz/v1',
|
|
21
|
+
venice: { baseUrl: 'https://api.venice.ai/api/v1', embedModel: null },
|
|
22
|
+
groq: { baseUrl: 'https://api.groq.com/openai/v1', embedModel: null },
|
|
23
|
+
together: { baseUrl: 'https://api.together.xyz/v1', embedModel: 'togethercomputer/m2-bert-80M-8k-retrieval' },
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -27,44 +28,89 @@ export const PRESETS = {
|
|
|
27
28
|
* @param {{ baseUrl: string, apiKey: string, model?: string, embedModel?: string }} config
|
|
28
29
|
*/
|
|
29
30
|
export function create(config) {
|
|
30
|
-
const baseUrl
|
|
31
|
-
const apiKey
|
|
32
|
-
const
|
|
33
|
-
const embedModel
|
|
31
|
+
const baseUrl = config.baseUrl.replace(/\/$/, '');
|
|
32
|
+
const apiKey = config.apiKey;
|
|
33
|
+
const defaultModel = config.model ?? 'gpt-4o-mini';
|
|
34
|
+
const embedModel = config.embedModel ?? null;
|
|
35
|
+
const providerTag = `openai-compat(${baseUrl})`;
|
|
34
36
|
|
|
35
37
|
const headers = {
|
|
36
38
|
'content-type': 'application/json',
|
|
37
39
|
'authorization': `Bearer ${apiKey}`,
|
|
38
40
|
};
|
|
39
41
|
|
|
40
|
-
async function chat(messages) {
|
|
42
|
+
async function chat(messages, opts = {}) {
|
|
43
|
+
const model = opts.model ?? defaultModel;
|
|
44
|
+
const max_tokens = opts.max_tokens ?? 1024;
|
|
45
|
+
const body = { model, messages, max_tokens };
|
|
46
|
+
if (opts.temperature != null) body.temperature = opts.temperature;
|
|
47
|
+
|
|
41
48
|
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
42
49
|
method: 'POST',
|
|
43
50
|
headers,
|
|
44
|
-
body: JSON.stringify(
|
|
51
|
+
body: JSON.stringify(body),
|
|
45
52
|
});
|
|
46
|
-
if (!res.ok)
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
let errBody;
|
|
55
|
+
try { errBody = await res.json(); } catch { errBody = await res.text(); }
|
|
56
|
+
throw new AiError(res.status, providerTag, errBody);
|
|
57
|
+
}
|
|
47
58
|
const data = await res.json();
|
|
48
59
|
return data.choices?.[0]?.message?.content?.trim() ?? '';
|
|
49
60
|
}
|
|
50
61
|
|
|
51
|
-
async function ask(prompt) {
|
|
52
|
-
return chat([{ role: 'user', content: String(prompt) }]);
|
|
62
|
+
async function ask(prompt, opts = {}) {
|
|
63
|
+
return chat([{ role: 'user', content: String(prompt) }], opts);
|
|
53
64
|
}
|
|
54
65
|
|
|
55
|
-
async function stream(messages, onChunk) {
|
|
66
|
+
async function stream(messages, onChunk, opts = {}) {
|
|
67
|
+
const model = opts.model ?? defaultModel;
|
|
68
|
+
const max_tokens = opts.max_tokens ?? 1024;
|
|
69
|
+
const body = { model, messages, max_tokens, stream: true };
|
|
70
|
+
if (opts.temperature != null) body.temperature = opts.temperature;
|
|
71
|
+
|
|
56
72
|
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
57
73
|
method: 'POST',
|
|
58
74
|
headers,
|
|
59
|
-
body: JSON.stringify(
|
|
75
|
+
body: JSON.stringify(body),
|
|
60
76
|
});
|
|
61
|
-
if (!res.ok) throw new
|
|
77
|
+
if (!res.ok) throw new AiError(res.status, providerTag, `stream HTTP ${res.status}`);
|
|
62
78
|
for await (const { data } of parseSSE(res.body)) {
|
|
63
79
|
const chunk = data.choices?.[0]?.delta?.content;
|
|
64
80
|
if (chunk) onChunk(chunk);
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
83
|
|
|
84
|
+
async function complete(messages, opts = {}) {
|
|
85
|
+
const model = opts.model ?? defaultModel;
|
|
86
|
+
const max_tokens = opts.max_tokens ?? 1024;
|
|
87
|
+
const body = { model, messages, max_tokens };
|
|
88
|
+
if (opts.temperature != null) body.temperature = opts.temperature;
|
|
89
|
+
|
|
90
|
+
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers,
|
|
93
|
+
body: JSON.stringify(body),
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
let errBody;
|
|
97
|
+
try { errBody = await res.json(); } catch { errBody = await res.text(); }
|
|
98
|
+
throw new AiError(res.status, providerTag, errBody);
|
|
99
|
+
}
|
|
100
|
+
const data = await res.json();
|
|
101
|
+
const text = data.choices?.[0]?.message?.content?.trim() ?? '';
|
|
102
|
+
return {
|
|
103
|
+
text,
|
|
104
|
+
model: data.model ?? model,
|
|
105
|
+
provider: providerTag,
|
|
106
|
+
tokens: {
|
|
107
|
+
input: data.usage?.prompt_tokens ?? 0,
|
|
108
|
+
output: data.usage?.completion_tokens ?? 0,
|
|
109
|
+
total: data.usage?.total_tokens ?? 0,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
68
114
|
async function embed(text) {
|
|
69
115
|
if (!embedModel) return keywordVector(String(text));
|
|
70
116
|
try {
|
|
@@ -81,5 +127,5 @@ export function create(config) {
|
|
|
81
127
|
}
|
|
82
128
|
}
|
|
83
129
|
|
|
84
|
-
return { name:
|
|
130
|
+
return { name: providerTag, ask, chat, complete, stream, embed };
|
|
85
131
|
}
|