anyclaude-sdk 0.1.0 → 0.3.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 +30 -3
- package/dist/agent.js +101 -12
- package/dist/commands/builtins.js +2 -2
- package/dist/commands/types.d.ts +1 -11
- package/dist/query.d.ts +25 -2
- package/dist/query.js +5 -0
- package/dist/session/adapters/kv.d.ts +25 -0
- package/dist/session/adapters/kv.js +55 -0
- package/dist/session/adapters/memory.d.ts +14 -0
- package/dist/session/adapters/memory.js +37 -0
- package/dist/session/adapters/postgres.d.ts +25 -0
- package/dist/session/adapters/postgres.js +72 -0
- package/dist/session/adapters/redis.d.ts +24 -0
- package/dist/session/adapters/redis.js +52 -0
- package/dist/session/adapters/supabase.d.ts +51 -0
- package/dist/session/adapters/supabase.js +65 -0
- package/dist/session/index.d.ts +6 -1
- package/dist/session/index.js +7 -0
- package/dist/session/store.d.ts +2 -2
- package/dist/session/store.js +2 -0
- package/dist/session/types.d.ts +22 -0
- package/dist/tools/ask_user.d.ts +2 -0
- package/dist/tools/ask_user.js +54 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/types.d.ts +10 -0
- package/package.json +1 -1
package/dist/agent.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { AgentDefinition, CanUseTool, CommandExecutor, FileSystem, HookCallback, HookEvent, PermissionMode, SDKMessage, SDKUserMessage } from './types/index.js';
|
|
1
|
+
import type { AgentDefinition, CanUseTool, CommandExecutor, ContentBlockParam, FileSystem, HookCallback, HookEvent, PermissionMode, SDKMessage, SDKUserMessage } from './types/index.js';
|
|
2
2
|
import type { FileReadLimits, Tool } from './tools/types.js';
|
|
3
3
|
import { type McpServers, type McpProxy } from './mcp/index.js';
|
|
4
4
|
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 {
|
|
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,23 @@ 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;
|
|
37
|
+
/** Tool names executed by the HOST/client, not the server. When the agent calls
|
|
38
|
+
* one, the loop emits a `client_tool_request` + pauses; the client runs it and
|
|
39
|
+
* resumes (continueRun) with `clientToolResults`. (e.g. bash on a browser WebContainer.) */
|
|
40
|
+
clientTools?: string[];
|
|
41
|
+
/** Results for client-tool calls, injected into the transcript before continuing. */
|
|
42
|
+
clientToolResults?: Array<{
|
|
43
|
+
tool_use_id: string;
|
|
44
|
+
content: string | ContentBlockParam[];
|
|
45
|
+
is_error?: boolean;
|
|
46
|
+
}>;
|
|
30
47
|
cwd?: string;
|
|
31
48
|
sessionId?: string;
|
|
32
49
|
abortController?: AbortController;
|
|
@@ -76,7 +93,7 @@ export interface AgentOptions {
|
|
|
76
93
|
/** This agent's name/label for messaging (default 'coordinator'). */
|
|
77
94
|
agentName?: string;
|
|
78
95
|
/** Persist the transcript to this store (keyed by sessionId) for resume. */
|
|
79
|
-
sessionStore?:
|
|
96
|
+
sessionStore?: SessionStoreLike;
|
|
80
97
|
/** Load the stored transcript for sessionId before the first turn. */
|
|
81
98
|
resume?: boolean;
|
|
82
99
|
/** Auto-compact the transcript when it approaches the context limit. */
|
|
@@ -95,6 +112,16 @@ export interface AgentOptions {
|
|
|
95
112
|
};
|
|
96
113
|
/** Prompt callback for 'ask' decisions; if absent, default mode allows / dontAsk denies. */
|
|
97
114
|
onPermissionAsk?: (toolName: string, input: Record<string, unknown>) => Promise<boolean>;
|
|
115
|
+
/** Surfaces `ask_user_question` to the host UI. When set, the tool is registered. */
|
|
116
|
+
onAskUser?: (q: {
|
|
117
|
+
question: string;
|
|
118
|
+
header?: string;
|
|
119
|
+
options: Array<{
|
|
120
|
+
label: string;
|
|
121
|
+
description?: string;
|
|
122
|
+
}>;
|
|
123
|
+
multiSelect?: boolean;
|
|
124
|
+
}) => Promise<string | string[]>;
|
|
98
125
|
/** Load + apply `.claude/settings.json` (project/local cascade) under explicit options. true, or a Settings object. */
|
|
99
126
|
settings?: boolean | Settings;
|
|
100
127
|
/** Load `.claude/skills/*.md` as slash commands + skill registry. true, or a Skill[] array. */
|
package/dist/agent.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// 7. Repeat until no tool calls or max turns reached
|
|
11
11
|
import { ALL_CLAUDE_CODE_TOOLS, toolByName, toolDefs } from './tools/index.js';
|
|
12
12
|
import { task as taskTool } from './tools/task.js';
|
|
13
|
+
import { askUserQuestion } from './tools/ask_user.js';
|
|
13
14
|
import { loadMcpServers } from './mcp/index.js';
|
|
14
15
|
import { runSlashCommand } from './commands/index.js';
|
|
15
16
|
import { BackgroundTaskManager, BACKGROUND_TOOLS } from './background/index.js';
|
|
@@ -201,6 +202,7 @@ export async function* runAgent(options) {
|
|
|
201
202
|
? options.backgroundManager ?? new BackgroundTaskManager()
|
|
202
203
|
: undefined;
|
|
203
204
|
const messageQueue = options.messageQueue;
|
|
205
|
+
const clientTools = new Set(options.clientTools ?? []);
|
|
204
206
|
// Teammates: a shared Mailbox + TaskBoard (reused from the parent when this
|
|
205
207
|
// is a sub-agent) + team tools + coordinator prompt.
|
|
206
208
|
const teamEnabled = options.team === true;
|
|
@@ -214,6 +216,9 @@ export async function* runAgent(options) {
|
|
|
214
216
|
const present = new Set(localTools.map((t) => t.def.function.name));
|
|
215
217
|
localTools = [...localTools, ...BACKGROUND_TOOLS.filter((t) => !present.has(t.def.function.name))];
|
|
216
218
|
}
|
|
219
|
+
if (options.onAskUser && !localTools.some((t) => t.def.function.name === 'ask_user_question')) {
|
|
220
|
+
localTools = [...localTools, askUserQuestion];
|
|
221
|
+
}
|
|
217
222
|
if (teamEnabled) {
|
|
218
223
|
const present = new Set(localTools.map((t) => t.def.function.name));
|
|
219
224
|
const teamSet = subagentsEnabled ? [...TEAM_TOOLS, ...TEAM_DISPATCH_TOOLS] : TEAM_TOOLS;
|
|
@@ -267,6 +272,7 @@ export async function* runAgent(options) {
|
|
|
267
272
|
signal,
|
|
268
273
|
store,
|
|
269
274
|
limits,
|
|
275
|
+
askUser: options.onAskUser,
|
|
270
276
|
background,
|
|
271
277
|
mailbox,
|
|
272
278
|
board,
|
|
@@ -308,6 +314,7 @@ export async function* runAgent(options) {
|
|
|
308
314
|
tools: subTools,
|
|
309
315
|
model: def?.model ?? model,
|
|
310
316
|
systemPrompt: subSystem,
|
|
317
|
+
onAskUser: options.onAskUser,
|
|
311
318
|
maxTurns,
|
|
312
319
|
cwd,
|
|
313
320
|
abortController: childController,
|
|
@@ -403,6 +410,10 @@ export async function* runAgent(options) {
|
|
|
403
410
|
});
|
|
404
411
|
const startedAt = Date.now();
|
|
405
412
|
const sessionUsage = emptyUsage();
|
|
413
|
+
const maxDurationMs = options.maxDurationMs;
|
|
414
|
+
let paused = false;
|
|
415
|
+
// Pending client-executed tool calls for the current turn (emitted on pause).
|
|
416
|
+
let clientRequests = [];
|
|
406
417
|
// Resume: seed the transcript from a prior session before the first turn.
|
|
407
418
|
if (options.resume && options.sessionStore) {
|
|
408
419
|
const prior = await options.sessionStore.load(sessionId);
|
|
@@ -412,12 +423,16 @@ export async function* runAgent(options) {
|
|
|
412
423
|
history.splice(0, history.length, ...prior);
|
|
413
424
|
}
|
|
414
425
|
}
|
|
415
|
-
|
|
426
|
+
// continueRun: prepend a sentinel turn so the loop continues the stored
|
|
427
|
+
// transcript with no new user message (used after a `paused` boundary).
|
|
428
|
+
const promptSrc = options.continueRun ? withContinueSentinel(prompt) : prompt;
|
|
429
|
+
for await (const userMsg of promptSrc) {
|
|
416
430
|
if (signal?.aborted)
|
|
417
431
|
break;
|
|
432
|
+
const isContinue = userMsg.__continue === true;
|
|
418
433
|
const content = userMsg.message.content;
|
|
419
434
|
// Slash-command interception: a string user turn beginning with '/'.
|
|
420
|
-
if (typeof content === 'string' && content.trim().startsWith('/')) {
|
|
435
|
+
if (!isContinue && typeof content === 'string' && content.trim().startsWith('/')) {
|
|
421
436
|
const outcome = await runSlashCommand(content, {
|
|
422
437
|
history,
|
|
423
438
|
tools: defs.map((d) => ({
|
|
@@ -479,7 +494,7 @@ export async function* runAgent(options) {
|
|
|
479
494
|
history.push({ role: 'user', content }); // unknown command → normal prompt
|
|
480
495
|
}
|
|
481
496
|
}
|
|
482
|
-
else {
|
|
497
|
+
else if (!isContinue) {
|
|
483
498
|
history.push({ role: 'user', content });
|
|
484
499
|
const pre = await runHooks('UserPromptSubmit', {
|
|
485
500
|
hook_event_name: 'UserPromptSubmit',
|
|
@@ -489,6 +504,28 @@ export async function* runAgent(options) {
|
|
|
489
504
|
if (extra)
|
|
490
505
|
history.push({ role: 'user', content: extra });
|
|
491
506
|
}
|
|
507
|
+
// Continue after a client-tool pause: inject the host-executed results into
|
|
508
|
+
// the transcript so the (paused) assistant tool_use calls are now resolved.
|
|
509
|
+
if (isContinue && options.clientToolResults?.length) {
|
|
510
|
+
const blocks = [];
|
|
511
|
+
for (const r of options.clientToolResults) {
|
|
512
|
+
history.push({
|
|
513
|
+
role: 'tool',
|
|
514
|
+
tool_call_id: r.tool_use_id,
|
|
515
|
+
content: typeof r.content === 'string' ? r.content : JSON.stringify(r.content),
|
|
516
|
+
});
|
|
517
|
+
blocks.push({ type: 'tool_result', tool_use_id: r.tool_use_id, content: r.content, is_error: r.is_error || undefined });
|
|
518
|
+
}
|
|
519
|
+
yield {
|
|
520
|
+
type: 'user',
|
|
521
|
+
message: { role: 'user', content: blocks },
|
|
522
|
+
parent_tool_use_id: null,
|
|
523
|
+
isSynthetic: true,
|
|
524
|
+
timestamp: new Date().toISOString(),
|
|
525
|
+
uuid: uuid(),
|
|
526
|
+
session_id: sessionId,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
492
529
|
let turns = 0;
|
|
493
530
|
let lastText = '';
|
|
494
531
|
let resultModel = model ?? 'unknown';
|
|
@@ -505,6 +542,11 @@ export async function* runAgent(options) {
|
|
|
505
542
|
hitMaxTurns = true;
|
|
506
543
|
break;
|
|
507
544
|
}
|
|
545
|
+
// Survivor: pause at this turn boundary if we're past the time budget.
|
|
546
|
+
if (maxDurationMs != null && Date.now() - startedAt >= maxDurationMs) {
|
|
547
|
+
paused = true;
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
508
550
|
turns++;
|
|
509
551
|
// Message queue: deliver one interjected user message per turn boundary.
|
|
510
552
|
// (Messages enqueued via options.messageQueue while this loop runs.)
|
|
@@ -648,12 +690,19 @@ export async function* runAgent(options) {
|
|
|
648
690
|
}
|
|
649
691
|
// Execute tool calls (permission gate + hooks around each).
|
|
650
692
|
const toolResultBlocks = [];
|
|
693
|
+
clientRequests = [];
|
|
651
694
|
const turnMedia = [];
|
|
652
695
|
for (const call of calls) {
|
|
653
696
|
if (signal?.aborted)
|
|
654
697
|
break;
|
|
655
698
|
const name = call.function.name;
|
|
656
699
|
let input = safeParse(call.function.arguments);
|
|
700
|
+
// Client-executed tool: don't run it here — record it; we pause after this
|
|
701
|
+
// turn's server tools and let the host execute + resume with the result.
|
|
702
|
+
if (clientTools.has(name)) {
|
|
703
|
+
clientRequests.push({ tool_use_id: call.id, name, input });
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
657
706
|
const tool = byName.get(name);
|
|
658
707
|
let content = '';
|
|
659
708
|
let isError = false;
|
|
@@ -818,15 +867,23 @@ export async function* runAgent(options) {
|
|
|
818
867
|
],
|
|
819
868
|
});
|
|
820
869
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
870
|
+
if (toolResultBlocks.length) {
|
|
871
|
+
yield {
|
|
872
|
+
type: 'user',
|
|
873
|
+
message: { role: 'user', content: toolResultBlocks },
|
|
874
|
+
parent_tool_use_id: null,
|
|
875
|
+
isSynthetic: true,
|
|
876
|
+
timestamp: new Date().toISOString(),
|
|
877
|
+
uuid: uuid(),
|
|
878
|
+
session_id: sessionId,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
// Client-executed tools were requested this turn → pause; the host runs
|
|
882
|
+
// them and resumes (continueRun + clientToolResults).
|
|
883
|
+
if (clientRequests.length) {
|
|
884
|
+
paused = true;
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
830
887
|
}
|
|
831
888
|
await runHooks('Stop', {
|
|
832
889
|
hook_event_name: 'Stop',
|
|
@@ -891,7 +948,39 @@ export async function* runAgent(options) {
|
|
|
891
948
|
/* persistence is best-effort */
|
|
892
949
|
}
|
|
893
950
|
}
|
|
951
|
+
// Survivor / client-tools: paused at a boundary. Transcript persisted above.
|
|
952
|
+
// Emit any client-tool requests for the host to execute, then signal the
|
|
953
|
+
// client to continue in a fresh invocation (resume + continueRun [+ results]).
|
|
954
|
+
if (paused) {
|
|
955
|
+
for (const req of clientRequests) {
|
|
956
|
+
yield {
|
|
957
|
+
type: 'system',
|
|
958
|
+
subtype: 'client_tool_request',
|
|
959
|
+
request: req,
|
|
960
|
+
session_id: sessionId,
|
|
961
|
+
uuid: uuid(),
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
yield {
|
|
965
|
+
type: 'system',
|
|
966
|
+
subtype: 'paused',
|
|
967
|
+
reason: clientRequests.length ? 'client_tool' : 'time_budget',
|
|
968
|
+
session_id: sessionId,
|
|
969
|
+
uuid: uuid(),
|
|
970
|
+
};
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
894
973
|
}
|
|
895
974
|
// The prompt stream is exhausted — the session is ending.
|
|
896
975
|
await runHooks('SessionEnd', { hook_event_name: 'SessionEnd', reason: 'prompt_input_exit' });
|
|
897
976
|
}
|
|
977
|
+
/** Prepend a continue-sentinel turn so the loop continues a resumed transcript. */
|
|
978
|
+
async function* withContinueSentinel(prompt) {
|
|
979
|
+
yield {
|
|
980
|
+
type: 'user',
|
|
981
|
+
message: { role: 'user', content: '' },
|
|
982
|
+
parent_tool_use_id: null,
|
|
983
|
+
__continue: true,
|
|
984
|
+
};
|
|
985
|
+
yield* prompt;
|
|
986
|
+
}
|
|
@@ -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)
|
package/dist/commands/types.d.ts
CHANGED
|
@@ -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 {
|
|
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,19 @@ 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;
|
|
29
|
+
/** Tool names the HOST/client executes (e.g. bash on a browser WebContainer). The agent
|
|
30
|
+
* emits a `client_tool_request` + pauses; the client runs it and resumes with results. */
|
|
31
|
+
clientTools?: string[];
|
|
32
|
+
/** Results for client-tool calls, injected before continuing (with continueRun). */
|
|
33
|
+
clientToolResults?: Array<{
|
|
34
|
+
tool_use_id: string;
|
|
35
|
+
content: string | import('./types/index.js').ContentBlockParam[];
|
|
36
|
+
is_error?: boolean;
|
|
37
|
+
}>;
|
|
25
38
|
cwd?: string;
|
|
26
39
|
sessionId?: string;
|
|
27
40
|
abortController?: AbortController;
|
|
@@ -59,7 +72,7 @@ export interface QueryOptions {
|
|
|
59
72
|
/** This agent's name/label for messaging (default 'coordinator'). */
|
|
60
73
|
agentName?: string;
|
|
61
74
|
/** Persist the transcript to this store (keyed by sessionId) for resume. */
|
|
62
|
-
sessionStore?:
|
|
75
|
+
sessionStore?: SessionStoreLike;
|
|
63
76
|
/** Load the stored transcript for sessionId before the first turn. */
|
|
64
77
|
resume?: boolean;
|
|
65
78
|
/** Auto-compact the transcript when it nears the context limit. */
|
|
@@ -78,6 +91,16 @@ export interface QueryOptions {
|
|
|
78
91
|
};
|
|
79
92
|
/** Prompt callback for 'ask' permission decisions. */
|
|
80
93
|
onPermissionAsk?: (toolName: string, input: Record<string, unknown>) => Promise<boolean>;
|
|
94
|
+
/** Handler for the `ask_user_question` tool. When set, the tool is registered. */
|
|
95
|
+
onAskUser?: (q: {
|
|
96
|
+
question: string;
|
|
97
|
+
header?: string;
|
|
98
|
+
options: Array<{
|
|
99
|
+
label: string;
|
|
100
|
+
description?: string;
|
|
101
|
+
}>;
|
|
102
|
+
multiSelect?: boolean;
|
|
103
|
+
}) => Promise<string | string[]>;
|
|
81
104
|
/** Load `.claude/settings.json` (project/local cascade), or pass a Settings object. */
|
|
82
105
|
settings?: boolean | import('./settings/index.js').Settings;
|
|
83
106
|
/** Load `.claude/skills/*.md` as slash commands + a skill registry, or pass a Skill[]. */
|
package/dist/query.js
CHANGED
|
@@ -20,6 +20,10 @@ 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,
|
|
25
|
+
clientTools: options.clientTools,
|
|
26
|
+
clientToolResults: options.clientToolResults,
|
|
23
27
|
cwd: options.cwd,
|
|
24
28
|
sessionId: options.sessionId,
|
|
25
29
|
abortController,
|
|
@@ -48,6 +52,7 @@ export function query(options) {
|
|
|
48
52
|
memory: options.memory,
|
|
49
53
|
permissionRules: options.permissionRules,
|
|
50
54
|
onPermissionAsk: options.onPermissionAsk,
|
|
55
|
+
onAskUser: options.onAskUser,
|
|
51
56
|
settings: options.settings,
|
|
52
57
|
skills: options.skills,
|
|
53
58
|
});
|
|
@@ -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
|
+
}
|
package/dist/session/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/session/index.js
CHANGED
|
@@ -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';
|
package/dist/session/store.d.ts
CHANGED
|
@@ -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;
|
package/dist/session/store.js
CHANGED
|
@@ -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;
|
package/dist/session/types.d.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const DESCRIPTION = `Ask the user a multiple-choice question and wait for their answer. Use ONLY when you hit a decision that's genuinely the user's to make (choosing between distinct approaches, confirming ambiguous scope) and you can't resolve it from the request or sensible defaults. Provide 2-4 concrete, mutually-exclusive options. Prefer acting on a reasonable default over asking.`;
|
|
2
|
+
export const askUserQuestion = {
|
|
3
|
+
def: {
|
|
4
|
+
type: 'function',
|
|
5
|
+
function: {
|
|
6
|
+
name: 'ask_user_question',
|
|
7
|
+
description: DESCRIPTION,
|
|
8
|
+
parameters: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
question: { type: 'string', description: 'The question to ask the user.' },
|
|
12
|
+
header: { type: 'string', description: 'Very short label/chip for the question (max ~12 chars).' },
|
|
13
|
+
options: {
|
|
14
|
+
type: 'array',
|
|
15
|
+
description: '2-4 options the user can choose from.',
|
|
16
|
+
items: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
label: { type: 'string', description: 'Concise choice text (1-5 words).' },
|
|
20
|
+
description: { type: 'string', description: 'What this option means / its trade-off.' },
|
|
21
|
+
},
|
|
22
|
+
required: ['label'],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
multiSelect: { type: 'boolean', description: 'Allow selecting multiple options.' },
|
|
26
|
+
},
|
|
27
|
+
required: ['question', 'options'],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
async run(input, ctx) {
|
|
32
|
+
if (!ctx.askUser) {
|
|
33
|
+
return {
|
|
34
|
+
content: 'Interactive questions are unavailable in this environment. Choose the most reasonable option yourself, state the assumption, and continue.',
|
|
35
|
+
isError: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const question = String(input.question ?? '').trim();
|
|
39
|
+
const raw = Array.isArray(input.options) ? input.options : [];
|
|
40
|
+
const options = raw.map((o) => typeof o === 'string'
|
|
41
|
+
? { label: o }
|
|
42
|
+
: { label: String(o.label ?? ''), description: o.description ? String(o.description) : undefined });
|
|
43
|
+
if (!question || !options.length)
|
|
44
|
+
return { content: 'Error: `question` and `options` are required.', isError: true };
|
|
45
|
+
const answer = await ctx.askUser({
|
|
46
|
+
question,
|
|
47
|
+
header: input.header ? String(input.header) : undefined,
|
|
48
|
+
options,
|
|
49
|
+
multiSelect: !!input.multiSelect,
|
|
50
|
+
});
|
|
51
|
+
const chosen = Array.isArray(answer) ? answer.join(', ') : String(answer);
|
|
52
|
+
return { content: `User answered "${question}" → ${chosen || '(no selection)'}` };
|
|
53
|
+
},
|
|
54
|
+
};
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export { bash, readFile, writeFile, editFile, deleteFile, listFiles, glob, grep
|
|
|
20
20
|
export { multiEdit, notebookEdit, todoWrite, webFetch, webSearch, toolSearch, config };
|
|
21
21
|
export { walk, globToRegExp, joinPath, DEFAULT_IGNORE } from './walk.js';
|
|
22
22
|
export { defineTool, type DefineToolSpec } from './define.js';
|
|
23
|
+
export { askUserQuestion } from './ask_user.js';
|
|
23
24
|
/** Every built-in Claude Code tool, ready to pass to `query()`. */
|
|
24
25
|
export declare const ALL_CLAUDE_CODE_TOOLS: Tool[];
|
|
25
26
|
/** Extract the OpenAI-shape definitions to send to the LLM. */
|
package/dist/tools/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export { bash, readFile, writeFile, editFile, deleteFile, listFiles, glob, grep
|
|
|
17
17
|
export { multiEdit, notebookEdit, todoWrite, webFetch, webSearch, toolSearch, config };
|
|
18
18
|
export { walk, globToRegExp, joinPath, DEFAULT_IGNORE } from './walk.js';
|
|
19
19
|
export { defineTool } from './define.js';
|
|
20
|
+
export { askUserQuestion } from './ask_user.js';
|
|
20
21
|
/** Every built-in Claude Code tool, ready to pass to `query()`. */
|
|
21
22
|
export const ALL_CLAUDE_CODE_TOOLS = [
|
|
22
23
|
bash,
|
package/dist/tools/types.d.ts
CHANGED
|
@@ -41,6 +41,16 @@ export interface ToolContext {
|
|
|
41
41
|
text: string;
|
|
42
42
|
isError?: boolean;
|
|
43
43
|
}>;
|
|
44
|
+
/** Ask the user a multiple-choice question (wired from query({ onAskUser })). */
|
|
45
|
+
askUser?: (q: {
|
|
46
|
+
question: string;
|
|
47
|
+
header?: string;
|
|
48
|
+
options: Array<{
|
|
49
|
+
label: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
}>;
|
|
52
|
+
multiSelect?: boolean;
|
|
53
|
+
}) => Promise<string | string[]>;
|
|
44
54
|
/** Background task manager, present when background tasks are enabled. */
|
|
45
55
|
background?: import('../background/manager.js').BackgroundTaskManager;
|
|
46
56
|
/** Inter-agent mailbox, present when teammates are enabled. Shared with sub-agents. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anyclaude-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|