feat(coding): add core host api composition
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user