Build AI agents
with components.
A TypeScript framework for AI applications. Components exchange data through ports, and a pipeline connects them into a graph that runs on HLang's reactive flow runtime.
npm install @astack-tech/core@beta- trimTrim
- upperUppercase
- exclaimExclaim
- Framework foundations
- TypeScript native
- Observable streams
- Flow-based programming
- MIT licensed
Everything is a component.
Connect them through ports. The graph runs on the HLang flow runtime.
class Agent extends ComponentOne component model
Component extends HLang’s TransformNode. Models, agents and your own logic share one interface: input ports, output ports and run(). Pipelines connect them.
input → transform → outputData flow paradigm
Components never call each other. You declare the connections, and the runtime sends each value along them. Model calls stay stateless; the agent keeps the history.
Dual execution modes
Independent execution with run() or reactive pipeline mode with _transform(). The same component works in both.
Named ports
Port.I() and Port.O() declare named ports. A component chooses which output port to send on, which is how branching works.
Pluggable models
Agents accept any object with chatCompletion(). DeepSeek and any OpenAI-compatible API work today, including OpenAI, Qwen and local models through Ollama; add others by implementing the same contract.
@astack-tech/{core,components,…}Modular design
Core, components, integrations, and tools as separate packages. Choose the pieces your application needs.
One model. Four ways to build.
Components, ports and pipelines. Workflows, branching, agents and streaming are built from these three ideas, not from separate subsystems.
A component is a unit of work
Extend Component and override run(). The same class works on its own and inside a pipeline, where the runtime feeds its input port.
Input portComponentOutput port
- Standalone: run(data)
- Reactive: _transform($i, $o)
- Extra named ports
import { Component } from '@astack-tech/core';
/**
* A component is a class with input and output ports.
* `run()` is its standalone mode: call it like a function.
*/
class Uppercase extends Component {
constructor() {
super({});
}
override run(text: unknown): string {
return String(text).toUpperCase();
}
}
const uppercase = new Uppercase();
console.log(uppercase.run('hello, astack'));
// HELLO, ASTACK
Explicit connections
Every path data can take is a connect() call you can read.
Built once
Each route's topology is built on its first run and reused afterwards.
Reactive streams
Each connection is an RxJS stream. Components react as values arrive.
From install to first agent.
Install the packages. Create an agent with a model provider. Run the agent from your terminal.
Node.js 20+ · npm, pnpm, or Yarn
Install AStack
npm install @astack-tech/core@beta @astack-tech/components@beta @astack-tech/integrations@beta @astack-tech/tools@betaRun your agent
Set your model provider's API key in your shell (the example uses
DEEPSEEK_API_KEY), then run your TypeScript file. Keep API keys on the server.Create your first agent
Read this example step by stepimport { Agent } from '@astack-tech/components'; import { ModelProvider } from '@astack-tech/integrations'; import { createTool } from '@astack-tech/tools'; const apiKey = process.env['DEEPSEEK_API_KEY']; if (apiKey === undefined || apiKey === '') { throw new Error('Set DEEPSEEK_API_KEY to run this example.'); } const multiply = createTool( 'multiply', 'Multiply two numbers.', args => Number(args['a']) * Number(args['b']), { type: 'object', properties: { a: { type: 'number', description: 'First factor' }, b: { type: 'number', description: 'Second factor' }, }, required: ['a', 'b'], } ); // The agent asks the model, runs the tools it requests, and repeats until the model answers. const agent = new Agent({ // Any model works here: OpenAICompatible for OpenAI, Qwen or Ollama, or your own ModelProvider. model: new ModelProvider.Deepseek({ apiKey }), tools: [multiply], systemPrompt: 'Use tools for arithmetic.', maxIterations: 5, }); const output = await agent.run('What is 37 times 43?'); console.log(output.message); console.log(output.toolCalls); // [ { tool: 'multiply', args: { a: 37, b: 43 }, result: 1591 } ]
The graph is the framework.
AStack's computation model is original. PocketFlow reached a similar idea on its own: a small core where agents, workflows and RAG are patterns, not features. PocketFlow makes a 100-line core its goal; AStack does not optimize for line count. Its API shape takes inspiration from Haystack.
| Aspect | AStack | PocketFlow | Haystack |
|---|---|---|---|
| Language | TypeScript | Python | Python |
| Unit of work | Component with named ports | Node with prep, exec and post | Component with typed sockets |
| How data moves | Along each connection, from an output port to an input port | Through a shared store that nodes read and write | Along connections between sockets |
| Branching | A component sends to the output port it chooses | post returns an action; the flow follows that labeled edge | Router components |
| Execution | Reactive streams; concurrent runs of a route resolve in order | A flow runs nodes one after another; async and parallel variants exist | A pipeline run loop |
| Design goal | A typed, reactive data-flow model | A minimal core of about 100 lines | A full framework for search and RAG |
The main difference from PocketFlow is where state lives. PocketFlow nodes share one store. In AStack each connection carries the data for that step, so every input and output is explicit in the graph.
Read the full comparisonStart with working code.
Each example is one short file that shows one idea. The code you read here is the code that runs in CI.