Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .changeset/core-browser-compat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
'@agentskit/core': major
'@agentskit/memory': minor
'@agentskit/cli': minor
'@agentskit/react': minor
'@agentskit/ink': minor
---

Remove Node-only code from `@agentskit/core` to restore browser compatibility.

**Breaking changes in `@agentskit/core`:**

- `createFileMemory(path)` removed. Use `fileChatMemory(path)` from `@agentskit/memory` instead.
- `loadConfig(options?)` and the `AgentsKitConfig` / `LoadConfigOptions` types removed. Use `loadConfig` / `AgentsKitConfig` / `LoadConfigOptions` from `@agentskit/cli` instead.

**Why:**

`@agentskit/core` must work in any environment (Node, Deno, edge, browser) per Manifesto principle 1. The previous implementation imported `node:fs/promises` at module load, crashing browser consumers at runtime. Closes #281.

**Migration:**

```diff
- import { createFileMemory } from '@agentskit/core'
+ import { fileChatMemory } from '@agentskit/memory'

- const memory = createFileMemory('./history.json')
+ const memory = fileChatMemory('./history.json')
```

```diff
- import { loadConfig } from '@agentskit/core'
+ import { loadConfig } from '@agentskit/cli'
```

`createInMemoryMemory` and `createLocalStorageMemory` remain in `@agentskit/core` — both are universal.
1 change: 1 addition & 0 deletions apps/example-ink/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"dependencies": {
"@agentskit/core": "workspace:*",
"@agentskit/ink": "workspace:*",
"@agentskit/memory": "workspace:*",
"ink": "^6.8.0",
"react": "^19.2.5"
},
Expand Down
4 changes: 2 additions & 2 deletions apps/example-ink/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import { render, Box, Text } from 'ink'
import { ChatContainer, InputBar, Message, ThinkingIndicator, ToolCallView, useChat } from '@agentskit/ink'
import { createFileMemory } from '@agentskit/core'
import { fileChatMemory } from '@agentskit/memory'
import type { AdapterFactory, ToolDefinition } from '@agentskit/core'

const getTimeTool: ToolDefinition = {
Expand Down Expand Up @@ -47,7 +47,7 @@ function createDemoAdapter(): AdapterFactory {
}
}

const memory = createFileMemory('.example-ink-history.json')
const memory = fileChatMemory('.example-ink-history.json')

