@capekai/core 1.0.4 → 1.0.6
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/package.json +3 -3
- package/src/core/agent.ts +1 -0
- package/src/core/build-tools.ts +3 -0
- package/src/core/chat-handler.ts +19 -4
- package/src/core/message-utils.ts +4 -1
- package/src/core/stream-handlers.ts +6 -1
- package/src/core/tool-builders/external-tools.ts +20 -7
- package/src/internal/hosts.ts +2 -0
- package/src/runtime/host-dependencies.ts +34 -1
- package/src/runtime/host.ts +29 -2
- package/src/subagent/child-session.ts +18 -5
- package/src/subagent/task-tool.ts +1 -0
- package/src/tool-output/policy.ts +11 -0
- package/src/tools/model-output.ts +75 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capekai/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Bun-native composable agent runtime and framework for Capek.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -77,8 +77,8 @@
|
|
|
77
77
|
"dependencies": {
|
|
78
78
|
"@ai-sdk/deepseek": "^2.0.35",
|
|
79
79
|
"@ai-sdk/openai": "^3.0.84",
|
|
80
|
-
"@capekai/tool": "^1.0.
|
|
81
|
-
"@capekai/types": "^1.0.
|
|
80
|
+
"@capekai/tool": "^1.0.3",
|
|
81
|
+
"@capekai/types": "^1.0.3",
|
|
82
82
|
"@openrouter/ai-sdk-provider": "^2.3.3",
|
|
83
83
|
"@zip.js/zip.js": "^2.7.60",
|
|
84
84
|
"ai": "^6.0.116",
|
package/src/core/agent.ts
CHANGED
|
@@ -121,6 +121,7 @@ export async function* streamChat(options: ChatOptions): AsyncGenerator<MessageE
|
|
|
121
121
|
broadcastFn: options.broadcastFn,
|
|
122
122
|
additionalPaths: effectiveAdditionalPaths,
|
|
123
123
|
agentId: preconfig.id,
|
|
124
|
+
workspaceRootId: session?.workspaceRootId ?? undefined,
|
|
124
125
|
});
|
|
125
126
|
|
|
126
127
|
const selfDelegationAvailable = Boolean(aiTools.task)
|
package/src/core/build-tools.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface BuildToolsOptions {
|
|
|
33
33
|
broadcastFn?: AskBroadcastFn;
|
|
34
34
|
additionalPaths?: string[];
|
|
35
35
|
agentId?: string | null;
|
|
36
|
+
workspaceRootId?: string;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export async function buildAiSdkTools(
|
|
@@ -53,6 +54,7 @@ export async function buildAiSdkTools(
|
|
|
53
54
|
broadcastFn,
|
|
54
55
|
additionalPaths,
|
|
55
56
|
agentId,
|
|
57
|
+
workspaceRootId,
|
|
56
58
|
} = options;
|
|
57
59
|
|
|
58
60
|
// Resolve root session ID by walking up the parent chain
|
|
@@ -92,6 +94,7 @@ export async function buildAiSdkTools(
|
|
|
92
94
|
modelId,
|
|
93
95
|
providerId,
|
|
94
96
|
additionalPaths,
|
|
97
|
+
workspaceRootId,
|
|
95
98
|
});
|
|
96
99
|
const tools: ToolMap = { ...externalTools };
|
|
97
100
|
|
package/src/core/chat-handler.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
isDefaultSessionTitle,
|
|
7
7
|
isSandboxActive,
|
|
8
8
|
emitTerminal,
|
|
9
|
+
resolveSessionWorkspace,
|
|
9
10
|
} from '../runtime/host-dependencies';
|
|
10
11
|
import { getDefaultPreconfig, getPreconfigOrAgent } from '../context';
|
|
11
12
|
import {
|
|
@@ -552,8 +553,15 @@ export async function handleChat<Origin>(
|
|
|
552
553
|
}
|
|
553
554
|
|
|
554
555
|
const workspace = session.workspaceId ? await getWorkspace(session.workspaceId) : null;
|
|
555
|
-
const
|
|
556
|
-
|
|
556
|
+
const workspaceContext = await resolveSessionWorkspace({
|
|
557
|
+
sessionId,
|
|
558
|
+
workspaceId: session.workspaceId || undefined,
|
|
559
|
+
workspaceRootId: session.workspaceRootId ?? undefined,
|
|
560
|
+
workspacePath: workspace?.path,
|
|
561
|
+
additionalPaths: workspace?.additionalPaths,
|
|
562
|
+
});
|
|
563
|
+
const workspacePath = workspaceContext.workspacePath;
|
|
564
|
+
const additionalPaths = workspaceContext.additionalPaths;
|
|
557
565
|
|
|
558
566
|
const preconfig = session.preconfigId
|
|
559
567
|
? await getPreconfigOrAgent(session.preconfigId)
|
|
@@ -810,8 +818,15 @@ export async function handleSessionEditMessage<Origin>(
|
|
|
810
818
|
});
|
|
811
819
|
|
|
812
820
|
const workspace = session.workspaceId ? await getWorkspace(session.workspaceId) : null;
|
|
813
|
-
const
|
|
814
|
-
|
|
821
|
+
const workspaceContext = await resolveSessionWorkspace({
|
|
822
|
+
sessionId: msg.sessionId,
|
|
823
|
+
workspaceId: session.workspaceId || undefined,
|
|
824
|
+
workspaceRootId: session.workspaceRootId ?? undefined,
|
|
825
|
+
workspacePath: workspace?.path,
|
|
826
|
+
additionalPaths: workspace?.additionalPaths,
|
|
827
|
+
});
|
|
828
|
+
const workspacePath = workspaceContext.workspacePath;
|
|
829
|
+
const additionalPaths = workspaceContext.additionalPaths;
|
|
815
830
|
|
|
816
831
|
const preconfig = session.preconfigId
|
|
817
832
|
? await getPreconfigOrAgent(session.preconfigId)
|
|
@@ -4,6 +4,7 @@ import { isTextPart, isToolPart, isImagePart, isFilePart, parseToolInput } from
|
|
|
4
4
|
import { stripVisualization } from '../utils/strip-visualization';
|
|
5
5
|
import { getAttachment } from '../storage/runtime';
|
|
6
6
|
import { isToolOutputArtifactReference, RETRIEVE_TOOL_OUTPUT_NAME } from '../tool-output/policy';
|
|
7
|
+
import { toolModelOutputToAiSdk } from '../tools/model-output';
|
|
7
8
|
|
|
8
9
|
type AiSdkContent = string | Array<{
|
|
9
10
|
type: 'text' | 'tool-call' | 'tool-result' | 'image' | 'file';
|
|
@@ -121,7 +122,9 @@ export async function convertToAiSdkMessages(
|
|
|
121
122
|
type: 'tool-result' as const,
|
|
122
123
|
toolCallId: toolPart.callId,
|
|
123
124
|
toolName: toolPart.name,
|
|
124
|
-
output:
|
|
125
|
+
output: toolPart.state.modelOutput
|
|
126
|
+
? toolModelOutputToAiSdk(toolPart.state.modelOutput)
|
|
127
|
+
: { type: 'json' as const, value: stripVisualization(toolPart.state.output) },
|
|
125
128
|
});
|
|
126
129
|
}
|
|
127
130
|
} else if (toolPart.state.status === 'error') {
|
|
@@ -2,6 +2,7 @@ import type { TextPart, ToolPart, ReasoningPart, MessageEvent } from '@capekai/t
|
|
|
2
2
|
import { createPart, updatePart, getPart, persistStreamingPartSnapshots } from '../storage/runtime';
|
|
3
3
|
import { parseToolInput } from './part-utils';
|
|
4
4
|
import { randomUUID } from 'crypto';
|
|
5
|
+
import { isCapekToolOutputEnvelope } from '../tools/model-output';
|
|
5
6
|
|
|
6
7
|
const STREAM_PART_PERSIST_INTERVAL_MS = 300;
|
|
7
8
|
|
|
@@ -184,8 +185,11 @@ export function createStreamHandlers(ctx: StreamHandlerContext) {
|
|
|
184
185
|
const latestPart = await getPart(existingToolPart.id) as ToolPart | null;
|
|
185
186
|
const latestState = latestPart?.state;
|
|
186
187
|
|
|
188
|
+
const capekOutput = isCapekToolOutputEnvelope(delta.output) ? delta.output : undefined;
|
|
187
189
|
let resultData: unknown;
|
|
188
|
-
if (
|
|
190
|
+
if (capekOutput) {
|
|
191
|
+
resultData = capekOutput.value;
|
|
192
|
+
} else if (typeof delta.output === 'string') {
|
|
189
193
|
try {
|
|
190
194
|
resultData = JSON.parse(delta.output);
|
|
191
195
|
} catch {
|
|
@@ -218,6 +222,7 @@ export function createStreamHandlers(ctx: StreamHandlerContext) {
|
|
|
218
222
|
status: 'completed' as const,
|
|
219
223
|
input: existingToolPart.state.input,
|
|
220
224
|
output: resultData,
|
|
225
|
+
...(capekOutput && { modelOutput: capekOutput.modelOutput }),
|
|
221
226
|
startedAt: Date.now(),
|
|
222
227
|
completedAt: Date.now(),
|
|
223
228
|
...(existingChildSessionId && { childSessionId: existingChildSessionId }),
|
|
@@ -19,7 +19,11 @@ import {
|
|
|
19
19
|
} from '../../runtime/domain-tool-source';
|
|
20
20
|
import { isToolAllowedInContext, type ToolExecutionScope } from '../tool-capabilities';
|
|
21
21
|
import type { ToolMap } from './types';
|
|
22
|
-
import type
|
|
22
|
+
import { resolveToolDefinition, type BroadcastFn } from '../../runtime/host-dependencies';
|
|
23
|
+
import {
|
|
24
|
+
capekToolOutputToAiSdk,
|
|
25
|
+
createCapekToolOutputEnvelope,
|
|
26
|
+
} from '../../tools/model-output';
|
|
23
27
|
|
|
24
28
|
export interface ExternalToolsOptions {
|
|
25
29
|
toolNames: string[];
|
|
@@ -35,6 +39,7 @@ export interface ExternalToolsOptions {
|
|
|
35
39
|
modelId?: string;
|
|
36
40
|
providerId?: string;
|
|
37
41
|
additionalPaths?: string[];
|
|
42
|
+
workspaceRootId?: string;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export async function buildExternalTools(options: ExternalToolsOptions): Promise<ToolMap> {
|
|
@@ -52,6 +57,7 @@ export async function buildExternalTools(options: ExternalToolsOptions): Promise
|
|
|
52
57
|
modelId,
|
|
53
58
|
providerId,
|
|
54
59
|
additionalPaths,
|
|
60
|
+
workspaceRootId,
|
|
55
61
|
} = options;
|
|
56
62
|
|
|
57
63
|
const tools: ToolMap = {};
|
|
@@ -79,7 +85,14 @@ export async function buildExternalTools(options: ExternalToolsOptions): Promise
|
|
|
79
85
|
const loadedTool = await getTool(name);
|
|
80
86
|
if (!loadedTool) continue;
|
|
81
87
|
|
|
82
|
-
const
|
|
88
|
+
const definition = await resolveToolDefinition({
|
|
89
|
+
sessionId,
|
|
90
|
+
workspaceId,
|
|
91
|
+
workspaceRootId,
|
|
92
|
+
workspacePath,
|
|
93
|
+
definition: loadedTool.definition,
|
|
94
|
+
});
|
|
95
|
+
if (!definition) continue;
|
|
83
96
|
|
|
84
97
|
if (!isToolAllowedInContext(definition.capabilities, executionScopes)) {
|
|
85
98
|
continue;
|
|
@@ -88,6 +101,7 @@ export async function buildExternalTools(options: ExternalToolsOptions): Promise
|
|
|
88
101
|
tools[name] = tool({
|
|
89
102
|
description: definition.description,
|
|
90
103
|
inputSchema: jsonSchema(definition.inputSchema),
|
|
104
|
+
toModelOutput: ({ output }) => capekToolOutputToAiSdk(output),
|
|
91
105
|
execute: async (args: Record<string, unknown>, { toolCallId }: { toolCallId: string }) => {
|
|
92
106
|
const toolAbortController = interruptManager.registerToolExecution(sessionId, toolCallId);
|
|
93
107
|
|
|
@@ -121,11 +135,10 @@ export async function buildExternalTools(options: ExternalToolsOptions): Promise
|
|
|
121
135
|
return { error: result.error ?? 'Tool execution failed' };
|
|
122
136
|
}
|
|
123
137
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return result.result;
|
|
138
|
+
const clientResult = result.visualization && result.result && typeof result.result === 'object'
|
|
139
|
+
? { ...result.result as Record<string, unknown>, _visualization: result.visualization }
|
|
140
|
+
: result.result;
|
|
141
|
+
return createCapekToolOutputEnvelope(clientResult, result.modelOutput);
|
|
129
142
|
} finally {
|
|
130
143
|
interruptManager.unregisterToolExecution(sessionId, toolCallId);
|
|
131
144
|
await rejectPendingAsksByToolCallId(toolCallId);
|
package/src/internal/hosts.ts
CHANGED
|
@@ -22,7 +22,9 @@ export {
|
|
|
22
22
|
type PermissionRequestStatus,
|
|
23
23
|
type RuntimeHost,
|
|
24
24
|
type SandboxBindings,
|
|
25
|
+
type SessionWorkspaceContext,
|
|
25
26
|
type TitleHost,
|
|
27
|
+
type ToolPolicyHost,
|
|
26
28
|
type WorkspaceCapabilityBindings,
|
|
27
29
|
} from '../runtime/host';
|
|
28
30
|
export { createStandaloneHost } from '../runtime/standalone-host';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Session } from '@capekai/types';
|
|
2
|
-
import {
|
|
2
|
+
import type { ToolDefinition } from '@capekai/tool';
|
|
3
|
+
import { getOptionalRuntimeHost, getRuntimeHost } from './host';
|
|
3
4
|
import type { RuntimeAudience, RuntimeDelivery, RuntimeEvent } from './events';
|
|
4
5
|
|
|
5
6
|
type HostRuntimeAudience = Exclude<RuntimeAudience, { scope: 'origin' }>;
|
|
@@ -66,6 +67,38 @@ export const generateSessionTitle = (...args: Parameters<ReturnType<typeof getRu
|
|
|
66
67
|
getRuntimeHost().titles.generateSessionTitle(...args);
|
|
67
68
|
export const getToolWorkspaceHost = (...args: Parameters<ReturnType<typeof getRuntimeHost>['workspace']['createToolWorkspaceHost']>) =>
|
|
68
69
|
getRuntimeHost().workspace.createToolWorkspaceHost(...args);
|
|
70
|
+
|
|
71
|
+
export async function resolveSessionWorkspace(options: {
|
|
72
|
+
sessionId: string;
|
|
73
|
+
workspaceId?: string;
|
|
74
|
+
workspaceRootId?: string;
|
|
75
|
+
workspacePath?: string;
|
|
76
|
+
additionalPaths?: string[];
|
|
77
|
+
}): Promise<{ workspacePath?: string; additionalPaths?: string[] }> {
|
|
78
|
+
const resolver = getRuntimeHost().workspace.resolveSessionWorkspace;
|
|
79
|
+
if (resolver) {
|
|
80
|
+
return await resolver(options);
|
|
81
|
+
}
|
|
82
|
+
if (options.workspaceRootId) {
|
|
83
|
+
throw new Error('Session workspace root requires a host resolver');
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
workspacePath: options.workspacePath,
|
|
87
|
+
additionalPaths: options.additionalPaths,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function resolveToolDefinition(options: {
|
|
92
|
+
sessionId: string;
|
|
93
|
+
workspaceId?: string;
|
|
94
|
+
workspaceRootId?: string;
|
|
95
|
+
workspacePath?: string;
|
|
96
|
+
definition: ToolDefinition;
|
|
97
|
+
}): Promise<ToolDefinition | null> {
|
|
98
|
+
const resolver = getOptionalRuntimeHost()?.toolPolicy?.resolveDefinition;
|
|
99
|
+
return resolver ? await resolver(options) : options.definition;
|
|
100
|
+
}
|
|
101
|
+
|
|
69
102
|
export const isSandboxActive = (): boolean => getRuntimeHost().sandbox.isSandboxActive();
|
|
70
103
|
|
|
71
104
|
export type { RuntimeEventSink, RuntimeEventSink as BroadcastFn } from './events';
|
package/src/runtime/host.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import type { HostLayout } from './host-layout';
|
|
3
|
-
import type { Ask } from '@capekai/tool';
|
|
3
|
+
import type { Ask, ToolDefinition } from '@capekai/tool';
|
|
4
4
|
import type {
|
|
5
5
|
AskRequestMessage, AskTimedOutMessage, AutoApproveSeverity, MessageWithParts, Session,
|
|
6
6
|
} from '@capekai/types';
|
|
@@ -82,7 +82,19 @@ export interface TitleHost {
|
|
|
82
82
|
generateSessionTitle(messages: MessageWithParts[]): Promise<string | null>;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
export interface SessionWorkspaceContext {
|
|
86
|
+
workspacePath?: string;
|
|
87
|
+
additionalPaths?: string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
85
90
|
export interface WorkspaceCapabilityBindings {
|
|
91
|
+
resolveSessionWorkspace?(options: {
|
|
92
|
+
sessionId: string;
|
|
93
|
+
workspaceId?: string;
|
|
94
|
+
workspaceRootId?: string;
|
|
95
|
+
workspacePath?: string;
|
|
96
|
+
additionalPaths?: string[];
|
|
97
|
+
}): SessionWorkspaceContext | Promise<SessionWorkspaceContext>;
|
|
86
98
|
createToolWorkspaceHost(options: {
|
|
87
99
|
workspaceId?: string;
|
|
88
100
|
workspacePath?: string;
|
|
@@ -91,6 +103,16 @@ export interface WorkspaceCapabilityBindings {
|
|
|
91
103
|
}): WorkspaceCapabilityHost;
|
|
92
104
|
}
|
|
93
105
|
|
|
106
|
+
export interface ToolPolicyHost {
|
|
107
|
+
resolveDefinition?(options: {
|
|
108
|
+
sessionId: string;
|
|
109
|
+
workspaceId?: string;
|
|
110
|
+
workspaceRootId?: string;
|
|
111
|
+
workspacePath?: string;
|
|
112
|
+
definition: ToolDefinition;
|
|
113
|
+
}): ToolDefinition | null | Promise<ToolDefinition | null>;
|
|
114
|
+
}
|
|
115
|
+
|
|
94
116
|
export interface SandboxBindings {
|
|
95
117
|
isSandboxActive(): boolean;
|
|
96
118
|
}
|
|
@@ -100,6 +122,7 @@ export interface RuntimeHost {
|
|
|
100
122
|
delivery: DeliveryHost;
|
|
101
123
|
titles: TitleHost;
|
|
102
124
|
workspace: WorkspaceCapabilityBindings;
|
|
125
|
+
toolPolicy?: ToolPolicyHost;
|
|
103
126
|
sandbox: SandboxBindings;
|
|
104
127
|
/** Host-supplied filesystem layout policy. */
|
|
105
128
|
layout?: HostLayout;
|
|
@@ -122,8 +145,12 @@ export function withRuntimeHost<T>(value: RuntimeHost, callback: () => T): T {
|
|
|
122
145
|
return scopedHost.run(value, callback);
|
|
123
146
|
}
|
|
124
147
|
|
|
148
|
+
export function getOptionalRuntimeHost(): RuntimeHost | null {
|
|
149
|
+
return scopedHost.getStore() ?? host;
|
|
150
|
+
}
|
|
151
|
+
|
|
125
152
|
export function getRuntimeHost(): RuntimeHost {
|
|
126
|
-
const active =
|
|
153
|
+
const active = getOptionalRuntimeHost();
|
|
127
154
|
if (!active) throw new Error('Runtime host has not been configured');
|
|
128
155
|
return active;
|
|
129
156
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
emitTerminal,
|
|
6
6
|
emitToAskTargets,
|
|
7
7
|
emitToController,
|
|
8
|
+
resolveSessionWorkspace,
|
|
8
9
|
} from '../runtime/host-dependencies';
|
|
9
10
|
import { getLLMSubagentMaxSteps } from '../configuration/runtime';
|
|
10
11
|
import {
|
|
@@ -59,9 +60,21 @@ export async function executeChildSession(options: {
|
|
|
59
60
|
streamChat = streamChatWithRetry,
|
|
60
61
|
} = options;
|
|
61
62
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
// Re-resolve the child session's host-owned root. Async child execution may
|
|
64
|
+
// enter after the parent context was assembled, so the persisted child
|
|
65
|
+
// binding is authoritative.
|
|
66
|
+
const childSession = await getSession(childSessionId);
|
|
67
|
+
const effectiveWorkspaceId = workspaceId ?? childSession?.workspaceId ?? undefined;
|
|
68
|
+
const workspace = effectiveWorkspaceId ? await getWorkspace(effectiveWorkspaceId) : null;
|
|
69
|
+
const workspaceContext = await resolveSessionWorkspace({
|
|
70
|
+
sessionId: childSessionId,
|
|
71
|
+
workspaceId: effectiveWorkspaceId,
|
|
72
|
+
workspaceRootId: childSession?.workspaceRootId ?? undefined,
|
|
73
|
+
workspacePath: workspacePath ?? workspace?.path,
|
|
74
|
+
additionalPaths: workspace?.additionalPaths,
|
|
75
|
+
});
|
|
76
|
+
const effectiveWorkspacePath = workspaceContext.workspacePath;
|
|
77
|
+
const additionalPaths = workspaceContext.additionalPaths;
|
|
65
78
|
|
|
66
79
|
let messages: MessageWithParts[];
|
|
67
80
|
|
|
@@ -172,8 +185,8 @@ export async function executeChildSession(options: {
|
|
|
172
185
|
sessionId: childSessionId,
|
|
173
186
|
preconfig,
|
|
174
187
|
messages,
|
|
175
|
-
workspacePath,
|
|
176
|
-
workspaceId,
|
|
188
|
+
workspacePath: effectiveWorkspacePath,
|
|
189
|
+
workspaceId: effectiveWorkspaceId,
|
|
177
190
|
additionalPaths,
|
|
178
191
|
modelId: modelId ?? undefined,
|
|
179
192
|
providerId: providerId ?? undefined,
|
|
@@ -429,6 +429,7 @@ async function runSubagent(
|
|
|
429
429
|
childSession = await createSessionFn({
|
|
430
430
|
id: randomUUID(),
|
|
431
431
|
workspaceId: workspaceId || parentSession?.workspaceId || '',
|
|
432
|
+
workspaceRootId: parentSession?.workspaceRootId ?? null,
|
|
432
433
|
preconfigId: subagent_type,
|
|
433
434
|
title: `${description} (@${subagent_type} subagent)`,
|
|
434
435
|
status: 'active',
|
|
@@ -34,6 +34,10 @@ import type {
|
|
|
34
34
|
ToolOutputPolicyContext,
|
|
35
35
|
ToolOutputPolicyOptions,
|
|
36
36
|
} from './contracts';
|
|
37
|
+
import {
|
|
38
|
+
isCapekToolOutputEnvelope,
|
|
39
|
+
type CapekToolOutputEnvelope,
|
|
40
|
+
} from '../tools/model-output';
|
|
37
41
|
|
|
38
42
|
export const TOOL_OUTPUT_THRESHOLD_CHARS = 50_000;
|
|
39
43
|
export const TOOL_OUTPUT_PREVIEW_CHARS = 10_000;
|
|
@@ -116,6 +120,13 @@ export function createToolOutputService(
|
|
|
116
120
|
const outputPolicyWrappedTools = new WeakSet<object>();
|
|
117
121
|
|
|
118
122
|
async function applyToolOutputPolicy(result: unknown, context: ToolOutputPolicyContext): Promise<unknown> {
|
|
123
|
+
if (isCapekToolOutputEnvelope(result)) {
|
|
124
|
+
return {
|
|
125
|
+
...result,
|
|
126
|
+
value: await applyToolOutputPolicy(result.value, context),
|
|
127
|
+
} satisfies CapekToolOutputEnvelope;
|
|
128
|
+
}
|
|
129
|
+
|
|
119
130
|
const visualization = result && typeof result === 'object' && !Array.isArray(result)
|
|
120
131
|
? (result as Record<string, unknown>)._visualization
|
|
121
132
|
: undefined;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { JSONValue, Tool } from 'ai';
|
|
2
|
+
import type { ToolModelOutputPart } from '@capekai/types';
|
|
3
|
+
|
|
4
|
+
type AiSdkToolResultOutput = Awaited<ReturnType<NonNullable<Tool['toModelOutput']>>>;
|
|
5
|
+
|
|
6
|
+
const CAPEK_TOOL_OUTPUT_TYPE = 'capek-tool-output';
|
|
7
|
+
|
|
8
|
+
export interface CapekToolOutputEnvelope {
|
|
9
|
+
type: typeof CAPEK_TOOL_OUTPUT_TYPE;
|
|
10
|
+
value: unknown;
|
|
11
|
+
modelOutput: ToolModelOutputPart[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isToolModelOutputPart(value: unknown): value is ToolModelOutputPart {
|
|
15
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
16
|
+
const part = value as Record<string, unknown>;
|
|
17
|
+
if (part.type === 'text') {
|
|
18
|
+
return typeof part.text === 'string';
|
|
19
|
+
}
|
|
20
|
+
if (part.type === 'image') {
|
|
21
|
+
return typeof part.data === 'string'
|
|
22
|
+
&& part.data.length > 0
|
|
23
|
+
&& !part.data.startsWith('data:')
|
|
24
|
+
&& typeof part.mediaType === 'string'
|
|
25
|
+
&& part.mediaType.startsWith('image/');
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeToolModelOutput(value: unknown): ToolModelOutputPart[] | undefined {
|
|
31
|
+
if (!Array.isArray(value) || value.length === 0 || !value.every(isToolModelOutputPart)) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createCapekToolOutputEnvelope(
|
|
38
|
+
value: unknown,
|
|
39
|
+
modelOutput: unknown,
|
|
40
|
+
): unknown {
|
|
41
|
+
const normalized = normalizeToolModelOutput(modelOutput);
|
|
42
|
+
return normalized
|
|
43
|
+
? { type: CAPEK_TOOL_OUTPUT_TYPE, value, modelOutput: normalized }
|
|
44
|
+
: value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isCapekToolOutputEnvelope(value: unknown): value is CapekToolOutputEnvelope {
|
|
48
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
49
|
+
const record = value as Record<string, unknown>;
|
|
50
|
+
return record.type === CAPEK_TOOL_OUTPUT_TYPE
|
|
51
|
+
&& 'value' in record
|
|
52
|
+
&& normalizeToolModelOutput(record.modelOutput) !== undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function toolModelOutputToAiSdk(parts: ToolModelOutputPart[]): AiSdkToolResultOutput {
|
|
56
|
+
return {
|
|
57
|
+
type: 'content',
|
|
58
|
+
value: parts.map((part) => part.type === 'text'
|
|
59
|
+
? { type: 'text' as const, text: part.text }
|
|
60
|
+
: {
|
|
61
|
+
type: 'image-data' as const,
|
|
62
|
+
data: part.data,
|
|
63
|
+
mediaType: part.mediaType,
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function capekToolOutputToAiSdk(output: unknown): AiSdkToolResultOutput {
|
|
69
|
+
if (isCapekToolOutputEnvelope(output)) {
|
|
70
|
+
return toolModelOutputToAiSdk(output.modelOutput);
|
|
71
|
+
}
|
|
72
|
+
return typeof output === 'string'
|
|
73
|
+
? { type: 'text', value: output }
|
|
74
|
+
: { type: 'json', value: (output ?? null) as JSONValue };
|
|
75
|
+
}
|