SLANG is also a TypeScript/JavaScript library for programmatic flow execution.
import { parse, runFlow, createOpenAIAdapter } from '@riktar/slang'
const source = `
flow "hello" {
agent Greeter {
stake greet("world") -> @out
commit
}
converge when: all_committed
}
`
const ast = parse(source)
const state = await runFlow(source, {
adapter: createOpenAIAdapter({ apiKey: process.env.OPENAI_API_KEY }),
onEvent: (event) => console.log(event),
})runFlow() and testFlow() execute exactly one top-level flow. If the source contains multiple flows, they fail explicitly instead of silently picking the first declaration.
import {
createOpenAIAdapter, // OpenAI / Ollama / any OpenAI-compatible
createAnthropicAdapter, // Anthropic
createOpenRouterAdapter, // OpenRouter (300+ models, one API key)
createSamplingAdapter, // MCP host delegation (no API key)
createEchoAdapter, // Testing
createRouterAdapter, // Multi-provider: different agents → different backends
} from '@riktar/slang'Turn agent tools: declarations into real handlers:
// tools.js
export default {
async web_search(args) {
const res = await fetch(`https://api.search.com?q=${encodeURIComponent(args.query)}`);
return await res.text();
},
};slang run research.slang --adapter openrouter --tools tools.jsOr via the API:
const state = await runFlow(source, {
adapter,
tools: {
web_search: async (args) => fetchSearchResults(args.query as string),
},
})Runtime composition is driven by importLoader:
const state = await runFlow(source, {
adapter,
sourcePath: "/absolute/path/to/root.slang",
importLoader: async (path, from) => {
const resolvedPath = resolve(dirname(from ?? "/absolute/path/to/root.slang"), path)
return {
source: await readFile(resolvedPath, 'utf8'),
path: resolvedPath,
}
},
})importLoader(path, from?)receives the literal import string plus the path of the file that declared it, when known.- The loader may return either a raw source string or
{ source, path }. - Return
{ source, path }when you want nested relative imports to keep resolving from the imported file. - If an import cannot be resolved, execution fails explicitly.
See examples/tools.js for template code with web_search and code_exec.
import { runFlow, serializeFlowState, deserializeFlowState } from '@riktar/slang'
const state = await runFlow(source, {
adapter,
checkpoint: async (snapshot) => {
await writeFile('checkpoint.json', serializeFlowState(snapshot))
},
})
// Later: resume from checkpoint
const saved = deserializeFlowState(await readFile('checkpoint.json', 'utf8'))
const resumed = await runFlow(source, { adapter, resumeFrom: saved })Execute side effects after flow termination using deliver: in the .slang file and deliverers in runtime options:
flow "report" {
agent Writer {
stake write(topic: "AI") -> @out
commit
}
deliver: save_file(path: "report.md")
deliver: webhook(url: "https://hooks.example.com/done")
converge when: all_committed
}const state = await runFlow(source, {
adapter,
deliverers: {
save_file: async (output, args) => {
await writeFile(args.path as string, String(output))
},
webhook: async (output, args) => {
await fetch(args.url as string, { method: 'POST', body: JSON.stringify(output) })
},
},
onConverge: async (finalState) => {
console.log(`Flow ended with ${finalState.status} in ${finalState.round} rounds`)
},
})Current runtime behavior:
deliver:handlers run on any terminal status:converged,budget_exceeded,deadlock, orescalated.onConvergeis a historical API name; it is also invoked after any terminal flow status.
See examples/finalizer.slang for the Finalizer pattern.
CLI usage:
slang run report.slang --adapter openrouter --deliverers deliverers.jsThe deliverers.js file follows the same pattern as tools.js — default-export an object where each key is a handler name and each value is async (output, args) => void:
// deliverers.js
export default {
async save_file(output, args) {
await writeFile(args.path, String(output))
},
async webhook(output, args) {
await fetch(args.url, { method: 'POST', body: JSON.stringify(output) })
},
}import { parse, resolveDeps, detectDeadlocks, analyzeFlow, parseWithRecovery } from '@riktar/slang'
// Deadlock detection
const graph = resolveDeps(parse(source).flows[0])
const deadlocks = detectDeadlocks(graph)
// Collect all errors instead of failing on first
const { program, errors } = parseWithRecovery(source)
// errors[0].code → "P201", errors[0].line → 3, errors[0].toJSON() → { code, message, line, column }Error codes follow a convention: L1xx (lexer), P2xx (parser), R3xx (resolver), E4xx (runtime). All errors include line/column and human-friendly messages with source context.
When a stake declares output: { field: "type" }, the runtime now validates the extracted JSON payload:
- every declared field must be present;
string,number, andbooleanfield types are enforced;- invalid structured output fails execution explicitly.