
Research
/Security News
Chrome and Firefox Extensions Posing as Free VPNs Add Clipboard Stealers via Malicious Updates
Malicious Chrome and Firefox extensions posed as free VPNs while stealing clipboard data through later extension updates.
A lightweight, extensible TypeScript SDK for building AI agents with streaming support, tool calling, and middleware pattern.
pnpm add goatchain
Simple Agent Loop example:
import process from 'node:process'
import { Agent, createModel, createOpenAIAdapter } from 'goatchain'
// Create model
const model = createModel({
adapter: createOpenAIAdapter({
defaultModelId: 'gpt-4o',
apiKey: process.env.OPENAI_API_KEY!,
}),
})
// Create Agent
const agent = new Agent({
name: 'Simple Assistant',
systemPrompt: 'You are a helpful assistant.',
model,
})
// Stream responses
const session = await agent.createSession()
session.send('Hello!')
for await (const event of session.receive()) {
if (event.type === 'text_delta') {
process.stdout.write(event.delta)
} else if (event.type === 'done') {
console.log('\nDone:', event.stopReason)
}
}
📖 Full Documentation: See docs/getting-started.md for more examples and complete guide.
GoatChain CLI is a terminal UI (TUI) built on @simon_he/vue-tui.
Global installation (recommended):
npm install -g goatchain-cli@latest
Or use npx without installation:
npx goatchain-cli@latest
After installation, simply run:
goatchain
Inside the TUI:
Ctrl+P opens the command palette (Sessions / New Session / Settings / Tool Approvals; Chat also has Theme)/settings, /approvals, /sessions, /new (Chat also supports /redo)Run it from this repo:
bun run cli
See docs/cli.md for the full feature list and flowcharts.
GoatChain can be exposed as an ACP (Agent Client Protocol) server for integration with editors like Zed.
bun run acp-server
Configuration for Zed (settings.json):
{
"agent_servers": {
"goatchain": {
"command": "pnpm",
"args": ["--dir", "/path/to/GoatChain", "acp-server"]
}
}
}
Features:
Available servers:
pnpm acp-server - Simple agent (recommended)pnpm acp-server:plan - With plan mode (experimental)📖 Full Documentation: See docs/acp-server.md for configuration and troubleshooting.
classDiagram
direction TB
class Agent {
+id: string
+name: string
+systemPrompt: string
+model: ModelClient
+tools: ToolRegistry
+stateStore: StateStore
+sessionManager: BaseSessionManager
+stats: AgentStats
+use(middleware): this
+createSession(options): BaseSession
+resumeSession(sessionId, options): BaseSession
+setModel(modelOrRef): void
}
class ModelClient {
<<interface>>
+modelId: string
+stream(request): AsyncIterable~ModelStreamEvent~
+run?(request): Promise~ModelRunResult~
}
class StateStore {
<<interface>>
+deleteOnComplete: boolean
+saveCheckpoint(checkpoint): Promise~void~
+loadCheckpoint(sessionId): Promise~AgentLoopCheckpoint~
+deleteCheckpoint(sessionId): Promise~void~
+listCheckpoints(): Promise~AgentLoopCheckpoint[]~
}
class BaseTool {
<<abstract>>
+name: string
+description: string
+parameters: JSONSchema
+execute(args, ctx?): Promise~unknown~
}
class ToolRegistry {
+register(tool): void
+unregister(name): boolean
+get(name): BaseTool
+list(): BaseTool[]
+toOpenAIFormat(): OpenAITool[]
}
class BaseSession {
<<abstract>>
+id: string
+status: SessionStatus
+messages: Message[]
+usage: Usage
+configOverride: SessionConfigOverride
+addMessage(message): void
+save(): Promise~void~
+toSnapshot(): SessionSnapshot
+restoreFromSnapshot(snapshot): void
}
class BaseSessionManager {
<<abstract>>
+create(sessionId?): Promise~BaseSession~
+get(sessionId): Promise~BaseSession~
+list(): Promise~BaseSession[]~
+destroy(sessionId): Promise~void~
}
class Middleware {
<<function>>
(ctx: AgentLoopState, next: NextFunction) => Promise~void~
}
class AgentLoopState {
+sessionId: string
+messages: Message[]
+iteration: number
+pendingToolCalls: ToolCallWithResult[]
+currentResponse: string
+shouldContinue: boolean
+usage: Usage
}
Agent --> ModelClient : uses
Agent --> ToolRegistry : uses
Agent --> StateStore : uses
Agent --> BaseSessionManager : uses
Agent ..> Middleware : applies
Agent ..> AgentLoopState : manages
ToolRegistry --> BaseTool : contains
BaseSessionManager --> BaseSession : manages
GoatChain uses a Koa-style onion model for middleware. Each middleware wraps around the core execution:
outer:before → inner:before → exec (model.stream) → inner:after → outer:after
Middlewares can be named for easier management and removal:
// Add named middleware (recommended)
agent.use(async (state, next) => {
const start = Date.now()
console.log(`[${state.iteration}] Before model call`)
const nextState = await next(state)
console.log(`[${state.iteration}] After model call (${Date.now() - start}ms)`)
return nextState
}, 'logging')
// Remove by name
agent.removeMiddleware('logging')
// View all middleware names
console.log(agent.middlewareNames) // ['logging', 'compression', ...]
// Use unsubscribe function
const unsubscribe = agent.use(middleware, 'temp')
unsubscribe() // Remove middleware
GoatChain's built-in middleware factories automatically provide default names:
// Plan mode middleware - automatically named 'plan-mode'
agent.use(createPlanModeMiddleware())
// Context compression middleware - automatically named 'context-compression'
agent.use(createContextCompressionMiddleware({
maxTokens: 128000,
}))
// View all middleware
console.log(agent.middlewareNames)
// Output: ['plan-mode', 'context-compression']
// Remove by default name
agent.removeMiddleware('plan-mode')
// Or customize the name
agent.use(createPlanModeMiddleware({ name: 'my-plan' }))
agent.use(createContextCompressionMiddleware({
maxTokens: 128000,
}), 'my-compression') // Override default name
// Logging middleware
agent.use(async (state, next) => {
const start = Date.now()
console.log(`[${state.iteration}] Before model call`)
const nextState = await next(state) // Execute model stream
console.log(`[${state.iteration}] After model call (${Date.now() - start}ms)`)
return nextState
}, 'logging')
// Error handling middleware
agent.use(async (state, next) => {
try {
return await next(state)
}
catch (error) {
state.shouldContinue = false
state.stopReason = 'error'
state.error = error
return state
}
}, 'error-handler')
// Rate limiting middleware
agent.use(async (state, next) => {
await rateLimiter.acquire()
return next(state)
}, 'rate-limiter')
The session receive stream emits the following events:
| Event | Description |
|---|---|
iteration_start | Beginning of a loop iteration |
text_delta | Partial text response from LLM |
thinking_start | Thinking phase begins (if supported) |
thinking_delta | Thinking content delta (if supported) |
thinking_end | Thinking phase ends (if supported) |
tool_call_start | Tool call begins |
tool_call_delta | Tool call arguments delta |
tool_call_end | Tool call is complete |
tool_result | Tool execution completed |
iteration_end | End of a loop iteration (includes usage) |
done | Stream completed (includes usage) |
error | Error occurred |
interface AgentEvent {
type:
| 'text_delta'
| 'tool_call_start'
| 'tool_call_delta'
| 'tool_call_end'
| 'tool_result'
| 'thinking_start'
| 'thinking_delta'
| 'thinking_end'
| 'error'
| 'done'
| 'iteration_start'
| 'iteration_end'
// ... event-specific fields
}
iteration_end and done events include optional usage: Usage with cumulative token counts.
Built-in checkpoint support for resuming interrupted agent executions:
import { Agent, FileStateStore } from 'goatchain'
// Create state store with configuration
const stateStore = new FileStateStore({
dir: './checkpoints',
deleteOnComplete: true, // Clean up after successful completion
})
// Agent automatically saves checkpoints when stateStore is provided
const agent = new Agent({
name: 'MyAgent',
systemPrompt: 'You are helpful.',
model,
stateStore,
})
// Run agent - checkpoints are saved automatically
const session = await agent.createSession()
session.send('Hello')
for await (const event of session.receive()) {
console.log(event)
}
// If interrupted, resume from checkpoint
const checkpoint = await stateStore.loadCheckpoint(session.id)
if (checkpoint) {
const resumed = await agent.resumeSession(session.id)
for await (const event of resumed.receive()) {
console.log(event)
}
}
Available state stores:
FileStateStore - File-based persistenceInMemoryStateStore - In-memory (for testing)| Method | Description |
|---|---|
constructor(options) | Create a new agent |
use(middleware) | Add middleware |
createSession(options) | Create a session |
resumeSession(sessionId, options) | Resume a session |
setModel(modelOrRef) | Switch/pin model at runtime |
| Method | Description |
|---|---|
send(input, options?) | Queue input for the session |
receive(options?) | Stream events, resuming from checkpoint if present |
messages | Conversation history |
MIT © Simon He
FAQs
Unknown package
The npm package goatchain receives a total of 57 weekly downloads. As such, goatchain popularity was classified as not popular.
We found that goatchain demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
Malicious Chrome and Firefox extensions posed as free VPNs while stealing clipboard data through later extension updates.

Research
/Security News
Miasma Mini Shai-Hulud hits @immobiliarelabs Backstage plugins, targeting GitLab and LDAP auth packages on npm.

Security News
Rolldown paused Rust React Compiler integration after a 5MB binary size increase raised concerns about shipping React-specific code to all Vite users.