feat(coding): add core host api composition

This commit is contained in:
2026-08-23 20:00:35 +08:00
parent 98bac20639
commit 22b4a9f9c4
23 changed files with 2258 additions and 59 deletions

View File

@@ -1,8 +1,12 @@
import type {
CodingConversationRuntime,
CodingRuntimeCommand,
CodingRuntimeDiagnostics,
CodingRuntimeErrorCode,
CodingRuntimePublicError,
ConversationModelState,
ConversationInteraction,
ConversationInteractionResponse,
ConversationPatch,
ConversationPatchEnvelope,
ConversationRuntimeState,
@@ -35,6 +39,7 @@ export class CodingRuntimeContractError extends Error {
export interface InMemoryConversationRuntimeOptions {
snapshots?: ConversationSnapshot[];
commands?: CodingRuntimeCommand[];
now?: () => number;
createId?: (kind: 'run' | 'node' | 'queue' | 'compaction') => string;
}
@@ -79,11 +84,15 @@ export class InMemoryConversationRuntime implements CodingConversationRuntime {
private readonly promptAcceptances = new Map<string, PromptAcceptance>();
private readonly queueAcceptances = new Map<string, QueueAcceptance>();
private readonly now: () => number;
private readonly commands: CodingRuntimeCommand[];
private readonly createId: InMemoryConversationRuntimeOptions['createId'];
private nextId = 0;
private providerRevision = 1;
private resourcesRevision = 1;
constructor(options: InMemoryConversationRuntimeOptions = {}) {
this.now = options.now ?? Date.now;
this.commands = clone(options.commands ?? []);
this.createId = options.createId;
for (const snapshot of options.snapshots ?? []) {
const state = createConversationReducerState(snapshot);
@@ -176,6 +185,19 @@ export class InMemoryConversationRuntime implements CodingConversationRuntime {
}
async prompt(input: PromptConversationInput): Promise<PromptAcceptance> {
if (input.mode === 'steer' || input.mode === 'follow-up') {
const acceptance = input.mode === 'steer'
? await this.steer(input)
: await this.followUp(input);
return {
accepted: true,
conversationId: input.conversationId,
clientRequestId: input.clientRequestId,
runId: this.snapshot(input.conversationId).run.runId ?? this.id('run'),
mode: input.mode,
queuePosition: acceptance.queuePosition,
};
}
const key = `${input.conversationId}:${input.clientRequestId}`;
const prior = this.promptAcceptances.get(key);
if (prior) return clone(prior);
@@ -432,6 +454,7 @@ export class InMemoryConversationRuntime implements CodingConversationRuntime {
}
async dispose(conversationId: string): Promise<void> {
if (!this.states.has(conversationId)) return;
const snapshot = this.snapshot(conversationId);
this.replaceSnapshot({
...snapshot,
@@ -444,6 +467,81 @@ export class InMemoryConversationRuntime implements CodingConversationRuntime {
});
}
async listCommands(conversationId: string): Promise<CodingRuntimeCommand[]> {
return this.states.has(conversationId) ? clone(this.commands) : [];
}
async listInteractions(conversationId?: string): Promise<ConversationInteraction[]> {
return [...this.states.values()].flatMap((state) => {
const snapshot = state.snapshot;
if (!snapshot || (conversationId && snapshot.conversation.id !== conversationId)) return [];
return clone(snapshot.pendingInteractions);
});
}
async respondInteraction(
conversationId: string,
response: ConversationInteractionResponse,
): Promise<void> {
const interaction = this.snapshot(conversationId).pendingInteractions.find(
({ id }) => id === response.interactionId,
);
if (!interaction) {
throw new CodingRuntimeContractError(
'CODING_RUNTIME_PROTOCOL_ERROR',
'Conversation interaction is not pending',
true,
);
}
this.emit(conversationId, { op: 'interaction.remove', interactionId: interaction.id }, interaction.runId);
}
getDiagnostics(): CodingRuntimeDiagnostics {
return {
revision: {
provider: this.providerRevision,
resources: this.resourcesRevision,
},
workers: [...this.states.values()].flatMap((state) => {
const snapshot = state.snapshot;
if (!snapshot) return [];
const runtimeState = snapshot.worker.status === 'error'
? 'crashed' as const
: snapshot.worker.status === 'starting' || snapshot.worker.status === 'recovering'
? 'spawning' as const
: snapshot.worker.status === 'stopped'
? 'idle' as const
: snapshot.run.status === 'queued'
? 'queued' as const
: snapshot.run.status === 'running'
? 'running' as const
: 'ready' as const;
return [{
conversationId: snapshot.conversation.id,
generation: snapshot.worker.generation,
state: runtimeState,
stage: runtimeState === 'crashed'
? 'failed' as const
: runtimeState === 'spawning'
? 'starting' as const
: runtimeState === 'queued'
? 'queued' as const
: runtimeState === 'running'
? 'running' as const
: 'idle' as const,
}];
}),
};
}
markProviderStale(): void {
this.providerRevision += 1;
}
markResourcesStale(): void {
this.resourcesRevision += 1;
}
subscribe(listener: (patch: ConversationPatchEnvelope) => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);