@meistrari/agent-core 0.0.0 → 0.1.1
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 +36 -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 +748 -0
- package/src/agents/claude/claude-provider.ts +191 -0
- package/src/agents/claude/claude-run.ts +468 -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 +721 -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/protocol/agent-event.ts +2 -1
- package/src/protocol/agent-usage.ts +14 -0
- package/src/provenance.gen.ts +3 -3
- package/src/supervisor/agent-provider-factory.ts +189 -0
- package/src/supervisor/bootstrap-binder.ts +133 -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 +435 -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 +206 -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 +127 -0
- package/src/worker-runtime-client/connection-attempt.ts +349 -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 +139 -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 +301 -0
- package/src/worker-runtime-client/token-crypto.ts +46 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { Options, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk'
|
|
2
|
+
import type { ClaudeModelId } from '../../protocol'
|
|
3
|
+
import type { AgentOperationOptions } from '../agent-operation'
|
|
4
|
+
import type { AgentProvider, AgentSessionOpenInput, AgentSessionResumeInput } from '../agent-provider'
|
|
5
|
+
import type { AgentTool } from '../agent-tool'
|
|
6
|
+
import type { InstructionComposer } from '../instructions'
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
import { query as queryClaude } from '@anthropic-ai/claude-agent-sdk'
|
|
10
|
+
import { claudeModelIdSchema } from '../../protocol'
|
|
11
|
+
import { AgentRunStateError } from '../agent-runtime-error'
|
|
12
|
+
import { selectAgentTools } from '../agent-tool'
|
|
13
|
+
import { emptyInstructionComposer } from '../instructions'
|
|
14
|
+
import { removeUndefined } from '../normalize'
|
|
15
|
+
import { UserInputRequestStore, userInputRequestStoreRootFromTranscriptPath } from '../user-input-request'
|
|
16
|
+
import { createClaudeEventMapperState } from './claude-event-mapper'
|
|
17
|
+
import { ClaudeRun } from './claude-run'
|
|
18
|
+
import { claudeAllowedToolNames, claudeToolActorHooks, createClaudeToolRuntimeRef, toClaudeMcpServers } from './claude-tool-mapper'
|
|
19
|
+
|
|
20
|
+
export interface ClaudeProviderConfig {
|
|
21
|
+
tools?: readonly AgentTool[]
|
|
22
|
+
executable?: string
|
|
23
|
+
environment?: Record<string, string | undefined>
|
|
24
|
+
instructions?: InstructionComposer
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
28
|
+
readonly id = 'claude' as const
|
|
29
|
+
private readonly tools: readonly AgentTool[]
|
|
30
|
+
private readonly executable: string | undefined
|
|
31
|
+
private readonly environment: Record<string, string | undefined>
|
|
32
|
+
private readonly instructions: InstructionComposer
|
|
33
|
+
|
|
34
|
+
private constructor(config: ClaudeProviderConfig) {
|
|
35
|
+
this.tools = config.tools ?? []
|
|
36
|
+
this.executable = config.executable
|
|
37
|
+
this.environment = config.environment ?? {}
|
|
38
|
+
this.instructions = config.instructions ?? emptyInstructionComposer
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static create(config: ClaudeProviderConfig = {}): ClaudeProvider {
|
|
42
|
+
return new ClaudeProvider(config)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async openSession(input: AgentSessionOpenInput<'claude'>, options: AgentOperationOptions = {}): Promise<ClaudeRun> {
|
|
46
|
+
// Prebind a caller-minted UUID (Options.sessionId) so the binding is known before any input.
|
|
47
|
+
const providerSessionId = crypto.randomUUID()
|
|
48
|
+
return await this.openRun({ input, session: { sessionId: providerSessionId }, expectedSessionId: providerSessionId, operationOptions: options })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async resumeSession(input: AgentSessionResumeInput<'claude'>, options: AgentOperationOptions = {}): Promise<ClaudeRun> {
|
|
52
|
+
return await this.openRun({ input, session: { resume: input.providerSessionId }, expectedSessionId: input.providerSessionId, operationOptions: options })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private async openRun(params: {
|
|
56
|
+
input: AgentSessionOpenInput<'claude'> | AgentSessionResumeInput<'claude'>
|
|
57
|
+
session: { sessionId?: string, resume?: string }
|
|
58
|
+
expectedSessionId: string
|
|
59
|
+
operationOptions: AgentOperationOptions
|
|
60
|
+
}): Promise<ClaudeRun> {
|
|
61
|
+
const { input, session, expectedSessionId, operationOptions } = params
|
|
62
|
+
operationOptions.signal?.throwIfAborted()
|
|
63
|
+
const abortController = new AbortController()
|
|
64
|
+
operationOptions.signal?.addEventListener('abort', () => abortController.abort(), { once: true })
|
|
65
|
+
// Lifecycle applies no user input: the stream stays empty until the run receives commands.
|
|
66
|
+
const inputQueue = createInputQueue()
|
|
67
|
+
const configDir = this.environment.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude')
|
|
68
|
+
const tools = selectAgentTools(this.tools, input.tools)
|
|
69
|
+
const state = createClaudeEventMapperState()
|
|
70
|
+
const runtime = createClaudeToolRuntimeRef({ turnId: () => state.currentTurnId })
|
|
71
|
+
const sdkOptions: Options = removeUndefined({
|
|
72
|
+
abortController,
|
|
73
|
+
cwd: input.cwd,
|
|
74
|
+
env: this.environment,
|
|
75
|
+
model: input.model,
|
|
76
|
+
thinking: { type: 'adaptive', display: 'summarized' },
|
|
77
|
+
effort: input.reasoningEffort,
|
|
78
|
+
sessionId: session.sessionId,
|
|
79
|
+
resume: session.resume,
|
|
80
|
+
includePartialMessages: true,
|
|
81
|
+
forwardSubagentText: true,
|
|
82
|
+
agentProgressSummaries: true,
|
|
83
|
+
permissionMode: 'bypassPermissions',
|
|
84
|
+
allowDangerouslySkipPermissions: true,
|
|
85
|
+
tools: { type: 'preset', preset: 'claude_code' },
|
|
86
|
+
systemPrompt: { type: 'preset', preset: 'claude_code', append: this.instructions({ provider: 'claude', tools }) },
|
|
87
|
+
disallowedTools: ['AskUserQuestion', 'EnterPlanMode', 'ExitPlanMode', 'NotebookEdit', 'EnterWorktree', 'ExitWorktree'],
|
|
88
|
+
mcpServers: toClaudeMcpServers({
|
|
89
|
+
tools,
|
|
90
|
+
runtime,
|
|
91
|
+
signal: abortController.signal,
|
|
92
|
+
}),
|
|
93
|
+
allowedTools: ['Bash', ...claudeAllowedToolNames(tools)],
|
|
94
|
+
hooks: claudeToolActorHooks(tools),
|
|
95
|
+
pathToClaudeCodeExecutable: this.executable,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const query = queryClaude({ prompt: inputQueue, options: sdkOptions })
|
|
99
|
+
try {
|
|
100
|
+
// Binding metadata comes from pre-input facts only: the live runtime emits system/init
|
|
101
|
+
// only after the first user message, so the run validates identity/config on the first
|
|
102
|
+
// input (init barrier). Observed runtime configuration arrives via session.started /
|
|
103
|
+
// session.configured after that validation.
|
|
104
|
+
const transcriptPath = join(configDir, 'projects', encodeClaudeProjectPath(input.cwd), `${expectedSessionId}.jsonl`)
|
|
105
|
+
const metadata = {
|
|
106
|
+
provider: 'claude' as const,
|
|
107
|
+
sessionId: input.sessionId,
|
|
108
|
+
providerSessionId: expectedSessionId,
|
|
109
|
+
cwd: input.cwd,
|
|
110
|
+
model: firstClaudeModelId(input.model),
|
|
111
|
+
transcriptPath,
|
|
112
|
+
}
|
|
113
|
+
const userInputRequests = await UserInputRequestStore.open({
|
|
114
|
+
sessionId: metadata.sessionId,
|
|
115
|
+
rootDir: userInputRequestStoreRootFromTranscriptPath(metadata.transcriptPath),
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const run = new ClaudeRun({
|
|
119
|
+
metadata,
|
|
120
|
+
query,
|
|
121
|
+
userInputRequests,
|
|
122
|
+
input: inputQueue,
|
|
123
|
+
eventMapperState: state,
|
|
124
|
+
init: {
|
|
125
|
+
expectedSessionId,
|
|
126
|
+
requestedModel: input.model,
|
|
127
|
+
reasoningEffort: input.reasoningEffort,
|
|
128
|
+
environment: this.environment,
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
runtime.attach({
|
|
132
|
+
metadata: run.metadata,
|
|
133
|
+
requestUserInput: async request => await run.requestUserInput(request),
|
|
134
|
+
})
|
|
135
|
+
run.begin()
|
|
136
|
+
return run
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
query.close()
|
|
140
|
+
inputQueue.close()
|
|
141
|
+
throw error
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function firstClaudeModelId(...values: Array<string | null | undefined>): ClaudeModelId | undefined {
|
|
147
|
+
for (const value of values) {
|
|
148
|
+
const parsed = claudeModelIdSchema.safeParse(value)
|
|
149
|
+
if (parsed.success)
|
|
150
|
+
return parsed.data
|
|
151
|
+
}
|
|
152
|
+
return undefined
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function createInputQueue(): AsyncIterable<SDKUserMessage> & { push: (message: SDKUserMessage) => Promise<void>, close: () => void } {
|
|
156
|
+
const items: SDKUserMessage[] = []
|
|
157
|
+
const waiters: Array<(value: IteratorResult<SDKUserMessage>) => void> = []
|
|
158
|
+
let closed = false
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
async push(message) {
|
|
162
|
+
// Never drop input silently: a closed stream means the run has ended.
|
|
163
|
+
if (closed)
|
|
164
|
+
throw new AgentRunStateError({ message: 'Claude input stream is closed.' })
|
|
165
|
+
const waiter = waiters.shift()
|
|
166
|
+
if (waiter)
|
|
167
|
+
waiter({ value: message, done: false })
|
|
168
|
+
else items.push(message)
|
|
169
|
+
},
|
|
170
|
+
close() {
|
|
171
|
+
closed = true
|
|
172
|
+
for (const waiter of waiters.splice(0)) waiter({ value: undefined, done: true })
|
|
173
|
+
},
|
|
174
|
+
[Symbol.asyncIterator]() {
|
|
175
|
+
return {
|
|
176
|
+
async next(): Promise<IteratorResult<SDKUserMessage>> {
|
|
177
|
+
const item = items.shift()
|
|
178
|
+
if (item)
|
|
179
|
+
return await Promise.resolve({ value: item, done: false })
|
|
180
|
+
if (closed)
|
|
181
|
+
return await Promise.resolve({ value: undefined, done: true })
|
|
182
|
+
return await new Promise(resolve => waiters.push(resolve))
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function encodeClaudeProjectPath(cwd: string): string {
|
|
190
|
+
return cwd.replaceAll('/', '-')
|
|
191
|
+
}
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import type { Query, SDKMessage, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk'
|
|
2
|
+
import type {
|
|
3
|
+
AgentError,
|
|
4
|
+
AgentReasoningEffort,
|
|
5
|
+
AgentRunMetadata,
|
|
6
|
+
InterruptAgentCommand,
|
|
7
|
+
RespondUserInputAgentCommand,
|
|
8
|
+
SendPromptCommand,
|
|
9
|
+
StopAgentCommand,
|
|
10
|
+
} from '../../protocol'
|
|
11
|
+
import type { AgentEventDraft } from '../agent-event-stream'
|
|
12
|
+
import type { AgentOperationOptions } from '../agent-operation'
|
|
13
|
+
import type { AgentPromptAcceptance, AgentRun, AgentUserInputSubmission } from '../agent-run'
|
|
14
|
+
import type { UserInputRequestInput, UserInputRequestStore } from '../user-input-request'
|
|
15
|
+
import type { ClaudeEventMapperState } from './claude-event-mapper'
|
|
16
|
+
import z from 'zod'
|
|
17
|
+
import { serializeAgentError } from '../agent-error-serializer'
|
|
18
|
+
import { AgentEventStream } from '../agent-event-stream'
|
|
19
|
+
import { AgentDeliveryUnknownError, AgentNotAcceptedError, AgentProviderMetadataError, AgentProviderProtocolError, AgentRunStateError } from '../agent-runtime-error'
|
|
20
|
+
import { sessionConfiguredEvent } from '../agent-session-events'
|
|
21
|
+
import { deriveMessageId } from '../message-id'
|
|
22
|
+
import { mapClaudeUserMessage, toClaudeUserMessage } from './claude-command-mapper'
|
|
23
|
+
import { mapClaudeMessage } from './claude-event-mapper'
|
|
24
|
+
|
|
25
|
+
// The ONE long-lived SDK input stream created with the query. SDK 0.3.207 Query.streamInput ends
|
|
26
|
+
// stdin after any finite iterable and the transport silently drops later writes, so every user
|
|
27
|
+
// message must go through this original stream instead.
|
|
28
|
+
interface ClaudeInputStream {
|
|
29
|
+
push: (message: SDKUserMessage) => Promise<void>
|
|
30
|
+
close: () => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Live runtime fact (CLI 2.1.207): system/init is emitted only after the first user message is
|
|
34
|
+
// consumed. The run therefore validates identity/config on the first input instead of at open.
|
|
35
|
+
interface ClaudeInitExpectation {
|
|
36
|
+
expectedSessionId: string
|
|
37
|
+
requestedModel?: string
|
|
38
|
+
reasoningEffort?: AgentReasoningEffort
|
|
39
|
+
environment: Record<string, string | undefined>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type ClaudeInitMessage = Extract<SDKMessage, { type: 'system', subtype: 'init' }>
|
|
43
|
+
interface ClaudeCommandLifecycleMessage {
|
|
44
|
+
type: 'command_lifecycle'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The Claude prompt-acceptance receipt is a pinned, live-verified but undocumented CLI frame
|
|
48
|
+
// (SDK 0.3.207 / bundled CLI 2.1.207): it is absent from the SDK's exported SDKMessage union,
|
|
49
|
+
// and its exact shape below was proven by scripts/smoke/claude-command-acceptance-probe.ts —
|
|
50
|
+
// which must be rerun before any SDK/CLI upgrade. On the pinned runtime, any shape or state
|
|
51
|
+
// deviation is provider protocol drift and fails the run closed. The state enum mirrors the pinned
|
|
52
|
+
// CLI's embedded schema exactly: `queued`/`started`/`completed` are positive; `cancelled`
|
|
53
|
+
// (steered-over/aborted) and `discarded` (session ended with the command still queued) are
|
|
54
|
+
// schema-valid NEGATIVE terminals that never mean acceptance.
|
|
55
|
+
const commandLifecycleFrameSchema = z.strictObject({
|
|
56
|
+
type: z.literal('command_lifecycle'),
|
|
57
|
+
command_uuid: z.uuid(),
|
|
58
|
+
session_id: z.uuid(),
|
|
59
|
+
state: z.enum(['queued', 'started', 'completed', 'cancelled', 'discarded']),
|
|
60
|
+
uuid: z.uuid(),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
interface AcceptanceWaiter {
|
|
64
|
+
promise: Promise<void>
|
|
65
|
+
resolve: () => void
|
|
66
|
+
reject: (error: unknown) => void
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function interruptTargetsTurn(interrupt: ClaudeEventMapperState['interrupt'], turnId: string): boolean {
|
|
70
|
+
return interrupt.status !== 'none' && interrupt.turnId === turnId
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface ClaudeRunInput {
|
|
74
|
+
metadata: AgentRunMetadata
|
|
75
|
+
query: Query
|
|
76
|
+
userInputRequests: UserInputRequestStore
|
|
77
|
+
input: ClaudeInputStream
|
|
78
|
+
eventMapperState: ClaudeEventMapperState
|
|
79
|
+
init: ClaudeInitExpectation
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class ClaudeRun implements AgentRun {
|
|
83
|
+
readonly events: AgentEventStream
|
|
84
|
+
readonly metadata: AgentRunMetadata
|
|
85
|
+
private readonly query: Query
|
|
86
|
+
private readonly userInputRequests: UserInputRequestStore
|
|
87
|
+
private readonly input: ClaudeInputStream
|
|
88
|
+
private readonly eventMapperState: ClaudeEventMapperState
|
|
89
|
+
private readonly init: ClaudeInitExpectation
|
|
90
|
+
private readonly initialization: Promise<void>
|
|
91
|
+
private initializationResolve: () => void = () => undefined
|
|
92
|
+
private initializationReject: (error: unknown) => void = () => undefined
|
|
93
|
+
private initialized = false
|
|
94
|
+
private firstInputPushed = false
|
|
95
|
+
private finished = false
|
|
96
|
+
private interruptRequest?: { turnId: string, promise: Promise<void> }
|
|
97
|
+
private readonly acceptanceWaiters = new Map<string, AcceptanceWaiter>()
|
|
98
|
+
|
|
99
|
+
constructor({ metadata, query, userInputRequests, input, eventMapperState, init }: ClaudeRunInput) {
|
|
100
|
+
this.metadata = metadata
|
|
101
|
+
this.query = query
|
|
102
|
+
this.userInputRequests = userInputRequests
|
|
103
|
+
this.input = input
|
|
104
|
+
this.eventMapperState = eventMapperState
|
|
105
|
+
this.init = init
|
|
106
|
+
this.initialization = new Promise<void>((resolve, reject) => {
|
|
107
|
+
this.initializationResolve = resolve
|
|
108
|
+
this.initializationReject = reject
|
|
109
|
+
})
|
|
110
|
+
// The barrier can reject with nothing awaiting it (pre-init EOF/stop); keep that handled.
|
|
111
|
+
this.initialization.catch(() => undefined)
|
|
112
|
+
this.events = new AgentEventStream({
|
|
113
|
+
provider: 'claude',
|
|
114
|
+
sessionId: metadata.sessionId,
|
|
115
|
+
providerSessionId: metadata.providerSessionId,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Starts the single background query consumer. command_lifecycle frames are the acceptance
|
|
120
|
+
// receipt: they are strictly validated and ROUTED to settle the matching sendPrompt waiter
|
|
121
|
+
// (never skipped, never mapped to an AgentEvent), including the first input's queued frame that
|
|
122
|
+
// legitimately precedes system/init. PRE_INIT is otherwise strict: the next non-lifecycle
|
|
123
|
+
// message must be a matching system/init, and the run's own session lifecycle AgentEvents
|
|
124
|
+
// (session.started/configured/state) are emitted only after that init validation.
|
|
125
|
+
begin(): void {
|
|
126
|
+
void this.consume()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pushEvent(draft: AgentEventDraft): void {
|
|
130
|
+
if (this.finished)
|
|
131
|
+
return
|
|
132
|
+
const decorated = this.userInputRequests.decorateTurnEnded(draft)
|
|
133
|
+
if (decorated.type === 'session.state.changed' && decorated.payload.state === 'idle' && this.userInputRequests.shouldHoldIdle()) {
|
|
134
|
+
this.events.push({ type: 'session.state.changed', payload: { state: 'waiting_for_user_input' } })
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
this.events.push(decorated)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async requestUserInput(input: UserInputRequestInput): Promise<{ requestId: string }> {
|
|
141
|
+
this.assertRunning()
|
|
142
|
+
const request = this.userInputRequests.create(input)
|
|
143
|
+
this.pushEvent(request.event)
|
|
144
|
+
return { requestId: request.requestId }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async prepareWorkspace(options: AgentOperationOptions = {}): Promise<void> {
|
|
148
|
+
options.signal?.throwIfAborted()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Claude discovers repository guidance from the filesystem; there is no provider-side root registry.
|
|
152
|
+
async addWorkspaceRepositoryRoot(_root: string, options: AgentOperationOptions = {}): Promise<void> {
|
|
153
|
+
options.signal?.throwIfAborted()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async sendPrompt(command: SendPromptCommand, options: AgentOperationOptions = {}): Promise<AgentPromptAcceptance> {
|
|
157
|
+
this.assertCommandTargetsRun(command)
|
|
158
|
+
this.assertRunning()
|
|
159
|
+
options.signal?.throwIfAborted()
|
|
160
|
+
const input = { prompt: command.prompt, mode: command.mode, commandId: command.commandId, origin: command.origin, author: command.author, senderContext: options.senderContext }
|
|
161
|
+
const message = await mapClaudeUserMessage(input, options.inputAttachmentPreparation)
|
|
162
|
+
options.signal?.throwIfAborted()
|
|
163
|
+
// Register before the push: the CLI's queued receipt can outrun the local push resolution,
|
|
164
|
+
// and a receipt with no registered waiter would be lost.
|
|
165
|
+
const clientMessageId = deriveMessageId(command.commandId)
|
|
166
|
+
const receipt = this.registerAcceptanceWaiter(clientMessageId)
|
|
167
|
+
try {
|
|
168
|
+
await this.deliverInput(message)
|
|
169
|
+
// `submitted` requires the provider's positive acceptance receipt — the pinned
|
|
170
|
+
// command_lifecycle frame matching this command's derived uuid and this session —
|
|
171
|
+
// in addition to delivery (and, for the first input, validated init identity).
|
|
172
|
+
// Local queue push, transport write, and init alone are never acceptance. No timeout:
|
|
173
|
+
// provider close/error settles the wait as delivery-unknown.
|
|
174
|
+
await receipt.promise
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
this.acceptanceWaiters.delete(clientMessageId)
|
|
178
|
+
}
|
|
179
|
+
return { kind: 'submitted' }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async respondUserInput(command: RespondUserInputAgentCommand, options: AgentOperationOptions = {}): Promise<AgentUserInputSubmission> {
|
|
183
|
+
this.assertCommandTargetsRun(command)
|
|
184
|
+
this.assertRunning()
|
|
185
|
+
options.signal?.throwIfAborted()
|
|
186
|
+
const resolved = this.userInputRequests.resolve(command)
|
|
187
|
+
if (!resolved)
|
|
188
|
+
return 'already_delivered'
|
|
189
|
+
// Deliver first: on a prebound resume this is the run's first input, and the resolved
|
|
190
|
+
// event must not precede the lifecycle events the init barrier emits.
|
|
191
|
+
await this.deliverInput(toClaudeUserMessage({ prompt: resolved.prompt, mode: 'next', commandId: command.commandId }))
|
|
192
|
+
if (resolved.event)
|
|
193
|
+
this.pushEvent(resolved.event)
|
|
194
|
+
this.userInputRequests.markDelivered(command.requestId)
|
|
195
|
+
return 'submitted'
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async interrupt(command: InterruptAgentCommand, options: AgentOperationOptions = {}): Promise<void> {
|
|
199
|
+
this.assertCommandTargetsRun(command)
|
|
200
|
+
this.assertRunning()
|
|
201
|
+
options.signal?.throwIfAborted()
|
|
202
|
+
const turnId = this.eventMapperState.currentTurnId
|
|
203
|
+
if (!turnId)
|
|
204
|
+
return
|
|
205
|
+
if (this.eventMapperState.interrupt.status === 'accepted' && this.eventMapperState.interrupt.turnId === turnId)
|
|
206
|
+
return
|
|
207
|
+
if (this.interruptRequest?.turnId === turnId)
|
|
208
|
+
return await this.interruptRequest.promise
|
|
209
|
+
|
|
210
|
+
const promise = this.requestInterrupt(turnId)
|
|
211
|
+
this.interruptRequest = { turnId, promise }
|
|
212
|
+
try {
|
|
213
|
+
await promise
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
if (this.interruptRequest?.promise === promise)
|
|
217
|
+
this.interruptRequest = undefined
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private async requestInterrupt(turnId: string): Promise<void> {
|
|
222
|
+
this.eventMapperState.interrupt = { status: 'requesting', turnId }
|
|
223
|
+
try {
|
|
224
|
+
// `still_queued` describes future Claude inputs; foreground-turn interruption does not cancel them.
|
|
225
|
+
await this.query.interrupt()
|
|
226
|
+
if (this.eventMapperState.interrupt.status === 'requesting' && this.eventMapperState.interrupt.turnId === turnId)
|
|
227
|
+
this.eventMapperState.interrupt = { status: 'accepted', turnId }
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
if (interruptTargetsTurn(this.eventMapperState.interrupt, turnId))
|
|
231
|
+
this.eventMapperState.interrupt = { status: 'none' }
|
|
232
|
+
throw error
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async stop(command: StopAgentCommand, options: AgentOperationOptions = {}): Promise<void> {
|
|
237
|
+
this.assertCommandTargetsRun(command)
|
|
238
|
+
options.signal?.throwIfAborted()
|
|
239
|
+
if (this.finished)
|
|
240
|
+
return
|
|
241
|
+
this.events.push({ type: 'session.state.changed', payload: { state: 'stopping' } })
|
|
242
|
+
if (!this.initialized)
|
|
243
|
+
this.initializationReject(new AgentRunStateError({ message: 'Claude run stopped before initialization.' }))
|
|
244
|
+
this.query.close()
|
|
245
|
+
this.finish('stopped')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
finish(reason: 'completed' | 'stopped' | 'interrupted' | 'failed', error?: AgentError): void {
|
|
249
|
+
if (this.finished)
|
|
250
|
+
return
|
|
251
|
+
this.finished = true
|
|
252
|
+
// The run ends before pending inputs saw their acceptance receipt: each may already be in
|
|
253
|
+
// the CLI stream, so their outcome is delivery-unknown — never a hang, retry, or terminal.
|
|
254
|
+
for (const waiter of this.acceptanceWaiters.values()) {
|
|
255
|
+
waiter.reject(new AgentDeliveryUnknownError({
|
|
256
|
+
message: `Claude run ended (${reason}) before the input's acceptance receipt was observed.`,
|
|
257
|
+
}))
|
|
258
|
+
}
|
|
259
|
+
this.acceptanceWaiters.clear()
|
|
260
|
+
this.events.push({ type: 'session.ended', payload: error ? { reason, error } : { reason } })
|
|
261
|
+
this.events.push({ type: 'session.state.changed', payload: { state: reason === 'failed' ? 'failed' : 'stopped' } })
|
|
262
|
+
this.events.end()
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// One pending acceptance per derived command uuid; the promise is pre-observed so a rejection
|
|
266
|
+
// while the caller is still awaiting delivery/init can never become an unhandled rejection.
|
|
267
|
+
private registerAcceptanceWaiter(clientMessageId: string): AcceptanceWaiter {
|
|
268
|
+
if (this.finished)
|
|
269
|
+
throw new AgentRunStateError({ message: 'Claude run has stopped.' })
|
|
270
|
+
if (this.acceptanceWaiters.has(clientMessageId))
|
|
271
|
+
throw new AgentRunStateError({ message: 'A prompt with this command id is already awaiting acceptance.' })
|
|
272
|
+
let resolve: () => void = () => undefined
|
|
273
|
+
let reject: (error: unknown) => void = () => undefined
|
|
274
|
+
const promise = new Promise<void>((promiseResolve, promiseReject) => {
|
|
275
|
+
resolve = promiseResolve
|
|
276
|
+
reject = promiseReject
|
|
277
|
+
})
|
|
278
|
+
promise.catch(() => undefined)
|
|
279
|
+
const waiter: AcceptanceWaiter = { promise, resolve, reject }
|
|
280
|
+
this.acceptanceWaiters.set(clientMessageId, waiter)
|
|
281
|
+
return waiter
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Strict pinned-runtime boundary for the undocumented receipt frame: shape/state deviation and
|
|
285
|
+
// session identity mismatch are provider protocol drift and fail the run closed. `cancelled`
|
|
286
|
+
// (a steered-over/aborted command) and `discarded` (the session ended with the command still
|
|
287
|
+
// queued) are valid observed states but NEGATIVE terminals — never a positive acceptance, so
|
|
288
|
+
// they reject any pending waiter as delivery-unknown so command processing can advance.
|
|
289
|
+
// Valid positive frames without a pending
|
|
290
|
+
// waiter are internal lifecycle for already-settled or non-prompt inputs and are ignored.
|
|
291
|
+
private settleAcceptance(frame: ClaudeCommandLifecycleMessage): void {
|
|
292
|
+
const parsed = commandLifecycleFrameSchema.safeParse(frame)
|
|
293
|
+
if (!parsed.success) {
|
|
294
|
+
throw new AgentProviderProtocolError({
|
|
295
|
+
message: 'Claude emitted a command_lifecycle frame with an unrecognized shape or state.',
|
|
296
|
+
cause: parsed.error,
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
if (parsed.data.session_id !== this.metadata.providerSessionId) {
|
|
300
|
+
throw new AgentProviderProtocolError({
|
|
301
|
+
message: 'Claude command_lifecycle frame targets a different session.',
|
|
302
|
+
details: { expected: this.metadata.providerSessionId, actual: parsed.data.session_id },
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
if (parsed.data.state === 'cancelled' || parsed.data.state === 'discarded') {
|
|
306
|
+
this.acceptanceWaiters.get(parsed.data.command_uuid)?.reject(new AgentDeliveryUnknownError({
|
|
307
|
+
message: `Claude command ended as ${parsed.data.state} before acceptance was observed.`,
|
|
308
|
+
}))
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
this.acceptanceWaiters.get(parsed.data.command_uuid)?.resolve()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Exactly one message may enter the stream before init: the first input wakes the CLI and then
|
|
315
|
+
// awaits identity/config validation; a concurrent input waits for the shared barrier first.
|
|
316
|
+
private async deliverInput(message: SDKUserMessage): Promise<void> {
|
|
317
|
+
if (this.initialized) {
|
|
318
|
+
await this.pushOwnedStream(message)
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
if (!this.firstInputPushed) {
|
|
322
|
+
// Claim the slot before the push so a concurrent input waits on the barrier, but
|
|
323
|
+
// release it on failure: the owned queue rejects before enqueue, so a failed first
|
|
324
|
+
// push provably wrote nothing and the next input may claim the slot again.
|
|
325
|
+
this.firstInputPushed = true
|
|
326
|
+
try {
|
|
327
|
+
await this.pushOwnedStream(message)
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
this.firstInputPushed = false
|
|
331
|
+
throw error
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
await this.initialization
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
// The message already entered the CLI stream, so an init failure cannot prove
|
|
338
|
+
// non-delivery: retrying could duplicate the input.
|
|
339
|
+
throw new AgentDeliveryUnknownError({
|
|
340
|
+
message: 'Claude initialization failed after the first input entered the stream.',
|
|
341
|
+
cause: error,
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
await this.initialization
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
// This input was never written: the barrier failed while it waited (proven not accepted).
|
|
351
|
+
throw new AgentNotAcceptedError({
|
|
352
|
+
message: 'Claude initialization failed before this input was written.',
|
|
353
|
+
recovery: { kind: 'retry', reason: 'provider_initialization_failed' },
|
|
354
|
+
cause: error,
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
await this.pushOwnedStream(message)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// The owned input queue rejects synchronously before enqueue when the stream is closed, so a
|
|
361
|
+
// rejected push is proven non-delivery and safe to retry against a fresh run.
|
|
362
|
+
private async pushOwnedStream(message: SDKUserMessage): Promise<void> {
|
|
363
|
+
try {
|
|
364
|
+
await this.input.push(message)
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
if (error instanceof AgentRunStateError) {
|
|
368
|
+
throw new AgentNotAcceptedError({
|
|
369
|
+
message: 'Claude input stream is closed; the input was not written.',
|
|
370
|
+
recovery: { kind: 'retry', reason: 'provider_stream_closed' },
|
|
371
|
+
cause: error,
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
throw error
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private async consume(): Promise<void> {
|
|
379
|
+
try {
|
|
380
|
+
for await (const raw of this.query) {
|
|
381
|
+
const message = raw as SDKMessage | ClaudeCommandLifecycleMessage
|
|
382
|
+
// Acceptance receipts are routed before PRE_INIT and normal mapping: the first
|
|
383
|
+
// input's queued frame legitimately precedes system/init.
|
|
384
|
+
if (message.type === 'command_lifecycle') {
|
|
385
|
+
this.settleAcceptance(message)
|
|
386
|
+
continue
|
|
387
|
+
}
|
|
388
|
+
if (!this.initialized) {
|
|
389
|
+
this.completeInitialization(message)
|
|
390
|
+
continue
|
|
391
|
+
}
|
|
392
|
+
for (const draft of mapClaudeMessage(message, this.eventMapperState)) this.pushEvent(draft)
|
|
393
|
+
}
|
|
394
|
+
// An intentional stop already finalized the run; the ended stream is expected then.
|
|
395
|
+
if (this.finished)
|
|
396
|
+
return
|
|
397
|
+
if (!this.initialized) {
|
|
398
|
+
throw new AgentProviderProtocolError({ message: 'Claude SDK ended before emitting system init.' })
|
|
399
|
+
}
|
|
400
|
+
this.finish('completed')
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
this.failRun(error)
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
this.input.close()
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private completeInitialization(message: SDKMessage): void {
|
|
411
|
+
if (message.type !== 'system' || message.subtype !== 'init') {
|
|
412
|
+
throw new AgentProviderProtocolError({ message: 'Claude SDK emitted an unexpected message before system init.' })
|
|
413
|
+
}
|
|
414
|
+
const init: ClaudeInitMessage = message
|
|
415
|
+
if (!init.session_id) {
|
|
416
|
+
throw new AgentProviderMetadataError({ message: 'Claude SDK init did not include session_id.', details: { provider: 'claude' } })
|
|
417
|
+
}
|
|
418
|
+
if (init.session_id !== this.init.expectedSessionId) {
|
|
419
|
+
throw new AgentProviderProtocolError({
|
|
420
|
+
message: 'Claude SDK init session_id does not match the requested session.',
|
|
421
|
+
details: { expected: this.init.expectedSessionId, actual: init.session_id },
|
|
422
|
+
})
|
|
423
|
+
}
|
|
424
|
+
const effectiveModel = init.model ?? this.init.requestedModel
|
|
425
|
+
if (!effectiveModel) {
|
|
426
|
+
throw new AgentProviderMetadataError({ message: 'Claude SDK init did not include model.', details: { provider: 'claude' } })
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
this.pushEvent({ type: 'session.started', payload: this.metadata })
|
|
430
|
+
this.pushEvent(sessionConfiguredEvent({
|
|
431
|
+
model: effectiveModel,
|
|
432
|
+
cwd: init.cwd ?? this.metadata.cwd,
|
|
433
|
+
reasoningEffort: this.init.reasoningEffort,
|
|
434
|
+
toolNames: init.tools,
|
|
435
|
+
environment: this.init.environment,
|
|
436
|
+
}))
|
|
437
|
+
this.pushEvent({ type: 'session.state.changed', payload: { state: 'ready' } })
|
|
438
|
+
for (const draft of mapClaudeMessage(init, this.eventMapperState)) this.pushEvent(draft)
|
|
439
|
+
this.initialized = true
|
|
440
|
+
this.initializationResolve()
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private failRun(error: unknown): void {
|
|
444
|
+
if (this.finished)
|
|
445
|
+
return
|
|
446
|
+
const agentError = serializeAgentError(error)
|
|
447
|
+
if (!this.initialized) {
|
|
448
|
+
// Pre-init failure: the first-input promise must reject typed instead of hanging.
|
|
449
|
+
this.initializationReject(error)
|
|
450
|
+
this.query.close()
|
|
451
|
+
this.finish('failed', agentError)
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
this.pushEvent({ type: 'error', payload: { message: agentError.message, fatal: true, source: 'runtime', error: agentError } })
|
|
455
|
+
this.finish('failed', agentError)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private assertRunning(): void {
|
|
459
|
+
if (this.finished)
|
|
460
|
+
throw new AgentRunStateError({ message: 'Claude run has stopped.' })
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private assertCommandTargetsRun(command: { provider: string, sessionId: string, providerSessionId: string }): void {
|
|
464
|
+
if (command.provider !== 'claude' || command.sessionId !== this.metadata.sessionId || command.providerSessionId !== this.metadata.providerSessionId) {
|
|
465
|
+
throw new AgentRunStateError({ message: 'Command does not target this Claude run.' })
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|