fix: orchestrator button + Pi SDK session error
- Move leftSidebarMode from URL state to local useState in unified-shell,
avoiding force-dynamic router round-trip that made the button appear broken - Replace fileURLToPath(new URL(..., import.meta.url)) with process.cwd()
in bb-pi-bootstrap.ts — import.meta.url is a webpack:// URL in Next.js,
causing cross-realm TypeError when passed to Node.js fileURLToPath()
This commit is contained in:
parent
643fa299dd
commit
d335e5bf71
98 changed files with 17851 additions and 944 deletions
38
src/app/api/runtime/agents/history/route.ts
Normal file
38
src/app/api/runtime/agents/history/route.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { workerSessionManager, type WorkerSession } from '../../../../../lib/worker-session-manager';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const projectRoot = searchParams.get('projectRoot');
|
||||
|
||||
if (!projectRoot) {
|
||||
return NextResponse.json({ ok: false, error: 'projectRoot required' });
|
||||
}
|
||||
|
||||
// Get completed/failed workers as history
|
||||
const workers = workerSessionManager.listWorkers(projectRoot);
|
||||
const history = workers
|
||||
.filter((w: WorkerSession) => w.status === 'completed' || w.status === 'failed')
|
||||
.sort((a: WorkerSession, b: WorkerSession) =>
|
||||
new Date(b.completedAt || 0).getTime() - new Date(a.completedAt || 0).getTime()
|
||||
)
|
||||
.slice(0, 50)
|
||||
.map((w: WorkerSession) => ({
|
||||
id: w.agentInstanceId || w.id,
|
||||
agentTypeId: w.agentTypeId || 'unknown',
|
||||
displayName: w.displayName || `Worker ${w.id}`,
|
||||
status: w.status,
|
||||
currentBeadId: w.taskId,
|
||||
startedAt: w.createdAt,
|
||||
completedAt: w.completedAt,
|
||||
result: w.result,
|
||||
error: w.error,
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
instances: history,
|
||||
});
|
||||
}
|
||||
42
src/app/api/runtime/agents/route.ts
Normal file
42
src/app/api/runtime/agents/route.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { workerSessionManager, type WorkerSession } from '../../../../lib/worker-session-manager';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const projectRoot = searchParams.get('projectRoot');
|
||||
|
||||
if (!projectRoot) {
|
||||
return NextResponse.json({ ok: false, error: 'projectRoot required' });
|
||||
}
|
||||
|
||||
const workers = workerSessionManager.listWorkers(projectRoot);
|
||||
|
||||
const instances = workers.map((w: WorkerSession) => ({
|
||||
id: w.agentInstanceId || w.id,
|
||||
agentTypeId: w.agentTypeId || 'unknown',
|
||||
displayName: w.displayName || `Worker ${w.id}`,
|
||||
status: w.status,
|
||||
currentBeadId: w.taskId,
|
||||
startedAt: w.createdAt,
|
||||
completedAt: w.completedAt,
|
||||
result: w.result,
|
||||
error: w.error,
|
||||
}));
|
||||
|
||||
const byType: Record<string, number> = {};
|
||||
for (const w of workers) {
|
||||
const typeId = w.agentTypeId || 'unknown';
|
||||
byType[typeId] = (byType[typeId] || 0) + 1;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
status: {
|
||||
totalActive: workers.filter((w: WorkerSession) => w.status === 'working' || w.status === 'spawning').length,
|
||||
byType,
|
||||
instances,
|
||||
},
|
||||
});
|
||||
}
|
||||
25
src/app/api/runtime/bootstrap/route.ts
Normal file
25
src/app/api/runtime/bootstrap/route.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { bootstrapManagedPi } from '../../../../lib/bb-pi-bootstrap';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function POST(): Promise<Response> {
|
||||
try {
|
||||
const result = await bootstrapManagedPi();
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
managedRoot: result.managedRoot,
|
||||
sdkPath: result.sdkPath,
|
||||
agentDir: result.agentDir,
|
||||
alreadyInstalled: result.alreadyInstalled,
|
||||
created: result.created,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Bootstrap API] Error:', error);
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: error instanceof Error ? error.message : 'Bootstrap failed' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
16
src/app/api/runtime/events/route.ts
Normal file
16
src/app/api/runtime/events/route.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { bbDaemon } from '../../../../lib/bb-daemon';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const projectRoot = searchParams.get('projectRoot');
|
||||
|
||||
if (!projectRoot) {
|
||||
return NextResponse.json({ ok: false, error: 'projectRoot is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
await bbDaemon.ensureRunning();
|
||||
return NextResponse.json({ ok: true, lifecycle: bbDaemon.getLifecycle(), data: bbDaemon.listEvents(projectRoot) });
|
||||
}
|
||||
55
src/app/api/runtime/launch/route.ts
Normal file
55
src/app/api/runtime/launch/route.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { bbDaemon } from '../../../../lib/bb-daemon';
|
||||
import type { LaunchSurface } from '../../../../lib/embedded-runtime';
|
||||
import type { readIssuesFromDisk as ReadIssuesFromDisk } from '../../../../lib/read-issues';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
interface LaunchDeps {
|
||||
readIssues?: typeof ReadIssuesFromDisk;
|
||||
}
|
||||
|
||||
function isLaunchSurface(value: string): value is LaunchSurface {
|
||||
return ['social', 'graph', 'swarm', 'sessions', 'activity', 'task'].includes(value);
|
||||
}
|
||||
|
||||
export async function handleRuntimeLaunchPost(request: Request, deps: LaunchDeps = {}): Promise<Response> {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const projectRoot = typeof body?.projectRoot === 'string' ? body.projectRoot.trim() : '';
|
||||
const taskId = typeof body?.taskId === 'string' ? body.taskId.trim() : '';
|
||||
const origin = typeof body?.origin === 'string' && isLaunchSurface(body.origin) ? body.origin : null;
|
||||
const swarmId = typeof body?.swarmId === 'string' ? body.swarmId : null;
|
||||
|
||||
if (!projectRoot || !taskId || !origin) {
|
||||
return NextResponse.json({ ok: false, error: 'projectRoot, taskId, and origin are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const read = deps.readIssues ?? (await import('../../../../lib/read-issues')).readIssuesFromDisk;
|
||||
const issues = await read({ projectRoot, preferBd: true });
|
||||
const issue = issues.find((entry) => entry.id === taskId);
|
||||
|
||||
if (!issue) {
|
||||
return NextResponse.json({ ok: false, error: 'task not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const lifecycle = await bbDaemon.ensureRunning();
|
||||
const result = await bbDaemon.launchFromIssue({
|
||||
projectRoot,
|
||||
issue,
|
||||
origin,
|
||||
swarmId,
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true, lifecycle, data: result });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: error instanceof Error ? error.message : 'Invalid request' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request): Promise<Response> {
|
||||
return handleRuntimeLaunchPost(request);
|
||||
}
|
||||
24
src/app/api/runtime/orchestrator/route.ts
Normal file
24
src/app/api/runtime/orchestrator/route.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { bbDaemon } from '../../../../lib/bb-daemon';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function POST(request: Request): Promise<Response> {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const projectRoot = typeof body?.projectRoot === 'string' ? body.projectRoot.trim() : '';
|
||||
|
||||
if (!projectRoot) {
|
||||
return NextResponse.json({ ok: false, error: 'projectRoot is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const lifecycle = await bbDaemon.ensureRunning();
|
||||
const orchestrator = await bbDaemon.ensureOrchestrator(projectRoot);
|
||||
return NextResponse.json({ ok: true, lifecycle, data: orchestrator });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: error instanceof Error ? error.message : 'Invalid request' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
29
src/app/api/runtime/prompt/route.ts
Normal file
29
src/app/api/runtime/prompt/route.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { bbDaemon } from '../../../../lib/bb-daemon';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function POST(request: Request): Promise<Response> {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const projectRoot = typeof body?.projectRoot === 'string' ? body.projectRoot.trim() : '';
|
||||
const text = typeof body?.text === 'string' ? body.text.trim() : '';
|
||||
|
||||
if (!projectRoot || !text) {
|
||||
return NextResponse.json({ ok: false, error: 'projectRoot and text are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
await bbDaemon.ensureRunning();
|
||||
|
||||
if (typeof (bbDaemon as any).prompt === 'function') {
|
||||
void (bbDaemon as any).prompt(projectRoot, text);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: error instanceof Error ? error.message : 'Invalid request' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
49
src/app/api/runtime/spawn/route.ts
Normal file
49
src/app/api/runtime/spawn/route.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// src/app/api/runtime/spawn/route.ts
|
||||
import { NextResponse } from 'next/server';
|
||||
import { workerSessionManager } from '../../../../lib/worker-session-manager';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { projectRoot, beadId, agentTypeId } = body;
|
||||
|
||||
// Validate required fields
|
||||
if (!projectRoot) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: 'projectRoot is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (!beadId) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: 'beadId is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (!agentTypeId) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: 'agentTypeId is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Spawn worker via session manager
|
||||
const worker = await workerSessionManager.spawnWorker({
|
||||
projectRoot,
|
||||
taskId: beadId,
|
||||
taskContext: `Work on ${beadId}`,
|
||||
agentType: agentTypeId,
|
||||
beadId,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
workerId: worker.id,
|
||||
displayName: worker.displayName,
|
||||
agentTypeId: worker.agentTypeId,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return NextResponse.json({ ok: false, error: message });
|
||||
}
|
||||
}
|
||||
8
src/app/api/runtime/status/route.ts
Normal file
8
src/app/api/runtime/status/route.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { bbDaemon } from '../../../../lib/bb-daemon';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
return NextResponse.json(bbDaemon.getStatus());
|
||||
}
|
||||
80
src/app/api/runtime/stream/route.ts
Normal file
80
src/app/api/runtime/stream/route.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { bbDaemon } from '../../../../lib/bb-daemon';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const HEARTBEAT_MS = 15_000;
|
||||
const POLL_MS = 250;
|
||||
|
||||
function toRuntimeSseFrame(event: unknown): string {
|
||||
return `event: runtime\ndata: ${JSON.stringify(event)}\n\n`;
|
||||
}
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const projectRoot = searchParams.get('projectRoot');
|
||||
|
||||
if (!projectRoot) {
|
||||
return Response.json({ ok: false, error: 'projectRoot is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
await bbDaemon.start();
|
||||
|
||||
let cleanup = () => {};
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
let closed = false;
|
||||
const write = (payload: string) => {
|
||||
if (!closed) controller.enqueue(encoder.encode(payload));
|
||||
};
|
||||
|
||||
write(': connected\n\n');
|
||||
|
||||
const seenIds = new Set<string>();
|
||||
const seed = bbDaemon.listEvents(projectRoot);
|
||||
for (const event of seed) {
|
||||
seenIds.add(event.id);
|
||||
write(toRuntimeSseFrame(event));
|
||||
}
|
||||
|
||||
const poll = setInterval(() => {
|
||||
const current = bbDaemon.listEvents(projectRoot);
|
||||
const unseen = current.filter((event) => !seenIds.has(event.id));
|
||||
for (const event of unseen.reverse()) {
|
||||
seenIds.add(event.id);
|
||||
write(toRuntimeSseFrame(event));
|
||||
}
|
||||
}, POLL_MS);
|
||||
|
||||
const heartbeat = setInterval(() => {
|
||||
write(': heartbeat\n\n');
|
||||
}, HEARTBEAT_MS);
|
||||
|
||||
const close = () => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
clearInterval(heartbeat);
|
||||
clearInterval(poll);
|
||||
request.signal.removeEventListener('abort', close);
|
||||
try {
|
||||
controller.close();
|
||||
} catch {}
|
||||
};
|
||||
|
||||
cleanup = close;
|
||||
request.signal.addEventListener('abort', close);
|
||||
},
|
||||
cancel() {
|
||||
cleanup();
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream; charset=utf-8',
|
||||
'Cache-Control': 'no-cache, no-transform',
|
||||
Connection: 'keep-alive',
|
||||
},
|
||||
});
|
||||
}
|
||||
37
src/app/api/runtime/worker-status/route.ts
Normal file
37
src/app/api/runtime/worker-status/route.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// src/app/api/runtime/worker-status/route.ts
|
||||
import { NextResponse } from 'next/server';
|
||||
import { workerSessionManager } from '../../../../lib/worker-session-manager';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const beadId = searchParams.get('beadId');
|
||||
|
||||
if (!beadId) {
|
||||
return NextResponse.json({ ok: false, error: 'beadId required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Find worker for this bead
|
||||
const workers = workerSessionManager.getAllWorkers();
|
||||
const worker = workers.find(w => w.beadId === beadId);
|
||||
|
||||
if (!worker) {
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
workerStatus: 'idle',
|
||||
agentTypeId: null,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
workerStatus: worker.status,
|
||||
workerDisplayName: worker.displayName,
|
||||
workerError: worker.error,
|
||||
agentTypeId: worker.agentTypeId,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return NextResponse.json({ ok: false, error: message });
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue