Skip to content
Open
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
86 changes: 61 additions & 25 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,76 @@
# Repository Guidelines
# Personal Preference

## Project Structure & Module Organization
## TypeScript

This repository is a `pnpm` workspace with TypeScript packages under `packages/`, including `packages/aikit` (the AI agent toolkit) and `packages/sdk` (the API SDK). `packages/utils` holds shared helpers that are bundled into dependents and marked `private`, so it is not published. Source files live in each package's `src/` directory. `aikit` tests live in `packages/aikit/test/` (`*.test.ts`), with opt-in live-provider suites under `packages/aikit/test/e2e/` (`*.e2e.test.ts`). Shared repo files include `vite.config.ts`, `models.json`, `assets/` for static assets, and `scripts/publish.js` for package publishing.
- Never use `any` unless 100% necessary or specifically instructed.
- Use effect only if the project uses effect or specifically instructed, use effect reference repository for latest architecture patterns.

## Build, Test, and Development Commands
## Commands

Use Node `>=24.14.1` and `pnpm@10`.
- Don't run dev server commands (e.g `pnpm run dev`) - assume it's already running.
- Don't run build commands unless specifically told to.
- Focus on checking commands like `pnpm run typecheck`, `pnpm run check`, `bun run lint`, etc

- `pnpm install`: install workspace dependencies.
- `pnpm lint`: run `vite-plus` lint checks across the repo.
- `pnpm check`: run TypeScript and repo validation checks.
- `pnpm test`: run the workspace test suite.
- `pnpm build`: build all packages.
- `pnpm --filter @codeworksh/aikit test`: run only `aikit` tests.
- `pnpm release:aikit:dry`: dry-run the publish flow for `aikit` (or `pnpm release:dry` inside `packages/aikit`).
## Package Managers

