fix(agent): stabilize sandboxed streaming workflows

This commit is contained in:
2026-08-26 12:26:29 +08:00
parent 80cfc1f2ab
commit 4ec010455b
12 changed files with 916 additions and 34 deletions
+51 -9
View File
@@ -1,6 +1,7 @@
import {
createOpencode,
type OpencodeClient,
type OutputFormat,
} from "@opencode-ai/sdk/v2";
import { randomUUID } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
@@ -38,6 +39,10 @@ type RuntimeModelOverride = {
modelID: string;
};
type RuntimePromptOptions = {
format?: OutputFormat;
};
export type PermissionReply = "once" | "always" | "reject";
export type QuestionAnswers = string[][];
@@ -180,7 +185,12 @@ export class OpencodeRuntimeAdapter {
return this.messages(sessionId);
}
async prompt(sessionId: string, text: string, model?: RuntimeModelOverride) {
async prompt(
sessionId: string,
text: string,
model?: RuntimeModelOverride,
options?: RuntimePromptOptions,
) {
const client = await this.ensureClient();
const startedAt = Date.now();
logDevelopmentDebug(
@@ -194,6 +204,7 @@ export class OpencodeRuntimeAdapter {
await client.session.prompt({
sessionID: sessionId,
model,
...(options?.format ? { format: options.format } : {}),
parts: [{ type: "text", text }],
});
logDevelopmentDebug(
@@ -299,35 +310,41 @@ export class OpencodeRuntimeAdapter {
);
}
async subscribeEvents() {
async subscribeEvents(directory?: string) {
const client = await this.ensureClient();
const response = await client.event.subscribe();
const response = await client.event.subscribe(
directory ? { directory } : undefined,
);
return response.stream;
}
async replyPermission(options: {
requestId: string;
sessionId?: string;
directory?: string;
reply: PermissionReply;
message?: string;
}) {
const client = await this.ensureClient();
const directory = await this.resolveInteractionDirectory(options);
if ("permission" in client && client.permission?.reply) {
const response = await client.permission.reply({
requestID: options.requestId,
directory,
reply: options.reply,
message: options.message,
});
return response.data;
return requireData(response.data, "permission.reply");
}
if ("permission" in client && client.permission?.respond && options.sessionId) {
const response = await client.permission.respond({
sessionID: options.sessionId,
permissionID: options.requestId,
directory,
response: options.reply,
});
return response.data;
return requireData(response.data, "permission.respond");
}
throw new Error("opencode permission reply API is unavailable");
@@ -336,16 +353,19 @@ export class OpencodeRuntimeAdapter {
async replyQuestion(options: {
requestId: string;
sessionId?: string;
directory?: string;
answers: QuestionAnswers;
}) {
const client = await this.ensureClient();
const directory = await this.resolveInteractionDirectory(options);
if ("question" in client && client.question?.reply) {
try {
const response = await client.question.reply({
requestID: options.requestId,
directory,
answers: options.answers,
});
return response.data;
return requireData(response.data, "question.reply");
} catch (error) {
if (!options.sessionId) {
throw error;
@@ -360,6 +380,7 @@ export class OpencodeRuntimeAdapter {
reply?: (parameters: {
sessionID: string;
requestID: string;
directory?: string;
questionV2Reply: { answers: QuestionAnswers };
}) => Promise<{ data: unknown }>;
};
@@ -371,11 +392,12 @@ export class OpencodeRuntimeAdapter {
const response = await v2Question.reply({
sessionID: options.sessionId,
requestID: options.requestId,
directory,
questionV2Reply: {
answers: options.answers,
},
});
return response.data;
return requireData(response.data, "question.v2.reply");
}
throw new Error("opencode question reply API is unavailable");
@@ -384,14 +406,17 @@ export class OpencodeRuntimeAdapter {
async rejectQuestion(options: {
requestId: string;
sessionId?: string;
directory?: string;
}) {
const client = await this.ensureClient();
const directory = await this.resolveInteractionDirectory(options);
if ("question" in client && client.question?.reject) {
try {
const response = await client.question.reject({
requestID: options.requestId,
directory,
});
return response.data;
return requireData(response.data, "question.reject");
} catch (error) {
if (!options.sessionId) {
throw error;
@@ -406,6 +431,7 @@ export class OpencodeRuntimeAdapter {
reject?: (parameters: {
sessionID: string;
requestID: string;
directory?: string;
}) => Promise<{ data: unknown }>;
};
};
@@ -416,13 +442,29 @@ export class OpencodeRuntimeAdapter {
const response = await v2Question.reject({
sessionID: options.sessionId,
requestID: options.requestId,
directory,
});
return response.data;
return requireData(response.data, "question.v2.reject");
}
throw new Error("opencode question reject API is unavailable");
}
private async resolveInteractionDirectory(options: {
sessionId?: string;
directory?: string;
}): Promise<string | undefined> {
const directory = options.directory?.trim();
if (directory) {
return directory;
}
if (!options.sessionId) {
return undefined;
}
const session = await this.getSession(options.sessionId);
return session.directory;
}
async dispose(): Promise<void> {
if (this.toolOutputCleanupTimer) {
clearInterval(this.toolOutputCleanupTimer);