feat(KB-159): complete Step 4 — update frontend for streaming display
This commit is contained in:
@@ -634,17 +634,32 @@ async function continueAgentConversation(session: Session, message: string): Pro
|
||||
|
||||
try {
|
||||
// Clear thinking output for this turn
|
||||
const previousThinking = session.thinkingOutput;
|
||||
session.thinkingOutput = "";
|
||||
|
||||
// Send message to agent - it will stream thinking via onThinking callback
|
||||
const response = await session.agent.session.send(message);
|
||||
// Send message to agent using .prompt() - it will stream thinking via onThinking callback
|
||||
await session.agent.session.prompt(message);
|
||||
|
||||
// Combine any thinking output with the response text
|
||||
const fullResponse = session.thinkingOutput + (response?.text || "");
|
||||
// Get the response text from the agent's state
|
||||
const lastMessage = session.agent.session.state.messages
|
||||
.filter(m => m.role === "assistant")
|
||||
.pop();
|
||||
|
||||
let responseText = session.thinkingOutput;
|
||||
if (lastMessage?.content) {
|
||||
// Handle both string and array content types
|
||||
if (typeof lastMessage.content === "string") {
|
||||
responseText = lastMessage.content;
|
||||
} else if (Array.isArray(lastMessage.content)) {
|
||||
// Extract text from content blocks
|
||||
responseText = lastMessage.content
|
||||
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
||||
.map(c => c.text)
|
||||
.join("");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the JSON response
|
||||
const parsed = parseAgentResponse(fullResponse);
|
||||
const parsed = parseAgentResponse(responseText);
|
||||
|
||||
if (parsed.type === "question") {
|
||||
session.currentQuestion = parsed.data;
|
||||
|
||||
@@ -2169,6 +2169,44 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/planning/start-streaming
|
||||
* Start a new planning session with AI agent streaming.
|
||||
* Body: { initialPlan: string }
|
||||
* Returns: { sessionId: string }
|
||||
*
|
||||
* After receiving sessionId, connect to GET /api/planning/:sessionId/stream
|
||||
* for real-time thinking output and questions.
|
||||
*/
|
||||
router.post("/planning/start-streaming", async (req, res) => {
|
||||
try {
|
||||
const { initialPlan } = req.body;
|
||||
|
||||
if (!initialPlan || typeof initialPlan !== "string") {
|
||||
res.status(400).json({ error: "initialPlan is required and must be a string" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (initialPlan.length > 500) {
|
||||
res.status(400).json({ error: "initialPlan must be 500 characters or less" });
|
||||
return;
|
||||
}
|
||||
|
||||
const ip = req.ip || req.socket.remoteAddress || "unknown";
|
||||
const rootDir = store.getRootDir();
|
||||
|
||||
const { createSessionWithAgent, RateLimitError } = await import("./planning.js");
|
||||
const sessionId = await createSessionWithAgent(ip, initialPlan, rootDir);
|
||||
res.status(201).json({ sessionId });
|
||||
} catch (err: any) {
|
||||
if (err.name === "RateLimitError") {
|
||||
res.status(429).json({ error: err.message });
|
||||
} else {
|
||||
res.status(500).json({ error: err.message || "Failed to start planning session" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/planning/respond
|
||||
* Submit a response to the current planning question.
|
||||
@@ -2322,7 +2360,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
// Subscribe to session events
|
||||
const unsubscribe = planningStreamManager.subscribe(sessionId, (event) => {
|
||||
try {
|
||||
res.write(`event: ${event.type}\ndata: ${JSON.stringify(event.data ?? {})}\n\n`);
|
||||
const data = (event as { data?: unknown }).data;
|
||||
res.write(`event: ${event.type}\ndata: ${JSON.stringify(data ?? {})}\n\n`);
|
||||
|
||||
// End stream on complete or error
|
||||
if (event.type === "complete" || event.type === "error") {
|
||||
|
||||
Reference in New Issue
Block a user