## Coding Style & Naming Conventions
- Use `pnpm`, `vp - vite plus` if the project already uses it, otherwise use `pnpm`
- Never use `npm` or `yarn

The codebase uses ESM TypeScript with strict compiler settings. Follow the formatting configured in `vite.config.ts`: tabs for indentation, tab width `3`, and print width `120`. Prefer named exports, keep modules small, and use clear singular file names such as `agent.ts`, `model.ts`, and `stream.ts`. Test files should end in `.test.ts`. Keep imports explicit and consistent with the existing source, including `.ts` extensions where already used.
## Tech Stack Preference

## Testing Guidelines
When uncertain, prefer: Effect, Tailwind, TypeScript, React, Clerk, TanStack, Vercel, Vite

Tests use `vite-plus/test`. Add coverage for every behavior change in the affected package; for `aikit`, place tests in `packages/aikit/test/`. Prefer focused unit tests that mirror the source area being changed, for example `packages/aikit/test/stream.test.ts` for streaming behavior. Run `pnpm test` before opening a PR and use package-scoped test commands while iterating.
## Code Style

## Commit & Pull Request Guidelines
- Always strive for concise, simple solutions.
- If a problem can be solved in a simpler way, propose it.
- No pre-mature optimizations, propose if required.
- Use single word filename whenever possible otherwise split into kabab-case(only if no options).

## General Preferences

- If asked to do too much work at once, stop and state that clearly.
- If computer use is helpful for completing or verifying work, use it.

## Maintainability

Long term maintainability is a core priority. If you add new functionality, first check if there is shared logic that can be extracted to a separate module. Duplicate logic across multiple files is a code smell and should be avoided. Don't be afraid to change existing code. Don't take shortcuts by just adding local logic to solve a problem.

## Package Roles

- `packages/aikit` - Low level SDK that provides unified LLM API for multiple LLM providers.
- `packages/harness` - Effect powered main Agent Harness application, uses `packages/aikit` under the hood.
- `packages/agent` - DEPRECATED.
- `packages/bridge` - WIP.
- `packages/desktop` - WIP.
- `packages/sdk` - WIP.
- `packages/utils` - DEPRECATED.
- `packages/webui` - WIP.

## Vendored Repositories

This project vendors external repositories under `.repos/` as read-only reference material for coding agents.

- Prefer examples and patterns from the vendored source code over generated guesses or web search results.
- Do not edit files under .repos/ unless explicitly asked.
- Do not import from `.repos/`; application code must continue importing from normal package dependencies.
- When writing Effect code, read `.repos/effect-smol/LLMS.md` first and inspect `.repos/effect-smol/` for examples of idiomatic usage, tests, module structure, and API design.

Recent history follows Conventional Commit style with optional package scopes, for example `feat(aikit): ...`, `fix(aikit): ...`, and `chore: ...`. Keep commit subjects imperative and concise.
## Personal Notes & Document Spec

PRs should describe the behavior change, list affected packages, and include test evidence such as `pnpm test` or `pnpm --filter ... test` output. Link related issues when applicable. Screenshots are only useful for docs or asset updates; library API changes should include code samples instead.
This project uses private design documents under `.notes/` as implementation reference for coding agents. When specified use it for planning, designing and brainstorming. When creating, mention in doc current date and git commit ID if available.

- Allow the design docs to be reviewed by subagents when see fit.
- Propose improvements when see fit.

## Task Completion Requirements

- `vp check` and `vp run typecheck` must pass before considering tasks completed, unless otherwise specified.
- Use `vp test` for the built-in Vite+ test command and `vp run test` when you specifically need the test package script.

## Commit & Pull Request Guidelines

Conventional Commit style with optional package scopes, e.g `feat(aikit): ...`, `fix(aikit): ...`, and `chore: ...`. Keep commit subjects imperative and concise.

## Releasing

Expand All @@ -40,9 +82,3 @@ Flow (shown for `aikit`; substitute `sdk` as needed):
2. `pnpm bump:aikit` (or `pnpm run bump` inside `packages/aikit`): pick the bump; it commits and tags `@codeworksh/aikit@<version>` without pushing.
3. `git push && git push --tag`.
4. `pnpm release:aikit:dry` to preview the tarball, then `pnpm release:aikit` to publish. Use `pnpm release:aikit:dev` for a prerelease under the `dev` dist-tag.

Inside a package, drop the `:aikit` suffix: `pnpm run bump`, `pnpm run release:dry`, `pnpm run release:dev`, `pnpm run release`.

## Security & Configuration Tips

Do not commit secrets. Provider keys such as `ANTHROPIC_API_KEY` should come from the environment. Treat `models.json` and provider integrations as compatibility-sensitive files and note any downstream impact when changing them.
42 changes: 39 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
## TypeScript

- Never use `any` unless 100% necessary or specifically instructed.
- Use effect only if the project uses effect or specifically instructed, use effect reference repository for latest architecture patterns: `repos/effect`.
- Use effect only if the project uses effect or specifically instructed, use effect reference repository for latest architecture patterns.

## Commands

- Don't run dev server commands (e.g `bun run dev`) - assume it's already running.
- Don't run dev server commands (e.g `bun run dev`, `pnpm run dev`) - assume it's already running.
- Don't run build commands unless specifically told to.
- Focus on checking commands like `bun run typecheck`, `pnpm run check`, `bun run lint`, etc.

Expand All @@ -25,12 +25,28 @@ When uncertain, prefer: Effect, Tailwind, TypeScript, React, Clerk, TanStack, Ve
- Always strive for concise, simple solutions.
- If a problem can be solved in a simpler way, propose it.
- No pre-mature optimizations, propose if required.
- Use single word filename whenever possible otherwise split into kabab-case(only if no options).

## General Preferences

- If asked to do too much work at once, stop and state that clearly.
- If computer use is helpful for completing or verifying work, shell out to gpt-5.5 with Codex for it

## Maintainability

Long term maintainability is a core priority. If you add new functionality, first check if there is shared logic that can be extracted to a separate module. Duplicate logic across multiple files is a code smell and should be avoided. Don't be afraid to change existing code. Don't take shortcuts by just adding local logic to solve a problem.

## Package Roles

- `packages/aikit` - Low level SDK that provides unified LLM API for multiple LLM providers.
- `packages/harness` - Effect powered main Agent Harness application, uses `packages/aikit` under the hood.
- `packages/agent` - DEPRECATED.
- `packages/bridge` - WIP.
- `packages/desktop` - WIP.
- `packages/sdk` - WIP.
- `packages/utils` - DEPRECATED.
- `packages/webui` - WIP.

## Picking the right models for workflows and subagents

Rankings, higher = better. Cost reflects what I actually pay, not list price. Intelligence is how hard a problem you can hand the model unsupervised. Taste covers UI/UX, code quality, API design, and copy.
Expand Down Expand Up @@ -72,7 +88,27 @@ This project vendors external repositories under `.repos/` as read-only refere

## Personal Notes & Document Spec

This project uses private design documents under `.notes/` as implementation reference for coding agents. When specified use it for planning, designing and brainstorming. When creating, mention in doc current date for future reference.
This project uses private design documents under `.notes/` as implementation reference for coding agents. When specified use it for planning, designing and brainstorming. When creating, mention in doc current date and git commit ID if available.

- Allow the design docs to be reviewed by subagents when see fit.
- Propose improvements when see fit.

## Task Completion Requirements

- `vp check` and `vp run typecheck` must pass before considering tasks completed, unless otherwise specified.
- Use `vp test` for the built-in Vite+ test command and `vp run test` when you specifically need the test package script.

## Commit & Pull Request Guidelines

Conventional Commit style with optional package scopes, e.g `feat(aikit): ...`, `fix(aikit): ...`, and `chore: ...`. Keep commit subjects imperative and concise.

## Releasing

Publishable packages (`@codeworksh/aikit`, `@codeworksh/sdk`) each own their release scripts; the root exposes `*:aikit` / `*:sdk` aliases so every command works from the repo root or from inside the package. Versioning uses `bumpp`, configured per package in `bump.config.ts` (tags follow `@codeworksh/<pkg>@<version>` and pushing is disabled). Build + publish run through `scripts/publish.js`, which builds, rewrites the manifest, and publishes from a temp dir. Pushing and publishing stay manual.

Flow (shown for `aikit`; substitute `sdk` as needed):

1. Update `CHANGELOG.md` — move the entry from `[Unreleased]` into a versioned section.
2. `pnpm bump:aikit` (or `pnpm run bump` inside `packages/aikit`): pick the bump; it commits and tags `@codeworksh/aikit@<version>` without pushing.
3. `git push && git push --tag`.
4. `pnpm release:aikit:dry` to preview the tarball, then `pnpm release:aikit` to publish. Use `pnpm release:aikit:dev` for a prerelease under the `dev` dist-tag.
62 changes: 62 additions & 0 deletions packages/harness/src/db/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,74 @@ export const migrations = {
title TEXT NOT NULL,
tag TEXT NOT NULL,
metadata TEXT,
cost REAL NOT NULL DEFAULT 0,
tokens_input INTEGER NOT NULL DEFAULT 0,
tokens_output INTEGER NOT NULL DEFAULT 0,
tokens_cache_read INTEGER NOT NULL DEFAULT 0,
tokens_cache_write INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`;

