@meistrari/agent-core 0.0.0 → 0.1.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.
Files changed (83) hide show
  1. package/README.md +24 -3
  2. package/bin/supervisor.ts +116 -0
  3. package/package.json +42 -3
  4. package/scripts/build-supervisor-executable.ts +32 -0
  5. package/src/agents/agent-error-serializer.ts +23 -0
  6. package/src/agents/agent-event-stream.ts +81 -0
  7. package/src/agents/agent-id.ts +17 -0
  8. package/src/agents/agent-operation.ts +11 -0
  9. package/src/agents/agent-provider.ts +37 -0
  10. package/src/agents/agent-run.ts +32 -0
  11. package/src/agents/agent-runtime-error.ts +161 -0
  12. package/src/agents/agent-session-events.ts +32 -0
  13. package/src/agents/agent-tool-runner.ts +83 -0
  14. package/src/agents/agent-tool.ts +111 -0
  15. package/src/agents/author-context.ts +36 -0
  16. package/src/agents/claude/claude-command-mapper.ts +234 -0
  17. package/src/agents/claude/claude-event-mapper.ts +736 -0
  18. package/src/agents/claude/claude-provider.ts +191 -0
  19. package/src/agents/claude/claude-run.ts +464 -0
  20. package/src/agents/claude/claude-tool-mapper.ts +186 -0
  21. package/src/agents/claude/index.ts +1 -0
  22. package/src/agents/codex/codex-auth.ts +34 -0
  23. package/src/agents/codex/codex-command-mapper.ts +78 -0
  24. package/src/agents/codex/codex-event-mapper.ts +708 -0
  25. package/src/agents/codex/codex-json-rpc-client.ts +326 -0
  26. package/src/agents/codex/codex-protocol.ts +36 -0
  27. package/src/agents/codex/codex-provider.ts +1050 -0
  28. package/src/agents/codex/codex-run.ts +404 -0
  29. package/src/agents/codex/codex-skill-catalog.ts +158 -0
  30. package/src/agents/codex/codex-skill-roots.ts +19 -0
  31. package/src/agents/codex/codex-tool-mapper.ts +55 -0
  32. package/src/agents/codex/codex.errors.ts +68 -0
  33. package/src/agents/codex/generated/meta.gen.ts +606 -0
  34. package/src/agents/codex/generated/namespaces.gen.ts +311 -0
  35. package/src/agents/codex/generated/schema.gen.ts +34883 -0
  36. package/src/agents/codex/index.ts +2 -0
  37. package/src/agents/index.ts +14 -0
  38. package/src/agents/input-attachment-preparation.ts +86 -0
  39. package/src/agents/input-attachment.errors.ts +16 -0
  40. package/src/agents/instructions.ts +8 -0
  41. package/src/agents/materialized-input-attachment.ts +26 -0
  42. package/src/agents/message-id.ts +23 -0
  43. package/src/agents/normalize.ts +8 -0
  44. package/src/agents/sandbox-environment.ts +1 -0
  45. package/src/agents/tools/ping.tool.ts +13 -0
  46. package/src/agents/user-input-request.ts +470 -0
  47. package/src/provenance.gen.ts +3 -3
  48. package/src/supervisor/agent-provider-factory.ts +189 -0
  49. package/src/supervisor/bootstrap-binder.ts +125 -0
  50. package/src/supervisor/config.ts +49 -0
  51. package/src/supervisor/control-authority-verifier.ts +135 -0
  52. package/src/supervisor/create-supervisor-runtime.ts +25 -0
  53. package/src/supervisor/errors.ts +24 -0
  54. package/src/supervisor/index.ts +34 -0
  55. package/src/supervisor/persistence/json.ts +21 -0
  56. package/src/supervisor/persistence/state-discovery.ts +56 -0
  57. package/src/supervisor/persistence/supervisor-store.ts +364 -0
  58. package/src/supervisor/ports/index.ts +109 -0
  59. package/src/supervisor/provider-factory.ts +37 -0
  60. package/src/supervisor/resident.ts +143 -0
  61. package/src/supervisor/rpc-client.ts +120 -0
  62. package/src/supervisor/runtime-handler.ts +309 -0
  63. package/src/supervisor/websocket-server.ts +434 -0
  64. package/src/supervisor-protocol/bootstrap.ts +1 -1
  65. package/src/template-onboarding.ts +47 -0
  66. package/src/testing/es256-test-keys.ts +73 -0
  67. package/src/testing/in-memory-runtime-control-plane.ts +205 -0
  68. package/src/testing/index.ts +6 -0
  69. package/src/testing/loopback-supervisor-connection.ts +71 -0
  70. package/src/testing/scripted-provider.ts +64 -0
  71. package/src/worker-runtime-client/command-pump.ts +132 -0
  72. package/src/worker-runtime-client/connection-attempt.ts +340 -0
  73. package/src/worker-runtime-client/control-authority-signer.ts +100 -0
  74. package/src/worker-runtime-client/e2b-supervisor-connection.ts +102 -0
  75. package/src/worker-runtime-client/frame-processor.ts +178 -0
  76. package/src/worker-runtime-client/index.ts +27 -0
  77. package/src/worker-runtime-client/lease-reconciler.ts +14 -0
  78. package/src/worker-runtime-client/ports.ts +137 -0
  79. package/src/worker-runtime-client/postgres-notification-listener.ts +91 -0
  80. package/src/worker-runtime-client/rpc-dispatcher.ts +27 -0
  81. package/src/worker-runtime-client/rpc-request-manager.ts +141 -0
  82. package/src/worker-runtime-client/sandbox-connection-runtime.ts +300 -0
  83. package/src/worker-runtime-client/token-crypto.ts +46 -0
