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