yield* sql`CREATE UNIQUE INDEX session_slug_idx ON session (slug)`;
yield* sql`CREATE INDEX session_tag_idx ON session (tag)`;

// The tree edge is a composite FK (session_id, parent_id) so a parent can
// never live in another session — cross-session edges would mess up the context
// NULL parent_id (roots) skips the FK per SQLite.
// In short: FK (session_id, parent_id); makes sure that a child entry via parent_id must belong
// to exact same session_id.
yield* sql`
CREATE TABLE session_entry (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL REFERENCES session(id) ON UPDATE CASCADE ON DELETE CASCADE,
parent_id TEXT,
seq INTEGER NOT NULL,
type TEXT NOT NULL,
data TEXT NOT NULL,
label TEXT,
metadata TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (session_id, parent_id) REFERENCES session_entry(session_id, id)
)
`;

// Parent key for the composite FKs (SQLite requires a UNIQUE covering
// the referenced columns).
yield* sql`CREATE UNIQUE INDEX session_entry_session_id_idx ON session_entry (session_id, id)`;

yield* sql`
CREATE TABLE session_entry_part (
id TEXT PRIMARY KEY,
entry_id TEXT NOT NULL,
session_id TEXT NOT NULL REFERENCES session(id) ON UPDATE CASCADE ON DELETE CASCADE,
part_index INTEGER NOT NULL,
type TEXT NOT NULL,
status TEXT,
call_id TEXT,
tool_name TEXT,
data TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
CHECK (type != 'toolCall' OR (status IS NOT NULL AND call_id IS NOT NULL AND tool_name IS NOT NULL)),
CHECK (type = 'toolCall' OR (status IS NULL AND call_id IS NULL AND tool_name IS NULL)),
FOREIGN KEY (session_id, entry_id) REFERENCES session_entry(session_id, id) ON UPDATE CASCADE ON DELETE CASCADE
)
`;

yield* sql`ALTER TABLE session ADD COLUMN leaf_entry_id TEXT REFERENCES session_entry(id)`;

yield* sql`CREATE UNIQUE INDEX session_entry_session_seq_idx ON session_entry (session_id, seq)`;
yield* sql`CREATE INDEX session_entry_parent_idx ON session_entry (session_id, parent_id)`;
yield* sql`CREATE INDEX session_entry_type_idx ON session_entry (session_id, type, seq)`;
yield* sql`CREATE INDEX session_entry_label_idx ON session_entry (session_id, seq) WHERE label IS NOT NULL`;

