@meistrari/agent-core 0.2.0-experimental.1 → 0.2.0-experimental.2
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 +50 -0
- package/docs/consumer-capability-matrix.md +374 -0
- package/package.json +17 -10
- package/scripts/generate-codex-protocol.ts +711 -0
- package/src/agents/agent-operation.ts +8 -0
- package/src/agents/agent-provider.ts +3 -0
- package/src/agents/agent-run.ts +14 -2
- package/src/agents/agent-runtime-error.ts +28 -0
- package/src/agents/agent-session-events.ts +33 -0
- package/src/agents/agent-tool-runner.ts +12 -11
- package/src/agents/agent-tool.ts +12 -4
- package/src/agents/author-context.ts +1 -1
- package/src/agents/claude/claude-auth.ts +40 -0
- package/src/agents/claude/claude-browser-turn-metadata.ts +70 -0
- package/src/agents/claude/claude-code-process.ts +98 -0
- package/src/agents/claude/claude-command-mapper.ts +63 -3
- package/src/agents/claude/claude-desktop.ts +55 -0
- package/src/agents/claude/claude-event-mapper.ts +864 -135
- package/src/agents/claude/claude-file-change.ts +128 -0
- package/src/agents/claude/claude-provider.ts +344 -29
- package/src/agents/claude/claude-run.ts +893 -130
- package/src/agents/claude/claude-selected-repository-instructions.ts +91 -0
- package/src/agents/claude/claude-skill-catalog.ts +75 -0
- package/src/agents/claude/claude-steered-notifications.ts +180 -0
- package/src/agents/claude/claude-tool-mapper.ts +81 -2
- package/src/agents/claude/claude-workspace-skills.ts +87 -0
- package/src/agents/claude/index.ts +2 -0
- package/src/agents/codex/codex-auth-mapper.ts +27 -0
- package/src/agents/codex/codex-auth.ts +19 -0
- package/src/agents/codex/codex-browser-auth-broker.ts +320 -0
- package/src/agents/codex/codex-browser-auth-status.ts +55 -0
- package/src/agents/codex/codex-command-mapper.ts +38 -3
- package/src/agents/codex/codex-event-mapper.ts +375 -64
- package/src/agents/codex/codex-json-rpc-client.ts +66 -21
- package/src/agents/codex/codex-provider.ts +118 -236
- package/src/agents/codex/codex-run-auth.ts +113 -0
- package/src/agents/codex/codex-run.ts +423 -53
- package/src/agents/codex/codex-tool-mapper.ts +2 -2
- package/src/agents/codex/codex.errors.ts +8 -0
- package/src/agents/codex/generated/meta.gen.ts +144 -6
- package/src/agents/codex/generated/namespaces.gen.ts +92 -3
- package/src/agents/codex/generated/schema.gen.ts +24698 -15638
- package/src/agents/codex/lazy-codex-run-auth.ts +47 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/input-attachment-preparation.ts +13 -0
- package/src/agents/mcp-tool-content.ts +82 -0
- package/src/agents/model-rebinding-run.ts +14 -3
- package/src/agents/selected-repository-instructions.ts +202 -0
- package/src/agents/session-context.ts +41 -0
- package/src/agents/subscription-token-refresh.ts +73 -0
- package/src/agents/user-input-request.ts +19 -20
- package/src/errors/index.ts +1 -1
- package/src/errors/validation-error.ts +6 -0
- package/src/protocol/agent-catalog.ts +159 -0
- package/src/protocol/agent-command.ts +52 -7
- package/src/protocol/agent-content.ts +36 -16
- package/src/protocol/agent-event.ts +210 -73
- package/src/protocol/agent-identity.ts +29 -0
- package/src/protocol/agent-json.ts +122 -8
- package/src/protocol/agent-metadata.ts +2 -2
- package/src/protocol/agent-model.ts +13 -1
- package/src/protocol/agent-notification.ts +110 -0
- package/src/protocol/agent-presentation.ts +71 -20
- package/src/protocol/agent-tool.ts +66 -10
- package/src/protocol/agent-usage.ts +3 -1
- package/src/protocol/agent-user-input.ts +4 -4
- package/src/protocol/agent-work.ts +12 -10
- package/src/protocol/index.ts +2 -0
- package/src/protocol/rejected-keys.ts +11 -0
- package/src/provenance.gen.ts +3 -3
- package/src/supervisor-protocol/agent-event-wrapper.ts +30 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { FileEditOutput, FileWriteOutput } from '@anthropic-ai/claude-agent-sdk/sdk-tools'
|
|
2
|
+
import { isAbsolute, relative } from 'node:path'
|
|
3
|
+
import z from 'zod'
|
|
4
|
+
import { CANONICAL_TOOL } from '../../protocol/agent-tool-name'
|
|
5
|
+
|
|
6
|
+
type ClaudePatchHunk = FileEditOutput['structuredPatch'][number]
|
|
7
|
+
// `staged` is typed only by newer SDKs (0.3.280); older pinned runtimes omit it, so it stays optional.
|
|
8
|
+
type ClaudeEditDiffEvidence = Pick<FileEditOutput, 'filePath' | 'oldString' | 'newString' | 'originalFile' | 'structuredPatch'> & { staged?: boolean }
|
|
9
|
+
type ClaudeWriteDiffEvidence = Pick<FileWriteOutput, 'type' | 'filePath' | 'content' | 'structuredPatch' | 'originalFile'> & { staged?: boolean }
|
|
10
|
+
interface DiffHeaderPaths { old: string, new: string }
|
|
11
|
+
|
|
12
|
+
const patchHunkSchema = z.object({
|
|
13
|
+
oldStart: z.number().int().nonnegative(),
|
|
14
|
+
oldLines: z.number().int().nonnegative(),
|
|
15
|
+
newStart: z.number().int().nonnegative(),
|
|
16
|
+
newLines: z.number().int().nonnegative(),
|
|
17
|
+
lines: z.array(z.string()),
|
|
18
|
+
}).passthrough() satisfies z.ZodType<ClaudePatchHunk>
|
|
19
|
+
|
|
20
|
+
const editDiffEvidenceSchema = z.object({
|
|
21
|
+
filePath: z.string().min(1),
|
|
22
|
+
oldString: z.string(),
|
|
23
|
+
newString: z.string(),
|
|
24
|
+
originalFile: z.string().nullable(),
|
|
25
|
+
structuredPatch: z.array(patchHunkSchema),
|
|
26
|
+
staged: z.boolean().optional(),
|
|
27
|
+
}).passthrough() satisfies z.ZodType<ClaudeEditDiffEvidence>
|
|
28
|
+
|
|
29
|
+
const writeDiffEvidenceSchema = z.object({
|
|
30
|
+
type: z.enum(['create', 'update']),
|
|
31
|
+
filePath: z.string().min(1),
|
|
32
|
+
content: z.string(),
|
|
33
|
+
structuredPatch: z.array(patchHunkSchema),
|
|
34
|
+
originalFile: z.string().nullable(),
|
|
35
|
+
staged: z.boolean().optional(),
|
|
36
|
+
}).passthrough() satisfies z.ZodType<ClaudeWriteDiffEvidence>
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Renders the host `tool_use_result` of a successful Claude Edit or Write call as a unified diff,
|
|
40
|
+
* the same output shape Codex reports for a file change. Returns undefined when the result does
|
|
41
|
+
* not prove a diff, so the caller keeps the model-facing text.
|
|
42
|
+
*/
|
|
43
|
+
export function claudeFileChangeText({ toolName, toolUseResult, cwd }: { toolName: string | undefined, toolUseResult: unknown, cwd: string | undefined }): string | undefined {
|
|
44
|
+
if (toolName === CANONICAL_TOOL.edit) {
|
|
45
|
+
const parsed = editDiffEvidenceSchema.safeParse(toolUseResult)
|
|
46
|
+
if (!parsed.success)
|
|
47
|
+
return undefined
|
|
48
|
+
const path = displayPath({ filePath: parsed.data.filePath, cwd })
|
|
49
|
+
const { oldString, newString, originalFile, structuredPatch, staged } = parsed.data
|
|
50
|
+
if (staged === true)
|
|
51
|
+
return noChangesText(path)
|
|
52
|
+
// The CLI accepts an empty old_string only for a missing or empty file ("Cannot create new
|
|
53
|
+
// file - file already exists." otherwise) and reports its original as "" (or null when the
|
|
54
|
+
// result is reconstructed), so this Edit created the file.
|
|
55
|
+
if (oldString === '' && !originalFile) {
|
|
56
|
+
const hunks = structuredPatch.length > 0 ? structuredPatch : createdFileHunks(newString)
|
|
57
|
+
return unifiedDiff({ paths: createdFilePaths(path), hunks })
|
|
58
|
+
}
|
|
59
|
+
// An empty Edit patch cannot tell an unchanged file from a diff the CLI gave up computing.
|
|
60
|
+
if (structuredPatch.length === 0)
|
|
61
|
+
return undefined
|
|
62
|
+
return unifiedDiff({ paths: gitHeaderPaths(path), hunks: structuredPatch })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (toolName === CANONICAL_TOOL.write) {
|
|
66
|
+
const parsed = writeDiffEvidenceSchema.safeParse(toolUseResult)
|
|
67
|
+
if (!parsed.success)
|
|
68
|
+
return undefined
|
|
69
|
+
const { type, content, structuredPatch, originalFile, staged } = parsed.data
|
|
70
|
+
const path = displayPath({ filePath: parsed.data.filePath, cwd })
|
|
71
|
+
if (staged === true)
|
|
72
|
+
return noChangesText(path)
|
|
73
|
+
if (type === 'create') {
|
|
74
|
+
const hunks = structuredPatch.length > 0 ? structuredPatch : createdFileHunks(content)
|
|
75
|
+
return unifiedDiff({ paths: createdFilePaths(path), hunks })
|
|
76
|
+
}
|
|
77
|
+
if (structuredPatch.length > 0)
|
|
78
|
+
return unifiedDiff({ paths: gitHeaderPaths(path), hunks: structuredPatch })
|
|
79
|
+
// An empty update patch also covers a timed-out diff or an original too large to include.
|
|
80
|
+
return originalFile === content ? noChangesText(path) : undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function displayPath({ filePath, cwd }: { filePath: string, cwd: string | undefined }): string {
|
|
87
|
+
if (!cwd || !isAbsolute(filePath))
|
|
88
|
+
return filePath
|
|
89
|
+
const relativePath = relative(cwd, filePath)
|
|
90
|
+
return relativePath && !relativePath.startsWith('..') && !isAbsolute(relativePath) ? relativePath : filePath
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function gitHeaderPaths(path: string): DiffHeaderPaths {
|
|
94
|
+
return isAbsolute(path) ? { old: path, new: path } : { old: `a/${path}`, new: `b/${path}` }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function createdFilePaths(path: string): DiffHeaderPaths {
|
|
98
|
+
return { old: '/dev/null', new: gitHeaderPaths(path).new }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createdFileHunks(content: string): ClaudePatchHunk[] {
|
|
102
|
+
if (content === '')
|
|
103
|
+
return []
|
|
104
|
+
const endsWithNewline = content.endsWith('\n')
|
|
105
|
+
const lines = (endsWithNewline ? content.slice(0, -1) : content).split('\n')
|
|
106
|
+
const hunkLines = lines.map(line => `+${line}`)
|
|
107
|
+
if (!endsWithNewline)
|
|
108
|
+
hunkLines.push('\')
|
|
109
|
+
return [{ oldStart: 1, oldLines: 0, newStart: 1, newLines: lines.length, lines: hunkLines }]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function unifiedDiff({ paths, hunks }: { paths: DiffHeaderPaths, hunks: ClaudePatchHunk[] }): string {
|
|
113
|
+
const lines = [`--- ${paths.old}`, `+++ ${paths.new}`]
|
|
114
|
+
for (const hunk of hunks)
|
|
115
|
+
lines.push(`@@ -${hunkRange(hunk.oldStart, hunk.oldLines)} +${hunkRange(hunk.newStart, hunk.newLines)} @@`, ...hunk.lines)
|
|
116
|
+
return `${lines.join('\n')}\n`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The CLI's structuredPatch is jsdiff's, which starts an empty range at the following line; a
|
|
120
|
+
// unified diff names the preceding line instead (so an empty original is `-0,0`), as jsdiff's own
|
|
121
|
+
// formatPatch does.
|
|
122
|
+
function hunkRange(start: number, count: number): string {
|
|
123
|
+
return `${count === 0 ? Math.max(start - 1, 0) : start},${count}`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function noChangesText(path: string): string {
|
|
127
|
+
return `No changes applied to ${path}.`
|
|
128
|
+
}
|
|
@@ -1,46 +1,171 @@
|
|
|
1
|
-
import type { Options, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk'
|
|
1
|
+
import type { McpStdioServerConfig, Options, Query, SDKControlInitializeResponse, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk'
|
|
2
2
|
import type { ClaudeModelId } from '../../protocol'
|
|
3
3
|
import type { AgentOperationOptions } from '../agent-operation'
|
|
4
4
|
import type { AgentProvider, AgentSessionOpenInput, AgentSessionResumeInput } from '../agent-provider'
|
|
5
5
|
import type { AgentTool } from '../agent-tool'
|
|
6
6
|
import type { InstructionComposer } from '../instructions'
|
|
7
|
+
import type { SessionContextComposer } from '../session-context'
|
|
8
|
+
import type { ClaudeProviderAuth } from './claude-auth'
|
|
9
|
+
import type { ClaudeDesktopConfig } from './claude-desktop'
|
|
7
10
|
import type { ClaudeStartupTiming } from './claude-startup-timing'
|
|
11
|
+
import type { ClaudeNativeToolActor } from './claude-tool-mapper'
|
|
12
|
+
import { randomUUID } from 'node:crypto'
|
|
8
13
|
import { homedir } from 'node:os'
|
|
9
14
|
import { join } from 'node:path'
|
|
10
15
|
import { query as queryClaude } from '@anthropic-ai/claude-agent-sdk'
|
|
11
16
|
import { claudeModelIdSchema } from '../../protocol'
|
|
12
|
-
import { AgentRunStateError } from '../agent-runtime-error'
|
|
17
|
+
import { AgentProviderLaunchError, AgentRunStateError } from '../agent-runtime-error'
|
|
13
18
|
import { selectAgentTools } from '../agent-tool'
|
|
19
|
+
import { CodexBrowserAuthBroker } from '../codex/codex-browser-auth-broker'
|
|
20
|
+
import { LazyCodexRunAuth } from '../codex/lazy-codex-run-auth'
|
|
14
21
|
import { emptyInstructionComposer } from '../instructions'
|
|
15
22
|
import { removeUndefined } from '../normalize'
|
|
23
|
+
import { pendingSessionContext } from '../session-context'
|
|
16
24
|
import { UserInputRequestStore, userInputRequestStoreRootFromTranscriptPath } from '../user-input-request'
|
|
25
|
+
import { ClaudeBrowserTurnMetadata, claudeBrowserTurnMetadataHooks } from './claude-browser-turn-metadata'
|
|
26
|
+
import { ClaudeCodeProcess } from './claude-code-process'
|
|
27
|
+
import { claudeDesktopMcpServers } from './claude-desktop'
|
|
17
28
|
import { createClaudeEventMapperState } from './claude-event-mapper'
|
|
18
29
|
import { ClaudeRun } from './claude-run'
|
|
30
|
+
import { ClaudeSelectedRepositoryInstructions } from './claude-selected-repository-instructions'
|
|
31
|
+
import { ClaudeSkillCatalog } from './claude-skill-catalog'
|
|
19
32
|
import { createClaudeStartupTiming, observeClaudeControlInitialization } from './claude-startup-timing'
|
|
20
|
-
import {
|
|
33
|
+
import { claudeSteeredNotificationHooks, ClaudeSteeredNotificationStore } from './claude-steered-notifications'
|
|
34
|
+
import { claudeAllowedToolNames, claudeNativeToolActorHooks, claudeToolActorHooks, createClaudeToolRuntimeRef, mergeClaudeToolHooks, toClaudeMcpServers } from './claude-tool-mapper'
|
|
35
|
+
import { ClaudeWorkspaceSkills, claudeWorkspaceSkillsPluginName } from './claude-workspace-skills'
|
|
36
|
+
|
|
37
|
+
export type { ClaudeNativeToolActor } from './claude-tool-mapper'
|
|
38
|
+
|
|
39
|
+
export type ClaudeNativeToolActorRun = Readonly<{
|
|
40
|
+
mcpServer: Readonly<{
|
|
41
|
+
name: 'mcp_repl'
|
|
42
|
+
config: McpStdioServerConfig
|
|
43
|
+
}>
|
|
44
|
+
toolNames: readonly string[]
|
|
45
|
+
register: (input: { toolName: string, toolUseId: string, actor: ClaudeNativeToolActor, signal: AbortSignal }) => Promise<void>
|
|
46
|
+
finish: (input: { toolUseId: string }) => Promise<void>
|
|
47
|
+
close: () => Promise<void>
|
|
48
|
+
}>
|
|
49
|
+
|
|
50
|
+
export type ClaudeNativeToolActorBridge = Readonly<{
|
|
51
|
+
openRun: (input: { sessionId: string, signal: AbortSignal }) => Promise<ClaudeNativeToolActorRun>
|
|
52
|
+
}>
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Skill sources for every run. `imagePlugin` is the image-baked local plugin whose `skills` links to
|
|
56
|
+
* the image skills directory (`null` outside the sandbox image); `workspacePluginDirectory` is where
|
|
57
|
+
* each run generates its workspace-repository skills plugin.
|
|
58
|
+
*/
|
|
59
|
+
export type ClaudeSkillsConfig = Readonly<{
|
|
60
|
+
imagePlugin: Readonly<{ path: string, name: string }> | null
|
|
61
|
+
workspacePluginDirectory: string
|
|
62
|
+
}>
|
|
21
63
|
|
|
22
64
|
export interface ClaudeProviderConfig {
|
|
65
|
+
// Routes model traffic through a credential proxy. Without it, the configured environment's own
|
|
66
|
+
// credentials (for example ANTHROPIC_API_KEY) authenticate Claude Code unchanged.
|
|
67
|
+
auth?: ClaudeProviderAuth
|
|
68
|
+
// Skill plugins for every run. Without it, runs load no generated skills plugin.
|
|
69
|
+
skills?: ClaudeSkillsConfig
|
|
70
|
+
// The sandbox desktop (Computer Use / Browser Use); omitted or `null` where no desktop exists.
|
|
71
|
+
desktop?: ClaudeDesktopConfig | null
|
|
23
72
|
tools?: readonly AgentTool[]
|
|
24
73
|
executable?: string
|
|
25
74
|
environment?: Record<string, string | undefined>
|
|
75
|
+
nativeToolActorBridge?: ClaudeNativeToolActorBridge
|
|
76
|
+
startupDeadlineMs?: number
|
|
77
|
+
// Product instructions appended to the Claude Code preset system prompt.
|
|
26
78
|
instructions?: InstructionComposer
|
|
79
|
+
// Per-session context delivered once with the first prompt of a session that has a web URL.
|
|
80
|
+
sessionContext?: SessionContextComposer
|
|
81
|
+
// Product context published with the selected-repository instructions, even with no repository.
|
|
82
|
+
selectedRepositoryContext?: string
|
|
27
83
|
onStartupTiming?: (timing: ClaudeStartupTiming) => void
|
|
84
|
+
// Defaults to project and local settings; `sdk-default` leaves the SDK option unset.
|
|
85
|
+
settingSources?: 'sdk-default' | NonNullable<Options['settingSources']>
|
|
86
|
+
// Reads persisted tool results back from the transcript (default true).
|
|
87
|
+
hydratePersistedToolOutput?: boolean
|
|
88
|
+
// Native tools unavailable to the model; defaults to the interactive and out-of-session tools.
|
|
89
|
+
disallowedTools?: readonly string[]
|
|
28
90
|
}
|
|
29
91
|
|
|
92
|
+
// Bounds the SDK `initialize` handshake. Measured Claude Code startup in the E2B image is about
|
|
93
|
+
// 0.4 to 2.7 s (locally about 0.13 s); 30 s is roughly ten times the slowest observation and matches
|
|
94
|
+
// the 30 s deadline the Codex browser-auth broker applies to its native startup.
|
|
95
|
+
export const CLAUDE_STARTUP_DEADLINE_MS = 30_000
|
|
96
|
+
|
|
97
|
+
// Credentials that must never reach Claude Code from the inherited environment: the run's only
|
|
98
|
+
// credential is the configured auth route, so an ambient key cannot silently take precedence.
|
|
99
|
+
const inheritedClaudeCredentialKeys = [
|
|
100
|
+
'ANTHROPIC_API_KEY',
|
|
101
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
102
|
+
'ANTHROPIC_BASE_URL',
|
|
103
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
104
|
+
'CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR',
|
|
105
|
+
'CLAUDE_CODE_OAUTH_REFRESH_TOKEN',
|
|
106
|
+
// Account facts for the env token; only the auth route may state them.
|
|
107
|
+
'CLAUDE_CODE_SUBSCRIPTION_TYPE',
|
|
108
|
+
'CLAUDE_CODE_RATE_LIMIT_TIER',
|
|
109
|
+
] as const
|
|
110
|
+
|
|
111
|
+
// Claude Code 2.1.280 gives an env OAuth token the subscription type CLAUDE_CODE_SUBSCRIPTION_TYPE
|
|
112
|
+
// (the variable it sets itself for the env-token child processes it spawns), else null. With
|
|
113
|
+
// _CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL, a null or team/enterprise type makes initialize fetch
|
|
114
|
+
// remote managed settings and policy limits from the hard-coded https://api.anthropic.com with
|
|
115
|
+
// the placeholder bearer (a 401 when reachable, ~210 ms locally; ~18 s of retries when not). For a
|
|
116
|
+
// Pro or Max account the CLI skips both as it does for a stored /login (`unsupported_subscription`,
|
|
117
|
+
// `prosumer_oauth`), so the environment states only those two real types; see
|
|
118
|
+
// docs/packages/agents.md#claude-authentication.
|
|
119
|
+
const claudeFetchFreeSubscriptionTypes = new Set(['pro', 'max'])
|
|
120
|
+
|
|
121
|
+
// Interactive native tools that a headless run cannot serve.
|
|
122
|
+
export const defaultClaudeDisallowedToolNames: readonly string[] = [
|
|
123
|
+
'AskUserQuestion',
|
|
124
|
+
'EnterPlanMode',
|
|
125
|
+
'ExitPlanMode',
|
|
126
|
+
'NotebookEdit',
|
|
127
|
+
'EnterWorktree',
|
|
128
|
+
'ExitWorktree',
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
// Claude Code 2.1.263 and 2.1.280 omit these native work tools from headless preset sessions unless
|
|
132
|
+
// they are named in allowedTools. Keep them loaded so their lifecycle maps to work.observed.
|
|
133
|
+
const claudeNativeWorkToolNames = ['TaskCreate', 'TaskGet', 'TaskList', 'TaskUpdate'] as const
|
|
134
|
+
|
|
30
135
|
export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
31
136
|
readonly id = 'claude' as const
|
|
137
|
+
private readonly auth: ClaudeProviderAuth | undefined
|
|
138
|
+
private readonly skills: ClaudeSkillsConfig | undefined
|
|
139
|
+
private readonly desktop: ClaudeDesktopConfig | null
|
|
32
140
|
private readonly tools: readonly AgentTool[]
|
|
33
141
|
private readonly executable: string | undefined
|
|
34
142
|
private readonly environment: Record<string, string | undefined>
|
|
143
|
+
private readonly nativeToolActorBridge: ClaudeNativeToolActorBridge | undefined
|
|
144
|
+
private readonly startupDeadlineMs: number
|
|
35
145
|
private readonly instructions: InstructionComposer
|
|
146
|
+
private readonly sessionContext: SessionContextComposer | undefined
|
|
147
|
+
private readonly selectedRepositoryContext: string | undefined
|
|
36
148
|
private readonly onStartupTiming: ClaudeProviderConfig['onStartupTiming']
|
|
149
|
+
private readonly settingSources: Options['settingSources']
|
|
150
|
+
private readonly hydratePersistedToolOutput: boolean
|
|
151
|
+
private readonly disallowedTools: readonly string[]
|
|
37
152
|
|
|
38
153
|
private constructor(config: ClaudeProviderConfig) {
|
|
154
|
+
this.auth = config.auth
|
|
155
|
+
this.skills = config.skills
|
|
156
|
+
this.desktop = config.desktop ?? null
|
|
39
157
|
this.tools = config.tools ?? []
|
|
40
158
|
this.executable = config.executable
|
|
41
159
|
this.environment = config.environment ?? {}
|
|
160
|
+
this.nativeToolActorBridge = config.nativeToolActorBridge
|
|
161
|
+
this.startupDeadlineMs = config.startupDeadlineMs ?? CLAUDE_STARTUP_DEADLINE_MS
|
|
42
162
|
this.instructions = config.instructions ?? emptyInstructionComposer
|
|
163
|
+
this.sessionContext = config.sessionContext
|
|
164
|
+
this.selectedRepositoryContext = config.selectedRepositoryContext
|
|
43
165
|
this.onStartupTiming = config.onStartupTiming
|
|
166
|
+
this.settingSources = config.settingSources === 'sdk-default' ? undefined : config.settingSources ?? ['project', 'local']
|
|
167
|
+
this.hydratePersistedToolOutput = config.hydratePersistedToolOutput ?? true
|
|
168
|
+
this.disallowedTools = config.disallowedTools ?? defaultClaudeDisallowedToolNames
|
|
44
169
|
}
|
|
45
170
|
|
|
46
171
|
static create(config: ClaudeProviderConfig = {}): ClaudeProvider {
|
|
@@ -62,6 +187,8 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
62
187
|
session: { sessionId?: string, resume?: string }
|
|
63
188
|
expectedSessionId: string
|
|
64
189
|
operationOptions: AgentOperationOptions
|
|
190
|
+
// Set on the one re-spawn after a spawn declared a subscription type the credential lacks.
|
|
191
|
+
respawnedForSubscriptionType?: true
|
|
65
192
|
}): Promise<ClaudeRun> {
|
|
66
193
|
const { input, session, expectedSessionId, operationOptions } = params
|
|
67
194
|
operationOptions.signal?.throwIfAborted()
|
|
@@ -73,11 +200,25 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
73
200
|
const tools = selectAgentTools(this.tools, input.tools)
|
|
74
201
|
const state = createClaudeEventMapperState()
|
|
75
202
|
const runtime = createClaudeToolRuntimeRef({ turnId: () => state.currentTurnId })
|
|
203
|
+
// The transcript location is a pre-input fact; session-local stores live beside it.
|
|
204
|
+
const transcriptPath = join(configDir, 'projects', encodeClaudeProjectPath(input.cwd), `${expectedSessionId}.jsonl`)
|
|
205
|
+
const steeredNotifications = ClaudeSteeredNotificationStore.open({ sessionId: input.sessionId, transcriptPath })
|
|
206
|
+
const resources = await this.openRunResources({ input, expectedSessionId, signal: abortController.signal })
|
|
207
|
+
const { nativeToolActorRun, workspaceSkills, selectedRepositoryInstructions, browserAuthBroker, browserTurnMetadata } = resources
|
|
208
|
+
const claudeProcess = new ClaudeCodeProcess()
|
|
209
|
+
const declaredSubscriptionType = this.auth ? fetchFreeSubscriptionType(this.auth.knownSubscriptionType()) : undefined
|
|
210
|
+
const runtimeMcpServers = toClaudeMcpServers({
|
|
211
|
+
tools,
|
|
212
|
+
runtime,
|
|
213
|
+
signal: abortController.signal,
|
|
214
|
+
})
|
|
76
215
|
const sdkOptions: Options = removeUndefined({
|
|
77
216
|
abortController,
|
|
78
217
|
cwd: input.cwd,
|
|
79
|
-
|
|
80
|
-
|
|
218
|
+
// Spawn is not delayed for the credential: before the first acquisition the declared type
|
|
219
|
+
// is the bootstrap's hint, which is re-checked below once the credential is acquired.
|
|
220
|
+
env: this.auth ? claudeRunEnvironment(this.environment, this.auth, declaredSubscriptionType) : this.environment,
|
|
221
|
+
settingSources: this.settingSources,
|
|
81
222
|
model: input.model,
|
|
82
223
|
thinking: { type: 'adaptive', display: 'summarized' },
|
|
83
224
|
effort: input.reasoningEffort,
|
|
@@ -89,29 +230,62 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
89
230
|
permissionMode: 'bypassPermissions',
|
|
90
231
|
allowDangerouslySkipPermissions: true,
|
|
91
232
|
tools: { type: 'preset', preset: 'claude_code' },
|
|
92
|
-
systemPrompt: { type: 'preset', preset: 'claude_code', append: this.instructions({ provider: 'claude', tools }) },
|
|
93
|
-
disallowedTools: [
|
|
94
|
-
mcpServers:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
233
|
+
systemPrompt: { type: 'preset', preset: 'claude_code', append: input.instructions ?? this.instructions({ provider: 'claude', tools }) },
|
|
234
|
+
disallowedTools: [...this.disallowedTools],
|
|
235
|
+
mcpServers: {
|
|
236
|
+
...(this.desktop && browserAuthBroker && browserTurnMetadata
|
|
237
|
+
? claudeDesktopMcpServers({ desktop: this.desktop, environment: this.environment, browserAuthSocketPath: browserAuthBroker.socketPath, turnMetadataPath: browserTurnMetadata.path })
|
|
238
|
+
: {}),
|
|
239
|
+
...runtimeMcpServers,
|
|
240
|
+
...(nativeToolActorRun ? { [nativeToolActorRun.mcpServer.name]: nativeToolActorRun.mcpServer.config } : {}),
|
|
241
|
+
},
|
|
242
|
+
plugins: workspaceSkills
|
|
243
|
+
? [
|
|
244
|
+
...(this.skills?.imagePlugin ? [{ type: 'local' as const, path: this.skills.imagePlugin.path }] : []),
|
|
245
|
+
{ type: 'local' as const, path: workspaceSkills.pluginPath },
|
|
246
|
+
]
|
|
247
|
+
: undefined,
|
|
248
|
+
allowedTools: ['Bash', ...claudeNativeWorkToolNames, ...claudeAllowedToolNames(tools), ...(nativeToolActorRun?.toolNames ?? [])],
|
|
249
|
+
hooks: mergeClaudeToolHooks(
|
|
250
|
+
claudeToolActorHooks(tools),
|
|
251
|
+
nativeToolActorRun
|
|
252
|
+
? claudeNativeToolActorHooks({
|
|
253
|
+
expectedSessionId,
|
|
254
|
+
toolNames: nativeToolActorRun.toolNames,
|
|
255
|
+
register: nativeToolActorRun.register,
|
|
256
|
+
finish: nativeToolActorRun.finish,
|
|
257
|
+
})
|
|
258
|
+
: undefined,
|
|
259
|
+
claudeSteeredNotificationHooks(steeredNotifications),
|
|
260
|
+
selectedRepositoryInstructions.hooks(),
|
|
261
|
+
this.desktop && browserTurnMetadata
|
|
262
|
+
? claudeBrowserTurnMetadataHooks({
|
|
263
|
+
serverName: this.desktop.browserAuth.serverName,
|
|
264
|
+
record: async () => await browserTurnMetadata.record({ sessionId: expectedSessionId, turnId: state.currentTurnId }),
|
|
265
|
+
})
|
|
266
|
+
: undefined,
|
|
267
|
+
),
|
|
101
268
|
pathToClaudeCodeExecutable: this.executable,
|
|
269
|
+
spawnClaudeCodeProcess: claudeProcess.spawn,
|
|
102
270
|
})
|
|
103
271
|
|
|
104
272
|
const recordStartup = createClaudeStartupTiming(this.onStartupTiming)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
273
|
+
let query: Query
|
|
274
|
+
try {
|
|
275
|
+
query = queryClaude({ prompt: inputQueue, options: sdkOptions })
|
|
276
|
+
recordStartup('query_created')
|
|
277
|
+
if (this.onStartupTiming)
|
|
278
|
+
observeClaudeControlInitialization(query.initializationResult(), recordStartup)
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
await closeRunResources(resources)
|
|
282
|
+
throw error
|
|
283
|
+
}
|
|
109
284
|
try {
|
|
110
285
|
// Binding metadata comes from pre-input facts only: the live runtime emits system/init
|
|
111
286
|
// only after the first user message, so the run validates identity/config on the first
|
|
112
287
|
// input (init barrier). Observed runtime configuration arrives via session.started /
|
|
113
288
|
// session.configured after that validation.
|
|
114
|
-
const transcriptPath = join(configDir, 'projects', encodeClaudeProjectPath(input.cwd), `${expectedSessionId}.jsonl`)
|
|
115
289
|
const metadata = {
|
|
116
290
|
provider: 'claude' as const,
|
|
117
291
|
sessionId: input.sessionId,
|
|
@@ -120,40 +294,181 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
120
294
|
model: firstClaudeModelId(input.model),
|
|
121
295
|
transcriptPath,
|
|
122
296
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
297
|
+
// The SDK spawns the CLI and sends the `initialize` control request inside query(); its
|
|
298
|
+
// response needs no user input. Awaiting it means an opened run, including a supervisor
|
|
299
|
+
// prewarm, holds a CLI that has finished startup, so the first prompt skips that stall.
|
|
300
|
+
// This is not system/init, which still arrives only after the first input. The
|
|
301
|
+
// credential is acquired alongside, so an account needing reauth fails the open.
|
|
302
|
+
const [userInputRequests, initialization, , sessionContext] = await Promise.all([
|
|
303
|
+
UserInputRequestStore.open({
|
|
304
|
+
sessionId: metadata.sessionId,
|
|
305
|
+
rootDir: userInputRequestStoreRootFromTranscriptPath(metadata.transcriptPath),
|
|
306
|
+
}),
|
|
307
|
+
awaitClaudeInitialization({ query, signal: abortController.signal, deadlineMs: this.startupDeadlineMs }),
|
|
308
|
+
this.auth?.acquire({ signal: abortController.signal }),
|
|
309
|
+
pendingSessionContext({ compose: this.sessionContext, sessionWebUrl: input.sessionWebUrl, resumedTranscriptPath: session.resume === undefined ? undefined : transcriptPath }),
|
|
310
|
+
])
|
|
311
|
+
// A CLI told a Pro/Max type that the acquired account does not have (a stale bootstrap
|
|
312
|
+
// hint, or an account replaced since) would skip that account's policy fetches for its
|
|
313
|
+
// lifetime; it has had no input yet, so it is replaced once before the run is published.
|
|
314
|
+
if (this.auth && declaredSubscriptionType !== undefined && declaredSubscriptionType !== fetchFreeSubscriptionType(this.auth.knownSubscriptionType()) && !params.respawnedForSubscriptionType) {
|
|
315
|
+
query.close()
|
|
316
|
+
inputQueue.close()
|
|
317
|
+
await claudeProcess.terminate()
|
|
318
|
+
await closeRunResources(resources)
|
|
319
|
+
return await this.openRun({ ...params, respawnedForSubscriptionType: true })
|
|
320
|
+
}
|
|
127
321
|
|
|
128
|
-
|
|
322
|
+
let unsubscribeAuthFailures = (): void => undefined
|
|
323
|
+
let run: ClaudeRun | undefined
|
|
324
|
+
const skillCatalog = new ClaudeSkillCatalog({
|
|
325
|
+
pluginScopes: new Map([
|
|
326
|
+
...(this.skills?.imagePlugin ? [[this.skills.imagePlugin.name, 'user'] as const] : []),
|
|
327
|
+
...(workspaceSkills ? [[claudeWorkspaceSkillsPluginName, 'repository'] as const] : []),
|
|
328
|
+
]),
|
|
329
|
+
emit: draft => run?.pushEvent(draft),
|
|
330
|
+
})
|
|
331
|
+
skillCatalog.publish(initialization.commands)
|
|
332
|
+
run = new ClaudeRun({
|
|
129
333
|
metadata,
|
|
130
334
|
query,
|
|
131
335
|
userInputRequests,
|
|
132
336
|
input: inputQueue,
|
|
133
337
|
eventMapperState: state,
|
|
134
|
-
recordStartup,
|
|
135
338
|
init: {
|
|
136
339
|
expectedSessionId,
|
|
137
340
|
requestedModel: input.model,
|
|
138
341
|
reasoningEffort: input.reasoningEffort,
|
|
139
342
|
environment: this.environment,
|
|
140
343
|
},
|
|
344
|
+
steeredNotifications,
|
|
345
|
+
sessionContext,
|
|
346
|
+
process: claudeProcess,
|
|
347
|
+
workspace: {
|
|
348
|
+
skills: workspaceSkills,
|
|
349
|
+
repositoryRoots: input.workspaceRepositoryRoots,
|
|
350
|
+
catalog: skillCatalog,
|
|
351
|
+
instructions: selectedRepositoryInstructions,
|
|
352
|
+
},
|
|
353
|
+
onFinished: async () => {
|
|
354
|
+
unsubscribeAuthFailures()
|
|
355
|
+
await closeRunResources(resources)
|
|
356
|
+
},
|
|
357
|
+
recordStartup,
|
|
358
|
+
hydratePersistedToolOutput: this.hydratePersistedToolOutput,
|
|
141
359
|
})
|
|
360
|
+
const openedRun = run
|
|
361
|
+
// The proxy owns the credential; a failure it cannot retry away ends this run typed.
|
|
362
|
+
if (this.auth)
|
|
363
|
+
unsubscribeAuthFailures = this.auth.failures.subscribe(failure => openedRun.failActiveTurnAndEndRun(failure))
|
|
142
364
|
runtime.attach({
|
|
143
|
-
metadata:
|
|
144
|
-
requestUserInput: async request => await
|
|
365
|
+
metadata: openedRun.metadata,
|
|
366
|
+
requestUserInput: async request => await openedRun.requestUserInput(request),
|
|
145
367
|
})
|
|
146
|
-
|
|
147
|
-
return
|
|
368
|
+
openedRun.begin()
|
|
369
|
+
return openedRun
|
|
148
370
|
}
|
|
149
371
|
catch (error) {
|
|
150
372
|
query.close()
|
|
151
373
|
inputQueue.close()
|
|
374
|
+
// A failed open (for example a missed initialize deadline) must not leave its CLI behind.
|
|
375
|
+
await claudeProcess.terminate()
|
|
376
|
+
await closeRunResources(resources)
|
|
377
|
+
throw error
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Run-owned resources opened before the CLI spawns; every failure path and run end closes them.
|
|
382
|
+
private async openRunResources(input: {
|
|
383
|
+
input: AgentSessionOpenInput<'claude'> | AgentSessionResumeInput<'claude'>
|
|
384
|
+
expectedSessionId: string
|
|
385
|
+
signal: AbortSignal
|
|
386
|
+
}): Promise<ClaudeRunResources> {
|
|
387
|
+
const opened: Partial<ClaudeRunResources> = {}
|
|
388
|
+
try {
|
|
389
|
+
if (this.skills) {
|
|
390
|
+
const workspaceSkills = opened.workspaceSkills = await ClaudeWorkspaceSkills.create(join(this.skills.workspacePluginDirectory, randomUUID()))
|
|
391
|
+
// Repositories may still be initializing; prepareWorkspace re-syncs after the Git baseline.
|
|
392
|
+
await workspaceSkills.sync(input.input.workspaceRepositoryRoots)
|
|
393
|
+
}
|
|
394
|
+
// Published before the CLI spawns: Claude Code reads instruction files once per process.
|
|
395
|
+
const selectedRepositoryInstructions = await ClaudeSelectedRepositoryInstructions.open({ cwd: input.input.cwd, repositoryRoots: input.input.workspaceRepositoryRoots, sharedContext: this.selectedRepositoryContext })
|
|
396
|
+
if (this.desktop) {
|
|
397
|
+
// The broker listens now but binds a Codex account only when Browser Use first asks.
|
|
398
|
+
opened.browserAuthBroker = await CodexBrowserAuthBroker.open({
|
|
399
|
+
executable: this.desktop.browserAuth.codexExecutable,
|
|
400
|
+
cwd: input.input.cwd,
|
|
401
|
+
environment: this.environment,
|
|
402
|
+
auth: new LazyCodexRunAuth(this.desktop.browserAuth.codexAuth),
|
|
403
|
+
})
|
|
404
|
+
opened.browserTurnMetadata = await ClaudeBrowserTurnMetadata.open()
|
|
405
|
+
}
|
|
406
|
+
if (this.nativeToolActorBridge)
|
|
407
|
+
opened.nativeToolActorRun = await this.nativeToolActorBridge.openRun({ sessionId: input.expectedSessionId, signal: input.signal })
|
|
408
|
+
return { workspaceSkills: opened.workspaceSkills, selectedRepositoryInstructions, browserAuthBroker: opened.browserAuthBroker, browserTurnMetadata: opened.browserTurnMetadata, nativeToolActorRun: opened.nativeToolActorRun }
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
await closeRunResources(opened)
|
|
152
412
|
throw error
|
|
153
413
|
}
|
|
154
414
|
}
|
|
155
415
|
}
|
|
156
416
|
|
|
417
|
+
interface ClaudeRunResources {
|
|
418
|
+
nativeToolActorRun: ClaudeNativeToolActorRun | undefined
|
|
419
|
+
workspaceSkills: ClaudeWorkspaceSkills | undefined
|
|
420
|
+
selectedRepositoryInstructions: ClaudeSelectedRepositoryInstructions
|
|
421
|
+
browserAuthBroker: CodexBrowserAuthBroker | undefined
|
|
422
|
+
browserTurnMetadata: ClaudeBrowserTurnMetadata | undefined
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function closeRunResources(resources: Partial<ClaudeRunResources>): Promise<void> {
|
|
426
|
+
await Promise.all([
|
|
427
|
+
resources.nativeToolActorRun?.close().catch(() => undefined),
|
|
428
|
+
resources.browserAuthBroker?.close().catch(() => undefined),
|
|
429
|
+
resources.browserTurnMetadata?.close().catch(() => undefined),
|
|
430
|
+
resources.workspaceSkills?.remove().catch(() => undefined),
|
|
431
|
+
])
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function awaitClaudeInitialization(input: { query: Query, signal: AbortSignal, deadlineMs: number }): Promise<SDKControlInitializeResponse> {
|
|
435
|
+
input.signal.throwIfAborted()
|
|
436
|
+
let onAbort: (() => void) | undefined
|
|
437
|
+
let deadline: ReturnType<typeof setTimeout> | undefined
|
|
438
|
+
try {
|
|
439
|
+
return await Promise.race([
|
|
440
|
+
input.query.initializationResult(),
|
|
441
|
+
new Promise<never>((_, reject) => {
|
|
442
|
+
onAbort = () => reject(input.signal.reason)
|
|
443
|
+
input.signal.addEventListener('abort', onAbort, { once: true })
|
|
444
|
+
deadline = setTimeout(() => reject(new AgentProviderLaunchError({
|
|
445
|
+
message: `Claude Code did not finish its initialize handshake within ${input.deadlineMs}ms.`,
|
|
446
|
+
retryable: true,
|
|
447
|
+
})), input.deadlineMs)
|
|
448
|
+
}),
|
|
449
|
+
])
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
clearTimeout(deadline)
|
|
453
|
+
if (onAbort)
|
|
454
|
+
input.signal.removeEventListener('abort', onAbort)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function claudeRunEnvironment(environment: Record<string, string | undefined>, auth: ClaudeProviderAuth, subscriptionType: string | undefined): Record<string, string | undefined> {
|
|
459
|
+
const runEnvironment = { ...environment }
|
|
460
|
+
for (const key of inheritedClaudeCredentialKeys) delete runEnvironment[key]
|
|
461
|
+
runEnvironment.ANTHROPIC_BASE_URL = auth.baseUrl
|
|
462
|
+
runEnvironment.CLAUDE_CODE_OAUTH_TOKEN = auth.oauthToken
|
|
463
|
+
if (subscriptionType !== undefined)
|
|
464
|
+
runEnvironment.CLAUDE_CODE_SUBSCRIPTION_TYPE = subscriptionType
|
|
465
|
+
return runEnvironment
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function fetchFreeSubscriptionType(subscriptionType: string | null): string | undefined {
|
|
469
|
+
return subscriptionType !== null && claudeFetchFreeSubscriptionTypes.has(subscriptionType) ? subscriptionType : undefined
|
|
470
|
+
}
|
|
471
|
+
|
|
157
472
|
function firstClaudeModelId(...values: Array<string | null | undefined>): ClaudeModelId | undefined {
|
|
158
473
|
for (const value of values) {
|
|
159
474
|
const parsed = claudeModelIdSchema.safeParse(value)
|