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,4 +1,7 @@
import type { ConversationInteraction } from '../contracts';
import type {
ConversationInteraction,
ConversationInteractionResponse,
} from '../contracts';
import type { PiRpcCommand, PiRpcEvent } from './rpc-client';
import type { PiGenerationResourceInput, PiWorkerPoolState } from './worker-pool';
@@ -17,12 +20,6 @@ interface StoredInteraction {
untrack(): void;
}
export type PiInteractionResponse =
| { interactionId: string; cancelled: true }
| { interactionId: string; optionId: string }
| { interactionId: string; confirmed: boolean }
| { interactionId: string; value: string };
function dialogEvent(event: PiRpcEvent): event is PiRpcEvent & {
id: string;
method: 'select' | 'confirm' | 'input' | 'editor';
@@ -101,7 +98,7 @@ export class PiInteractionStore {
return structuredClone(interaction);
}
async respond(conversationId: string, response: PiInteractionResponse): Promise<ConversationInteraction> {
async respond(conversationId: string, response: ConversationInteractionResponse): Promise<ConversationInteraction> {
const stored = this.pending.get(this.key(conversationId, response.interactionId));
if (!stored) throw new Error('Pi interaction is not pending');
if (stored.phase === 'responding') throw new Error('Pi interaction response is already in progress');

View File

@@ -12,7 +12,11 @@ import {
import { PiProviderRefreshCoordinator } from './provider-refresh';
import type {
CodingConversationRuntime,
CodingRuntimeCommand,
CodingRuntimeDiagnostics,
CodingRuntimePublicError,
ConversationInteraction,
ConversationInteractionResponse,
ConversationModelState,
ConversationPatch,
ConversationPatchEnvelope,
@@ -78,7 +82,6 @@ import { PiManagedExtensionHost } from './extension-host';
import type { PiSubagentScheduler } from './subagent';
import {
PiInteractionStore,
type PiInteractionResponse,
} from './interaction';
import {
PiExtensionUiProjector,
@@ -836,6 +839,52 @@ export class PiConversationRuntime implements CodingConversationRuntime {
this.hydrationFlights.delete(conversationId);
}
async listCommands(conversationId: string): Promise<CodingRuntimeCommand[]> {
const state = this.pool.getState(conversationId);
if (!state || state.state === 'spawning' || state.state === 'crashed') return [];
const response = await this.pool.request<unknown>(
conversationId,
{ type: 'get_commands' },
{ retry: 'read-only-once' },
);
const record = response.data && typeof response.data === 'object' && !Array.isArray(response.data)
? response.data as Record<string, unknown>
: null;
const candidates = Array.isArray(response.data)
? response.data
: Array.isArray(record?.commands)
? record.commands
: [];
return candidates.flatMap((candidate) => {
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return [];
const command = candidate as Record<string, unknown>;
const name = typeof command.name === 'string' ? command.name.trim() : '';
if (!name) return [];
return [{
name,
...(typeof command.description === 'string'
? { description: command.description }
: {}),
}];
});
}
async listInteractions(conversationId?: string): Promise<ConversationInteraction[]> {
return this.interactions.list(conversationId);
}
getDiagnostics(): CodingRuntimeDiagnostics {
return this.pool.getDiagnostics();
}
markProviderStale(): void {
this.pool.markProviderStale();
}
markResourcesStale(): void {
this.pool.markResourcesStale();
}
subscribe(listener: (patch: ConversationPatchEnvelope) => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
@@ -843,7 +892,7 @@ export class PiConversationRuntime implements CodingConversationRuntime {
async respondInteraction(
conversationId: string,
response: PiInteractionResponse,
response: ConversationInteractionResponse,
): Promise<void> {
await this.interactions.respond(conversationId, response);
}

View File

@@ -114,7 +114,9 @@ export class PiSessionRegistry {
if (!project) throw new Error('Coding project does not exist');
const configRead = await readCodingProjectConfigV2(project.path);
if (configRead.status !== 'valid') throw new Error('Coding project configuration is unavailable');
const agent = configRead.config.agents.find((candidate) => candidate.id === input.agentId);
const agent = configRead.config.agents.find((candidate) => (
candidate.id === input.agentId && candidate.enabled && !candidate.archivedAt
));
if (!agent) throw new Error('Coding Agent does not exist');
const store = createCodingConversationStore(project.path);
const conversation = await store.get(input.conversationId);

View File

@@ -1,5 +1,8 @@
import type { PrepareConversationInput } from '../contracts';
import type { ConversationModelState } from '../contracts';
import type {
CodingRuntimeDiagnostics,
ConversationModelState,
PrepareConversationInput,
} from '../contracts';
import type { PiProcessError, PiProcessErrorCode } from './process-errors';
import type {
PiRpcCommand,
@@ -347,6 +350,27 @@ export class PiWorkerPool {
return record ? this.publicState(record) : null;
}
getDiagnostics(): CodingRuntimeDiagnostics {
const stage = (
state: PiWorkerPoolState['state'],
): CodingRuntimeDiagnostics['workers'][number]['stage'] => {
if (state === 'spawning') return 'starting';
if (state === 'queued') return 'queued';
if (state === 'running') return 'running';
if (state === 'crashed') return 'failed';
return 'idle';
};
return {
revision: this.revisions.current,
workers: [...this.workers.values()].map((record) => ({
conversationId: record.conversation.conversationId,
generation: record.generation,
state: record.state,
stage: stage(record.state),
})),
};
}
async reclaimIdleWorker(signal?: AbortSignal): Promise<boolean> {
while (true) {
if (signal?.aborted) throw new Error('Pi idle worker reclaim cancelled');