v2.2-paseo-providers: Paseo provider stack + v2.2.1 pane-scoped chat fixes
Ship Paseo-equivalent provider snapshot, AgentComposerBar, ACP dispatch rewrite with streaming/persist, permission prompts, and agent commands. Follow-up: pane-scoped chat resolution, CoderMessageList tool timeline, WS user-delta replace, and inference orphan tool_call stripping. Archive openspec v2-2; update CHANGELOG and CURRENT. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
import type { Sql } from '../db.js';
|
||||
import type { FastifyBaseLogger } from 'fastify';
|
||||
import type { Broker } from '@boocode/server/broker';
|
||||
import type { WsFrame } from '@boocode/server/ws-frames';
|
||||
import type { Config } from '../config.js';
|
||||
import { createWorktree, diffWorktree, cleanupWorktree } from './worktrees.js';
|
||||
import { dispatchViaAcp } from './acp-dispatch.js';
|
||||
import { dispatchViaPty } from './pty-dispatch.js';
|
||||
import { clearTaskCommands, setTaskCommands } from './agent-commands-cache.js';
|
||||
import { getManifestCommands } from './provider-commands.js';
|
||||
import { persistExternalAgentTurn } from './agent-turn-persist.js';
|
||||
|
||||
interface InferenceRunner {
|
||||
enqueue: (sessionId: string, chatId: string, assistantId: string, user: string) => void;
|
||||
@@ -24,7 +28,7 @@ const POLL_INTERVAL_MS = 5_000;
|
||||
const COMPLETION_POLL_MS = 2_000;
|
||||
|
||||
export function createDispatcher(deps: Deps): { start(): void; stop(): Promise<void> } {
|
||||
const { sql, inference, log, config } = deps;
|
||||
const { sql, inference, broker, log, config } = deps;
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
let running = false;
|
||||
let stopping = false;
|
||||
@@ -34,8 +38,17 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise<v
|
||||
if (running || stopping) return;
|
||||
|
||||
// Grab one pending task
|
||||
const rows = await sql<{ id: string; project_id: string; input: string; agent: string | null; model: string | null; session_id: string | null }[]>`
|
||||
SELECT id, project_id, input, agent, model, session_id
|
||||
const rows = await sql<{
|
||||
id: string;
|
||||
project_id: string;
|
||||
input: string;
|
||||
agent: string | null;
|
||||
model: string | null;
|
||||
mode_id: string | null;
|
||||
thinking_option_id: string | null;
|
||||
session_id: string | null;
|
||||
}[]>`
|
||||
SELECT id, project_id, input, agent, model, mode_id, thinking_option_id, session_id
|
||||
FROM tasks
|
||||
WHERE state = 'pending'
|
||||
ORDER BY created_at
|
||||
@@ -51,7 +64,16 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise<v
|
||||
});
|
||||
}
|
||||
|
||||
async function runTask(task: { id: string; project_id: string; input: string; agent: string | null; model: string | null; session_id: string | null }): Promise<void> {
|
||||
async function runTask(task: {
|
||||
id: string;
|
||||
project_id: string;
|
||||
input: string;
|
||||
agent: string | null;
|
||||
model: string | null;
|
||||
mode_id: string | null;
|
||||
thinking_option_id: string | null;
|
||||
session_id: string | null;
|
||||
}): Promise<void> {
|
||||
const taskId = task.id;
|
||||
|
||||
// Determine execution path: if agent is specified AND exists in available_agents → Path B
|
||||
@@ -179,7 +201,16 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise<v
|
||||
// ─── Path B: External Agent Dispatch ──────<E29480><E29480><EFBFBD>─────────────────────────────────
|
||||
|
||||
async function runExternalAgent(
|
||||
task: { id: string; project_id: string; input: string; agent: string | null; model: string | null; session_id: string | null },
|
||||
task: {
|
||||
id: string;
|
||||
project_id: string;
|
||||
input: string;
|
||||
agent: string | null;
|
||||
model: string | null;
|
||||
mode_id: string | null;
|
||||
thinking_option_id: string | null;
|
||||
session_id: string | null;
|
||||
},
|
||||
supportsAcp: boolean,
|
||||
installPath: string | null,
|
||||
): Promise<void> {
|
||||
@@ -265,6 +296,33 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise<v
|
||||
|
||||
// Step 2: Dispatch to agent
|
||||
let outputSummary: string;
|
||||
let assistantContent = '';
|
||||
let acpReasoning = '';
|
||||
|
||||
const [assistantMsg] = await sql<{ id: string }[]>`
|
||||
INSERT INTO messages (session_id, chat_id, role, content, status, created_at)
|
||||
VALUES (${sessionId}, ${chatId}, 'assistant', '', 'streaming', clock_timestamp())
|
||||
RETURNING id
|
||||
`;
|
||||
const assistantId = assistantMsg!.id;
|
||||
|
||||
broker.publishFrame(sessionId, {
|
||||
type: 'message_started',
|
||||
message_id: assistantId,
|
||||
chat_id: chatId,
|
||||
role: 'assistant',
|
||||
} as WsFrame);
|
||||
|
||||
const manifestCommands = getManifestCommands(agent);
|
||||
if (manifestCommands.length > 0) {
|
||||
setTaskCommands(taskId, manifestCommands);
|
||||
broker.publishFrame(sessionId, {
|
||||
type: 'agent_commands',
|
||||
task_id: taskId,
|
||||
session_id: sessionId,
|
||||
commands: manifestCommands,
|
||||
} as WsFrame);
|
||||
}
|
||||
|
||||
if (supportsAcp) {
|
||||
const result = await dispatchViaAcp({
|
||||
@@ -273,16 +331,20 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise<v
|
||||
worktreePath,
|
||||
installPath: installPath ?? undefined,
|
||||
model: task.model ?? undefined,
|
||||
modeId: task.mode_id ?? undefined,
|
||||
thinkingOptionId: task.thinking_option_id ?? undefined,
|
||||
taskId,
|
||||
sessionId,
|
||||
chatId,
|
||||
messageId: assistantId,
|
||||
broker,
|
||||
signal: ac.signal,
|
||||
log,
|
||||
});
|
||||
assistantContent = result.output.slice(0, 50_000);
|
||||
acpReasoning = result.reasoningText.slice(0, 200_000);
|
||||
outputSummary = result.output.slice(0, 500);
|
||||
|
||||
// Store agent output as an assistant message
|
||||
await sql`
|
||||
INSERT INTO messages (session_id, chat_id, role, content, status, created_at)
|
||||
VALUES (${sessionId}, ${chatId}, 'assistant', ${result.output.slice(0, 50_000)}, 'complete', clock_timestamp())
|
||||
`;
|
||||
await persistExternalAgentTurn(sql, assistantId, result.toolSnapshots, acpReasoning);
|
||||
} else {
|
||||
const result = await dispatchViaPty({
|
||||
agent,
|
||||
@@ -290,19 +352,36 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise<v
|
||||
worktreePath,
|
||||
installPath: installPath ?? undefined,
|
||||
model: task.model ?? undefined,
|
||||
modeId: task.mode_id ?? undefined,
|
||||
thinkingOptionId: task.thinking_option_id ?? undefined,
|
||||
signal: ac.signal,
|
||||
log,
|
||||
});
|
||||
assistantContent = (result.stdout || result.stderr || '(no output)').slice(0, 50_000);
|
||||
outputSummary = (result.stdout || result.stderr).slice(0, 500);
|
||||
|
||||
// Store agent output as an assistant message
|
||||
const content = result.stdout || result.stderr || '(no output)';
|
||||
await sql`
|
||||
INSERT INTO messages (session_id, chat_id, role, content, status, created_at)
|
||||
VALUES (${sessionId}, ${chatId}, 'assistant', ${content.slice(0, 50_000)}, 'complete', clock_timestamp())
|
||||
`;
|
||||
if (assistantContent) {
|
||||
broker.publishFrame(sessionId, {
|
||||
type: 'delta',
|
||||
message_id: assistantId,
|
||||
chat_id: chatId,
|
||||
content: assistantContent,
|
||||
} as WsFrame);
|
||||
}
|
||||
}
|
||||
|
||||
await sql`
|
||||
UPDATE messages
|
||||
SET content = ${assistantContent}, status = 'complete', finished_at = clock_timestamp()
|
||||
WHERE id = ${assistantId}
|
||||
`;
|
||||
|
||||
broker.publishFrame(sessionId, {
|
||||
type: 'message_complete',
|
||||
message_id: assistantId,
|
||||
chat_id: chatId,
|
||||
} as WsFrame);
|
||||
|
||||
if (stopping) {
|
||||
await sql`
|
||||
UPDATE tasks SET state = 'cancelled', ended_at = clock_timestamp() WHERE id = ${taskId}
|
||||
@@ -344,6 +423,7 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise<v
|
||||
WHERE id = ${taskId}
|
||||
`;
|
||||
log.info({ taskId, agent, costTokens: extCostTokens }, 'dispatcher: task completed (external)');
|
||||
clearTaskCommands(taskId);
|
||||
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
@@ -357,6 +437,7 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise<v
|
||||
|
||||
// Best-effort cleanup
|
||||
await cleanupWorktree(projectPath, taskId);
|
||||
clearTaskCommands(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user