feat(agent): add frontend action bridge
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
ResultReferenceStore,
|
||||
} from "./results/store.js";
|
||||
import { buildChatRouter } from "./routes/chat.js";
|
||||
import { FrontendActionCoordinator, FrontendActionError } from "./frontendAction/coordinator.js";
|
||||
import { opencodeRuntime } from "./runtime/opencode.js";
|
||||
import {
|
||||
getRuntimeSessionContext,
|
||||
@@ -38,6 +39,7 @@ const learningOrchestrator = new LearningOrchestrator(
|
||||
const resultReferenceStore = new ResultReferenceStore();
|
||||
const resultReferenceResolver = new ResultReferenceResolver(resultReferenceStore);
|
||||
const internalToken = config.AGENT_INTERNAL_TOKEN ?? randomUUID();
|
||||
const frontendActionCoordinator = new FrontendActionCoordinator();
|
||||
|
||||
// 这个 token 只用于仍需服务端上下文的工具桥(store_render_ref)。
|
||||
process.env.TJWATER_AGENT_INTERNAL_TOKEN = internalToken;
|
||||
@@ -45,6 +47,42 @@ process.env.TJWATER_AGENT_INTERNAL_TOKEN = internalToken;
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: "1mb" }));
|
||||
|
||||
app.post("/internal/frontend-actions/request", async (req, res) => {
|
||||
if (req.header("x-agent-internal-token") !== internalToken) {
|
||||
res.status(403).json({ message: "forbidden" });
|
||||
return;
|
||||
}
|
||||
const runtimeSessionId = typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
|
||||
const context = runtimeSessionId ? getRuntimeSessionContext(runtimeSessionId) : null;
|
||||
if (!context) {
|
||||
res.status(404).json({ message: "session context not found" });
|
||||
return;
|
||||
}
|
||||
if (!context.capabilities?.includes("frontend-action@1")) {
|
||||
// Legacy clients still execute the existing UIEnvelope emitted from the tool call.
|
||||
res.json({
|
||||
version: "frontend-action-result@1",
|
||||
status: "succeeded",
|
||||
output: { legacy: true },
|
||||
detail: "legacy client executes the action from its UIEnvelope",
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await frontendActionCoordinator.request({
|
||||
sessionId: context.clientSessionId,
|
||||
toolCallId: typeof req.body?.call_id === "string" ? req.body.call_id : "",
|
||||
name: typeof req.body?.name === "string" ? req.body.name : "",
|
||||
params: req.body?.params,
|
||||
fallbackText: typeof req.body?.fallback_text === "string" ? req.body.fallback_text : undefined,
|
||||
});
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
const status = error instanceof FrontendActionError ? error.status : 500;
|
||||
res.status(status).json({ message: error instanceof Error ? error.message : String(error) });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/health", async (_req, res) => {
|
||||
try {
|
||||
const runtime = await opencodeRuntime.health();
|
||||
@@ -308,6 +346,7 @@ app.use(
|
||||
sessionTranscriptStore,
|
||||
learningOrchestrator,
|
||||
resultReferenceResolver,
|
||||
frontendActionCoordinator,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user