@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,18 @@
|
|
|
1
|
+
import type { AuthErrorMessage, ContextOverflowErrorMessage, InvalidRequestErrorMessage, ErrorMessage } from '@capekai/types';
|
|
2
|
+
import { ApiErrorType, ERROR_AUTH, ERROR_INVALID_REQUEST, ERROR_CHAT_FAILED } from '../utils/errors';
|
|
3
|
+
import type { ClassifiedError } from '../utils/errors';
|
|
4
|
+
|
|
5
|
+
export type ErrorEvent = AuthErrorMessage | ContextOverflowErrorMessage | InvalidRequestErrorMessage | ErrorMessage;
|
|
6
|
+
|
|
7
|
+
export function createErrorEvent(classified: ClassifiedError): ErrorEvent {
|
|
8
|
+
switch (classified.type) {
|
|
9
|
+
case ApiErrorType.Authentication:
|
|
10
|
+
return { type: 'error.auth', code: ERROR_AUTH, message: classified.message };
|
|
11
|
+
case ApiErrorType.ContextOverflow:
|
|
12
|
+
return { type: 'error.context_overflow', code: 'context_overflow', message: classified.message };
|
|
13
|
+
case ApiErrorType.InvalidRequest:
|
|
14
|
+
return { type: 'error.invalid_request', code: ERROR_INVALID_REQUEST, message: classified.message };
|
|
15
|
+
default:
|
|
16
|
+
return { type: 'error', code: ERROR_CHAT_FAILED, message: classified.message };
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/core/fork.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Message, MessageWithParts, Part, Session } from '@capekai/types';
|
|
2
|
+
import {
|
|
3
|
+
createMessage,
|
|
4
|
+
createPart,
|
|
5
|
+
createSession,
|
|
6
|
+
getSession,
|
|
7
|
+
getWorkspaceAutoApproveSeverity,
|
|
8
|
+
listMessagesWithParts,
|
|
9
|
+
} from '../storage/runtime';
|
|
10
|
+
|
|
11
|
+
interface ForkOptions {
|
|
12
|
+
sessionId: string;
|
|
13
|
+
targetMessageId: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ForkResult {
|
|
18
|
+
forkedSession: Session;
|
|
19
|
+
messages: MessageWithParts[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function generateId(): string {
|
|
23
|
+
return crypto.randomUUID();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function copyMessage(
|
|
27
|
+
message: Message,
|
|
28
|
+
newSessionId: string,
|
|
29
|
+
newMessageId: string,
|
|
30
|
+
idMap: Map<string, string>,
|
|
31
|
+
): Message {
|
|
32
|
+
if (message.role === 'assistant') {
|
|
33
|
+
const parentId = (message as { parentId?: string }).parentId;
|
|
34
|
+
return {
|
|
35
|
+
...message,
|
|
36
|
+
id: newMessageId,
|
|
37
|
+
sessionId: newSessionId,
|
|
38
|
+
status: 'completed' as const,
|
|
39
|
+
...(parentId ? { parentId: idMap.get(parentId) ?? parentId } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
...message,
|
|
44
|
+
id: newMessageId,
|
|
45
|
+
sessionId: newSessionId,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function copyPart(part: Part, newMessageId: string, newPartId: string): Part {
|
|
50
|
+
const { id: _oldId, messageId: _oldMsgId, ...rest } = part;
|
|
51
|
+
return {
|
|
52
|
+
...rest,
|
|
53
|
+
id: newPartId,
|
|
54
|
+
messageId: newMessageId,
|
|
55
|
+
} as Part;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function forkSession(options: ForkOptions): Promise<ForkResult> {
|
|
59
|
+
const { sessionId, targetMessageId, title } = options;
|
|
60
|
+
const sourceSession = await getSession(sessionId);
|
|
61
|
+
if (!sourceSession) throw new Error('Source session not found');
|
|
62
|
+
|
|
63
|
+
const allMessages = await listMessagesWithParts(sessionId);
|
|
64
|
+
const targetIndex = allMessages.findIndex((entry) => entry.message.id === targetMessageId);
|
|
65
|
+
if (targetIndex === -1) throw new Error('Target message not found');
|
|
66
|
+
|
|
67
|
+
const messagesToFork = allMessages.slice(0, targetIndex + 1);
|
|
68
|
+
const forkedSession = await createSession({
|
|
69
|
+
id: generateId(),
|
|
70
|
+
workspaceId: sourceSession.workspaceId,
|
|
71
|
+
preconfigId: sourceSession.preconfigId,
|
|
72
|
+
title: title || `${sourceSession.title || 'Untitled'} (fork)`,
|
|
73
|
+
status: 'active',
|
|
74
|
+
metadata: { ...(sourceSession.metadata || {}), forkedFrom: sessionId },
|
|
75
|
+
parentId: null,
|
|
76
|
+
agentName: null,
|
|
77
|
+
selectedModel: sourceSession.selectedModel,
|
|
78
|
+
selectedProvider: sourceSession.selectedProvider,
|
|
79
|
+
promptTokens: sourceSession.promptTokens,
|
|
80
|
+
completionTokens: sourceSession.completionTokens,
|
|
81
|
+
totalTokens: sourceSession.totalTokens,
|
|
82
|
+
autoApproveSeverity: sourceSession.autoApproveSeverity ?? await getWorkspaceAutoApproveSeverity(sourceSession.workspaceId),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const forkedMessages: MessageWithParts[] = [];
|
|
86
|
+
const idMap = new Map<string, string>();
|
|
87
|
+
for (const { message } of messagesToFork) idMap.set(message.id, generateId());
|
|
88
|
+
|
|
89
|
+
for (const { message, parts } of messagesToFork) {
|
|
90
|
+
const newMessageId = idMap.get(message.id)!;
|
|
91
|
+
const newMessage = copyMessage(message, forkedSession.id, newMessageId, idMap);
|
|
92
|
+
await createMessage(newMessage);
|
|
93
|
+
const newParts: Part[] = [];
|
|
94
|
+
for (const part of parts) {
|
|
95
|
+
const newPart = copyPart(part, newMessageId, generateId());
|
|
96
|
+
await createPart(newPart, forkedSession.id);
|
|
97
|
+
newParts.push(newPart);
|
|
98
|
+
}
|
|
99
|
+
forkedMessages.push({ message: newMessage, parts: newParts });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { forkedSession, messages: forkedMessages };
|
|
103
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { InterruptReason, SessionInterruptResult } from '@capekai/types';
|
|
2
|
+
import { isSandboxActive } from '../runtime/host-dependencies';
|
|
3
|
+
import { getSandboxController } from '../sandbox/controller';
|
|
4
|
+
import { getChildSessions, getSession, updateSession } from '../storage/runtime';
|
|
5
|
+
import { rejectPendingAsksBySession } from '../permission/ask-user-api';
|
|
6
|
+
|
|
7
|
+
interface ToolExecution {
|
|
8
|
+
controller: AbortController;
|
|
9
|
+
listener: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SessionAbortContext {
|
|
13
|
+
sessionId: string;
|
|
14
|
+
parentId?: string;
|
|
15
|
+
controller: AbortController;
|
|
16
|
+
toolControllers: Map<string, ToolExecution>;
|
|
17
|
+
childSessionIds: Set<string>;
|
|
18
|
+
parentAbortListener?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class InterruptManager {
|
|
22
|
+
private sessionContexts = new Map<string, SessionAbortContext>();
|
|
23
|
+
private pendingParentLinks = new Map<string, Promise<void>>();
|
|
24
|
+
|
|
25
|
+
registerSession(sessionId: string, parentId?: string): AbortController {
|
|
26
|
+
if (this.sessionContexts.has(sessionId)) {
|
|
27
|
+
throw new Error(`Session ${sessionId} already has an active execution`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const context: SessionAbortContext = {
|
|
32
|
+
sessionId,
|
|
33
|
+
controller,
|
|
34
|
+
toolControllers: new Map(),
|
|
35
|
+
childSessionIds: new Set(),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
this.sessionContexts.set(sessionId, context);
|
|
39
|
+
|
|
40
|
+
const linkToParent = (resolvedParentId: string): void => {
|
|
41
|
+
const parentContext = this.sessionContexts.get(resolvedParentId);
|
|
42
|
+
if (!parentContext) return;
|
|
43
|
+
parentContext.childSessionIds.add(sessionId);
|
|
44
|
+
const listener = () => {
|
|
45
|
+
void this.interruptSession(sessionId, 'cascade');
|
|
46
|
+
};
|
|
47
|
+
context.parentId = resolvedParentId;
|
|
48
|
+
context.parentAbortListener = listener;
|
|
49
|
+
parentContext.controller.signal.addEventListener('abort', listener);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (parentId) {
|
|
53
|
+
linkToParent(parentId);
|
|
54
|
+
} else {
|
|
55
|
+
const pending = getSession(sessionId).then(session => {
|
|
56
|
+
if (session?.parentId) linkToParent(session.parentId);
|
|
57
|
+
}).catch((err: unknown) => {
|
|
58
|
+
console.error('[interrupt] Failed to resolve session parent', err);
|
|
59
|
+
});
|
|
60
|
+
this.pendingParentLinks.set(sessionId, pending);
|
|
61
|
+
void pending.finally(() => {
|
|
62
|
+
if (this.pendingParentLinks.get(sessionId) === pending) this.pendingParentLinks.delete(sessionId);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return controller;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
registerToolExecution(sessionId: string, toolCallId: string): AbortController {
|
|
70
|
+
const context = this.sessionContexts.get(sessionId);
|
|
71
|
+
|
|
72
|
+
const toolController = new AbortController();
|
|
73
|
+
|
|
74
|
+
if (context) {
|
|
75
|
+
const listener = () => {
|
|
76
|
+
toolController.abort();
|
|
77
|
+
};
|
|
78
|
+
context.toolControllers.set(toolCallId, { controller: toolController, listener });
|
|
79
|
+
context.controller.signal.addEventListener('abort', listener);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return toolController;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async interruptSession(
|
|
86
|
+
sessionId: string,
|
|
87
|
+
_reason: InterruptReason = 'user_request'
|
|
88
|
+
): Promise<SessionInterruptResult> {
|
|
89
|
+
const context = this.sessionContexts.get(sessionId);
|
|
90
|
+
const cascadedTo: string[] = [];
|
|
91
|
+
const interruptedTools: string[] = [];
|
|
92
|
+
const rejectedAsks: string[] = [];
|
|
93
|
+
await Promise.all(this.pendingParentLinks.values());
|
|
94
|
+
|
|
95
|
+
if (context) {
|
|
96
|
+
for (const [toolCallId, { controller: toolController }] of context.toolControllers) {
|
|
97
|
+
if (!toolController.signal.aborted) {
|
|
98
|
+
toolController.abort();
|
|
99
|
+
interruptedTools.push(toolCallId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!context.controller.signal.aborted) {
|
|
104
|
+
context.controller.abort();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const childId of context.childSessionIds) {
|
|
108
|
+
cascadedTo.push(childId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Reject any pending asks for this session to unblock waiting tool executions
|
|
113
|
+
const pendingAskIds = await rejectPendingAsksBySession(sessionId);
|
|
114
|
+
rejectedAsks.push(...pendingAskIds);
|
|
115
|
+
|
|
116
|
+
// Reject any pending sandbox calls for this session to unblock waitForResponse()
|
|
117
|
+
if (isSandboxActive()) {
|
|
118
|
+
getSandboxController().rejectAllPendingForSession(sessionId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Only set subagentStatus for actual subagent sessions (those with a parentId)
|
|
122
|
+
// Main sessions should not have their status changed to error on interrupt
|
|
123
|
+
const session = await getSession(sessionId);
|
|
124
|
+
if (session?.parentId) {
|
|
125
|
+
await updateSession(sessionId, {
|
|
126
|
+
subagentStatus: 'interrupted',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const childSessions = await getChildSessions(sessionId);
|
|
131
|
+
for (const child of childSessions) {
|
|
132
|
+
if (child.subagentStatus === 'running') {
|
|
133
|
+
const childResult = await this.interruptSession(child.id, 'cascade');
|
|
134
|
+
cascadedTo.push(...childResult.cascadedTo.filter(id => !cascadedTo.includes(id)));
|
|
135
|
+
interruptedTools.push(...childResult.interruptedTools);
|
|
136
|
+
rejectedAsks.push(...childResult.rejectedAsks);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
sessionId,
|
|
142
|
+
success: true,
|
|
143
|
+
cascadedTo: [...new Set(cascadedTo)],
|
|
144
|
+
interruptedTools: [...new Set(interruptedTools)],
|
|
145
|
+
rejectedAsks: [...new Set(rejectedAsks)],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
isSessionActive(sessionId: string): boolean {
|
|
150
|
+
const context = this.sessionContexts.get(sessionId);
|
|
151
|
+
return context !== undefined && !context.controller.signal.aborted;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
isSessionInterrupted(sessionId: string): boolean {
|
|
155
|
+
const context = this.sessionContexts.get(sessionId);
|
|
156
|
+
return context?.controller.signal.aborted ?? false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
unregisterSession(sessionId: string): void {
|
|
160
|
+
const context = this.sessionContexts.get(sessionId);
|
|
161
|
+
if (context) {
|
|
162
|
+
// Remove this session's abort listener from parent's signal
|
|
163
|
+
if (context.parentAbortListener && context.parentId) {
|
|
164
|
+
const parentContext = this.sessionContexts.get(context.parentId);
|
|
165
|
+
if (parentContext) {
|
|
166
|
+
parentContext.controller.signal.removeEventListener('abort', context.parentAbortListener);
|
|
167
|
+
parentContext.childSessionIds.delete(sessionId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Remove all tool execution listeners from this session's signal
|
|
172
|
+
for (const { listener } of context.toolControllers.values()) {
|
|
173
|
+
context.controller.signal.removeEventListener('abort', listener);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.sessionContexts.delete(sessionId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
unregisterToolExecution(sessionId: string, toolCallId: string): void {
|
|
181
|
+
const context = this.sessionContexts.get(sessionId);
|
|
182
|
+
if (context) {
|
|
183
|
+
const toolExec = context.toolControllers.get(toolCallId);
|
|
184
|
+
if (toolExec) {
|
|
185
|
+
context.controller.signal.removeEventListener('abort', toolExec.listener);
|
|
186
|
+
}
|
|
187
|
+
context.toolControllers.delete(toolCallId);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export const interruptManager = new InterruptManager();
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { MessageWithParts, CompactionPart, ImagePart, FilePart } from '@capekai/types';
|
|
2
|
+
import type { ModelMessage } from 'ai';
|
|
3
|
+
import { isTextPart, isToolPart, isImagePart, isFilePart, parseToolInput } from './part-utils';
|
|
4
|
+
import { stripVisualization } from '../utils/strip-visualization';
|
|
5
|
+
import { getAttachment } from '../storage/runtime';
|
|
6
|
+
import { isToolOutputArtifactReference, RETRIEVE_TOOL_OUTPUT_NAME } from '../tool-output/policy';
|
|
7
|
+
|
|
8
|
+
type AiSdkContent = string | Array<{
|
|
9
|
+
type: 'text' | 'tool-call' | 'tool-result' | 'image' | 'file';
|
|
10
|
+
text?: string;
|
|
11
|
+
toolCallId?: string;
|
|
12
|
+
toolName?: string;
|
|
13
|
+
input?: unknown;
|
|
14
|
+
value?: unknown;
|
|
15
|
+
output?: unknown;
|
|
16
|
+
image?: URL | Uint8Array;
|
|
17
|
+
data?: URL | Uint8Array;
|
|
18
|
+
mediaType?: string;
|
|
19
|
+
filename?: string;
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
interface ModelCapabilities {
|
|
23
|
+
input?: {
|
|
24
|
+
text?: boolean;
|
|
25
|
+
image?: boolean;
|
|
26
|
+
video?: boolean;
|
|
27
|
+
file?: string[];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function resolveAttachmentPath(part: ImagePart | FilePart): Promise<{ absolutePath: string; mimeType: string } | null> {
|
|
32
|
+
const urlWithoutQuery = part.url.split('?')[0];
|
|
33
|
+
const match = urlWithoutQuery.match(/^\/api\/sessions\/([^/]+)\/attachments\/([^/]+)\/content$/);
|
|
34
|
+
if (!match) return null;
|
|
35
|
+
|
|
36
|
+
const [, sessionId, attachmentId] = match;
|
|
37
|
+
try {
|
|
38
|
+
const attachment = await getAttachment(sessionId, attachmentId);
|
|
39
|
+
if (!attachment) return null;
|
|
40
|
+
return { absolutePath: attachment.absolutePath, mimeType: attachment.mimeType };
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function convertToAiSdkMessages(
|
|
47
|
+
messages: MessageWithParts[],
|
|
48
|
+
modelCapabilities?: ModelCapabilities,
|
|
49
|
+
): Promise<ModelMessage[]> {
|
|
50
|
+
const result: { role: 'user' | 'assistant' | 'system' | 'tool'; content: AiSdkContent }[] = [];
|
|
51
|
+
|
|
52
|
+
for (const msgWithParts of messages) {
|
|
53
|
+
const msg = msgWithParts.message;
|
|
54
|
+
const parts = msgWithParts.parts;
|
|
55
|
+
|
|
56
|
+
const textBlocks: string[] = [];
|
|
57
|
+
const toolCallBlocks: Array<{
|
|
58
|
+
type: 'tool-call';
|
|
59
|
+
toolCallId: string;
|
|
60
|
+
toolName: string;
|
|
61
|
+
input: unknown;
|
|
62
|
+
}> = [];
|
|
63
|
+
const toolResultBlocks: Array<{
|
|
64
|
+
type: 'tool-result';
|
|
65
|
+
toolCallId: string;
|
|
66
|
+
toolName: string;
|
|
67
|
+
output: unknown;
|
|
68
|
+
}> = [];
|
|
69
|
+
const imageParts: ImagePart[] = [];
|
|
70
|
+
const fileParts: FilePart[] = [];
|
|
71
|
+
|
|
72
|
+
const hasCompactionTrigger = parts.some(p => p.type === 'compaction');
|
|
73
|
+
|
|
74
|
+
if (msg.role === 'assistant' && (msg.mode === 'compact_failed' || msg.mode === 'retry_failed')) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const part of parts) {
|
|
79
|
+
if (isTextPart(part)) {
|
|
80
|
+
textBlocks.push(part.text);
|
|
81
|
+
} else if (part.type === 'compaction') {
|
|
82
|
+
const compactionPart = part as CompactionPart;
|
|
83
|
+
if (compactionPart.overflow) {
|
|
84
|
+
textBlocks.push('Continue from where we left off, summarizing what we did so far.');
|
|
85
|
+
} else {
|
|
86
|
+
textBlocks.push('What did we do so far?');
|
|
87
|
+
}
|
|
88
|
+
} else if (isToolPart(part)) {
|
|
89
|
+
const toolPart = part;
|
|
90
|
+
|
|
91
|
+
toolCallBlocks.push({
|
|
92
|
+
type: 'tool-call' as const,
|
|
93
|
+
toolCallId: toolPart.callId,
|
|
94
|
+
toolName: toolPart.name,
|
|
95
|
+
input: parseToolInput(toolPart.state.input),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (toolPart.state.status === 'completed') {
|
|
99
|
+
const isCompacted = !!(toolPart.state as { compactedAt?: number }).compactedAt;
|
|
100
|
+
const isSkillTool = toolPart.name === 'skill';
|
|
101
|
+
|
|
102
|
+
if (isCompacted && !isSkillTool) {
|
|
103
|
+
const output = stripVisualization(toolPart.state.output);
|
|
104
|
+
const value = isToolOutputArtifactReference(output)
|
|
105
|
+
? {
|
|
106
|
+
...output,
|
|
107
|
+
preview: '[Old tool result content cleared]',
|
|
108
|
+
message: `[Old tool result content cleared] Exact output remains available with ${RETRIEVE_TOOL_OUTPUT_NAME} using artifactId ${output.artifactId}.`,
|
|
109
|
+
}
|
|
110
|
+
: '[Old tool result content cleared]';
|
|
111
|
+
toolResultBlocks.push({
|
|
112
|
+
type: 'tool-result' as const,
|
|
113
|
+
toolCallId: toolPart.callId,
|
|
114
|
+
toolName: toolPart.name,
|
|
115
|
+
output: isToolOutputArtifactReference(output)
|
|
116
|
+
? { type: 'json' as const, value }
|
|
117
|
+
: { type: 'text' as const, value },
|
|
118
|
+
});
|
|
119
|
+
} else {
|
|
120
|
+
toolResultBlocks.push({
|
|
121
|
+
type: 'tool-result' as const,
|
|
122
|
+
toolCallId: toolPart.callId,
|
|
123
|
+
toolName: toolPart.name,
|
|
124
|
+
output: { type: 'json' as const, value: stripVisualization(toolPart.state.output) },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
} else if (toolPart.state.status === 'error') {
|
|
128
|
+
toolResultBlocks.push({
|
|
129
|
+
type: 'tool-result' as const,
|
|
130
|
+
toolCallId: toolPart.callId,
|
|
131
|
+
toolName: toolPart.name,
|
|
132
|
+
output: { type: 'text' as const, value: JSON.stringify(stripVisualization({ error: toolPart.state.error })) },
|
|
133
|
+
});
|
|
134
|
+
} else {
|
|
135
|
+
// Handle 'pending', 'running', or 'interrupted' tool states
|
|
136
|
+
// These can occur when a session was interrupted or crashed mid-execution.
|
|
137
|
+
// Synthesize an error result so the AI SDK gets a valid tool-result for every tool-call.
|
|
138
|
+
const statusLabel = toolPart.state.status === 'interrupted'
|
|
139
|
+
? 'interrupted'
|
|
140
|
+
: `${toolPart.state.status} (session interrupted)`;
|
|
141
|
+
toolResultBlocks.push({
|
|
142
|
+
type: 'tool-result' as const,
|
|
143
|
+
toolCallId: toolPart.callId,
|
|
144
|
+
toolName: toolPart.name,
|
|
145
|
+
output: { type: 'text' as const, value: JSON.stringify({ error: `Tool execution was ${statusLabel}` }) },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
} else if (isImagePart(part)) {
|
|
149
|
+
if (modelCapabilities?.input?.image) {
|
|
150
|
+
imageParts.push(part);
|
|
151
|
+
} else {
|
|
152
|
+
const attachmentRecord = await resolveAttachmentPath(part);
|
|
153
|
+
const fallbackText = attachmentRecord
|
|
154
|
+
? `User attached an image.\nFile path: ${attachmentRecord.absolutePath}\nIf the image contents matter, ask the user to describe it or inspect the file if your runtime supports access to that path.`
|
|
155
|
+
: 'User attached an image (file path unavailable).';
|
|
156
|
+
textBlocks.push(fallbackText);
|
|
157
|
+
}
|
|
158
|
+
} else if (isFilePart(part)) {
|
|
159
|
+
if (modelCapabilities?.input?.file && modelCapabilities.input.file.includes(part.mimeType)) {
|
|
160
|
+
fileParts.push(part);
|
|
161
|
+
} else {
|
|
162
|
+
const attachmentRecord = await resolveAttachmentPath(part);
|
|
163
|
+
const fallbackText = attachmentRecord
|
|
164
|
+
? `User attached a file: ${part.filename || 'unnamed'} (type: ${part.mimeType}).\nFile path: ${attachmentRecord.absolutePath}\nIf the file contents matter, ask the user to describe it or inspect the file if your runtime supports access to that path.`
|
|
165
|
+
: `User attached a file: ${part.filename || 'unnamed'} (type: ${part.mimeType}).`;
|
|
166
|
+
textBlocks.push(fallbackText);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const hasText = textBlocks.length > 0;
|
|
172
|
+
const hasToolCalls = toolCallBlocks.length > 0;
|
|
173
|
+
const hasImages = imageParts.length > 0;
|
|
174
|
+
const hasFiles = fileParts.length > 0;
|
|
175
|
+
const hasContentParts = hasText || hasImages || hasFiles || hasToolCalls;
|
|
176
|
+
|
|
177
|
+
if (!hasContentParts) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const contentParts: Array<{
|
|
182
|
+
type: 'text' | 'tool-call' | 'image' | 'file';
|
|
183
|
+
text?: string;
|
|
184
|
+
toolCallId?: string;
|
|
185
|
+
toolName?: string;
|
|
186
|
+
input?: unknown;
|
|
187
|
+
image?: URL | Uint8Array;
|
|
188
|
+
data?: URL | Uint8Array;
|
|
189
|
+
mimeType?: string;
|
|
190
|
+
mediaType?: string;
|
|
191
|
+
filename?: string;
|
|
192
|
+
}> = [];
|
|
193
|
+
|
|
194
|
+
if (hasText) {
|
|
195
|
+
contentParts.push({ type: 'text', text: textBlocks.join('\n\n') });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const imgPart of imageParts) {
|
|
199
|
+
const resolved = await resolveAttachmentPath(imgPart);
|
|
200
|
+
if (resolved) {
|
|
201
|
+
try {
|
|
202
|
+
const file = Bun.file(resolved.absolutePath);
|
|
203
|
+
const buffer = await file.arrayBuffer();
|
|
204
|
+
contentParts.push({ type: 'image', image: new Uint8Array(buffer), mimeType: resolved.mimeType });
|
|
205
|
+
} catch {
|
|
206
|
+
// skip if file can't be read
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const filePart of fileParts) {
|
|
212
|
+
const resolved = await resolveAttachmentPath(filePart);
|
|
213
|
+
if (resolved) {
|
|
214
|
+
try {
|
|
215
|
+
const file = Bun.file(resolved.absolutePath);
|
|
216
|
+
const buffer = await file.arrayBuffer();
|
|
217
|
+
contentParts.push({
|
|
218
|
+
type: 'file',
|
|
219
|
+
data: new Uint8Array(buffer),
|
|
220
|
+
mediaType: resolved.mimeType,
|
|
221
|
+
filename: filePart.filename || 'unnamed',
|
|
222
|
+
});
|
|
223
|
+
} catch {
|
|
224
|
+
// skip if file can't be read
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
for (const toolCall of toolCallBlocks) {
|
|
230
|
+
contentParts.push({
|
|
231
|
+
type: 'tool-call',
|
|
232
|
+
toolCallId: toolCall.toolCallId,
|
|
233
|
+
toolName: toolCall.toolName,
|
|
234
|
+
input: toolCall.input,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!hasToolCalls && !hasImages && !hasFiles) {
|
|
239
|
+
const content = textBlocks.join('\n\n');
|
|
240
|
+
result.push({
|
|
241
|
+
role: hasCompactionTrigger ? 'user' : (msg.role as 'user' | 'assistant' | 'system'),
|
|
242
|
+
content,
|
|
243
|
+
});
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
result.push({
|
|
248
|
+
role: msg.role as 'user' | 'assistant' | 'system',
|
|
249
|
+
content: contentParts,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
for (const toolResult of toolResultBlocks) {
|
|
253
|
+
result.push({
|
|
254
|
+
role: 'tool' as const,
|
|
255
|
+
content: [toolResult],
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return result as unknown as ModelMessage[];
|
|
261
|
+
}
|