anyclaude-sdk 0.1.0 → 0.2.0

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/dist/agent.d.ts CHANGED
@@ -5,7 +5,7 @@ import type { SlashCommand } from './commands/index.js';
5
5
  import { BackgroundTaskManager } from './background/index.js';
6
6
  import { Mailbox, TaskBoard } from './team/index.js';
7
7
  import type { MemoryStore } from './memory/index.js';
8
- import type { SessionStore } from './session/index.js';
8
+ import type { SessionStoreLike } from './session/index.js';
9
9
  import { type Settings } from './settings/index.js';
10
10
  import { type Skill } from './skills/index.js';
11
11
  export type Workspace = FileSystem & CommandExecutor;
@@ -27,6 +27,13 @@ export interface AgentOptions {
27
27
  /** Denylist of tool names, applied after allowedTools. */
28
28
  disallowedTools?: string[];
29
29
  maxTurns?: number;
30
+ /** Wall-clock budget (ms). At a turn boundary past this, the loop pauses: it
31
+ * persists to sessionStore and emits a `paused` system message instead of
32
+ * continuing — for spanning serverless function time limits ("survivor"). */
33
+ maxDurationMs?: number;
34
+ /** Resume + CONTINUE the tool loop on the stored transcript without a new user
35
+ * message (pairs with `resume`). Used to continue after a `paused` boundary. */
36
+ continueRun?: boolean;
30
37
  cwd?: string;
31
38
  sessionId?: string;
32
39
  abortController?: AbortController;
@@ -76,7 +83,7 @@ export interface AgentOptions {
76
83
  /** This agent's name/label for messaging (default 'coordinator'). */
77
84
  agentName?: string;
78
85
  /** Persist the transcript to this store (keyed by sessionId) for resume. */
79
- sessionStore?: SessionStore;
86
+ sessionStore?: SessionStoreLike;
80
87
  /** Load the stored transcript for sessionId before the first turn. */
81
88
  resume?: boolean;
82
89
  /** Auto-compact the transcript when it approaches the context limit. */
package/dist/agent.js CHANGED
@@ -403,6 +403,8 @@ export async function* runAgent(options) {
403
403
  });
404
404
  const startedAt = Date.now();
405
405
  const sessionUsage = emptyUsage();
406
+ const maxDurationMs = options.maxDurationMs;
407
+ let paused = false;
406
408
  // Resume: seed the transcript from a prior session before the first turn.
407
409
  if (options.resume && options.sessionStore) {
408
410
  const prior = await options.sessionStore.load(sessionId);
@@ -412,12 +414,16 @@ export async function* runAgent(options) {
412
414
  history.splice(0, history.length, ...prior);
413
415
  }
414
416
  }
415
- for await (const userMsg of prompt) {
417
+ // continueRun: prepend a sentinel turn so the loop continues the stored
418
+ // transcript with no new user message (used after a `paused` boundary).
419
+ const promptSrc = options.continueRun ? withContinueSentinel(prompt) : prompt;
420
+ for await (const userMsg of promptSrc) {
416
421
  if (signal?.aborted)
417
422
  break;
423
+ const isContinue = userMsg.__continue === true;
418
424
  const content = userMsg.message.content;
419
425
  // Slash-command interception: a string user turn beginning with '/'.
420
- if (typeof content === 'string' && content.trim().startsWith('/')) {
426
+ if (!isContinue && typeof content === 'string' && content.trim().startsWith('/')) {
421
427
  const outcome = await runSlashCommand(content, {
422
428
  history,
423
429
  tools: defs.map((d) => ({
@@ -479,7 +485,7 @@ export async function* runAgent(options) {
479
485
  history.push({ role: 'user', content }); // unknown command → normal prompt
480
486
  }
481
487
  }
482
- else {
488
+ else if (!isContinue) {
483
489
  history.push({ role: 'user', content });
484
490
  const pre = await runHooks('UserPromptSubmit', {
485
491
  hook_event_name: 'UserPromptSubmit',
@@ -505,6 +511,11 @@ export async function* runAgent(options) {
505
511
  hitMaxTurns = true;
506
512
  break;
507
513
  }
514
+ // Survivor: pause at this turn boundary if we're past the time budget.
515
+ if (maxDurationMs != null && Date.now() - startedAt >= maxDurationMs) {
516
+ paused = true;
517
+ break;
518
+ }
508
519
  turns++;
509
520
  // Message queue: deliver one interjected user message per turn boundary.
510
521
  // (Messages enqueued via options.messageQueue while this loop runs.)
@@ -891,7 +902,29 @@ export async function* runAgent(options) {
891
902
  /* persistence is best-effort */
892
903
  }
893
904
  }
905
+ // Survivor: hit the time budget. The transcript is persisted (above); signal
906
+ // the client to continue the run in a fresh invocation (resume + continueRun).
907
+ if (paused) {
908
+ yield {
909
+ type: 'system',
910
+ subtype: 'paused',
911
+ reason: 'time_budget',
912
+ session_id: sessionId,
913
+ uuid: uuid(),
914
+ };
915
+ break;
916
+ }
894
917
  }
895
918
  // The prompt stream is exhausted — the session is ending.
896
919
  await runHooks('SessionEnd', { hook_event_name: 'SessionEnd', reason: 'prompt_input_exit' });
897
920
  }
921
+ /** Prepend a continue-sentinel turn so the loop continues a resumed transcript. */
922
+ async function* withContinueSentinel(prompt) {
923
+ yield {
924
+ type: 'user',
925
+ message: { role: 'user', content: '' },
926
+ parent_tool_use_id: null,
927
+ __continue: true,
928
+ };
929
+ yield* prompt;
930
+ }
@@ -123,7 +123,7 @@ const sessions = {
123
123
  name: 'sessions',
124
124
  description: 'List saved sessions',
125
125
  async run(_args, ctx) {
126
- if (!ctx.sessionStore)
126
+ if (!ctx.sessionStore?.list)
127
127
  return { systemText: 'No session store configured.' };
128
128
  const list = await ctx.sessionStore.list();
129
129
  if (!list.length)
@@ -153,7 +153,7 @@ const rename = {
153
153
  description: 'Rename the current session',
154
154
  argumentHint: '<title>',
155
155
  async run(args, ctx) {
156
- if (!ctx.sessionStore || !ctx.sessionId)
156
+ if (!ctx.sessionStore?.rename || !ctx.sessionId)
157
157
  return { systemText: 'No active session to rename.' };
158
158
  const title = args.trim();
159
159
  if (!title)
@@ -36,17 +36,7 @@ export interface SlashCommandContext {
36
36
  /** Current session id. */
37
37
  sessionId?: string;
38
38
  /** Session store, for /sessions, /resume, /rename. */
39
- sessionStore?: {
40
- list(): Promise<Array<{
41
- sessionId: string;
42
- title?: string;
43
- updatedAt: number;
44
- messageCount: number;
45
- }>>;
46
- load(id: string): Promise<ChatMsg[] | null>;
47
- rename(id: string, title: string): Promise<void>;
48
- remove(id: string): Promise<void>;
49
- };
39
+ sessionStore?: import('../session/types.js').SessionStoreLike;
50
40
  /** Paths the model has read this session, for /files. */
51
41
  readFiles?: Set<string>;
52
42
  /** Configured sub-agents, for /agents. */
package/dist/query.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { AgentDefinition, CanUseTool, HookCallback, HookEvent, LLMClient, P
2
2
  import type { FileReadLimits, Tool } from './tools/types.js';
3
3
  import type { McpServers, McpProxy } from './mcp/index.js';
4
4
  import type { SlashCommand } from './commands/index.js';
5
- import type { SessionStore } from './session/index.js';
5
+ import type { SessionStoreLike } from './session/index.js';
6
6
  import type { MemoryStore } from './memory/index.js';
7
7
  import { type Workspace } from './agent.js';
8
8
  export interface QueryOptions {
@@ -22,6 +22,10 @@ export interface QueryOptions {
22
22
  allowedTools?: string[];
23
23
  disallowedTools?: string[];
24
24
  maxTurns?: number;
25
+ /** Wall-clock budget (ms): pause at a turn boundary past this + emit `paused` (survivor). */
26
+ maxDurationMs?: number;
27
+ /** Resume + continue the tool loop with no new user message (after a `paused` boundary). */
28
+ continueRun?: boolean;
25
29
  cwd?: string;
26
30
  sessionId?: string;
27
31
  abortController?: AbortController;
@@ -59,7 +63,7 @@ export interface QueryOptions {
59
63
  /** This agent's name/label for messaging (default 'coordinator'). */
60
64
  agentName?: string;
61
65
  /** Persist the transcript to this store (keyed by sessionId) for resume. */
62
- sessionStore?: SessionStore;
66
+ sessionStore?: SessionStoreLike;
63
67
  /** Load the stored transcript for sessionId before the first turn. */
64
68
  resume?: boolean;
65
69
  /** Auto-compact the transcript when it nears the context limit. */
package/dist/query.js CHANGED
@@ -20,6 +20,8 @@ export function query(options) {
20
20
  allowedTools: options.allowedTools,
21
21
  disallowedTools: options.disallowedTools,
22
22
  maxTurns: options.maxTurns,
23
+ maxDurationMs: options.maxDurationMs,
24
+ continueRun: options.continueRun,
23
25
  cwd: options.cwd,
24
26
  sessionId: options.sessionId,
25
27
  abortController,
@@ -0,0 +1,25 @@
1
+ import type { ChatMsg } from '../../types/index.js';
2
+ import type { SessionMeta, SessionStoreLike, StoredSession } from '../types.js';
3
+ /** Structural view of a KV client (Vercel KV / Upstash Redis / Cloudflare KV). */
4
+ export interface KVClientLike {
5
+ get(key: string): Promise<unknown>;
6
+ set(key: string, value: string): Promise<unknown>;
7
+ del?(key: string): Promise<unknown>;
8
+ /** Glob-style key listing (e.g. Upstash/ioredis `keys('bcs:session:*')`). */
9
+ keys?(pattern: string): Promise<string[]>;
10
+ }
11
+ export declare class KVSessionStore implements SessionStoreLike {
12
+ private readonly kv;
13
+ private readonly prefix;
14
+ constructor(kv: KVClientLike, prefix?: string);
15
+ private key;
16
+ get(sessionId: string): Promise<StoredSession | null>;
17
+ load(sessionId: string): Promise<ChatMsg[] | null>;
18
+ save(sessionId: string, transcript: ChatMsg[], meta?: {
19
+ title?: string;
20
+ model?: string;
21
+ }): Promise<void>;
22
+ list(): Promise<SessionMeta[]>;
23
+ rename(sessionId: string, title: string): Promise<void>;
24
+ remove(sessionId: string): Promise<void>;
25
+ }
@@ -0,0 +1,55 @@
1
+ function parse(raw) {
2
+ if (raw == null)
3
+ return null;
4
+ const obj = typeof raw === 'string' ? JSON.parse(raw) : raw;
5
+ return obj && Array.isArray(obj.transcript) ? obj : null;
6
+ }
7
+ export class KVSessionStore {
8
+ constructor(kv, prefix = 'bcs:session:') {
9
+ this.kv = kv;
10
+ this.prefix = prefix;
11
+ }
12
+ key(id) {
13
+ return this.prefix + id;
14
+ }
15
+ async get(sessionId) {
16
+ return parse(await this.kv.get(this.key(sessionId)));
17
+ }
18
+ async load(sessionId) {
19
+ const s = await this.get(sessionId);
20
+ return s ? s.transcript : null;
21
+ }
22
+ async save(sessionId, transcript, meta = {}) {
23
+ const now = Date.now();
24
+ const existing = await this.get(sessionId);
25
+ const row = {
26
+ sessionId,
27
+ title: meta.title ?? existing?.title,
28
+ model: meta.model ?? existing?.model,
29
+ createdAt: existing?.createdAt ?? now,
30
+ updatedAt: now,
31
+ messageCount: transcript.length,
32
+ transcript,
33
+ };
34
+ await this.kv.set(this.key(sessionId), JSON.stringify(row));
35
+ }
36
+ async list() {
37
+ if (!this.kv.keys)
38
+ return [];
39
+ const keys = await this.kv.keys(this.prefix + '*');
40
+ const rows = await Promise.all(keys.map((k) => this.kv.get(k).then(parse)));
41
+ return rows
42
+ .filter((s) => !!s)
43
+ .map(({ transcript: _t, ...meta }) => meta)
44
+ .sort((a, b) => b.updatedAt - a.updatedAt);
45
+ }
46
+ async rename(sessionId, title) {
47
+ const s = await this.get(sessionId);
48
+ if (!s)
49
+ return;
50
+ await this.kv.set(this.key(sessionId), JSON.stringify({ ...s, title, updatedAt: Date.now() }));
51
+ }
52
+ async remove(sessionId) {
53
+ await this.kv.del?.(this.key(sessionId));
54
+ }
55
+ }
@@ -0,0 +1,14 @@
1
+ import type { ChatMsg } from '../../types/index.js';
2
+ import type { SessionMeta, SessionStoreLike, StoredSession } from '../types.js';
3
+ export declare class MemorySessionStore implements SessionStoreLike {
4
+ private readonly data;
5
+ load(sessionId: string): Promise<ChatMsg[] | null>;
6
+ save(sessionId: string, transcript: ChatMsg[], meta?: {
7
+ title?: string;
8
+ model?: string;
9
+ }): Promise<void>;
10
+ get(sessionId: string): Promise<StoredSession | null>;
11
+ list(): Promise<SessionMeta[]>;
12
+ rename(sessionId: string, title: string): Promise<void>;
13
+ remove(sessionId: string): Promise<void>;
14
+ }
@@ -0,0 +1,37 @@
1
+ export class MemorySessionStore {
2
+ constructor() {
3
+ this.data = new Map();
4
+ }
5
+ async load(sessionId) {
6
+ return this.data.get(sessionId)?.transcript ?? null;
7
+ }
8
+ async save(sessionId, transcript, meta) {
9
+ const now = Date.now();
10
+ const prev = this.data.get(sessionId);
11
+ this.data.set(sessionId, {
12
+ sessionId,
13
+ title: meta?.title ?? prev?.title,
14
+ model: meta?.model ?? prev?.model,
15
+ createdAt: prev?.createdAt ?? now,
16
+ updatedAt: now,
17
+ messageCount: transcript.length,
18
+ transcript: transcript.slice(),
19
+ });
20
+ }
21
+ async get(sessionId) {
22
+ return this.data.get(sessionId) ?? null;
23
+ }
24
+ async list() {
25
+ return [...this.data.values()]
26
+ .map(({ transcript: _t, ...meta }) => meta)
27
+ .sort((a, b) => b.updatedAt - a.updatedAt);
28
+ }
29
+ async rename(sessionId, title) {
30
+ const s = this.data.get(sessionId);
31
+ if (s)
32
+ s.title = title;
33
+ }
34
+ async remove(sessionId) {
35
+ this.data.delete(sessionId);
36
+ }
37
+ }
@@ -0,0 +1,25 @@
1
+ import type { ChatMsg } from '../../types/index.js';
2
+ import type { SessionMeta, SessionStoreLike, StoredSession } from '../types.js';
3
+ /** Structural Postgres query runner (node-postgres Pool/Client compatible). */
4
+ export interface PgRunnerLike {
5
+ query(text: string, params?: unknown[]): Promise<{
6
+ rows: Record<string, unknown>[];
7
+ }>;
8
+ }
9
+ export declare const POSTGRES_SCHEMA: string;
10
+ export declare class PostgresSessionStore implements SessionStoreLike {
11
+ private readonly pg;
12
+ private readonly table;
13
+ constructor(pg: PgRunnerLike, table?: string);
14
+ /** Create the table if it doesn't exist (optional convenience). */
15
+ migrate(): Promise<void>;
16
+ load(sessionId: string): Promise<ChatMsg[] | null>;
17
+ get(sessionId: string): Promise<StoredSession | null>;
18
+ save(sessionId: string, transcript: ChatMsg[], meta?: {
19
+ title?: string;
20
+ model?: string;
21
+ }): Promise<void>;
22
+ list(): Promise<SessionMeta[]>;
23
+ rename(sessionId: string, title: string): Promise<void>;
24
+ remove(sessionId: string): Promise<void>;
25
+ }
@@ -0,0 +1,72 @@
1
+ export const POSTGRES_SCHEMA = `
2
+ CREATE TABLE IF NOT EXISTS sessions (
3
+ id text PRIMARY KEY,
4
+ title text,
5
+ model text,
6
+ created_at bigint NOT NULL,
7
+ updated_at bigint NOT NULL,
8
+ message_count int NOT NULL DEFAULT 0,
9
+ transcript jsonb NOT NULL DEFAULT '[]'::jsonb
10
+ );
11
+ CREATE INDEX IF NOT EXISTS sessions_updated_at_idx ON sessions (updated_at DESC);
12
+ `.trim();
13
+ function toMeta(r) {
14
+ return {
15
+ sessionId: String(r.id),
16
+ title: r.title ?? undefined,
17
+ model: r.model ?? undefined,
18
+ createdAt: Number(r.created_at),
19
+ updatedAt: Number(r.updated_at),
20
+ messageCount: Number(r.message_count),
21
+ };
22
+ }
23
+ export class PostgresSessionStore {
24
+ constructor(pg, table = 'sessions') {
25
+ this.pg = pg;
26
+ this.table = table;
27
+ }
28
+ /** Create the table if it doesn't exist (optional convenience). */
29
+ async migrate() {
30
+ await this.pg.query(POSTGRES_SCHEMA);
31
+ }
32
+ async load(sessionId) {
33
+ const { rows } = await this.pg.query(`SELECT transcript FROM ${this.table} WHERE id = $1`, [sessionId]);
34
+ if (!rows.length)
35
+ return null;
36
+ const t = rows[0].transcript;
37
+ return (typeof t === 'string' ? JSON.parse(t) : t);
38
+ }
39
+ async get(sessionId) {
40
+ const { rows } = await this.pg.query(`SELECT * FROM ${this.table} WHERE id = $1`, [sessionId]);
41
+ if (!rows.length)
42
+ return null;
43
+ const r = rows[0];
44
+ const t = r.transcript;
45
+ return { ...toMeta(r), transcript: (typeof t === 'string' ? JSON.parse(t) : t) };
46
+ }
47
+ async save(sessionId, transcript, meta = {}) {
48
+ const now = Date.now();
49
+ await this.pg.query(`INSERT INTO ${this.table} (id, title, model, created_at, updated_at, message_count, transcript)
50
+ VALUES ($1, $2, $3, $4, $4, $5, $6::jsonb)
51
+ ON CONFLICT (id) DO UPDATE SET
52
+ title = COALESCE(EXCLUDED.title, ${this.table}.title),
53
+ model = COALESCE(EXCLUDED.model, ${this.table}.model),
54
+ updated_at = EXCLUDED.updated_at,
55
+ message_count = EXCLUDED.message_count,
56
+ transcript = EXCLUDED.transcript`, [sessionId, meta.title ?? null, meta.model ?? null, now, transcript.length, JSON.stringify(transcript)]);
57
+ }
58
+ async list() {
59
+ const { rows } = await this.pg.query(`SELECT id, title, model, created_at, updated_at, message_count FROM ${this.table} ORDER BY updated_at DESC`);
60
+ return rows.map(toMeta);
61
+ }
62
+ async rename(sessionId, title) {
63
+ await this.pg.query(`UPDATE ${this.table} SET title = $2, updated_at = $3 WHERE id = $1`, [
64
+ sessionId,
65
+ title,
66
+ Date.now(),
67
+ ]);
68
+ }
69
+ async remove(sessionId) {
70
+ await this.pg.query(`DELETE FROM ${this.table} WHERE id = $1`, [sessionId]);
71
+ }
72
+ }
@@ -0,0 +1,24 @@
1
+ import type { ChatMsg } from '../../types/index.js';
2
+ import type { SessionMeta, SessionStoreLike, StoredSession } from '../types.js';
3
+ /** Structural view of a Redis client (ioredis / node-redis). */
4
+ export interface RedisClientLike {
5
+ get(key: string): Promise<string | null>;
6
+ set(key: string, value: string): Promise<unknown>;
7
+ del(key: string): Promise<unknown>;
8
+ keys(pattern: string): Promise<string[]>;
9
+ }
10
+ export declare class RedisSessionStore implements SessionStoreLike {
11
+ private readonly redis;
12
+ private readonly prefix;
13
+ constructor(redis: RedisClientLike, prefix?: string);
14
+ private key;
15
+ get(sessionId: string): Promise<StoredSession | null>;
16
+ load(sessionId: string): Promise<ChatMsg[] | null>;
17
+ save(sessionId: string, transcript: ChatMsg[], meta?: {
18
+ title?: string;
19
+ model?: string;
20
+ }): Promise<void>;
21
+ list(): Promise<SessionMeta[]>;
22
+ rename(sessionId: string, title: string): Promise<void>;
23
+ remove(sessionId: string): Promise<void>;
24
+ }
@@ -0,0 +1,52 @@
1
+ export class RedisSessionStore {
2
+ constructor(redis, prefix = 'bcs:session:') {
3
+ this.redis = redis;
4
+ this.prefix = prefix;
5
+ }
6
+ key(id) {
7
+ return this.prefix + id;
8
+ }
9
+ async get(sessionId) {
10
+ const raw = await this.redis.get(this.key(sessionId));
11
+ if (!raw)
12
+ return null;
13
+ const obj = JSON.parse(raw);
14
+ return Array.isArray(obj.transcript) ? obj : null;
15
+ }
16
+ async load(sessionId) {
17
+ const s = await this.get(sessionId);
18
+ return s ? s.transcript : null;
19
+ }
20
+ async save(sessionId, transcript, meta = {}) {
21
+ const now = Date.now();
22
+ const existing = await this.get(sessionId);
23
+ const row = {
24
+ sessionId,
25
+ title: meta.title ?? existing?.title,
26
+ model: meta.model ?? existing?.model,
27
+ createdAt: existing?.createdAt ?? now,
28
+ updatedAt: now,
29
+ messageCount: transcript.length,
30
+ transcript,
31
+ };
32
+ await this.redis.set(this.key(sessionId), JSON.stringify(row));
33
+ }
34
+ async list() {
35
+ const keys = await this.redis.keys(this.prefix + '*');
36
+ const raws = await Promise.all(keys.map((k) => this.redis.get(k)));
37
+ return raws
38
+ .filter((r) => !!r)
39
+ .map((r) => JSON.parse(r))
40
+ .map(({ transcript: _t, ...meta }) => meta)
41
+ .sort((a, b) => b.updatedAt - a.updatedAt);
42
+ }
43
+ async rename(sessionId, title) {
44
+ const s = await this.get(sessionId);
45
+ if (!s)
46
+ return;
47
+ await this.redis.set(this.key(sessionId), JSON.stringify({ ...s, title, updatedAt: Date.now() }));
48
+ }
49
+ async remove(sessionId) {
50
+ await this.redis.del(this.key(sessionId));
51
+ }
52
+ }
@@ -0,0 +1,51 @@
1
+ import type { ChatMsg } from '../../types/index.js';
2
+ import type { SessionMeta, SessionStoreLike, StoredSession } from '../types.js';
3
+ export declare const SUPABASE_SCHEMA: string;
4
+ interface SessionRow {
5
+ id: string;
6
+ title: string | null;
7
+ model: string | null;
8
+ created_at: number;
9
+ updated_at: number;
10
+ message_count: number;
11
+ transcript: ChatMsg[];
12
+ }
13
+ interface SupaResp<T> {
14
+ data: T | null;
15
+ error: {
16
+ message: string;
17
+ } | null;
18
+ }
19
+ interface SupaQuery<T> extends PromiseLike<SupaResp<T[]>> {
20
+ select(columns?: string): SupaQuery<T>;
21
+ eq(column: string, value: unknown): SupaQuery<T>;
22
+ order(column: string, options?: {
23
+ ascending?: boolean;
24
+ }): SupaQuery<T>;
25
+ single(): PromiseLike<SupaResp<T>>;
26
+ }
27
+ interface SupaTable<T> {
28
+ select(columns?: string): SupaQuery<T>;
29
+ upsert(values: Partial<T> | Partial<T>[]): PromiseLike<SupaResp<T[]>>;
30
+ update(values: Partial<T>): SupaQuery<T>;
31
+ delete(): SupaQuery<T>;
32
+ }
33
+ /** Structural view of a @supabase/supabase-js client. */
34
+ export interface SupabaseClientLike {
35
+ from(relation: string): SupaTable<SessionRow>;
36
+ }
37
+ export declare class SupabaseSessionStore implements SessionStoreLike {
38
+ private readonly supabase;
39
+ private readonly table;
40
+ constructor(supabase: SupabaseClientLike, table?: string);
41
+ get(sessionId: string): Promise<StoredSession | null>;
42
+ load(sessionId: string): Promise<ChatMsg[] | null>;
43
+ save(sessionId: string, transcript: ChatMsg[], meta?: {
44
+ title?: string;
45
+ model?: string;
46
+ }): Promise<void>;
47
+ list(): Promise<SessionMeta[]>;
48
+ rename(sessionId: string, title: string): Promise<void>;
49
+ remove(sessionId: string): Promise<void>;
50
+ }
51
+ export {};
@@ -0,0 +1,65 @@
1
+ export const SUPABASE_SCHEMA = `
2
+ create table if not exists sessions (
3
+ id text primary key,
4
+ title text,
5
+ model text,
6
+ created_at bigint not null,
7
+ updated_at bigint not null,
8
+ message_count int not null default 0,
9
+ transcript jsonb not null default '[]'::jsonb
10
+ );
11
+ create index if not exists sessions_updated_at_idx on sessions (updated_at desc);
12
+ `.trim();
13
+ const toMeta = (r) => ({
14
+ sessionId: r.id,
15
+ title: r.title ?? undefined,
16
+ model: r.model ?? undefined,
17
+ createdAt: Number(r.created_at),
18
+ updatedAt: Number(r.updated_at),
19
+ messageCount: Number(r.message_count),
20
+ });
21
+ export class SupabaseSessionStore {
22
+ constructor(supabase, table = 'sessions') {
23
+ this.supabase = supabase;
24
+ this.table = table;
25
+ }
26
+ async get(sessionId) {
27
+ const { data } = await this.supabase.from(this.table).select('*').eq('id', sessionId).single();
28
+ if (!data)
29
+ return null;
30
+ return { ...toMeta(data), transcript: Array.isArray(data.transcript) ? data.transcript : [] };
31
+ }
32
+ async load(sessionId) {
33
+ const s = await this.get(sessionId);
34
+ return s ? s.transcript : null;
35
+ }
36
+ async save(sessionId, transcript, meta = {}) {
37
+ const now = Date.now();
38
+ const existing = await this.get(sessionId);
39
+ const row = {
40
+ id: sessionId,
41
+ title: meta.title ?? existing?.title ?? null,
42
+ model: meta.model ?? existing?.model ?? null,
43
+ created_at: existing?.createdAt ?? now,
44
+ updated_at: now,
45
+ message_count: transcript.length,
46
+ transcript,
47
+ };
48
+ const { error } = await this.supabase.from(this.table).upsert(row);
49
+ if (error)
50
+ throw new Error('SupabaseSessionStore.save: ' + error.message);
51
+ }
52
+ async list() {
53
+ const { data } = await this.supabase
54
+ .from(this.table)
55
+ .select('id,title,model,created_at,updated_at,message_count')
56
+ .order('updated_at', { ascending: false });
57
+ return (data ?? []).map(toMeta);
58
+ }
59
+ async rename(sessionId, title) {
60
+ await this.supabase.from(this.table).update({ title, updated_at: Date.now() }).eq('id', sessionId);
61
+ }
62
+ async remove(sessionId) {
63
+ await this.supabase.from(this.table).delete().eq('id', sessionId);
64
+ }
65
+ }
@@ -1,2 +1,7 @@
1
1
  export { SessionStore } from './store.js';
2
- export type { SessionMeta, StoredSession, SessionStoreOptions } from './types.js';
2
+ export type { SessionMeta, StoredSession, SessionStoreOptions, SessionStoreLike } from './types.js';
3
+ export { MemorySessionStore } from './adapters/memory.js';
4
+ export { KVSessionStore, type KVClientLike } from './adapters/kv.js';
5
+ export { RedisSessionStore, type RedisClientLike } from './adapters/redis.js';
6
+ export { PostgresSessionStore, POSTGRES_SCHEMA, type PgRunnerLike } from './adapters/postgres.js';
7
+ export { SupabaseSessionStore, SUPABASE_SCHEMA, type SupabaseClientLike } from './adapters/supabase.js';
@@ -4,3 +4,10 @@
4
4
  // IndexedDB (via Dexie), enabling listSessions / resume / fork / rename across
5
5
  // reloads.
6
6
  export { SessionStore } from './store.js';
7
+ // Pluggable backends for the survivor / serverless persistence (structural
8
+ // clients — the DB packages stay optional).
9
+ export { MemorySessionStore } from './adapters/memory.js';
10
+ export { KVSessionStore } from './adapters/kv.js';
11
+ export { RedisSessionStore } from './adapters/redis.js';
12
+ export { PostgresSessionStore, POSTGRES_SCHEMA } from './adapters/postgres.js';
13
+ export { SupabaseSessionStore, SUPABASE_SCHEMA } from './adapters/supabase.js';
@@ -1,6 +1,6 @@
1
1
  import type { ChatMsg } from '../types/index.js';
2
- import type { SessionMeta, SessionStoreOptions, StoredSession } from './types.js';
3
- export declare class SessionStore {
2
+ import type { SessionMeta, SessionStoreOptions, StoredSession, SessionStoreLike } from './types.js';
3
+ export declare class SessionStore implements SessionStoreLike {
4
4
  private readonly dbName;
5
5
  private db;
6
6
  private opening;
@@ -4,6 +4,8 @@
4
4
  //
5
5
  // `dexie` is an OPTIONAL peer dependency — imported dynamically so this module
6
6
  // loads even when Dexie isn't installed (the error surfaces only on first use).
7
+ // The built-in IndexedDB (Dexie) session store. `implements SessionStoreLike`
8
+ // guarantees it stays compatible with the pluggable interface the agent expects.
7
9
  export class SessionStore {
8
10
  constructor(options = {}) {
9
11
  this.db = null;
@@ -20,3 +20,25 @@ export interface SessionStoreOptions {
20
20
  /** IndexedDB database name. Default: 'bcs-sessions'. */
21
21
  dbName?: string;
22
22
  }
23
+ /**
24
+ * Minimal pluggable session store — implement this to back persistence with any
25
+ * database (Supabase, Neon/Postgres, Vercel KV, Upstash Redis, files, …). The
26
+ * agent loop only needs `load` + `save`; the rest are for UIs/management.
27
+ */
28
+ export interface SessionStoreLike {
29
+ /** Return the stored transcript for a session, or null if none. */
30
+ load(sessionId: string): Promise<ChatMsg[] | null>;
31
+ /** Persist the transcript (called after each turn + on a paused boundary). */
32
+ save(sessionId: string, transcript: ChatMsg[], meta?: {
33
+ title?: string;
34
+ model?: string;
35
+ }): Promise<void>;
36
+ /** Optional: list sessions (metadata only). */
37
+ list?(): Promise<SessionMeta[]>;
38
+ /** Optional: full stored session. */
39
+ get?(sessionId: string): Promise<StoredSession | null>;
40
+ /** Optional: rename a session. */
41
+ rename?(sessionId: string, title: string): Promise<void>;
42
+ /** Optional: delete a session. */
43
+ remove?(sessionId: string): Promise<void>;
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anyclaude-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Standalone, browser-compatible SDK providing Claude Code agent capabilities (tools, tool loop, multi-turn, MCP, sub-agents, sessions) against any OpenAI/Anthropic-compatible LLM endpoint. Runs in the browser (WebContainer), Node, and Bun — no backend required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",