Built-in design generation now runs as an agentic MCP tool-loop (reusing the agent-rs BuiltInProvider), gated behind OPENPENCIL_DESIGN_AGENT_LOOP / the Settings experimental toggle; the orchestrator stays the default. - design-agent system prompt + in-process design toolset (parity-locked with the MCP surface) + flag-gated Intent::Design routing - spawn_agents execution as sequential sub-loops + live creation-mode badges (per-agent glow + 'N/M designing...' header) - new MCP tools: get_guidelines, ToolSearch, get_screenshot, get_editor_state, export_nodes, spawn_agents; style-guide local audit - #27 AI panel restyle: rounded tool cards + green check-rings, gray user bubbles, model-pill bottom toolbar, header, empty-state pills, the PARALLEL AGENTS (agent_team_size) 1x-6x chip dropdown - multi-chat tabs: ChatSessions model (Deref-to-active) + tab row UI (switch / close / + / Cmd+T) with each run bound to its tab Large checkpoint commit spanning the working tree (Rust shell crates).
165 lines
4.6 KiB
TypeScript
165 lines
4.6 KiB
TypeScript
import type {
|
|
QueryEngineHandle,
|
|
IteratorHandle,
|
|
ProviderHandle,
|
|
ToolRegistryHandle,
|
|
TeamHandle,
|
|
} from '@zseven-w/agent-native';
|
|
import type { ClientSideConnection } from '@agentclientprotocol/sdk';
|
|
import type { LayoutPhase } from './agent-tool-guard';
|
|
import {
|
|
abortEngine,
|
|
destroyIterator,
|
|
destroyQueryEngine,
|
|
destroyToolRegistry,
|
|
destroyProvider,
|
|
abortTeam,
|
|
destroyTeam,
|
|
} from '@zseven-w/agent-native';
|
|
|
|
export interface NativeAgentSession {
|
|
type: 'native';
|
|
engine?: QueryEngineHandle;
|
|
team?: TeamHandle;
|
|
iter?: IteratorHandle;
|
|
provider: ProviderHandle;
|
|
tools?: ToolRegistryHandle;
|
|
memberHandles?: Array<{ provider: ProviderHandle; tools: ToolRegistryHandle }>;
|
|
createdAt: number;
|
|
lastActivity: number;
|
|
/** toolCallId → memberId — routes async tool results to the correct member engine. */
|
|
toolOwners: Map<string, string>;
|
|
/** toolCallId → tool name — used for session-level tool guards and state updates. */
|
|
toolNames: Map<string, string>;
|
|
/** memberId → role — used for delegation-time skill resolution. */
|
|
memberRoles: Map<string, string>;
|
|
/** Session-local layout progress for builtin single-agent guardrails. */
|
|
layoutPhase: LayoutPhase;
|
|
layoutRootId: string | null;
|
|
}
|
|
|
|
export interface AcpAgentSession {
|
|
type: 'acp';
|
|
acpSessionId: string;
|
|
acpAgentId: string;
|
|
connection: ClientSideConnection;
|
|
createdAt: number;
|
|
lastActivity: number;
|
|
toolNames: Map<string, string>;
|
|
toolOwners: Map<string, string>;
|
|
layoutPhase: LayoutPhase;
|
|
layoutRootId: string | null;
|
|
}
|
|
|
|
export type AgentSession = NativeAgentSession | AcpAgentSession;
|
|
|
|
/** Create a native session with required defaults. */
|
|
export function createSession(
|
|
fields: Omit<
|
|
NativeAgentSession,
|
|
'type' | 'toolOwners' | 'toolNames' | 'memberRoles' | 'layoutPhase' | 'layoutRootId'
|
|
> &
|
|
Partial<
|
|
Pick<
|
|
NativeAgentSession,
|
|
'toolOwners' | 'toolNames' | 'memberRoles' | 'layoutPhase' | 'layoutRootId'
|
|
>
|
|
>,
|
|
): NativeAgentSession {
|
|
return {
|
|
type: 'native',
|
|
...fields,
|
|
toolOwners: fields.toolOwners ?? new Map(),
|
|
toolNames: fields.toolNames ?? new Map(),
|
|
memberRoles: fields.memberRoles ?? new Map(),
|
|
layoutPhase: fields.layoutPhase ?? 'idle',
|
|
layoutRootId: fields.layoutRootId ?? null,
|
|
};
|
|
}
|
|
|
|
/** Create an ACP session with required defaults. */
|
|
export function createAcpSession(fields: {
|
|
acpSessionId: string;
|
|
acpAgentId: string;
|
|
connection: ClientSideConnection;
|
|
}): AcpAgentSession {
|
|
return {
|
|
type: 'acp',
|
|
...fields,
|
|
createdAt: Date.now(),
|
|
lastActivity: Date.now(),
|
|
toolNames: new Map(),
|
|
toolOwners: new Map(),
|
|
layoutPhase: 'idle',
|
|
layoutRootId: null,
|
|
};
|
|
}
|
|
|
|
export const agentSessions = new Map<string, AgentSession>();
|
|
|
|
/** Mark a session as active so long-running external tool callbacks are not expired. */
|
|
export function touchSession(session: Pick<AgentSession, 'lastActivity'>, now = Date.now()): void {
|
|
session.lastActivity = now;
|
|
}
|
|
|
|
/** Idempotent cleanup — nullifies handles after destroying to prevent double-free. */
|
|
export function cleanup(session: AgentSession): void {
|
|
if (session.type === 'acp') return; // ACP connections managed by acp-connection-manager
|
|
if (session.iter) {
|
|
destroyIterator(session.iter);
|
|
session.iter = undefined;
|
|
}
|
|
if (session.team) {
|
|
abortTeam(session.team);
|
|
destroyTeam(session.team);
|
|
session.team = undefined;
|
|
}
|
|
if (session.engine) {
|
|
destroyQueryEngine(session.engine);
|
|
session.engine = undefined;
|
|
}
|
|
if (session.memberHandles) {
|
|
for (const mh of session.memberHandles) {
|
|
destroyToolRegistry(mh.tools);
|
|
destroyProvider(mh.provider);
|
|
}
|
|
session.memberHandles = undefined;
|
|
}
|
|
if (session.tools) {
|
|
destroyToolRegistry(session.tools);
|
|
session.tools = undefined;
|
|
}
|
|
if (session.provider) {
|
|
destroyProvider(session.provider);
|
|
(session as any).provider = undefined;
|
|
}
|
|
}
|
|
|
|
/** Abort a session — makes pending nextEvent resolve null. */
|
|
export function abortSession(session: AgentSession): void {
|
|
if (session.type === 'acp') {
|
|
try {
|
|
(session.connection as any).cancel?.({ sessionId: session.acpSessionId });
|
|
} catch {}
|
|
return;
|
|
}
|
|
if (session.team) abortTeam(session.team);
|
|
else if (session.engine) abortEngine(session.engine);
|
|
}
|
|
|
|
// Cleanup stale sessions every 60s (5-minute TTL from last activity)
|
|
setInterval(() => {
|
|
try {
|
|
const now = Date.now();
|
|
for (const [id, session] of agentSessions) {
|
|
if (now - session.lastActivity > 5 * 60_000) {
|
|
abortSession(session);
|
|
cleanup(session);
|
|
agentSessions.delete(id);
|
|
}
|
|
}
|
|
} catch {
|
|
/* ignore cleanup errors */
|
|
}
|
|
}, 60_000);
|