feat: support opencode

This commit is contained in:
weishu
2026-01-29 10:34:57 +08:00
parent 66ab96bf38
commit 70b5c22c8f
47 changed files with 2737 additions and 28 deletions
@@ -47,9 +47,39 @@ function normalizePlanEntries(entries: unknown): PlanItem[] {
export class AcpMessageHandler {
private readonly toolCalls = new Map<string, { name: string; input: unknown }>();
private bufferedText = '';
constructor(private readonly onMessage: (message: AgentMessage) => void) {}
flushText(): void {
if (!this.bufferedText) {
return;
}
this.onMessage({ type: 'text', text: this.bufferedText });
this.bufferedText = '';
}
private appendTextChunk(text: string): void {
if (!text) {
return;
}
if (!this.bufferedText) {
this.bufferedText = text;
return;
}
if (text === this.bufferedText) {
return;
}
if (text.startsWith(this.bufferedText)) {
this.bufferedText = text;
return;
}
if (this.bufferedText.startsWith(text)) {
return;
}
this.bufferedText += text;
}
handleUpdate(update: unknown): void {
if (!isObject(update)) return;
const updateType = asString(update.sessionUpdate);
@@ -59,7 +89,7 @@ export class AcpMessageHandler {
const content = update.content;
const text = extractTextContent(content);
if (text) {
this.onMessage({ type: 'text', text });
this.appendTextChunk(text);
}
return;
}
@@ -69,16 +99,19 @@ export class AcpMessageHandler {
}
if (updateType === ACP_SESSION_UPDATE_TYPES.toolCall) {
this.flushText();
this.handleToolCall(update);
return;
}
if (updateType === ACP_SESSION_UPDATE_TYPES.toolCallUpdate) {
this.flushText();
this.handleToolCallUpdate(update);
return;
}
if (updateType === ACP_SESSION_UPDATE_TYPES.plan) {
this.flushText();
const items = normalizePlanEntries(update.entries);
if (items.length > 0) {
this.onMessage({ type: 'plan', items });
@@ -106,6 +106,31 @@ export class AcpSdkBackend implements AgentBackend {
return sessionId;
}
async loadSession(config: AgentSessionConfig & { sessionId: string }): Promise<string> {
if (!this.transport) {
throw new Error('ACP transport not initialized');
}
const response = await withRetry(
() => this.transport!.sendRequest('session/load', {
sessionId: config.sessionId,
cwd: config.cwd,
mcpServers: config.mcpServers
}),
{
...AcpSdkBackend.INIT_RETRY_OPTIONS,
onRetry: (error, attempt, nextDelayMs) => {
logger.debug(`[ACP] session/load attempt ${attempt} failed, retrying in ${nextDelayMs}ms`, error);
}
}
);
const loadedSessionId = isObject(response) ? asString(response.sessionId) : null;
const sessionId = loadedSessionId ?? config.sessionId;
this.activeSessionId = sessionId;
return sessionId;
}
async prompt(
sessionId: string,
content: PromptContent[],
@@ -129,9 +154,11 @@ export class AcpSdkBackend implements AgentBackend {
const stopReason = isObject(response) ? asString(response.stopReason) : null;
if (stopReason) {
this.messageHandler?.flushText();
onUpdate({ type: 'turn_complete', stopReason });
}
} finally {
this.messageHandler?.flushText();
this.messageHandler = null;
this.isProcessingMessage = false;
this.notifyResponseComplete();