goatchain 0.0.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/README.md ADDED
@@ -0,0 +1,526 @@
1
+ # GoatChain ๐Ÿ
2
+
3
+ > A lightweight, extensible TypeScript SDK for building AI agents with streaming support, tool calling, and middleware pattern.
4
+
5
+ [![npm version](https://badge.fury.io/js/goatchain.svg)](https://badge.fury.io/js/goatchain)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## โœจ Features
9
+
10
+ - **๐Ÿ”„ Agentic Loop** - Automatic tool calling loop with configurable max iterations
11
+ - **๐Ÿ“ก Streaming First** - Real-time streaming responses with detailed events
12
+ - **๐Ÿง… Middleware Pattern** - Koa-style onion model for extensible hooks
13
+ - **๐Ÿ”ง Tool System** - Easy-to-use tool registration and execution
14
+ - **๐Ÿ’พ State Management** - Two-level state store (Agent + Session level)
15
+ - **๐Ÿ“ธ Snapshot/Restore** - Full persistence support for agents and sessions
16
+ - **๐ŸŽฏ TypeScript Native** - Full type safety with comprehensive type exports
17
+
18
+ ## ๐Ÿ“ฆ Installation
19
+
20
+ ```bash
21
+ pnpm add goatchain
22
+ ```
23
+
24
+ ## ๐Ÿ“– Documentation
25
+
26
+ ๅฎŒๆ•ด็š„ๆ–‡ๆกฃ่ฏทๆŸฅ็œ‹ [docs/](./docs/) ็›ฎๅฝ•๏ผš
27
+
28
+ - [ๅฟซ้€Ÿๅผ€ๅง‹](./docs/getting-started.md) - ๅˆ›ๅปบไฝ ็š„็ฌฌไธ€ไธช Agent Loop
29
+ - [ๆ–‡ๆกฃ็ดขๅผ•](./docs/README.md) - ๆ‰€ๆœ‰ๆ–‡ๆกฃ็š„ๅฎŒๆ•ดๅˆ—่กจ
30
+
31
+ ## ๐Ÿงฐ CLI
32
+
33
+ After installing `goatchain-cli` globally (or using `pnpm -s cli` in this repo), run:
34
+
35
+ ```bash
36
+ goatchain
37
+ ```
38
+
39
+ Common options:
40
+
41
+ - `-k, --api-key <key>` (or set `OPENAI_API_KEY`)
42
+ - `-m, --model <id>`
43
+ - `--base-url <url>`
44
+ - `--max-tokens <n>`
45
+ - `--temperature <n>`
46
+
47
+ Commands:
48
+
49
+ - `/help` help
50
+ - `/model <id>` switch model id (OpenAI)
51
+ - `/set <k> <v>` set request params (e.g. `temperature`, `maxTokens`, `topP`)
52
+ - `/unset <k>` clear a request param
53
+ - `/params` show current request params
54
+ - `/base-url <url>` set base URL
55
+ - `/api-key <key>` set API key (not printed)
56
+ - `/tools` list enabled tools (Read/Write/Edit/Glob/Grep/WebSearch*)
57
+ - `/sessions` list and pick a saved session
58
+ - `/use <sessionId>` restore a saved session (prints recent history)
59
+ - `/save` persist current config/session
60
+ - `/status` show current model/session info
61
+ - `/new` start a new conversation (clears history)
62
+
63
+ Requires `OPENAI_API_KEY` in the environment.
64
+
65
+ Web search (optional):
66
+
67
+ - Set `SERPER_API_KEY` (or `GOATCHAIN_SERPER_API_KEY`) to enable the builtin `WebSearch` tool for up-to-date info like weather.
68
+ - You can also set it in `./.goatchain/config.json` (workspace-scoped, gitignored):
69
+ - `{"tools":{"webSearch":{"apiKey":"...","apiEndpoint":"...","numResults":10}}}`
70
+
71
+ Local persistence (workspace-scoped):
72
+
73
+ - Config and sessions are saved under `./.goatchain/` (auto-created).
74
+ - `.goatchain/` is gitignored to avoid accidentally committing secrets.
75
+
76
+ DeepSeek thinking mode compatibility:
77
+
78
+ - Some OpenAI-compatible gateways (e.g. DeepSeek thinking mode) require `reasoning_content` to be present on assistant messages that contain `tool_calls` (and may reject empty strings). GoatChain will attach the accumulated thinking content when available.
79
+ - If you use DeepSeek via a proxy where GoatChain can't detect it from `baseUrl`/`modelId`, set `openai.compat.requireReasoningContentForToolCalls=true` in `./.goatchain/config.json`.
80
+
81
+ ## ๐Ÿ—๏ธ Architecture
82
+
83
+ ```mermaid
84
+ classDiagram
85
+ direction TB
86
+
87
+ class Agent {
88
+ +id: string
89
+ +name: string
90
+ +systemPrompt: string
91
+ +model: BaseModel
92
+ +tools: ToolRegistry
93
+ +stateStore: StateStore
94
+ +sessionManager: BaseSessionManager
95
+ +stats: AgentStats
96
+ +use(middleware): this
97
+ +createSession(options): BaseSession
98
+ +resumeSession(sessionId, options): BaseSession
99
+ +setModel(modelOrRef): void
100
+ }
101
+
102
+ class BaseModel {
103
+ <<abstract>>
104
+ +modelId: string
105
+ +invoke(messages, options): Promise~ChatResponse~
106
+ +stream(messages, options): AsyncIterable~StreamEvent~
107
+ }
108
+
109
+ class StateStore {
110
+ <<interface>>
111
+ +savePoint: string
112
+ +deleteOnComplete: boolean
113
+ +saveCheckpoint(checkpoint): Promise~void~
114
+ +loadCheckpoint(sessionId): Promise~AgentLoopCheckpoint~
115
+ +deleteCheckpoint(sessionId): Promise~void~
116
+ +listCheckpoints(agentId): Promise~AgentLoopCheckpoint[]~
117
+ }
118
+
119
+ class BaseTool {
120
+ <<abstract>>
121
+ +name: string
122
+ +description: string
123
+ +parameters: JSONSchema
124
+ +execute(args, ctx?): Promise~unknown~
125
+ }
126
+
127
+ class ToolRegistry {
128
+ +register(tool): void
129
+ +unregister(name): boolean
130
+ +get(name): BaseTool
131
+ +list(): BaseTool[]
132
+ +toOpenAIFormat(): OpenAITool[]
133
+ }
134
+
135
+ class BaseSession {
136
+ <<abstract>>
137
+ +id: string
138
+ +agentId: string
139
+ +status: SessionStatus
140
+ +messages: Message[]
141
+ +usage: Usage
142
+ +configOverride: SessionConfigOverride
143
+ +addMessage(message): void
144
+ +save(): Promise~void~
145
+ +toSnapshot(): SessionSnapshot
146
+ +restoreFromSnapshot(snapshot): void
147
+ }
148
+
149
+ class BaseSessionManager {
150
+ <<abstract>>
151
+ +create(agentId, metadata): Promise~BaseSession~
152
+ +get(sessionId): Promise~BaseSession~
153
+ +list(agentId): Promise~BaseSession[]~
154
+ +destroy(sessionId): Promise~void~
155
+ }
156
+
157
+ class Middleware {
158
+ <<function>>
159
+ (ctx: AgentLoopState, next: NextFunction) => Promise~void~
160
+ }
161
+
162
+ class AgentLoopState {
163
+ +sessionId: string
164
+ +agentId: string
165
+ +messages: Message[]
166
+ +iteration: number
167
+ +pendingToolCalls: ToolCallWithResult[]
168
+ +currentResponse: string
169
+ +shouldContinue: boolean
170
+ +usage: Usage
171
+ }
172
+
173
+ Agent --> BaseModel : uses
174
+ Agent --> ToolRegistry : uses
175
+ Agent --> StateStore : uses
176
+ Agent --> BaseSessionManager : uses
177
+ Agent ..> Middleware : applies
178
+ Agent ..> AgentLoopState : manages
179
+ ToolRegistry --> BaseTool : contains
180
+ BaseSessionManager --> BaseSession : manages
181
+ ```
182
+
183
+ ## ๐Ÿš€ Quick Start
184
+
185
+ ๆœ€็ฎ€ๅ•็š„ Agent Loop ็คบไพ‹๏ผš
186
+
187
+ ```typescript
188
+ import process from 'node:process'
189
+ import { Agent, createModel, createOpenAIAdapter } from 'goatchain'
190
+
191
+ // ๅˆ›ๅปบๆจกๅž‹
192
+ const model = createModel({
193
+ adapters: [
194
+ createOpenAIAdapter({
195
+ defaultModelId: 'gpt-4o',
196
+ apiKey: process.env.OPENAI_API_KEY!,
197
+ }),
198
+ ],
199
+ })
200
+
201
+ // ๅˆ›ๅปบ Agent
202
+ const agent = new Agent({
203
+ name: 'Simple Assistant',
204
+ systemPrompt: 'You are a helpful assistant.',
205
+ model,
206
+ })
207
+
208
+ // ๆตๅผๅค„็†ๅ“ๅบ”
209
+ const session = await agent.createSession()
210
+ session.send('Hello!')
211
+ for await (const event of session.receive()) {
212
+ if (event.type === 'text_delta') {
213
+ process.stdout.write(event.delta)
214
+ } else if (event.type === 'done') {
215
+ console.log('\nๅฎŒๆˆ:', event.stopReason)
216
+ }
217
+ }
218
+ ```
219
+
220
+ ๐Ÿ“– **่ฏฆ็ป†ๆ–‡ๆกฃ**: ๆŸฅ็œ‹ [docs/getting-started.md](./docs/getting-started.md) ไบ†่งฃๆ›ดๅคš็คบไพ‹ๅ’ŒๅฎŒๆ•ดๆŒ‡ๅ—ใ€‚
221
+
222
+ ## ๐Ÿง… Middleware Pattern
223
+
224
+ GoatChain uses a Koa-style onion model for middleware. Each middleware wraps around the core execution:
225
+
226
+ ```
227
+ outer:before โ†’ inner:before โ†’ exec (model.stream) โ†’ inner:after โ†’ outer:after
228
+ ```
229
+
230
+ ```typescript
231
+ // Logging middleware
232
+ agent.use(async (state, next) => {
233
+ const start = Date.now()
234
+ console.log(`[${state.iteration}] Before model call`)
235
+
236
+ await next() // Execute model stream
237
+
238
+ console.log(`[${state.iteration}] After model call (${Date.now() - start}ms)`)
239
+ })
240
+
241
+ // Error handling middleware
242
+ agent.use(async (state, next) => {
243
+ try {
244
+ await next()
245
+ } catch (error) {
246
+ state.shouldContinue = false
247
+ state.stopReason = 'error'
248
+ state.error = error
249
+ }
250
+ })
251
+
252
+ // Rate limiting middleware
253
+ agent.use(async (state, next) => {
254
+ await rateLimiter.acquire()
255
+ await next()
256
+ })
257
+ ```
258
+
259
+ ## ๐Ÿ“ก Event Types
260
+
261
+ The session receive stream emits the following events:
262
+
263
+ | Event | Description |
264
+ | ----------------- | ------------------------------------- |
265
+ | `iteration_start` | Beginning of a loop iteration |
266
+ | `text_delta` | Partial text response from LLM |
267
+ | `thinking_start` | Thinking phase begins (if supported) |
268
+ | `thinking_delta` | Thinking content delta (if supported) |
269
+ | `thinking_end` | Thinking phase ends (if supported) |
270
+ | `tool_call_start` | Tool call begins |
271
+ | `tool_call_delta` | Tool call arguments delta |
272
+ | `tool_call_end` | Tool call is complete |
273
+ | `tool_result` | Tool execution completed |
274
+ | `iteration_end` | End of a loop iteration (includes usage) |
275
+ | `done` | Stream completed (includes usage) |
276
+ | `error` | Error occurred |
277
+
278
+ ```typescript
279
+ interface AgentEvent {
280
+ type:
281
+ | 'text_delta'
282
+ | 'tool_call_start'
283
+ | 'tool_call_delta'
284
+ | 'tool_call_end'
285
+ | 'tool_result'
286
+ | 'thinking_start'
287
+ | 'thinking_delta'
288
+ | 'thinking_end'
289
+ | 'error'
290
+ | 'done'
291
+ | 'iteration_start'
292
+ | 'iteration_end'
293
+ // ... event-specific fields
294
+ }
295
+ ```
296
+
297
+ `iteration_end` and `done` events include optional `usage: Usage` with cumulative token counts.
298
+
299
+ ## ๐Ÿ’พ Checkpoint & Resume
300
+
301
+ Built-in checkpoint support for resuming interrupted agent executions:
302
+
303
+ ```typescript
304
+ import { Agent, FileStateStore } from 'goatchain'
305
+
306
+ // Create state store with configuration
307
+ const stateStore = new FileStateStore({
308
+ dir: './checkpoints',
309
+ savePoint: 'before', // Save before each iteration
310
+ deleteOnComplete: true, // Clean up after successful completion
311
+ })
312
+
313
+ // Agent automatically saves checkpoints when stateStore is provided
314
+ const agent = new Agent({
315
+ name: 'MyAgent',
316
+ systemPrompt: 'You are helpful.',
317
+ model,
318
+ stateStore,
319
+ })
320
+
321
+ // Run agent - checkpoints are saved automatically
322
+ const session = await agent.createSession()
323
+ session.send('Hello')
324
+ for await (const event of session.receive()) {
325
+ console.log(event)
326
+ }
327
+
328
+ // If interrupted, resume from checkpoint
329
+ const checkpoint = await stateStore.loadCheckpoint(session.id)
330
+ if (checkpoint) {
331
+ const resumed = await agent.resumeSession(session.id)
332
+ for await (const event of resumed.receive()) {
333
+ console.log(event)
334
+ }
335
+ }
336
+ ```
337
+
338
+ Available state stores:
339
+
340
+ - `FileStateStore` - File-based persistence
341
+ - `InMemoryStateStore` - In-memory (for testing)
342
+
343
+ ## ๐Ÿ”ง Session Management
344
+
345
+ Sessions represent individual conversations with per-session configuration overrides:
346
+
347
+ ```typescript
348
+ // Create session
349
+ const session = await sessionManager.create(agent.id, {
350
+ customField: 'value',
351
+ })
352
+
353
+ // Session-level overrides
354
+ session.setModelOverride({ modelId: 'gpt-4o-mini' })
355
+ session.setSystemPromptOverride('You are a concise assistant.')
356
+ session.disableTools(['dangerous_tool'])
357
+
358
+ // Track session activity
359
+ session.addMessage({ role: 'user', content: 'Hello!' })
360
+ session.addUsage({ promptTokens: 10, completionTokens: 5, totalTokens: 15 })
361
+ session.recordResponse(1500) // ms
362
+
363
+ // Get session snapshot for persistence
364
+ const snapshot = session.toSnapshot()
365
+ ```
366
+
367
+ ## ๐Ÿ“ Project Structure
368
+
369
+ ```
370
+ src/
371
+ โ”œโ”€โ”€ index.ts # Public exports
372
+ โ”œโ”€โ”€ types/
373
+ โ”‚ โ”œโ”€โ”€ index.ts # Re-export all types
374
+ โ”‚ โ”œโ”€โ”€ message.ts # Message types (User, Assistant, Tool, System)
375
+ โ”‚ โ”œโ”€โ”€ event.ts # Stream event types
376
+ โ”‚ โ”œโ”€โ”€ common.ts # Shared types (ToolCall, Usage, JSONSchema)
377
+ โ”‚ โ””โ”€โ”€ snapshot.ts # Snapshot types (Agent, Session)
378
+ โ”œโ”€โ”€ model/
379
+ โ”‚ โ”œโ”€โ”€ index.ts
380
+ โ”‚ โ”œโ”€โ”€ base.ts # BaseModel abstract class
381
+ โ”‚ โ””โ”€โ”€ types.ts # Model-specific types
382
+ โ”œโ”€โ”€ state/
383
+ โ”‚ โ”œโ”€โ”€ index.ts
384
+ โ”‚ โ”œโ”€โ”€ stateStore.ts # StateStore interface
385
+ โ”‚ โ”œโ”€โ”€ FileStateStore.ts # File-based state storage
386
+ โ”‚ โ””โ”€โ”€ InMemoryStateStore.ts # In-memory state storage
387
+ โ”œโ”€โ”€ tool/
388
+ โ”‚ โ”œโ”€โ”€ index.ts
389
+ โ”‚ โ”œโ”€โ”€ base.ts # BaseTool abstract class
390
+ โ”‚ โ””โ”€โ”€ registry.ts # ToolRegistry class
391
+ โ”œโ”€โ”€ session/
392
+ โ”‚ โ”œโ”€โ”€ index.ts
393
+ โ”‚ โ”œโ”€โ”€ base.ts # BaseSession abstract class
394
+ โ”‚ โ””โ”€โ”€ manager.ts # BaseSessionManager abstract class
395
+ โ””โ”€โ”€ agent/
396
+ โ”œโ”€โ”€ index.ts
397
+ โ”œโ”€โ”€ agent.ts # Agent class
398
+ โ”œโ”€โ”€ types.ts # Agent types (AgentLoopState, AgentInput, etc.)
399
+ โ”œโ”€โ”€ middleware.ts # Middleware compose function
400
+ โ””โ”€โ”€ errors.ts # Agent-specific errors
401
+ ```
402
+
403
+ ## ๐Ÿ›ก๏ธ Error Handling
404
+
405
+ ```typescript
406
+ import { AgentAbortError, AgentMaxIterationsError } from 'goatchain'
407
+
408
+ // Cancellation support
409
+ const controller = new AbortController()
410
+
411
+ try {
412
+ const session = await agent.createSession({ maxIterations: 5 })
413
+ session.send('Hello', { signal: controller.signal })
414
+ for await (const event of session.receive()) {
415
+ // Handle events...
416
+ }
417
+ } catch (error) {
418
+ if (error instanceof AgentAbortError) {
419
+ console.log('Agent was cancelled')
420
+ } else if (error instanceof AgentMaxIterationsError) {
421
+ console.log('Max iterations reached')
422
+ }
423
+ }
424
+
425
+ // Cancel from another context
426
+ controller.abort()
427
+ ```
428
+
429
+ ## ๐Ÿ“– API Reference
430
+
431
+ ### Agent
432
+
433
+ | Method | Description |
434
+ | ------------------------------- | ----------------------- |
435
+ | `constructor(options)` | Create a new agent |
436
+ | `use(middleware)` | Add middleware |
437
+ | `createSession(options)` | Create a session |
438
+ | `resumeSession(sessionId, options)` | Resume a session |
439
+ | `setModel(modelOrRef)` | Switch/pin model at runtime |
440
+
441
+ ### Session
442
+
443
+ | Method | Description |
444
+ | ----------------------- | -------------------------------------------- |
445
+ | `send(input, options?)` | Queue input for the session |
446
+ | `receive(options?)` | Stream events, resuming from checkpoint if present |
447
+ | `messages` | Conversation history |
448
+
449
+ ### CreateSessionOptions
450
+
451
+ | Property | Type | Description |
452
+ | ---------------- | ------------- | ---------------------------------------- |
453
+ | `model?` | `ModelRef` | Optional model override for this session |
454
+ | `maxIterations?` | `number` | Max loop iterations (default: 10) |
455
+ | `hooks?` | `ToolHooks` | Tool execution hooks |
456
+ | `requestParams?` | `object` | Model request parameters |
457
+
458
+ ### SendOptions
459
+
460
+ | Property | Type | Description |
461
+ | ---------------- | ------------- | ---------------------------------------- |
462
+ | `signal?` | `AbortSignal` | Cancellation support |
463
+ | `toolContext?` | `object` | Tool execution context input |
464
+
465
+ ## ๐Ÿ”„ Model Switching
466
+
467
+ ### Per-Request Model Override
468
+
469
+ Temporarily use a different model for a single request:
470
+
471
+ ```typescript
472
+ const session = await agent.createSession({
473
+ model: { provider: 'openai', modelId: 'gpt-4' }, // Use GPT-4 for this session
474
+ })
475
+ session.send('Explain quantum physics')
476
+ for await (const event of session.receive()) {
477
+ // ...
478
+ }
479
+ ```
480
+
481
+ ### Persistent Model Switch
482
+
483
+ Change the default model for all subsequent requests:
484
+
485
+ ```typescript
486
+ // Switch model at runtime
487
+ model.setModelId('gpt-4')
488
+
489
+ // All subsequent requests will use the new model
490
+ const session = await agent.createSession()
491
+ session.send('Hello')
492
+ for await (const event of session.receive()) {
493
+ // Uses gpt-4
494
+ }
495
+ ```
496
+
497
+ ### Pin Default Model (Overrides Routing)
498
+
499
+ If your `ModelClient` supports routing (e.g. created via `createModel()`), you can pin a specific default model at the agent level:
500
+
501
+ ```typescript
502
+ agent.setModel({ provider: 'openai', modelId: 'gpt-4' })
503
+ ```
504
+
505
+ ### Multi-Provider Fallback
506
+
507
+ Configure multiple models with automatic fallback:
508
+
509
+ ```typescript
510
+ const model = createModel({
511
+ adapters: [
512
+ createOpenAIAdapter({ defaultModelId: 'gpt-4' }),
513
+ createAnthropicAdapter({ defaultModelId: 'claude-3' }),
514
+ ],
515
+ routing: {
516
+ fallbackOrder: [
517
+ { provider: 'openai', modelId: 'gpt-4' }, // Try first
518
+ { provider: 'anthropic', modelId: 'claude-3' }, // Fallback if first fails
519
+ ],
520
+ },
521
+ })
522
+ ```
523
+
524
+ ## ๐Ÿ“„ License
525
+
526
+ MIT ยฉ [Simon He](https://github.com/Simon-He95)