@ai-sdk/harness-claude-code 1.0.158 → 1.0.159

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": "@ai-sdk/harness-claude-code",
3
- "version": "1.0.158",
3
+ "version": "1.0.159",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -26,9 +26,9 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@ai-sdk/harness": "1.0.154",
30
- "@ai-sdk/provider": "4.0.29",
31
- "@ai-sdk/provider-utils": "5.0.62",
29
+ "@ai-sdk/harness": "1.0.155",
30
+ "@ai-sdk/provider": "4.0.30",
31
+ "@ai-sdk/provider-utils": "5.0.63",
32
32
  "ws": "^8.21.0"
33
33
  },
34
34
  "peerDependencies": {
@@ -843,7 +843,7 @@ function createQueryInput({
843
843
  userMessages,
844
844
  abortSignal,
845
845
  }: {
846
- initialUserMessage: string;
846
+ initialUserMessage: StartMessage['prompt'];
847
847
  userMessages: Experimental_BridgeUserMessageQueue;
848
848
  abortSignal: AbortSignal;
849
849
  }): {
@@ -882,14 +882,28 @@ function createQueryInput({
882
882
  }
883
883
 
884
884
  const toUserMessage = (options: {
885
- text: string;
885
+ content: StartMessage['prompt'];
886
886
  messageId: string;
887
887
  priority?: 'next';
888
888
  }): unknown => ({
889
889
  type: 'user',
890
890
  message: {
891
891
  role: 'user',
892
- content: [{ type: 'text', text: options.text }],
892
+ content:
893
+ typeof options.content === 'string'
894
+ ? [{ type: 'text', text: options.content }]
895
+ : options.content.map(part =>
896
+ part.type === 'text'
897
+ ? part
898
+ : {
899
+ type: 'image',
900
+ source: {
901
+ type: 'base64',
902
+ media_type: part.mediaType,
903
+ data: part.data,
904
+ },
905
+ },
906
+ ),
893
907
  },
894
908
  parent_tool_use_id: null,
895
909
  uuid: options.messageId,
@@ -963,7 +977,7 @@ function createQueryInput({
963
977
  sentInitial = true;
964
978
  return {
965
979
  value: toUserMessage({
966
- text: initialUserMessage,
980
+ content: initialUserMessage,
967
981
  messageId: initialMessageId,
968
982
  }),
969
983
  done: false,
@@ -983,7 +997,7 @@ function createQueryInput({
983
997
  sentMessageIds.add(nextMessage.value.messageId);
984
998
  return {
985
999
  value: toUserMessage({
986
- text: nextMessage.value.text,
1000
+ content: nextMessage.value.text,
987
1001
  messageId: nextMessage.value.messageId,
988
1002
  priority: 'next',
989
1003
  }),
@@ -30,7 +30,24 @@ const thinkingSchema = z.discriminatedUnion('type', [
30
30
  z.object({ type: z.literal('disabled') }),
31
31
  ]);
32
32
 
33
+ const promptSchema = z.union([
34
+ z.string(),
35
+ z.array(
36
+ z.discriminatedUnion('type', [
37
+ z.object({ type: z.literal('text'), text: z.string() }),
38
+ z.object({
39
+ type: z.literal('image'),
40
+ data: z.string(),
41
+ mediaType: z.string(),
42
+ }),
43
+ ]),
44
+ ),
45
+ ]);
46
+
47
+ export type PromptContent = z.infer<typeof promptSchema>;
48
+
33
49
  export const startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
50
+ prompt: promptSchema,
34
51
  instructions: z.string().optional(),
35
52
  thinking: thinkingSchema,
36
53
  effort: z.enum(['low', 'medium', 'high', 'xhigh', 'max']).optional(),
@@ -47,10 +47,18 @@ import {
47
47
  type SandboxChannelReconnectOptions,
48
48
  } from '@ai-sdk/harness/utils';
49
49
  import {
50
+ convertInlineFileDataToUint8Array,
51
+ convertUint8ArrayToBase64,
52
+ detectMediaType,
53
+ getTopLevelMediaType,
54
+ isFullMediaType,
55
+ isProviderReference,
50
56
  safeParseJSON,
51
57
  tool,
52
58
  type Experimental_SandboxSession as SandboxSession,
53
59
  type Experimental_SandboxProcess,
60
+ type FilePart,
61
+ type TextPart,
54
62
  } from '@ai-sdk/provider-utils';
55
63
  import { WebSocket } from 'ws';
56
64
  import { z } from 'zod/v4';
@@ -72,6 +80,7 @@ import {
72
80
  outboundMessageSchema,
73
81
  type InboundMessage,
74
82
  type OutboundMessage,
83
+ type PromptContent,
75
84
  } from './claude-code-bridge-protocol';
76
85
  import type { ClaudeCodeThinkingConfig } from './claude-code-thinking';
77
86
  import { VERSION } from './version';
@@ -1836,7 +1845,7 @@ function createSession({
1836
1845
 
1837
1846
  const startMessage = {
1838
1847
  type: 'start' as const,
1839
- prompt: extractUserText(promptOpts.prompt),
1848
+ prompt: convertUserPrompt(promptOpts.prompt),
1840
1849
  tools: (promptOpts.tools ?? []).map(t => ({
1841
1850
  name: t.name,
1842
1851
  description: t.description,
@@ -2149,25 +2158,110 @@ function createSession({
2149
2158
  }
2150
2159
 
2151
2160
  /*
2152
- * Reduce a `HarnessV1Prompt` to the plain user text the bridge forwards
2153
- * to the Claude SDK. File and image parts on the message are not yet
2154
- * supported by the underlying runtime — throw rather than silently drop
2155
- * them so callers learn about the gap instead of seeing mysteriously
2156
- * truncated prompts.
2161
+ * Reduce a `HarnessV1Prompt` to the prompt the bridge forwards to the Claude
2162
+ * SDK: plain text when every part is text, otherwise text and inline image
2163
+ * blocks. Remote URLs, provider references, and non-image files are not
2164
+ * supported: throw rather than silently drop them so callers learn about the
2165
+ * gap instead of seeing mysteriously truncated prompts.
2157
2166
  */
2158
- function extractUserText(prompt: HarnessV1Prompt): string {
2167
+ function convertUserPrompt(prompt: HarnessV1Prompt): PromptContent {
2159
2168
  if (typeof prompt === 'string') return prompt;
2160
2169
  const { content } = prompt;
2161
2170
  if (typeof content === 'string') return content;
2162
- const parts: string[] = [];
2163
- for (const part of content) {
2164
- if (part.type !== 'text') {
2165
- throw new HarnessCapabilityUnsupportedError({
2166
- harnessId: 'claude-code',
2167
- message: `The claude-code harness does not yet support user message parts of type '${part.type}'. Pass a string or a user message whose content contains only text parts.`,
2168
- });
2171
+ if (content.every((part): part is TextPart => part.type === 'text')) {
2172
+ return content.map(part => part.text).join('\n\n');
2173
+ }
2174
+ return content.map(part => {
2175
+ switch (part.type) {
2176
+ case 'text':
2177
+ return { type: 'text', text: part.text };
2178
+ case 'image':
2179
+ return convertUserImage({
2180
+ data: part.image,
2181
+ mediaType: part.mediaType ?? 'image',
2182
+ });
2183
+ case 'file':
2184
+ return convertUserImage(part);
2185
+ }
2186
+ });
2187
+ }
2188
+
2189
+ function convertUserImage({
2190
+ data,
2191
+ mediaType,
2192
+ }: {
2193
+ data: FilePart['data'];
2194
+ mediaType: string;
2195
+ }): Exclude<PromptContent, string>[number] {
2196
+ if (getTopLevelMediaType(mediaType) !== 'image') {
2197
+ throw unsupportedUserContent(`files of media type '${mediaType}'`);
2198
+ }
2199
+ const image = getInlineImage(data);
2200
+ const resolvedMediaType = isFullMediaType(mediaType)
2201
+ ? mediaType
2202
+ : (image.mediaType ??
2203
+ detectMediaType({ data: image.data, topLevelType: 'image' }));
2204
+ if (resolvedMediaType == null) {
2205
+ throw unsupportedUserContent('images whose media type cannot be detected');
2206
+ }
2207
+ return { type: 'image', data: image.data, mediaType: resolvedMediaType };
2208
+ }
2209
+
2210
+ function getInlineImage(data: FilePart['data']): {
2211
+ data: string;
2212
+ mediaType?: string;
2213
+ } {
2214
+ if (data instanceof URL) return getDataUrlImage(data);
2215
+ if (typeof data === 'string') {
2216
+ let url: URL | undefined;
2217
+ try {
2218
+ url = new URL(data);
2219
+ } catch {
2220
+ return { data };
2169
2221
  }
2170
- parts.push(part.text);
2222
+ return getDataUrlImage(url);
2171
2223
  }
2172
- return parts.join('\n\n');
2224
+ if (data instanceof Uint8Array || data instanceof ArrayBuffer) {
2225
+ return {
2226
+ data: convertUint8ArrayToBase64(
2227
+ convertInlineFileDataToUint8Array({ type: 'data', data }),
2228
+ ),
2229
+ };
2230
+ }
2231
+ if (isProviderReference(data)) {
2232
+ throw unsupportedUserContent('provider references');
2233
+ }
2234
+ switch (data.type) {
2235
+ case 'data':
2236
+ return typeof data.data === 'string'
2237
+ ? { data: data.data }
2238
+ : getInlineImage(data.data);
2239
+ case 'url':
2240
+ return getDataUrlImage(data.url);
2241
+ case 'reference':
2242
+ throw unsupportedUserContent('provider references');
2243
+ case 'text':
2244
+ throw unsupportedUserContent('text data for images');
2245
+ }
2246
+ }
2247
+
2248
+ function getDataUrlImage(url: URL): { data: string; mediaType?: string } {
2249
+ if (url.protocol !== 'data:') {
2250
+ throw unsupportedUserContent('remote image URLs');
2251
+ }
2252
+ const [header, data] = url.href.split(',', 2);
2253
+ if (!header.endsWith(';base64') || data == null) {
2254
+ throw unsupportedUserContent('data URLs that are not base64 encoded');
2255
+ }
2256
+ const mediaType = header.slice('data:'.length).split(';')[0];
2257
+ return { data, ...(mediaType ? { mediaType } : {}) };
2258
+ }
2259
+
2260
+ function unsupportedUserContent(
2261
+ description: string,
2262
+ ): HarnessCapabilityUnsupportedError {
2263
+ return new HarnessCapabilityUnsupportedError({
2264
+ harnessId: 'claude-code',
2265
+ message: `The claude-code harness does not support ${description} in user messages. Pass text parts and inline images as bytes, base64 strings, or data URLs.`,
2266
+ });
2173
2267
  }