@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capekai/core",
3
- "version": "1.0.4",
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.2",
81
- "@capekai/types": "^1.0.1",
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)
@@ -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
 
@@ -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 workspacePath = workspace?.path;
556
- const additionalPaths = workspace?.additionalPaths;
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 workspacePath = workspace?.path;
814
- const additionalPaths = workspace?.additionalPaths;
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: { type: 'json' as const, value: stripVisualization(toolPart.state.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 (typeof delta.output === 'string') {
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 { BroadcastFn } from '../../runtime/host-dependencies';
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 { definition } = loadedTool;
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
- if (result.visualization && result.result && typeof result.result === 'object') {
125
- return { ...result.result as Record<string, unknown>, _visualization: result.visualization };
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);
@@ -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 { getRuntimeHost } from './host';
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';
@@ -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 = scopedHost.getStore() ?? host;
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
- // Resolve additionalPaths from workspace
63
- const workspace = workspaceId ? await getWorkspace(workspaceId) : null;
64
- const additionalPaths = workspace?.additionalPaths;
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
+ }