yield* sql`CREATE UNIQUE INDEX session_entry_part_entry_idx ON session_entry_part (entry_id, part_index)`;
yield* sql`CREATE INDEX session_entry_part_call_idx ON session_entry_part (session_id, call_id) WHERE call_id IS NOT NULL`;
yield* sql`CREATE UNIQUE INDEX session_entry_part_call_uidx ON session_entry_part (entry_id, call_id) WHERE call_id IS NOT NULL`;
yield* sql`CREATE INDEX session_entry_part_unsettled_idx ON session_entry_part (session_id, status) WHERE status IN ('pending', 'running')`;
yield* sql`CREATE INDEX session_entry_part_session_idx ON session_entry_part (session_id, entry_id, part_index)`;
}),
};
70 changes: 70 additions & 0 deletions packages/harness/src/db/schema.sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,33 @@ export const Timestamps = {

const Metadata = Schema.Record(Schema.String, Schema.String);

// Session entry discriminant. Frozen once shipped — values live in persisted
// rows, so a rename is a data migration. Additions are code-only (the column is unconstrained TEXT).
export const entryTypes = [
"user",
"assistant",
"synthetic",
"compaction",
"branchSummary",
"custom",
"configChange",
] as const;
export type EntryType = (typeof entryTypes)[number];

export const messageEntryTypes = ["user", "assistant", "synthetic"] as const;
export type MessageEntryType = (typeof messageEntryTypes)[number];

// Part discriminant: aikit wire literals, verbatim — the column value always
// equals json_extract(data, '$.type').
export const partTypes = ["text", "image", "thinking", "toolCall"] as const;
export type PartType = (typeof partTypes)[number];

// aikit ToolStatusEnum, verbatim. "running" is never persisted in v0 (live-UI only)
// but stays in the vocabulary so persisting live progress later needs no
// migration; read-side code treats it like "pending" (unsettled).
export const toolStatuses = ["pending", "running", "completed", "error", "skipped", "aborted"] as const;
export type ToolStatus = (typeof toolStatuses)[number];

// Column names derive from field names via the client's camelToSnake
// transform, so `sandboxEnvId` (not `sandboxEnvID`) maps to `sandbox_env_id`.
export class ProjectRow extends Model.Class<ProjectRow>("ProjectRow")({
Expand All @@ -37,5 +64,48 @@ export class SessionRow extends Model.Class<SessionRow>("SessionRow")({
title: Schema.String,
tag: Schema.String,
metadata: Model.FieldOption(Model.JsonFromString(Metadata)),
// Durable leaf cursor: read anchor (path walks leaf→root) and append anchor
// (new entries attach here). Moved inside every append/branch transaction.
leafEntryId: Model.FieldOption(Schema.String),
// Session-lifetime billed usage, mirroring aikit Usage. Bumped in the
// assistant-append transaction; a cache — assistant envelopes are truth.
cost: Model.GeneratedByDb(Schema.Number),
tokensInput: Model.GeneratedByDb(Schema.Int),
tokensOutput: Model.GeneratedByDb(Schema.Int),
tokensCacheRead: Model.GeneratedByDb(Schema.Int),
tokensCacheWrite: Model.GeneratedByDb(Schema.Int),
...Timestamps,
}) {}

// One timeline/tree node. Identity and `data` are immutable after insert;
// only the annotation columns (`label`, `metadata`) may change. For message
// types (user/assistant/synthetic), `data` holds the aikit message envelope
// (everything except `parts`); parts live in SessionEntryPartRow.
export class SessionEntryRow extends Model.Class<SessionEntryRow>("SessionEntryRow")({
id: Schema.String,
sessionId: Schema.String,
parentId: Model.FieldOption(Schema.String), // tree edge; NULL = root
seq: Model.GeneratedByDb(Schema.Int), // per-session append order, computed in the INSERT
type: Schema.Literals(entryTypes),
data: Schema.String, // JSON: full payload, or message envelope
label: Model.FieldOption(Schema.String), // annotation: bookmark text
metadata: Model.FieldOption(Model.JsonFromString(Metadata)), // annotation: engine scratch
...Timestamps,
}) {}

// One aikit message part. `data` is the verbatim aikit part JSON and is
// authoritative; `status`/`callId`/`toolName` are promoted copies for indexed
// queries, written together with `data`. The only mutation is toolCall
// settlement (data + status), bounded by turn.end.
export class SessionEntryPartRow extends Model.Class<SessionEntryPartRow>("SessionEntryPartRow")({
id: Schema.String,
entryId: Schema.String,
sessionId: Schema.String, // denormalized for session-scoped queries
partIndex: Schema.Int, // dense 0..n-1 within the entry; the loop's addressing unit
type: Schema.Literals(partTypes),
status: Model.FieldOption(Schema.Literals(toolStatuses)), // toolCall only
callId: Model.FieldOption(Schema.String), // toolCall only
toolName: Model.FieldOption(Schema.String), // toolCall only
data: Schema.String, // verbatim aikit part JSON
...Timestamps,
}) {}
Loading