Skip to content
2 changes: 2 additions & 0 deletions packages/cli/src/commands/drive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Re-export from KERN-generated drive command
export { driveCommand } from '../generated/commands/drive.js';
502 changes: 502 additions & 0 deletions packages/cli/src/generated/bridge/agentic-brain-client.ts

Large diffs are not rendered by default.

79 changes: 76 additions & 3 deletions packages/cli/src/generated/bridge/agon-serve.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @generated by kern v4.0.0 — DO NOT EDIT. Source: src/kern/bridge/agon-serve.kern

import type { BrainClient } from '@kernlang/agon-core';
import type { BrainClient, CapabilitySpec } from '@kernlang/agon-core';

import { getSessionHost, eventLogAppend, eventLogFlush, MAX_DISPATCH_IMAGES, MAX_DISPATCH_IMAGE_BYTES } from '@kernlang/agon-core';

Expand Down Expand Up @@ -124,6 +124,13 @@ export class AgonServe {
}
if (method === 'POST' && path === '/send') { await this.handleSend(req, res, origin); return; }
if (method === 'POST' && path === '/cancel') { await this.handleCancel(req, res, origin); return; }
// Agent tool-loop control plane — these call the brain DIRECTLY (never chained
// on turnTail), so a capability-request the live turn is awaiting can be answered
// while handleSend still holds the per-session write lock (no deadlock).
if (method === 'POST' && path === '/register-capability') { await this.handleRegisterCapability(req, res, origin); return; }
if (method === 'POST' && path === '/unregister-capability') { await this.handleUnregisterCapability(req, res, origin); return; }
if (method === 'POST' && path === '/capability-result') { await this.handleCapabilityResult(req, res, origin); return; }
if (method === 'POST' && path === '/approval') { await this.handleApproval(req, res, origin); return; }
this.sendJson(res, 404, { error: 'not found' }, origin);
} catch (err) {
// Never double-write: if the response already started (e.g. an SSE replay
Expand Down Expand Up @@ -184,7 +191,15 @@ export class AgonServe {
eventLogAppend(this.sessionId, { kind: 'provenance', clientId, origin, turnId }, { kind: 'bridge' });
const gen = this.brain.runTurn({ sessionId: this.sessionId, turnId, clientId, input, images, engineId });
let r = await gen.next();
while (!r.done) { eventLogAppend(this.sessionId, r.value, { kind: 'bridge' }); r = await gen.next(); }
while (!r.done) {
eventLogAppend(this.sessionId, r.value, { kind: 'bridge' });
// A capability/approval request must reach the client NOW — the turn is about
// to block awaiting the reply, so flush past the ~50ms coalesce window instead
// of letting the round-trip stall on the timer.
const k = (r.value as { kind?: string }).kind;
if (k === 'capability-request' || k === 'approval-request') eventLogFlush(this.sessionId);
r = await gen.next();
}
eventLogFlush(this.sessionId);
this.sendJson(res, 200, { turnId, result: r.value }, origin);
} finally {
Expand All @@ -201,6 +216,64 @@ export class AgonServe {
this.sendJson(res, 200, ack, origin);
}

private async handleRegisterCapability(req: IncomingMessage, res: ServerResponse, origin: string): Promise<void> {
const body = await this.readJson(req);
const clientId = typeof body.clientId === 'string' ? body.clientId : 'http';
const spec = body.spec as { name?: unknown; description?: unknown; inputSchema?: unknown; isReadOnly?: unknown; isDestructive?: unknown } | undefined;
if (!spec || typeof spec !== 'object' || typeof spec.name !== 'string' || typeof spec.description !== 'string') {
this.sendJson(res, 400, { error: 'invalid capability spec (need string name + description)' }, origin);
return;
}
const clean: CapabilitySpec = {
name: spec.name,
description: spec.description,
inputSchema: (spec.inputSchema && typeof spec.inputSchema === 'object') ? spec.inputSchema as Record<string, unknown> : {},
isReadOnly: spec.isReadOnly === true,
isDestructive: spec.isDestructive === true,
};
// Bound a single tool spec: its name/description/inputSchema are serialized into
// EVERY subsequent dispatch's system prompt, so an unbounded spec would inflate the
// token cost of the whole turn. 4 KiB is generous for a real tool, tiny for an attack.
if (JSON.stringify(clean).length > 4096) {
this.sendJson(res, 400, { error: 'capability spec too large (max 4096 bytes)' }, origin);
return;
}
const ack = await this.brain.registerCapability({ sessionId: this.sessionId, clientId, spec: clean });
this.sendJson(res, 200, ack, origin);
}

private async handleUnregisterCapability(req: IncomingMessage, res: ServerResponse, origin: string): Promise<void> {
const body = await this.readJson(req);
const name = typeof body.name === 'string' ? body.name : '';
if (!name) { this.sendJson(res, 400, { error: 'missing name' }, origin); return; }
const clientId = typeof body.clientId === 'string' ? body.clientId : 'http';
const ack = await this.brain.unregisterCapability({ sessionId: this.sessionId, clientId, name });
this.sendJson(res, 200, ack, origin);
}

private async handleCapabilityResult(req: IncomingMessage, res: ServerResponse, origin: string): Promise<void> {
const body = await this.readJson(req);
const requestId = typeof body.requestId === 'string' ? body.requestId : '';
if (!requestId) { this.sendJson(res, 400, { error: 'missing requestId' }, origin); return; }
const clientId = typeof body.clientId === 'string' ? body.clientId : 'http';
const ok = body.ok === true;
const output = typeof body.output === 'string' ? body.output : undefined;
const error = typeof body.error === 'string' ? body.error : undefined;
const ack = await this.brain.provideCapabilityResult({ sessionId: this.sessionId, requestId, clientId, ok, output, error });
this.sendJson(res, 200, ack, origin);
}

private async handleApproval(req: IncomingMessage, res: ServerResponse, origin: string): Promise<void> {
const body = await this.readJson(req);
const requestId = typeof body.requestId === 'string' ? body.requestId : '';
const decision = typeof body.decision === 'string' ? body.decision : '';
const allowed = ['approve', 'approve-session', 'deny', 'deny-session', 'abort'];
if (!requestId || !allowed.includes(decision)) { this.sendJson(res, 400, { error: 'missing requestId or invalid decision' }, origin); return; }
const clientId = typeof body.clientId === 'string' ? body.clientId : 'http';
const ack = await this.brain.provideApproval({ sessionId: this.sessionId, requestId, clientId, decision: decision as 'approve' | 'approve-session' | 'deny' | 'deny-session' | 'abort' });
this.sendJson(res, 200, ack, origin);
}

private async readJson(req: IncomingMessage): Promise<Record<string, unknown>> {
const chunks: Buffer[] = [];
let size = 0;
Expand Down Expand Up @@ -229,7 +302,7 @@ export class AgonServe {
/**
* Factory: build the loopback bridge for a session given an opened BrainClient and the session id.
*/
// @kern-source: agon-serve:266
// @kern-source: agon-serve:346
export function createAgonServe(opts: { brain: BrainClient, sessionId: string, allowedOrigins?: string[], engines?: string[], engineId?: string }): AgonServe {
return new AgonServe(opts);
}
Loading