@@ -0,0 +1,32 @@
1
+ import type { AgentReasoningEffort, AgentToolDefinition, SessionConfiguredAgentEvent } from '../protocol'
2
+ import type { AgentEventDraft } from './agent-event-stream'
3
+
4
+ type SessionConfiguredPayload = SessionConfiguredAgentEvent['payload']
5
+ type SessionConfiguredEventDraft = Extract<AgentEventDraft, { type: 'session.configured' }>
6
+
7
+ export type SessionConfiguredEventInput = Pick<SessionConfiguredPayload, 'model' | 'cwd'> & {
8
+ reasoningEffort?: AgentReasoningEffort
9
+ tools?: readonly AgentToolDefinition[]
10
+ toolNames?: readonly SessionConfiguredPayload['tools'][number][]
11
+ environment: Record<string, string | undefined>
12
+ }
13
+
14
+ export function sessionConfiguredEvent(input: SessionConfiguredEventInput): SessionConfiguredEventDraft {
15
+ const payload: SessionConfiguredPayload = {
16
+ model: input.model,
17
+ cwd: input.cwd,
18
+ approvalMode: 'full-access',
19
+ tools: [...(input.toolNames ?? (input.tools ?? []).map(tool => tool.name))],
20
+ reasoningEffort: input.reasoningEffort,
21
+ environment: redactedEnvironment(input.environment),
22
+ }
23
+
24
+ return {
25
+ type: 'session.configured',
26
+ payload,
27
+ }
28
+ }
29
+
30
+ export function redactedEnvironment(environment: Record<string, string | undefined>): SessionConfiguredPayload['environment'] {
31
+ return Object.fromEntries(Object.keys(environment).sort().map(key => [key, '[REDACTED]']))
32
+ }
@@ -0,0 +1,83 @@
1
+ import type { AppError } from '../errors'
2
+ import type { AgentToolResult, JsonValue } from '../protocol'
3
+ import type { AgentTool, AgentToolActor, AgentToolContext } from './agent-tool'
4
+ import { isAppError } from '../errors'
5
+ import { AgentOperationAbortedError, AgentToolValidationError } from './agent-runtime-error'
6
+ import { agentToolActorDenialMessage, agentToolAllowsActor } from './agent-tool'
7
+
8
+ export async function runAgentTool(input: {
9
+ tool: AgentTool
10
+ actor: AgentToolActor
11
+ rawInput: JsonValue
12
+ context: AgentToolContext
13
+ }): Promise<AgentToolResult> {
14
+ if (!agentToolAllowsActor(input.tool, input.actor))
15
+ throw new AgentToolValidationError({ message: agentToolActorDenialMessage(input.tool) })
16
+
17
+ const parsed = input.tool.inputSchema.safeParse(input.rawInput)
18
+ if (!parsed.success) {
19
+ return {
20
+ success: false,
21
+ content: [{ type: 'text', text: parsed.error.message }],
22
+ }
23
+ }
24
+
25
+ const abortController = new AbortController()
26
+ const abort = (): void => abortController.abort()
27
+ const timeout = setTimeout(abort, input.tool.timeoutMs)
28
+ input.context.signal.addEventListener('abort', abort, { once: true })
29
+ if (input.context.signal.aborted)
30
+ abort()
31
+
32
+ try {
33
+ const result = await Promise.race([
34
+ input.tool.execute({
35
+ input: parsed.data,
36
+ context: {
37
+ ...input.context,
38
+ signal: abortController.signal,
39
+ },
40
+ }),
41
+ waitForAbort(abortController.signal, input.tool.name, input.tool.timeoutMs),
42
+ ])
43
+
44
+ validateToolResult(result)
45
+ return result
46
+ }
47
+ catch (error) {
48
+ if (!isAppError(error))
49
+ throw error
50
+ return appErrorToolResult(error)
51
+ }
52
+ finally {
53
+ clearTimeout(timeout)
54
+ input.context.signal.removeEventListener('abort', abort)
55
+ }
56
+ }
57
+
58
+ async function waitForAbort(signal: AbortSignal, toolName: string, timeoutMs: number): Promise<never> {
59
+ const abortedError = new AgentOperationAbortedError({
60
+ message: `Tool ${toolName} exceeded ${timeoutMs}ms or was aborted.`,
61
+ details: { toolName, timeoutMs },
62
+ })
63
+ if (signal.aborted)
64
+ throw abortedError
65
+
66
+ return await new Promise((_, reject) => {
67
+ signal.addEventListener('abort', () => reject(abortedError), { once: true })
68
+ })
69
+ }
70
+
71
+ function appErrorToolResult(error: AppError): AgentToolResult {
72
+ const serializedDetails = error.details === undefined ? '' : `\n\nDetails:\n${JSON.stringify(error.details, null, 2)}`
73
+ return {
74
+ success: false,
75
+ content: [{ type: 'text', text: `${error.publicMessage}${serializedDetails}` }],
76
+ }
77
+ }
78
+
79
+ function validateToolResult(result: AgentToolResult): void {
80
+ if (typeof result.success !== 'boolean' || !Array.isArray(result.content)) {
81
+ throw new AgentToolValidationError({ message: 'Tool result must include success and content array.' })
82
+ }
83
+ }
@@ -0,0 +1,111 @@
1
+ import type { ZodRawShape } from 'zod'
2
+ import type { AgentProviderId, AgentToolDefinition, AgentToolResult, AgentUserInputQuestion } from '../protocol'
3
+ import { z } from 'zod'
4
+ import { jsonObjectSchema } from '../protocol'
5
+ import { AgentToolValidationError } from './agent-runtime-error'
6
+
7
+ export interface AgentToolContext {
8
+ provider: AgentProviderId
9
+ sessionId: string
10
+ providerSessionId: string
11
+ turnId: string
12
+ toolCallId: string
13
+ cwd: string
14
+ signal: AbortSignal
15
+ requestUserInput: (input: { prompt: string, questions: AgentUserInputQuestion[] }) => Promise<{ requestId: string }>
16
+ }
17
+
18
+ export interface AgentToolExecuteInput<TShape extends ZodRawShape = ZodRawShape> {
19
+ input: z.infer<z.ZodObject<TShape>>
20
+ context: AgentToolContext
21
+ }
22
+
23
+ export type AgentToolActor = 'main' | 'subagent'
24
+ export type AgentToolActorScope = 'all' | 'main'
25
+
26
+ export interface AgentTool {
27
+ readonly name: string
28
+ readonly description: string
29
+ readonly actorScope: AgentToolActorScope
30
+ readonly inputShape: ZodRawShape
31
+ readonly inputSchema: z.ZodObject<ZodRawShape>
32
+ readonly timeoutMs: number
33
+ execute: (input: AgentToolExecuteInput) => Promise<AgentToolResult>
34
+ definition: () => AgentToolDefinition
35
+ }
36
+
37
+ export interface CreateAgentToolInput<TShape extends ZodRawShape> {
38
+ name: string
39
+ description: string
40
+ actorScope?: AgentToolActorScope
41
+ inputSchema: z.ZodObject<TShape>
42
+ timeoutMs?: number
43
+ execute: (input: AgentToolExecuteInput<TShape>) => Promise<AgentToolResult>
44
+ }
45
+
46
+ const DEFAULT_TOOL_TIMEOUT_MS = 60_000
47
+ const TOOL_NAME_PATTERN = /^[\w-]{1,128}$/
48
+
49
+ export function tool<TShape extends ZodRawShape>(input: CreateAgentToolInput<TShape>): AgentTool {
50
+ if (!TOOL_NAME_PATTERN.test(input.name)) {
51
+ throw new AgentToolValidationError({ message: `Invalid tool name: ${input.name}` })
52
+ }
53
+
54
+ if (!input.description.trim()) {
55
+ throw new AgentToolValidationError({ message: `Tool ${input.name} must have a description.` })
56
+ }
57
+
58
+ const timeoutMs = input.timeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS
59
+ const inputShape = input.inputSchema.shape
60
+
61
+ return {
62
+ name: input.name,
63
+ description: input.description,
64
+ actorScope: input.actorScope ?? 'all',
65
+ inputShape,
66
+ inputSchema: input.inputSchema,
67
+ timeoutMs,
68
+ execute: async ({ input: rawInput, context }) => await input.execute({
69
+ input: input.inputSchema.parse(rawInput),
70
+ context,
71
+ }),
72
+ definition: () => ({
73
+ name: input.name,
74
+ description: input.description,
75
+ inputSchema: jsonObjectSchema.parse(z.toJSONSchema(input.inputSchema)),
76
+ }),
77
+ }
78
+ }
79
+
80
+ export function toolDefinitions(tools: readonly AgentTool[]): AgentToolDefinition[] {
81
+ return tools.map(runtimeTool => runtimeTool.definition())
82
+ }
83
+
84
+ export function selectAgentTools(tools: readonly AgentTool[], definitions: readonly AgentToolDefinition[] | undefined): readonly AgentTool[] {
85
+ if (!definitions)
86
+ return tools
87
+
88
+ const selectedNames = new Set(definitions.map(definition => definition.name))
89
+ return tools.filter(runtimeTool => selectedNames.has(runtimeTool.name))
90
+ }
91
+
92
+ export function agentToolAllowsActor(tool: AgentTool, actor: AgentToolActor): boolean {
93
+ return tool.actorScope === 'all' || actor === 'main'
94
+ }
95
+
96
+ export function agentToolActorDenialMessage(tool: AgentTool): string {
97
+ return `${tool.name} is available only to the main agent.`
98
+ }
99
+
100
+ export function toolByName(tools: readonly AgentTool[]): Map<string, AgentTool> {
101
+ const byName = new Map<string, AgentTool>()
102
+
103
+ for (const runtimeTool of tools) {
104
+ if (byName.has(runtimeTool.name)) {
105
+ throw new AgentToolValidationError({ message: `Duplicate tool name: ${runtimeTool.name}` })
106
+ }
107
+ byName.set(runtimeTool.name, runtimeTool)
108
+ }
109
+
110
+ return byName
111
+ }
@@ -0,0 +1,36 @@
1
+ import type { AgentAuthor } from '../protocol'
2
+ import type { AgentSenderContext } from './agent-operation'
3
+
4
+ const OPEN_TAG = '<coding-agent-context>'
5
+ const CLOSE_TAG = '</coding-agent-context>'
6
+
7
+ const AUTHOR_CONTEXT_PATTERN = /^<coding-agent-context>[\s\S]*?<\/coding-agent-context>\n*/
8
+ const slackMentionPattern = /^<@([\w-]+)(?:\|[^>]*)?>$/u
9
+
10
+ export function renderAuthorContext(author: AgentAuthor, senderContext: AgentSenderContext = { kind: 'application' }): string {
11
+ const attributes = [
12
+ `name="${escapeXmlAttribute(author.name)}"`,
13
+ author.email ? `email="${escapeXmlAttribute(author.email)}"` : undefined,
14
+ author.gitEmail ? `git-email="${escapeXmlAttribute(author.gitEmail)}"` : undefined,
15
+ ].filter((attribute): attribute is string => attribute !== undefined)
16
+ const providerUserId = senderContext.kind === 'slack'
17
+ ? slackMentionPattern.exec(senderContext.mention)?.[1]
18
+ : undefined
19
+ const sender = providerUserId
20
+ ? ` <sender ${attributes.join(' ')}>\n <mention kind="user" provider="slack" provider_user_id="${escapeXmlAttribute(providerUserId)}"/>\n </sender>`
21
+ : ` <sender ${attributes.join(' ')} />`
22
+ return `${OPEN_TAG}\n${sender}\n${CLOSE_TAG}`
23
+ }
24
+
25
+ export function stripAuthorContext(text: string): string {
26
+ return text.replace(AUTHOR_CONTEXT_PATTERN, '')
27
+ }
28
+
29
+ function escapeXmlAttribute(value: string): string {
30
+ return value
31
+ .replaceAll('&', '&amp;')
32
+ .replaceAll('<', '&lt;')
33
+ .replaceAll('>', '&gt;')
34
+ .replaceAll('"', '&quot;')
35
+ .replaceAll('\'', '&apos;')
36
+ }
@@ -0,0 +1,234 @@
1
+ import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk'
2
+ import type { MessageParam } from '@anthropic-ai/sdk/resources'
3
+ import type { AgentAuthor, AgentInputBlock, AgentPrompt } from '../../protocol'
4
+ import type { AgentSenderContext } from '../agent-operation'
5
+ import type { AgentInputAttachmentPreparation, AgentInputAttachmentProjection, MaterializedAgentInputAttachment } from '../materialized-input-attachment'
6
+ import { Buffer } from 'node:buffer'
7
+ import { AgentUnsupportedPromptBlockError } from '../agent-runtime-error'
8
+ import { renderAuthorContext } from '../author-context'
9
+ import { filesystemFallbackLabel, nativeAttachmentLabel, nativeImageMimeType, readAttachmentBytes } from '../input-attachment-preparation'
10
+ import { deriveMessageId } from '../message-id'
11
+
12
+ const claudeNativeImageEncodedByteLimit = 10_000_000
13
+ const claudeNativeCurrentMessageByteLimit = 24_000_000
14
+ const pdfSignature = '%PDF-'
15
+ const pdfSignatureStartLimit = 1024
16
+
17
+ type ClaudeContentBlock = Exclude<MessageParam['content'], string>[number]
18
+ type ClaudeImageMimeType = 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp'
19
+ interface ClaudeUserMessageInput {
20
+ prompt: AgentPrompt
21
+ mode?: 'next' | 'steer'
22
+ commandId: string
23
+ origin?: 'user' | 'supervisor'
24
+ author?: AgentAuthor
25
+ senderContext?: AgentSenderContext
26
+ }
27
+ type ClaudeAttachmentProjection
28
+ = | {
29
+ attachment: MaterializedAgentInputAttachment
30
+ route: 'native'
31
+ nativeKind: 'image' | 'document'
32
+ nativeBlock: ClaudeContentBlock
33
+ }
34
+ | {
35
+ attachment: MaterializedAgentInputAttachment
36
+ route: 'filesystem_fallback'
37
+ }
38
+
39
+ export function toClaudeUserMessage(input: ClaudeUserMessageInput): SDKUserMessage {
40
+ return claudeUserMessage(input, claudeMessageContent(input))
41
+ }
42
+
43
+ export async function mapClaudeUserMessage(input: ClaudeUserMessageInput, preparation?: AgentInputAttachmentPreparation): Promise<SDKUserMessage> {
44
+ return preparation
45
+ ? await prepareClaudeUserMessage({ ...input, preparation })
46
+ : toClaudeUserMessage(input)
47
+ }
48
+
49
+ async function prepareClaudeUserMessage(input: ClaudeUserMessageInput & { preparation: AgentInputAttachmentPreparation }): Promise<SDKUserMessage> {
50
+ input.preparation.signal.throwIfAborted()
51
+ const originalContent = claudeMessageContent(input)
52
+ const projections: ClaudeAttachmentProjection[] = []
53
+
54
+ for (const attachment of input.preparation.attachments) {
55
+ const bytes = await readAttachmentBytes(attachment, input.preparation.signal)
56
+ projections.push(claudeAttachmentProjection(attachment, bytes))
57
+ }
58
+
59
+ demoteForCurrentMessageBudget(originalContent, projections)
60
+ const content = [...originalContent, ...projections.flatMap(claudeAttachmentBlocks)]
61
+ const records = projections.map(toProjectionRecord)
62
+ input.preparation.recordInputAttachmentProjections(records)
63
+ return claudeUserMessage(input, content)
64
+ }
65
+
66
+ function claudeUserMessage(input: ClaudeUserMessageInput, content: ClaudeContentBlock[]): SDKUserMessage {
67
+ const { mode = 'next', commandId, origin = 'user' } = input
68
+ return {
69
+ type: 'user',
70
+ message: {
71
+ role: 'user',
72
+ content,
73
+ },
74
+ parent_tool_use_id: null,
75
+ priority: mode === 'steer' ? 'now' : 'next',
76
+ uuid: deriveMessageId(commandId),
77
+ ...(origin === 'supervisor' ? { isSynthetic: true, origin: { kind: 'coordinator' as const } } : {}),
78
+ }
79
+ }
80
+
81
+ function claudeMessageContent({ prompt, origin = 'user', author, senderContext }: ClaudeUserMessageInput): ClaudeContentBlock[] {
82
+ const blocks = prompt.blocks.map(toClaudeContentBlock)
83
+ return origin === 'user' && author
84
+ ? [{ type: 'text', text: renderAuthorContext(author, senderContext) }, ...blocks]
85
+ : blocks
86
+ }
87
+
88
+ function claudeAttachmentProjection(attachment: MaterializedAgentInputAttachment, bytes: Buffer): ClaudeAttachmentProjection {
89
+ const imageMimeType = nativeImageMimeType(attachment.mediaType, bytes)
90
+ if (imageMimeType && encodedBase64ByteSize(attachment.byteSize) <= claudeNativeImageEncodedByteLimit) {
91
+ return {
92
+ attachment,
93
+ route: 'native',
94
+ nativeKind: 'image',
95
+ nativeBlock: {
96
+ type: 'image',
97
+ source: {
98
+ type: 'base64',
99
+ media_type: imageMimeType,
100
+ data: bytes.toString('base64'),
101
+ },
102
+ },
103
+ }
104
+ }
105
+
106
+ if (attachment.mediaType === 'application/pdf') {
107
+ const signatureOffset = bytes
108
+ .subarray(0, pdfSignatureStartLimit + pdfSignature.length)
109
+ .indexOf(pdfSignature)
110
+ if (signatureOffset >= 0) {
111
+ return {
112
+ attachment,
113
+ route: 'native',
114
+ nativeKind: 'document',
115
+ nativeBlock: {
116
+ type: 'document',
117
+ title: attachment.filename,
118
+ source: {
119
+ type: 'base64',
120
+ media_type: 'application/pdf',
121
+ data: bytes.toString('base64'),
122
+ },
123
+ },
124
+ }
125
+ }
126
+ }
127
+
128
+ if (attachment.mediaType === 'text/plain' || attachment.mediaType === 'text/markdown') {
129
+ const text = decodeNonemptyUtf8(bytes)
130
+ if (text !== undefined) {
131
+ return {
132
+ attachment,
133
+ route: 'native',
134
+ nativeKind: 'document',
135
+ nativeBlock: {
136
+ type: 'document',
137
+ title: attachment.filename,
138
+ source: {
139
+ type: 'text',
140
+ media_type: 'text/plain',
141
+ data: text,
142
+ },
143
+ },
144
+ }
145
+ }
146
+ }
147
+
148
+ return { attachment, route: 'filesystem_fallback' }
149
+ }
150
+
151
+ function demoteForCurrentMessageBudget(originalContent: readonly ClaudeContentBlock[], projections: ClaudeAttachmentProjection[]): void {
152
+ while (serializedContentByteSize(originalContent, projections) > claudeNativeCurrentMessageByteLimit) {
153
+ const index = projections.findLastIndex(projection => projection.route === 'native')
154
+ if (index < 0)
155
+ return
156
+ const projection = projections[index]!
157
+ projections[index] = { attachment: projection.attachment, route: 'filesystem_fallback' }
158
+ }
159
+ }
160
+
161
+ function serializedContentByteSize(originalContent: readonly ClaudeContentBlock[], projections: readonly ClaudeAttachmentProjection[]): number {
162
+ const content = [...originalContent, ...projections.flatMap(claudeAttachmentBlocks)]
163
+ return Buffer.byteLength(JSON.stringify(content))
164
+ }
165
+
166
+ function claudeAttachmentBlocks(projection: ClaudeAttachmentProjection): ClaudeContentBlock[] {
167
+ if (projection.route === 'filesystem_fallback')
168
+ return [{ type: 'text', text: filesystemFallbackLabel(projection.attachment) }]
169
+ return [
170
+ { type: 'text', text: nativeAttachmentLabel(projection.attachment) },
171
+ projection.nativeBlock,
172
+ ]
173
+ }
174
+
175
+ function toProjectionRecord(projection: ClaudeAttachmentProjection): AgentInputAttachmentProjection {
176
+ const { attachment } = projection
177
+ if (projection.route === 'filesystem_fallback') {
178
+ return {
179
+ attachmentId: attachment.attachmentId,
180
+ ordinal: attachment.ordinal,
181
+ sha256: attachment.sha256,
182
+ route: 'filesystem_fallback',
183
+ }
184
+ }
185
+ return {
186
+ attachmentId: attachment.attachmentId,
187
+ ordinal: attachment.ordinal,
188
+ sha256: attachment.sha256,
189
+ route: 'native',
190
+ nativeKind: projection.nativeKind,
191
+ }
192
+ }
193
+
194
+ function decodeNonemptyUtf8(bytes: Uint8Array): string | undefined {
195
+ const text = new TextDecoder('utf-8', { ignoreBOM: true }).decode(bytes)
196
+ if (text.length === 0)
197
+ return undefined
198
+ const encoded = new TextEncoder().encode(text)
199
+ if (encoded.length !== bytes.length || !encoded.every((byte, index) => byte === bytes[index]))
200
+ return undefined
201
+ return text
202
+ }
203
+
204
+ function encodedBase64ByteSize(byteSize: number): number {
205
+ return 4 * Math.ceil(byteSize / 3)
206
+ }
207
+
208
+ function toClaudeContentBlock(block: AgentInputBlock): ClaudeContentBlock {
209
+ if (block.type === 'text')
210
+ return { type: 'text', text: block.text }
211
+
212
+ if (block.type === 'image' && block.source.kind === 'base64') {
213
+ return {
214
+ type: 'image',
215
+ source: {
216
+ type: 'base64',
217
+ media_type: toClaudeImageMimeType(block.source.mimeType),
218
+ data: block.source.data,
219
+ },
220
+ }
221
+ }
222
+
223
+ if (block.type === 'file' && block.source.kind === 'text') {
224
+ return { type: 'text', text: `<file name="${block.source.name ?? 'file'}">\n${block.source.text}\n</file>` }
225
+ }
226
+
227
+ throw new AgentUnsupportedPromptBlockError({ provider: 'claude', blockType: block.type })
228
+ }
229
+
230
+ function toClaudeImageMimeType(mimeType: string): ClaudeImageMimeType {
231
+ if (mimeType === 'image/jpeg' || mimeType === 'image/png' || mimeType === 'image/gif' || mimeType === 'image/webp')
232
+ return mimeType
233
+ throw new AgentUnsupportedPromptBlockError({ provider: 'claude', blockType: `image:${mimeType}` })
234
+ }