@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.
- package/README.md +24 -3
- package/bin/supervisor.ts +116 -0
- package/package.json +42 -3
- package/scripts/build-supervisor-executable.ts +32 -0
- package/src/agents/agent-error-serializer.ts +23 -0
- package/src/agents/agent-event-stream.ts +81 -0
- package/src/agents/agent-id.ts +17 -0
- package/src/agents/agent-operation.ts +11 -0
- package/src/agents/agent-provider.ts +37 -0
- package/src/agents/agent-run.ts +32 -0
- package/src/agents/agent-runtime-error.ts +161 -0
- package/src/agents/agent-session-events.ts +32 -0
- package/src/agents/agent-tool-runner.ts +83 -0
- package/src/agents/agent-tool.ts +111 -0
- package/src/agents/author-context.ts +36 -0
- package/src/agents/claude/claude-command-mapper.ts +234 -0
- package/src/agents/claude/claude-event-mapper.ts +736 -0
- package/src/agents/claude/claude-provider.ts +191 -0
- package/src/agents/claude/claude-run.ts +464 -0
- package/src/agents/claude/claude-tool-mapper.ts +186 -0
- package/src/agents/claude/index.ts +1 -0
- package/src/agents/codex/codex-auth.ts +34 -0
- package/src/agents/codex/codex-command-mapper.ts +78 -0
- package/src/agents/codex/codex-event-mapper.ts +708 -0
- package/src/agents/codex/codex-json-rpc-client.ts +326 -0
- package/src/agents/codex/codex-protocol.ts +36 -0
- package/src/agents/codex/codex-provider.ts +1050 -0
- package/src/agents/codex/codex-run.ts +404 -0
- package/src/agents/codex/codex-skill-catalog.ts +158 -0
- package/src/agents/codex/codex-skill-roots.ts +19 -0
- package/src/agents/codex/codex-tool-mapper.ts +55 -0
- package/src/agents/codex/codex.errors.ts +68 -0
- package/src/agents/codex/generated/meta.gen.ts +606 -0
- package/src/agents/codex/generated/namespaces.gen.ts +311 -0
- package/src/agents/codex/generated/schema.gen.ts +34883 -0
- package/src/agents/codex/index.ts +2 -0
- package/src/agents/index.ts +14 -0
- package/src/agents/input-attachment-preparation.ts +86 -0
- package/src/agents/input-attachment.errors.ts +16 -0
- package/src/agents/instructions.ts +8 -0
- package/src/agents/materialized-input-attachment.ts +26 -0
- package/src/agents/message-id.ts +23 -0
- package/src/agents/normalize.ts +8 -0
- package/src/agents/sandbox-environment.ts +1 -0
- package/src/agents/tools/ping.tool.ts +13 -0
- package/src/agents/user-input-request.ts +470 -0
- package/src/provenance.gen.ts +3 -3
- package/src/supervisor/agent-provider-factory.ts +189 -0
- package/src/supervisor/bootstrap-binder.ts +125 -0
- package/src/supervisor/config.ts +49 -0
- package/src/supervisor/control-authority-verifier.ts +135 -0
- package/src/supervisor/create-supervisor-runtime.ts +25 -0
- package/src/supervisor/errors.ts +24 -0
- package/src/supervisor/index.ts +34 -0
- package/src/supervisor/persistence/json.ts +21 -0
- package/src/supervisor/persistence/state-discovery.ts +56 -0
- package/src/supervisor/persistence/supervisor-store.ts +364 -0
- package/src/supervisor/ports/index.ts +109 -0
- package/src/supervisor/provider-factory.ts +37 -0
- package/src/supervisor/resident.ts +143 -0
- package/src/supervisor/rpc-client.ts +120 -0
- package/src/supervisor/runtime-handler.ts +309 -0
- package/src/supervisor/websocket-server.ts +434 -0
- package/src/supervisor-protocol/bootstrap.ts +1 -1
- package/src/template-onboarding.ts +47 -0
- package/src/testing/es256-test-keys.ts +73 -0
- package/src/testing/in-memory-runtime-control-plane.ts +205 -0
- package/src/testing/index.ts +6 -0
- package/src/testing/loopback-supervisor-connection.ts +71 -0
- package/src/testing/scripted-provider.ts +64 -0
- package/src/worker-runtime-client/command-pump.ts +132 -0
- package/src/worker-runtime-client/connection-attempt.ts +340 -0
- package/src/worker-runtime-client/control-authority-signer.ts +100 -0
- package/src/worker-runtime-client/e2b-supervisor-connection.ts +102 -0
- package/src/worker-runtime-client/frame-processor.ts +178 -0
- package/src/worker-runtime-client/index.ts +27 -0
- package/src/worker-runtime-client/lease-reconciler.ts +14 -0
- package/src/worker-runtime-client/ports.ts +137 -0
- package/src/worker-runtime-client/postgres-notification-listener.ts +91 -0
- package/src/worker-runtime-client/rpc-dispatcher.ts +27 -0
- package/src/worker-runtime-client/rpc-request-manager.ts +141 -0
- package/src/worker-runtime-client/sandbox-connection-runtime.ts +300 -0
- 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('&', '&')
|
|
32
|
+
.replaceAll('<', '<')
|
|
33
|
+
.replaceAll('>', '>')
|
|
34
|
+
.replaceAll('"', '"')
|
|
35
|
+
.replaceAll('\'', ''')
|
|
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
|
+
}
|