@capekai/core 1.0.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/README.md +12 -0
- package/package.json +105 -0
- package/src/adapters/ai-sdk.ts +84 -0
- package/src/compaction/contracts.ts +82 -0
- package/src/compaction/executor.ts +161 -0
- package/src/compaction/policy.ts +318 -0
- package/src/compaction/recovery.ts +139 -0
- package/src/compaction/task.ts +540 -0
- package/src/configuration/contracts.ts +58 -0
- package/src/configuration/defaults.ts +27 -0
- package/src/configuration/runtime.ts +42 -0
- package/src/configuration/single-model.ts +75 -0
- package/src/context/assembler.ts +112 -0
- package/src/context/index.ts +2 -0
- package/src/context/sources.ts +119 -0
- package/src/context/workspace.ts +63 -0
- package/src/core/agent.ts +401 -0
- package/src/core/build-tools.ts +139 -0
- package/src/core/chat-handler.ts +858 -0
- package/src/core/error-handling.ts +18 -0
- package/src/core/fork.ts +103 -0
- package/src/core/interrupt.ts +192 -0
- package/src/core/message-utils.ts +261 -0
- package/src/core/model-utils.ts +149 -0
- package/src/core/part-utils.ts +88 -0
- package/src/core/provider-utils.ts +67 -0
- package/src/core/revert.ts +46 -0
- package/src/core/step-handlers.ts +157 -0
- package/src/core/stream/finalization.ts +65 -0
- package/src/core/stream/stream-config.ts +82 -0
- package/src/core/stream-handlers.ts +242 -0
- package/src/core/structured-output.ts +68 -0
- package/src/core/tool-builders/agent-tools.ts +71 -0
- package/src/core/tool-builders/external-tools.ts +179 -0
- package/src/core/tool-builders/types.ts +16 -0
- package/src/core/tool-builders/workspace-tools.ts +293 -0
- package/src/core/tool-capabilities.ts +65 -0
- package/src/goals/evaluator.ts +171 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/loop.ts +167 -0
- package/src/goals/service.ts +39 -0
- package/src/index.ts +10 -0
- package/src/internal/ask-authority.ts +29 -0
- package/src/internal/composition.ts +44 -0
- package/src/internal/configuration.ts +22 -0
- package/src/internal/execution.ts +108 -0
- package/src/internal/hosts.ts +64 -0
- package/src/internal/plugins.ts +71 -0
- package/src/internal/providers.ts +32 -0
- package/src/internal/sandbox.ts +19 -0
- package/src/internal/tools.ts +48 -0
- package/src/internal/workspace.ts +25 -0
- package/src/kernel/diagnostics.ts +249 -0
- package/src/kernel/errors.ts +120 -0
- package/src/kernel/events.ts +82 -0
- package/src/kernel/index.ts +72 -0
- package/src/kernel/kernel.ts +62 -0
- package/src/kernel/lifecycle.ts +72 -0
- package/src/kernel/plugin.ts +218 -0
- package/src/kernel/registry.ts +493 -0
- package/src/kernel/scope.ts +776 -0
- package/src/kernel/service-key.ts +19 -0
- package/src/kernel/types.ts +317 -0
- package/src/memory/index.ts +2 -0
- package/src/memory/memory-tool.ts +75 -0
- package/src/memory/registry.ts +172 -0
- package/src/permission/ask-user-api.ts +70 -0
- package/src/permission/contracts.ts +135 -0
- package/src/permission/permission-request-manager.ts +58 -0
- package/src/permission/policy.ts +277 -0
- package/src/permission/runtime.ts +612 -0
- package/src/plugins/compaction-policy.ts +46 -0
- package/src/plugins/compose.ts +171 -0
- package/src/plugins/context-sections.ts +246 -0
- package/src/plugins/default-agent-driver.ts +14 -0
- package/src/plugins/facade-plugins.ts +129 -0
- package/src/plugins/goal-domain.ts +82 -0
- package/src/plugins/legacy-system-message.ts +152 -0
- package/src/plugins/loaded-tools.ts +23 -0
- package/src/plugins/memory-domain.ts +264 -0
- package/src/plugins/orchestrator-session.ts +29 -0
- package/src/plugins/permission-policy.ts +49 -0
- package/src/plugins/retry-policy.ts +28 -0
- package/src/plugins/scheduler-domain.ts +192 -0
- package/src/plugins/service-keys.ts +294 -0
- package/src/plugins/session-search-domain.ts +238 -0
- package/src/plugins/skills-domain.ts +272 -0
- package/src/plugins/subagent-domain.ts +287 -0
- package/src/plugins/tool-catalog.ts +78 -0
- package/src/plugins/tool-output-policy.ts +52 -0
- package/src/plugins/value-plugins.ts +150 -0
- package/src/plugins/workflow-domain.ts +198 -0
- package/src/plugins/workspace-policy.ts +37 -0
- package/src/providers/registry.ts +63 -0
- package/src/providers/types.ts +44 -0
- package/src/retry/policy.ts +282 -0
- package/src/retry/stream-chat.ts +312 -0
- package/src/runtime/agent-runtime.ts +83 -0
- package/src/runtime/default-agent-driver.ts +23 -0
- package/src/runtime/domain-tool-source.ts +156 -0
- package/src/runtime/events.ts +61 -0
- package/src/runtime/host-dependencies.ts +71 -0
- package/src/runtime/host-guidance.ts +22 -0
- package/src/runtime/host-layout.ts +23 -0
- package/src/runtime/host.ts +129 -0
- package/src/runtime/standalone-host.ts +118 -0
- package/src/sandbox/controller.ts +204 -0
- package/src/sandbox/model.ts +305 -0
- package/src/sandbox/provider.ts +53 -0
- package/src/sandbox/types.ts +110 -0
- package/src/scheduler/host.ts +22 -0
- package/src/scheduler/scheduler-tool.ts +172 -0
- package/src/session-search/host.ts +56 -0
- package/src/session-search/index.ts +23 -0
- package/src/session-search/session-search-tool.ts +151 -0
- package/src/skills/index.ts +3 -0
- package/src/skills/registry.ts +63 -0
- package/src/skills/skill-manage-tool.ts +205 -0
- package/src/skills/skill-tool.ts +42 -0
- package/src/storage/contracts.ts +159 -0
- package/src/storage/memory.ts +321 -0
- package/src/storage/options.ts +75 -0
- package/src/storage/runtime.ts +115 -0
- package/src/storage/sqlite-tool-output-artifacts.ts +106 -0
- package/src/storage/sqlite.ts +321 -0
- package/src/storage/tool-output-artifacts.ts +75 -0
- package/src/storage.ts +31 -0
- package/src/subagent/child-session.ts +282 -0
- package/src/subagent/guidance.ts +8 -0
- package/src/subagent/policy.ts +198 -0
- package/src/subagent/task-tool.ts +584 -0
- package/src/tool-output/contracts.ts +111 -0
- package/src/tool-output/policy.ts +410 -0
- package/src/tool.ts +1 -0
- package/src/tools/executor.ts +258 -0
- package/src/tools/install-manifest.ts +40 -0
- package/src/tools/llm-api.ts +77 -0
- package/src/tools/registry.ts +206 -0
- package/src/tools/tool-artifact.ts +182 -0
- package/src/tools/tool-source.ts +53 -0
- package/src/utils/errors.ts +334 -0
- package/src/utils/strip-visualization.ts +50 -0
- package/src/workflow/decomposer.ts +139 -0
- package/src/workflow/execution.ts +523 -0
- package/src/workflow/orchestrator-session.ts +161 -0
- package/src/workflow/synthesizer.ts +130 -0
- package/src/workspace/contracts.ts +135 -0
- package/src/workspace/policy.ts +327 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { AutoApproveSeverity, Message, MessageWithParts, Part, QueuedMessage, ResponseFormat, Session, ToolPart, Workspace } from '@capekai/types';
|
|
2
|
+
import type { AttachmentKind } from '@capekai/types';
|
|
3
|
+
|
|
4
|
+
export interface StreamingPartSnapshot {
|
|
5
|
+
id: string;
|
|
6
|
+
messageId: string;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
type: 'text' | 'reasoning';
|
|
9
|
+
createdAt: number;
|
|
10
|
+
text: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AttachmentRecord {
|
|
14
|
+
id: string;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
workspaceId: string;
|
|
17
|
+
kind: AttachmentKind;
|
|
18
|
+
filename: string;
|
|
19
|
+
mimeType: string;
|
|
20
|
+
sizeBytes: number;
|
|
21
|
+
absolutePath: string;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
accessKey: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface EffectiveContextHistory {
|
|
27
|
+
messages: MessageWithParts[];
|
|
28
|
+
latestCompactionBoundary: string | null;
|
|
29
|
+
hasCompaction: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TranscriptPageResult {
|
|
33
|
+
messages: MessageWithParts[];
|
|
34
|
+
pagination: {
|
|
35
|
+
hasOlder: boolean;
|
|
36
|
+
oldestSequence: number | null;
|
|
37
|
+
newestSequence: number | null;
|
|
38
|
+
limit: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type SessionUpdates = Partial<Pick<Session,
|
|
43
|
+
| 'title'
|
|
44
|
+
| 'status'
|
|
45
|
+
| 'metadata'
|
|
46
|
+
| 'preconfigId'
|
|
47
|
+
| 'selectedModel'
|
|
48
|
+
| 'selectedProvider'
|
|
49
|
+
| 'selectedVariant'
|
|
50
|
+
| 'promptTokens'
|
|
51
|
+
| 'completionTokens'
|
|
52
|
+
| 'totalTokens'
|
|
53
|
+
| 'cacheReadTokens'
|
|
54
|
+
| 'cacheWriteTokens'
|
|
55
|
+
| 'noCacheTokens'
|
|
56
|
+
| 'parentId'
|
|
57
|
+
| 'agentName'
|
|
58
|
+
| 'subagentStatus'
|
|
59
|
+
| 'runningAt'
|
|
60
|
+
| 'compacting'
|
|
61
|
+
| 'tags'
|
|
62
|
+
| 'autoApproveSeverity'
|
|
63
|
+
| 'agentId'
|
|
64
|
+
>>;
|
|
65
|
+
|
|
66
|
+
export interface ConversationStore {
|
|
67
|
+
createSession(session: Omit<Session, 'createdAt' | 'updatedAt'> & { createdAt?: string; updatedAt?: string }): Promise<Session>;
|
|
68
|
+
getSession(id: string): Promise<Session | null>;
|
|
69
|
+
updateSession(id: string, updates: SessionUpdates): Promise<Session | null>;
|
|
70
|
+
getChildSessions(parentId: string): Promise<Session[]>;
|
|
71
|
+
createMessage(message: Message): Promise<Message>;
|
|
72
|
+
getMessage(id: string): Promise<Message | null>;
|
|
73
|
+
getMessageWithParts(messageId: string): Promise<MessageWithParts | null>;
|
|
74
|
+
updateMessage(id: string, updates: Partial<Message>): Promise<Message | null>;
|
|
75
|
+
deleteMessage(messageId: string): Promise<boolean>;
|
|
76
|
+
listMessagesWithParts(sessionId: string): Promise<MessageWithParts[]>;
|
|
77
|
+
listLatestMessagesWithPartsPage(sessionId: string, limit?: number): Promise<TranscriptPageResult>;
|
|
78
|
+
buildEffectiveContextHistory(sessionId: string): Promise<EffectiveContextHistory>;
|
|
79
|
+
createPart(part: Part, sessionId: string): Promise<Part>;
|
|
80
|
+
getPart(id: string): Promise<Part | null>;
|
|
81
|
+
getPartsByMessage(messageId: string): Promise<Part[]>;
|
|
82
|
+
getPartsBySession(sessionId: string): Promise<Part[]>;
|
|
83
|
+
updatePart(id: string, updates: Record<string, unknown>): Promise<Part | null>;
|
|
84
|
+
persistStreamingPartSnapshots(snapshots: StreamingPartSnapshot[]): Promise<number>;
|
|
85
|
+
transitionToolToRunningByCallId(sessionId: string, callId: string, childSessionId?: string): Promise<ToolPart | null>;
|
|
86
|
+
transitionToolToInterrupted(partId: string, reason: 'user_request' | 'timeout' | 'error' | 'cascade'): Promise<ToolPart | null>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type ToolOutputArtifactFormat = 'json' | 'text';
|
|
90
|
+
|
|
91
|
+
export interface ToolOutputArtifact {
|
|
92
|
+
id: string;
|
|
93
|
+
sessionId: string;
|
|
94
|
+
workspaceId?: string;
|
|
95
|
+
toolCallId: string;
|
|
96
|
+
toolName: string;
|
|
97
|
+
content: string;
|
|
98
|
+
format: ToolOutputArtifactFormat;
|
|
99
|
+
size: number;
|
|
100
|
+
createdAt: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type CreateToolOutputArtifact = Omit<ToolOutputArtifact, 'id' | 'size' | 'createdAt'>;
|
|
104
|
+
|
|
105
|
+
export interface ToolOutputArtifactPage {
|
|
106
|
+
artifactId: string;
|
|
107
|
+
toolCallId: string;
|
|
108
|
+
toolName: string;
|
|
109
|
+
format: ToolOutputArtifactFormat;
|
|
110
|
+
content: string;
|
|
111
|
+
offset: number;
|
|
112
|
+
limit: number;
|
|
113
|
+
totalChars: number;
|
|
114
|
+
nextOffset: number | null;
|
|
115
|
+
complete: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface ToolOutputArtifactStore {
|
|
119
|
+
create(input: CreateToolOutputArtifact): Promise<ToolOutputArtifact>;
|
|
120
|
+
getPage(sessionId: string, artifactId: string, offset?: number, limit?: number): Promise<ToolOutputArtifactPage | null>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface MessageQueueStore {
|
|
124
|
+
addMessage(sessionId: string, content: string, attachments?: Array<{ id: string; kind: string }>): Promise<QueuedMessage>;
|
|
125
|
+
peek(sessionId: string): Promise<QueuedMessage | null>;
|
|
126
|
+
delete(id: string): Promise<boolean>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface AttachmentStore {
|
|
130
|
+
get(sessionId: string, attachmentId: string): Promise<AttachmentRecord | null>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface WorkspaceStore {
|
|
134
|
+
get(id: string): Promise<Workspace | null>;
|
|
135
|
+
getAutoApproveSeverity(workspaceId: string): Promise<AutoApproveSeverity>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface ResponseFormatStore {
|
|
139
|
+
get(id: string): Promise<ResponseFormat | null>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface ConversationIndex {
|
|
143
|
+
syncMessage(messageId: string): Promise<void>;
|
|
144
|
+
removeMessage?(messageId: string): Promise<void>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface StorageBundle {
|
|
148
|
+
conversation: ConversationStore;
|
|
149
|
+
toolOutputArtifacts: ToolOutputArtifactStore;
|
|
150
|
+
queue: MessageQueueStore;
|
|
151
|
+
attachments: AttachmentStore;
|
|
152
|
+
workspaces: WorkspaceStore;
|
|
153
|
+
responseFormats: ResponseFormatStore;
|
|
154
|
+
index: ConversationIndex;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface ClosableStore {
|
|
158
|
+
close(): void;
|
|
159
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import type { Message, MessageWithParts, Part, QueuedMessage, ResponseFormat, Session, ToolPart, Workspace } from '@capekai/types';
|
|
2
|
+
import { createInMemoryToolOutputArtifactStore } from './tool-output-artifacts';
|
|
3
|
+
import type {
|
|
4
|
+
AttachmentRecord,
|
|
5
|
+
AttachmentStore,
|
|
6
|
+
ConversationIndex,
|
|
7
|
+
ConversationStore,
|
|
8
|
+
MessageQueueStore,
|
|
9
|
+
ResponseFormatStore,
|
|
10
|
+
SessionUpdates,
|
|
11
|
+
StorageBundle,
|
|
12
|
+
StreamingPartSnapshot,
|
|
13
|
+
TranscriptPageResult,
|
|
14
|
+
WorkspaceStore,
|
|
15
|
+
} from './contracts';
|
|
16
|
+
|
|
17
|
+
interface StoredMessage {
|
|
18
|
+
message: Message;
|
|
19
|
+
sequence: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface StoredPart {
|
|
23
|
+
part: Part;
|
|
24
|
+
sessionId: string;
|
|
25
|
+
insertionOrder: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function copy<T>(value: T): T {
|
|
29
|
+
return structuredClone(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function compareParts(left: Part, right: Part): number {
|
|
33
|
+
return left.createdAt - right.createdAt;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createInMemoryConversationStore(): ConversationStore {
|
|
37
|
+
const sessions = new Map<string, Session>();
|
|
38
|
+
const messages = new Map<string, StoredMessage>();
|
|
39
|
+
const parts = new Map<string, StoredPart>();
|
|
40
|
+
const nextSequence = new Map<string, number>();
|
|
41
|
+
let nextPartInsertionOrder = 0;
|
|
42
|
+
|
|
43
|
+
const getPartsByMessage = (messageId: string): Part[] => [...parts.values()]
|
|
44
|
+
.filter(record => record.part.messageId === messageId)
|
|
45
|
+
.map(record => copy(record.part))
|
|
46
|
+
.sort(compareParts);
|
|
47
|
+
|
|
48
|
+
const orderedMessages = (sessionId: string): StoredMessage[] => [...messages.values()]
|
|
49
|
+
.filter(record => record.message.sessionId === sessionId)
|
|
50
|
+
.sort((left, right) => left.sequence - right.sequence);
|
|
51
|
+
|
|
52
|
+
const listMessagesWithParts = (sessionId: string): MessageWithParts[] => orderedMessages(sessionId)
|
|
53
|
+
.map(record => ({
|
|
54
|
+
message: copy(record.message),
|
|
55
|
+
parts: getPartsByMessage(record.message.id),
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
const store: ConversationStore = {
|
|
59
|
+
async createSession(input) {
|
|
60
|
+
const now = new Date().toISOString();
|
|
61
|
+
const session = copy({
|
|
62
|
+
...input,
|
|
63
|
+
tags: input.tags ?? [],
|
|
64
|
+
createdAt: input.createdAt || now,
|
|
65
|
+
updatedAt: input.updatedAt || now,
|
|
66
|
+
}) as Session;
|
|
67
|
+
if (sessions.has(session.id)) throw new Error(`Session already exists: ${session.id}`);
|
|
68
|
+
sessions.set(session.id, session);
|
|
69
|
+
return copy(session);
|
|
70
|
+
},
|
|
71
|
+
async getSession(id) {
|
|
72
|
+
const session = sessions.get(id);
|
|
73
|
+
return session ? copy(session) : null;
|
|
74
|
+
},
|
|
75
|
+
async updateSession(id, updates: SessionUpdates) {
|
|
76
|
+
const current = sessions.get(id);
|
|
77
|
+
if (!current) return null;
|
|
78
|
+
const updated = copy({ ...current, ...updates, updatedAt: new Date().toISOString() }) as Session;
|
|
79
|
+
sessions.set(id, updated);
|
|
80
|
+
return copy(updated);
|
|
81
|
+
},
|
|
82
|
+
async getChildSessions(parentId) {
|
|
83
|
+
return [...sessions.values()]
|
|
84
|
+
.filter(session => session.parentId === parentId)
|
|
85
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt))
|
|
86
|
+
.map(copy);
|
|
87
|
+
},
|
|
88
|
+
async createMessage(message) {
|
|
89
|
+
if (!sessions.has(message.sessionId)) throw new Error(`Session does not exist: ${message.sessionId}`);
|
|
90
|
+
if (messages.has(message.id)) throw new Error(`Message already exists: ${message.id}`);
|
|
91
|
+
const sequence = nextSequence.get(message.sessionId) ?? 1;
|
|
92
|
+
nextSequence.set(message.sessionId, sequence + 1);
|
|
93
|
+
messages.set(message.id, { message: copy(message), sequence });
|
|
94
|
+
return copy(message);
|
|
95
|
+
},
|
|
96
|
+
async getMessage(id) {
|
|
97
|
+
const record = messages.get(id);
|
|
98
|
+
return record ? copy(record.message) : null;
|
|
99
|
+
},
|
|
100
|
+
async getMessageWithParts(messageId) {
|
|
101
|
+
const record = messages.get(messageId);
|
|
102
|
+
return record ? { message: copy(record.message), parts: getPartsByMessage(messageId) } : null;
|
|
103
|
+
},
|
|
104
|
+
async updateMessage(id, updates) {
|
|
105
|
+
const current = messages.get(id);
|
|
106
|
+
if (!current) return null;
|
|
107
|
+
current.message = copy({ ...current.message, ...updates }) as Message;
|
|
108
|
+
return copy(current.message);
|
|
109
|
+
},
|
|
110
|
+
async deleteMessage(messageId) {
|
|
111
|
+
if (!messages.delete(messageId)) return false;
|
|
112
|
+
for (const [id, record] of parts) {
|
|
113
|
+
if (record.part.messageId === messageId) parts.delete(id);
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
},
|
|
117
|
+
async listMessagesWithParts(sessionId) { return listMessagesWithParts(sessionId); },
|
|
118
|
+
async listLatestMessagesWithPartsPage(sessionId, limit = 50): Promise<TranscriptPageResult> {
|
|
119
|
+
const effectiveLimit = Math.min(Math.max(limit, 1), 100);
|
|
120
|
+
const ordered = orderedMessages(sessionId);
|
|
121
|
+
const selected = ordered.slice(-effectiveLimit);
|
|
122
|
+
return {
|
|
123
|
+
messages: selected.map(record => ({
|
|
124
|
+
message: copy(record.message),
|
|
125
|
+
parts: getPartsByMessage(record.message.id),
|
|
126
|
+
})),
|
|
127
|
+
pagination: {
|
|
128
|
+
hasOlder: ordered.length > selected.length,
|
|
129
|
+
oldestSequence: selected[0]?.sequence ?? null,
|
|
130
|
+
newestSequence: selected.at(-1)?.sequence ?? null,
|
|
131
|
+
limit: effectiveLimit,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
async buildEffectiveContextHistory(sessionId) {
|
|
136
|
+
const ordered = orderedMessages(sessionId);
|
|
137
|
+
let boundary: StoredMessage | undefined;
|
|
138
|
+
for (let index = ordered.length - 1; index >= 0; index -= 1) {
|
|
139
|
+
const candidate = ordered[index];
|
|
140
|
+
const message = candidate.message;
|
|
141
|
+
if (message.role !== 'assistant' || message.summary !== true || message.mode !== 'compaction' || !message.parentId) continue;
|
|
142
|
+
const trigger = messages.get(message.parentId);
|
|
143
|
+
if (!trigger || trigger.message.sessionId !== sessionId) continue;
|
|
144
|
+
if (!getPartsByMessage(trigger.message.id).some(part => part.type === 'compaction')) continue;
|
|
145
|
+
boundary = trigger;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
const effective = boundary
|
|
149
|
+
? ordered.filter(record => record.sequence >= boundary.sequence)
|
|
150
|
+
: ordered;
|
|
151
|
+
return {
|
|
152
|
+
messages: effective.map(record => ({
|
|
153
|
+
message: copy(record.message),
|
|
154
|
+
parts: getPartsByMessage(record.message.id),
|
|
155
|
+
})),
|
|
156
|
+
latestCompactionBoundary: boundary?.message.id ?? null,
|
|
157
|
+
hasCompaction: Boolean(boundary),
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
async createPart(part, sessionId) {
|
|
161
|
+
const message = messages.get(part.messageId);
|
|
162
|
+
if (!message || message.message.sessionId !== sessionId) throw new Error(`Message does not exist in session: ${part.messageId}`);
|
|
163
|
+
if (parts.has(part.id)) throw new Error(`Part already exists: ${part.id}`);
|
|
164
|
+
parts.set(part.id, {
|
|
165
|
+
part: copy(part),
|
|
166
|
+
sessionId,
|
|
167
|
+
insertionOrder: nextPartInsertionOrder++,
|
|
168
|
+
});
|
|
169
|
+
return copy(part);
|
|
170
|
+
},
|
|
171
|
+
async getPart(id) {
|
|
172
|
+
const record = parts.get(id);
|
|
173
|
+
return record ? copy(record.part) : null;
|
|
174
|
+
},
|
|
175
|
+
async getPartsByMessage(messageId) {
|
|
176
|
+
return getPartsByMessage(messageId);
|
|
177
|
+
},
|
|
178
|
+
async getPartsBySession(sessionId) {
|
|
179
|
+
return [...parts.values()]
|
|
180
|
+
.filter(record => record.sessionId === sessionId)
|
|
181
|
+
.map(record => copy(record.part))
|
|
182
|
+
.sort(compareParts);
|
|
183
|
+
},
|
|
184
|
+
async updatePart(id, updates) {
|
|
185
|
+
const current = parts.get(id);
|
|
186
|
+
if (!current) return null;
|
|
187
|
+
current.part = copy({ ...current.part, ...updates }) as Part;
|
|
188
|
+
return copy(current.part);
|
|
189
|
+
},
|
|
190
|
+
async persistStreamingPartSnapshots(snapshots: StreamingPartSnapshot[]) {
|
|
191
|
+
let count = 0;
|
|
192
|
+
for (const snapshot of snapshots) {
|
|
193
|
+
const record = parts.get(snapshot.id);
|
|
194
|
+
if (!record
|
|
195
|
+
|| record.part.messageId !== snapshot.messageId
|
|
196
|
+
|| record.sessionId !== snapshot.sessionId
|
|
197
|
+
|| record.part.type !== snapshot.type) continue;
|
|
198
|
+
record.part = copy({ ...record.part, text: snapshot.text }) as Part;
|
|
199
|
+
count += 1;
|
|
200
|
+
}
|
|
201
|
+
return count;
|
|
202
|
+
},
|
|
203
|
+
async transitionToolToRunningByCallId(sessionId, callId, childSessionId) {
|
|
204
|
+
const candidates = [...parts.values()]
|
|
205
|
+
.filter(record => record.sessionId === sessionId
|
|
206
|
+
&& record.part.type === 'tool'
|
|
207
|
+
&& record.part.callId === callId)
|
|
208
|
+
.sort((left, right) => {
|
|
209
|
+
const leftPart = left.part as ToolPart;
|
|
210
|
+
const rightPart = right.part as ToolPart;
|
|
211
|
+
return Number(rightPart.state.status === 'pending') - Number(leftPart.state.status === 'pending')
|
|
212
|
+
|| rightPart.createdAt - leftPart.createdAt
|
|
213
|
+
|| right.insertionOrder - left.insertionOrder;
|
|
214
|
+
});
|
|
215
|
+
const toolPart = candidates[0]?.part as ToolPart | undefined;
|
|
216
|
+
if (!toolPart || toolPart.state.status !== 'pending') return null;
|
|
217
|
+
return await store.updatePart(toolPart.id, {
|
|
218
|
+
state: {
|
|
219
|
+
status: 'running',
|
|
220
|
+
input: toolPart.state.input,
|
|
221
|
+
startedAt: Date.now(),
|
|
222
|
+
...(childSessionId ? { childSessionId } : {}),
|
|
223
|
+
},
|
|
224
|
+
}) as ToolPart;
|
|
225
|
+
},
|
|
226
|
+
async transitionToolToInterrupted(partId, reason) {
|
|
227
|
+
const current = parts.get(partId)?.part;
|
|
228
|
+
if (!current || current.type !== 'tool') return null;
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
return await store.updatePart(partId, {
|
|
231
|
+
state: {
|
|
232
|
+
status: 'interrupted',
|
|
233
|
+
input: current.state.input,
|
|
234
|
+
startedAt: current.state.status === 'running' ? current.state.startedAt : now,
|
|
235
|
+
interruptedAt: now,
|
|
236
|
+
reason,
|
|
237
|
+
...('childSessionId' in current.state && current.state.childSessionId
|
|
238
|
+
? { childSessionId: current.state.childSessionId }
|
|
239
|
+
: {}),
|
|
240
|
+
},
|
|
241
|
+
}) as ToolPart;
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
return store;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function createInMemoryMessageQueueStore(options: { attachments?: AttachmentStore } = {}): MessageQueueStore {
|
|
249
|
+
const records = new Map<string, QueuedMessage>();
|
|
250
|
+
let id = 0;
|
|
251
|
+
return {
|
|
252
|
+
async addMessage(sessionId, content, attachments) {
|
|
253
|
+
const position = Math.max(-1, ...[...records.values()]
|
|
254
|
+
.filter(record => record.sessionId === sessionId)
|
|
255
|
+
.map(record => record.position)) + 1;
|
|
256
|
+
const enriched: Array<{ id: string; kind: string; filename?: string; mimeType?: string; accessKey?: string }> | undefined = attachments ? [] : undefined;
|
|
257
|
+
if (attachments && enriched) {
|
|
258
|
+
for (const attachment of attachments) {
|
|
259
|
+
const record = await options.attachments?.get(sessionId, attachment.id);
|
|
260
|
+
enriched.push(record ? {
|
|
261
|
+
...attachment,
|
|
262
|
+
filename: record.filename,
|
|
263
|
+
mimeType: record.mimeType,
|
|
264
|
+
accessKey: record.accessKey,
|
|
265
|
+
} : attachment);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const message: QueuedMessage = {
|
|
269
|
+
id: `queue-${++id}`,
|
|
270
|
+
sessionId,
|
|
271
|
+
content,
|
|
272
|
+
position,
|
|
273
|
+
createdAt: Date.now(),
|
|
274
|
+
...(enriched ? { attachments: enriched } : {}),
|
|
275
|
+
};
|
|
276
|
+
records.set(message.id, copy(message));
|
|
277
|
+
return copy(message);
|
|
278
|
+
},
|
|
279
|
+
async peek(sessionId) {
|
|
280
|
+
const message = [...records.values()]
|
|
281
|
+
.filter(record => record.sessionId === sessionId)
|
|
282
|
+
.sort((left, right) => left.position - right.position || left.id.localeCompare(right.id))[0];
|
|
283
|
+
return message ? copy(message) : null;
|
|
284
|
+
},
|
|
285
|
+
async delete(idToDelete) {
|
|
286
|
+
return records.delete(idToDelete);
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface InMemoryAuxiliaryRecords {
|
|
292
|
+
attachments?: AttachmentRecord[];
|
|
293
|
+
workspaces?: Workspace[];
|
|
294
|
+
responseFormats?: ResponseFormat[];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function createInMemoryStorageBundle(records: InMemoryAuxiliaryRecords = {}): StorageBundle {
|
|
298
|
+
const attachments = new Map(records.attachments?.map(record => [`${record.sessionId}:${record.id}`, copy(record)]));
|
|
299
|
+
const workspaces = new Map(records.workspaces?.map(record => [record.id, copy(record)]));
|
|
300
|
+
const responseFormats = new Map(records.responseFormats?.map(record => [record.id, copy(record)]));
|
|
301
|
+
const attachmentStore: AttachmentStore = {
|
|
302
|
+
get: async (sessionId, attachmentId) => copy(attachments.get(`${sessionId}:${attachmentId}`) ?? null),
|
|
303
|
+
};
|
|
304
|
+
const workspaceStore: WorkspaceStore = {
|
|
305
|
+
get: async id => copy(workspaces.get(id) ?? null),
|
|
306
|
+
getAutoApproveSeverity: async id => workspaces.get(id)?.settings.autoApproveSeverity ?? 'low',
|
|
307
|
+
};
|
|
308
|
+
const responseFormatStore: ResponseFormatStore = {
|
|
309
|
+
get: async id => copy(responseFormats.get(id) ?? null),
|
|
310
|
+
};
|
|
311
|
+
const index: ConversationIndex = { syncMessage: async () => {} };
|
|
312
|
+
return {
|
|
313
|
+
conversation: createInMemoryConversationStore(),
|
|
314
|
+
toolOutputArtifacts: createInMemoryToolOutputArtifactStore(),
|
|
315
|
+
queue: createInMemoryMessageQueueStore({ attachments: attachmentStore }),
|
|
316
|
+
attachments: attachmentStore,
|
|
317
|
+
workspaces: workspaceStore,
|
|
318
|
+
responseFormats: responseFormatStore,
|
|
319
|
+
index,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ConversationStore, StorageBundle } from './contracts';
|
|
2
|
+
import { createInMemoryStorageBundle } from './memory';
|
|
3
|
+
|
|
4
|
+
export type AgentStorageOption =
|
|
5
|
+
| ConversationStore
|
|
6
|
+
| { type: 'memory' }
|
|
7
|
+
| { type: 'sqlite'; path: string };
|
|
8
|
+
|
|
9
|
+
/** The storage bundle plus a close() for resources the chosen driver owns
|
|
10
|
+
* (e.g. sqlite handles). The bundle is spread directly: use the result as a
|
|
11
|
+
* StorageBundle anywhere, call close() when you are done with it. */
|
|
12
|
+
export interface AgentStorage extends StorageBundle {
|
|
13
|
+
close(): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function composeConversationStore(conversation: ConversationStore): StorageBundle {
|
|
17
|
+
return {
|
|
18
|
+
...createInMemoryStorageBundle(),
|
|
19
|
+
conversation,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isStorageDescriptor(
|
|
24
|
+
option: AgentStorageOption,
|
|
25
|
+
): option is { type: 'memory' } | { type: 'sqlite'; path: string } {
|
|
26
|
+
const record = option as Record<string, unknown>;
|
|
27
|
+
const keys = Object.keys(record).sort();
|
|
28
|
+
return (record.type === 'memory' && keys.length === 1)
|
|
29
|
+
|| (record.type === 'sqlite'
|
|
30
|
+
&& typeof record.path === 'string'
|
|
31
|
+
&& keys.length === 2
|
|
32
|
+
&& keys[0] === 'path'
|
|
33
|
+
&& keys[1] === 'type');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function memoryComposition(): AgentStorage {
|
|
37
|
+
return {
|
|
38
|
+
...createInMemoryStorageBundle(),
|
|
39
|
+
close: () => {},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The sqlite driver is imported dynamically: importing the package entry
|
|
44
|
+
* must not pull bun:sqlite into runtimes that only use memory or custom
|
|
45
|
+
* stores. The driver loads only when { type: 'sqlite' } is requested. */
|
|
46
|
+
export async function createAgentStorage(option?: AgentStorageOption): Promise<AgentStorage> {
|
|
47
|
+
if (!option) {
|
|
48
|
+
return memoryComposition();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (isStorageDescriptor(option)) {
|
|
52
|
+
if (option.type === 'memory') {
|
|
53
|
+
return memoryComposition();
|
|
54
|
+
}
|
|
55
|
+
const [{ createSqliteConversationStore }, { createSqliteToolOutputArtifactStore }] = await Promise.all([
|
|
56
|
+
import('./sqlite'),
|
|
57
|
+
import('./sqlite-tool-output-artifacts'),
|
|
58
|
+
]);
|
|
59
|
+
const conversation = createSqliteConversationStore({ path: option.path });
|
|
60
|
+
const toolOutputArtifacts = createSqliteToolOutputArtifactStore({ path: option.path });
|
|
61
|
+
return {
|
|
62
|
+
...composeConversationStore(conversation),
|
|
63
|
+
toolOutputArtifacts,
|
|
64
|
+
close: () => {
|
|
65
|
+
toolOutputArtifacts.close();
|
|
66
|
+
conversation.close();
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
...composeConversationStore(option),
|
|
73
|
+
close: () => {},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import type { StorageBundle } from './contracts';
|
|
3
|
+
import { createInMemoryStorageBundle } from './memory';
|
|
4
|
+
|
|
5
|
+
let storage: StorageBundle = createInMemoryStorageBundle();
|
|
6
|
+
const scopedStorage = new AsyncLocalStorage<StorageBundle>();
|
|
7
|
+
|
|
8
|
+
function activeStorage(): StorageBundle {
|
|
9
|
+
return scopedStorage.getStore() ?? storage;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function withStorage<T>(value: StorageBundle, callback: () => T): T {
|
|
13
|
+
return scopedStorage.run(value, callback);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function configureStorage(value: StorageBundle): void {
|
|
17
|
+
storage = value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getStorage(): StorageBundle {
|
|
21
|
+
return activeStorage();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const createSession = async (...args: Parameters<StorageBundle['conversation']['createSession']>) =>
|
|
25
|
+
activeStorage().conversation.createSession(...args);
|
|
26
|
+
export const getSession = async (...args: Parameters<StorageBundle['conversation']['getSession']>) =>
|
|
27
|
+
activeStorage().conversation.getSession(...args);
|
|
28
|
+
export const updateSession = async (...args: Parameters<StorageBundle['conversation']['updateSession']>) =>
|
|
29
|
+
activeStorage().conversation.updateSession(...args);
|
|
30
|
+
export const getChildSessions = async (...args: Parameters<StorageBundle['conversation']['getChildSessions']>) =>
|
|
31
|
+
activeStorage().conversation.getChildSessions(...args);
|
|
32
|
+
export const createMessage = async (...args: Parameters<StorageBundle['conversation']['createMessage']>) =>
|
|
33
|
+
activeStorage().conversation.createMessage(...args);
|
|
34
|
+
export const getMessage = async (...args: Parameters<StorageBundle['conversation']['getMessage']>) =>
|
|
35
|
+
activeStorage().conversation.getMessage(...args);
|
|
36
|
+
export const getMessageWithParts = async (...args: Parameters<StorageBundle['conversation']['getMessageWithParts']>) =>
|
|
37
|
+
activeStorage().conversation.getMessageWithParts(...args);
|
|
38
|
+
export async function updateMessage(
|
|
39
|
+
id: string,
|
|
40
|
+
updates: Parameters<StorageBundle['conversation']['updateMessage']>[1],
|
|
41
|
+
options?: { syncFts?: boolean },
|
|
42
|
+
) {
|
|
43
|
+
const current = activeStorage();
|
|
44
|
+
const result = await current.conversation.updateMessage(id, updates);
|
|
45
|
+
if (result && options?.syncFts !== false) await current.index.syncMessage(id);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
export const deleteMessage = async (...args: Parameters<StorageBundle['conversation']['deleteMessage']>) => {
|
|
49
|
+
const current = activeStorage();
|
|
50
|
+
await current.index.removeMessage?.(args[0]);
|
|
51
|
+
return current.conversation.deleteMessage(...args);
|
|
52
|
+
};
|
|
53
|
+
export const listMessagesWithParts = async (...args: Parameters<StorageBundle['conversation']['listMessagesWithParts']>) =>
|
|
54
|
+
activeStorage().conversation.listMessagesWithParts(...args);
|
|
55
|
+
export const listLatestMessagesWithPartsPage = async (...args: Parameters<StorageBundle['conversation']['listLatestMessagesWithPartsPage']>) =>
|
|
56
|
+
activeStorage().conversation.listLatestMessagesWithPartsPage(...args);
|
|
57
|
+
export const buildEffectiveContextHistory = async (...args: Parameters<StorageBundle['conversation']['buildEffectiveContextHistory']>) =>
|
|
58
|
+
activeStorage().conversation.buildEffectiveContextHistory(...args);
|
|
59
|
+
export async function createPart(
|
|
60
|
+
part: Parameters<StorageBundle['conversation']['createPart']>[0],
|
|
61
|
+
sessionId: string,
|
|
62
|
+
options?: { syncFts?: boolean },
|
|
63
|
+
) {
|
|
64
|
+
const current = activeStorage();
|
|
65
|
+
const result = await current.conversation.createPart(part, sessionId);
|
|
66
|
+
if (result && options?.syncFts !== false && (part.type === 'text' || part.type === 'tool')) {
|
|
67
|
+
await current.index.syncMessage(part.messageId);
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
export const getPart = async (...args: Parameters<StorageBundle['conversation']['getPart']>) =>
|
|
72
|
+
activeStorage().conversation.getPart(...args);
|
|
73
|
+
export const getPartsByMessage = async (...args: Parameters<StorageBundle['conversation']['getPartsByMessage']>) =>
|
|
74
|
+
activeStorage().conversation.getPartsByMessage(...args);
|
|
75
|
+
export const getPartsBySession = async (...args: Parameters<StorageBundle['conversation']['getPartsBySession']>) =>
|
|
76
|
+
activeStorage().conversation.getPartsBySession(...args);
|
|
77
|
+
export async function updatePart(
|
|
78
|
+
id: string,
|
|
79
|
+
updates: Record<string, unknown>,
|
|
80
|
+
options?: { syncFts?: boolean },
|
|
81
|
+
) {
|
|
82
|
+
const current = activeStorage();
|
|
83
|
+
const previous = await current.conversation.getPart(id);
|
|
84
|
+
const result = await current.conversation.updatePart(id, updates);
|
|
85
|
+
if (result && options?.syncFts !== false
|
|
86
|
+
&& (previous?.type === 'text' || previous?.type === 'tool' || result.type === 'text' || result.type === 'tool')) {
|
|
87
|
+
await current.index.syncMessage(result.messageId);
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
export const persistStreamingPartSnapshots = async (...args: Parameters<StorageBundle['conversation']['persistStreamingPartSnapshots']>) =>
|
|
92
|
+
activeStorage().conversation.persistStreamingPartSnapshots(...args);
|
|
93
|
+
export const transitionToolToRunningByCallId = async (...args: Parameters<StorageBundle['conversation']['transitionToolToRunningByCallId']>) =>
|
|
94
|
+
activeStorage().conversation.transitionToolToRunningByCallId(...args);
|
|
95
|
+
export const transitionToolToInterrupted = async (...args: Parameters<StorageBundle['conversation']['transitionToolToInterrupted']>) =>
|
|
96
|
+
activeStorage().conversation.transitionToolToInterrupted(...args);
|
|
97
|
+
export const syncMessageFts = async (messageId: string): Promise<void> => activeStorage().index.syncMessage(messageId);
|
|
98
|
+
export const createToolOutputArtifact = (...args: Parameters<StorageBundle['toolOutputArtifacts']['create']>) =>
|
|
99
|
+
activeStorage().toolOutputArtifacts.create(...args);
|
|
100
|
+
export const getToolOutputArtifactPage = (...args: Parameters<StorageBundle['toolOutputArtifacts']['getPage']>) =>
|
|
101
|
+
activeStorage().toolOutputArtifacts.getPage(...args);
|
|
102
|
+
export const addMessageToQueue = (...args: Parameters<StorageBundle['queue']['addMessage']>) =>
|
|
103
|
+
activeStorage().queue.addMessage(...args);
|
|
104
|
+
export const getNextQueuedMessage = (...args: Parameters<StorageBundle['queue']['peek']>) =>
|
|
105
|
+
activeStorage().queue.peek(...args);
|
|
106
|
+
export const deleteQueuedMessage = (...args: Parameters<StorageBundle['queue']['delete']>) =>
|
|
107
|
+
activeStorage().queue.delete(...args);
|
|
108
|
+
export const getAttachment = (...args: Parameters<StorageBundle['attachments']['get']>) =>
|
|
109
|
+
activeStorage().attachments.get(...args);
|
|
110
|
+
export const getWorkspace = async (...args: Parameters<StorageBundle['workspaces']['get']>) =>
|
|
111
|
+
activeStorage().workspaces.get(...args);
|
|
112
|
+
export const getWorkspaceAutoApproveSeverity = async (...args: Parameters<StorageBundle['workspaces']['getAutoApproveSeverity']>) =>
|
|
113
|
+
activeStorage().workspaces.getAutoApproveSeverity(...args);
|
|
114
|
+
export const getResponseFormat = (...args: Parameters<StorageBundle['responseFormats']['get']>) =>
|
|
115
|
+
activeStorage().responseFormats.get(...args);
|