function App() {
const chat = useChat({
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@changesets/cli": "2.30.0",
"@size-limit/file": "^12.1.0",
"size-limit": "^12.1.0",
"turbo": "2.9.5",
"turbo": "2.9.6",
"typescript": "^6.0.2",
"vitest": "^4.1.4"
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ChatContainer, InputBar, Message, ThinkingIndicator, ToolCallView, useC
import { resolveChatProvider } from './providers'
import { resolveTools, resolveMemory, skillRegistry } from './resolve'

import type { AgentsKitConfig } from '@agentskit/core'
import type { AgentsKitConfig } from './config'

export interface ChatCommandOptions {
provider: string
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import React from 'react'
import { render } from 'ink'
import { Command } from 'commander'
import path from 'node:path'
import { loadConfig } from '@agentskit/core'
import type { AgentsKitConfig } from '@agentskit/core'
import { loadConfig } from './config'
import type { AgentsKitConfig } from './config'
import { ChatApp, renderChatHeader } from './chat'
import { writeStarterProject } from './init'
import { runAgent } from './run'
Expand Down
13 changes: 10 additions & 3 deletions packages/core/src/config.ts → packages/cli/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,26 @@ export interface LoadConfigOptions {
cwd?: string
}

/**
* Load an AgentsKit config file. Node-only — uses fs/promises.
*
* Tries in order:
* 1. `.agentskit.config.ts` (imported as a module)
* 2. `.agentskit.config.json`
* 3. `"agentskit"` field in `package.json`
*
* Returns `undefined` if none found.
*/
export async function loadConfig(options?: LoadConfigOptions): Promise<AgentsKitConfig | undefined> {
const cwd = resolve(options?.cwd ?? process.cwd())

// 1. Try .agentskit.config.ts
const tsPath = join(cwd, '.agentskit.config.ts')
const tsConfig = await loadTsConfig(tsPath)
if (tsConfig) return tsConfig

// 2. Try .agentskit.config.json
const jsonPath = join(cwd, '.agentskit.config.json')
const jsonConfig = await loadJsonConfig(jsonPath)
if (jsonConfig) return jsonConfig

// 3. Try package.json "agentskit" field
return await loadPackageJsonConfig(cwd)
}
2 changes: 2 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { createCli } from './commands'
export { loadConfig } from './config'
export type { AgentsKitConfig, LoadConfigOptions } from './config'
export { ChatApp, renderChatHeader } from './chat'
export { writeStarterProject } from './init'
export { resolveChatProvider } from './providers'
Expand Down
5 changes: 2 additions & 3 deletions packages/cli/src/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { createFileMemory } from '@agentskit/core'
import type { ChatMemory, SkillDefinition, ToolDefinition } from '@agentskit/core'
import { webSearch, filesystem, shell } from '@agentskit/tools'
import { researcher, coder, planner, critic, summarizer, composeSkills } from '@agentskit/skills'
import { sqliteChatMemory } from '@agentskit/memory'
import { fileChatMemory, sqliteChatMemory } from '@agentskit/memory'

export const skillRegistry: Record<string, SkillDefinition> = {
researcher,
Expand Down Expand Up @@ -61,6 +60,6 @@ export function resolveMemory(backend: string | undefined, memoryPath: string):
return sqliteChatMemory({ path: memoryPath.replace(/\.json$/, '.db') })
case 'file':
default:
return createFileMemory(memoryPath)
return fileChatMemory(memoryPath)
}
}
4 changes: 1 addition & 3 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { createChatController } from './controller'
export { createInMemoryMemory, createLocalStorageMemory, createFileMemory, serializeMessages, deserializeMessages } from './memory'
export { createInMemoryMemory, createLocalStorageMemory, serializeMessages, deserializeMessages } from './memory'
export { createStaticRetriever, formatRetrievedDocuments } from './rag'
export {
generateId,
Expand Down Expand Up @@ -51,5 +51,3 @@ export type {
EvalResult,
EvalSuite,
} from './types'
export { loadConfig } from "./config"
export type { AgentsKitConfig, LoadConfigOptions } from "./config"
26 changes: 0 additions & 26 deletions packages/core/src/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,29 +56,3 @@ export function createLocalStorageMemory(key: string): ChatMemory {
},
}
}

export function createFileMemory(path: string): ChatMemory {
return {
async load() {
try {
const fs = await import('node:fs/promises')
const raw = await fs.readFile(path, 'utf8')
return deserializeMessages(JSON.parse(raw) as MemoryRecord)
} catch {
return []
}
},
async save(messages) {
const fs = await import('node:fs/promises')
await fs.writeFile(path, JSON.stringify(serializeMessages(messages), null, 2), 'utf8')
},
async clear() {
try {
const fs = await import('node:fs/promises')
await fs.unlink(path)
} catch {
// Ignore missing files.
}
},
}
}
46 changes: 2 additions & 44 deletions packages/core/tests/memory.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { createInMemoryMemory, createFileMemory } from '../src/memory'
import { describe, it, expect } from 'vitest'
import { createInMemoryMemory } from '../src/memory'
import type { Message } from '../src/types'
import { unlink } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'

const sampleMessage: Message = {
id: 'test-1',
Expand Down Expand Up @@ -47,42 +44,3 @@ describe('createInMemoryMemory', () => {
expect(loaded1).not.toBe(loaded2)
})
})

describe('createFileMemory', () => {
let filepath: string

beforeEach(() => {
filepath = join(tmpdir(), `agentskit-test-${Date.now()}.json`)
})

afterEach(async () => {
try { await unlink(filepath) } catch {}
})

it('returns empty array when file does not exist', async () => {
const mem = createFileMemory(filepath)
expect(await mem.load()).toEqual([])
})

it('save then load round-trips with date serialization', async () => {
const mem = createFileMemory(filepath)
await mem.save([sampleMessage])
const loaded = await mem.load()
expect(loaded).toHaveLength(1)
expect(loaded[0].content).toBe('hello')
expect(loaded[0].createdAt).toBeInstanceOf(Date)
expect(loaded[0].createdAt.toISOString()).toBe('2026-01-01T00:00:00.000Z')
})

it('clear removes the file', async () => {
const mem = createFileMemory(filepath)
await mem.save([sampleMessage])
await mem.clear!()
expect(await mem.load()).toEqual([])
})

it('clear on non-existent file does not throw', async () => {
const mem = createFileMemory(filepath)
await expect(mem.clear!()).resolves.toBeUndefined()
})
})
1 change: 0 additions & 1 deletion packages/ink/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export {
createChatController,
createInMemoryMemory,
createLocalStorageMemory,
createFileMemory,
createStaticRetriever,
formatRetrievedDocuments,
} from '@agentskit/core'
Expand Down
37 changes: 37 additions & 0 deletions packages/memory/src/file-chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { ChatMemory, MemoryRecord } from '@agentskit/core'
import { deserializeMessages, serializeMessages } from '@agentskit/core'

/**
* ChatMemory backed by a JSON file on disk. Node-only.
*
* Implements the Memory contract (ADR 0003):
* - load() returns a snapshot (CM1)
* - save() is replace-all, not append (CM2)
* - empty state returns [] (CM5)
* - clear() is optional but provided here
*/
export function fileChatMemory(path: string): ChatMemory {
return {
async load() {
try {
const fs = await import('node:fs/promises')
const raw = await fs.readFile(path, 'utf8')
return deserializeMessages(JSON.parse(raw) as MemoryRecord)
} catch {
return []
}
},
async save(messages) {
const fs = await import('node:fs/promises')
await fs.writeFile(path, JSON.stringify(serializeMessages(messages), null, 2), 'utf8')
},
async clear() {
try {
const fs = await import('node:fs/promises')
await fs.unlink(path)
} catch {
// Ignore missing files.
}
},
}
}
2 changes: 2 additions & 0 deletions packages/memory/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { fileChatMemory } from './file-chat'

export { sqliteChatMemory } from './sqlite'
export type { SqliteChatMemoryConfig } from './sqlite'

Expand Down
53 changes: 53 additions & 0 deletions packages/memory/tests/file-chat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { fileChatMemory } from '../src/file-chat'
import type { Message } from '@agentskit/core'
import { unlink } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'

const sampleMessage: Message = {
id: 'test-1',
role: 'user',
content: 'hello',
status: 'complete',
createdAt: new Date('2026-01-01T00:00:00Z'),
}

describe('fileChatMemory', () => {
let filepath: string

beforeEach(() => {
filepath = join(tmpdir(), `agentskit-test-${Date.now()}.json`)
})

afterEach(async () => {
try { await unlink(filepath) } catch {}
})

it('returns empty array when file does not exist', async () => {
const mem = fileChatMemory(filepath)
expect(await mem.load()).toEqual([])
})

it('save then load round-trips with date serialization', async () => {
const mem = fileChatMemory(filepath)
await mem.save([sampleMessage])
const loaded = await mem.load()
expect(loaded).toHaveLength(1)
expect(loaded[0].content).toBe('hello')
expect(loaded[0].createdAt).toBeInstanceOf(Date)
expect(loaded[0].createdAt.toISOString()).toBe('2026-01-01T00:00:00.000Z')
})

it('clear removes the file', async () => {
const mem = fileChatMemory(filepath)
await mem.save([sampleMessage])
await mem.clear!()
expect(await mem.load()).toEqual([])
})

it('clear on non-existent file does not throw', async () => {
const mem = fileChatMemory(filepath)
await expect(mem.clear!()).resolves.toBeUndefined()
})
})
1 change: 0 additions & 1 deletion packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export {
createChatController,
createInMemoryMemory,
createLocalStorageMemory,
createFileMemory,
createStaticRetriever,
formatRetrievedDocuments,
} from '@agentskit/core'
Expand Down
Loading
Loading