Skip to content

Latest commit

 

History

History
194 lines (152 loc) · 5.51 KB

File metadata and controls

194 lines (152 loc) · 5.51 KB

API

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.

Adapters

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'

Functional Tools

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.js

Or via the API:

const state = await runFlow(source, {
  adapter,
  tools: {
    web_search: async (args) => fetchSearchResults(args.query as string),
  },
})

Composition Imports

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.

Checkpoint & Resume

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 })

Deliver & onConverge

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, or escalated.
  • onConverge is 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.js

The 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) })
  },
}

Static Analysis & Error Handling

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.

Structured Output Contracts

When a stake declares output: { field: "type" }, the runtime now validates the extracted JSON payload:

  • every declared field must be present;
  • string, number, and boolean field types are enforced;
  • invalid structured output fails execution explicitly.